feat(web): object page and some initial routing

This commit is contained in:
əlemi 2024-04-15 22:03:32 +02:00
parent ee26596568
commit c28b5fd49c
Signed by: alemi
GPG key ID: A4895B84D311642C
3 changed files with 82 additions and 21 deletions

View file

@ -15,7 +15,7 @@ pub struct Context {
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Cache { pub struct Cache {
pub user: DashMap<String, serde_json::Value>, pub actors: DashMap<String, serde_json::Value>,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]

View file

@ -15,6 +15,14 @@ struct LoginForm {
password: String, password: String,
} }
fn web_uri(kind: &str, url: &str) -> String {
if url.starts_with(URL_BASE) {
format!("/web/{kind}/{}", url.split('/').last().unwrap_or_default().to_string())
} else {
format!("/web/{kind}/+{}", url.replace("https://", "").replace('/', "@"))
}
}
#[component] #[component]
pub fn LoginBox( pub fn LoginBox(
rx: Signal<Option<String>>, rx: Signal<Option<String>>,
@ -114,7 +122,8 @@ pub fn ActorBanner(object: serde_json::Value) -> impl IntoView {
<div><b>{id}</b></div> <div><b>{id}</b></div>
}, },
serde_json::Value::Object(_) => { serde_json::Value::Object(_) => {
let uid = object.id().unwrap_or_default().split('/').last().unwrap_or_default().to_string(); let uid = object.id().unwrap_or_default().to_string();
let uri = web_uri("users", &uid);
let avatar_url = object.icon().get().map(|x| x.url().id().unwrap_or_default()).unwrap_or_default(); 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 display_name = object.name().unwrap_or_default().to_string();
let username = object.preferred_username().unwrap_or_default().to_string(); let username = object.preferred_username().unwrap_or_default().to_string();
@ -127,7 +136,7 @@ pub fn ActorBanner(object: serde_json::Value) -> impl IntoView {
<td><b>{display_name}</b></td> <td><b>{display_name}</b></td>
</tr> </tr>
<tr> <tr>
<td class="top" ><a class="clean" href={format!("/web/users/{uid}")} ><small>{username}@{domain}</small></a></td> <td class="top" ><a class="clean" href={uri} ><small>{username}@{domain}</small></a></td>
</tr> </tr>
</table> </table>
</div> </div>
@ -145,7 +154,7 @@ pub fn Actor() -> impl IntoView {
let actor = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), |uid| { let actor = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), |uid| {
async move { async move {
let uid = format!("{URL_BASE}/users/{uid}"); let uid = format!("{URL_BASE}/users/{uid}");
match CTX.cache.user.get(&uid) { match CTX.cache.actors.get(&uid) {
Some(x) => x.clone(), Some(x) => x.clone(),
None => reqwest::get(uid) None => reqwest::get(uid)
.await .await
@ -163,16 +172,16 @@ pub fn Actor() -> impl IntoView {
<div class="ml-3 mr-3 mt-3"> <div class="ml-3 mr-3 mt-3">
<ActorBanner object=x.clone() /> <ActorBanner object=x.clone() />
<p <p
class="center pb-2 pt-2 pr-2 pl-2" class="pb-2 pt-2 pr-2 pl-2"
style={format!( style={format!(
"background-image: url({}); background-size: cover;", "background-image: url({}); background-size: cover;",
x.image().get().map(|x| x.url().id().unwrap_or_default()).unwrap_or_default() x.image().get().map(|x| x.url().id().unwrap_or_default()).unwrap_or_default()
)} )}
> >
<b>{x.actor_type().unwrap_or(apb::ActorType::Person).as_ref().to_string()}</b> {x.summary().unwrap_or("").to_string()}
</p> </p>
<p><small>{x.summary().unwrap_or("").to_string()}</small></p>
<ul> <ul>
<li><code>type</code>" "<b>{x.actor_type().unwrap_or(apb::ActorType::Person).as_ref().to_string()}</b></li>
<li><code>following</code>" "<b>{x.following().get().map(|x| x.total_items().unwrap_or(0))}</b></li> <li><code>following</code>" "<b>{x.following().get().map(|x| x.total_items().unwrap_or(0))}</b></li>
<li><code>followers</code>" "<b>{x.followers().get().map(|x| x.total_items().unwrap_or(0))}</b></li> <li><code>followers</code>" "<b>{x.followers().get().map(|x| x.total_items().unwrap_or(0))}</b></li>
<li><code>created</code>" "{x.published().map(|x| x.to_rfc3339())}</li> <li><code>created</code>" "{x.published().map(|x| x.to_rfc3339())}</li>
@ -184,11 +193,60 @@ pub fn Actor() -> impl IntoView {
} }
#[component] #[component]
pub fn Activity(activity: serde_json::Value) -> impl IntoView { pub fn ObjectPage() -> impl IntoView {
let params = use_params_map();
let object = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), |oid| {
async move {
let uid = format!("{URL_BASE}/objects/{oid}");
match CTX.cache.actors.get(&uid) {
Some(x) => x.clone(),
None => reqwest::get(uid)
.await
.unwrap()
.json::<serde_json::Value>()
.await
.unwrap(),
}
}
});
view! {
{move || match object.get() {
Some(o) => view!{ <Object object=o /> }.into_view(),
None => view! { <p> loading ... </p> }.into_view(),
}}
}
}
#[component]
pub fn Object(object: serde_json::Value) -> impl IntoView {
let summary = object.summary().unwrap_or_default().to_string();
let content = object.content().unwrap_or_default().to_string();
let date = object.published().map(|x| x.to_rfc3339()).unwrap_or_default();
let author_id = object.attributed_to().id().unwrap_or_default();
let author = CTX.cache.actors.get(&author_id).map(|x| view! { <ActorBanner object=x.clone() /> });
view! {
{author}
<table>
<tr>
<td>{summary}</td>
</tr>
<tr>
<td>{content}</td>
</tr>
<tr>
<td>{date}</td>
</tr>
</table>
}
}
#[component]
pub fn InlineActivity(activity: serde_json::Value) -> impl IntoView {
let object = activity.clone().object().extract().unwrap_or_else(|| let object = activity.clone().object().extract().unwrap_or_else(||
serde_json::Value::String(activity.object().id().unwrap_or_default()) serde_json::Value::String(activity.object().id().unwrap_or_default())
); );
let object_id = object.id().unwrap_or_default().to_string(); let object_id = object.id().unwrap_or_default().to_string();
let object_uri = web_uri("objects", &object_id);
let content = dissolve::strip_html_tags(object.content().unwrap_or_default()); let content = dissolve::strip_html_tags(object.content().unwrap_or_default());
let addressed = activity.addressed(); let addressed = activity.addressed();
let audience = format!("[ {} ]", addressed.join(", ")); let audience = format!("[ {} ]", addressed.join(", "));
@ -200,7 +258,9 @@ pub fn Activity(activity: serde_json::Value) -> impl IntoView {
"[private]" "[private]"
}; };
let title = object.summary().unwrap_or_default().to_string(); let title = object.summary().unwrap_or_default().to_string();
let date = object.published().map(|x| x.to_rfc3339()).unwrap_or_default(); let date = object.published().map(|x| x.to_rfc3339()).unwrap_or_else(||
activity.published().map(|x| x.to_rfc3339()).unwrap_or_default()
);
let kind = activity.activity_type().unwrap_or(apb::ActivityType::Activity); let kind = activity.activity_type().unwrap_or(apb::ActivityType::Activity);
view! { view! {
{match kind { {match kind {
@ -208,16 +268,17 @@ pub fn Activity(activity: serde_json::Value) -> impl IntoView {
apb::ActivityType::Create => view! { apb::ActivityType::Create => view! {
<div> <div>
<p><i>{title}</i></p> <p><i>{title}</i></p>
<For {
each=move || content.clone() // TODO wtf this clone?? content
key=|x| x.to_string() // TODO what about this clone? .into_iter()
children=move |x: String| view! { <p>{x}</p> } .map(|x| view! { <p>{x}</p> }.into_view())
/> .collect::<Vec<View>>()
}
</div> </div>
}, },
kind => view! { kind => view! {
<div> <div>
<b>{kind.as_ref().to_string()}</b>" >> "<i>{object_id}</i> <b>{kind.as_ref().to_string()}</b>" >> "<a href={object_uri}>{object_id}</a>
</div> </div>
}, },
}} }}
@ -240,7 +301,7 @@ pub fn Timeline(
view! { view! {
<div class="ml-1"> <div class="ml-1">
<TimelinePicker tx=set_timeline rx=timeline /> <TimelinePicker tx=set_timeline rx=timeline />
<div class="boxscroll mt-1" > <div class="boxscroll" >
<ErrorBoundary fallback=move |err| view! { <p>{format!("{:?}", err.get())}</p> } > <ErrorBoundary fallback=move |err| view! { <p>{format!("{:?}", err.get())}</p> } >
{move || items.with(|x| match x { {move || items.with(|x| match x {
None => Ok(view! { <p>loading...</p> }.into_view()), None => Ok(view! { <p>loading...</p> }.into_view()),
@ -256,7 +317,7 @@ pub fn Timeline(
view! { view! {
<div class="ml-1 mr-1 mt-1"> <div class="ml-1 mr-1 mt-1">
<ActorBanner object=actor /> <ActorBanner object=actor />
<Activity activity=object.clone() /> <InlineActivity activity=object.clone() />
</div> </div>
<hr/ > <hr/ >
} }
@ -297,7 +358,7 @@ async fn fetch_activities_with_users(
let mut out = Vec::new(); let mut out = Vec::new();
for x in activities { for x in activities {
if let Some(uid) = x.actor().id() { if let Some(uid) = x.actor().id() {
if let Some(actor) = CTX.cache.user.get(&uid) { if let Some(actor) = CTX.cache.actors.get(&uid) {
out.push(x.set_actor(apb::Node::object(actor.clone()))) out.push(x.set_actor(apb::Node::object(actor.clone())))
} else { } else {
let mut req = reqwest::Client::new() let mut req = reqwest::Client::new()
@ -309,7 +370,7 @@ async fn fetch_activities_with_users(
// TODO don't fail whole timeline fetch when one user fails fetching... // TODO don't fail whole timeline fetch when one user fails fetching...
let actor = req.send().await?.json::<serde_json::Value>().await?; let actor = req.send().await?.json::<serde_json::Value>().await?;
CTX.cache.user.insert(uid, actor.clone()); CTX.cache.actors.insert(uid, actor.clone());
out.push(x.set_actor(apb::Node::object(actor))) out.push(x.set_actor(apb::Node::object(actor)))
} }

View file

@ -3,7 +3,7 @@ use leptos_router::*;
use leptos_use::{use_cookie, utils::FromToStringCodec}; use leptos_use::{use_cookie, utils::FromToStringCodec};
use upub_web::{ use upub_web::{
Actor, LoginBox, PostBox, Timeline Actor, LoginBox, ObjectPage, PostBox, Timeline
}; };
@ -45,7 +45,7 @@ fn main() {
<Route path="/" view=move || view! { <Redirect path="/web" /> } /> <Route path="/" view=move || view! { <Redirect path="/web" /> } />
<Route path="/web" view=move || view! { <Timeline token=cookie /> } /> <Route path="/web" view=move || view! { <Timeline token=cookie /> } />
<Route path="/web/users/:id" view=Actor /> <Route path="/web/users/:id" view=Actor />
// <Route path="/object/:id" view=Object /> <Route path="/web/objects/:id" view=ObjectPage />
</Routes> </Routes>
</main> </main>
</Router> </Router>