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 {
|
span.json-text {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
span.tab-active {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
pre.striped {
|
pre.striped {
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
|
|
|
@ -105,7 +105,7 @@ pub fn App() -> impl IntoView {
|
||||||
// refresh notifications
|
// refresh notifications
|
||||||
let (notifications, set_notifications) = create_signal(0);
|
let (notifications, set_notifications) = create_signal(0);
|
||||||
let fetch_notifications = move || spawn_local(async move {
|
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");
|
let notif_url = format!("{actor_id}/notifications");
|
||||||
match Http::fetch::<serde_json::Value>(¬if_url, auth).await {
|
match Http::fetch::<serde_json::Value>(¬if_url, auth).await {
|
||||||
Err(e) => tracing::error!("failed fetching notifications: {e}"),
|
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="objects/:id" view=ObjectView >
|
||||||
<Route path="" view=ObjectContext />
|
<Route path="" view=ObjectContext />
|
||||||
<Route path="replies" view=ObjectReplies />
|
<Route path="replies" view=ObjectReplies />
|
||||||
<Route path="context" view=ObjectContext />
|
|
||||||
// <Route path="liked" view=ObjectLiked />
|
// <Route path="liked" view=ObjectLiked />
|
||||||
// <Route path="announced" view=ObjectAnnounced />
|
// <Route path="announced" view=ObjectAnnounced />
|
||||||
</Route>
|
</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]
|
#[component]
|
||||||
fn Scrollable() -> impl IntoView {
|
fn Scrollable() -> impl IntoView {
|
||||||
let location = use_location();
|
let location = use_location();
|
||||||
let feeds = use_context::<Feeds>().expect("missing feeds context");
|
let feeds = use_context::<Feeds>().expect("missing feeds context");
|
||||||
let auth = use_context::<Auth>().expect("missing auth context");
|
let auth = use_context::<Auth>().expect("missing auth context");
|
||||||
let config = use_context::<Signal<crate::Config>>().expect("missing config 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 relevant_timeline = Signal::derive(move || {
|
||||||
let path = location.pathname.get();
|
let path = location.pathname.get();
|
||||||
if path.contains("/web/home") {
|
if path.contains("/web/home") {
|
||||||
|
set_route.set(FeedRoute::Home);
|
||||||
Some(feeds.home)
|
Some(feeds.home)
|
||||||
} else if path.contains("/web/global") {
|
} else if path.contains("/web/global") {
|
||||||
|
set_route.set(FeedRoute::Global);
|
||||||
Some(feeds.global)
|
Some(feeds.global)
|
||||||
} else if path.contains("/web/local") {
|
} else if path.contains("/web/local") {
|
||||||
|
set_route.set(FeedRoute::Server);
|
||||||
Some(feeds.server)
|
Some(feeds.server)
|
||||||
} else if path.starts_with("/web/notifications") {
|
} else if path.starts_with("/web/notifications") {
|
||||||
|
set_route.set(FeedRoute::Notifications);
|
||||||
Some(feeds.notifications)
|
Some(feeds.notifications)
|
||||||
} else if path.starts_with("/web/actors") {
|
} else if path.starts_with("/web/actors") {
|
||||||
|
set_route.set(FeedRoute::User);
|
||||||
Some(feeds.user)
|
Some(feeds.user)
|
||||||
} else if path.starts_with("/web/objects") {
|
} 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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
provide_context(route);
|
||||||
|
provide_context(relevant_timeline);
|
||||||
let breadcrumb = Signal::derive(move || {
|
let breadcrumb = Signal::derive(move || {
|
||||||
let path = location.pathname.get();
|
let path = location.pathname.get();
|
||||||
let mut path_iter = path.split('/').skip(2);
|
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 lookup(&self, key: &str) -> Option<impl Deref<Target = LookupStatus<Self::Item>>>;
|
||||||
fn store(&self, key: &str, value: Self::Item) -> Option<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 {
|
fn get(&self, key: &str) -> Option<Self::Item> where Self::Item : Clone {
|
||||||
Some(self.lookup(key)?.deref().inner()?.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))
|
self.0.insert(key.to_string(), LookupStatus::Found(value))
|
||||||
.and_then(|x| if let LookupStatus::Found(x) = x { Some(x) } else { None } )
|
.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> {
|
impl DashmapCache<Object> {
|
||||||
pub async fn resolve(&self, key: &str, kind: UriClass, auth: Auth) -> Option<Object> {
|
pub async fn resolve(&self, key: &str, kind: UriClass, auth: Auth) -> Option<Object> {
|
||||||
let full_key = Uri::full(kind, key);
|
let full_key = Uri::full(kind, key);
|
||||||
|
tracing::info!("resolving {key} -> {full_key}");
|
||||||
match self.get(&full_key) {
|
match self.get(&full_key) {
|
||||||
Some(x) => Some(x),
|
Some(x) => Some(x),
|
||||||
None => {
|
None => {
|
||||||
|
|
|
@ -8,14 +8,12 @@ use crate::prelude::*;
|
||||||
pub fn ObjectContext() -> impl IntoView {
|
pub fn ObjectContext() -> impl IntoView {
|
||||||
let feeds = use_context::<Feeds>().expect("missing feeds context");
|
let feeds = use_context::<Feeds>().expect("missing feeds context");
|
||||||
let params = use_params::<IdParam>();
|
let params = use_params::<IdParam>();
|
||||||
let id = Signal::derive(move || {
|
let id = move || {
|
||||||
let id = params.get().ok()
|
let id = params.with(|p| p.as_ref().ok().and_then(|x| x.id.as_ref()).cloned()).unwrap_or_default();
|
||||||
.and_then(|x| x.id)
|
|
||||||
.unwrap_or_default();
|
|
||||||
Uri::full(U::Object, &id)
|
Uri::full(U::Object, &id)
|
||||||
});
|
};
|
||||||
let context_id = Signal::derive(move ||
|
let context_id = Signal::derive(move ||
|
||||||
cache::OBJECTS.get(&id.get())
|
cache::OBJECTS.get(&id())
|
||||||
.and_then(|x| x.context().id().ok())
|
.and_then(|x| x.context().id().ok())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
);
|
);
|
||||||
|
@ -27,7 +25,7 @@ pub fn ObjectContext() -> impl IntoView {
|
||||||
});
|
});
|
||||||
view! {
|
view! {
|
||||||
<div class="mr-1-r ml-1-r">
|
<div class="mr-1-r ml-1-r">
|
||||||
<Thread tl=feeds.context root=id.get() />
|
<Thread tl=feeds.context root=id() />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use crate::{prelude::*, URL_SENSITIVE};
|
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]
|
#[component]
|
||||||
pub fn Object(object: crate::Object) -> impl IntoView {
|
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?
|
.filter_map(|x| x.into_inner().ok()) // TODO maybe show links?
|
||||||
.map(|x| view! { <Attachment object=x sensitive=sensitive /> })
|
.map(|x| view! { <Attachment object=x sensitive=sensitive /> })
|
||||||
.collect_view();
|
.collect_view();
|
||||||
let comments = object.replies().inner().and_then(|x| x.total_items()).unwrap_or_default();
|
let comments = object.replies_count().unwrap_or_default();
|
||||||
let shares = object.shares().inner().and_then(|x| x.total_items()).unwrap_or_default();
|
let shares = object.shares_count().unwrap_or_default();
|
||||||
let likes = object.likes().inner().and_then(|x| x.total_items()).unwrap_or_default();
|
let likes = object.likes_count().unwrap_or_default();
|
||||||
let already_liked = object.liked_by_me().unwrap_or(false);
|
let already_liked = object.liked_by_me().unwrap_or(false);
|
||||||
|
|
||||||
let attachments_padding = if object.attachment().is_empty() {
|
let attachments_padding = if object.attachment().is_empty() {
|
||||||
|
@ -220,7 +220,7 @@ pub fn Summary(summary: Option<String>, children: Children) -> impl IntoView {
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn LikeButton(
|
pub fn LikeButton(
|
||||||
n: u64,
|
n: i32,
|
||||||
target: String,
|
target: String,
|
||||||
liked: bool,
|
liked: bool,
|
||||||
author: String,
|
author: String,
|
||||||
|
@ -279,7 +279,7 @@ pub fn LikeButton(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[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 reply = use_context::<ReplyControls>().expect("missing reply controls context");
|
||||||
let auth = use_context::<Auth>().expect("missing auth context");
|
let auth = use_context::<Auth>().expect("missing auth context");
|
||||||
let comments = if n > 0 {
|
let comments = if n > 0 {
|
||||||
|
@ -304,7 +304,7 @@ pub fn ReplyButton(n: u64, target: String) -> impl IntoView {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[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 (count, set_count) = create_signal(n);
|
||||||
let (clicked, set_clicked) = create_signal(true);
|
let (clicked, set_clicked) = create_signal(true);
|
||||||
let auth = use_context::<Auth>().expect("missing auth context");
|
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 feeds = use_context::<Feeds>().expect("missing feeds context");
|
||||||
let params = use_params::<IdParam>();
|
let params = use_params::<IdParam>();
|
||||||
let id = Signal::derive(move ||
|
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 |_| {
|
create_effect(move |_| {
|
||||||
let tl_url = format!("{}/replies/page", Uri::api(U::Object, &id.get(), false));
|
let tl_url = format!("{}/replies/page", Uri::api(U::Object, &id.get(), false));
|
||||||
|
|
|
@ -1,16 +1,23 @@
|
||||||
|
use ev::MouseEvent;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::*;
|
use leptos_router::*;
|
||||||
use crate::prelude::*;
|
use crate::{app::FeedRoute, prelude::*, Config};
|
||||||
|
|
||||||
use apb::{Base, Object};
|
use apb::{Base, Object};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ObjectView() -> impl IntoView {
|
pub fn ObjectView() -> impl IntoView {
|
||||||
let params = use_params_map();
|
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 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(
|
let object = create_local_resource(
|
||||||
move || params.get().get("id").cloned().unwrap_or_default(),
|
move || (id.get(), loading.get()),
|
||||||
move |oid| async move {
|
move |(oid, loading)| async move {
|
||||||
|
tracing::info!("rerunning fetcher");
|
||||||
let obj = cache::OBJECTS.resolve(&oid, U::Object, auth).await?;
|
let obj = cache::OBJECTS.resolve(&oid, U::Object, auth).await?;
|
||||||
if let Ok(author) = obj.attributed_to().id() {
|
if let Ok(author) = obj.attributed_to().id() {
|
||||||
cache::OBJECTS.resolve(&author, U::Actor, auth).await;
|
cache::OBJECTS.resolve(&author, U::Actor, auth).await;
|
||||||
|
@ -26,39 +33,69 @@ pub fn ObjectView() -> impl IntoView {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
{move || match object.get() {
|
view! {
|
||||||
None => view! { <Loader /> }.into_view(),
|
{move || match object.get() {
|
||||||
Some(None) => {
|
None => view! { <Loader /> }.into_view(),
|
||||||
let raw_id = params.get().get("id").cloned().unwrap_or_default();
|
Some(None) => {
|
||||||
let uid = uriproxy::uri(URL_BASE, uriproxy::UriClass::Object, &raw_id);
|
let raw_id = params.get().get("id").cloned().unwrap_or_default();
|
||||||
view! { <p class="center"><code>loading failed</code><sup><small><a class="clean" href={uid} target="_blank">"↗"</a></small></sup></p> }.into_view()
|
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();
|
Some(Some(o)) => {
|
||||||
let oid = o.id().unwrap_or_default();
|
tracing::info!("redrawing object");
|
||||||
let base = Uri::web(U::Object, &oid);
|
view! { <Object object=o.clone() /> }.into_view()
|
||||||
let api = Uri::api(U::Object, &oid, false);
|
},
|
||||||
view!{
|
}}
|
||||||
<Object object=object />
|
|
||||||
<hr class="color ma-2" />
|
<p>
|
||||||
<code class="cw color center mt-1 mb-1 ml-3 mr-3">
|
<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>
|
||||||
<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>
|
<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>
|
||||||
{if auth.present() {
|
{move || if auth.present() {
|
||||||
Some(view! {
|
if loading.get() {
|
||||||
" | "<a class="clean" href="#crawl" on:click=move |_| crawl(api.clone(), auth) ><span class="emoji">"↺"</span></a>
|
Some(view! {
|
||||||
})
|
<span style="float: right">
|
||||||
} else { None }}
|
"fetching "<span class="dots"></span>
|
||||||
</code>
|
</span>
|
||||||
<Outlet />
|
})
|
||||||
}.into_view()
|
} 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 {
|
spawn_local(async move {
|
||||||
if let Err(e) = Http::fetch::<serde_json::Value>(&format!("{base}/replies?fetch=true"), auth).await {
|
if let Err(e) = Http::fetch::<serde_json::Value>(&format!("{api}/replies?fetch=true"), auth).await {
|
||||||
tracing::error!("failed crawling replies for {base}: {e}");
|
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