pub mod context; use apb::{target::Addressed, Activity, ActivityMut, Actor, Base, Collection, Object, ObjectMut}; use context::CTX; use leptos::{leptos_dom::logging::console_log, *}; use leptos_router::*; pub const URL_BASE: &str = "https://feditest.alemi.dev"; pub const URL_PREFIX: &str = "/web"; #[derive(Debug, serde::Serialize)] struct LoginForm { email: String, password: String, } #[component] pub fn LoginBox( rx: Signal>, tx: WriteSignal>, ) -> impl IntoView { let username_ref: NodeRef = create_node_ref(); let password_ref: NodeRef = create_node_ref(); view! {
"Hello "test

() .await.unwrap(); tx.set(Some(auth)); }); } />

} } #[component] pub fn PostBox(token: Signal>) -> impl IntoView { let summary_ref: NodeRef = create_node_ref(); let content_ref: NodeRef = create_node_ref(); view! {
} } #[component] pub fn TimelinePicker( tx: WriteSignal, rx: ReadSignal, ) -> impl IntoView { let targets = ( "https://feditest.alemi.dev/users/test/inbox/page".to_string(), "https://feditest.alemi.dev/users/test/outbox/page".to_string(), "https://feditest.alemi.dev/inbox/page".to_string(), "https://feditest.alemi.dev/outbox/page".to_string(), ); let (my_in, my_out, our_in, our_out) = targets.clone(); let (my_in_, my_out_, our_in_, our_out_) = targets; view! { } } #[component] pub fn ActorBanner(object: serde_json::Value) -> impl IntoView { match object { serde_json::Value::String(id) => view! {
{id}
}, serde_json::Value::Object(_) => { let uid = object.id().unwrap_or_default().split('/').last().unwrap_or_default().to_string(); 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(); let domain = object.id().unwrap_or_default().replace("https://", "").split('/').next().unwrap_or_default().to_string(); view! {
{display_name}
{username}@{domain}
} }, _ => view! {
invalid actor
} } } #[component] pub fn Actor() -> impl IntoView { let params = use_params_map(); let actor = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), |uid| { async move { let uid = format!("{URL_BASE}/users/{uid}"); match CTX.cache.user.get(&uid) { Some(x) => x.clone(), None => reqwest::get(uid) .await .unwrap() .json::() .await .unwrap(), } } }); view! { {move || match actor.get() { None => view! {

loading...

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

{x.actor_type().unwrap_or(apb::ActorType::Person).as_ref().to_string()}

{x.summary().unwrap_or("").to_string()}

  • following" "{x.following().get().map(|x| x.total_items().unwrap_or(0))}
  • followers" "{x.followers().get().map(|x| x.total_items().unwrap_or(0))}
  • created" "{x.published().map(|x| x.to_rfc3339())}
}.into_view(), }} } } #[component] pub fn Activity(activity: serde_json::Value) -> impl IntoView { let object = activity.clone().object().extract().unwrap_or_else(|| serde_json::Value::String(activity.object().id().unwrap_or_default()) ); let object_id = object.id().unwrap_or_default().to_string(); let content = dissolve::strip_html_tags(object.content().unwrap_or_default()); let addressed = activity.addressed(); let audience = format!("[ {} ]", addressed.join(", ")); let privacy = if addressed.iter().any(|x| x == apb::target::PUBLIC) { "[public]" } else if addressed.iter().any(|x| x.ends_with("/followers")) { "[followers]" } else { "[private]" }; let title = object.summary().unwrap_or_default().to_string(); let date = object.published().map(|x| x.to_rfc3339()).unwrap_or_default(); let kind = activity.activity_type().unwrap_or(apb::ActivityType::Activity); view! { {match kind { // post apb::ActivityType::Create => view! {

{title}

{x}

} />
}, kind => view! {
{kind.as_ref().to_string()}" >> "{object_id}
}, }} {privacy}" "{date} } } #[derive(Debug, thiserror::Error)] #[error("{0}")] struct OmgReqwestErrorIsNotClonable(String); #[component] pub fn Timeline( token: Signal>, ) -> impl IntoView { let (timeline, set_timeline) = create_signal(format!("{URL_BASE}/inbox/page")); let items = create_local_resource(move || timeline.get(), move |feed_url| async move { fetch_activities_with_users(&feed_url, token).await }); view! {
{format!("{:?}", err.get())}

} > {move || items.with(|x| match x { None => Ok(view! {

loading...

}.into_view()), Some(data) => match data { Err(e) => Err(OmgReqwestErrorIsNotClonable(e.to_string())), Ok(values) => Ok( values .iter() .map(|object| { let actor = object.actor().extract().unwrap_or_else(|| serde_json::Value::String(object.actor().id().unwrap_or_default()) ); view! {

} }) .collect::>() .into_view() ), } })}
} } async fn fetch_activities_with_users( feed_url: &str, token: Signal>, ) -> reqwest::Result> { let mut req = reqwest::Client::new().get(feed_url); if let Some(token) = token.get() { req = req.header("Authorization", format!("Bearer {token}")); } let activities : Vec = req .send() .await? .json::() .await? .ordered_items() .collect(); // i could make this fancier with iterators and futures::join_all but they would run // concurrently and make a ton of parallel request, we actually want these sequential because // first one may fetch same user as second one // some fancier logic may make a set of all actors and fetch uniques concurrently... let mut out = Vec::new(); for x in activities { if let Some(uid) = x.actor().id() { if let Some(actor) = CTX.cache.user.get(&uid) { out.push(x.set_actor(apb::Node::object(actor.clone()))) } else { let mut req = reqwest::Client::new() .get(format!("https://feditest.alemi.dev/users/+?id={uid}")); if let Some(token) = token.get() { req = req.header("Authorization", format!("Bearer {token}")); } // TODO don't fail whole timeline fetch when one user fails fetching... let actor = req.send().await?.json::().await?; CTX.cache.user.insert(uid, actor.clone()); out.push(x.set_actor(apb::Node::object(actor))) } } else { out.push(x) } } Ok(out) }