From 0fec18582d3ab49215b3f9963df892dc3e0cffd8 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 10 Jun 2024 21:44:28 +0200 Subject: [PATCH] feat(web): rework actors page --- web/src/actors/follow.rs | 55 ++++++++++++++ web/src/actors/mod.rs | 9 +++ web/src/actors/posts.rs | 20 ++++++ web/src/actors/view.rs | 128 +++++++++++++++++++++++++++++++++ web/src/app.rs | 128 +++++++++++++++++++++++---------- web/src/lib.rs | 1 + web/src/page/actor/activity.rs | 0 web/src/page/actor/follow.rs | 69 ------------------ web/src/page/actor/mod.rs | 3 - web/src/page/actor/view.rs | 75 ------------------- web/src/page/mod.rs | 4 -- web/src/prelude.rs | 5 ++ 12 files changed, 307 insertions(+), 190 deletions(-) create mode 100644 web/src/actors/follow.rs create mode 100644 web/src/actors/mod.rs create mode 100644 web/src/actors/posts.rs create mode 100644 web/src/actors/view.rs delete mode 100644 web/src/page/actor/activity.rs delete mode 100644 web/src/page/actor/follow.rs delete mode 100644 web/src/page/actor/mod.rs delete mode 100644 web/src/page/actor/view.rs diff --git a/web/src/actors/follow.rs b/web/src/actors/follow.rs new file mode 100644 index 0000000..6cee437 --- /dev/null +++ b/web/src/actors/follow.rs @@ -0,0 +1,55 @@ +use leptos::*; +use leptos_router::*; +use crate::prelude::*; + +use std::sync::Arc; + +use apb::Collection; + +#[component] +pub fn FollowList(outgoing: bool) -> impl IntoView { + let follow___ = if outgoing { "following" } else { "followers" }; + let symbol = if outgoing { "👥" } else { "📢" }; + let params = use_params::(); + let auth = use_context::().expect("missing auth context"); + let resource = create_local_resource( + move || params.get().ok().and_then(|x| x.id).unwrap_or_default(), + move |id| { + async move { + Ok::<_, String>( + Http::fetch::(&format!("{URL_BASE}/actors/{id}/{follow___}/page"), auth) + .await + .map_err(|e| e.to_string())? + .ordered_items() + .all_ids() + ) + } + } + ); + view! { + {symbol}" "{follow___} +
+ {move || match resource.get() { + None => view! { }.into_view(), + Some(Err(e)) => view! {

could not load {follow___}: {e}

}.into_view(), + Some(Ok(arr)) => view! { + x, + None => Arc::new(serde_json::Value::String(id)), + }; + view! { + +
+ }.into_view() + } + / > + }.into_view(), + }} +
+ } +} + diff --git a/web/src/actors/mod.rs b/web/src/actors/mod.rs new file mode 100644 index 0000000..d662171 --- /dev/null +++ b/web/src/actors/mod.rs @@ -0,0 +1,9 @@ +pub mod follow; +pub mod view; +pub mod posts; + +use leptos_router::Params; // TODO can i remove this? +#[derive(Clone, leptos::Params, PartialEq)] +struct IdParam { + id: Option, +} diff --git a/web/src/actors/posts.rs b/web/src/actors/posts.rs new file mode 100644 index 0000000..2d15d7a --- /dev/null +++ b/web/src/actors/posts.rs @@ -0,0 +1,20 @@ +use leptos::*; +use leptos_router::*; +use crate::prelude::*; + +#[component] +pub fn ActorPosts() -> impl IntoView { + let feeds = use_context::().expect("missing feeds context"); + let params = use_params::(); + Signal::derive(move || { + let id = params.get().ok().and_then(|x| x.id).unwrap_or_default(); + let tl_url = format!("{}/outbox/page", Uri::api(U::Actor, &id, false)); + if !feeds.user.next.get_untracked().starts_with(&tl_url) { + feeds.user.reset(Some(tl_url)); + } + }).track(); + view! { + "🖂"" posts" + + } +} diff --git a/web/src/actors/view.rs b/web/src/actors/view.rs new file mode 100644 index 0000000..5d695e9 --- /dev/null +++ b/web/src/actors/view.rs @@ -0,0 +1,128 @@ +use leptos::*; +use leptos_router::*; +use crate::{getters::Getter, prelude::*, DEFAULT_AVATAR_URL}; + +use apb::{field::OptionalString, ActivityMut, Actor, Base, Object, ObjectMut}; + +#[component] +pub fn ActorHeader() -> impl IntoView { + let params = use_params::(); + let auth = use_context::().expect("missing auth context"); + let actor = create_local_resource( + move || params.get().ok().and_then(|x| x.id).unwrap_or_default(), + move |id| { + async move { + match CACHE.get(&Uri::full(U::Actor, &id)) { + Some(x) => Ok::<_, String>(x.clone()), + None => { + let user : serde_json::Value = Http::fetch(&Uri::api(U::Actor, &id, true), auth) + .await + .map_err(|e| e.to_string())?; + let user = std::sync::Arc::new(user); + CACHE.put(Uri::full(U::Actor, &id), user.clone()); + Ok(user) + }, + } + } + } + ); + move || match actor.get() { + None => view! { }.into_view(), + Some(Err(e)) => view! { "could not resolve user: "{e} }.into_view(), + Some(Ok(actor)) => { + let avatar_url = actor.icon().get().map(|x| x.url().id().str().unwrap_or(DEFAULT_AVATAR_URL.into())).unwrap_or(DEFAULT_AVATAR_URL.into()); + let background_url = actor.image().get().map(|x| x.url().id().str().unwrap_or(DEFAULT_AVATAR_URL.into())).unwrap_or(DEFAULT_AVATAR_URL.into()); + let username = actor.preferred_username().unwrap_or_default().to_string(); + let name = actor.name().str().unwrap_or(username.clone()); + let created = actor.published().ok(); + let following_me = actor.following_me().unwrap_or(false); + let followed_by_me = actor.followed_by_me().unwrap_or(false); + let domain = actor.id().unwrap_or_default().replace("https://", "").split('/').next().unwrap_or_default().to_string(); + let actor_type = actor.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 uid = actor.id().unwrap_or_default().to_string(); + let web_path = Uri::web(U::Actor, &uid); + let _uid = uid.clone(); + view! { +
+ +
+ + + + + + + + + + + + + + + +
+ + + {name}{actor_type_tag} + + {actor.statuses_count().want()}" ""\u{1f582}" +
+ {username.clone()}@{domain} + + {actor.following_count().want()}" ""👥" +
+ + + {actor.followers_count().want()}" ""📢" +
+
+ {if followed_by_me { + view! { following }.into_view() + } else { + view! { }.into_view() + }} + {if following_me { + Some(view! { follows you }) + } else { + None + }} +
+
+
+ + }.into_view() + }, + } +} + +async fn send_follow_response(kind: apb::ActivityType, target: String, to: String, auth: Auth) { + let payload = apb::new() + .set_activity_type(Some(kind)) + .set_object(apb::Node::link(target)) + .set_to(apb::Node::links(vec![to])); + if let Err(e) = Http::post(&auth.outbox(), &payload, auth).await { + tracing::error!("failed posting follow response: {e}"); + } +} + +fn send_follow_request(target: String) { + let auth = use_context::().expect("missing auth context"); + spawn_local(async move { + let payload = apb::new() + .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}"); + } + }) +} diff --git a/web/src/app.rs b/web/src/app.rs index 1b7c38e..a20dca0 100644 --- a/web/src/app.rs +++ b/web/src/app.rs @@ -118,49 +118,43 @@ pub fn App() -> impl IntoView {
404 -
-

nothing to see here!

-

-
- }.into_view() + fallback=|| view! { } > - // TODO this is kind of ugly: the whole router gets rebuilt every time we log in/out - // in a sense it's what we want: refreshing the home tl is main purpose, but also - // server tl may contain stuff we can no longer see, or otherwise we may now be - // entitled to see new posts. so while being ugly it's techically correct ig?
- } - } else { - view! { } - } - /> - - } /> - } /> - } /> - } /> - - - } /> - - - - - } /> - } /> - - - // } /> - - - - } /> + + } + } else { + view! { } + } + /> + } /> + } /> + } /> + } /> + + + } /> + + + // TODO can we avoid this? + + + } /> + } /> + + + + + + // } /> + + + +
@@ -174,3 +168,59 @@ pub fn App() -> impl IntoView { } } + +#[component] +fn Navigable() -> impl IntoView { + let location = use_location(); + let breadcrumb = Signal::derive(move || { + let path = location.pathname.get(); + let mut path_iter = path.split('/').skip(1); + // TODO wow this breadcrumb logic really isnt nice can we make it better?? + match path_iter.next() { + Some("actors") => match path_iter.next() { + None => "actors :: all".to_string(), + Some(id) => { + let mut out = "actors :: ".to_string(); + if id.starts_with('+') { + out.push_str("proxy"); + } else { + out.push_str(id); + } + if let Some(x) = path_iter.next() { + out.push_str(" :: "); + out.push_str(x); + } + out + }, + }, + Some(p) => p.to_string(), + None => "?".to_string(), + } + }); + view! { +
+ "<<" + {crate::NAME}" :: "{breadcrumb} +
+ + } +} + +#[component] +pub fn NotFound() -> impl IntoView { + view! { +
+

