From dc3758004b24dce0e99d80cd6c61482cece57b7b Mon Sep 17 00:00:00 2001 From: blankie Date: Fri, 24 Nov 2023 17:37:26 +1100 Subject: [PATCH] Add poll support --- models.cpp | 16 ++++++++++++++++ models.h | 15 +++++++++++++++ routes/css.cpp | 19 +++++++++++++++++++ routes/home.cpp | 1 + servehelper.cpp | 40 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 91 insertions(+) diff --git a/models.cpp b/models.cpp index 7ef6f42..34e0980 100644 --- a/models.cpp +++ b/models.cpp @@ -71,6 +71,19 @@ void from_json(const json& j, Media& media) { } } +void from_json(const json& j, PollOption& option) { + j.at("title").get_to(option.title); + j.at("votes_count").get_to(option.votes_count); +} + +void from_json(const json& j, Poll& poll) { + poll.expires_at = parse_rfc3339(j.at("expires_at").get_ref()); + j.at("expired").get_to(poll.expired); + j.at("voters_count").get_to(poll.voters_count); + j.at("options").get_to(poll.options); + j.at("emojis").get_to(poll.emojis); +} + void from_json(const json& j, Post& post) { j.at("id").get_to(post.id); post.created_at = parse_rfc3339(j.at("created_at").get_ref()); @@ -98,6 +111,9 @@ void from_json(const json& j, Post& post) { j.at("account").get_to(post.account); j.at("media_attachments").get_to(post.media_attachments); j.at("emojis").get_to(post.emojis); + if (!j.at("poll").is_null()) { + post.poll = j["poll"].get(); + } } void from_json(const json& j, PostContext& context) { diff --git a/models.h b/models.h index 54c6623..a833750 100644 --- a/models.h +++ b/models.h @@ -58,6 +58,18 @@ struct Media { std::optional description; }; +struct PollOption { + std::string title; + uint64_t votes_count; +}; +struct Poll { + time_t expires_at; + bool expired; + uint64_t voters_count; + std::vector options; + std::vector emojis; +}; + struct Post { std::string id; time_t created_at; @@ -74,6 +86,7 @@ struct Post { Account account; std::vector media_attachments; std::vector emojis; + std::optional poll; }; struct PostContext { @@ -85,5 +98,7 @@ void from_json(const nlohmann::json& j, Emoji& emoji); void from_json(const nlohmann::json& j, AccountField& field); void from_json(const nlohmann::json& j, Account& account); void from_json(const nlohmann::json& j, Media& media); +void from_json(const nlohmann::json& j, PollOption& option); +void from_json(const nlohmann::json& j, Poll& poll); void from_json(const nlohmann::json& j, Post& post); void from_json(const nlohmann::json& j, PostContext& context); diff --git a/routes/css.cpp b/routes/css.cpp index 6efce36..4e5ffee 100644 --- a/routes/css.cpp +++ b/routes/css.cpp @@ -125,6 +125,25 @@ svg { height: 40px; } +/* POLL */ +.poll-option { + margin-top: 0.5em; + margin-bottom: 0.5em; +} +.poll-percentage { + margin-right: 0.5em; +} +.poll-bar { + height: 0.5em; + display: block; + background-color: lightgray; + border-radius: 10em; + margin-top: 0.25em; +} +.poll-bar[width="0%"] { + width: 0.5em; +} + /* ERROR PAGE */ .error { text-align: center; diff --git a/routes/home.cpp b/routes/home.cpp index 4ace82c..0529f05 100644 --- a/routes/home.cpp +++ b/routes/home.cpp @@ -8,6 +8,7 @@ void home_route(const httplib::Request& req, httplib::Response& res) { Element("ul", { Element("li", {Element("a", {{"href", get_origin(req) + "/vt.social/@lina"}}, {"Asahi Lina (朝日リナ) // nullptr::live"})}), Element("li", {Element("a", {{"href", get_origin(req) + "/vt.social/@LucydiaLuminous/111290028216105435"}}, {"\"I love kids and their creativity. So I was explain…\""})}), + Element("li", {Element("a", {{"href", get_origin(req) + "/ruby.social/@CoralineAda/109951421922797743"}}, {"\"My partner just said \"I'm starting to think nostal…\""})}), }), Element("hr"), Element("p", { diff --git a/servehelper.cpp b/servehelper.cpp index 7ec0d79..c3a7832 100644 --- a/servehelper.cpp +++ b/servehelper.cpp @@ -25,6 +25,7 @@ struct PostStatus { }; static Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, bool main_post, const std::optional& post_status); static inline Element serialize_media(const Media& media); +static inline Element serialize_poll(const httplib::Request& req, const Poll& poll); class CurlUrlException : public std::exception { public: @@ -370,6 +371,7 @@ static Element serialize_post(const httplib::Request& req, const std::string& se const char* time_badge = post.edited_at < 0 ? "" : " (edited)"; Element contents("div", {{"class", "post-contents"}}, {preprocess_html(req, server, post.emojis, post.content)}); + Element post_attachments("div", {{"class", "post-attachments"}}, {}); post_attachments.nodes.reserve(post.media_attachments.size()); for (const Media& media : post.media_attachments) { @@ -377,6 +379,10 @@ static Element serialize_post(const httplib::Request& req, const std::string& se } contents.nodes.push_back(std::move(post_attachments)); + if (post.poll) { + contents.nodes.push_back(serialize_poll(req, *post.poll)); + } + if (post.sensitive) { std::string spoiler_text = !post.spoiler_text.empty() ? post.spoiler_text : "See more"; contents = Element("details", { @@ -453,3 +459,37 @@ static inline Element serialize_media(const Media& media) { return element; } + +static inline Element serialize_poll(const httplib::Request& req, const Poll& poll) { + Element div("div"); + + auto pick_form = [](uint64_t count, const char* singular, const char* plural) { + return count == 1 ? singular : plural; + }; + + div.nodes.reserve(poll.options.size() + 1); + for (const PollOption& option : poll.options) { + std::string percentage = poll.voters_count + ? std::to_string(option.votes_count * 100 / poll.voters_count) + '%' + : "0%"; + + div.nodes.push_back(Element("div", {{"class", "poll-option"}, {"title", std::to_string(option.votes_count) + pick_form(option.votes_count, " vote", " votes")}}, { + Element("b", {{"class", "poll-percentage"}}, {percentage}), " ", preprocess_html(req, poll.emojis, option.title), + Element("object", {{"class", "poll-bar"}, {"width", percentage}}, {}), + })); + } + + Element p("p", { + std::to_string(poll.voters_count), " ", pick_form(poll.voters_count, "voter", "voters"), + " / ", + }); + if (poll.expired) { + p.nodes.push_back("Expired"); + } else { + p.nodes.push_back("Expires in "); + p.nodes.push_back(relative_time(current_time(), poll.expires_at)); + } + div.nodes.push_back(std::move(p)); + + return div; +}