From 0617663b249f9ca488e5de652108b17d67fbaf45 Mon Sep 17 00:00:00 2001 From: Vika Date: Sat, 29 Jul 2023 21:59:56 +0300 Subject: Moved the entire Kittybox tree into the root --- templates/src/mf2.rs | 478 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 478 insertions(+) create mode 100644 templates/src/mf2.rs (limited to 'templates/src/mf2.rs') diff --git a/templates/src/mf2.rs b/templates/src/mf2.rs new file mode 100644 index 0000000..33cd84a --- /dev/null +++ b/templates/src/mf2.rs @@ -0,0 +1,478 @@ +use ellipse::Ellipse; + +pub static POSTS_PER_PAGE: usize = 20; + +/// Return a pretty location specifier from a geo: URI. +fn decode_geo_uri(uri: &str) -> String { + if let Some(part) = uri.split(':').collect::>().get(1) { + if let Some(part) = part.split(';').next() { + let mut parts = part.split(','); + let lat = parts.next().unwrap(); + let lon = parts.next().unwrap(); + + // TODO - format them as proper latitude and longitude + format!("{}, {}", lat, lon) + } else { + uri.to_string() + } + } else { + uri.to_string() + } +} + +markup::define! { + Entry<'a>(post: &'a serde_json::Value) { + @if post.pointer("/properties/like-of").is_none() && post.pointer("/properties/bookmark-of").is_none() { + @FullEntry { post } + } else { + // Show a mini-post. + @MiniEntry { post } + } + } + MiniEntry<'a>(post: &'a serde_json::Value) { + article."h-entry mini-entry" { + @if let Some(author) = post["properties"]["author"][0].as_object() { + span."mini-h-card"."u-author" { + a."u-author"[href=author["properties"]["uid"][0].as_str().unwrap()] { + @if let Some(photo) = author["properties"]["photo"][0].as_str() { + img[src=photo, loading="lazy"]; + } else if author["properties"]["photo"][0].is_object() { + img[ + src=author["properties"]["photo"][0]["value"].as_str().unwrap(), + alt=author["properties"]["photo"][0]["alt"].as_str().unwrap(), + loading="lazy" + ]; + } + @author["properties"]["name"][0].as_str().unwrap() + } + } + @if let Some(likeof) = post["properties"]["like-of"][0].as_str() { + " " + span."like-icon"["aria-label"="liked"] { + span."like-icon-label"["aria-hidden"="true"] { + "❤️" + } + } + " " + a."u-like-of"[href=likeof] { @likeof } + } else if let Some(likeof) = post["properties"]["like-of"][0].as_object() { + a."u-like-of"[href=likeof["properties"]["url"][0].as_str().unwrap()] { + @likeof["properties"]["name"][0] + .as_str() + .unwrap_or_else(|| likeof["properties"]["url"][0].as_str().unwrap()) + } + } + @if let Some(bookmarkof) = post["properties"]["bookmark-of"][0].as_str() { + " 🔖 " + a."u-bookmark-of"[href=bookmarkof] { @bookmarkof } + } else if let Some(bookmarkof) = post["properties"]["bookmark-of"][0].as_object() { + a."u-bookmark-of"[href=bookmarkof["properties"]["url"][0].as_str().unwrap()] { + @bookmarkof["properties"]["name"][0] + .as_str() + .unwrap_or_else(|| bookmarkof["properties"]["url"][0].as_str().unwrap()) + } + } + " " + a."u-url"."u-uid"[href=post["properties"]["uid"][0].as_str().unwrap()] { + @if let Some(published) = post["properties"]["published"][0].as_str() { + time."dt-published"[datetime=published] { + @chrono::DateTime::parse_from_rfc3339(published) + .map(|dt| dt.format("on %a %b %e %T %Y").to_string()) + .unwrap_or("sometime in the past".to_string()) + } + } else { + "sometime in the past" + } + } + } + } + } + FullEntry<'a>(post: &'a serde_json::Value) { + article."h-entry" { + header.metadata { + @if let Some(name) = post["properties"]["name"][0].as_str() { + h1."p-name" { @name } + } + @if let Some(author) = post["properties"]["author"][0].as_object() { + section."mini-h-card" { + a.larger."u-author"[href=author["properties"]["uid"][0].as_str().unwrap()] { + @if let Some(photo) = author["properties"]["photo"][0].as_str() { + img[src=photo, loading="lazy"]; + } else if let Some(photo) = author["properties"]["photo"][0].as_object() { + img[ + src=photo["value"].as_str().unwrap(), + alt=photo["alt"].as_str().unwrap(), + loading="lazy" + ]; + } + + @author["properties"]["name"][0].as_str().unwrap() + } + } + } + div { + span { + a."u-url"."u-uid"[href=post["properties"]["uid"][0].as_str().unwrap()] { + @if let Some(published) = post["properties"]["published"][0].as_str() { + time."dt-published"[datetime=published] { + @chrono::DateTime::parse_from_rfc3339(published) + .map(|dt| dt.format("%a %b %e %T %Y").to_string()) + .unwrap_or("sometime in the past".to_string()) + } + } + } + } + @if post["properties"]["visibility"][0].as_str().unwrap_or("public") != "public" { + span."p-visibility"[value=post["properties"]["visibility"][0].as_str().unwrap()] { + @post["properties"]["visibility"][0].as_str().unwrap() + } + } + @if post["properties"]["category"].is_array() { + span { + ul.categories { + "Tagged: " + @for cat in post["properties"]["category"].as_array().unwrap() { + li."p-category" { @cat.as_str().unwrap() } + } + } + } + } + @if post["properties"]["in-reply-to"].is_array() { + span { + "In reply to: " + ul.replyctx { + @for ctx in post["properties"]["in-reply-to"].as_array().unwrap() { + @if let Some(ctx) = ctx.as_str() { + li { + a."u-in-reply-to"[href=ctx] { + @ctx.truncate_ellipse(48).as_ref() + } + } + } else if let Some(ctx) = ctx.as_object() { + li { + a."u-in-reply-to"[href=ctx["properties"]["uid"][0] + .as_str() + .unwrap_or_else(|| ctx["properties"]["url"][0].as_str().unwrap())] + { + @ctx["properties"]["uid"][0] + .as_str() + .unwrap_or_else(|| ctx["properties"]["url"][0].as_str().unwrap()) + .truncate_ellipse(48) + .as_ref() + } + } + } + } + } + } + } + } + @if post["properties"]["url"].as_array().unwrap().len() > 1 { + hr; + ul { + "Pretty permalinks for this post:" + @for url in post["properties"]["url"].as_array().unwrap().iter().filter(|i| **i != post["properties"]["uid"][0]).map(|i| i.as_str().unwrap()) { + li { + a."u-url"[href=url] { @url } + } + } + } + } + @if let Some(links) = post["properties"]["syndication"].as_array() { + @if !links.is_empty() { + hr; + ul { + "Also published on:" + @for url in links.iter().filter_map(|i| i.as_str()) { + li { a."u-syndication"[href=url] { @url } } + } + } + } + } + @if post["properties"]["location"].is_array() || post["properties"]["checkin"].is_array() { + div { + @if post["properties"]["checkin"].is_array() { + span { + "Check-in to: " + @if post["properties"]["checkin"][0].is_string() { + // It's a URL + a."u-checkin"[href=post["properties"]["checkin"][0].as_str().unwrap()] { + @post["properties"]["checkin"][0].as_str().unwrap().truncate_ellipse(24).as_ref() + } + } else { + a."u-checkin"[href=post["properties"]["checkin"][0]["properties"]["uid"][0].as_str().unwrap()] { + @post["properties"]["checkin"][0]["properties"]["name"][0].as_str().unwrap() + } + } + } + } + @if post["properties"]["location"].is_array() { + span { + "Location: " + @if post["properties"]["location"][0].is_string() { + // It's a geo: URL + // We need to decode it + a."u-location"[href=post["properties"]["location"][0].as_str().unwrap()] { + @decode_geo_uri(post["properties"]["location"][0].as_str().unwrap()) + } + } else { + // It's an inner h-geo object + a."u-location"[href=post["properties"]["location"][0]["value"].as_str().map(|x| x.to_string()).unwrap_or(format!("geo:{},{}", post["properties"]["location"][0]["properties"]["latitude"][0].as_str().unwrap(), post["properties"]["location"][0]["properties"]["longitude"][0].as_str().unwrap()))] { + // I'm a lazy bitch + @decode_geo_uri(&post["properties"]["location"][0]["value"].as_str().map(|x| x.to_string()).unwrap_or(format!("geo:{},{}", post["properties"]["location"][0]["properties"]["latitude"][0].as_str().unwrap(), post["properties"]["location"][0]["properties"]["longitude"][0].as_str().unwrap()))) + } + } + } + } + } + } + @if post["properties"]["ate"].is_array() || post["properties"]["drank"].is_array() { + div { + @if post["properties"]["ate"].is_array() { + span { ul { + "Ate:" + @for food in post["properties"]["ate"].as_array().unwrap() { + li { + @if food.is_string() { + // If this is a string, it's a URL. + a."u-ate"[href=food.as_str().unwrap()] { + @food.as_str().unwrap().truncate_ellipse(24).as_ref() + } + } else { + // This is a rich food object (mm, sounds tasty! I wanna eat something tasty) + a."u-ate"[href=food["properties"]["uid"][0].as_str().unwrap_or("#")] { + @food["properties"]["name"][0].as_str() + .unwrap_or(food["properties"]["uid"][0].as_str().unwrap_or("#").truncate_ellipse(24).as_ref()) + } + } + } + } + } } + } + @if post["properties"]["drank"].is_array() { + span { ul { + "Drank:" + @for food in post["properties"]["drank"].as_array().unwrap() { + li { + @if food.is_string() { + // If this is a string, it's a URL. + a."u-drank"[href=food.as_str().unwrap()] { + @food.as_str().unwrap().truncate_ellipse(24).as_ref() + } + } else { + // This is a rich food object (mm, sounds tasty! I wanna eat something tasty) + a."u-drank"[href=food["properties"]["uid"][0].as_str().unwrap_or("#")] { + @food["properties"]["name"][0].as_str() + .unwrap_or(food["properties"]["uid"][0].as_str().unwrap_or("#").truncate_ellipse(24).as_ref()) + } + } + } + } + } } + } + } + } + } + @PhotoGallery { photos: post["properties"]["photo"].as_array() } + @if post["properties"]["content"][0]["html"].is_string() { + main."e-content" { + @markup::raw(post["properties"]["content"][0]["html"].as_str().unwrap().trim()) + } + } + @WebInteractions { post } + } + } + VCard<'a>(card: &'a serde_json::Value) { + article."h-card" { + @if card["properties"]["photo"][0].is_string() { + img."u-photo"[src=card["properties"]["photo"][0].as_str().unwrap()]; + } else if card["properties"]["photo"][0].is_object() { + img."u-photo"[ + src=card["properties"]["photo"][0]["value"].as_str().unwrap(), + alt=card["properties"]["photo"][0]["alt"].as_str().unwrap() + ]; + } + h1 { + a."u-url"."u-uid"."p-name"[href=card["properties"]["uid"][0].as_str().unwrap()] { + @card["properties"]["name"][0].as_str().unwrap() + } + } + @if card["properties"]["pronoun"].is_array() { + span { + "(" + @for (i, pronoun) in card["properties"]["pronoun"].as_array().unwrap().iter().filter_map(|v| v.as_str()).enumerate() { + span."p-pronoun" { + @pronoun + } + // Insert commas between multiple sets of pronouns + @if i < (card["properties"]["pronoun"].as_array().unwrap().len() - 1) {", "} + } + ")" + } + } + @if card["properties"]["note"].is_array() { + p."p-note" { + @card["properties"]["note"][0]["value"].as_str().unwrap_or_else(|| card["properties"]["note"][0].as_str().unwrap()) + } + } + @if card["properties"]["url"].is_array() { + ul { + "Can be found elsewhere at:" + @for url in card["properties"]["url"] + .as_array() + .unwrap() + .iter() + .filter_map(|v| v.as_str()) + .filter(|v| v != &card["properties"]["uid"][0].as_str().unwrap()) + .filter(|v| !card["properties"]["author"][0].as_str().is_some_and(|a| v.starts_with(a))) + { + li { a."u-url"[href=url, rel="me"] { @url } } + } + } + } + } + } + Food<'a>(food: &'a serde_json::Value) { + article."h-food" { + header.metadata { + h1 { + a."p-name"."u-url"[href=food["properties"]["url"][0].as_str().unwrap()] { + @food["properties"]["name"][0].as_str().unwrap() + } + } + } + @PhotoGallery { photos: food["properties"]["photo"].as_array() } + } + } + Feed<'a>(feed: &'a serde_json::Value, cursor: Option<&'a str>) { + div."h-feed" { + div.metadata { + @if feed["properties"]["name"][0].is_string() { + h1."p-name".titanic { + a[href=feed["properties"]["uid"][0].as_str().unwrap(), rel="feed"] { + @feed["properties"]["name"][0].as_str().unwrap() + } + } + } + } + @if feed["children"].is_array() { + @for child in feed["children"].as_array().unwrap() { + @match child["type"][0].as_str().unwrap() { + "h-entry" => { @Entry { post: child } } + "h-feed" => { @Feed { feed: child, cursor: None } } + "h-food" => { @Food { food: child } } + //"h-event" => { } + "h-card" => { @VCard { card: child } } + something_else => { + p { + "There's supposed to be an " + @something_else + " object here. But Kittybox can't render it right now." + small { "Sorry! TToTT" } + } + } + } + } + } + @if let Some(cursor) = cursor { + a[rel="prev", href=format!("{}?after={}", feed["properties"]["uid"][0].as_str().unwrap(), cursor)] { + "Older posts" + } + } else { + p { + "Looks like you reached the end. Wanna jump back to the " + a[href=feed["properties"]["uid"][0].as_str().unwrap()] { + "beginning" + } "?" + } + } + } + } + + //======================================= + // Components library + //======================================= + PhotoGallery<'a>(photos: Option<&'a Vec>) { + @if let Some(photos) = photos { + @for photo in photos.iter() { + @if let Some(photo) = photo.as_str() { + img."u-photo"[src=photo, loading="lazy"]; + } else if photo.is_object() { + @if let Some(thumbnail) = photo["thumbnail"].as_str() { + a."u-photo"[href=photo["value"].as_str().unwrap()] { + img[src=thumbnail, + loading="lazy", + alt=photo["alt"].as_str().unwrap_or("") + ]; + } + } else { + img."u-photo"[src=photo["value"].as_str().unwrap(), + loading="lazy", + alt=photo["alt"].as_str().unwrap_or("") + ]; + } + } + } + } + } + WebInteractions<'a>(post: &'a serde_json::Value) { + footer.webinteractions { + p[style="display: none", "aria-hidden"="false"] { + "Webmention counters:" + } + ul.counters { + li { + span."icon like-icon"["aria-label"="likes"] { + span."like-icon-label"["aria-hidden"="true"] { + "❤️" + } + } + span.counter { @post["properties"]["like"].as_array().map(|a| a.len()).unwrap_or(0) } + } + li { + span.icon["aria-label"="replies"] { "💬" } + span.counter { @post["properties"]["comment"].as_array().map(|a| a.len()).unwrap_or(0) } + } + li { + span.icon["aria-label"="reposts"] { "🔄" } + span.counter { @post["properties"]["repost"].as_array().map(|a| a.len()).unwrap_or(0) } + } + li { + span.icon["aria-label"="bookmarks"] { "🔖" } + span.counter { @post["properties"]["bookmark"].as_array().map(|a| a.len()).unwrap_or(0) } + } + } + /*@if ( + post["properties"]["like"].as_array().map(|a| a.len()).unwrap_or(0) + + post["properties"]["bookmark"].as_array().map(|a| a.len()).unwrap_or(0) + + post["properties"]["repost"].as_array().map(|a| a.len()).unwrap_or(0) + + post["properties"]["comment"].as_array().map(|a| a.len()).unwrap_or(0) + ) > 0 { + details { + summary { "Show comments and reactions" } + // TODO actually render facepiles and comments + @if let Some(likes) = post["properties"]["like"].as_array() { + @if !likes.is_empty() { + // Show a facepile of likes for a post + } + } + @if let Some(bookmarks) = post["properties"]["bookmark"].as_array() { + @if !bookmarks.is_empty() { + // Show a facepile of bookmarks for a post + } + } + @if let Some(reposts) = post["properties"]["repost"].as_array() { + @if !reposts.is_empty() { + // Show a facepile of reposts for a post + } + } + @if let Some(comments) = post["properties"]["comment"].as_array() { + @for comment in comments.iter() { + // Show all the comments recursively (so we could do Salmention with them) + } + } + } + }*/ + } + } +} -- cgit 1.4.1