diff --git a/src/ui/layouts/conversations.rs b/src/ui/layouts/conversations.rs new file mode 100644 index 0000000..80cb846 --- /dev/null +++ b/src/ui/layouts/conversations.rs @@ -0,0 +1,252 @@ +use std::rc::Rc; + +use dioxus::prelude::*; +use futures::join; + +turf::style_sheet!("src/ui/layouts/conversations.scss"); + +use crate::ui::components::chat_panel::ChatPanel; +use crate::ui::components::conversations::Conversations as ConversationsComponent; +use crate::ui::components::wallpaper::Wallpaper; + +// TODO: Get from SCSS +const WIDGET_HEIGHT_RATIO: f64 = 0.95; +const ASPECT_RATIO: f64 = 1.0 / 1.618; + +async fn on_carousel_scroll( + parent_div: &Rc, + first_div: &Rc, + last_div: &Rc, +) { + let results = join!( + parent_div.get_scroll_offset(), + parent_div.get_scroll_size(), + last_div.get_client_rect() + ); + if let (Ok(offset), Ok(size), Ok(last_div_rect)) = results { + let left = offset.x; + let width = size.width; + // The left border of the first div has been exceeded, scrool to the last div. + if left <= 0.0 { + let _ = last_div.scroll_to(ScrollBehavior::Smooth).await; + } + // The left border of the last div has been exceeded, scrool to the first div. + else { + let last_div_width = last_div_rect.size.width; + let distance_to_tail = width - left - last_div_width; + + if distance_to_tail < 1.0 { + let first_div = first_div.as_ref(); //.unwrap(); + let _ = first_div.scroll_to(ScrollBehavior::Smooth).await; + } + } + } +} + +fn LayoutSmall() -> Element { + let mut carousel_div = use_signal(|| None::>); + let mut first_div = use_signal(|| None::>); + let mut last_div = use_signal(|| None::>); + + let conversation_panels_nb = 3; + let conversation_panels = (0..conversation_panels_nb + 1).map(|i| { + let inner = rsx! { + div { + class: ClassName::CONVERSATIONS_VIEW_SMALL_PANEL_INNER, + ChatPanel { name: format!("CHAT #{i}") }, + } + }; + + if i == conversation_panels_nb { + rsx! { + div { + class: ClassName::CONVERSATIONS_VIEW_SMALL_PANEL, + onmounted: move |cx: Event| last_div.set(Some(cx.data())), + {inner} + } + } + } else { + rsx! { + div { + class: ClassName::CONVERSATIONS_VIEW_SMALL_PANEL, + {inner} + } + } + } + }); + + rsx! { + style { {STYLE_SHEET} }, + + div { + class: ClassName::CONVERSATIONS_VIEW_SMALL, + + onmounted: move |cx| async move { + let data = cx.data(); + carousel_div.set(Some(data)); + }, + + onscroll: move |_| { + async move { + if let (Some(carousel_div), Some(first_div), Some(last_div)) = ( + carousel_div.read().as_ref(), + first_div.read().as_ref(), + last_div.read().as_ref(), + ) { + on_carousel_scroll(carousel_div, first_div, last_div).await; + } + } + }, + + div { + class: ClassName::CONVERSATIONS_VIEW_HEAD, + } + + div { + class: ClassName::CONVERSATIONS_VIEW_SMALL_CONVERSATIONS_PANEL, + onmounted: move |cx| async move { + let data = cx.data(); + let _ = data.as_ref().scroll_to(ScrollBehavior::Smooth).await; + first_div.set(Some(data)); + }, + + div { + class: ClassName::CONVERSATIONS_VIEW_SMALL_CONVERSATIONS_PANEL_INNER, + ConversationsComponent {}, + }, + }, + + {conversation_panels} + + div { + class: ClassName::CONVERSATIONS_VIEW_TAIL, + } + } + } +} + +fn LayoutBig() -> Element { + let mut carousel_div = use_signal(|| None::>); + let mut first_div = use_signal(|| None::>); + let mut last_div = use_signal(|| None::>); + + let conversation_panels_nb = 3; + let conversation_panels = (0..conversation_panels_nb + 1).map(|i| { + if i == 0 { + rsx! { + div { + class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATION_PANELS_PANEL, + onmounted: move |cx| async move { + let data = cx.data(); + let _ = data.as_ref().scroll_to(ScrollBehavior::Smooth).await; + first_div.set(Some(data)); + }, + ChatPanel { name: format!("CHAT #{i}") }, + } + } + } else if i == conversation_panels_nb { + rsx! { + div { + class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATION_PANELS_PANEL, + onmounted: move |cx: Event| last_div.set(Some(cx.data())), + ChatPanel { name: format!("CHAT #{i}") }, + } + } + } else { + rsx! { + div { + class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATION_PANELS_PANEL, + ChatPanel { name: format!("CHAT #{i}") }, + } + } + } + }); + + rsx! { + style { {STYLE_SHEET} }, + + div { + class: ClassName::CONVERSATIONS_VIEW_BIG, + + div { + class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATIONS_PANEL, + + ConversationsComponent {}, + }, + + div { + class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATION_PANELS, + + onmounted: move |cx| async move { + let data = cx.data(); + carousel_div.set(Some(data)); + }, + + onscroll: move |_| { + async move { + if let (Some(carousel_div), Some(first_div), Some(last_div)) = ( + carousel_div.read().as_ref(), + first_div.read().as_ref(), + last_div.read().as_ref(), + ) { + on_carousel_scroll(carousel_div, first_div, last_div).await; + } + } + }, + + div { + class: ClassName::CONVERSATIONS_VIEW_HEAD, + } + + {conversation_panels} + + div { + class: ClassName::CONVERSATIONS_VIEW_TAIL, + } + + }, + } + } +} + +pub fn Conversations() -> Element { + let mut view_size = use_signal(|| None::<(f64, f64)>); + + // TODO: Make the layout reactive (on window resize) + let layout = { + move || { + if let Some((width, height)) = view_size.read().as_ref() { + let component_width = height * WIDGET_HEIGHT_RATIO * ASPECT_RATIO; + let breakpoint_width = component_width * 2_f64; + + if *width >= breakpoint_width { + return rsx! { LayoutBig {} }; + } + } + rsx! {LayoutSmall {}} + } + }(); + + rsx! { + style { {STYLE_SHEET} }, + + Wallpaper { + display_version: true + }, + + div { + class: ClassName::CONVERSATIONS_VIEW, + onmounted: move |cx| { + async move { + let data = cx.data(); + + if let Ok(client_rect) = data.get_client_rect().await { + view_size.set(Some((client_rect.size.width, client_rect.size.height))); + } + } + }, + + {layout} + } + } +} diff --git a/src/ui/layouts/conversations.scss b/src/ui/layouts/conversations.scss new file mode 100644 index 0000000..8844a62 --- /dev/null +++ b/src/ui/layouts/conversations.scss @@ -0,0 +1,136 @@ +@import "../_base.scss"; +@import "../components/_panel.scss"; + +.conversations-view-head { + height: 100%; + width: 1px; + + flex-shrink: 0; + scroll-snap-align: end; +} + +.conversations-view-tail { + height: 100%; + width: 1px; + + flex-shrink: 0; + scroll-snap-align: start; +} + +.conversations-view { + $height: 100vh; + $width: 100vw; + $conversations-panel-height: calc($height * 0.95); + $conversations-panel-width: calc($conversations-panel-height * $panel-aspect-ratio); + $gap: 1%; + $content-height: 95%; + $ratio: 2; + + height: 100%; + width: 100%; + + position: relative; + top: -100vh; + margin-bottom: -100vh; + + &__small { + scroll-snap-type: x mandatory; + + height: 100%; + width: 100%; + + display: flex; + flex-direction: row; + gap: $gap; + + justify-content: safe center; + align-items: safe center; + + overflow-x: scroll; + + &__conversations-panel { + height: $content-height; + width: 100%; + + flex-shrink: 0; + scroll-snap-align: center; + + display: flex; + align-items: center; + justify-content: center; + + &__inner { + height: 100%; + + // TODO: Is aspect-ratio the best criteria to defined that inner shall take all the available space ? + @media (max-aspect-ratio: $panel-aspect-ratio) { + width: 100%; + } + @media (min-aspect-ratio: $panel-aspect-ratio) { + aspect-ratio: $panel-aspect-ratio; + } + } + } + + &__panel { + flex-shrink: 0; + + height: $content-height; + width: 100%; + + display: flex; + align-items: center; + justify-content: center; + + scroll-snap-align: center; + + &__inner { + height: 100%; + width: calc(100% - (2 * $gap)); + } + } + } + + &__big { + height: 100%; + width: 100%; + width: calc(100% - (2 * $gap)); + + display: flex; + flex-direction: row; + align-items: safe center; + gap: $gap; + + margin: 0 $gap; + + &__conversations-panel { + height: $content-height; + aspect-ratio: $panel-aspect-ratio; + } + + &__conversation-panels { + height: $content-height; + + flex-grow: 1; + + display: flex; + flex-direction: row; + overflow-x: scroll; + + justify-content: safe center; + align-items: safe center; + scroll-snap-type: x mandatory; + + gap: $gap; + + &__panel { + flex-shrink: 0; + + height: 100%; + width: 100%; + + scroll-snap-align: center; + } + } + } +} diff --git a/src/ui/layouts/mod.rs b/src/ui/layouts/mod.rs index 8b4eddb..1d1f310 100644 --- a/src/ui/layouts/mod.rs +++ b/src/ui/layouts/mod.rs @@ -1 +1,2 @@ +pub(crate) mod conversations; pub(crate) mod login;