⬆️ 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
[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 = "<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
- [ ] Test dioxus-radio.
- [ ] Design system ?
- [ ] 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
// 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<AppSettings> = AtomRef(|_| AppSettings::new());
async fn on_room(room_id: OwnedRoomId, room: Room, rooms_ref: &UseAtomRef<ByIdRooms>) {
async fn on_room(room_id: OwnedRoomId, room: Room, by_id_rooms: &GlobalSignal<ByIdRooms>) {
// TODO: Update rooms
rooms_ref
by_id_rooms
.write()
.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(
room_id: OwnedRoomId,
room: Room,
rooms_ref: &UseAtomRef<ByIdRooms>,
by_id_rooms: &GlobalSignal<ByIdRooms>,
) {
debug!(
"You're invited to join the \"{}\" room",
room.name().unwrap()
);
// TODO: Update rooms
rooms_ref
by_id_rooms
.write()
.insert(room_id, RefCell::<Room>::new(room));
}
pub async fn on_room_topic(room_id: OwnedRoomId, topic: String, rooms_ref: &UseAtomRef<ByIdRooms>) {
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<ByIdRooms>) {
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<ByIdRooms>, room_id: OwnedRoomId) {
error!("== sync_messages ==");
}
pub async fn sync_rooms(
mut rx: UnboundedReceiver<bool>,
receivers: Receivers,
rooms_ref: UseAtomRef<ByIdRooms>,
by_id_rooms: &GlobalSignal<ByIdRooms>,
) {
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<bool>,
app_settings_ref: UseAtomRef<AppSettings>,
session_ref: UseAtomRef<Session>,
app_settings: &GlobalSignal<AppSettings>,
session: &GlobalSignal<Session>,
) {
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<ByIdRooms> = AtomRef(|_| ByIdRooms::new());
pub static SESSION: AtomRef<Session> = AtomRef(|_| Session::new());
pub static CHATS_WIN_INTERFACE: AtomRef<ChatsWinInterface> = AtomRef(|_| ChatsWinInterface::new());
pub static APP_SETTINGS: GlobalSignal<AppSettings> = Signal::global(AppSettings::new);
pub static ROOMS: GlobalSignal<ByIdRooms> = Signal::global(ByIdRooms::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");
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",
},
},
})
}
}

View File

@@ -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<String>,
onclick: Option<EventHandler<MouseEvent>>,
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<EventHandler<'a, MouseEvent>>,
style: &'static str,
id: Option<String>,
onclick: Option<EventHandler<MouseEvent>>,
style: Option<String>,
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
{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");

View File

@@ -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 {
},
},
},
})
}
}

View File

@@ -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 {
},
},
},
})
}
}

View File

@@ -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};

View File

@@ -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<Interface>,
pub interface: Signal<Interface>,
}
fn render_rooms_tabs<'a>(
rooms_atom_ref: &'a UseAtomRef<HashMap<OwnedRoomId, RefCell<Room>>>,
displayed_room_ids_ref: &'a UseRef<HashSet<OwnedRoomId>>,
) -> Vec<LazyNodes<'a, 'a>> {
let rooms_ref = rooms_atom_ref.read();
let displayed_room_ids = displayed_room_ids_ref.read();
fn render_rooms_tabs(
by_id_rooms: &GlobalSignal<HashMap<OwnedRoomId, RefCell<Room>>>,
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| -> 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<Receiver<Tasks>>,
displayed_room_ids_ref: &'a UseRef<HashSet<OwnedRoomId>>,
fn render_rooms_conversations(
by_id_rooms: &GlobalSignal<HashMap<OwnedRoomId, RefCell<Room>>>,
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 {
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<ChatsWindowProps>) -> 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::<OwnedRoomId>::new);
let rooms_ref = use_atom_ref(cx, &ROOMS);
let displayed_room_ids = use_ref(cx, HashSet::<OwnedRoomId>::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<ChatsWindowProps>) -> 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<ChatsWindowProps>) -> Element {
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");
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",
},
},
})
}
}

View File

@@ -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)},
},
})
}
}

View File

