diff --git a/web/index.html b/web/index.html index 250f77a..00b84c7 100644 --- a/web/index.html +++ b/web/index.html @@ -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, diff --git a/web/src/app.rs b/web/src/app.rs index f7813dc..6fe5fad 100644 --- a/web/src/app.rs +++ b/web/src/app.rs @@ -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::(¬if_url, auth).await { Err(e) => tracing::error!("failed fetching notifications: {e}"), @@ -180,7 +180,6 @@ pub fn App() -> impl IntoView { - // // @@ -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::().expect("missing feeds context"); let auth = use_context::().expect("missing auth context"); let config = use_context::>().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); diff --git a/web/src/lib.rs b/web/src/lib.rs index 0dfdea4..7696835 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -60,6 +60,7 @@ pub trait Cache { fn lookup(&self, key: &str) -> Option>>; fn store(&self, key: &str, value: Self::Item) -> Option; + fn invalidate(&self, key: &str); fn get(&self, key: &str) -> Option where Self::Item : Clone { Some(self.lookup(key)?.deref().inner()?.clone()) @@ -88,11 +89,16 @@ impl Cache for DashmapCache { 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 { pub async fn resolve(&self, key: &str, kind: UriClass, auth: Auth) -> Option { let full_key = Uri::full(kind, key); + tracing::info!("resolving {key} -> {full_key}"); match self.get(&full_key) { Some(x) => Some(x), None => { diff --git a/web/src/objects/context.rs b/web/src/objects/context.rs index 775ef0a..26ed2e6 100644 --- a/web/src/objects/context.rs +++ b/web/src/objects/context.rs @@ -8,14 +8,12 @@ use crate::prelude::*; pub fn ObjectContext() -> impl IntoView { let feeds = use_context::().expect("missing feeds context"); let params = use_params::(); - 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! {
- +
} } diff --git a/web/src/objects/item.rs b/web/src/objects/item.rs index 91f1ffa..12a1363 100644 --- a/web/src/objects/item.rs +++ b/web/src/objects/item.rs @@ -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! { }) .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, 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::().expect("missing reply controls context"); let auth = use_context::().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::().expect("missing auth context"); diff --git a/web/src/objects/replies.rs b/web/src/objects/replies.rs index 8e4838e..4cdb6bf 100644 --- a/web/src/objects/replies.rs +++ b/web/src/objects/replies.rs @@ -8,7 +8,7 @@ pub fn ObjectReplies() -> impl IntoView { let feeds = use_context::().expect("missing feeds context"); let params = use_params::(); 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)); diff --git a/web/src/objects/view.rs b/web/src/objects/view.rs index 9aa7db5..ea1f3f1 100644 --- a/web/src/objects/view.rs +++ b/web/src/objects/view.rs @@ -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::>().expect("missing route context"); let auth = use_context::().expect("missing auth context"); + let config = use_context::>().expect("missing config context"); + let relevant_tl = use_context::>>().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! { }.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! {

loading failed"↗"

}.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!{ - -
- - "🕸️"" "context" | ""📫"" "replies - {if auth.present() { - Some(view! { - " | ""↺" - }) - } else { None }} - - - }.into_view() - }, - }} + view! { + {move || match object.get() { + None => view! { }.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! {

loading failed"↗"

}.into_view() + }, + Some(Some(o)) => { + tracing::info!("redrawing object"); + view! { }.into_view() + }, + }} + +

+ "🕸️ ""context" + "📫 ""replies" + {move || if auth.present() { + if loading.get() { + Some(view! { + + "fetching " + + }) + } else { + Some(view! { + + + "↺ ""fetch" + + + }) + } + } else { + None + }} +

+
+ + {move || if object.get().is_some() { + tracing::info!("redrawing outlet"); + Some(view! { }) + } else { + None + }} + } } -fn crawl(base: String, auth: Auth) { +fn fetch_cb(ev: MouseEvent, set_loading: WriteSignal, oid: String, auth: Auth, config: Signal, relevant_tl: Signal>) { + 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::(&format!("{base}/replies?fetch=true"), auth).await { - tracing::error!("failed crawling replies for {base}: {e}"); + if let Err(e) = Http::fetch::(&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)); }); }