From abed664f0a88a0c48157c8759a3365e3be51e11c Mon Sep 17 00:00:00 2001 From: alemi Date: Sun, 21 Apr 2024 18:56:25 +0200 Subject: [PATCH] feat(web): better timeline, idk cant think read the diff bad day --- web/src/auth.rs | 2 +- web/src/components/activity.rs | 11 +++------ web/src/components/object.rs | 15 ++++++------ web/src/components/timeline.rs | 43 ++++++++++++++++++++++++-------- web/src/components/user.rs | 12 +++------ web/src/lib.rs | 45 +++++++++++++++++++++++++++++++--- web/src/page.rs | 19 +++++++------- web/src/prelude.rs | 2 +- 8 files changed, 102 insertions(+), 47 deletions(-) diff --git a/web/src/auth.rs b/web/src/auth.rs index 1b8969a8..2803d688 100644 --- a/web/src/auth.rs +++ b/web/src/auth.rs @@ -23,7 +23,7 @@ pub fn LoginBox( view! {
- "hi "{move || username.get().unwrap_or_default() } + "hi "{move || username.get().unwrap_or_default() } impl IntoView { let object_id = activity.object().id().unwrap_or_default(); - let object = CACHE.get(&object_id).unwrap_or(serde_json::Value::String(object_id.clone())); - let addressed = activity.addressed(); let actor_id = activity.actor().id().unwrap_or_default(); - let actor = match CACHE.get(&actor_id) { - Some(a) => a, - None => serde_json::Value::String(actor_id.clone()), - }; + let actor = CACHE.get_or(&actor_id, serde_json::Value::String(actor_id.clone())); let avatar = actor.icon().get().map(|x| x.url().id().unwrap_or_default()).unwrap_or_default(); let username = actor.preferred_username().unwrap_or_default().to_string(); let domain = actor.id().unwrap_or_default().replace("https://", "").split('/').next().unwrap_or_default().to_string(); @@ -24,13 +19,13 @@ pub fn ActivityLine(activity: serde_json::Value) -> impl IntoView {
- + {username}@{domain} {kind.as_ref().to_string()} - + diff --git a/web/src/components/object.rs b/web/src/components/object.rs index 0f0cab48..cf275fc2 100644 --- a/web/src/components/object.rs +++ b/web/src/components/object.rs @@ -1,7 +1,7 @@ use leptos::*; use crate::prelude::*; -use apb::{target::Addressed, Activity, Actor, Base, Object}; +use apb::{target::Addressed, Base, Object}; #[component] @@ -10,9 +10,8 @@ pub fn Object(object: serde_json::Value) -> impl IntoView { let in_reply_to = object.in_reply_to().id().unwrap_or_default(); let summary = object.summary().unwrap_or_default().to_string(); let content = dissolve::strip_html_tags(object.content().unwrap_or_default()); - let date = object.published(); let author_id = object.attributed_to().id().unwrap_or_default(); - let author = CACHE.get(&author_id).unwrap_or(serde_json::Value::String(author_id.clone())); + let author = CACHE.get_or(&author_id, serde_json::Value::String(author_id.clone())); view! {
@@ -20,7 +19,7 @@ pub fn Object(object: serde_json::Value) -> impl IntoView { Some(view! { }) @@ -38,7 +37,7 @@ pub fn Object(object: serde_json::Value) -> impl IntoView { } - +
- "in reply to "{Uri::pretty(&in_reply_to)} + "in reply to "{Uri::pretty(&in_reply_to)}
@@ -52,15 +51,17 @@ pub fn Object(object: serde_json::Value) -> impl IntoView { } #[component] -pub fn ObjectInline(object: serde_json::Value, author: serde_json::Value) -> impl IntoView { +pub fn ObjectInline(object: serde_json::Value) -> impl IntoView { let summary = object.summary().unwrap_or_default().to_string(); let content = dissolve::strip_html_tags(object.content().unwrap_or_default()); + let author_id = object.attributed_to().id().unwrap_or_default(); + let author = CACHE.get_or(&author_id, serde_json::Value::String(author_id.clone())); view! {
- + diff --git a/web/src/components/timeline.rs b/web/src/components/timeline.rs index 0efb05e3..c60d7483 100644 --- a/web/src/components/timeline.rs +++ b/web/src/components/timeline.rs @@ -1,6 +1,6 @@ -use std::collections::BTreeSet; +use std::{collections::BTreeSet, pin::Pin}; -use apb::{Activity, Base}; +use apb::{Activity, Base, Object}; use leptos::*; use crate::prelude::*; @@ -55,10 +55,10 @@ pub fn TimelineFeed(tl: Timeline) -> impl IntoView { match CACHE.get(&id) { Some(item) => match item.base_type() { Some(apb::BaseType::Object(apb::ObjectType::Activity(_))) => { - let author_id = item.actor().id().unwrap_or_default(); - let author = CACHE.get(&author_id).unwrap_or(serde_json::Value::String(author_id.clone())); let object_id = item.object().id().unwrap_or_default(); - let object = CACHE.get(&object_id).map(|obj| view! { }); + let object = CACHE.get(&object_id).map(|obj| { + view! { } + }); view! { {object} @@ -66,7 +66,7 @@ pub fn TimelineFeed(tl: Timeline) -> impl IntoView { }.into_view() }, Some(apb::BaseType::Object(apb::ObjectType::Note)) => view! { - +
}.into_view(), _ => view! {

type not implemented


}.into_view(), @@ -96,21 +96,28 @@ async fn process_activities( auth: Signal>, ) -> Vec { use apb::ActivityMut; - let mut sub_tasks = Vec::new(); + let mut sub_tasks : Vec>>> = Vec::new(); let mut gonna_fetch = BTreeSet::new(); + let mut actors_seen = BTreeSet::new(); let mut out = Vec::new(); for activity in activities { // save embedded object if present if let Some(object) = activity.object().get() { + // also fetch actor attributed to + if let Some(attributed_to) = object.attributed_to().id() { + actors_seen.insert(attributed_to); + } if let Some(object_uri) = object.id() { CACHE.put(object_uri.to_string(), object.clone()); + } else { + tracing::warn!("embedded object without id: {object:?}"); } } else { // try fetching it if let Some(object_id) = activity.object().id() { if !gonna_fetch.contains(&object_id) { gonna_fetch.insert(object_id.clone()); - sub_tasks.push(fetch_and_update("objects", object_id, auth)); + sub_tasks.push(Box::pin(fetch_and_update_with_user(FetchKind::Object, object_id, auth))); } } } @@ -128,20 +135,36 @@ async fn process_activities( if let Some(uid) = activity.actor().id() { if CACHE.get(&uid).is_none() && !gonna_fetch.contains(&uid) { gonna_fetch.insert(uid.clone()); - sub_tasks.push(fetch_and_update("users", uid, auth)); + sub_tasks.push(Box::pin(fetch_and_update(FetchKind::User, uid, auth))); } } } + for user in actors_seen { + sub_tasks.push(Box::pin(fetch_and_update(FetchKind::User, user, auth))); + } + futures::future::join_all(sub_tasks).await; out } -async fn fetch_and_update(kind: &'static str, id: String, auth: Signal>) { +async fn fetch_and_update(kind: FetchKind, id: String, auth: Signal>) { match Http::fetch(&Uri::api(kind, &id, false), auth).await { Ok(data) => CACHE.put(id, data), Err(e) => console_warn(&format!("could not fetch '{id}': {e}")), } } +async fn fetch_and_update_with_user(kind: FetchKind, id: String, auth: Signal>) { + fetch_and_update(kind.clone(), id.clone(), auth).await; + if let Some(obj) = CACHE.get(&id) { + if let Some(actor_id) = match kind { + FetchKind::Object => obj.attributed_to().id(), + FetchKind::Activity => obj.actor().id(), + FetchKind::User | FetchKind::Context => None, + } { + fetch_and_update(FetchKind::User, actor_id, auth).await; + } + } +} diff --git a/web/src/components/user.rs b/web/src/components/user.rs index 3fdef69b..60fe8997 100644 --- a/web/src/components/user.rs +++ b/web/src/components/user.rs @@ -1,22 +1,18 @@ use leptos::*; use crate::prelude::*; -use apb::{target::Addressed, Activity, Actor, Base, Object}; +use apb::{Actor, Base, Object}; #[component] -pub fn ActorBanner( - object: serde_json::Value, - #[prop(optional)] - tiny: bool -) -> impl IntoView { +pub fn ActorBanner(object: serde_json::Value) -> impl IntoView { match object { serde_json::Value::String(id) => view! { - + }, serde_json::Value::Object(_) => { let uid = object.id().unwrap_or_default().to_string(); - let uri = Uri::web("users", &uid); + let uri = Uri::web(FetchKind::User, &uid); let avatar_url = object.icon().get().map(|x| x.url().id().unwrap_or_default()).unwrap_or_default(); let display_name = object.name().unwrap_or_default().to_string(); let username = object.preferred_username().unwrap_or_default().to_string(); diff --git a/web/src/lib.rs b/web/src/lib.rs index cdcdc3c9..4bb03e6b 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -28,12 +28,48 @@ impl ObjectCache { self.0.get(k).map(|x| x.clone()) } + pub fn get_or(&self, k: &str, or: serde_json::Value) -> serde_json::Value { + self.get(k).unwrap_or(or) + } + pub fn put(&self, k: String, v: serde_json::Value) { self.0.insert(k, v); } + + pub async fn fetch(&self, k: &str, kind: FetchKind) -> reqwest::Result { + match self.get(k) { + Some(x) => Ok(x), + None => { + let obj = reqwest::get(Uri::api(kind, k, true)) + .await? + .json::() + .await?; + self.put(k.to_string(), obj); + Ok(self.get(k).expect("not found in cache after insertion")) + } + } + } } +#[derive(Debug, Clone)] +pub enum FetchKind { + User, + Object, + Activity, + Context, +} + +impl AsRef for FetchKind { + fn as_ref(&self) -> &str { + match self { + Self::User => "users", + Self::Object => "objects", + Self::Activity => "activities", + Self::Context => "context", + } + } +} pub struct Http; @@ -79,7 +115,8 @@ impl Http { pub struct Uri; impl Uri { - pub fn full(kind: &str, id: &str) -> String { + pub fn full(kind: FetchKind, id: &str) -> String { + let kind = kind.as_ref(); if id.starts_with('+') { id.replace('+', "https://").replace('@', "/") } else { @@ -111,7 +148,8 @@ impl Uri { /// - https://other.domain.net/unexpected/path/root /// - +other.domain.net@users@root /// - root - pub fn web(kind: &str, url: &str) -> String { + pub fn web(kind: FetchKind, url: &str) -> String { + let kind = kind.as_ref(); format!("/web/{kind}/{}", Self::short(url)) } @@ -123,7 +161,8 @@ impl Uri { /// - https://other.domain.net/unexpected/path/root /// - +other.domain.net@users@root /// - root - pub fn api(kind: &str, url: &str, fetch: bool) -> String { + pub fn api(kind: FetchKind, url: &str, fetch: bool) -> String { + let kind = kind.as_ref(); format!("{URL_BASE}/{kind}/{}{}", Self::short(url), if fetch { "?fetch=true" } else { "" }) } } diff --git a/web/src/page.rs b/web/src/page.rs index 96933c77..3e02b2a6 100644 --- a/web/src/page.rs +++ b/web/src/page.rs @@ -38,11 +38,11 @@ pub fn UserPage(tl: Timeline) -> impl IntoView { let _id = id.clone(); // wtf triple clone??? TODO!! let actor = create_local_resource(move || _id.clone(), move |id| { async move { - match CACHE.get(&Uri::full("users", &id)) { + match CACHE.get(&Uri::full(FetchKind::User, &id)) { Some(x) => Some(x.clone()), None => { - let user : serde_json::Value = Http::fetch(&Uri::api("users", &id, true), auth).await.ok()?; - CACHE.put(Uri::full("users", &id), user.clone()); + let user : serde_json::Value = Http::fetch(&Uri::api(FetchKind::User, &id, true), auth).await.ok()?; + CACHE.put(Uri::full(FetchKind::User, &id), user.clone()); Some(user) }, } @@ -71,7 +71,7 @@ pub fn UserPage(tl: Timeline) -> impl IntoView { let following = object.following().get().map(|x| x.total_items().unwrap_or(0)).unwrap_or(0); let followers = object.followers().get().map(|x| x.total_items().unwrap_or(0)).unwrap_or(0); let statuses = object.outbox().get().map(|x| x.total_items().unwrap_or(0)).unwrap_or(0); - let tl_url = format!("{}/outbox/page", Uri::api("users", &id.clone(), false)); + let tl_url = format!("{}/outbox/page", Uri::api(FetchKind::User, &id.clone(), false)); if !tl.next.get().starts_with(&tl_url) { tl.reset(tl_url); } @@ -136,11 +136,11 @@ pub fn ObjectPage(tl: Timeline) -> impl IntoView { let auth = use_context::().expect("missing auth context"); let object = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), move |oid| { async move { - match CACHE.get(&Uri::full("objects", &oid)) { + match CACHE.get(&Uri::full(FetchKind::Object, &oid)) { Some(x) => Some(x.clone()), None => { - let obj = Http::fetch::(&Uri::api("objects", &oid, true), auth).await.ok()?; - CACHE.put(Uri::full("objects", &oid), obj.clone()); + let obj = Http::fetch::(&Uri::api(FetchKind::Object, &oid, true), auth).await.ok()?; + CACHE.put(Uri::full(FetchKind::Object, &oid), obj.clone()); Some(obj) } } @@ -154,12 +154,13 @@ pub fn ObjectPage(tl: Timeline) -> impl IntoView { None => view! {

loading ...

}.into_view(), Some(None) => view! {

loading failed

}.into_view(), Some(Some(o)) => { - let tl_url = format!("{}/page", Uri::api("context", &o.context().id().unwrap_or_default(), false)); + let object = o.clone(); + let tl_url = format!("{}/page", Uri::api(FetchKind::Context, &o.context().id().unwrap_or_default(), false)); if !tl.next.get().starts_with(&tl_url) { tl.reset(tl_url); } view!{ - +
diff --git a/web/src/prelude.rs b/web/src/prelude.rs index 231c8634..c47bfa27 100644 --- a/web/src/prelude.rs +++ b/web/src/prelude.rs @@ -1,5 +1,5 @@ pub use crate::{ - Http, Uri, + Http, Uri, FetchKind, CACHE, URL_BASE, auth::{Auth, AuthToken}, page::*,