nothing to see here!

+

+
+ } +} + +#[component] +pub fn Loader(#[prop(optional)] margin: bool) -> impl IntoView { + view! { +
+ +
+ } +} diff --git a/web/src/lib.rs b/web/src/lib.rs index 2468061..478d022 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -4,6 +4,7 @@ mod components; mod page; mod config; +pub mod actors; pub use app::App; pub use config::Config; pub use auth::Auth; diff --git a/web/src/page/actor/activity.rs b/web/src/page/actor/activity.rs deleted file mode 100644 index e69de29..0000000 diff --git a/web/src/page/actor/follow.rs b/web/src/page/actor/follow.rs deleted file mode 100644 index e231f12..0000000 --- a/web/src/page/actor/follow.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::sync::Arc; - -use leptos::*; -use leptos_router::*; -use crate::prelude::*; - -use apb::Collection; - -#[component] -pub fn FollowPage(outgoing: bool) -> impl IntoView { - let follow___ = if outgoing { "following" } else { "followers" }; - let symbol = if outgoing { "👥" } else { "📢" }; - let params = use_params_map(); - let auth = use_context::().expect("missing auth context"); - let user = Signal::derive(move ||{ - let id =params.get().get("id").cloned().unwrap_or_default(); - CACHE.get(&Uri::full(U::Actor, &id)) - }); - let resource = create_local_resource( - move || params.get().get("id").cloned().unwrap_or_default(), - move |id| { - async move { - match Http::fetch::(&format!("{URL_BASE}/actors/{id}/{follow___}/page"), auth).await { - Err(e) => { - tracing::error!("failed getting {follow___} for {id}: {e}"); - None - }, - Ok(x) => { - Some(x.ordered_items().all_ids()) - }, - - } - } - } - ); - view! { -
- - actors::view::{follow___} - -
- {move || user.get().map(|x| view! { })} - {symbol}" "{follow___} -
- {move || match resource.get() { - None => view! {

"loading "

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

could not load {follow___}

}.into_view(), - Some(Some(arr)) => view! { - x, - None => Arc::new(serde_json::Value::String(id)), - }; - view! { - -
- }.into_view() - } - / > - }.into_view(), - }} -
-
-
- } -} diff --git a/web/src/page/actor/mod.rs b/web/src/page/actor/mod.rs deleted file mode 100644 index 198e354..0000000 --- a/web/src/page/actor/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod activity; -pub mod follow; -pub mod view; diff --git a/web/src/page/actor/view.rs b/web/src/page/actor/view.rs deleted file mode 100644 index 1883e53..0000000 --- a/web/src/page/actor/view.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::sync::Arc; - -use leptos::*; -use leptos_router::*; -use crate::prelude::*; - -use apb::Object; - -#[component] -pub fn UserPage() -> impl IntoView { - let params = use_params_map(); - let feeds = use_context::().expect("missing feeds context"); - let auth = use_context::().expect("missing auth context"); - let id = params.get() - .get("id") - .cloned() - .unwrap_or_default(); - let uid = uriproxy::uri(URL_BASE, uriproxy::UriClass::Actor, &id); - let actor = create_local_resource( - move || params.get().get("id").cloned().unwrap_or_default(), - move |id| { - async move { - let tl_url = format!("{}/outbox/page", Uri::api(U::Actor, &id, false)); - if !feeds.user.next.get_untracked().starts_with(&tl_url) { - feeds.user.reset(Some(tl_url)); - } - match CACHE.get(&Uri::full(U::Actor, &id)) { - Some(x) => Some(x.clone()), - None => { - let user : serde_json::Value = Http::fetch(&Uri::api(U::Actor, &id, true), auth).await.ok()?; - let user = Arc::new(user); - CACHE.put(Uri::full(U::Actor, &id), user.clone()); - Some(user) - }, - } - } - } - ); - view! { -
- - actors::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)) => { - view! { -
- -

-
- - }.into_view() - }, - } - }} -
-
- } -} diff --git a/web/src/page/mod.rs b/web/src/page/mod.rs index 178cfdd..ee4253e 100644 --- a/web/src/page/mod.rs +++ b/web/src/page/mod.rs @@ -18,7 +18,3 @@ pub use search::SearchPage; mod timeline; pub use timeline::TimelinePage; - -mod actor; -pub use actor::view::UserPage; -pub use actor::follow::FollowPage; diff --git a/web/src/prelude.rs b/web/src/prelude.rs index d2c6ef6..b81aabd 100644 --- a/web/src/prelude.rs +++ b/web/src/prelude.rs @@ -5,6 +5,11 @@ pub use crate::{ auth::Auth, page::*, components::*, + actors::{ + view::ActorHeader, + follow::FollowList, + posts::ActorPosts, + } }; pub use uriproxy::UriClass as U;