Skip to main content

upub_web/objects/
view.rs

1use leptos::{either::Either, ev::MouseEvent};
2use leptos::prelude::*;
3use leptos_router::components::Outlet;
4use leptos_router::hooks::use_params_map;
5use crate::{app::FeedRoute, prelude::*};
6
7use apb::Object;
8
9#[component]
10pub fn ObjectView() -> impl IntoView {
11	let params = use_params_map();
12	let matched_route = use_context::<ReadSignal<crate::app::FeedRoute>>().expect("missing route context");
13	let auth = use_context::<Auth>().expect("missing auth context");
14	let refresh = use_context::<WriteSignal<()>>().expect("missing refresh context");
15	let (loading, set_loading) = signal(false);
16	let id = Signal::derive(move || params.get().get("id").unwrap_or_default());
17	let object = LocalResource::new(
18		move || {
19			let (oid, _loading) = (id.get(), loading.get());
20			async move {
21				tracing::info!("rerunning fetcher");
22				let obj = cache::OBJECTS.fetch(&oid, U::Object, auth).await?;
23
24				// TODO these two can be parallelized
25				if let Ok(author) = obj.attributed_to().id() {
26					cache::OBJECTS.fetch(&author, U::Actor, auth).await;
27				}
28				if let Ok(quote) = obj.quote_url().id() {
29					cache::OBJECTS.fetch(&quote, U::Object, auth).await;
30				}
31
32				Some(obj)
33
34				// if let Ok(ctx) = obj.context().id() {
35				// 	let tl_url = format!("{}/context/page", Uri::api(U::Object, ctx, false));
36				// 	if !feeds.context.next.get_untracked().starts_with(&tl_url) {
37				// 		feeds.context.reset(Some(tl_url));
38				// 	}
39				// }
40			}
41		}
42	);
43
44	// TODO since updating dependencies, this tries to get redrawn while it gets disposed, causing
45	//      panics. honestly this makes no sense: it's probably a race condition because i've done
46	//      something funky, but nonetheless i can't figure out what exactly right away. so we use
47	//      .try_get() instead, which makes this nested option hell, but allows us to catch the
48	//      "already disposed" case
49	//    + i think it may be due to the <Loadable> wrapper
50	view! {
51		{move || match object.try_get() {
52			None => ().into_any(), // already disposed
53			Some(None) => view! { <Loader /> }.into_any(), // still loading
54			Some(Some(None)) => { // error loading
55				let raw_id = params.get().get("id").unwrap_or_default();
56				let uid =  uriproxy::uri(URL_BASE, uriproxy::UriClass::Object, &raw_id);
57				view! { <p class="center"><code>loading failed</code><sup><small><a class="clean" href={uid} target="_blank">"↗"</a></small></sup></p> }.into_any()
58			},
59			Some(Some(Some(o))) => { // loaded ok
60				tracing::info!("redrawing object");
61				view! { <Object object=o.clone() /> }.into_any()
62			},
63		}}
64
65		<p>
66			<span class:tab-active=move || matches!(matched_route.get(), FeedRoute::Context)><a class="clean" href=move || format!("/web/objects/{}", id.get())><span class="emoji ml-2">"🕸️"</span><span class:hidden-on-mobile=move || !matches!(matched_route.get(), FeedRoute::Context)>" context"</span></a></span>
67			<span class:tab-active=move || matches!(matched_route.get(), FeedRoute::Replies)><a class="clean" href=move || format!("/web/objects/{}/replies", id.get())><span class="emoji ml-2">"📫"</span><span class:hidden-on-mobile=move || !matches!(matched_route.get(), FeedRoute::Replies)>" replies"</span></a></span>
68			<span class:tab-active=move || matches!(matched_route.get(), FeedRoute::ObjectLikes)><a class="clean" href=move || format!("/web/objects/{}/likes", id.get())><span class="emoji ml-2">"⭐"</span><span class:hidden-on-mobile=move || !matches!(matched_route.get(), FeedRoute::ObjectLikes)>" likes"</span></a></span>
69			{move || if auth.present() {
70				if loading.get() {
71					Some(Either::Left(view! {
72						<span style="float: right">
73							"fetching "<span class="dots"></span>
74						</span>
75					}))
76				} else {
77					Some(Either::Right(view! {
78						<span style="float: right">
79							<a
80								class="clean"
81								on:click=move |ev| fetch_cb(ev, set_loading, id.get(), auth, refresh)
82								href="#"
83							>
84								<span class="emoji ml-2">"↺ "</span>"fetch"
85							</a>
86						</span>
87					}))
88				}
89			} else {
90				None
91			}}
92		</p>
93		<hr class="color" />
94
95		{move || if object.try_get().is_some_and(|x| x.is_some()) {
96			tracing::info!("redrawing outlet");
97			Some(view! { <Outlet /> })
98		} else {
99			None
100		}}
101	}
102}
103
104fn fetch_cb(ev: MouseEvent, set_loading: WriteSignal<bool>, oid: String, auth: Auth, refresh: WriteSignal<()>) {
105	let api = Uri::api(U::Object, &oid, false);
106	ev.prevent_default();
107	set_loading.set(true);
108	leptos::task::spawn_local(async move {
109		if let Err(e) = Http::fetch::<serde_json::Value>(&format!("{api}/replies?fetch=true"), auth).await {
110			tracing::error!("failed crawling replies for {oid}: {e}");
111		}
112		cache::OBJECTS.invalidate(&Uri::full(U::Object, &oid));
113		tracing::info!("invalidated {}", Uri::full(U::Object, &oid));
114		set_loading.set(false);
115		refresh.set(());
116	});
117}