@@ -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<ByIdRooms>) -> Vec<RefCell<Room>> {
let rooms = rooms_atom.read();
let mut filtered_rooms = Vec::<RefCell<Room>>::with_capacity(rooms.len());
pub(super) fn filter_people_conversations(
by_id_rooms: &GlobalSignal<ByIdRooms>,
) -> 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();
if !is_direct {
filtered_rooms.push(room.to_owned());
@@ -36,11 +40,14 @@ pub(super) fn filter_people_conversations(rooms_atom: UseAtomRef<ByIdRooms>) ->
filtered_rooms
}
pub(super) fn filter_room_conversations(rooms_atom: UseAtomRef<ByIdRooms>) -> Vec<RefCell<Room>> {
let rooms = rooms_atom.read();
let mut filtered_rooms = Vec::<RefCell<Room>>::with_capacity(rooms.len());
pub(super) fn filter_room_conversations(
by_id_rooms: &GlobalSignal<ByIdRooms>,
) -> 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();
if is_direct {
filtered_rooms.push(room.to_owned());
@@ -52,30 +59,33 @@ pub(super) fn filter_room_conversations(rooms_atom: UseAtomRef<ByIdRooms>) -> Ve
// TODO: Handle errors
fn on_clicked_room(
room_id: &OwnedRoomId,
chats_window_interface: &UseAtomRef<ChatsWindowInterface>,
chats_window_interface: &GlobalSignal<ChatsWindowInterface>,
) {
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<ByIdRooms>) -> Vec<RefCell<Room>>,
) -> Element {
#[derive(Props, Clone)]
pub struct ContactsSectionProps {
name: String,
filter: Rc<dyn Fn(&GlobalSignal<ByIdRooms>) -> Vec<RefCell<Room>>>,
}
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),
rsx! {
li {
onclick: move |_| on_clicked_room(&room_id, &CHATS_WIN_INTERFACE),
img {
src: "./images/status_online.png",
},
p {
formatted,
{formatted},
},
p {
style: "color: darkgrey;",
room_topic,
{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()},
},
},
})
}
}

View File

@@ -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,
},
},
})
}
}

View File

@@ -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 = "<Enter a personal message>";
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<bool> = 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 {},
},
},
},
})
}
}

View File

@@ -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 {
},
},
}
})
}
}

View File

@@ -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<String>,
#[props(default = 0.5)]
color: Option<&'a str>,
ratio: f64,
progress_color: Option<&'a str>,
progress_color: Option<String>,
}
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 },
}
}
})
}

View File

@@ -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 {},
}
}
})
}
}

View File

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

View File

@@ -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 {}
}
}
})
}

View File

@@ -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<u32>,
}
#[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<String>,
pub children: Element,
#[props(optional)]
pub on_confirm: Option<EventHandler<'a, MouseEvent>>,
pub on_confirm: Option<EventHandler<MouseEvent>>,
}
fn dicebear_variants() -> &'static HashMap<Severity, DicebearConfig<'static>> {
@@ -96,10 +96,10 @@ fn render_dicebear_variants(values: &[u32]) -> String {
.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 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<Stri
}
#[component]
pub fn Modal<'a>(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> {
},
},
},
})
}
}

View File

@@ -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<SpinnerProps>) -> 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,
}
}
})
}
}

View File

@@ -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<EventHandler<'a, Event<FormData>>>,
state: Option<&'a UseRef<D>>,
#[derive(Props, Clone, PartialEq)]
pub struct InputProps<D: InputPropsData + 'static + std::cmp::PartialEq> {
value: Option<String>,
placeholder: Option<String>,
oninput: Option<EventHandler<Event<FormData>>>,
state: Option<Signal<D>>,
}
#[derive(PartialEq)]
#[derive(Clone, PartialEq)]
pub struct TextInputState {
pub is_valid: bool,
pub helper_text: Option<String>,
@@ -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<TextInputState>) -> 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<PasswordInputState>) -> Element {
let mut criticity_class = "";
let mut helper_text: String = "".to_string();
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();
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_SHOW_TOGGLE,
onclick: move |_| {
show_password.set(!**show_password);
let current_state = *show_password.read();
show_password.set(!current_state);
},
if **show_password {
rsx!(
if *show_password.read() {
Icon {
icon: IoEyeOff,
}
)
}
else {
rsx!(
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}
}
},
}
})
}
}

View File

@@ -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,
}
}
})
}
}

View File

@@ -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 {
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#"
<style type="text/css">
html, body {
height: 100%;
width: 100%;
// let window_cfg = Config::default().with_custom_head(
// r#"
// <style type="text/css">
// html, body {
// height: 100%;
// width: 100%;
margin: 0;
}
#main, #bodywrap {
height: 100%;
width: 100%;
}
</style>
"#
.to_owned(),
);
// margin: 0;
// }
// #main, #bodywrap {
// height: 100%;
// width: 100%;
// }
// </style>
// "#
// .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! {
Login {},
})
}
} else {
cx.render(rsx! {
rsx! {
Login {},
}
}
} else {
rsx! {
LoadingPage {},
})
}
}
}
@@ -129,6 +113,5 @@ fn main() {
.with_max_level(Level::DEBUG)
.init();
dioxus_desktop::launch(App);
// dioxus_web::launch(App);
launch(app);
}

View File

@@ -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};

View File

@@ -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<Receiver<RoomEvent>>,
}
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<MatrixClient>,