Преглед на файлове

Merge pull request #776 from iTzBoboCz/polls

Matthew Esposito преди 2 години
родител
ревизия
4a1b448abb
променени са 3 файла, в които са добавени 157 реда и са изтрити 5 реда
  1. 76 5
      src/utils.rs
  2. 44 0
      static/style.css
  3. 37 0
      templates/utils.html

+ 76 - 5
src/utils.rs

@@ -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

+ 44 - 0
static/style.css

@@ -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; }
 	

+ 37 - 0
templates/utils.html

@@ -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 %}