feat(web): way better fetch button

it changes and automatically refreshes!!! to achieve this a lot of
refactor happened...
This commit is contained in:
əlemi 2024-11-21 02:41:19 +01:00
parent bc9f55d58c
commit d8af116667
Signed by: alemi
GPG key ID: A4895B84D311642C
7 changed files with 117 additions and 51 deletions

View file

@ -387,6 +387,10 @@
span.json-text {
color: var(--text);
}
span.tab-active {
color: var(--accent);
font-weight: bold;
}
pre.striped {
background: repeating-linear-gradient(
135deg,

View file

@ -105,7 +105,7 @@ pub fn App() -> impl IntoView {
// refresh notifications
let (notifications, set_notifications) = create_signal(0);
let fetch_notifications = move || spawn_local(async move {
let actor_id = userid.get().unwrap_or_default();
let actor_id = userid.get_untracked().unwrap_or_default();
let notif_url = format!("{actor_id}/notifications");
match Http::fetch::<serde_json::Value>(&notif_url, auth).await {
Err(e) => tracing::error!("failed fetching notifications: {e}"),
@ -180,7 +180,6 @@ pub fn App() -> impl IntoView {
<Route path="objects/:id" view=ObjectView >
<Route path="" view=ObjectContext />
<Route path="replies" view=ObjectReplies />
<Route path="context" view=ObjectContext />
// <Route path="liked" view=ObjectLiked />
// <Route path="announced" view=ObjectAnnounced />
</Route>
@ -204,30 +203,52 @@ pub fn App() -> impl IntoView {
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum FeedRoute {
Home, Global, Server, Notifications, User, Replies, Context
}
#[component]
fn Scrollable() -> impl IntoView {
let location = use_location();
let feeds = use_context::<Feeds>().expect("missing feeds context");
let auth = use_context::<Auth>().expect("missing auth context");
let config = use_context::<Signal<crate::Config>>().expect("missing config context");
// TODO this is terrible!! omg maybe it should receive from context current timeline?? idk this
// is awful and i patched it another time instead of doing it properly...
// at least im going to provide a route enum to use in other places
let (route, set_route) = create_signal(FeedRoute::Home);
let relevant_timeline = Signal::derive(move || {
let path = location.pathname.get();
if path.contains("/web/home") {
set_route.set(FeedRoute::Home);
Some(feeds.home)
} else if path.contains("/web/global") {
set_route.set(FeedRoute::Global);
Some(feeds.global)
} else if path.contains("/web/local") {
set_route.set(FeedRoute::Server);
Some(feeds.server)
} else if path.starts_with("/web/notifications") {
set_route.set(FeedRoute::Notifications);
Some(feeds.notifications)
} else if path.starts_with("/web/actors") {
set_route.set(FeedRoute::User);
Some(feeds.user)
} else if path.starts_with("/web/objects") {
if matches!(path.split('/').nth(4), Some("replies")) {
set_route.set(FeedRoute::Replies);
Some(feeds.replies)
} else {
set_route.set(FeedRoute::Context);
Some(feeds.context)
}
} else {
None
}
});
provide_context(route);
provide_context(relevant_timeline);
let breadcrumb = Signal::derive(move || {
let path = location.pathname.get();
let mut path_iter = path.split('/').skip(2);

View file

@ -60,6 +60,7 @@ pub trait Cache {
fn lookup(&self, key: &str) -> Option<impl Deref<Target = LookupStatus<Self::Item>>>;
fn store(&self, key: &str, value: Self::Item) -> Option<Self::Item>;
fn invalidate(&self, key: &str);
fn get(&self, key: &str) -> Option<Self::Item> where Self::Item : Clone {
Some(self.lookup(key)?.deref().inner()?.clone())
@ -88,11 +89,16 @@ impl<T> Cache for DashmapCache<T> {
self.0.insert(key.to_string(), LookupStatus::Found(value))
.and_then(|x| if let LookupStatus::Found(x) = x { Some(x) } else { None } )
}
fn invalidate(&self, key: &str) {
self.0.remove(key);
}
}
impl DashmapCache<Object> {
pub async fn resolve(&self, key: &str, kind: UriClass, auth: Auth) -> Option<Object> {
let full_key = Uri::full(kind, key);
tracing::info!("resolving {key} -> {full_key}");
match self.get(&full_key) {
Some(x) => Some(x),
None => {

View file

@ -8,14 +8,12 @@ use crate::prelude::*;
pub fn ObjectContext() -> impl IntoView {
let feeds = use_context::<Feeds>().expect("missing feeds context");
let params = use_params::<IdParam>();
let id = Signal::derive(move || {
let id = params.get().ok()
.and_then(|x| x.id)
.unwrap_or_default();
let id = move || {
let id = params.with(|p| p.as_ref().ok().and_then(|x| x.id.as_ref()).cloned()).unwrap_or_default();
Uri::full(U::Object, &id)
});
};
let context_id = Signal::derive(move ||
cache::OBJECTS.get(&id.get())
cache::OBJECTS.get(&id())
.and_then(|x| x.context().id().ok())
.unwrap_or_default()
);
@ -27,7 +25,7 @@ pub fn ObjectContext() -> impl IntoView {
});
view! {
<div class="mr-1-r ml-1-r">
<Thread tl=feeds.context root=id.get() />
<Thread tl=feeds.context root=id() />
</div>
}
}

View file

@ -3,7 +3,7 @@ use std::sync::Arc;
use leptos::*;
use crate::{prelude::*, URL_SENSITIVE};
use apb::{target::Addressed, ActivityMut, Base, Collection, CollectionMut, Object, ObjectMut};
use apb::{target::Addressed, ActivityMut, Base, Collection, CollectionMut, Object, ObjectMut, Shortcuts};
#[component]
pub fn Object(object: crate::Object) -> impl IntoView {
@ -20,9 +20,9 @@ pub fn Object(object: crate::Object) -> impl IntoView {
.filter_map(|x| x.into_inner().ok()) // TODO maybe show links?
.map(|x| view! { <Attachment object=x sensitive=sensitive /> })
.collect_view();
let comments = object.replies().inner().and_then(|x| x.total_items()).unwrap_or_default();
let shares = object.shares().inner().and_then(|x| x.total_items()).unwrap_or_default();
let likes = object.likes().inner().and_then(|x| x.total_items()).unwrap_or_default();
let comments = object.replies_count().unwrap_or_default();
let shares = object.shares_count().unwrap_or_default();
let likes = object.likes_count().unwrap_or_default();
let already_liked = object.liked_by_me().unwrap_or(false);
let attachments_padding = if object.attachment().is_empty() {
@ -220,7 +220,7 @@ pub fn Summary(summary: Option<String>, children: Children) -> impl IntoView {
#[component]
pub fn LikeButton(
n: u64,
n: i32,
target: String,
liked: bool,
author: String,
@ -279,7 +279,7 @@ pub fn LikeButton(
}
#[component]
pub fn ReplyButton(n: u64, target: String) -> impl IntoView {
pub fn ReplyButton(n: i32, target: String) -> impl IntoView {
let reply = use_context::<ReplyControls>().expect("missing reply controls context");
let auth = use_context::<Auth>().expect("missing auth context");
let comments = if n > 0 {
@ -304,7 +304,7 @@ pub fn ReplyButton(n: u64, target: String) -> impl IntoView {
}
#[component]
pub fn RepostButton(n: u64, target: String) -> impl IntoView {
pub fn RepostButton(n: i32, target: String) -> impl IntoView {
let (count, set_count) = create_signal(n);
let (clicked, set_clicked) = create_signal(true);
let auth = use_context::<Auth>().expect("missing auth context");

View file

@ -8,7 +8,7 @@ pub fn ObjectReplies() -> impl IntoView {
let feeds = use_context::<Feeds>().expect("missing feeds context");
let params = use_params::<IdParam>();
let id = Signal::derive(move ||
params.get().ok().and_then(|x| x.id).unwrap_or_default()
params.with(|p| p.as_ref().ok().and_then(|x| x.id.as_ref()).cloned()).unwrap_or_default()
);
create_effect(move |_| {
let tl_url = format!("{}/replies/page", Uri::api(U::Object, &id.get(), false));

View file

@ -1,16 +1,23 @@
use ev::MouseEvent;
use leptos::*;
use leptos_router::*;
use crate::prelude::*;
use crate::{app::FeedRoute, prelude::*, Config};
use apb::{Base, Object};
#[component]
pub fn ObjectView() -> impl IntoView {
let params = use_params_map();
let matched_route = use_context::<ReadSignal<crate::app::FeedRoute>>().expect("missing route context");
let auth = use_context::<Auth>().expect("missing auth context");
let config = use_context::<Signal<Config>>().expect("missing config context");
let relevant_tl = use_context::<Signal<Option<Timeline>>>().expect("missing relevant timeline context");
let (loading, set_loading) = create_signal(false);
let id = Signal::derive(move || params.get().get("id").cloned().unwrap_or_default());
let object = create_local_resource(
move || params.get().get("id").cloned().unwrap_or_default(),
move |oid| async move {
move || (id.get(), loading.get()),
move |(oid, loading)| async move {
tracing::info!("rerunning fetcher");
let obj = cache::OBJECTS.resolve(&oid, U::Object, auth).await?;
if let Ok(author) = obj.attributed_to().id() {
cache::OBJECTS.resolve(&author, U::Actor, auth).await;
@ -26,6 +33,7 @@ pub fn ObjectView() -> impl IntoView {
}
);
view! {
{move || match object.get() {
None => view! { <Loader /> }.into_view(),
Some(None) => {
@ -34,31 +42,60 @@ pub fn ObjectView() -> impl IntoView {
view! { <p class="center"><code>loading failed</code><sup><small><a class="clean" href={uid} target="_blank">""</a></small></sup></p> }.into_view()
},
Some(Some(o)) => {
let object = o.clone();
let oid = o.id().unwrap_or_default();
let base = Uri::web(U::Object, &oid);
let api = Uri::api(U::Object, &oid, false);
view!{
<Object object=object />
<hr class="color ma-2" />
<code class="cw color center mt-1 mb-1 ml-3 mr-3">
<a class="clean" href=format!("{base}/context")><span class="emoji">"🕸️"</span>" "<b>context</b></a>" | "<a class="clean" href=format!("{base}/replies")><span class="emoji">"📫"</span>" "<b>replies</b></a>
{if auth.present() {
Some(view! {
" | "<a class="clean" href="#crawl" on:click=move |_| crawl(api.clone(), auth) ><span class="emoji">""</span></a>
})
} else { None }}
</code>
<Outlet />
}.into_view()
tracing::info!("redrawing object");
view! { <Object object=o.clone() /> }.into_view()
},
}}
<p>
<span class:tab-active=move || matches!(matched_route.get(), FeedRoute::Context)><a class="clean" href=format!("/web/objects/{}", id.get())><span class="emoji ml-2">"🕸️ "</span>"context"</a></span>
<span class:tab-active=move || matches!(matched_route.get(), FeedRoute::Replies)><a class="clean" href=format!("/web/objects/{}/replies", id.get())><span class="emoji ml-2">"📫 "</span>"replies"</a></span>
{move || if auth.present() {
if loading.get() {
Some(view! {
<span style="float: right">
"fetching "<span class="dots"></span>
</span>
})
} else {
Some(view! {
<span style="float: right">
<a
class="clean"
on:click=move |ev| fetch_cb(ev, set_loading, id.get(), auth, config, relevant_tl)
href="#"
>
<span class="emoji ml-2">""</span>"fetch"
</a>
</span>
})
}
} else {
None
}}
</p>
<hr class="color" />
{move || if object.get().is_some() {
tracing::info!("redrawing outlet");
Some(view! { <Outlet /> })
} else {
None
}}
}
}
fn crawl(base: String, auth: Auth) {
fn fetch_cb(ev: MouseEvent, set_loading: WriteSignal<bool>, oid: String, auth: Auth, config: Signal<Config>, relevant_tl: Signal<Option<Timeline>>) {
let api = Uri::api(U::Object, &oid, false);
ev.prevent_default();
set_loading.set(true);
spawn_local(async move {
if let Err(e) = Http::fetch::<serde_json::Value>(&format!("{base}/replies?fetch=true"), auth).await {
tracing::error!("failed crawling replies for {base}: {e}");
if let Err(e) = Http::fetch::<serde_json::Value>(&format!("{api}/replies?fetch=true"), auth).await {
tracing::error!("failed crawling replies for {oid}: {e}");
}
cache::OBJECTS.invalidate(&Uri::full(U::Object, &oid));
tracing::info!("invalidated {}", Uri::full(U::Object, &oid));
set_loading.set(false);
relevant_tl.get().inspect(|x| x.refresh(auth, config));
});
}