feat(web): way better fetch button
it changes and automatically refreshes!!! to achieve this a lot of refactor happened...
This commit is contained in:
parent
bc9f55d58c
commit
d8af116667
7 changed files with 117 additions and 51 deletions
|
@ -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,
|
||||
|
|
|
@ -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>(¬if_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") {
|
||||
Some(feeds.context)
|
||||
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);
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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,39 +33,69 @@ pub fn ObjectView() -> impl IntoView {
|
|||
}
|
||||
);
|
||||
|
||||
{move || match object.get() {
|
||||
None => view! { <Loader /> }.into_view(),
|
||||
Some(None) => {
|
||||
let raw_id = params.get().get("id").cloned().unwrap_or_default();
|
||||
let uid = uriproxy::uri(URL_BASE, uriproxy::UriClass::Object, &raw_id);
|
||||
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()
|
||||
},
|
||||
}}
|
||||
view! {
|
||||
{move || match object.get() {
|
||||
None => view! { <Loader /> }.into_view(),
|
||||
Some(None) => {
|
||||
let raw_id = params.get().get("id").cloned().unwrap_or_default();
|
||||
let uid = uriproxy::uri(URL_BASE, uriproxy::UriClass::Object, &raw_id);
|
||||
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)) => {
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue