use leptos::prelude::*; use shared_types::message::MessageRecord; use std::sync::Arc; use wasm_bindgen::prelude::*; use web_sys::IntersectionObserver; use leptos::html; const GROUP_WINDOW_MS: i64 = 5 * 60 * 1000; fn group_messages(messages: Vec) -> Vec> { let mut groups: Vec> = Vec::new(); for msg in messages { if let Some(last_group) = groups.last_mut() { let same_user = last_group.first() .map(|m| m.user_id == msg.user_id) .unwrap_or(false); let same_window = last_group.last() .map(|m| (m.created_at - msg.created_at).abs() < GROUP_WINDOW_MS) .unwrap_or(false); if same_user && same_window { last_group.push(msg); continue; } } groups.push(vec![msg]); } groups } #[component] pub fn MessageFeed( messages: Vec, #[prop(optional)] empty_text: &'static str, #[prop(optional)] loading: bool, #[prop(optional)] has_more: bool, #[prop(optional)] loading_more: bool, #[prop(optional)] on_load_more: Option>, on_reanalyze: Arc, ) -> impl IntoView { let sentinel_ref = create_node_ref::(); let (intersecting, set_intersecting) = create_signal(false); create_effect(move |_| { let _ = intersecting.get(); // track signal if let Some(node) = sentinel_ref.get() { let on_load_more = on_load_more.clone(); let cb = Closure::)>::new(move |entries: Vec| { for entry in entries { if let Some(entry) = entry.dyn_ref::() { if entry.is_intersecting() { if let Some(ref cb) = on_load_more { cb(); } } } } }); let observer = IntersectionObserver::new(cb.as_ref().unchecked_ref()) .expect("IntersectionObserver failed"); observer.observe(&node); on_cleanup(move || { observer.disconnect(); }); // Keep closure alive cb.forget(); } }); // Loading state if loading { return view! {
{std::iter::repeat_with(|| { use super::message_card::MessageCardSkeleton; view! { } }).take(3).collect::>()}
}.into_any(); } if messages.is_empty() { return view! {
{if empty_text.is_empty() { "No messages" } else { empty_text }}
}.into_any(); } let groups = group_messages(messages); let has_more_val = has_more; let loading_more_val = loading_more; view! {
{groups.into_iter().map(|group| { let cb = on_reanalyze.clone(); view! { } }).collect::>()} {/* Infinite scroll sentinel */} {has_more_val.then(|| { view! {
{loading_more_val.then(|| { use super::message_card::MessageCardSkeleton; view! { } })}
} })}
}.into_any() } #[component] fn MessageCardGroup( messages: Vec, on_reanalyze: Arc, ) -> impl IntoView { use super::message_card::MessageCard; view! { } }