diff --git a/web/index.html b/web/index.html index 476217cd..97418f42 100644 --- a/web/index.html +++ b/web/index.html @@ -144,6 +144,11 @@ background-color: var(--background); border: .3em solid #BF616A; } + img.avatar-inline { + display: inline; + height: .75em; + border-radius: 50%; + } img.inline-avatar { max-height: 2em; } diff --git a/web/src/components/activity.rs b/web/src/components/activity.rs new file mode 100644 index 00000000..a68339e8 --- /dev/null +++ b/web/src/components/activity.rs @@ -0,0 +1,42 @@ + +use leptos::*; +use crate::prelude::*; + +use apb::{target::Addressed, Activity, Actor, Base, Object}; + + +#[component] +pub fn ActivityLine(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 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 avatar = actor.icon().get().map(|x| x.url().id().unwrap_or_default()).unwrap_or_default(); + let username = actor.preferred_username().unwrap_or_default().to_string(); + let domain = actor.id().unwrap_or_default().replace("https://", "").split('/').next().unwrap_or_default().to_string(); + let kind = activity.activity_type().unwrap_or(apb::ActivityType::Activity); + view! { +
+ + + + + +
+ + {username}@{domain} + + + {kind.as_ref().to_string()} + + + + +
+
+ } +} diff --git a/web/src/components/mod.rs b/web/src/components/mod.rs new file mode 100644 index 00000000..cfe95081 --- /dev/null +++ b/web/src/components/mod.rs @@ -0,0 +1,42 @@ +mod activity; +pub use activity::ActivityLine; + +mod object; +pub use object::{Object, ObjectInline}; + +mod user; +pub use user::ActorBanner; + +mod timeline; +pub use timeline::{TimelineFeed, Timeline}; + +use leptos::*; + +#[component] +pub fn DateTime(t: Option>) -> impl IntoView { + match t { + Some(t) => { + let pretty = t.format("%Y/%m/%d %H:%M:%S").to_string(); + let rfc = t.to_rfc3339(); + Some(view! { + {pretty} + }) + }, + None => None, + } +} + +#[component] +pub fn PrivacyMarker(addressed: Vec) -> impl IntoView { + let privacy = if addressed.iter().any(|x| x == apb::target::PUBLIC) { + "🌐" + } else if addressed.iter().any(|x| x.ends_with("/followers")) { + "🔒" + } else { + "🔗" + }; + let audience = format!("[ {} ]", addressed.join(", ")); + view! { + {privacy} + } +} diff --git a/web/src/components/object.rs b/web/src/components/object.rs new file mode 100644 index 00000000..0f0cab48 --- /dev/null +++ b/web/src/components/object.rs @@ -0,0 +1,76 @@ +use leptos::*; +use crate::prelude::*; + +use apb::{target::Addressed, Activity, Actor, Base, Object}; + + +#[component] +pub fn Object(object: serde_json::Value) -> impl IntoView { + let oid = object.id().unwrap_or_default().to_string(); + let in_reply_to = object.in_reply_to().id().unwrap_or_default(); + let summary = object.summary().unwrap_or_default().to_string(); + let content = dissolve::strip_html_tags(object.content().unwrap_or_default()); + let date = object.published(); + 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 !in_reply_to.is_empty() { + Some(view! { + + + + }) + } else { None }} + {move || if !summary.is_empty() { + Some(view! { + + + + }) + } else { None }} + + + + + + + +
+ "in reply to "{Uri::pretty(&in_reply_to)} +
{summary.clone()}
{ + content.into_iter().map(|x| view! {

{x}

}).collect_view() + }
+ + + + +
+
+ } +} + +#[component] +pub fn ObjectInline(object: serde_json::Value, author: 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()); + view! { + + + + + +
+ + + + +
+
+ {if summary.is_empty() { None } else { Some(view! { {summary} })}} + {content.into_iter().map(|x| view! {

{x}

}).collect_view()} +
+ } +} + diff --git a/web/src/timeline.rs b/web/src/components/timeline.rs similarity index 85% rename from web/src/timeline.rs rename to web/src/components/timeline.rs index 729213bf..0efb05e3 100644 --- a/web/src/timeline.rs +++ b/web/src/components/timeline.rs @@ -1,6 +1,6 @@ use std::collections::BTreeSet; -use apb::Base; +use apb::{Activity, Base}; use leptos::*; use crate::prelude::*; @@ -53,13 +53,20 @@ pub fn TimelineFeed(tl: Timeline) -> impl IntoView { key=|k| k.to_string() children=move |id: String| { match CACHE.get(&id) { - Some(object) => match object.base_type() { - Some(apb::BaseType::Object(apb::ObjectType::Activity(_))) => view! { - + Some(item) => match item.base_type() { + Some(apb::BaseType::Object(apb::ObjectType::Activity(_))) => { + let author_id = item.actor().id().unwrap_or_default(); + let author = CACHE.get(&author_id).unwrap_or(serde_json::Value::String(author_id.clone())); + let object_id = item.object().id().unwrap_or_default(); + let object = CACHE.get(&object_id).map(|obj| view! { }); + view! { + + {object}
- }.into_view(), + }.into_view() + }, Some(apb::BaseType::Object(apb::ObjectType::Note)) => view! { - +
}.into_view(), _ => view! {

type not implemented


}.into_view(), @@ -88,7 +95,7 @@ async fn process_activities( activities: Vec, auth: Signal>, ) -> Vec { - use apb::{Activity, ActivityMut}; + use apb::ActivityMut; let mut sub_tasks = Vec::new(); let mut gonna_fetch = BTreeSet::new(); let mut out = Vec::new(); diff --git a/web/src/components/user.rs b/web/src/components/user.rs new file mode 100644 index 00000000..3fdef69b --- /dev/null +++ b/web/src/components/user.rs @@ -0,0 +1,42 @@ +use leptos::*; +use crate::prelude::*; + +use apb::{target::Addressed, Activity, Actor, Base, Object}; + + +#[component] +pub fn ActorBanner( + object: serde_json::Value, + #[prop(optional)] + tiny: bool +) -> impl IntoView { + match object { + serde_json::Value::String(id) => view! { + + }, + 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
+ } + } +} diff --git a/web/src/lib.rs b/web/src/lib.rs index f3ad655e..cdcdc3c9 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -1,13 +1,10 @@ mod app; mod auth; -mod timeline; -mod view; +mod components; mod page; mod control; pub use app::App; -pub use timeline::Timeline; -pub use auth::{Auth, AuthToken}; pub mod prelude; diff --git a/web/src/prelude.rs b/web/src/prelude.rs index 0022e1fe..231c8634 100644 --- a/web/src/prelude.rs +++ b/web/src/prelude.rs @@ -1,11 +1,9 @@ pub use crate::{ - AuthToken, - Auth, Timeline, Http, Uri, + Http, Uri, CACHE, URL_BASE, + auth::{Auth, AuthToken}, page::*, control::*, - view::*, - app::App, + components::*, auth::LoginBox, - timeline::TimelineFeed, }; diff --git a/web/src/view.rs b/web/src/view.rs deleted file mode 100644 index 30f84301..00000000 --- a/web/src/view.rs +++ /dev/null @@ -1,163 +0,0 @@ -use leptos::*; -use crate::prelude::*; - -use apb::{target::Addressed, Activity, Actor, Base, Object}; - - -#[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) { - "🌐" - } else if addressed.iter().any(|x| x.ends_with("/followers")) { - "🔒" - } else { - "🔗" - }; - let date = object.published().or(activity.published()); - let kind = activity.activity_type().unwrap_or(apb::ActivityType::Activity); - view! { -
- - - - - - - - -
- - - {kind.as_ref().to_string()} - {privacy} -
- - - -
-
- {match kind { - // post - apb::ActivityType::Create => view! { }.into_view(), - _ => view! {}.into_view(), - }} - } -} - -#[component] -pub fn ActorBanner( - object: serde_json::Value, - #[prop(optional)] - tiny: bool -) -> impl IntoView { - match object { - serde_json::Value::String(id) => view! { - - }, - 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 Object(object: serde_json::Value) -> impl IntoView { - let oid = object.id().unwrap_or_default().to_string(); - let in_reply_to = object.in_reply_to().id().unwrap_or_default(); - let summary = object.summary().unwrap_or_default().to_string(); - let content = dissolve::strip_html_tags(object.content().unwrap_or_default()); - let date = object.published(); - 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 !in_reply_to.is_empty() { - Some(view! { - - - - }) - } else { None }} - {move || if !summary.is_empty() { - Some(view! { - - - - }) - } else { None }} - - - - - - - -
- "in reply to "{Uri::pretty(&in_reply_to)} -
{summary.clone()}
{ - content.into_iter().map(|x| view! {

{x}

}).collect_view() - }
- - - -
-
- } -} - -#[component] -pub fn ObjectInline(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()); - view! { - {if summary.is_empty() { None } else { Some(view! { {summary} })}} -
- {content.into_iter().map(|x| view! {

{x}

}).collect_view()} -
- } -} - -#[component] -pub fn DateTime(t: Option>) -> impl IntoView { - match t { - Some(t) => { - let pretty = t.format("%Y/%m/%d %H:%M:%S").to_string(); - let rfc = t.to_rfc3339(); - Some(view! { - {pretty} - }) - }, - None => None, - } -}