pub mod context; use apb::{target::Addressed, Activity, Actor, Base, Collection, Object, ObjectMut}; use leptos::{leptos_dom::logging::{console_error, console_log}, *}; use leptos_router::*; use crate::context::{Http, Timeline, Uri, CACHE}; 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, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Auth { pub token: String, pub user: String, pub expires: chrono::DateTime, } pub trait MaybeToken { fn present(&self) -> bool; fn token(&self) -> String; } impl MaybeToken for Option { fn token(&self) -> String { match self { None => String::new(), Some(x) => x.clone(), } } fn present(&self) -> bool { match self { None => false, Some(x) => !x.is_empty(), } } } #[component] pub fn LoginBox( token_tx: WriteSignal>, token: Signal>, username: Signal>, username_tx: WriteSignal>, home_tl: Timeline, ) -> impl IntoView { let username_ref: NodeRef = create_node_ref(); let password_ref: NodeRef = create_node_ref(); view! {
() .await.unwrap(); console_log(&format!("logged in until {}", auth.expires)); let username = auth.user.split('/').last().unwrap_or_default().to_string(); // reset home feed and point it to our user's inbox home_tl.set_feed(vec![]); home_tl.set_next(format!("{URL_BASE}/users/{}/inbox/page", username)); // update our username and token cookies username_tx.set(Some(username)); token_tx.set(Some(auth.token)); }); } />
} } #[component] pub fn TimelineNavigation() -> impl IntoView { let auth = use_context::>>().expect("missing auth context"); view! { } } #[component] pub fn PostBox(username: Signal>) -> impl IntoView { let auth = use_context::>>().expect("missing auth context"); let summary_ref: NodeRef = create_node_ref(); let content_ref: NodeRef = create_node_ref(); let public_ref: NodeRef = create_node_ref(); let followers_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().to_string(); let uri = Uri::web("users", &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(); 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 UserPage() -> impl IntoView { let params = use_params_map(); let auth = use_context::>>().expect("missing auth context"); let actor = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), move |id| { async move { match CACHE.get(&Uri::full("users", &id)) { Some(x) => Some(x.clone()), None => { let user : serde_json::Value = Http::fetch(&Uri::api("users", &id), auth).await.ok()?; CACHE.put(Uri::full("users", &id), user.clone()); Some(user) }, } } }); view! {
view::user
{move || match actor.get() { None => view! {

loading...

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

error loading

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

{ dissolve::strip_html_tags(x.summary().unwrap_or("")) .into_iter() .map(|x| view! {

{x}

}) .collect_view() }

  • type" "{x.actor_type().unwrap_or(apb::ActorType::Person).as_ref().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 ObjectPage() -> impl IntoView { let params = use_params_map(); 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)) { Some(x) => Some(x.clone()), None => { let obj = Http::fetch::(&Uri::api("objects", &oid), auth).await.ok()?; CACHE.put(Uri::full("objects", &oid), obj.clone()); Some(obj) } } } }); view! {
view::object
{move || match object.get() { Some(Some(o)) => view!{ }.into_view(), Some(None) => view! {

loading failed

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

loading ...

}.into_view(), }} } } #[component] pub fn Object(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 date = object.published().map(|x| x.format("%Y/%m/%d %H:%M:%S").to_string()).unwrap_or_default(); let date_rfc = object.published().map(|x| x.to_rfc3339()).unwrap_or_default(); 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())); view! {
{move || if !summary.is_empty() { view! { }.into_view() } else { view! { }.into_view() }}
{summary.clone()}
{ content.into_iter().map(|x| view! {

{x}

}).collect_view() }
{date}
} } #[component] pub fn InlineActivity(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 audience = format!("[ {} ]", addressed.join(", ")); 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 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 date = object.published().map(|x| x.format("%Y/%m/%d %H:%M:%S").to_string()).unwrap_or_else(|| activity.published().map(|x| x.format("%Y/%m/%d %H:%M:%S").to_string()).unwrap_or_default() ); let kind = activity.activity_type().unwrap_or(apb::ActivityType::Activity); view! {
{privacy} {kind.as_ref().to_string()}
{date}
{match kind { // post apb::ActivityType::Create => view! { }.into_view(), _ => view! {}.into_view(), }} } } #[component] pub fn About() -> impl IntoView { view! {
landing
nothing to see here! pick a timeline to start browsing
} } #[derive(Debug, thiserror::Error)] #[error("{0}")] struct OmgReqwestErrorIsNotClonable(String); #[component] pub fn TimelinePage(name: &'static str, tl: Timeline) -> impl IntoView { view! {
{name}
} } #[component] pub fn TimelineFeed(tl: Timeline) -> impl IntoView { let auth = use_context::>>().expect("missing auth context"); view! { { view! {

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

{id}" "[go]

}.into_view(), } } / >
} }