forked from alemi/upub
feat(web): better timeline, idk
cant think read the diff bad day
This commit is contained in:
parent
63bde2b5e0
commit
abed664f0a
8 changed files with 102 additions and 47 deletions
|
@ -23,7 +23,7 @@ pub fn LoginBox(
|
|||
view! {
|
||||
<div>
|
||||
<div class="w-100" class:hidden=move || !token.present() >
|
||||
"hi "<a href={move || Uri::web("users", &username.get().unwrap_or_default() )} >{move || username.get().unwrap_or_default() }</a>
|
||||
"hi "<a href={move || Uri::web(FetchKind::User, &username.get().unwrap_or_default() )} >{move || username.get().unwrap_or_default() }</a>
|
||||
<input style="float:right" type="submit" value="logout" on:click=move |_| {
|
||||
token_tx.set(None);
|
||||
home_tl.reset(format!("{URL_BASE}/outbox/page"));
|
||||
|
|
|
@ -8,13 +8,8 @@ use apb::{target::Addressed, Activity, Actor, Base, Object};
|
|||
#[component]
|
||||
pub fn ActivityLine(activity: serde_json::Value) -> 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 {
|
|||
<table class="align w-100" style="table-layout: fixed">
|
||||
<tr>
|
||||
<td>
|
||||
<a href={Uri::web("users", &actor_id)} class="clean hover">
|
||||
<a href={Uri::web(FetchKind::User, &actor_id)} class="clean hover">
|
||||
<img src={avatar} class="avatar-inline mr-s ml-1" /><b>{username}</b><small>@{domain}</small>
|
||||
</a>
|
||||
</td>
|
||||
<td class="rev" >
|
||||
<code class="color moreinfo" title={object_id.clone()} >{kind.as_ref().to_string()}</code>
|
||||
<a class="hover ml-1" href={Uri::web("objects", &object_id)} >
|
||||
<a class="hover ml-1" href={Uri::web(FetchKind::Object, &object_id)} >
|
||||
<DateTime t=activity.published() />
|
||||
</a>
|
||||
<PrivacyMarker addressed=activity.addressed() />
|
||||
|
|
|
@ -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! {
|
||||
<div>
|
||||
<table class="w-100 post-table pa-1 mb-s" >
|
||||
|
@ -20,7 +19,7 @@ pub fn Object(object: serde_json::Value) -> impl IntoView {
|
|||
Some(view! {
|
||||
<tr class="post-table" >
|
||||
<td class="post-table pa-1" colspan="2" >
|
||||
"in reply to "<small><a class="clean hover" href={Uri::web("objects", &in_reply_to)}>{Uri::pretty(&in_reply_to)}</a></small>
|
||||
"in reply to "<small><a class="clean hover" href={Uri::web(FetchKind::Object, &in_reply_to)}>{Uri::pretty(&in_reply_to)}</a></small>
|
||||
</td>
|
||||
</tr>
|
||||
})
|
||||
|
@ -38,7 +37,7 @@ pub fn Object(object: serde_json::Value) -> impl IntoView {
|
|||
}</td>
|
||||
</tr>
|
||||
<tr class="post-table" >
|
||||
<td class="post-table pa-1" ><ActorBanner object=author tiny=true /></td>
|
||||
<td class="post-table pa-1" ><ActorBanner object=author /></td>
|
||||
<td class="post-table pa-1 center" >
|
||||
<a class="clean hover" href={oid} target="_blank">
|
||||
<DateTime t=object.published() />
|
||||
|
@ -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! {
|
||||
<table class="align w-100">
|
||||
<tr>
|
||||
<td><ActorBanner object=author /></td>
|
||||
<td class="rev" >
|
||||
<a class="clean hover" href={Uri::web("objects", object.id().unwrap_or_default())}>
|
||||
<a class="clean hover" href={Uri::web(FetchKind::Object, object.id().unwrap_or_default())}>
|
||||
<DateTime t=object.published() />
|
||||
</a>
|
||||
<PrivacyMarker addressed=object.addressed() />
|
||||
|
|
|
@ -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! { <ObjectInline object=obj author=author /> });
|
||||
let object = CACHE.get(&object_id).map(|obj| {
|
||||
view! { <ObjectInline object=obj /> }
|
||||
});
|
||||
view! {
|
||||
<ActivityLine activity=item />
|
||||
{object}
|
||||
|
@ -66,7 +66,7 @@ pub fn TimelineFeed(tl: Timeline) -> impl IntoView {
|
|||
}.into_view()
|
||||
},
|
||||
Some(apb::BaseType::Object(apb::ObjectType::Note)) => view! {
|
||||
<Object object=item />
|
||||
<Object object=item.clone() />
|
||||
<hr/ >
|
||||
}.into_view(),
|
||||
_ => view! { <p><code>type not implemented</code></p><hr /> }.into_view(),
|
||||
|
@ -96,21 +96,28 @@ async fn process_activities(
|
|||
auth: Signal<Option<String>>,
|
||||
) -> Vec<String> {
|
||||
use apb::ActivityMut;
|
||||
let mut sub_tasks = Vec::new();
|
||||
let mut sub_tasks : Vec<Pin<Box<dyn futures::Future<Output = ()>>>> = 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<Option<String>>) {
|
||||
async fn fetch_and_update(kind: FetchKind, id: String, auth: Signal<Option<String>>) {
|
||||
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<Option<String>>) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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! {
|
||||
<div><b>?</b>" "<a class="clean hover" href={Uri::web("users", &id)}>{Uri::pretty(&id)}</a></div>
|
||||
<div><b>?</b>" "<a class="clean hover" href={Uri::web(FetchKind::User, &id)}>{Uri::pretty(&id)}</a></div>
|
||||
},
|
||||
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();
|
||||
|
|
|
@ -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<serde_json::Value> {
|
||||
match self.get(k) {
|
||||
Some(x) => Ok(x),
|
||||
None => {
|
||||
let obj = reqwest::get(Uri::api(kind, k, true))
|
||||
.await?
|
||||
.json::<serde_json::Value>()
|
||||
.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<str> 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 { "" })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::<Auth>().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::<serde_json::Value>(&Uri::api("objects", &oid, true), auth).await.ok()?;
|
||||
CACHE.put(Uri::full("objects", &oid), obj.clone());
|
||||
let obj = Http::fetch::<serde_json::Value>(&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! { <p> loading ... </p> }.into_view(),
|
||||
Some(None) => view! { <p><code>loading failed</code></p> }.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!{
|
||||
<Object object=o.clone() />
|
||||
<Object object=object />
|
||||
<div class="ml-1 mr-1 mt-2">
|
||||
<TimelineFeed tl=tl />
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
pub use crate::{
|
||||
Http, Uri,
|
||||
Http, Uri, FetchKind,
|
||||
CACHE, URL_BASE,
|
||||
auth::{Auth, AuthToken},
|
||||
page::*,
|
||||
|
|
Loading…
Reference in a new issue