From c1c867a5ffa9bba5959d980b5712817119df6b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Pe=C5=A1ek?= Date: Thu, 23 Mar 2023 13:21:09 +0100 Subject: [PATCH] feat: add polls --- src/utils.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++ static/style.css | 43 +++++++++++++++++++++++++++++++ templates/utils.html | 31 ++++++++++++++++++++++ 3 files changed, 135 insertions(+) diff --git a/src/utils.rs b/src/utils.rs index d07efad..6dcfe93 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -96,6 +96,62 @@ pub struct Author { pub distinguished: String, } +pub struct Poll { + pub poll_options: Vec, + pub voting_end_timestamp: (String, String), + pub total_vote_count: u64, +} + +impl Poll { + pub fn parse(poll_data: &Value) -> Option { + if poll_data.as_object().is_none() { return None }; + + let total_vote_count = poll_data["total_vote_count"].as_u64()?; + // voting_end_timestamp is in the format of milliseconds + let voting_end_timestamp = time(poll_data["voting_end_timestamp"].as_f64()? / 1000.0); + let poll_options = PollOption::parse(&poll_data["options"]); + + Some(Self { + poll_options, + total_vote_count, + voting_end_timestamp + }) + } + + pub fn most_votes(&self) -> u64 { + self.poll_options.iter().map(|o| o.vote_count).max().unwrap_or(0) + } +} + +pub struct PollOption { + pub id: u64, + pub text: String, + pub vote_count: u64 +} + +impl PollOption { + pub fn parse(options: &Value) -> Vec { + options + .as_array() + .unwrap_or(&Vec::new()) + .iter() + .map(|option| { + // For each poll option + let id = option["id"].as_u64().unwrap_or_default(); + let text = option["text"].as_str().unwrap_or_default().to_owned(); + let vote_count = option["vote_count"].as_u64().unwrap_or_default(); + + // Construct PollOption items + Self { + id, + text, + vote_count + } + }) + .collect::>() + } +} + // Post flags with nsfw and stickied pub struct Flags { pub nsfw: bool, @@ -233,6 +289,7 @@ pub struct Post { pub body: String, pub author: Author, pub permalink: String, + pub poll: Option, pub score: (String, String), pub upvote_ratio: i64, pub post_type: String, @@ -342,6 +399,7 @@ impl Post { stickied: data["stickied"].as_bool().unwrap_or_default() || data["pinned"].as_bool().unwrap_or_default(), }, permalink: val(post, "permalink"), + poll: Poll::parse(&data["poll_data"]), rel_time, created, num_duplicates: post["data"]["num_duplicates"].as_u64().unwrap_or(0), @@ -600,6 +658,8 @@ pub async fn parse_post(post: &serde_json::Value) -> Post { let permalink = val(post, "permalink"); + let poll = Poll::parse(&post["data"]["poll_data"]); + let body = if val(post, "removed_by_category") == "moderator" { format!( "

[removed] — view removed post

", @@ -630,6 +690,7 @@ pub async fn parse_post(post: &serde_json::Value) -> Post { distinguished: val(post, "distinguished"), }, permalink, + poll, score: format_num(score), upvote_ratio: ratio as i64, post_type, diff --git a/static/style.css b/static/style.css index d64ad67..851d88b 100644 --- a/static/style.css +++ b/static/style.css @@ -752,6 +752,7 @@ a.search_subreddit:hover { "post_score post_title post_thumbnail" 1fr "post_score post_media post_thumbnail" auto "post_score post_body post_thumbnail" auto + "post_score post_poll post_thumbnail" auto "post_score post_notification post_thumbnail" auto "post_score post_footer post_thumbnail" auto / minmax(40px, auto) minmax(0, 1fr) fit-content(min(20%, 152px)); @@ -952,6 +953,43 @@ a.search_subreddit:hover { overflow-wrap: anywhere; } +.post_poll { + grid-area: post_poll; + padding: 5px 15px 5px 12px; +} + +.poll_option { + position: relative; + margin-right: 15px; + margin-top: 14px; + z-index: 0; + display: flex; + align-items: center; +} + +.poll_chart { + padding: 14px 0; + background-color: var(--accent); + opacity: 0.2; + border-radius: 5px; + z-index: -1; + position: absolute; +} + +.poll_option span { + margin-left: 8px; + color: var(--text); +} + +.poll_option span:nth-of-type(1) { + min-width: 10%; + font-weight: bold; +} + +.most_voted { + opacity: 0.45; +} + /* Used only for text post preview */ .post_preview { -webkit-mask-image: linear-gradient(180deg,#000 60%,transparent);; @@ -1563,6 +1601,7 @@ td, th { "post_title post_title post_thumbnail" 1fr "post_media post_media post_thumbnail" auto "post_body post_body post_thumbnail" auto + "post_poll post_poll post_thumbnail" auto "post_notification post_notification post_thumbnail" auto "post_score post_footer post_thumbnail" auto / auto 1fr fit-content(min(20%, 152px)); @@ -1572,6 +1611,10 @@ td, th { margin: 5px 0px 20px 15px; padding: 0; } + + .post_poll { + padding: 5px 15px 10px 12px; + } .compact .post_score { padding: 0; } diff --git a/templates/utils.html b/templates/utils.html index 3fdd76d..4bf7bdf 100644 --- a/templates/utils.html +++ b/templates/utils.html @@ -148,6 +148,9 @@
{{ post.body|safe }}
{{ post.score.0 }} Upvotes
+ + {% call poll(post) %} + {%- endmacro %} + +{% macro poll(post) -%} + {% match post.poll %} + {% when Some with (poll) %} + {% let widest = poll.most_votes() %} +
+ {{ poll.total_vote_count }} votes + {{ poll.voting_end_timestamp.0 }} + {% for option in poll.poll_options %} +
+ {# Posts without vote_count (all open polls) will show up as having 0 votes. + This is an issue with Reddit API, it doesn't work on Old Reddit either. #} + {% if option.vote_count == widest %} +
+ {% else %} +
+ {% endif %} + {{ option.vote_count }} + {{ option.text }} +
+ {% endfor %} +
+ {% when None %} + {% endmatch %} +{%- endmacro %}