From f75f0cc209789fc9102611c3fbd482ebb74e02fd Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 10 Jun 2024 03:12:05 +0200 Subject: [PATCH] feat(web): followers/following pages --- web/src/app.rs | 3 + web/src/components/user.rs | 86 +++++++++++++++++++ web/src/page/actor/activity.rs | 0 web/src/page/actor/follow.rs | 64 ++++++++++++++ web/src/page/actor/mod.rs | 3 + web/src/page/actor/view.rs | 75 +++++++++++++++++ web/src/page/mod.rs | 5 +- web/src/page/user.rs | 150 --------------------------------- 8 files changed, 234 insertions(+), 152 deletions(-) create mode 100644 web/src/page/actor/activity.rs create mode 100644 web/src/page/actor/follow.rs create mode 100644 web/src/page/actor/mod.rs create mode 100644 web/src/page/actor/view.rs delete mode 100644 web/src/page/user.rs diff --git a/web/src/app.rs b/web/src/app.rs index ff5eb898..aecba5a4 100644 --- a/web/src/app.rs +++ b/web/src/app.rs @@ -150,6 +150,9 @@ pub fn App() -> impl IntoView { + } /> + } /> + // } /> diff --git a/web/src/components/user.rs b/web/src/components/user.rs index fd731f4e..5252f66d 100644 --- a/web/src/components/user.rs +++ b/web/src/components/user.rs @@ -103,3 +103,89 @@ async fn send_follow_response(kind: apb::ActivityType, target: String, to: Strin tracing::error!("failed posting follow response: {e}"); } } + +#[component] +pub fn ActorHeader(object: crate::Object) -> impl IntoView { + let auth = use_context::().expect("missing auth context"); + let avatar_url = object.icon().get().map(|x| x.url().id().str().unwrap_or(DEFAULT_AVATAR_URL.into())).unwrap_or(DEFAULT_AVATAR_URL.into()); + let background_url = object.image().get().map(|x| x.url().id().str().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 created = object.published().ok(); + 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 following_me = object.following_me().unwrap_or(false); + let followed_by_me = object.followed_by_me().unwrap_or(false); + 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 uid = object.id().unwrap_or_default().to_string(); + let web_path = Uri::web(U::Actor, &uid); + 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() +} + +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}"); + } + }) +} diff --git a/web/src/page/actor/activity.rs b/web/src/page/actor/activity.rs new file mode 100644 index 00000000..e69de29b diff --git a/web/src/page/actor/follow.rs b/web/src/page/actor/follow.rs new file mode 100644 index 00000000..1dec0897 --- /dev/null +++ b/web/src/page/actor/follow.rs @@ -0,0 +1,64 @@ +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 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! { })} + {move || match resource.get() { + None => view! {

"loading "

}.into_view(), + Some(None) => view! { could not load following }.into_view(), + Some(Some(arr)) => view! { + x, + None => Arc::new(serde_json::Value::String(id)), + }; + view! { + + }.into_view() + } + / > + }, + }} +
+
+ } +} diff --git a/web/src/page/actor/mod.rs b/web/src/page/actor/mod.rs new file mode 100644 index 00000000..198e354d --- /dev/null +++ b/web/src/page/actor/mod.rs @@ -0,0 +1,3 @@ +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 new file mode 100644 index 00000000..1883e532 --- /dev/null +++ b/web/src/page/actor/view.rs @@ -0,0 +1,75 @@ +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 3e63ded7..178cfdd1 100644 --- a/web/src/page/mod.rs +++ b/web/src/page/mod.rs @@ -19,5 +19,6 @@ pub use search::SearchPage; mod timeline; pub use timeline::TimelinePage; -mod user; -pub use user::UserPage; +mod actor; +pub use actor::view::UserPage; +pub use actor::follow::FollowPage; diff --git a/web/src/page/user.rs b/web/src/page/user.rs deleted file mode 100644 index 916fe0e7..00000000 --- a/web/src/page/user.rs +++ /dev/null @@ -1,150 +0,0 @@ -use std::sync::Arc; - -use leptos::*; -use leptos_router::*; -use crate::{prelude::*, DEFAULT_AVATAR_URL}; - -use apb::{field::OptionalString, ActivityMut, Actor, Base, Object, ObjectMut}; - -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() -> 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! { -
- - 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().str().unwrap_or(DEFAULT_AVATAR_URL.into())).unwrap_or(DEFAULT_AVATAR_URL.into()); - let background_url = object.image().get().map(|x| x.url().id().str().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().ok(); - 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 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() - }, - } - }} -
-
- } -}