Merge pull request #776 from iTzBoboCz/polls

This commit is contained in:
Matthew Esposito 2023-04-17 18:12:02 -04:00 committed by GitHub
commit 4a1b448abb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 157 additions and 5 deletions

View file

@ -96,6 +96,61 @@ pub struct Author {
pub distinguished: String,
}
pub struct Poll {
pub poll_options: Vec<PollOption>,
pub voting_end_timestamp: (String, String),
pub total_vote_count: u64,
}
impl Poll {
pub fn parse(poll_data: &Value) -> Option<Self> {
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"])?;
Some(Self {
poll_options,
total_vote_count,
voting_end_timestamp,
})
}
pub fn most_votes(&self) -> u64 {
self.poll_options.iter().filter_map(|o| o.vote_count).max().unwrap_or(0)
}
}
pub struct PollOption {
pub id: u64,
pub text: String,
pub vote_count: Option<u64>,
}
impl PollOption {
pub fn parse(options: &Value) -> Option<Vec<Self>> {
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::<u64>().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::<Vec<Self>>(),
)
}
}
// Post flags with nsfw and stickied
pub struct Flags {
pub nsfw: bool,
@ -233,6 +288,7 @@ pub struct Post {
pub body: String,
pub author: Author,
pub permalink: String,
pub poll: Option<Poll>,
pub score: (String, String),
pub upvote_ratio: i64,
pub post_type: String,
@ -342,6 +398,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 +657,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!(
"<div class=\"md\"><p>[removed] — <a href=\"https://www.unddit.com{}\">view removed post</a></p></div>",
@ -630,6 +689,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,
@ -815,20 +875,31 @@ 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 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
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 time_delta <= Duration::days(30) {
if now < time {
rel_time += " left";
} else {
rel_time += " ago";
}
}
(
rel_time,
time

View file

@ -761,6 +761,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));
@ -961,6 +962,44 @@ 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;
width: 100%;
}
/* Used only for text post preview */
.post_preview {
-webkit-mask-image: linear-gradient(180deg,#000 60%,transparent);;
@ -1573,6 +1612,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));
@ -1582,6 +1622,10 @@ td, th {
margin: 5px 0px 20px 15px;
padding: 0;
}
.post_poll {
padding: 5px 15px 10px 12px;
}
.compact .post_score { padding: 0; }

View file

@ -148,6 +148,9 @@
<!-- POST BODY -->
<div class="post_body">{{ post.body|safe }}</div>
<div class="post_score" title="{{ post.score.1 }}">{{ post.score.0 }}<span class="label"> Upvotes</span></div>
{% call poll(post) %}
<div class="post_footer">
<ul id="post_links">
<li class="desktop_item"><a href="{{ post.permalink }}">permalink</a></li>
@ -272,6 +275,9 @@
<div class="post_body post_preview">
{{ post.body|safe }}
</div>
{% call poll(post) %}
<div class="post_footer">
<a href="{{ post.permalink }}" class="post_comments" title="{{ post.comments.1 }} {% if post.comments.1 == "1" %}comment{% else %}comments{% endif %}">{{ post.comments.0 }} {% if post.comments.1 == "1" %}comment{% else %}comments{% endif %}</a>
</div>
@ -299,3 +305,34 @@
</div>
</div>
{%- endmacro %}
{% macro poll(post) -%}
{% match post.poll %}
{% when Some with (poll) %}
{% let widest = poll.most_votes() %}
<div class="post_poll">
<span>{{ poll.total_vote_count }} votes,</span>
<span title="{{ poll.voting_end_timestamp.1 }}">{{ poll.voting_end_timestamp.0 }}</span>
{% for option in poll.poll_options %}
<div class="poll_option">
{# 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. #}
{% match option.vote_count %}
{% when Some with (vote_count) %}
{% if vote_count.eq(widest) || widest == 0 %}
<div class="poll_chart most_voted"></div>
{% else %}
<div class="poll_chart" style="width: {{ (vote_count * 100) / widest }}%"></div>
{% endif %}
<span>{{ vote_count }}</span>
{% when None %}
<div class="poll_chart most_voted"></div>
<span></span>
{% endmatch %}
<span>{{ option.text }}</span>
</div>
{% endfor %}
</div>
{% when None %}
{% endmatch %}
{%- endmacro %}