✨ Add a first Conversations layout
This commit is contained in:
252
src/ui/layouts/conversations.rs
Normal file
252
src/ui/layouts/conversations.rs
Normal file
@@ -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<MountedData>,
|
||||||
|
first_div: &Rc<MountedData>,
|
||||||
|
last_div: &Rc<MountedData>,
|
||||||
|
) {
|
||||||
|
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::<Rc<MountedData>>);
|
||||||
|
let mut first_div = use_signal(|| None::<Rc<MountedData>>);
|
||||||
|
let mut last_div = use_signal(|| None::<Rc<MountedData>>);
|
||||||
|
|
||||||
|
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<MountedData>| 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::<Rc<MountedData>>);
|
||||||
|
let mut first_div = use_signal(|| None::<Rc<MountedData>>);
|
||||||
|
let mut last_div = use_signal(|| None::<Rc<MountedData>>);
|
||||||
|
|
||||||
|
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<MountedData>| 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}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
136
src/ui/layouts/conversations.scss
Normal file
136
src/ui/layouts/conversations.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1 +1,2 @@
|
|||||||
|
pub(crate) mod conversations;
|
||||||
pub(crate) mod login;
|
pub(crate) mod login;
|
||||||
|
Reference in New Issue
Block a user