diff --git a/src/components/atoms/button.rs b/src/components/atoms/button.rs index 38f112c..1d3cdfc 100644 --- a/src/components/atoms/button.rs +++ b/src/components/atoms/button.rs @@ -3,6 +3,7 @@ use dioxus::prelude::*; pub enum Variant { Primary, Secondary, + Tertiary, } #[derive(Props)] @@ -21,6 +22,7 @@ pub fn Button<'a>(cx: Scope<'a, ButtonProps<'a>>) -> Element<'a> { let variant = match cx.props.variant { Variant::Primary => "button--primary", Variant::Secondary => "button--secondary", + Variant::Tertiary => "button--tertiary", }; let disabled = if cx.props.disabled { diff --git a/src/components/atoms/header.rs b/src/components/atoms/header.rs index c1ac28f..83c3185 100644 --- a/src/components/atoms/header.rs +++ b/src/components/atoms/header.rs @@ -7,6 +7,7 @@ use super::header_main::HeaderEvent; #[derive(Props)] pub struct HeaderProps<'a> { avatar_element: Option>, + menu: Option>, text: &'a str, on_event: EventHandler<'a, HeaderEvent>, } @@ -15,25 +16,25 @@ pub fn Header<'a>(cx: Scope<'a, HeaderProps<'a>>) -> Element<'a> { cx.render(rsx!( nav { class: "nav", - button { - class: "nav__cta", - onclick: move |_| {cx.props.on_event.call(HeaderEvent { value: HeaderCallOptions::CLOSE })}, - Icon { - stroke: "var(--text-1)", - icon: ArrowLeft, - height: 24, - width: 24 + div { + class: "nav-wrapper", + button { + class: "nav__cta", + onclick: move |_| {cx.props.on_event.call(HeaderEvent { value: HeaderCallOptions::CLOSE })}, + Icon { + stroke: "var(--text-1)", + icon: ArrowLeft, + height: 24, + width: 24 + } + } + cx.props.avatar_element.clone().map(|e| render!(e)) + span { + class: "nav__title", + "{cx.props.text}" } } - if let Some(element) = &cx.props.avatar_element { - rsx!( - element - ) - } - span { - class: "nav__title", - "{cx.props.text}" - } + cx.props.menu.clone().map(|e| render!(e)) } )) } diff --git a/src/components/atoms/icons/arrow_down_circle.rs b/src/components/atoms/icons/arrow_down_circle.rs new file mode 100644 index 0000000..f885ffc --- /dev/null +++ b/src/components/atoms/icons/arrow_down_circle.rs @@ -0,0 +1,15 @@ +use dioxus::prelude::*; + +use super::icon::IconShape; + +pub struct ArrowDownCircle; +impl IconShape for ArrowDownCircle { + fn view_box(&self) -> String { + String::from("0 0 24 24") + } + fn child_elements(&self) -> LazyNodes { + rsx!(path { + d: "M13 9L10 12L7 9M19 10C19 5.02944 14.9706 1 10 1C5.02944 1 1 5.02944 1 10C1 14.9706 5.02944 19 10 19C14.9706 19 19 14.9706 19 10Z" + }) + } +} diff --git a/src/components/atoms/icons/arrow_up_circle.rs b/src/components/atoms/icons/arrow_up_circle.rs new file mode 100644 index 0000000..5724f33 --- /dev/null +++ b/src/components/atoms/icons/arrow_up_circle.rs @@ -0,0 +1,15 @@ +use dioxus::prelude::*; + +use super::icon::IconShape; + +pub struct ArrowUpCircle; +impl IconShape for ArrowUpCircle { + fn view_box(&self) -> String { + String::from("0 0 24 24") + } + fn child_elements(&self) -> LazyNodes { + rsx!(path { + d: "M7 11L10 8L13 11M19 10C19 5.02944 14.9706 1 10 1C5.02944 1 1 5.02944 1 10C1 14.9706 5.02944 19 10 19C14.9706 19 19 14.9706 19 10Z" + }) + } +} diff --git a/src/components/atoms/icons/exit.rs b/src/components/atoms/icons/exit.rs new file mode 100644 index 0000000..3370c05 --- /dev/null +++ b/src/components/atoms/icons/exit.rs @@ -0,0 +1,15 @@ +use dioxus::prelude::*; + +use super::icon::IconShape; + +pub struct Exit; +impl IconShape for Exit { + fn view_box(&self) -> String { + String::from("0 0 24 24") + } + fn child_elements(&self) -> LazyNodes { + rsx!(path { + d: "M12 15L15 12M15 12L12 9M15 12H4M4 7.24802V7.2002C4 6.08009 4 5.51962 4.21799 5.0918C4.40973 4.71547 4.71547 4.40973 5.0918 4.21799C5.51962 4 6.08009 4 7.2002 4H16.8002C17.9203 4 18.4796 4 18.9074 4.21799C19.2837 4.40973 19.5905 4.71547 19.7822 5.0918C20 5.5192 20 6.07899 20 7.19691V16.8036C20 17.9215 20 18.4805 19.7822 18.9079C19.5905 19.2842 19.2837 19.5905 18.9074 19.7822C18.48 20 17.921 20 16.8031 20H7.19691C6.07899 20 5.5192 20 5.0918 19.7822C4.71547 19.5905 4.40973 19.2839 4.21799 18.9076C4 18.4798 4 17.9201 4 16.8V16.75" + }) + } +} diff --git a/src/components/atoms/icons/mod.rs b/src/components/atoms/icons/mod.rs index 6a0b8a3..76518be 100644 --- a/src/components/atoms/icons/mod.rs +++ b/src/components/atoms/icons/mod.rs @@ -1,9 +1,12 @@ +pub mod arrow_down_circle; pub mod arrow_left; +pub mod arrow_up_circle; pub mod attachment; pub mod chat_conversation; pub mod close; pub mod copy; pub mod edit; +pub mod exit; pub mod file_download; pub mod group; pub mod icon; @@ -18,12 +21,15 @@ pub mod trash; pub mod user_circle; pub mod warning; +pub use arrow_down_circle::ArrowDownCircle; pub use arrow_left::ArrowLeft; +pub use arrow_up_circle::ArrowUpCircle; pub use attachment::Attachment; pub use chat_conversation::ChatConversation; pub use close::Close; pub use copy::CopyIcon; pub use edit::Edit; +pub use exit::Exit; pub use file_download::FileDownload; pub use group::Group; pub use icon::Icon; diff --git a/src/components/atoms/room.rs b/src/components/atoms/room.rs index 627db89..f7d3060 100644 --- a/src/components/atoms/room.rs +++ b/src/components/atoms/room.rs @@ -16,15 +16,18 @@ pub struct RoomViewProps<'a> { #[props(!optional)] avatar_uri: Option, description: Option<&'a str>, + #[props(default = false)] + wrap: bool, on_click: EventHandler<'a, MouseEvent>, } pub fn RoomView<'a>(cx: Scope<'a, RoomViewProps<'a>>) -> Element<'a> { let description = cx.props.description.unwrap_or(""); + let room_view_wrap = if cx.props.wrap { "room-view--wrap" } else { "" }; cx.render(rsx! { div { - class: "room-view fade-in", + class: "room-view {room_view_wrap} fade-in", onclick: move |event| cx.props.on_click.call(event), Avatar { diff --git a/src/components/molecules/rooms.rs b/src/components/molecules/rooms.rs index 925b2bf..d4111e7 100644 --- a/src/components/molecules/rooms.rs +++ b/src/components/molecules/rooms.rs @@ -18,6 +18,8 @@ pub struct FormRoomEvent { pub struct RoomsListProps<'a> { rooms: Vec, is_loading: bool, + #[props(default = false)] + wrap: bool, on_submit: EventHandler<'a, FormRoomEvent>, } @@ -27,17 +29,20 @@ pub fn RoomsList<'a>(cx: Scope<'a, RoomsListProps<'a>>) -> Element<'a> { } else { "rooms-list--skeleton" }; + let room_list_wrap = if cx.props.wrap { "room-list--wrap" } else { "" }; cx.render(rsx! { section { - class:"rooms-list {rooms_list_skeleton} fade-in", + class:"rooms-list {room_list_wrap} {rooms_list_skeleton} fade-in", if !cx.props.rooms.is_empty() { rsx!(cx.props.rooms.iter().map(|room| { - rsx!(RoomView { + rsx!( + RoomView { key: "{room.id}", displayname: room.name.as_str(), avatar_uri: room.avatar_uri.clone(), description: "", + wrap: cx.props.wrap, on_click: move |_| { cx.props.on_submit.call(FormRoomEvent { room: CurrentRoom { @@ -47,13 +52,16 @@ pub fn RoomsList<'a>(cx: Scope<'a, RoomsListProps<'a>>) -> Element<'a> { }, }) } - }) + } + ) })) } else if cx.props.is_loading { rsx!( - (0..20).map(|_| { + (0..20).map(|i| { rsx!( - RoomViewSkeleton {} + RoomViewSkeleton { + key: "{i}" + } ) }) ) diff --git a/src/components/organisms/chat/active_room.rs b/src/components/organisms/chat/active_room.rs index 614e6c8..8972eb6 100644 --- a/src/components/organisms/chat/active_room.rs +++ b/src/components/organisms/chat/active_room.rs @@ -1,33 +1,45 @@ use dioxus::prelude::*; use dioxus_router::prelude::use_navigator; use dioxus_std::{i18n::use_i18, translate}; +use futures::TryFutureExt; use crate::{ components::{ atoms::{ header_main::{HeaderCallOptions, HeaderEvent}, - Avatar, Close, Header, Icon, + ArrowDownCircle, ArrowUpCircle, Avatar, Close, Exit, Header, Icon, }, molecules::{input_message::FormMessageEvent, rooms::CurrentRoom, InputMessage, List}, }, hooks::{ use_chat::{use_chat, UseChat}, + use_client::use_client, + use_lifecycle::use_lifecycle, use_messages::use_messages, + use_notification::use_notification, use_reply::use_reply, use_room::use_room, + use_rooms::use_rooms, use_send_attach::use_send_attach, use_send_message::use_send_message, use_thread::use_thread, }, pages::{chat::chat::MessageItem, route::Route}, - services::matrix::matrix::{Attachment, AttachmentStream}, + services::matrix::matrix::{leave_room, Attachment, AttachmentStream, LeaveRoomError}, }; -pub fn ActiveRoom(cx: Scope) -> Element { +#[derive(Props)] +pub struct ActiveRoomProps<'a> { + on_back: EventHandler<'a, ()>, +} +pub fn ActiveRoom<'a>(cx: Scope<'a, ActiveRoomProps<'a>>) -> Element<'a> { let i18 = use_i18(cx); let nav = use_navigator(cx); let room = use_room(cx); + let rooms = use_rooms(cx); let messages = use_messages(cx); + let client = use_client(cx); + let notification = use_notification(cx); let send_message = use_send_message(cx); let send_attach = use_send_attach(cx); @@ -42,12 +54,35 @@ pub fn ActiveRoom(cx: Scope) -> Element { task: _, } = use_m.get(); + let messages_lifecycle = messages.clone(); + let replying_to_lifecycle = replying_to.clone(); + let threading_to_lifecycle = threading_to.clone(); let messages = messages.get(); + let key_chat_common_error_room_id = translate!(i18, "chat.common.error.room_id"); + let key_chat_common_error_room_not_found = translate!(i18, "chat.common.error.room_not_found"); + let key_chat_actions_leave = translate!(i18, "chat.actions.leave"); + let input_placeholder = use_state::(cx, || { translate!(i18, "chat.inputs.plain_message.placeholder") }); + use_lifecycle( + &cx, + || {}, + move || { + to_owned![ + messages_lifecycle, + replying_to_lifecycle, + threading_to_lifecycle + ]; + + messages_lifecycle.set(vec![]); + replying_to_lifecycle.set(None); + threading_to_lifecycle.set(None); + }, + ); + let header_event = move |evt: HeaderEvent| { to_owned![room]; @@ -55,6 +90,7 @@ pub fn ActiveRoom(cx: Scope) -> Element { HeaderCallOptions::CLOSE => { nav.push(Route::ChatList {}); room.set(CurrentRoom::default()); + cx.props.on_back.call(()) } _ => {} } @@ -89,6 +125,46 @@ pub fn ActiveRoom(cx: Scope) -> Element { }); }; + let on_handle_leave = move |_| { + cx.spawn({ + to_owned![ + client, + room, + rooms, + notification, + key_chat_common_error_room_id, + key_chat_common_error_room_not_found, + key_chat_actions_leave + ]; + async move { + let id = room.get().id; + leave_room(&client.get(), &id).await?; + rooms + .remove_joined(&id) + .map_err(|_| LeaveRoomError::RoomNotFound)?; + room.default(); + + Ok::<(), LeaveRoomError>(()) + } + .unwrap_or_else(move |e: LeaveRoomError| { + let message = match e { + LeaveRoomError::InvalidRoomId => &key_chat_common_error_room_id, + LeaveRoomError::RoomNotFound => &key_chat_common_error_room_not_found, + LeaveRoomError::Failed => &key_chat_actions_leave, + }; + + notification.handle_error(&message); + }) + }) + }; + + let show_room_menu = use_state(cx, || false); + let on_handle_menu = move |_| { + let show_value = *show_room_menu.get(); + + show_room_menu.set(!show_value); + }; + cx.render(rsx! { div { class: "active-room", @@ -101,6 +177,56 @@ pub fn ActiveRoom(cx: Scope) -> Element { uri: room.get().avatar_uri.clone() } )), + menu: render!(rsx!( + section { + button { + class: "nav__cta", + onclick: on_handle_menu, + if *show_room_menu.get() { + rsx!( + Icon { + stroke: "var(--text-1)", + icon: ArrowUpCircle, + height: 24, + width: 24 + } + ) + } else { + rsx!( + Icon { + stroke: "var(--text-1)", + icon: ArrowDownCircle, + height: 24, + width: 24 + } + ) + }, + } + if *show_room_menu.get() { + rsx!( + div { + class: "room-menu", + ul { + li { + class: "room-menu__item", + button { + class: "room-menu__cta", + onclick: on_handle_leave, + Icon { + stroke: "var(--text-1)", + icon: Exit + } + span { + translate!(i18, "chat.room-menu.leave") + } + } + } + } + } + ) + } + } + )), on_event: header_event } List { diff --git a/src/components/organisms/chat/mod.rs b/src/components/organisms/chat/mod.rs index dc56075..998364e 100644 --- a/src/components/organisms/chat/mod.rs +++ b/src/components/organisms/chat/mod.rs @@ -1,4 +1,8 @@ pub mod active_room; +pub mod preview_room; +pub mod public_rooms; pub mod utils; +pub use public_rooms::PublicRooms; pub use active_room::ActiveRoom; +pub use preview_room::PreviewRoom; diff --git a/src/components/organisms/chat/preview_room.rs b/src/components/organisms/chat/preview_room.rs new file mode 100644 index 0000000..3522941 --- /dev/null +++ b/src/components/organisms/chat/preview_room.rs @@ -0,0 +1,336 @@ +use std::rc::Rc; + +use dioxus::prelude::*; +use dioxus_router::prelude::use_navigator; +use dioxus_std::{i18n::use_i18, translate}; +use futures::TryFutureExt; +use ruma::RoomId; + +use crate::{ + components::{ + atoms::{ + button::Variant, + header_main::{HeaderCallOptions, HeaderEvent}, + Avatar, Button, Header, + }, + molecules::rooms::CurrentRoom, + }, + hooks::{ + use_client::use_client, + use_notification::use_notification, + use_room::use_room, + use_room_preview::{use_room_preview, PreviewRoom}, + use_rooms::use_rooms, + }, + pages::route::Route, + services::matrix::matrix::join_room, +}; + +pub enum PreviewRoomError { + InvalidRoomId, + InvitationNotFound, + AcceptFailed, + JoinFailed, +} + +#[derive(Props)] +pub struct PreviewRoomProps<'a> { + on_back: EventHandler<'a, ()>, +} +pub fn PreviewRoom<'a>(cx: Scope<'a, PreviewRoomProps<'a>>) -> Element<'a> { + let i18 = use_i18(cx); + let nav = use_navigator(cx); + let preview = use_room_preview(cx); + let room = use_room(cx); + let rooms = use_rooms(cx); + let client = use_client(cx); + let notification = use_notification(cx); + + let key_chat_preview_invited_cta_accept = translate!(i18, "chat.preview.invited.cta.accept"); + let key_chat_preview_invited_cta_reject = translate!(i18, "chat.preview.invited.cta.reject"); + let key_chat_preview_join_cta_accept = translate!(i18, "chat.preview.join.cta.accept"); + let key_chat_preview_join_cta_back = translate!(i18, "chat.preview.join.cta.back"); + + let header_event = move |evt: HeaderEvent| { + to_owned![preview]; + + match evt.value { + HeaderCallOptions::CLOSE => { + nav.push(Route::ChatList {}); + preview.set(PreviewRoom::default()); + cx.props.on_back.call(()) + } + _ => {} + } + }; + + let on_handle_accept_invitation = move |r: Rc| { + let key_chat_common_error_room_id = translate!(i18, "chat.common.error.room_id"); + let key_chat_preview_error_not_found = translate!(i18, "chat.preview_error_not_found"); + let key_chat_preview_error_accept = translate!(i18, "chat.preview_error_accept"); + let key_chat_preview_error_join = translate!(i18, "chat.preview_error_join"); + + cx.spawn({ + to_owned![preview, room, client, notification, rooms]; + + async move { + let room_id = RoomId::parse(&*r.id).map_err(|_| PreviewRoomError::InvalidRoomId)?; + let invitation = client + .get() + .get_invited_room(&room_id) + .ok_or(PreviewRoomError::InvitationNotFound)?; + + invitation + .accept_invitation() + .await + .map_err(|_| PreviewRoomError::AcceptFailed)?; + + preview.default(); + room.set(CurrentRoom { + id: r.id.to_string(), + name: r.name.to_string(), + avatar_uri: r.avatar_uri.clone(), + }); + + let item = rooms + .remove_invited(&room_id.to_string()) + .map_err(|_| PreviewRoomError::InvitationNotFound)?; + + rooms.push_joined(item); + + Ok::<(), PreviewRoomError>(()) + } + .unwrap_or_else(move |e: PreviewRoomError| { + let message = match e { + PreviewRoomError::InvalidRoomId => &key_chat_common_error_room_id, + PreviewRoomError::InvitationNotFound => &key_chat_preview_error_not_found, + PreviewRoomError::AcceptFailed => &key_chat_preview_error_accept, + PreviewRoomError::JoinFailed => &key_chat_preview_error_join, + }; + + notification.handle_error(&message); + }) + }) + }; + + let on_handle_reject_invitation = move |r: Rc| { + let key_chat_common_error_room_id = translate!(i18, "chat.common.error.room_id"); + let key_chat_preview_error_not_found = translate!(i18, "chat.preview_error_not_found"); + let key_chat_preview_error_accept = translate!(i18, "chat.preview_error_accept"); + let key_chat_preview_error_join = translate!(i18, "chat.preview_error_join"); + + cx.spawn({ + to_owned![preview, room, client, notification, rooms]; + + async move { + let room_id = RoomId::parse(&*r.id).map_err(|_| PreviewRoomError::InvalidRoomId)?; + let invitation = client + .get() + .get_invited_room(&room_id) + .ok_or(PreviewRoomError::InvitationNotFound)?; + + invitation + .reject_invitation() + .await + .map_err(|_| PreviewRoomError::AcceptFailed)?; + + preview.default(); + room.default(); + + rooms + .remove_invited(&room_id.to_string()) + .map_err(|_| PreviewRoomError::InvitationNotFound)?; + + Ok::<(), PreviewRoomError>(()) + } + .unwrap_or_else(move |e: PreviewRoomError| { + let message = match e { + PreviewRoomError::InvalidRoomId => &key_chat_common_error_room_id, + PreviewRoomError::InvitationNotFound => &key_chat_preview_error_not_found, + PreviewRoomError::AcceptFailed => &key_chat_preview_error_accept, + PreviewRoomError::JoinFailed => &key_chat_preview_error_join, + }; + + notification.handle_error(&message); + }) + }) + }; + + let on_handle_join = move |r: Rc| { + let key_chat_common_error_room_id = translate!(i18, "chat.common.error.room_id"); + let key_chat_preview_error_not_found = translate!(i18, "chat.preview_error_not_found"); + let key_chat_preview_error_accept = translate!(i18, "chat.preview_error_accept"); + let key_chat_preview_error_join = translate!(i18, "chat.preview_error_join"); + + cx.spawn({ + to_owned![client, notification, room]; + async move { + let room_id = RoomId::parse(&*r.id).map_err(|_| PreviewRoomError::InvalidRoomId)?; + + join_room(&client.get(), &room_id) + .await + .map_err(|_| PreviewRoomError::JoinFailed)?; + + room.set(CurrentRoom { + id: r.id.to_string(), + name: r.name.to_string(), + avatar_uri: r.avatar_uri.clone(), + }); + + Ok::<(), PreviewRoomError>(()) + } + .unwrap_or_else(move |e: PreviewRoomError| { + let message = match e { + PreviewRoomError::InvalidRoomId => &key_chat_common_error_room_id, + PreviewRoomError::InvitationNotFound => &key_chat_preview_error_not_found, + PreviewRoomError::AcceptFailed => &key_chat_preview_error_accept, + PreviewRoomError::JoinFailed => &key_chat_preview_error_join, + }; + + notification.handle_error(&message); + }) + }) + }; + + let on_handle_back = move || { + cx.spawn({ + to_owned![preview, room]; + + async move { + preview.default(); + room.default(); + } + }) + }; + + render!(rsx! { + div { + class: "active-room", + match preview.get() { + PreviewRoom::Invited(room) => { + let room = Rc::new(room); + let room_to_header = room.clone(); + let room_to_avatar = room.clone(); + let room_action_accept = room.clone(); + let room_action_reject = room.clone(); + + render!( + rsx!( + Header { + text: "{room_to_header.name.clone()}", + avatar_element: render!(rsx!( + Avatar { + name: room_to_header.name.to_string(), + size: 32, + uri: room_to_header.avatar_uri.clone() + } + )), + on_event: header_event + } + + section { + class: "preview-room", + h3 { + class: "preview-room__title", + translate!(i18, "chat.preview.invited.title") "{room.name.clone()}?" + } + Avatar { + name: room_to_avatar.name.to_string(), + size: 32, + uri: room_to_avatar.avatar_uri.clone() + } + div { + class: "preview-room__content", + Button { + text: "{key_chat_preview_invited_cta_accept}", + on_click: move |_| { + on_handle_accept_invitation(room_action_accept.clone()) + }, + status: None + } + + Button { + text: "{key_chat_preview_invited_cta_reject}", + variant: &Variant::Tertiary, + on_click: move |_| { + on_handle_reject_invitation(room_action_reject.clone()) + }, + status: None + } + } + + } + ) + ) + } + PreviewRoom::Joining(room) => { + let room = Rc::new(room); + let room_to_header = room.clone(); + let room_action_join = room.clone(); + + render!( + rsx!( + Header { + text: "{room_to_header.name.clone()}", + avatar_element: render!(rsx!( + Avatar { + name: room_to_header.name.to_string(), + size: 32, + uri: room_to_header.avatar_uri.clone() + } + )), + on_event: header_event + } + + section { + class: "preview-room", + h3 { + class: "preview-room__title", + translate!(i18, "chat.preview.join.title") + } + div { + class: "preview-room__content", + Button { + text: "{key_chat_preview_join_cta_accept}", + on_click: move |_| { + on_handle_join(room_action_join.clone()) + }, + status: None + } + + Button { + text: "{key_chat_preview_join_cta_back}", + variant: &Variant::Tertiary, + on_click: move |_| { + on_handle_back() + }, + status: None + } + } + + } + ) + ) + } + PreviewRoom::Creating(room) => { + render!( + rsx!( + Header { + text: "{room.name.clone()}", + avatar_element: render!(rsx!( + Avatar { + name: room.name.to_string(), + size: 32, + uri: room.avatar_uri.clone() + } + )), + on_event: header_event + } + ) + ) + } + _ => None + } + } + }) +} diff --git a/src/components/organisms/chat/public_rooms.rs b/src/components/organisms/chat/public_rooms.rs new file mode 100644 index 0000000..8ed7c8f --- /dev/null +++ b/src/components/organisms/chat/public_rooms.rs @@ -0,0 +1,77 @@ +use dioxus::prelude::*; +use dioxus_router::prelude::use_navigator; +use dioxus_std::{i18n::use_i18, translate}; + +use crate::{ + components::{ + atoms::{ + header_main::{HeaderCallOptions, HeaderEvent}, + Header, + }, + molecules::{rooms::FormRoomEvent, RoomsList}, + }, + hooks::{ + use_messages::use_messages, + use_public::use_public, + use_room_preview::{use_room_preview, PreviewRoom}, + use_rooms::use_rooms, + }, + pages::route::Route, +}; + +pub enum PreviewRoomError { + InvalidRoomId, + InvitationNotFound, + AcceptFailed, +} + +#[derive(Props)] +pub struct PublicRoomProps<'a> { + on_back: EventHandler<'a, ()>, +} +pub fn PublicRooms<'a>(cx: Scope<'a, PublicRoomProps<'a>>) -> Element<'a> { + let i18 = use_i18(cx); + let nav = use_navigator(cx); + let preview = use_room_preview(cx); + let rooms = use_rooms(cx); + let messages = use_messages(cx); + let public = use_public(cx); + + let key_public_title = translate!(i18, "chat.public.title"); + + let header_event = move |evt: HeaderEvent| { + to_owned![public]; + + match evt.value { + HeaderCallOptions::CLOSE => { + nav.push(Route::ChatList {}); + public.default(); + cx.props.on_back.call(()) + } + _ => {} + } + }; + + let on_click_room = move |evt: FormRoomEvent| { + messages.reset(); + preview.set(PreviewRoom::Joining(evt.room.clone())); + public.default(); + }; + + render!(rsx! { + div { + class: "active-room", + Header { + text: "{key_public_title}", + on_event: header_event + } + + RoomsList { + rooms: rooms.get_public().clone(), + is_loading: false, + on_submit: on_click_room, + wrap: true, + } + } + }) +} diff --git a/src/hooks/use_init_app.rs b/src/hooks/use_init_app.rs index e9e0cc8..85ae646 100644 --- a/src/hooks/use_init_app.rs +++ b/src/hooks/use_init_app.rs @@ -13,6 +13,8 @@ use ruma::api::client::uiaa::AuthType; use super::use_auth::CacheLogin; use super::use_notification::NotificationItem; +use super::use_public::PublicState; +use super::use_room_preview::PreviewRoom; use super::use_rooms::RoomsList; use super::use_send_attach::SendAttachStatus; use super::use_session::UserSession; @@ -41,6 +43,7 @@ pub fn use_init_app(cx: &ScopeState) { // change when we push a ChatRoom from a different nest route use_shared_state_provider::(cx, || CurrentRoom::default()); + use_shared_state_provider::(cx, || PreviewRoom::default()); use_shared_state_provider::(cx, || RoomsList::default()); use_shared_state_provider::(cx, || Vec::new()); use_shared_state_provider::>(cx, || None); @@ -60,4 +63,5 @@ pub fn use_init_app(cx: &ScopeState) { value: HashMap::new(), }); use_shared_state_provider(cx, || SendAttachStatus::Loading(0)); + use_shared_state_provider::(cx, || PublicState::default()); } diff --git a/src/hooks/use_public.rs b/src/hooks/use_public.rs new file mode 100644 index 0000000..bcdbde0 --- /dev/null +++ b/src/hooks/use_public.rs @@ -0,0 +1,34 @@ +use dioxus::prelude::*; + +#[derive(Default, Debug, Clone)] +pub struct PublicState { + pub show: bool, +} + +pub fn use_public(cx: &ScopeState) -> &UsePublicState { + let public_state = use_shared_state::(cx).expect("Unable to use PublicState"); + + cx.use_hook(move || UsePublicState { + inner: public_state.clone(), + }) +} + +#[derive(Clone)] +pub struct UsePublicState { + inner: UseSharedState, +} + +impl UsePublicState { + pub fn get(&self) -> PublicState { + self.inner.read().clone() + } + + pub fn set(&self, room: PublicState) { + let mut inner = self.inner.write(); + *inner = room; + } + + pub fn default(&self) { + self.set(PublicState::default()) + } +} diff --git a/src/hooks/use_room_preview.rs b/src/hooks/use_room_preview.rs new file mode 100644 index 0000000..2265c38 --- /dev/null +++ b/src/hooks/use_room_preview.rs @@ -0,0 +1,49 @@ +use dioxus::prelude::*; + +use crate::components::molecules::rooms::CurrentRoom; + +#[derive(Clone, Debug, PartialEq, Hash, Eq, Default)] +pub enum PreviewRoom { + Invited(CurrentRoom), + Creating(CurrentRoom), + Joining(CurrentRoom), + #[default] + None, +} + +impl PreviewRoom { + pub fn is_none(self) -> bool { + match self { + PreviewRoom::None => true, + _ => false, + } + } +} + +pub fn use_room_preview(cx: &ScopeState) -> &UseRoomState { + let preview_room = use_shared_state::(cx).expect("Unable to use PreviewRoom"); + + cx.use_hook(move || UseRoomState { + inner: preview_room.clone(), + }) +} + +#[derive(Clone)] +pub struct UseRoomState { + inner: UseSharedState, +} + +impl UseRoomState { + pub fn get(&self) -> PreviewRoom { + self.inner.read().clone() + } + + pub fn set(&self, room: PreviewRoom) { + let mut inner = self.inner.write(); + *inner = room; + } + + pub fn default(&self) { + self.set(PreviewRoom::default()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 4c6b925..2598af5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,8 +20,10 @@ pub mod hooks { pub mod use_messages; pub mod use_modal; pub mod use_notification; + pub mod use_public; pub mod use_reply; pub mod use_room; + pub mod use_room_preview; pub mod use_rooms; pub mod use_send_attach; pub mod use_send_message; diff --git a/src/pages/chat/chat_list.rs b/src/pages/chat/chat_list.rs index 651dabb..fab008b 100644 --- a/src/pages/chat/chat_list.rs +++ b/src/pages/chat/chat_list.rs @@ -2,22 +2,21 @@ use std::collections::HashMap; use dioxus::prelude::*; use dioxus_std::{i18n::use_i18, translate}; +use futures::TryFutureExt; +use wasm_bindgen::JsCast; +use web_sys::HtmlElement; use crate::{ components::{ atoms::{ - helper::HelperData, input::InputType, message::Messages, room::RoomItem, Helper, - MessageInput, Space, SpaceSkeleton, + input::InputType, message::Messages, room::RoomItem, MessageInput, Space, SpaceSkeleton, }, molecules::{ rooms::{CurrentRoom, FormRoomEvent}, RoomsList, }, organisms::{ - chat::{ - utils::handle_command::{self, Command}, - ActiveRoom, PreviewRoom, PublicRooms, - }, + chat::{ActiveRoom, PreviewRoom, PublicRooms}, main::TitleHeaderMain, }, }, @@ -26,19 +25,23 @@ use crate::{ use_lifecycle::use_lifecycle, use_messages::use_messages, use_notification::use_notification, - use_public::{use_public, PublicState}, + use_public::use_public, use_room::use_room, use_room_preview::{use_room_preview, PreviewRoom}, use_rooms::{use_rooms, RoomsList}, use_session::use_session, }, - pages::chat::chat::MessageItem, services::matrix::matrix::{ invited_rooms, list_rooms_and_spaces, public_rooms_and_spaces, Conversations, }, - services::matrix::matrix::{list_rooms_and_spaces, Conversations}, }; +pub enum ChatListError { + SessionNotFound, + InvitedRooms, + PublicRooms, +} + #[inline_props] pub fn ChatList(cx: Scope) -> Element { let i18 = use_i18(cx); @@ -46,24 +49,33 @@ pub fn ChatList(cx: Scope) -> Element { let session = use_session(cx); let notification = use_notification(cx); let room = use_room(cx); + let public = use_public(cx); + let rooms_list = use_rooms(cx); + let preview = use_room_preview(cx); let messages = use_messages(cx); let room_tabs = use_ref::>(cx, || HashMap::new()); let key_chat_list_home = translate!(i18, "chat.list.home"); let key_chat_list_search = translate!(i18, "chat.list.search"); + let key_chat_list_errors_public_rooms = translate!(i18, "chat.list.errors.public_rooms"); + let key_chat_list_errors_invited_rooms = translate!(i18, "chat.list.errors.invited_rooms"); let key_session_error_not_found = translate!(i18, "chat.session.error.not_found"); + let key_chat_helper_rooms_title = translate!(i18, "chat.helpers.rooms.title"); + let key_chat_helper_rooms_description = translate!(i18, "chat.helpers.rooms.description"); + let key_chat_helper_rooms_subtitle = translate!(i18, "chat.helpers.rooms.subtitle"); + let rooms = use_state::>(cx, || Vec::new()); let all_rooms = use_state::>(cx, || Vec::new()); let spaces = use_state::>>(cx, || HashMap::new()); - let rooms_to_list = use_ref::>(cx, || Vec::new()); let pattern = use_state(cx, String::new); let rooms_filtered = use_ref(cx, || Vec::new()); let selected_space = use_ref::(cx, || String::new()); let title_header = use_shared_state::(cx).expect("Unable to read title header"); let is_loading = use_state(cx, || false); + let chat_list_wrapper_ref = use_ref::>>(cx, || None); let r = room.clone(); use_lifecycle( @@ -76,18 +88,12 @@ pub fn ChatList(cx: Scope) -> Element { }, ); - let on_click_room = move |evt: FormRoomEvent| { - room.set(evt.room.clone()); - room_tabs.with_mut(|tabs| tabs.insert(evt.room, vec![])); - messages.reset(); - }; - - use_coroutine(cx, |_: UnboundedReceiver| { + use_coroutine(cx, |_: UnboundedReceiver<()>| { to_owned![ client, + rooms_list, rooms, spaces, - rooms_to_list, rooms_filtered, all_rooms, selected_space, @@ -101,15 +107,22 @@ pub fn ChatList(cx: Scope) -> Element { async move { is_loading.set(true); - let Some(session_data) = session.get() else { - return notification.handle_error(&key_session_error_not_found); - }; + + let session_data = session.get().ok_or(ChatListError::SessionNotFound)?; + + let invited = invited_rooms(&client) + .await + .map_err(|_| ChatListError::InvitedRooms)?; let Conversations { rooms: r, spaces: s, } = list_rooms_and_spaces(&client, session_data).await; + let public_rooms = public_rooms_and_spaces(&client, None, None, None) + .await + .map_err(|_| ChatListError::PublicRooms)?; + rooms.set(r.clone()); spaces.set(s.clone()); @@ -123,14 +136,29 @@ pub fn ChatList(cx: Scope) -> Element { all_r.extend_from_slice(&r.clone()); }); - rooms_to_list.set(r.clone()); + rooms_list.set(RoomsList { + public: public_rooms.rooms, + invited, + joined: r.clone(), + }); rooms_filtered.set(r); selected_space.set(key_chat_list_home.clone()); title_header.write().title = key_chat_list_home.clone(); is_loading.set(false); + + Ok::<(), ChatListError>(()) } + .unwrap_or_else(move |e: ChatListError| { + let message = match e { + ChatListError::SessionNotFound => &key_session_error_not_found, + ChatListError::PublicRooms => &key_chat_list_errors_public_rooms, + ChatListError::InvitedRooms => &key_chat_list_errors_invited_rooms, + }; + + notification.handle_error(&message); + }) }); enum ScrollToPosition { @@ -167,101 +195,88 @@ pub fn ChatList(cx: Scope) -> Element { on_scroll_chat_list_wrapper(ScrollToPosition::Right); }; - let on_click_helper = move |_| { - on_scroll_chat_list_wrapper(ScrollToPosition::Right); - cx.spawn({ - to_owned![client, notification, public]; - async move { - let message_item = MessageItem { - room_id: String::new(), - msg: String::from("!rooms"), - reply_to: None, - send_to_thread: false, - }; - match handle_command::handle_command(&message_item, &client).await { - Ok(Command::Join(_)) => {} - Ok(Command::PublicRooms) => public.set(PublicState { show: true }), - Err(error) => { - let message = match error { - _ => "Error", - }; - - notification.handle_error(message); - } - } - } - }) - }; - render! { - section { - class: "chat-list options", - div { - if !spaces.get().is_empty() { - rsx!( - ul { - class: "chat-list__wrapper", - Space { - text: "{key_chat_list_home}", - uri: None, - on_click: move |_| { - rooms_to_list.set(rooms.get().clone()); - rooms_filtered.set(rooms.get().clone()); - selected_space.set(key_chat_list_home.clone()); - title_header.write().title = key_chat_list_home.clone(); - - if !rooms.get().iter().any(|r| { - room.get().id.eq(&r.id) - }) { - room.default() + div { + class: "chat-list-wrapper", + onmounted: move |event| { + event.data.get_raw_element() + .ok() + .and_then(|raw_element| raw_element.downcast_ref::()) + .and_then(|element| element.clone().dyn_into::().ok()) + .map(|html_element| chat_list_wrapper_ref.set(Some(Box::new(html_element.clone())))); + }, + section { + class: "chat-list options", + div { + class: "chat-list__spaces", + if !spaces.get().is_empty() { + rsx!( + ul { + class: "chat-list__wrapper", + Space { + text: "{key_chat_list_home}", + uri: None, + on_click: move |_| { + rooms_list.set_joined(rooms.get().clone()); + rooms_filtered.set(rooms.get().clone()); + selected_space.set(key_chat_list_home.clone()); + title_header.write().title = key_chat_list_home.clone(); + + if !rooms.get().iter().any(|r| { + room.get().id.eq(&r.id) + }) { + room.default() + } } } - } - spaces.get().iter().map(|(space, value)|{ - let name = space.name.clone(); - rsx!( - Space { - text: "{name}", - uri: space.avatar_uri.clone(), - on_click: move |_| { - rooms_to_list.set(value.clone()); - rooms_filtered.set(value.clone()); - selected_space.set(space.name.clone()); - title_header.write().title = space.name.clone(); - - if !value.iter().any(|r| { - room.get().id.eq(&r.id) - }) { - room.default() + spaces.get().iter().map(|(space, value)|{ + let name = space.name.clone(); + rsx!( + Space { + text: "{name}", + uri: space.avatar_uri.clone(), + on_click: move |_| { + rooms_list.set_joined(value.clone()); + rooms_filtered.set(value.clone()); + selected_space.set(space.name.clone()); + title_header.write().title = space.name.clone(); + + if !value.iter().any(|r| { + room.get().id.eq(&r.id) + }) { + room.default() + } } } - } - ) - }) - } - ) - } else if *is_loading.get() { - rsx!( - ul { - class: "chat-list__wrapper", - (0..5).map(|_| { - rsx!( - SpaceSkeleton { - size: 50 - } - ) - }) - } - ) - } else { - rsx!( div {}) + ) + }) + } + ) + } else if *is_loading.get() { + rsx!( + ul { + class: "chat-list__wrapper", + (0..5).map(|_| { + rsx!( + SpaceSkeleton { + size: 50 + } + ) + }) + } + ) + } else { + rsx!( div {}) + } } - } - rsx!( + div { class: "chat-list__rooms", + onclick: move |_| { + on_scroll_chat_list_wrapper(ScrollToPosition::Left) + }, MessageInput { message: "{pattern}", placeholder: "{key_chat_list_search}", @@ -281,75 +296,80 @@ pub fn ChatList(cx: Scope) -> Element { rooms_filtered.set(x); } else { - rooms_filtered.set(rooms_to_list.read().clone()) + rooms_filtered.set(rooms_list.get_joined().clone()) } }, on_keypress: move |_| {}, - on_click: move |_| {} + on_click: move |_| { + on_scroll_chat_list_wrapper(ScrollToPosition::Right) + }, } + if !rooms_list.get_invited().is_empty() { + rsx!{ + h2 { + class: "header__title", + translate!(i18, "chat.list.invitate") + } + RoomsList { + rooms: rooms_list.get_invited().clone(), + is_loading: *is_loading.get(), + on_submit: on_click_invitation + } + } + } + + h2 { + class: "header__title", + translate!(i18, "chat.list.rooms") + } RoomsList { - rooms: rooms_filtered.read().clone(), + rooms: rooms_list.get_joined().clone(), is_loading: *is_loading.get(), on_submit: on_click_room } } - ) - - - div { - class: "chat-list__content", - onclick: move |_| { - on_scroll_chat_list_wrapper(ScrollToPosition::Right) - }, - if public.get().show { - rsx!( - section { - class: "chat-list__active-room", - PublicRooms { - on_back: move |_| { - on_scroll_chat_list_wrapper(ScrollToPosition::Left) + + + div { + class: "chat-list__content", + onclick: move |_| { + on_scroll_chat_list_wrapper(ScrollToPosition::Right) + }, + if public.get().show { + rsx!( + section { + class: "chat-list__active-room", + PublicRooms { + on_back: move |_| { + on_scroll_chat_list_wrapper(ScrollToPosition::Left) + } } } - } - ) - } else if !preview.get().is_none() { - rsx!( - section { - class: "chat-list__active-room", - PreviewRoom { - on_back: move |_| { - on_scroll_chat_list_wrapper(ScrollToPosition::Left) + ) + } else if !preview.get().is_none() { + rsx!( + section { + class: "chat-list__active-room", + PreviewRoom { + on_back: move |_| { + on_scroll_chat_list_wrapper(ScrollToPosition::Left) + } } } - } - ) - } else if !room.get().name.is_empty(){ - rsx!( - section { - class: "chat-list__active-room", - ActiveRoom { - on_back: move |_| { - on_scroll_chat_list_wrapper(ScrollToPosition::Left) + ) + } else if !room.get().name.is_empty(){ + rsx!( + section { + class: "chat-list__active-room", + ActiveRoom { + on_back: move |_| { + on_scroll_chat_list_wrapper(ScrollToPosition::Left) + } } } - } - ) - } else { - rsx!( - section { - class: "chat-list__static", - Helper { - helper: HelperData { - title: key_chat_helper_rooms_title, - description: key_chat_helper_rooms_description, - subtitle: key_chat_helper_rooms_subtitle, - example: String::from("!rooms") - }, - on_click: on_click_helper - } - } - ) + ) + } } } }