From 9071b0073cccbd083a11ac9ae05fdb2c7f7e2f8d Mon Sep 17 00:00:00 2001 From: Adrien Date: Sun, 31 Mar 2024 23:26:10 +0200 Subject: [PATCH] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20the=20components?= =?UTF-8?q?=20to=20take=20the=20dioxus=200.5=20rework=20into=20account?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 13 +- README.md | 1 + src/base.rs | 61 ++-- src/components/avatar_selector.rs | 8 +- src/components/button.rs | 79 +++--- src/components/chats_window/conversation.rs | 19 +- src/components/chats_window/edit_section.rs | 8 +- src/components/chats_window/interface.rs | 3 +- src/components/chats_window/mod.rs | 81 +++--- src/components/chats_window/navbar.rs | 8 +- src/components/contacts_window/contacts.rs | 15 +- .../contacts_window/contacts_section.rs | 110 ++++---- src/components/contacts_window/mod.rs | 8 +- src/components/contacts_window/user_infos.rs | 13 +- src/components/header.rs | 9 +- src/components/icons.rs | 43 +-- src/components/loading.rs | 9 +- src/components/login.rs | 264 +++++++++--------- src/components/main_window.rs | 14 +- src/components/modal.rs | 46 ++- src/components/spinner.rs | 16 +- src/components/text_input.rs | 89 +++--- src/components/wallpaper.rs | 9 +- src/main.rs | 133 ++++----- src/matrix_interface/client.rs | 2 +- src/matrix_interface/requester.rs | 10 +- 26 files changed, 537 insertions(+), 534 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d3e0b06..419cd09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,11 +6,10 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus = "0.4.3" -dioxus-desktop = "0.4.3" -dioxus-free-icons = { version = "0.7.0", features = ["material-design-icons-navigation", "ionicons"] } -dioxus-std = { version = "0.4.1", features = ["utils"] } -fermi = { version = "0.4.3" } +dioxus = "0.5.0" +dioxus-desktop = "0.5.0" +dioxus-free-icons = { version = "0.8", features = ["material-design-icons-navigation", "ionicons"] } +dioxus-std = { git = "https://github.com/DioxusLabs/dioxus-std.git", branch = "master", features = ["utils"] } # matrix-sdk = { version = "0.6.2", 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] template = "--" + +[features] +default = [] +desktop = ["dioxus/desktop"] diff --git a/README.md b/README.md index 954dfd9..c9ff9b0 100644 --- a/README.md +++ b/README.md @@ -38,5 +38,6 @@ learn Rust) graphical library should be selected. The [Dioxus](https://dioxuslab # TODO +- [ ] Test dioxus-radio. - [ ] Design system ? - [ ] Implement MSN messenger features using Matrix.org sdk... diff --git a/src/base.rs b/src/base.rs index 03991ec..6ba67ab 100644 --- a/src/base.rs +++ b/src/base.rs @@ -3,10 +3,13 @@ // (used by [UnboundedReceiver]) by adding 'futures_util' as a dependency to your project // and adding the use futures_util::stream::StreamExt; use futures_util::stream::StreamExt; +use std::cell::RefCell; 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 fermi::*; use matrix_sdk::{ room::{Room as MatrixRoom, RoomMember}, ruma::{OwnedRoomId, OwnedUserId}, @@ -15,9 +18,6 @@ use tokio::select; use tracing::{debug, error, warn}; 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)] // pub struct UserInfo { @@ -140,11 +140,9 @@ impl AppSettings { } } -pub static APP_SETTINGS: AtomRef = AtomRef(|_| AppSettings::new()); - -async fn on_room(room_id: OwnedRoomId, room: Room, rooms_ref: &UseAtomRef) { +async fn on_room(room_id: OwnedRoomId, room: Room, by_id_rooms: &GlobalSignal) { // TODO: Update rooms - rooms_ref + by_id_rooms .write() .insert(room_id, RefCell::::new(room)); } @@ -152,31 +150,36 @@ async fn on_room(room_id: OwnedRoomId, room: Room, rooms_ref: &UseAtomRef, + by_id_rooms: &GlobalSignal, ) { debug!( "You're invited to join the \"{}\" room", room.name().unwrap() ); // TODO: Update rooms - rooms_ref + by_id_rooms .write() .insert(room_id, RefCell::::new(room)); } -pub async fn on_room_topic(room_id: OwnedRoomId, topic: String, rooms_ref: &UseAtomRef) { - if let Some(room_ref) = rooms_ref.read().get(&room_id) { - let mut room = room_ref.borrow_mut(); +async fn on_room_topic(room_id: OwnedRoomId, topic: String, by_id_rooms: &GlobalSignal) { + if let Some(room) = by_id_rooms.read().get(&room_id) { + let mut room = room.borrow_mut(); room.topic = Some(RefCell::new(topic)); } else { warn!("No room found with the \"{}\" id", room_id); } } +pub async fn sync_messages(by_id_rooms: &GlobalSignal, room_id: OwnedRoomId) { + error!("== sync_messages =="); + +} + pub async fn sync_rooms( mut rx: UnboundedReceiver, receivers: Receivers, - rooms_ref: UseAtomRef, + by_id_rooms: &GlobalSignal, ) { if let Some(_is_logged) = rx.next().await { let mut rooms_receiver = receivers.room_receiver.borrow_mut(); @@ -187,9 +190,9 @@ pub async fn sync_rooms( res = rooms_receiver.recv() => { if let Ok(room_event) = res { match room_event { - RoomEvent::MemberEvent(room_id, room) => on_room(room_id, room, &rooms_ref).await, - RoomEvent::InviteEvent(room_id, room) => on_joining_invitation(room_id, room, &rooms_ref).await, - RoomEvent::TopicEvent(room_id, topic) => on_room_topic(room_id, topic, &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, &by_id_rooms).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( mut rx: UnboundedReceiver, - app_settings_ref: UseAtomRef, - session_ref: UseAtomRef, + app_settings: &GlobalSignal, + session: &GlobalSignal, ) { while let Some(is_logged) = rx.next().await { if !is_logged { - let homeserver_url = session_ref.read().homeserver_url.clone(); - let username = session_ref.read().username.clone(); - let password = session_ref.read().password.clone(); + let homeserver_url = session.read().homeserver_url.clone(); + let username = session.read().username.clone(); + let password = session.read().password.clone(); if homeserver_url.is_some() && username.is_some() && password.is_some() { let client = Client::spawn(homeserver_url.unwrap()).await; @@ -222,14 +225,14 @@ pub async fn login( { Ok(_) => { debug!("successfully logged"); - session_ref.write().is_logged = true; + session.write().is_logged = true; } Err(err) => { error!("Error during login: {err}"); // invalid_login.modify(|_| true); } } - app_settings_ref.write().set_requester(RefCell::new(client)); + app_settings.write().set_requester(RefCell::new(client)); } else { 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, } impl Session { - fn new() -> Self { + pub fn new() -> Self { Self { homeserver_url: None, username: None, @@ -267,6 +270,8 @@ impl Session { } } -pub static ROOMS: AtomRef = AtomRef(|_| ByIdRooms::new()); -pub static SESSION: AtomRef = AtomRef(|_| Session::new()); -pub static CHATS_WIN_INTERFACE: AtomRef = AtomRef(|_| ChatsWinInterface::new()); +pub static APP_SETTINGS: GlobalSignal = Signal::global(AppSettings::new); +pub static ROOMS: GlobalSignal = Signal::global(ByIdRooms::new); +pub static SESSION: GlobalSignal = Signal::global(Session::new); +pub static CHATS_WIN_INTERFACE: GlobalSignal = + Signal::global(ChatsWinInterface::new); diff --git a/src/components/avatar_selector.rs b/src/components/avatar_selector.rs index 3e47f10..b0da778 100644 --- a/src/components/avatar_selector.rs +++ b/src/components/avatar_selector.rs @@ -2,9 +2,9 @@ use dioxus::prelude::*; turf::style_sheet!("src/components/avatar_selector.scss"); -pub fn AvatarSelector(cx: Scope) -> Element { - cx.render(rsx! { - style { STYLE_SHEET }, +pub fn AvatarSelector() -> Element { + rsx! { + style { {STYLE_SHEET} }, div { class: ClassName::AVATAR_SELECTOR, @@ -50,5 +50,5 @@ pub fn AvatarSelector(cx: Scope) -> Element { src: "./images/default-avatar.png", }, }, - }) + } } diff --git a/src/components/button.rs b/src/components/button.rs index 847735f..3facb47 100644 --- a/src/components/button.rs +++ b/src/components/button.rs @@ -3,8 +3,19 @@ use dioxus_free_icons::{Icon, IconShape}; turf::style_sheet!("src/components/button.scss"); +#[derive(PartialEq, Clone, Props)] +struct _ButtonProps { + children: Element, + #[props(default = false)] + focus: bool, + id: Option, + onclick: Option>, + style: String, +} + macro_rules! svg_text_icon { ($name:ident,$text:literal) => { + #[derive(Copy, Clone, PartialEq)] struct $name; impl IconShape for $name { fn view_box(&self) -> String { @@ -15,7 +26,7 @@ macro_rules! svg_text_icon { String::from("http://www.w3.org/2000/svg") } - fn child_elements(&self) -> LazyNodes { + fn child_elements(&self) -> Element { rsx! { text { x: "50%", @@ -30,78 +41,64 @@ macro_rules! svg_text_icon { macro_rules! svg_text_button { ($name:ident,$style:ident,$icon:ident) => { - pub fn $name<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> { - cx.render(rsx! { - style { STYLE_SHEET }, + pub fn $name(props: ButtonProps) -> Element { + rsx! { + style { {STYLE_SHEET} }, Button { - id: cx.props.id.unwrap_or(""), + id: props.id, - style: ClassName::$style, + style: {ClassName::$style}, - onclick: |event| { - if let Some(cb) = &cx.props.onclick { + onclick: move |event| { + if let Some(cb) = &props.onclick { cb.call(event); } }, - focus: cx.props.focus, + focus: props.focus, Icon { icon: $icon, } } - }) + } } }; } -#[derive(Props)] -struct _ButtonProps<'a> { - children: Element<'a>, +#[derive(PartialEq, Clone, Props)] +pub struct ButtonProps { #[props(default = false)] focus: bool, - #[props(optional)] - id: Option<&'a str>, - #[props(optional)] - onclick: Option>, - style: &'static str, + id: Option, + onclick: Option>, + style: Option, + children: Element, } -fn Button<'a>(cx: Scope<'a, _ButtonProps<'a>>) -> Element<'a> { - let focus = cx.props.focus; - - cx.render(rsx! { - style { STYLE_SHEET }, +fn Button(props: ButtonProps) -> Element { + rsx! { + style { {STYLE_SHEET} }, button { - id: cx.props.id, + id: props.id, - class: cx.props.style, + class: props.style, - onmounted: move |cx| async move { - let _ = cx.inner().set_focus(focus).await; + onmounted: move |evt| async move { + _ = evt.set_focus(props.focus).await; }, onclick: move |evt| { - if let Some(cb) = &cx.props.onclick { + if let Some(cb) = &props.onclick { cb.call(evt); } }, - &cx.props.children - } - }) -} - -#[derive(Props)] -pub struct ButtonProps<'a> { - #[props(default = false)] - focus: bool, - #[props(optional)] - id: Option<&'a str>, - #[props(optional)] - onclick: Option>, + {props.children}, + }, + } } svg_text_icon!(RegisterText, "REGISTER"); diff --git a/src/components/chats_window/conversation.rs b/src/components/chats_window/conversation.rs index 00f097d..689fa2b 100644 --- a/src/components/chats_window/conversation.rs +++ b/src/components/chats_window/conversation.rs @@ -1,17 +1,24 @@ use dioxus::prelude::*; -use tracing::debug; +use matrix_sdk::ruma::OwnedRoomId; +use tracing::error; use super::edit_section::EditSection; +use crate::base::{sync_messages, ROOMS}; use crate::components::avatar_selector::AvatarSelector; use crate::components::icons::DownArrowIcon; turf::style_sheet!("src/components/chats_window/conversation.scss"); -pub(super) fn Conversation(cx: Scope) -> Element { - debug!("Conversation {} rendering", "TBD"); +#[component] +pub(super) fn Conversation(room_id: OwnedRoomId) -> Element { + error!("Conversation {} rendering", room_id); - cx.render(rsx! { - style { STYLE_SHEET }, + let _sync_message_coro: Coroutine<()> = + use_coroutine(|_: UnboundedReceiver<_>| sync_messages(&ROOMS, room_id)); + + + rsx! { + style { {STYLE_SHEET} }, div { class: ClassName::CONVERSATION, @@ -76,5 +83,5 @@ pub(super) fn Conversation(cx: Scope) -> Element { }, }, }, - }) + } } diff --git a/src/components/chats_window/edit_section.rs b/src/components/chats_window/edit_section.rs index 285a4d0..597053f 100644 --- a/src/components/chats_window/edit_section.rs +++ b/src/components/chats_window/edit_section.rs @@ -2,9 +2,9 @@ use dioxus::prelude::*; turf::style_sheet!("src/components/chats_window/edit_section.scss"); -pub fn EditSection(cx: Scope) -> Element { - cx.render(rsx! { - style { STYLE_SHEET }, +pub fn EditSection() -> Element { + rsx! { + style { {STYLE_SHEET} }, div { class: ClassName::INPUT_AREA, @@ -48,5 +48,5 @@ pub fn EditSection(cx: Scope) -> Element { }, }, }, - }) + } } diff --git a/src/components/chats_window/interface.rs b/src/components/chats_window/interface.rs index 0a43fd3..8595910 100644 --- a/src/components/chats_window/interface.rs +++ b/src/components/chats_window/interface.rs @@ -1,4 +1,5 @@ -use dioxus::prelude::*; +use std::cell::RefCell; + use matrix_sdk::ruma::OwnedRoomId; use tokio::sync::broadcast::error::SendError; use tokio::sync::broadcast::{channel, Receiver, Sender}; diff --git a/src/components/chats_window/mod.rs b/src/components/chats_window/mod.rs index 73505b0..2dd933d 100644 --- a/src/components/chats_window/mod.rs +++ b/src/components/chats_window/mod.rs @@ -3,10 +3,10 @@ mod edit_section; pub mod interface; mod navbar; +use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use dioxus::prelude::*; -use fermi::*; use matrix_sdk::ruma::OwnedRoomId; use tokio::sync::broadcast::Receiver; use tracing::{debug, error}; @@ -20,32 +20,31 @@ use interface::{Interface, Tasks}; turf::style_sheet!("src/components/chats_window/chats_window.scss"); +#[derive(Props, Clone, PartialEq)] pub struct ChatsWindowProps { pub receivers: Receivers, - pub interface: UseAtomRef, + pub interface: Signal, } -fn render_rooms_tabs<'a>( - rooms_atom_ref: &'a UseAtomRef>>, - displayed_room_ids_ref: &'a UseRef>, -) -> Vec> { - let rooms_ref = rooms_atom_ref.read(); - let displayed_room_ids = displayed_room_ids_ref.read(); +fn render_rooms_tabs( + by_id_rooms: &GlobalSignal>>, + displayed_room_ids: Signal>, +) -> Vec { + 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| -> LazyNodes { + .map(|room| { let room = room.borrow(); let room_name = room.name().unwrap_or(room.id().to_string()); rsx!( div { class: ClassName::TAB, - button { img { src: "./images/status_online.png", }, - "{room_name}", }, }, @@ -54,9 +53,25 @@ fn render_rooms_tabs<'a>( .collect() } -async fn handle_controls<'a>( - receiver_ref: &'a RefCell>, - displayed_room_ids_ref: &'a UseRef>, +fn render_rooms_conversations( + by_id_rooms: &GlobalSignal>>, + displayed_room_ids: Signal>, +) -> Vec { + 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>, + mut displayed_room_ids: Signal>, ) { loop { let result = receiver_ref.borrow_mut().recv().await; @@ -64,7 +79,7 @@ async fn handle_controls<'a>( Ok(task) => match task { Tasks::ToggleRoom(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) { Some(_) => { error!("{} room already dispayed... close it", room_id); @@ -81,45 +96,41 @@ async fn handle_controls<'a>( } } -pub fn ChatsWindow(cx: Scope) -> Element { +pub fn ChatsWindow(props: ChatsWindowProps) -> Element { debug!("ChatsWindow rendering"); - use_init_atom_root(cx); + let receivers = &props.receivers; + let interface_ref = &props.interface; - let receivers = &cx.props.receivers; - let interface_ref = &cx.props.interface; + let displayed_room_ids = use_signal(HashSet::::new); - let rooms_ref = use_atom_ref(cx, &ROOMS); - - let displayed_room_ids = use_ref(cx, HashSet::::new); - - let sync_rooms_coro = use_coroutine(cx, |rx| { + let sync_rooms_coro = use_coroutine(|rx| { to_owned![receivers]; - sync_rooms(rx, receivers, rooms_ref.clone()) + sync_rooms(rx, receivers, &ROOMS) }); sync_rooms_coro.send(true); - let _: &Coroutine<()> = use_coroutine(cx, |_: UnboundedReceiver<_>| { + let _: Coroutine<()> = use_coroutine(|_: UnboundedReceiver<_>| { to_owned![interface_ref, displayed_room_ids]; async move { let interface = interface_ref.read(); 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! { - style { STYLE_SHEET }, + rsx! { + style { {STYLE_SHEET} }, div { class: ClassName::CHATS_WINDOW, div { class: ClassName::TABS, - - rendered_rooms_tabs.into_iter(), + {rendered_rooms_tabs.into_iter()}, }, div { @@ -133,13 +144,11 @@ pub fn ChatsWindow(cx: Scope) -> Element { p { class: ClassName::ROOM_NAME, - "MON POTE", }, p { class: ClassName::ROOM_TOPIC, - "LE STATUT A MON POTE", }, }, @@ -147,8 +156,8 @@ pub fn ChatsWindow(cx: Scope) -> Element { Navbar {}, }, - Conversation {}, + {rendered_rooms_conversations.into_iter()}, }, }, - }) + } } diff --git a/src/components/chats_window/navbar.rs b/src/components/chats_window/navbar.rs index 8dbf410..cebf9e7 100644 --- a/src/components/chats_window/navbar.rs +++ b/src/components/chats_window/navbar.rs @@ -3,11 +3,11 @@ use tracing::debug; turf::style_sheet!("src/components/chats_window/navbar.scss"); -pub fn Navbar(cx: Scope) -> Element { +pub fn Navbar() -> Element { debug!("Navbar rendering"); - cx.render(rsx! { - style { STYLE_SHEET }, + rsx! { + style { {STYLE_SHEET} }, div { class: ClassName::NAVBAR, @@ -46,5 +46,5 @@ pub fn Navbar(cx: Scope) -> Element { style: "background: url(./images/settings.png) center no-repeat", }, }, - }) + } } diff --git a/src/components/contacts_window/contacts.rs b/src/components/contacts_window/contacts.rs index 7a7a495..bbd9dde 100644 --- a/src/components/contacts_window/contacts.rs +++ b/src/components/contacts_window/contacts.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use dioxus::prelude::*; use tracing::debug; @@ -7,19 +9,18 @@ use crate::components::contacts_window::contacts_section::{ turf::style_sheet!("src/components/contacts_window/contacts.scss"); -pub fn Contacts(cx: Scope) -> Element { +pub fn Contacts() -> Element { debug!("Contacts rendering"); // TODO: Test overflow // TODO: Add offline users ? - cx.render(rsx! { - style { STYLE_SHEET }, + rsx! { + style { {STYLE_SHEET} }, div { class: ClassName::CONTACTS, - - ContactsSection {name: "Groups", filter: &filter_room_conversations}, - ContactsSection {name: "Available", filter: &filter_people_conversations}, + ContactsSection {name: "Groups", filter: Rc::new(filter_room_conversations)}, + ContactsSection {name: "Available", filter: Rc::new(filter_people_conversations)}, }, - }) + } } diff --git a/src/components/contacts_window/contacts_section.rs b/src/components/contacts_window/contacts_section.rs index ce5d1e0..5292c46 100644 --- a/src/components/contacts_window/contacts_section.rs +++ b/src/components/contacts_window/contacts_section.rs @@ -1,33 +1,37 @@ +use std::cell::RefCell; +use std::rc::Rc; + use dioxus::prelude::*; use dioxus_free_icons::icons::io_icons::IoChevronDown; use dioxus_free_icons::Icon; -use fermi::prelude::*; use matrix_sdk::{ruma::OwnedRoomId, RoomState}; -use tracing::{debug, warn}; +use tracing::debug; use crate::base::{ByIdRooms, Room, CHATS_WIN_INTERFACE, ROOMS}; use crate::components::chats_window::interface::Interface as ChatsWindowInterface; turf::style_sheet!("src/components/contacts_window/contacts_section.scss"); -fn ContactsArrow(cx: Scope) -> Element { - cx.render(rsx! { - style { STYLE_SHEET }, - +fn ContactsArrow() -> Element { + rsx! { + style { {STYLE_SHEET} }, Icon { icon: IoChevronDown, }, - }) + } } static NO_NAME_REPR: &str = "No name"; static NO_SUBJECT_REPR: &str = "No subject"; -pub(super) fn filter_people_conversations(rooms_atom: UseAtomRef) -> Vec> { - let rooms = rooms_atom.read(); - let mut filtered_rooms = Vec::>::with_capacity(rooms.len()); +pub(super) fn filter_people_conversations( + by_id_rooms: &GlobalSignal, +) -> Vec> { + let by_id_rooms = by_id_rooms.read(); - for room in rooms.values() { + let mut filtered_rooms = Vec::>::with_capacity(by_id_rooms.len()); + + for room in by_id_rooms.values() { let is_direct = room.borrow().is_direct.unwrap(); if !is_direct { filtered_rooms.push(room.to_owned()); @@ -36,11 +40,14 @@ pub(super) fn filter_people_conversations(rooms_atom: UseAtomRef) -> filtered_rooms } -pub(super) fn filter_room_conversations(rooms_atom: UseAtomRef) -> Vec> { - let rooms = rooms_atom.read(); - let mut filtered_rooms = Vec::>::with_capacity(rooms.len()); +pub(super) fn filter_room_conversations( + by_id_rooms: &GlobalSignal, +) -> Vec> { + let by_id_rooms = by_id_rooms.read(); - for room in rooms.values() { + let mut filtered_rooms = Vec::>::with_capacity(by_id_rooms.len()); + + for room in by_id_rooms.values() { let is_direct = room.borrow().is_direct.unwrap(); if is_direct { filtered_rooms.push(room.to_owned()); @@ -52,30 +59,33 @@ pub(super) fn filter_room_conversations(rooms_atom: UseAtomRef) -> Ve // TODO: Handle errors fn on_clicked_room( room_id: &OwnedRoomId, - chats_window_interface: &UseAtomRef, + chats_window_interface: &GlobalSignal, ) { let _ = chats_window_interface.read().toggle_room(room_id.clone()); } -#[component] -pub fn ContactsSection<'a>( - cx: Scope, - name: &'a str, - filter: &'a dyn Fn(UseAtomRef) -> Vec>, -) -> Element { +#[derive(Props, Clone)] +pub struct ContactsSectionProps { + name: String, + filter: Rc) -> Vec>>, +} +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"); - let rooms_atom_ref = use_atom_ref(cx, &ROOMS); - let chats_window_interface_ref = use_atom_ref(cx, &CHATS_WIN_INTERFACE); - - let contacts = filter(rooms_atom_ref.clone()); + let contacts = props.filter.to_owned()(&ROOMS); let contacts_len = contacts.len(); - let show = use_state(cx, || false); + let mut show = use_signal(|| false); let classes = [ ClassName::SECTION, - if **show { ClassName::ACTIVE } else { "" }, + if *show.read() { ClassName::ACTIVE } else { "" }, ] .join(" "); @@ -102,40 +112,44 @@ pub fn ContactsSection<'a>( } ); - rsx!(li { - onclick: move |_| on_clicked_room(&room_id, chats_window_interface_ref), - - img { - src: "./images/status_online.png", - }, - p { - formatted, - }, - p { - style: "color: darkgrey;", - room_topic, - }, - }) + rsx! { + li { + onclick: move |_| on_clicked_room(&room_id, &CHATS_WIN_INTERFACE), + img { + src: "./images/status_online.png", + }, + p { + {formatted}, + }, + p { + style: "color: darkgrey;", + {room_topic}, + }, + } + } }); - cx.render(rsx! { - style { STYLE_SHEET }, + rsx! { + style { {STYLE_SHEET} }, div { class: "{classes}", p { class: ClassName::HEADER, - onclick: move |_| show.set(!show), + onclick: move |_| { + let state = *show.read(); + show.set(!state) + }, ContactsArrow {}, - format!("{name} ({contacts_len})"), + {format!("{} ({contacts_len})", props.name)}, }, ul { - rendered_contacts.into_iter(), + {rendered_contacts.into_iter()}, }, }, - }) + } } diff --git a/src/components/contacts_window/mod.rs b/src/components/contacts_window/mod.rs index d32e0c8..5458823 100644 --- a/src/components/contacts_window/mod.rs +++ b/src/components/contacts_window/mod.rs @@ -10,11 +10,11 @@ use crate::components::contacts_window::user_infos::UserInfos; turf::style_sheet!("src/components/contacts_window/contacts_window.scss"); -pub fn ContactsWindow(cx: Scope) -> Element { +pub fn ContactsWindow() -> Element { debug!("ContactsWindow rendering"); - cx.render(rsx! { - style { STYLE_SHEET }, + rsx! { + style { {STYLE_SHEET} }, div { class: ClassName::CONTACTS_WINDOW, @@ -94,5 +94,5 @@ pub fn ContactsWindow(cx: Scope) -> Element { class: ClassName::FOOTER, }, }, - }) + } } diff --git a/src/components/contacts_window/user_infos.rs b/src/components/contacts_window/user_infos.rs index 3597ff0..5d8395e 100644 --- a/src/components/contacts_window/user_infos.rs +++ b/src/components/contacts_window/user_infos.rs @@ -1,8 +1,6 @@ use dioxus::prelude::*; -// use fermi::*; use tracing::debug; -// use crate::base::APP_SETTINGS; use crate::components::avatar_selector::AvatarSelector; use crate::components::icons::DownArrowIcon; @@ -10,7 +8,7 @@ turf::style_sheet!("src/components/contacts_window/user_infos.scss"); static MESSAGE_PLACEHOLDER: &str = ""; -pub fn UserInfos(cx: Scope) -> Element { +pub fn UserInfos() -> Element { debug!("UserInfos rendering"); // 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 user_display_name_option: Option = None; + let user_display_name = "AIE"; // let user_id_option = &store.user_id; // if user_id_option.is_some() { @@ -36,8 +35,8 @@ pub fn UserInfos(cx: Scope) -> Element { // } // } - cx.render(rsx! { - style { STYLE_SHEET }, + rsx! { + style { {STYLE_SHEET} }, div { class: ClassName::USER_INFO, @@ -67,11 +66,11 @@ pub fn UserInfos(cx: Scope) -> Element { class: ClassName::USER_MESSAGE, p { // TODO: Handle user message - MESSAGE_PLACEHOLDER, + {MESSAGE_PLACEHOLDER}, } DownArrowIcon {}, }, }, }, - }) + } } diff --git a/src/components/header.rs b/src/components/header.rs index 1b672a0..d670556 100644 --- a/src/components/header.rs +++ b/src/components/header.rs @@ -2,14 +2,13 @@ use dioxus::prelude::*; turf::style_sheet!("src/components/header.scss"); -pub fn Header(cx: Scope) -> Element { - cx.render(rsx! { - style { STYLE_SHEET }, +pub fn Header() -> Element { + rsx! { + style { {STYLE_SHEET} }, div { class: ClassName::ROOT, img { - // src: "./assets/live-logo2.png" src: "./images/logo-msn.png" } svg { @@ -22,5 +21,5 @@ pub fn Header(cx: Scope) -> Element { }, }, } - }) + } } diff --git a/src/components/icons.rs b/src/components/icons.rs index dca1134..cf099e9 100644 --- a/src/components/icons.rs +++ b/src/components/icons.rs @@ -8,15 +8,15 @@ include!(concat!(env!("OUT_DIR"), "/style_vars.rs")); use style::{COLOR_PRIMARY_100, COLOR_TERNARY_100}; -pub fn DownArrowIcon(cx: Scope) -> Element { - cx.render(rsx! { - style { STYLE_SHEET }, +pub fn DownArrowIcon() -> Element { + rsx! { + style { {STYLE_SHEET} }, Icon { class: ClassName::DOWN_ARROW_ICON, icon: MdArrowDropDown, } - }) + } } 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_Y: f64 = _PYRAMID_LEFT_EDGE_E2_Y; -struct PyramidShape<'a> { - color: &'a str, +#[derive(PartialEq, Clone)] +struct PyramidShape { + color: String, ratio: f64, - progress_color: &'a str, + progress_color: String, } -impl<'a> IconShape for PyramidShape<'a> { +impl IconShape for PyramidShape { fn view_box(&self) -> String { let height = _PYRAMID_CENTRAL_EDGE_E2_Y + _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") } - fn child_elements(&self) -> LazyNodes { + fn child_elements(&self) -> Element { let inverted_ratio = 1.0 - self.ratio; let central_edge_ratio_e2_y = @@ -107,24 +108,26 @@ impl<'a> IconShape for PyramidShape<'a> { } } -#[derive(Props)] -pub struct PyramidProps<'a> { +#[derive(PartialEq, Clone, Props)] +pub struct PyramidProps { + color: Option, #[props(default = 0.5)] - color: Option<&'a str>, ratio: f64, - progress_color: Option<&'a str>, + progress_color: Option, } -pub fn Pyramid<'a>(cx: Scope<'a, PyramidProps<'a>>) -> Element<'a> { - let progress_color = cx.props.progress_color.unwrap_or(COLOR_PRIMARY_100); - let color = cx.props.color.unwrap_or(COLOR_TERNARY_100); +pub fn Pyramid(props: PyramidProps) -> Element { + let color = props.color.unwrap_or(COLOR_PRIMARY_100.to_string()); + let progress_color = props + .progress_color + .unwrap_or(COLOR_TERNARY_100.to_string()); - cx.render(rsx! { - style { STYLE_SHEET }, + rsx! { + style { {STYLE_SHEET} }, Icon { class: ClassName::PYRAMID_ICON, - icon: PyramidShape { ratio: cx.props.ratio, color, progress_color }, + icon: PyramidShape { ratio: props.ratio, color, progress_color }, } - }) + } } diff --git a/src/components/loading.rs b/src/components/loading.rs index c1a5f14..8d07831 100644 --- a/src/components/loading.rs +++ b/src/components/loading.rs @@ -6,12 +6,11 @@ use super::wallpaper::Wallpaper; turf::style_sheet!("src/components/loading.scss"); -#[component] -pub fn LoadingPage(cx: Scope) -> Element { +pub fn LoadingPage() -> Element { debug!("LoadingPage rendering"); - cx.render(rsx! { - style { STYLE_SHEET }, + rsx! { + style { {STYLE_SHEET} }, div { class: ClassName::LOADING, @@ -23,5 +22,5 @@ pub fn LoadingPage(cx: Scope) -> Element { Spinner {}, } } - }) + } } diff --git a/src/components/login.rs b/src/components/login.rs index 6f4e49c..e0fda5c 100644 --- a/src/components/login.rs +++ b/src/components/login.rs @@ -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 { +async fn generate_random_avatar(url: String) -> Option { 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; } @@ -128,18 +129,18 @@ impl Clone for Box { #[derive(Clone)] struct TextInputHandler { - state_ref: UseRef, + state: Signal, } 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 { @@ -149,17 +150,17 @@ impl OnValidationError for TextInputHandler { #[derive(Clone)] struct UrlInputHandler { - state_ref: UseRef, + state: Signal, } impl UrlInputHandler { - pub fn new(state_ref: UseRef) -> Self { - Self { state_ref } + pub fn new(state: Signal) -> 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 { @@ -186,17 +187,17 @@ impl OnValidationError for UrlInputHandler { #[derive(Clone)] struct EmailInputHandler { - state_ref: UseRef, + state: Signal, } impl EmailInputHandler { - pub fn new(state_ref: UseRef) -> Self { - Self { state_ref } + pub fn new(state: Signal) -> 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 { @@ -222,17 +223,17 @@ impl OnValidationError for EmailInputHandler { #[derive(Clone)] struct PasswordInputHandler { - state_ref: UseRef, + state: Signal, } impl PasswordInputHandler { - pub fn new(state_ref: UseRef) -> Self { - Self { state_ref } + pub fn new(state: Signal) -> 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 { @@ -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, process: &Process) -> Result<(), Ok(()) } -fn on_login( - session_ref: &UseAtomRef, - data_ref: &UseRef, -) -> Result<(), ValidationErrors> { - let login = data_ref.read(); +fn on_login(session: &GlobalSignal, data: Signal) -> 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, - data_ref: &UseRef, + _session: &GlobalSignal, + data: Signal, ) -> 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>, + handlers: HashMap<&'a str, Rc>>, } + impl<'a> InputHandlers<'a> { fn new() -> Self { Self { @@ -508,28 +509,24 @@ impl<'a> InputHandlers<'a> { } } fn insert(&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>> { 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) + 'b, -) -> LazyNodes<'a, 'b> -where - 'b: 'a, -{ +fn generate_modal( + config: &PasswordSuggestionsModalConfig, + on_confirm: impl FnMut(Event) + 'static, +) -> Element { let suggestions = config.suggestions.get(PASSWORD_FIELD_NAME); - let mut rendered_suggestions = Vec::::new(); + let mut rendered_suggestions = Vec::::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, } -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::::new); - let modal_config = use_state(cx, || None::); + let mut modal_configs = use_signal(Vec::::new); + let mut modal_config = use_signal(|| None::); - 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| { 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}, + } } diff --git a/src/components/main_window.rs b/src/components/main_window.rs index bc0e673..dc75a22 100644 --- a/src/components/main_window.rs +++ b/src/components/main_window.rs @@ -1,19 +1,15 @@ use dioxus::prelude::*; -use fermi::*; use tracing::debug; use crate::base::SESSION; use crate::components::contacts_window::ContactsWindow; -pub fn MainWindow(cx: Scope) -> Element { +pub fn MainWindow() -> Element { debug!("MainWindow rendering"); - let session_ref = use_atom_ref(cx, &SESSION); - let is_logged = session_ref.read().is_logged; - - cx.render(rsx! { - if is_logged { - rsx!(ContactsWindow {}) + rsx! { + if SESSION.read().is_logged { + ContactsWindow {} } - }) + } } diff --git a/src/components/modal.rs b/src/components/modal.rs index a440fb1..1860868 100644 --- a/src/components/modal.rs +++ b/src/components/modal.rs @@ -14,7 +14,7 @@ use style::{COLOR_CRITICAL_100, COLOR_SUCCESS_100, COLOR_WARNING_100}; turf::style_sheet!("src/components/modal.scss"); -#[derive(Clone, Eq, PartialEq, Hash)] +#[derive(Clone, Copy, Eq, PartialEq, Hash)] pub enum Severity { Ok, Warning, @@ -39,14 +39,14 @@ struct DicebearConfig<'a> { lips: Vec, } -#[derive(Props)] -pub struct ModalProps<'a> { +#[derive(Props, Clone, PartialEq)] +pub struct ModalProps { pub severity: Severity, #[props(optional)] - pub title: Option<&'a str>, - pub children: Element<'a>, + pub title: Option, + pub children: Element, #[props(optional)] - pub on_confirm: Option>, + pub on_confirm: Option>, } fn dicebear_variants() -> &'static HashMap> { @@ -96,10 +96,10 @@ fn render_dicebear_variants(values: &[u32]) -> String { .join(",") } -async fn generate_random_figure(url: &String, severity: Severity) -> Option { +async fn generate_random_figure(url: &str, severity: &Severity) -> Option { let mut res: Option = None; - let config = match dicebear_variants().get(&severity) { + let config = match dicebear_variants().get(severity) { Some(config) => config, None => { error!("No dicebear configuration found for \"{severity}\""); @@ -144,17 +144,14 @@ async fn generate_random_figure(url: &String, severity: Severity) -> Option(cx: Scope<'a, ModalProps<'a>>) -> Element<'a> { +pub fn Modal(props: ModalProps) -> Element { // 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 { - generate_random_figure(&url, severity).await - }); - - let figure = match random_figure_future.value() { + let figure = match &*random_figure_future.read_unchecked() { Some(Some(svg)) => Some(rsx! { div { class: ClassName::MODAL_CONTENT_ICON_PLACEHOLDER, @@ -163,7 +160,7 @@ pub fn Modal<'a>(cx: Scope<'a, ModalProps<'a>>) -> Element<'a> { }), Some(None) => { 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::Warning => "./images/modal-default-warning-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, }; - let button_class = match cx.props.severity { + let button_class = match &props.severity { Severity::Ok => SuccessButton, Severity::Warning => WarningButton, Severity::Critical => ErrorButton, @@ -188,8 +185,8 @@ pub fn Modal<'a>(cx: Scope<'a, ModalProps<'a>>) -> Element<'a> { figure.as_ref()?; - cx.render(rsx! { - style { STYLE_SHEET }, + rsx! { + style { {STYLE_SHEET} }, div { class: ClassName::MODAL, @@ -204,20 +201,19 @@ pub fn Modal<'a>(cx: Scope<'a, ModalProps<'a>>) -> Element<'a> { div { class: ClassName::MODAL_CONTENT_TITLE, - cx.props.title, + {props.title}, }, div { class: ClassName::MODAL_CONTENT_MSG, - - &cx.props.children, + {props.children}, }, div { class: ClassName::MODAL_CONTENT_BUTTONS, button_class { onclick: move |evt| { - if let Some(cb) = &cx.props.on_confirm { + if let Some(cb) = &props.on_confirm { cb.call(evt); } }, @@ -225,5 +221,5 @@ pub fn Modal<'a>(cx: Scope<'a, ModalProps<'a>>) -> Element<'a> { }, }, }, - }) + } } diff --git a/src/components/spinner.rs b/src/components/spinner.rs index d453081..3cc2ae5 100644 --- a/src/components/spinner.rs +++ b/src/components/spinner.rs @@ -3,7 +3,7 @@ use dioxus_free_icons::{Icon, IconShape}; turf::style_sheet!("src/components/spinner.scss"); -#[derive(Copy, Clone, Debug)] +#[derive(Clone, PartialEq)] struct _Spinner; impl IconShape for _Spinner { fn view_box(&self) -> String { @@ -12,7 +12,7 @@ impl IconShape for _Spinner { fn xmlns(&self) -> String { String::from("http://www.w3.org/2000/svg") } - fn child_elements(&self) -> LazyNodes { + fn child_elements(&self) -> Element { rsx! { path { "stroke-linejoin": "round", @@ -23,23 +23,23 @@ impl IconShape for _Spinner { } } -#[derive(PartialEq, Props)] +#[derive(PartialEq, Clone, Props)] pub struct SpinnerProps { #[props(default = true)] animate: bool, } -pub fn Spinner(cx: Scope) -> Element { - cx.render(rsx! { - style { STYLE_SHEET }, +pub fn Spinner(props: SpinnerProps) -> Element { + rsx! { + style { {STYLE_SHEET} }, div { class: ClassName::SPINNER, Icon { - class: if cx.props.animate { "" } else { ClassName::PAUSED }, + class: if props.animate { "" } else { ClassName::PAUSED }, icon: _Spinner, } } - }) + } } diff --git a/src/components/text_input.rs b/src/components/text_input.rs index 56d770e..03b8dc9 100644 --- a/src/components/text_input.rs +++ b/src/components/text_input.rs @@ -9,15 +9,15 @@ turf::style_sheet!("src/components/text_input.scss"); pub trait InputPropsData {} -#[derive(Props)] -pub struct InputProps<'a, D: InputPropsData + 'a> { - value: Option<&'a str>, - placeholder: Option<&'a str>, - oninput: Option>>, - state: Option<&'a UseRef>, +#[derive(Props, Clone, PartialEq)] +pub struct InputProps { + value: Option, + placeholder: Option, + oninput: Option>>, + state: Option>, } -#[derive(PartialEq)] +#[derive(Clone, PartialEq)] pub struct TextInputState { pub is_valid: bool, pub helper_text: Option, @@ -50,11 +50,11 @@ impl Default for TextInputState { impl InputPropsData for TextInputState {} -pub fn TextInput<'a>(cx: Scope<'a, InputProps<'a, TextInputState>>) -> Element<'a> { +pub fn TextInput(props: InputProps) -> Element { let mut criticity_class = ""; let mut helper_text = "".to_string(); - if let Some(state) = cx.props.state { + if let Some(state) = props.state { let state = state.read(); if !state.is_valid { 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(" "); - cx.render(rsx! { - style { STYLE_SHEET }, + rsx! { + style { {STYLE_SHEET} }, div { class: ClassName::TEXT_INPUT, @@ -75,11 +75,11 @@ pub fn TextInput<'a>(cx: Scope<'a, InputProps<'a, TextInputState>>) -> Element<' input { class: "{input_classes_str}", r#type: "text", - placeholder: cx.props.placeholder, - value: cx.props.value, + placeholder: props.placeholder, + value: props.value, oninput: move |evt| { - if let Some(cb) = &cx.props.oninput { + if let Some(cb) = &props.oninput { cb.call(evt); } }, @@ -90,14 +90,14 @@ pub fn TextInput<'a>(cx: Scope<'a, InputProps<'a, TextInputState>>) -> Element<' p { class: criticity_class, - helper_text + {helper_text} } } } - }) + } } -#[derive(PartialEq, Props)] +#[derive(Props, Clone, PartialEq)] pub struct PasswordInputState { text_input_state: TextInputState, #[props(default = 0.0)] @@ -129,14 +129,14 @@ impl Default for PasswordInputState { impl InputPropsData for PasswordInputState {} -pub fn PasswordTextInput<'a>(cx: Scope<'a, InputProps<'a, PasswordInputState>>) -> Element<'a> { +pub fn PasswordTextInput(props: InputProps) -> Element { let mut criticity_class = ""; let mut helper_text: String = "".to_string(); let mut score: Option = 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(); if !state.text_input_state.is_valid { criticity_class = ClassName::INVALID; @@ -158,55 +158,50 @@ pub fn PasswordTextInput<'a>(cx: Scope<'a, InputProps<'a, PasswordInputState>>) .join(" "); let input_classes = [ClassName::PASSWORD_TEXT_INPUT_INPUT, criticity_class].join(" "); - cx.render(rsx! { - style { STYLE_SHEET }, + rsx! { + style { {STYLE_SHEET} }, div { class: "{text_input_classes}", input { class: "{input_classes}", - r#type: if **show_password { "text" } else { "password" }, - placeholder: cx.props.placeholder, - value: cx.props.value, + r#type: if *show_password.read() { "text" } else { "password" }, + placeholder: props.placeholder, + value: props.value, oninput: move |evt| { - if let Some(cb) = &cx.props.oninput { + if let Some(cb) = &props.oninput { cb.call(evt); } }, }, if let Some(score) = score { - rsx!( - div { - class: ClassName::PASSWORD_TEXT_INPUT_STRENGTH_LEVEL, - Pyramid { - ratio: score, - } + div { + class: ClassName::PASSWORD_TEXT_INPUT_STRENGTH_LEVEL, + Pyramid { + ratio: score, } - ) + } }, div { class: ClassName::PASSWORD_TEXT_INPUT_SHOW_TOGGLE, onclick: move |_| { - show_password.set(!**show_password); + let current_state = *show_password.read(); + show_password.set(!current_state); }, - if **show_password { - rsx!( - Icon { - icon: IoEyeOff, - } - ) + if *show_password.read() { + Icon { + icon: IoEyeOff, + } } else { - rsx!( - Icon { - icon: IoEye, - } - ) + Icon { + icon: IoEye, + } } }, @@ -215,9 +210,9 @@ pub fn PasswordTextInput<'a>(cx: Scope<'a, InputProps<'a, PasswordInputState>>) p { class: criticity_class, - helper_text + {helper_text} } }, } - }) + } } diff --git a/src/components/wallpaper.rs b/src/components/wallpaper.rs index 00abdd9..3d02b5b 100644 --- a/src/components/wallpaper.rs +++ b/src/components/wallpaper.rs @@ -2,10 +2,9 @@ use dioxus::prelude::*; turf::style_sheet!("src/components/wallpaper.scss"); -#[component] -pub fn Wallpaper(cx: Scope) -> Element { - cx.render(rsx! { - style { STYLE_SHEET }, +pub fn Wallpaper() -> Element { + rsx! { + style { {STYLE_SHEET} }, div { class: ClassName::WALLPAPER, @@ -13,5 +12,5 @@ pub fn Wallpaper(cx: Scope) -> Element { class: ClassName::WALLPAPER_CONTENT, } } - }) + } } diff --git a/src/main.rs b/src/main.rs index 7465a14..2bc55db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,121 +5,105 @@ pub mod matrix_interface; pub mod utils; use dioxus::prelude::*; -use dioxus_desktop::Config; -use fermi::*; use tokio::time::{sleep, Duration}; use tracing::{debug, Level}; -use crate::base::{login, sync_rooms, APP_SETTINGS, CHATS_WIN_INTERFACE, ROOMS, SESSION}; -use crate::components::chats_window::{ChatsWindow, ChatsWindowProps}; +use crate::base::{login, sync_rooms}; +use crate::base::{APP_SETTINGS, ROOMS, SESSION}; use crate::components::loading::LoadingPage; use crate::components::login::Login; use crate::components::main_window::MainWindow; mod base; -fn App(cx: Scope) -> Element { +fn app() -> Element { debug!("*** App rendering ***"); - use_init_atom_root(cx); - - 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); + let mut ready = use_signal(|| false); // Dummy timer simulating the loading of the application - let _: &Coroutine<()> = use_coroutine(cx, |_: UnboundedReceiver<_>| { - to_owned![ready]; - async move { - debug!("Not ready"); - sleep(Duration::from_secs(3)).await; - // sleep(Duration::from_secs(0)).await; - debug!("Ready"); - ready.set(true); - } + let _: Coroutine<()> = use_coroutine(|_: UnboundedReceiver<_>| async move { + debug!("Not ready"); + sleep(Duration::from_secs(3)).await; + // sleep(Duration::from_secs(0)).await; + debug!("Ready"); + ready.set(true); }); - let chats_win_state = use_state(cx, || None); - - let login_coro = use_coroutine(cx, |rx| { - login(rx, app_settings_ref.clone(), session_ref.clone()) - }); + let login_coro = use_coroutine(|rx| login(rx, &APP_SETTINGS, &SESSION)); let mut sync_rooms_coro = None; - if let Some(requester) = &app_settings_ref.read().requester { - sync_rooms_coro = Some(use_coroutine(cx, |rx| { - sync_rooms(rx, requester.borrow().receivers.clone(), rooms_ref.clone()) + if let Some(requester) = &APP_SETTINGS.read().requester { + sync_rooms_coro = Some(use_coroutine(|rx| { + sync_rooms(rx, requester.borrow().receivers.clone(), &ROOMS) })); } - if !session_ref.read().is_logged { + if !SESSION.read().is_logged { login_coro.send(false); } else { if let Some(coro) = sync_rooms_coro { coro.send(true); } - if chats_win_state.is_none() { - let chats_window = dioxus_desktop::use_window(cx); + // if chats_win_state.read().is_none() { + // let chats_window = dioxus_desktop::use_window(cx); - let receivers = app_settings_ref - .read() - .requester - .as_ref() - .unwrap() - .borrow() - .receivers - .clone(); + // let receivers = app_settings + // .read() + // .requester + // .as_ref() + // .unwrap() + // .borrow() + // .receivers + // .clone(); - let chats_props = ChatsWindowProps { - receivers, - interface: chats_win_interface_ref.clone(), - }; + // let chats_props = ChatsWindowProps { + // receivers, + // 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( - r#" - - "# - .to_owned(), - ); + // margin: 0; + // } + // #main, #bodywrap { + // height: 100%; + // width: 100%; + // } + // + // "# + // .to_owned(), + // ); - let chats_window_desktop_service = chats_window.new_window(chats_dom, window_cfg); - chats_win_state.set(Some(chats_window_desktop_service)); - } + // let chats_window_desktop_service = chats_window.new_window(chats_dom, window_cfg); + // chats_win_state.set(Some(chats_window_desktop_service)); + // } } - if **ready { - if session_ref.read().is_logged { + if *ready.read() { + if SESSION.read().is_logged { debug!("Should render the MainWindow component"); - cx.render(rsx! { + rsx! { MainWindow {}, - }) + } } else { - cx.render(rsx! { + rsx! { Login {}, - }) + } } } else { - cx.render(rsx! { + rsx! { LoadingPage {}, - }) + } } } @@ -129,6 +113,5 @@ fn main() { .with_max_level(Level::DEBUG) .init(); - dioxus_desktop::launch(App); - // dioxus_web::launch(App); + launch(app); } diff --git a/src/matrix_interface/client.rs b/src/matrix_interface/client.rs index 0a5afbe..7056708 100644 --- a/src/matrix_interface/client.rs +++ b/src/matrix_interface/client.rs @@ -1,6 +1,6 @@ +use std::cell::RefCell; use std::sync::Arc; -use dioxus::prelude::*; use tokio::sync::broadcast; use tokio::sync::broadcast::Sender; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; diff --git a/src/matrix_interface/requester.rs b/src/matrix_interface/requester.rs index 2be1977..bf6c905 100644 --- a/src/matrix_interface/requester.rs +++ b/src/matrix_interface/requester.rs @@ -1,6 +1,6 @@ +use std::cell::RefCell; use std::sync::Arc; -use dioxus::prelude::*; use matrix_sdk::Client as MatrixClient; use tokio::sync::broadcast::Receiver; use tokio::sync::mpsc::UnboundedSender; @@ -12,7 +12,6 @@ use crate::utils::oneshot; pub struct Receivers { pub room_receiver: RefCell>, } - impl Clone for Receivers { fn clone(&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 matrix_client: Arc,