use std::{collections::HashSet, rc::Rc}; use base64::{engine::general_purpose, Engine as _}; use dioxus::prelude::*; use futures::join; use tracing::warn; use crate::{ domain::model::room::RoomId, ui::{ components::{ chat_panel::ChatPanel, conversations::Conversations as ConversationsComponent, wallpaper::Wallpaper, }, STORE, }, }; turf::style_sheet!("src/ui/layouts/conversations.scss"); include!(concat!(env!("OUT_DIR"), "/style_component_panel.rs")); include!(concat!(env!("OUT_DIR"), "/style_layout_conversations.rs")); use conversations::INNER_PANEL_HEIGHT_RATIO; use panel::ASPECT_RATIO; 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 first_div = use_signal(|| None::>); let mut last_div = use_signal(|| None::>); let mut carousel_div = use_signal(|| None::>); let displayed_room_ids = STORE.read().displayed_room_ids(); let mut conversation_panels = Vec::new(); let mut displayed_room_ids_it = displayed_room_ids.iter().peekable(); while let Some(room_id) = displayed_room_ids_it.next() { if let Some(room) = STORE.read().rooms().get(room_id) { let room = room.signal(); let room_name_repr = room.name().unwrap_or(room.id().to_string()); let inner = rsx! { div { class: ClassName::CONVERSATIONS_VIEW_SMALL_PANEL_INNER, ChatPanel { name: format!("CHAT {room_name_repr}") } } }; // If this is the last iteration let panel = if displayed_room_ids_it.peek().is_none() { 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} } } }; if let Ok(panel) = panel { conversation_panels.push(panel); } } else { warn!("No {} room found", room_id); } } // Add tail div to dynamic rendered conversation_panels avoids side effects on layout changes conversation_panels.push( rsx! { div { class: ClassName::CONVERSATIONS_VIEW_TAIL, } } .unwrap(), ); 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.iter()} } } } #[component] fn Tab(room_id: RoomId) -> Element { let rooms = STORE.read().rooms(); let room = rooms.get(&room_id).unwrap().signal(); let room_avatar = if let Some(content) = room.avatar() { let encoded = general_purpose::STANDARD.encode(content); rsx! { div { class: ClassName::TAB_AVATAR_IMAGE, div { class: ClassName::TAB_AVATAR_IMAGE, background_image: format!("url(data:image/jpeg;base64,{encoded})"), } } } } else { VNode::empty() }; rsx! { div { class: ClassName::TAB, {room_avatar} div { class: ClassName::TAB_NAME, {room.name()} } } } } #[component] fn TabsBar(room_ids: HashSet) -> Element { let tabs = room_ids .iter() .map(|room_id| rsx! { Tab { room_id: room_id.clone() }}); rsx! { div { class: ClassName::TABS_BAR, {tabs} } } } 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 displayed_room_ids = STORE.read().displayed_room_ids(); let mut conversation_panels = Vec::new(); let mut displayed_room_ids_it = displayed_room_ids.iter().peekable(); let mut is_first = true; while let Some(room_id) = displayed_room_ids_it.next() { if let Some(room) = STORE.read().rooms().get(room_id) { let room = room.signal(); let room_name_repr = format!("CHAT {}", room.name().unwrap_or(room.id().to_string())); let panel = if is_first { is_first = false; rsx! { div { class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATIONS_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: room_name_repr } } } } else if displayed_room_ids_it.peek().is_none() { rsx! { div { class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATIONS_PANELS_PANEL, onmounted: move |cx: Event| last_div.set(Some(cx.data())), ChatPanel { name: room_name_repr } } } } else { rsx! { div { class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATIONS_PANELS_PANEL, ChatPanel { name: room_name_repr } } } }; if let Ok(panel) = panel { conversation_panels.push(panel); } } else { warn!("No {} room found", room_id); } } // Add tail div to dynamic rendered conversation_panels avoids side effects on layout changes conversation_panels.push( rsx! { div { class: ClassName::CONVERSATIONS_VIEW_TAIL, } } .unwrap(), ); 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_CONVERSATIONS, div { class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATIONS_TABS_BAR, TabsBar { room_ids: displayed_room_ids} } div { class: ClassName::CONVERSATIONS_VIEW_BIG_CONVERSATIONS_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.iter()} } } } } } pub fn Conversations() -> Element { let mut use_big_layout = use_signal(|| false); rsx! { style { {STYLE_SHEET} } Wallpaper { display_version: true } div { class: ClassName::CONVERSATIONS_VIEW, onresize: move |cx| { let data = cx.data(); if let Ok(size) = data.get_border_box_size() { // Use LayoutBig if the layout can contain 2 panels side by side let component_width = size.height * INNER_PANEL_HEIGHT_RATIO * ASPECT_RATIO; let breakpoint_width = component_width * 2_f64; use_big_layout.set(size.width > breakpoint_width); } }, if use_big_layout() { LayoutBig {} } else { LayoutSmall {} } } } }