pub mod timeago; use std::borrow::{Borrow, Cow}; use std::collections::HashMap; use crate::resp_types::{ RespCommentInfo, RespMinimalAuthorInfo, RespMinimalCommentInfo, RespMinimalCommunityInfo, RespNotification, RespNotificationInfo, RespPostCommentInfo, RespPostInfo, RespPostListPost, RespThingComment, RespThingInfo, }; use crate::util::{abbreviate_link, author_is_me}; use crate::PageBaseData; pub use timeago::TimeAgo; #[render::component] pub fn Comment<'a>( comment: &'a RespPostCommentInfo<'a>, base_data: &'a PageBaseData, lang: &'a crate::Translator, ) { render::rsx! {
  • { if base_data.login.is_some() { Some(render::rsx! {
    { if comment.your_vote.is_some() { render::rsx! {
    } } else { render::rsx! {
    } } }
    }) } else { None } }
    {" "} { comment.attachments.iter().map(|attachment| { let href = &attachment.url; render::rsx! {
    {lang.tr("comment_attachment_prefix", None)} {" "} {abbreviate_link(&href)}{" ↗"}
    } }) .collect::>() }
    { if base_data.login.is_some() { Some(render::rsx! { {lang.tr("reply", None)} }) } else { None } } { if author_is_me(&comment.author, &base_data.login) || (comment.local && base_data.is_site_admin()) { Some(render::rsx! { {lang.tr("delete", None)} }) } else { None } }
    { if let Some(replies) = &comment.replies { Some(render::rsx! { }) } else { None } } { if comment.replies.is_none() && comment.has_replies { Some(render::rsx! { }) } else { None } }
  • } } pub struct CommunityLink<'community> { pub community: &'community RespMinimalCommunityInfo<'community>, } impl<'community> render::Render for CommunityLink<'community> { fn render_into(self, writer: &mut W) -> std::fmt::Result { let community = &self.community; let href = format!("/communities/{}", community.id); (render::rsx! { { (if community.local { community.name.as_ref().into() } else { Cow::Owned(format!("{}@{}", community.name, community.host)) }).as_ref() } }) .render_into(writer) } } pub trait HavingContent { fn content_text(&self) -> Option<&str>; fn content_html(&self) -> Option<&str>; } impl<'a> HavingContent for RespMinimalCommentInfo<'a> { fn content_text(&self) -> Option<&str> { self.content_text.as_deref() } fn content_html(&self) -> Option<&str> { self.content_html.as_deref() } } impl<'a> HavingContent for RespThingComment<'a> { fn content_text(&self) -> Option<&str> { self.base.content_text() } fn content_html(&self) -> Option<&str> { self.base.content_html() } } impl<'a> HavingContent for RespPostCommentInfo<'a> { fn content_text(&self) -> Option<&str> { self.base.content_text() } fn content_html(&self) -> Option<&str> { self.base.content_html() } } impl<'a> HavingContent for RespCommentInfo<'a> { fn content_text(&self) -> Option<&str> { self.base.content_text() } fn content_html(&self) -> Option<&str> { self.base.content_html() } } impl<'a> HavingContent for RespPostInfo<'a> { fn content_text(&self) -> Option<&str> { self.content_text.as_deref() } fn content_html(&self) -> Option<&str> { self.content_html.as_deref() } } pub struct Content<'a, T: HavingContent + 'a> { pub src: &'a T, } impl<'a, T: HavingContent + 'a> render::Render for Content<'a, T> { fn render_into(self, writer: &mut W) -> std::fmt::Result { match self.src.content_html() { Some(html) => { let cleaned = ammonia::clean(&html); writer.write_str("

    ")?; render::raw!(cleaned.as_ref()).render_into(writer)?; writer.write_str("

    ")?; } None => { if let Some(text) = self.src.content_text() { writer.write_str("

    ")?; text.render_into(writer)?; writer.write_str("

    ")?; } } } Ok(()) } } #[render::component] pub fn HTPage<'a, Children: render::Render>( base_data: &'a PageBaseData, lang: &'a crate::Translator, title: &'a str, children: Children, ) { render::rsx! { <> {title}
    { if let Some(login) = &base_data.login { Some(render::rsx! { <> { if login.user.has_unread_notifications { hitide_icons::NOTIFICATIONS_SOME.img() } else { hitide_icons::NOTIFICATIONS.img() } } {hitide_icons::PERSON.img()}
    }) } else { None } } { if base_data.login.is_none() { Some(render::rsx! { {lang.tr("login", None)} }) } else { None } }
    {children} } } #[render::component] pub fn PostItem<'a>( post: &'a RespPostListPost<'a>, in_community: bool, no_user: bool, lang: &'a crate::Translator, ) { render::rsx! {
  • {post.as_ref().as_ref().title.as_ref()} { if let Some(href) = &post.as_ref().href { Some(render::rsx! { <> {" "} {abbreviate_link(&href)}{" ↗"} }) } else { None } }
    {lang.tr("submitted", None)} {" "} { if no_user { None } else { Some(render::rsx! { <> {" "}{lang.tr("by", None)}{" "} }) } } { if !in_community { Some(render::rsx! { <>{" "}{lang.tr("to", None)}{" "} }) } else { None } } {" | "} {lang.tr("post_comments_count", Some(&fluent::fluent_args!["count" => post.replies_count_total])).into_owned()}
  • } } pub struct ThingItem<'a> { pub lang: &'a crate::Translator, pub thing: &'a RespThingInfo<'a>, } impl<'a> render::Render for ThingItem<'a> { fn render_into(self, writer: &mut W) -> std::fmt::Result { let lang = self.lang; match self.thing { RespThingInfo::Post(post) => { (PostItem { post, in_community: false, no_user: true, lang: self.lang }).render_into(writer) }, RespThingInfo::Comment(comment) => { (render::rsx! {
  • {lang.tr("comment", None)} {" "}{lang.tr("on", None)}{" "}{comment.post.title.as_ref()}{":"}
  • }).render_into(writer) } } } } pub struct UserLink<'user> { pub user: Option<&'user RespMinimalAuthorInfo<'user>>, } impl<'user> render::Render for UserLink<'user> { fn render_into(self, writer: &mut W) -> std::fmt::Result { match self.user { None => "[unknown]".render_into(writer), Some(user) => { let href = format!("/users/{}", user.id); (render::rsx! { { (if user.local { user.username.as_ref().into() } else { Cow::Owned(format!("{}@{}", user.username, user.host)) }).as_ref() } }) .render_into(writer) } } } } pub trait GetIndex { fn get(&self, key: K) -> Option<&V>; } impl + Eq + std::hash::Hash, V, Q: ?Sized + Eq + std::hash::Hash> GetIndex<&Q, V> for HashMap { fn get<'a>(&'a self, key: &Q) -> Option<&'a V> { HashMap::get(self, key) } } impl GetIndex for serde_json::Value { fn get(&self, key: I) -> Option<&serde_json::Value> { self.get(key) } } fn maybe_fill_value<'a, 'b, M: GetIndex<&'b str, serde_json::Value>>( values: &'a Option<&'a M>, name: &'b str, default_value: Option<&'a str>, ) -> &'a str { values .and_then(|values| values.get(name)) .and_then(serde_json::Value::as_str) .or(default_value) .unwrap_or("") } #[render::component] pub fn MaybeFillInput<'a, M: GetIndex<&'a str, serde_json::Value>>( values: &'a Option<&'a M>, r#type: &'a str, name: &'a str, required: bool, id: &'a str, ) { let value = maybe_fill_value(values, name, None); if required { render::rsx! { } } else { render::rsx! { } } } #[render::component] pub fn MaybeFillTextArea<'a, M: GetIndex<&'a str, serde_json::Value>>( values: &'a Option<&'a M>, name: &'a str, default_value: Option<&'a str>, ) { render::rsx! { } } #[render::component] pub fn BoolSubmitButton<'a>(value: bool, do_text: &'a str, done_text: &'a str) { if value { render::rsx! { } } else { render::rsx! { } } } pub struct NotificationItem<'a> { pub notification: &'a RespNotification<'a>, pub lang: &'a crate::Translator, } impl<'a> render::Render for NotificationItem<'a> { fn render_into(self, writer: &mut W) -> std::fmt::Result { let lang = self.lang; write!(writer, "
  • ")?; match &self.notification.info { RespNotificationInfo::Unknown => { "[unknown notification type]".render_into(writer)?; } RespNotificationInfo::PostReply { reply, post } => { (render::rsx! { <> {lang.tr("comment", None)} {" "}{lang.tr("on_your_post", None)}{" "}{post.title.as_ref()}{":"} }).render_into(writer)?; } RespNotificationInfo::CommentReply { reply, comment, post, } => { (render::rsx! { <> {lang.tr("reply_to", None)} {" "} {lang.tr("your_comment", None)} { if let Some(post) = post { Some(render::rsx! { <>{" "}{lang.tr("on", None)}{" "}{post.title.as_ref()} }) } else { None } } {":"} }).render_into(writer)?; } } write!(writer, "
  • ") } } pub trait IconExt { fn img(&self) -> render::SimpleElement<()>; } impl IconExt for hitide_icons::Icon { fn img(&self) -> render::SimpleElement<()> { render::rsx! { } } }