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

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(&regex.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>
}
}