diff --git a/web/src/components/timeline.rs b/web/src/components/timeline.rs index 21062b6..8fee171 100644 --- a/web/src/components/timeline.rs +++ b/web/src/components/timeline.rs @@ -2,6 +2,7 @@ use std::{collections::BTreeSet, pin::Pin, sync::Arc}; use apb::{Activity, ActivityMut, Base, Object}; use leptos::*; +use leptos_use::{signal_debounced, signal_throttled, use_display_media, use_document_visibility, use_element_size, use_infinite_scroll_with_options, use_scroll, use_scroll_with_options, use_window, use_window_scroll, UseDisplayMediaReturn, UseElementSizeReturn, UseInfiniteScrollOptions, UseScrollOptions, UseScrollReturn}; use crate::prelude::*; #[derive(Debug, Clone, Copy)] @@ -137,41 +138,67 @@ pub fn TimelineReplies(tl: Timeline, root: String) -> impl IntoView { #[component] pub fn TimelineFeed(tl: Timeline) -> impl IntoView { let auth = use_context::().expect("missing auth context"); - view! { - view! { - - }.into_view(), - None => view! { -

{id}" "[go]

-
- }.into_view(), + let config = use_context::>().expect("missing config context"); + // double view height: preload when 1 screen away + let view_height = 2.0 * window() + .inner_height() + .map_or(500.0, |v| v.as_f64().unwrap_or_default()); + let scroll_ref = create_node_ref(); + let UseElementSizeReturn { width: _w, height } = use_element_size(scroll_ref); + let (_x, scroll) = use_window_scroll(); + let scroll_debounced = signal_throttled(scroll, 500.0); + let _auto_loader = create_local_resource( + move || (scroll_debounced.get(), height.get()), + move |(s, h)| async move { + if !config.get().infinite_scroll { return } + if s > 0.0 && h - s < view_height && !tl.loading.get() { + if let Err(e) = tl.more(auth).await { + tracing::error!("auto load failed: {e}"); } } - / > + }, + ); + view! { +
+ view! { + + }.into_view(), + None => view! { +

{id}" "[go]

+
+ }.into_view(), + } + } + / > +
+ on:click=move |_| load_more(tl, auth) + > + {move || if tl.loading.get() { + view! { "loading" }.into_view() + } else { "more".into_view() }} +
} } +fn load_more(tl: Timeline, auth: Auth) { + if !tl.loading.get() { + spawn_local(async move { + if let Err(e) = tl.more(auth).await { + tracing::error!("error fetching more items for timeline: {e}"); + } + }) + } +} + async fn process_activities(activities: Vec, auth: Auth) -> Vec { let mut sub_tasks : Vec>>> = Vec::new(); let mut gonna_fetch = BTreeSet::new(); diff --git a/web/src/config.rs b/web/src/config.rs index c14ff59..1fcb389 100644 --- a/web/src/config.rs +++ b/web/src/config.rs @@ -11,6 +11,9 @@ pub struct Config { #[serde_inline_default(true)] pub loop_videos: bool, + #[serde_inline_default(true)] + pub infinite_scroll: bool, + #[serde_inline_default("#BF616A".to_string())] pub accent_color: String, } diff --git a/web/src/page/config.rs b/web/src/page/config.rs index 4a06fa2..9a12b57 100644 --- a/web/src/page/config.rs +++ b/web/src/page/config.rs @@ -57,6 +57,14 @@ pub fn ConfigPage(setter: WriteSignal) -> impl IntoView { /> collapse content warnings

+

+ + infinite scroll + +

accent color