use std::sync::Arc; use apb::{ActivityMut, Actor, Base, Object, ObjectMut}; use leptos::*; use leptos_router::*; use crate::{prelude::*, DEFAULT_AVATAR_URL}; #[component] pub fn AboutPage() -> impl IntoView { view! {
about

μpub" is a micro social network powered by "ActivityPub

"the "fediverse" is an ensemble of social networks, which, while independently hosted, can communicate with each other"

content is aggregated in timelines, logged out users can only access global server timeline

} } #[component] pub fn ConfigPage() -> impl IntoView { view! {
config

"not implemented :("

} } fn send_follow_request(target: String) { let auth = use_context::().expect("missing auth context"); spawn_local(async move { let payload = serde_json::Value::Object(serde_json::Map::default()) .set_activity_type(Some(apb::ActivityType::Follow)) .set_object(apb::Node::link(target.clone())) .set_to(apb::Node::links(vec![target])); if let Err(e) = Http::post(&auth.outbox(), &payload, auth).await { tracing::error!("failed sending follow request: {e}"); } }) } #[component] pub fn UserPage(tl: Timeline) -> impl IntoView { let params = use_params_map(); let auth = use_context::().expect("missing auth context"); let id = params.get() .get("id") .cloned() .unwrap_or_default(); let mut uid = id .replace("/web/objects/", "") .replacen('+', "https://", 1) .replace('@', "/"); if !uid.starts_with("http") { uid = format!("{URL_BASE}/web/objects/{uid}"); } let actor = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), move |id| { async move { match CACHE.get(&Uri::full(FetchKind::User, &id)) { Some(x) => Some(x.clone()), None => { let user : serde_json::Value = Http::fetch(&Uri::api(FetchKind::User, &id, true), auth).await.ok()?; let user = Arc::new(user); CACHE.put(Uri::full(FetchKind::User, &id), user.clone()); Some(user) }, } } }); view! {
users::view "\u{1f5d8}"
{move || { let uid = uid.clone(); match actor.get() { None => view! {

loading...

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

loading failed"↗"

}.into_view() }, Some(Some(object)) => { let uid = object.id().unwrap_or_default().to_string(); let avatar_url = object.icon().get().map(|x| x.url().id().unwrap_or(DEFAULT_AVATAR_URL.into())).unwrap_or(DEFAULT_AVATAR_URL.into()); let background_url = object.image().get().map(|x| x.url().id().unwrap_or(DEFAULT_AVATAR_URL.into())).unwrap_or(DEFAULT_AVATAR_URL.into()); let display_name = object.name().unwrap_or_default().to_string(); let username = object.preferred_username().unwrap_or_default().to_string(); let summary = object.summary().unwrap_or_default().to_string(); let domain = object.id().unwrap_or_default().replace("https://", "").split('/').next().unwrap_or_default().to_string(); let actor_type = object.actor_type().unwrap_or(apb::ActorType::Person); let actor_type_tag = if actor_type == apb::ActorType::Person { None } else { Some(view! { "["{actor_type.as_ref().to_lowercase()}"]" } ) }; let created = object.published(); let following = object.following_count().unwrap_or(0); let followers = object.followers_count().unwrap_or(0); let statuses = object.statuses_count().unwrap_or(0); 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); } let following_me = object.following_me().unwrap_or(false); let followed_by_me = object.followed_by_me().unwrap_or(false); let _uid = uid.clone(); view! {
{display_name}{actor_type_tag} {statuses}" ""\u{1f582}"
{username.clone()}@{domain} {following}" ""👥"
{followers}" ""📢"
{if followed_by_me { view! { following }.into_view() } else { view! { }.into_view() }} {if following_me { Some(view! { follows you }) } else { None }}

}.into_view() }, } }}
} } #[component] pub fn ObjectPage(tl: Timeline) -> impl IntoView { let params = use_params_map(); let auth = use_context::().expect("missing auth context"); let mut uid = params.get().get("id") .cloned() .unwrap_or_default() .replace("/web/objects/", "") .replacen('+', "https://", 1) .replace('@', "/"); if !uid.starts_with("http") { uid = format!("{URL_BASE}/web/objects/{uid}"); } let object = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), move |oid| { async move { match CACHE.get(&Uri::full(FetchKind::Object, &oid)) { Some(x) => Some(x.clone()), None => { let obj = Http::fetch::(&Uri::api(FetchKind::Object, &oid, true), auth).await.ok()?; let obj = Arc::new(obj); CACHE.put(Uri::full(FetchKind::Object, &oid), obj.clone()); Some(obj) } } } }); view! {
objects::view "\u{1f5d8}"
{move || match object.get() { None => view! {

loading ...

}.into_view(), Some(None) => { let uid = uid.clone(); view! {

loading failed"↗"

}.into_view() }, Some(Some(o)) => { 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!{
}.into_view() }, }} } } #[component] pub fn TimelinePage(name: &'static str, tl: Timeline) -> impl IntoView { let auth = use_context::().expect("missing auth context"); view! { } } #[component] pub fn DebugPage() -> impl IntoView { let (object, set_object) = create_signal(Arc::new(serde_json::Value::String( "use this view to fetch remote AP objects and inspect their content".into()) )); let cached_ref: NodeRef = create_node_ref(); let auth = use_context::().expect("missing auth context"); let (query, set_query) = create_signal("".to_string()); view! {
debug
set_object.set(x), None => set_object.set(Arc::new(serde_json::Value::String("not in cache!".into()))), } } else { let url = format!("{URL_BASE}/dbg?id={fetch_url}"); spawn_local(async move { set_object.set(Arc::new(debug_fetch(&url, auth).await)) }); } } >
obj " " usr
				{move || serde_json::to_string_pretty(object.get().as_ref()).unwrap_or("unserializable".to_string())}
			
} } #[component] pub fn SearchPage() -> impl IntoView { let auth = use_context::().expect("missing auth context"); let user = create_local_resource( move || use_query_map().get().get("q").cloned().unwrap_or_default(), move |q| { let user_fetch = Uri::api(FetchKind::User, &q, true); async move { Some(Arc::new(Http::fetch::(&user_fetch, auth).await.ok()?)) } } ); let object = create_local_resource( move || use_query_map().get().get("q").cloned().unwrap_or_default(), move |q| { let object_fetch = Uri::api(FetchKind::Object, &q, true); async move { Some(Arc::new(Http::fetch::(&object_fetch, auth).await.ok()?)) } } ); view! { search
users {move || match user.get() { None => None, Some(None) => None, Some(Some(u)) => Some(view! { }), }}
objects {move || match object.get() { None => None, Some(None) => None, Some(Some(o)) => Some(view! { }), }} } } // this is a rather weird way to fetch but i want to see the bare error text if it fails! async fn debug_fetch(url: &str, token: Auth) -> serde_json::Value { match Http::request::<()>(reqwest::Method::GET, url, None, token).await { Err(e) => serde_json::Value::String(format!("[!] failed sending request: {e}")), Ok(res) => match res.text().await { Err(e) => serde_json::Value::String(format!("[!] invalid response body: {e}")), Ok(x) => match serde_json::from_str(&x) { Err(_) => serde_json::Value::String(x), Ok(v) => v, }, } } }