use std::sync::Arc;
use apb::{Activity, Base, Collection, CollectionPage, Object};
use leptos::{either::Either, prelude::*};
use uriproxy::UriClass;
use crate::{Auth, Cache};
#[component]
pub fn Loadable<El, V>(
base: String,
element: El,
#[prop(default = UriClass::Activity)] convert: UriClass,
#[prop(default = true)] preload: bool,
#[prop(default = false)] replies: bool,
#[prop(optional)] thread: Option<String>,
) -> impl IntoView
where
El: Send + Sync + Fn(crate::Doc) -> V + 'static,
V: IntoView + 'static
{
let config = use_context::<Signal<crate::Config>>().expect("missing config context");
let auth = use_context::<crate::Auth>().expect("missing auth context");
let fun = Arc::new(element);
let (older_next, older_items) = crate::cache::TIMELINES.get(&base)
.unwrap_or((Some(base.clone()), vec![]));
let (next, set_next) = signal(older_next);
let (items, set_items) = signal(older_items);
let (loading, set_loading) = signal(false);
let load = move |key: String| {
if loading.get_untracked() { return }
set_loading.set(true);
leptos::task::spawn_local(async move {
let Some(mut url) = next.get_untracked() else {
set_loading.set(false);
return
};
if !replies && !config.get_untracked().filters.replies {
if url.contains('?') {
url += "&replies=false";
} else {
url += "?replies=false";
}
}
let object = match crate::Http::fetch::<serde_json::Value>(&url, auth).await {
Ok(x) => x,
Err(e) => {
tracing::error!("could not fetch items ({url}): {e} -- {e:?}");
set_next.set(None);
set_loading.set(false);
return;
},
};
let new_next = object.next().id().ok();
set_next.set(new_next.clone());
let store = process_activities(
object,
items.get_untracked(),
preload,
convert,
auth
).await;
crate::cache::TIMELINES.store(&key, (new_next, store.clone()));
set_items.set(store);
set_loading.set(false);
})
};
let auto_scroll = use_context::<Signal<bool>>().expect("missing auto-scroll signal");
let _base = base.clone();
let _ = Effect::watch(
move || auto_scroll.get(),
move |at_end, _, _| if *at_end && config.get_untracked().infinite_scroll {
load(_base.clone())
},
false,
);
let reload = use_context::<ReadSignal<()>>().expect("missing reload signal");
let _base = base.clone();
let _ = Effect::watch(
move || reload.get(),
move |_, _, _| {
set_items.set(vec![]);
set_next.set(Some(_base.clone()));
crate::cache::TIMELINES.invalidate(&_base);
load(_base.clone());
},
false
);
if items.get_untracked().is_empty() {
load(base.clone());
}
let _base = base.clone();
view! {
<div>
{if let Some(root) = thread {
Either::Left(view! { <FeedRecursive items=items root=root element=fun /> })
} else {
Either::Right(view! { <FeedLinear items=items element=fun /> })
}}
{move || if loading.get() {
Some(Either::Left(view! {
<div class="center mt-1 mb-1" >
<button type="button" disabled>"loading "<span class="dots"></span></button>
</div>
}))
} else if next.with(|x| x.is_some()) {
let _base = base.clone();
Some(Either::Right(view! {
<div class="center mt-1 mb-1" >
<button type="button" on:click=move |_| load(_base.clone()) >"load more"</button>
</div>
}))
} else {
None
}}
</div>
}
}
#[component]
fn FeedLinear<El, V>(items: ReadSignal<Vec<String>>, element: Arc<El>) -> impl IntoView
where
El: Send + Sync + Fn(crate::Doc) -> V + 'static,
V: IntoView + 'static
{
view! {
<For
each=move || items.get()
key=|id: &String| id.clone()
children=move |id: String| match crate::cache::OBJECTS.get(&id) {
Some(obj) => Either::Left(element(obj)),
None => Either::Right(view!{ <p><code class="center color cw">{id}</code></p> }),
}
/>
}
}
#[component]
fn FeedRecursive<El, V>(items: ReadSignal<Vec<String>>, root: String, element: Arc<El>) -> impl IntoView
where
El: Send + Sync + Fn(crate::Doc) -> V + 'static,
V: IntoView + 'static
{
let root_values = move || items.get()
.into_iter()
.filter_map(|x| {
let document = crate::cache::OBJECTS.get(&x)?;
let (oid, reply) = match document.object_type().ok()? {
apb::ObjectType::Activity(apb::ActivityType::Create) => {
let object = crate::cache::OBJECTS.get(&document.object().id().ok()?)?;
(object.id().ok()?, object.in_reply_to().id().ok()?)
},
apb::ObjectType::Note => (document.id().ok()?, document.in_reply_to().id().ok()?),
_ => (document.id().ok()?, document.object().id().ok()?),
};
if reply == root {
Some((oid, document))
} else {
None
}
})
.collect::<Vec<(String, crate::Doc)>>();
view! {
<For
each=root_values
key=|(id, _obj)| id.clone()
children=move |(id, obj)|
view! {
<details class="thread context depth-r" open>
<summary></summary>
{element(obj)}
<div class="depth-r">
<FeedRecursive items=items root=id element=element.clone() />
</div>
</details>
}
/ >
}.into_any()
}
pub async fn process_activities(
object: serde_json::Value,
mut store: Vec<String>,
preload: bool,
convert: UriClass,
auth: Auth,
) -> Vec<String> {
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::from_iter(store.clone());
let mut sub_tasks = Vec::new();
for node in object.ordered_items().flat() {
let mut added_something = false;
if let Ok(id) = node.id() {
added_something = true;
if !seen.contains(&id) {
seen.insert(id.clone());
store.push(id.clone());
}
if preload {
sub_tasks.push(crate::cache::OBJECTS.preload(id, convert, auth));
}
}
if let Ok(doc) = node.into_inner() {
if let Ok(sub_doc) = doc.object().into_inner() {
if let Ok(sub_id) = sub_doc.id() {
if !added_something && !seen.contains(&sub_id) {
seen.insert(sub_id.clone());
store.push(sub_id.clone());
}
if preload {
sub_tasks.push(crate::cache::OBJECTS.preload(sub_id, UriClass::Object, auth));
}
}
}
crate::cache::OBJECTS.include(Arc::new(doc));
};
}
futures::future::join_all(sub_tasks).await;
store
}