You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
211 lines
6.4 KiB
Rust
211 lines
6.4 KiB
Rust
use std::str::FromStr;
|
|
|
|
use chrono::DateTime;
|
|
use futures::executor;
|
|
use regex::Regex;
|
|
use yew::{function_component, html, AttrValue, Html, Renderer, ServerRenderer};
|
|
|
|
#[cfg(debug_assertions)]
|
|
use console_error_panic_hook::set_once as set_panic_hook;
|
|
#[cfg(debug_assertions)]
|
|
use yew::prelude::*;
|
|
|
|
use crate::{
|
|
data::{Attachment, CommonFields, Item, Outbox, Person, Post as DataPost},
|
|
error::Ærror,
|
|
};
|
|
|
|
#[derive(yew::Properties, PartialEq)]
|
|
struct Props {
|
|
pub outbox: Outbox,
|
|
pub author: Person,
|
|
pub archive_time: DateTime<chrono::Utc>,
|
|
}
|
|
|
|
#[derive(yew::Properties, PartialEq)]
|
|
struct PostProps {
|
|
pub meta: CommonFields,
|
|
pub post: DataPost,
|
|
pub author: Person,
|
|
}
|
|
|
|
#[cfg(not(debug_assertions))]
|
|
pub fn render_ssr(
|
|
outbox: Outbox,
|
|
author: Person,
|
|
archive_time: DateTime<chrono::Utc>,
|
|
) -> Result<String, Ærror> {
|
|
let output_template = include_str!("../index.html");
|
|
let output_string = executor::block_on(render_async(outbox, author, archive_time));
|
|
|
|
Ok(output_template.replace(
|
|
"<link data-trunk rel=\"rust\" href=\"Cargo.toml\" />",
|
|
&output_string,
|
|
))
|
|
}
|
|
|
|
#[cfg(debug_assertions)]
|
|
pub fn render_client(
|
|
outbox: Outbox,
|
|
author: Person,
|
|
archive_time: DateTime<chrono::Utc>,
|
|
) -> Result<(), Ærror> {
|
|
set_panic_hook();
|
|
Renderer::<Layout>::with_props(Props {
|
|
outbox,
|
|
author,
|
|
archive_time,
|
|
})
|
|
.render();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn render_async(
|
|
outbox: Outbox,
|
|
author: Person,
|
|
archive_time: DateTime<chrono::Utc>,
|
|
) -> String {
|
|
let renderer = ServerRenderer::<Layout>::with_props(move || -> Props {
|
|
Props {
|
|
outbox,
|
|
author,
|
|
archive_time,
|
|
}
|
|
})
|
|
.hydratable(false);
|
|
|
|
renderer.render().await
|
|
}
|
|
|
|
#[function_component]
|
|
fn Layout(props: &Props) -> Html {
|
|
html! {
|
|
<>
|
|
<ProfileHeader outbox={props.outbox.clone()} author={props.author.clone()} archive_time={props.archive_time}/>
|
|
<Posts outbox={props.outbox.clone()} author={props.author.clone()} archive_time={props.archive_time}/>
|
|
</>
|
|
}
|
|
}
|
|
|
|
#[function_component]
|
|
fn ProfileHeader(props: &Props) -> Html {
|
|
html! {
|
|
<div class="header">
|
|
<img class="banner" src="/media/header.png"/>
|
|
<img class="avatar" src="/media/avatar.png"/>
|
|
<h1 class="name">{props.author.name.as_str()}</h1>
|
|
<a href={props.author.url.clone()} class="username" target="_blank">
|
|
{props.author.full_username().unwrap().as_str()}
|
|
</a>
|
|
{ if props.author.attachments.is_some() {
|
|
html! {
|
|
<div class="profile-grid">
|
|
{
|
|
props.author.clone().attachments.unwrap().into_iter().filter_map(|attach| match attach {
|
|
Attachment::Property {name, value} => Some((name, value)),
|
|
_ => None,
|
|
}).map(|(name, value)| {
|
|
html! {
|
|
<div key={name.clone()} class="profile_property">
|
|
<span class="property_name">{name}</span>
|
|
<span class="property_value">{
|
|
if value.find("<a").is_some() {
|
|
Html::from_html_unchecked(AttrValue::from(value))
|
|
} else {
|
|
Html::from(value)
|
|
}
|
|
}</span>
|
|
</div>
|
|
}
|
|
}).collect::<Html>()
|
|
}
|
|
</div>
|
|
}
|
|
} else {
|
|
html! {
|
|
<></>
|
|
}
|
|
}}
|
|
<div class="bio">
|
|
{Html::from_html_unchecked(
|
|
AttrValue::from(
|
|
props.author.summary.replace("class=\"u-url mention\"", "class=\"mention\" target=\"_blank\"")
|
|
)
|
|
)}
|
|
</div>
|
|
<span class="archived_on">
|
|
{"Archived "}
|
|
{props.archive_time.format("%b %e, %Y")}
|
|
</span>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
#[function_component]
|
|
fn Posts(props: &Props) -> Html {
|
|
let public = &String::from_str("https://www.w3.org/ns/activitystreams#Public").unwrap();
|
|
let author_str = props.author.clone().id;
|
|
|
|
let mut ordered_items = props.outbox.clone().ordered_items;
|
|
ordered_items.reverse();
|
|
|
|
let posts: Vec<(CommonFields, DataPost)> = ordered_items
|
|
.into_iter()
|
|
.filter_map(|item| match item {
|
|
Item::Post { meta, object } => {
|
|
if meta.to.contains(public)
|
|
&& object.in_reply_to.is_none()
|
|
&& meta.actor == author_str
|
|
{
|
|
Some((meta, object))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
|
|
html! {
|
|
<>
|
|
<div class="posts-info">
|
|
{posts.len()}
|
|
{" posts"}
|
|
</div>
|
|
<div class="posts">
|
|
{posts.into_iter().map(|(meta, post)| {
|
|
html! {
|
|
<Post post={post} meta={meta} author={props.author.clone()}/>
|
|
}
|
|
}).collect::<Html>()}
|
|
</div>
|
|
</>
|
|
}
|
|
}
|
|
|
|
#[function_component]
|
|
fn Post(props: &PostProps) -> Html {
|
|
let post = props.post.clone();
|
|
let meta = props.meta.clone();
|
|
|
|
let post_time_utc = DateTime::parse_from_rfc3339(meta.published.as_str()).unwrap();
|
|
let post_time = DateTime::<chrono::offset::Local>::from(post_time_utc);
|
|
|
|
let mut content = String::from_str(&post.content).unwrap();
|
|
|
|
if post.quote_uri.is_some() {
|
|
let regex = Regex::new("<span class=\"quote-inline\">.*</span>").unwrap();
|
|
content = String::from_str(®ex.replace(&content.as_str(), "")).unwrap();
|
|
}
|
|
|
|
html! {
|
|
<div class="post">
|
|
<div class="post-contents">
|
|
{Html::from_html_unchecked(AttrValue::from(content))}
|
|
</div>
|
|
<span class="post-date">{post_time.format("%b %e, %Y, %H:%M")}</span>
|
|
</div>
|
|
}
|
|
}
|