feat(web): infinite scroll (optional) in timelines

This commit is contained in:
əlemi 2024-05-23 05:52:37 +02:00
parent c2f4f24586
commit 9b64503c02
Signed by: alemi
GPG key ID: A4895B84D311642C
3 changed files with 64 additions and 26 deletions

View file

@ -2,6 +2,7 @@ use std::{collections::BTreeSet, pin::Pin, sync::Arc};
use apb::{Activity, ActivityMut, Base, Object}; use apb::{Activity, ActivityMut, Base, Object};
use leptos::*; 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::*; use crate::prelude::*;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -137,41 +138,67 @@ pub fn TimelineReplies(tl: Timeline, root: String) -> impl IntoView {
#[component] #[component]
pub fn TimelineFeed(tl: Timeline) -> impl IntoView { pub fn TimelineFeed(tl: Timeline) -> impl IntoView {
let auth = use_context::<Auth>().expect("missing auth context"); let auth = use_context::<Auth>().expect("missing auth context");
view! { let config = use_context::<Signal<crate::Config>>().expect("missing config context");
<For // double view height: preload when 1 screen away
each=move || tl.feed.get() let view_height = 2.0 * window()
key=|k| k.to_string() .inner_height()
children=move |id: String| { .map_or(500.0, |v| v.as_f64().unwrap_or_default());
match CACHE.get(&id) { let scroll_ref = create_node_ref();
Some(i) => view! { let UseElementSizeReturn { width: _w, height } = use_element_size(scroll_ref);
<Item item=i sep=true /> let (_x, scroll) = use_window_scroll();
}.into_view(), let scroll_debounced = signal_throttled(scroll, 500.0);
None => view! { let _auto_loader = create_local_resource(
<p><code>{id}</code>" "[<a href={uri}>go</a>]</p> move || (scroll_debounced.get(), height.get()),
<hr /> move |(s, h)| async move {
}.into_view(), 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! {
<div ref=scroll_ref>
<For
each=move || tl.feed.get()
key=|k| k.to_string()
children=move |id: String| {
match CACHE.get(&id) {
Some(i) => view! {
<Item item=i sep=true />
}.into_view(),
None => view! {
<p><code>{id}</code>" "[<a href={uri}>go</a>]</p>
<hr />
}.into_view(),
}
}
/ >
</div>
<div class="center mt-1 mb-1" class:hidden=tl.over > <div class="center mt-1 mb-1" class:hidden=tl.over >
<button type="button" <button type="button"
prop:disabled=tl.loading prop:disabled=tl.loading
on:click=move |_| { on:click=move |_| load_more(tl, auth)
spawn_local(async move { >
if let Err(e) = tl.more(auth).await { {move || if tl.loading.get() {
tracing::error!("error fetching more items for timeline: {e}"); view! { "loading"<span class="dots"></span> }.into_view()
} } else { "more".into_view() }}
}) </button>
}
>
{move || if tl.loading.get() {
view! { "loading"<span class="dots"></span> }.into_view()
} else { "more".into_view() }}
</button>
</div> </div>
} }
} }
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<serde_json::Value>, auth: Auth) -> Vec<String> { async fn process_activities(activities: Vec<serde_json::Value>, auth: Auth) -> Vec<String> {
let mut sub_tasks : Vec<Pin<Box<dyn futures::Future<Output = ()>>>> = Vec::new(); let mut sub_tasks : Vec<Pin<Box<dyn futures::Future<Output = ()>>>> = Vec::new();
let mut gonna_fetch = BTreeSet::new(); let mut gonna_fetch = BTreeSet::new();

View file

@ -11,6 +11,9 @@ pub struct Config {
#[serde_inline_default(true)] #[serde_inline_default(true)]
pub loop_videos: bool, pub loop_videos: bool,
#[serde_inline_default(true)]
pub infinite_scroll: bool,
#[serde_inline_default("#BF616A".to_string())] #[serde_inline_default("#BF616A".to_string())]
pub accent_color: String, pub accent_color: String,
} }

View file

@ -57,6 +57,14 @@ pub fn ConfigPage(setter: WriteSignal<crate::Config>) -> impl IntoView {
/> collapse content warnings /> collapse content warnings
</span> </span>
</p> </p>
<p>
<span title="new posts will be fetched automatically when scrolling down enough">
<input type="checkbox" class="mr-1"
prop:checked=get_cfg!(infinite_scroll)
on:input=set_cfg!(infinite_scroll)
/> infinite scroll
</span>
</p>
<p> <p>
accent color accent color
<input type="text" class="ma-1" <input type="text" class="ma-1"