From 5dc3279ac30d020f7f2bce1846d4e2a7241be877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Pe=C5=A1ek?= Date: Thu, 23 Mar 2023 13:18:48 +0100 Subject: [PATCH 1/7] fix: make time work with future dates --- src/utils.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index e6cb2f7..d07efad 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -815,20 +815,28 @@ pub fn format_num(num: i64) -> (String, String) { // Parse a relative and absolute time from a UNIX timestamp pub fn time(created: f64) -> (String, String) { let time = OffsetDateTime::from_unix_timestamp(created.round() as i64).unwrap_or(OffsetDateTime::UNIX_EPOCH); - let time_delta = OffsetDateTime::now_utc() - time; + let min = time.min(OffsetDateTime::now_utc()); + let max = time.max(OffsetDateTime::now_utc()); + let time_delta = max - min; // If the time difference is more than a month, show full date - let rel_time = if time_delta > Duration::days(30) { + let mut rel_time = if time_delta > Duration::days(30) { time.format(format_description!("[month repr:short] [day] '[year repr:last_two]")).unwrap_or_default() // Otherwise, show relative date/time } else if time_delta.whole_days() > 0 { - format!("{}d ago", time_delta.whole_days()) + format!("{}d", time_delta.whole_days()) } else if time_delta.whole_hours() > 0 { - format!("{}h ago", time_delta.whole_hours()) + format!("{}h", time_delta.whole_hours()) } else { - format!("{}m ago", time_delta.whole_minutes()) + format!("{}m", time_delta.whole_minutes()) }; + if OffsetDateTime::now_utc() < time { + rel_time += " left"; + } else { + rel_time += " ago"; + } + ( rel_time, time 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 2/7] 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 %} From 8bed342a6d7e2c5ee3888d4a98263285b580c658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Pe=C5=A1ek?= Date: Sat, 1 Apr 2023 13:21:15 +0200 Subject: [PATCH 3/7] fix: print time suffix only for relative dates --- src/utils.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 6dcfe93..769aa9d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -892,10 +892,12 @@ pub fn time(created: f64) -> (String, String) { format!("{}m", time_delta.whole_minutes()) }; - if OffsetDateTime::now_utc() < time { - rel_time += " left"; - } else { - rel_time += " ago"; + if time_delta <= Duration::days(30) { + if OffsetDateTime::now_utc() < time { + rel_time += " left"; + } else { + rel_time += " ago"; + } } ( From 75af98415445ab1457f3a2b78759ad286e665697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Pe=C5=A1ek?= Date: Sat, 1 Apr 2023 14:26:04 +0200 Subject: [PATCH 4/7] fix(polls): apply suggestions and fix id parsing --- src/utils.rs | 30 ++++++++++++++++-------------- templates/utils.html | 20 +++++++++++++------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 769aa9d..ad7b913 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -104,12 +104,12 @@ pub struct Poll { impl Poll { pub fn parse(poll_data: &Value) -> Option { - if poll_data.as_object().is_none() { return None }; + poll_data.as_object()?; 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"]); + let poll_options = PollOption::parse(&poll_data["options"])?; Some(Self { poll_options, @@ -119,36 +119,38 @@ impl Poll { } pub fn most_votes(&self) -> u64 { - self.poll_options.iter().map(|o| o.vote_count).max().unwrap_or(0) + self.poll_options.iter().map(|o| o.vote_count).flatten().max().unwrap_or(0) } } pub struct PollOption { pub id: u64, pub text: String, - pub vote_count: u64 + pub vote_count: Option } impl PollOption { - pub fn parse(options: &Value) -> Vec { - options - .as_array() - .unwrap_or(&Vec::new()) + pub fn parse(options: &Value) -> Option> { + Some(options + .as_array()? .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(); + + // we can't just use as_u64() because "id": String("...") and serde would parse it as None + let id = option["id"].as_str()?.parse::().ok()?; + let text = option["text"].as_str()?.to_owned(); + let vote_count = option["vote_count"].as_u64(); // Construct PollOption items - Self { + Some(Self { id, text, vote_count - } + }) }) - .collect::>() + .flatten() + .collect::>()) } } diff --git a/templates/utils.html b/templates/utils.html index 4bf7bdf..d5f3f69 100644 --- a/templates/utils.html +++ b/templates/utils.html @@ -315,14 +315,20 @@ {{ 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. + {# Posts without vote_count (all open polls) will show up without 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 }} + {% match option.vote_count %} + {% when Some with (vote_count) %} + {% if vote_count.eq(widest) || widest == 0 %} +
+ {% else %} +
+ {% endif %} + {{ vote_count }} + {% when None %} +
+ + {% endmatch %} {{ option.text }}
{% endfor %} From 94a781c82c02e475365526ea30e3727e4effb777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Pe=C5=A1ek?= Date: Sat, 1 Apr 2023 14:31:39 +0200 Subject: [PATCH 5/7] fix(polls): minor improvements --- static/style.css | 1 + templates/utils.html | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/static/style.css b/static/style.css index 851d88b..b8633d2 100644 --- a/static/style.css +++ b/static/style.css @@ -988,6 +988,7 @@ a.search_subreddit:hover { .most_voted { opacity: 0.45; + width: 100%; } /* Used only for text post preview */ diff --git a/templates/utils.html b/templates/utils.html index d5f3f69..8e77d26 100644 --- a/templates/utils.html +++ b/templates/utils.html @@ -311,8 +311,8 @@ {% when Some with (poll) %} {% let widest = poll.most_votes() %}
- {{ poll.total_vote_count }} votes - {{ poll.voting_end_timestamp.0 }} + {{ 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 without votes. @@ -320,13 +320,13 @@ {% match option.vote_count %} {% when Some with (vote_count) %} {% if vote_count.eq(widest) || widest == 0 %} -
+
{% else %}
{% endif %} {{ vote_count }} {% when None %} -
+
{% endmatch %} {{ option.text }} From ec226e0cabf5df520f98b50d2a51e85b249b0ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Pe=C5=A1ek?= Date: Sat, 8 Apr 2023 10:41:12 +0200 Subject: [PATCH 6/7] fix(polls): apply clippy suggestions --- src/utils.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index ad7b913..a8dbb61 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -119,7 +119,7 @@ impl Poll { } pub fn most_votes(&self) -> u64 { - self.poll_options.iter().map(|o| o.vote_count).flatten().max().unwrap_or(0) + self.poll_options.iter().filter_map(|o| o.vote_count).max().unwrap_or(0) } } @@ -134,7 +134,7 @@ impl PollOption { Some(options .as_array()? .iter() - .map(|option| { + .filter_map(|option| { // For each poll option // we can't just use as_u64() because "id": String("...") and serde would parse it as None @@ -149,7 +149,6 @@ impl PollOption { vote_count }) }) - .flatten() .collect::>()) } } From 991677cd1e0a1ed834f1207bd548b145dd303962 Mon Sep 17 00:00:00 2001 From: Matthew Esposito Date: Mon, 17 Apr 2023 18:00:41 -0400 Subject: [PATCH 7/7] Add variable for now_utc, format --- src/utils.rs | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index a8dbb61..32ea41b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -105,7 +105,7 @@ pub struct Poll { impl Poll { pub fn parse(poll_data: &Value) -> Option { poll_data.as_object()?; - + 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); @@ -114,7 +114,7 @@ impl Poll { Some(Self { poll_options, total_vote_count, - voting_end_timestamp + voting_end_timestamp, }) } @@ -126,30 +126,28 @@ impl Poll { pub struct PollOption { pub id: u64, pub text: String, - pub vote_count: Option + pub vote_count: Option, } impl PollOption { pub fn parse(options: &Value) -> Option> { - Some(options - .as_array()? - .iter() - .filter_map(|option| { - // For each poll option + Some( + options + .as_array()? + .iter() + .filter_map(|option| { + // For each poll option - // we can't just use as_u64() because "id": String("...") and serde would parse it as None - let id = option["id"].as_str()?.parse::().ok()?; - let text = option["text"].as_str()?.to_owned(); - let vote_count = option["vote_count"].as_u64(); + // we can't just use as_u64() because "id": String("...") and serde would parse it as None + let id = option["id"].as_str()?.parse::().ok()?; + let text = option["text"].as_str()?.to_owned(); + let vote_count = option["vote_count"].as_u64(); - // Construct PollOption items - Some(Self { - id, - text, - vote_count - }) - }) - .collect::>()) + // Construct PollOption items + Some(Self { id, text, vote_count }) + }) + .collect::>(), + ) } } @@ -877,8 +875,9 @@ pub fn format_num(num: i64) -> (String, String) { // Parse a relative and absolute time from a UNIX timestamp pub fn time(created: f64) -> (String, String) { let time = OffsetDateTime::from_unix_timestamp(created.round() as i64).unwrap_or(OffsetDateTime::UNIX_EPOCH); - let min = time.min(OffsetDateTime::now_utc()); - let max = time.max(OffsetDateTime::now_utc()); + let now = OffsetDateTime::now_utc(); + let min = time.min(now); + let max = time.max(now); let time_delta = max - min; // If the time difference is more than a month, show full date @@ -894,7 +893,7 @@ pub fn time(created: f64) -> (String, String) { }; if time_delta <= Duration::days(30) { - if OffsetDateTime::now_utc() < time { + if now < time { rel_time += " left"; } else { rel_time += " ago";