Feat: search for comments within posts
Add the ability to search for specific comments within posts. Known issues: - Just like on reddit, this does not work with comment sorting. The sorting order is ignored during the search and changing the sorting order after the search does not change anything. I do not think we can fix this before reddit does, since in my understanding we rely on them for the sorting. However we could implement a default sorting method ourselves by taking the vector of comments returned from the search and sorting it manually. - The UI could be improved on mobile. On screens with a max width inferior to 480 pixels, the comment search bar is displayed below the comment sorting form. It would be great if we could make the search bar have the same width as the whole comment sorting form but I do not have the willpower to write any more css.
This commit is contained in:
parent
e25622dac2
commit
1e418619f1
4
build.rs
4
build.rs
@ -1,6 +1,4 @@
|
|||||||
use std::{
|
use std::process::{Command, ExitStatus, Output};
|
||||||
process::{Command, ExitStatus, Output},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
|
109
src/post.rs
109
src/post.rs
@ -8,6 +8,8 @@ use crate::utils::{
|
|||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::Regex;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
@ -20,13 +22,18 @@ struct PostTemplate {
|
|||||||
prefs: Preferences,
|
prefs: Preferences,
|
||||||
single_thread: bool,
|
single_thread: bool,
|
||||||
url: String,
|
url: String,
|
||||||
|
url_without_query: String,
|
||||||
|
comment_query: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static COMMENT_SEARCH_CAPTURE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"\?q=(.*)&type=comment"#).unwrap());
|
||||||
|
|
||||||
pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
// Build Reddit API path
|
// Build Reddit API path
|
||||||
let mut path: String = format!("{}.json?{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default());
|
let mut path: String = format!("{}.json?{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default());
|
||||||
let sub = req.param("sub").unwrap_or_default();
|
let sub = req.param("sub").unwrap_or_default();
|
||||||
let quarantined = can_access_quarantine(&req, &sub);
|
let quarantined = can_access_quarantine(&req, &sub);
|
||||||
|
let url = req.uri().to_string();
|
||||||
|
|
||||||
// Set sort to sort query parameter
|
// Set sort to sort query parameter
|
||||||
let sort = param(&path, "sort").unwrap_or_else(|| {
|
let sort = param(&path, "sort").unwrap_or_else(|| {
|
||||||
@ -63,17 +70,26 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
return Ok(nsfw_landing(req).await.unwrap_or_default());
|
return Ok(nsfw_landing(req).await.unwrap_or_default());
|
||||||
}
|
}
|
||||||
|
|
||||||
let comments = parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &req);
|
let query = match COMMENT_SEARCH_CAPTURE.captures(&url) {
|
||||||
let url = req.uri().to_string();
|
Some(captures) => captures.get(1).unwrap().as_str().replace("%20", " ").replace("+", " "),
|
||||||
|
None => String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let comments = match query.as_str() {
|
||||||
|
"" => parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &req),
|
||||||
|
_ => query_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req), &query, &req),
|
||||||
|
};
|
||||||
|
|
||||||
// Use the Post and Comment structs to generate a website to show users
|
// Use the Post and Comment structs to generate a website to show users
|
||||||
template(PostTemplate {
|
template(PostTemplate {
|
||||||
comments,
|
comments,
|
||||||
post,
|
post,
|
||||||
|
url_without_query: url.clone().trim_end_matches(&format!("?q={query}&type=comment")).to_string(),
|
||||||
sort,
|
sort,
|
||||||
prefs: Preferences::new(&req),
|
prefs: Preferences::new(&req),
|
||||||
single_thread,
|
single_thread,
|
||||||
url,
|
url,
|
||||||
|
comment_query: query,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// If the Reddit API returns an error, exit and send error page to user
|
// If the Reddit API returns an error, exit and send error page to user
|
||||||
@ -88,7 +104,8 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// COMMENTS
|
/* COMMENTS */
|
||||||
|
|
||||||
fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, highlighted_comment: &str, filters: &HashSet<String>, req: &Request<Body>) -> Vec<Comment> {
|
fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, highlighted_comment: &str, filters: &HashSet<String>, req: &Request<Body>) -> Vec<Comment> {
|
||||||
// Parse the comment JSON into a Vector of Comments
|
// Parse the comment JSON into a Vector of Comments
|
||||||
let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
|
let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
|
||||||
@ -97,8 +114,67 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
|
|||||||
comments
|
comments
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|comment| {
|
.map(|comment| {
|
||||||
let kind = comment["kind"].as_str().unwrap_or_default().to_string();
|
|
||||||
let data = &comment["data"];
|
let data = &comment["data"];
|
||||||
|
let replies: Vec<Comment> = if data["replies"].is_object() {
|
||||||
|
parse_comments(&data["replies"], post_link, post_author, highlighted_comment, filters, req)
|
||||||
|
} else {
|
||||||
|
Vec::with_capacity(0)
|
||||||
|
};
|
||||||
|
build_comment(&comment, data, replies, post_link, post_author, highlighted_comment, filters, req)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_comments(
|
||||||
|
json: &serde_json::Value,
|
||||||
|
post_link: &str,
|
||||||
|
post_author: &str,
|
||||||
|
highlighted_comment: &str,
|
||||||
|
filters: &HashSet<String>,
|
||||||
|
query: &str,
|
||||||
|
req: &Request<Body>,
|
||||||
|
) -> Vec<Comment> {
|
||||||
|
let comments = json["data"]["children"].as_array().map_or(Vec::new(), std::borrow::ToOwned::to_owned);
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
comments.into_iter().for_each(|comment| {
|
||||||
|
let data = &comment["data"];
|
||||||
|
|
||||||
|
// If this comment contains replies, handle those too
|
||||||
|
if data["replies"].is_object() {
|
||||||
|
results.append(&mut query_comments(&data["replies"], post_link, post_author, highlighted_comment, filters, query, req))
|
||||||
|
}
|
||||||
|
|
||||||
|
let c = build_comment(&comment, data, Vec::with_capacity(0), post_link, post_author, highlighted_comment, filters, req);
|
||||||
|
if c.body.to_lowercase().contains(&query.to_lowercase()) {
|
||||||
|
results.push(c);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_comment(
|
||||||
|
comment: &serde_json::Value,
|
||||||
|
data: &serde_json::Value,
|
||||||
|
replies: Vec<Comment>,
|
||||||
|
post_link: &str,
|
||||||
|
post_author: &str,
|
||||||
|
highlighted_comment: &str,
|
||||||
|
filters: &HashSet<String>,
|
||||||
|
req: &Request<Body>,
|
||||||
|
) -> Comment {
|
||||||
|
let id = val(&comment, "id");
|
||||||
|
|
||||||
|
let body = if (val(&comment, "author") == "[deleted]" && val(&comment, "body") == "[removed]") || val(&comment, "body") == "[ Removed by Reddit ]" {
|
||||||
|
format!(
|
||||||
|
"<div class=\"md\"><p>[removed] — <a href=\"https://www.unddit.com{}{}\">view removed comment</a></p></div>",
|
||||||
|
post_link, id
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
rewrite_urls(&val(&comment, "body_html"))
|
||||||
|
};
|
||||||
|
let kind = comment["kind"].as_str().unwrap_or_default().to_string();
|
||||||
|
|
||||||
let unix_time = data["created_utc"].as_f64().unwrap_or_default();
|
let unix_time = data["created_utc"].as_f64().unwrap_or_default();
|
||||||
let (rel_time, created) = time(unix_time);
|
let (rel_time, created) = time(unix_time);
|
||||||
@ -114,30 +190,13 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
|
|||||||
// Note that in certain (seemingly random) cases, the count is simply wrong.
|
// Note that in certain (seemingly random) cases, the count is simply wrong.
|
||||||
let more_count = data["count"].as_i64().unwrap_or_default();
|
let more_count = data["count"].as_i64().unwrap_or_default();
|
||||||
|
|
||||||
// If this comment contains replies, handle those too
|
|
||||||
let replies: Vec<Comment> = if data["replies"].is_object() {
|
|
||||||
parse_comments(&data["replies"], post_link, post_author, highlighted_comment, filters, req)
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let awards: Awards = Awards::parse(&data["all_awardings"]);
|
let awards: Awards = Awards::parse(&data["all_awardings"]);
|
||||||
|
|
||||||
let parent_kind_and_id = val(&comment, "parent_id");
|
let parent_kind_and_id = val(&comment, "parent_id");
|
||||||
let parent_info = parent_kind_and_id.split('_').collect::<Vec<&str>>();
|
let parent_info = parent_kind_and_id.split('_').collect::<Vec<&str>>();
|
||||||
|
|
||||||
let id = val(&comment, "id");
|
|
||||||
let highlighted = id == highlighted_comment;
|
let highlighted = id == highlighted_comment;
|
||||||
|
|
||||||
let body = if (val(&comment, "author") == "[deleted]" && val(&comment, "body") == "[removed]") || val(&comment, "body") == "[ Removed by Reddit ]" {
|
|
||||||
format!(
|
|
||||||
"<div class=\"md\"><p>[removed] — <a href=\"https://www.unddit.com{}{}\">view removed comment</a></p></div>",
|
|
||||||
post_link, id
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
rewrite_urls(&val(&comment, "body_html"))
|
|
||||||
};
|
|
||||||
|
|
||||||
let author = Author {
|
let author = Author {
|
||||||
name: val(&comment, "author"),
|
name: val(&comment, "author"),
|
||||||
flair: Flair {
|
flair: Flair {
|
||||||
@ -162,7 +221,7 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
|
|||||||
let is_stickied = data["stickied"].as_bool().unwrap_or_default();
|
let is_stickied = data["stickied"].as_bool().unwrap_or_default();
|
||||||
let collapsed = (is_moderator_comment && is_stickied) || is_filtered;
|
let collapsed = (is_moderator_comment && is_stickied) || is_filtered;
|
||||||
|
|
||||||
Comment {
|
return Comment {
|
||||||
id,
|
id,
|
||||||
kind,
|
kind,
|
||||||
parent_id: parent_info[1].to_string(),
|
parent_id: parent_info[1].to_string(),
|
||||||
@ -185,8 +244,6 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
|
|||||||
collapsed,
|
collapsed,
|
||||||
is_filtered,
|
is_filtered,
|
||||||
more_count,
|
more_count,
|
||||||
prefs: Preferences::new(req),
|
prefs: Preferences::new(&req),
|
||||||
}
|
};
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
@ -629,6 +629,15 @@ button.submit:hover > svg { stroke: var(--accent); }
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#commentQueryForms {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#allCommentsLink {
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
#sort, #search_sort {
|
#sort, #search_sort {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -1550,6 +1559,7 @@ td, th {
|
|||||||
#user, #sidebar { margin: 20px 0; }
|
#user, #sidebar { margin: 20px 0; }
|
||||||
#logo, #links { margin-bottom: 5px; }
|
#logo, #links { margin-bottom: 5px; }
|
||||||
#searchbox { width: calc(100vw - 35px); }
|
#searchbox { width: calc(100vw - 35px); }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 480px) {
|
@media screen and (max-width: 480px) {
|
||||||
@ -1623,4 +1633,9 @@ td, th {
|
|||||||
.popup-inner {
|
.popup-inner {
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#commentQueryForms {
|
||||||
|
display: initial;
|
||||||
|
justify-content: initial;
|
||||||
|
}
|
||||||
}
|
}
|
@ -35,7 +35,7 @@
|
|||||||
<div class="comment_body {% if highlighted %}highlighted{% endif %}">{{ body|safe }}</div>
|
<div class="comment_body {% if highlighted %}highlighted{% endif %}">{{ body|safe }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<blockquote class="replies">{% for c in replies -%}{{ c.render().unwrap()|safe }}{%- endfor %}
|
<blockquote class="replies">{% for c in replies -%}{{ c.render().unwrap()|safe }}{%- endfor %}
|
||||||
</blockquote>
|
</bockquote>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -43,11 +43,13 @@
|
|||||||
{% call utils::post(post) %}
|
{% call utils::post(post) %}
|
||||||
|
|
||||||
<!-- SORT FORM -->
|
<!-- SORT FORM -->
|
||||||
|
<div id="commentQueryForms">
|
||||||
<form id="sort">
|
<form id="sort">
|
||||||
<p id="comment_count">{{post.comments.0}} {% if post.comments.0 == "1" %}comment{% else %}comments{% endif %} <span id="sorted_by">sorted by </span></p>
|
<p id="comment_count">{{post.comments.0}} {% if post.comments.0 == "1" %}comment{% else %}comments{% endif %} <span id="sorted_by">sorted by </span></p>
|
||||||
<select name="sort" title="Sort comments by">
|
<select name="sort" title="Sort comments by" id="commentSortSelect">
|
||||||
{% call utils::options(sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
|
{% call utils::options(sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
|
||||||
</select><button id="sort_submit" class="submit">
|
</select>
|
||||||
|
<button id="sort_submit" class="submit">
|
||||||
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
|
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
|
||||||
<path d="M20 50 H100" />
|
<path d="M20 50 H100" />
|
||||||
<path d="M75 15 L100 50 L75 85" />
|
<path d="M75 15 L100 50 L75 85" />
|
||||||
@ -55,6 +57,18 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
<!-- SEARCH FORM -->
|
||||||
|
<form id="sort">
|
||||||
|
<input id="search" type="search" name="q" value="{{ comment_query }}" placeholder="Search comments">
|
||||||
|
<input type="hidden" name="type" value="comment">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% if comment_query != "" %}
|
||||||
|
Comments containing "{{ comment_query }}" | <a id="allCommentsLink" href="{{ url_without_query }}">All comments</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- COMMENTS -->
|
<!-- COMMENTS -->
|
||||||
{% for c in comments -%}
|
{% for c in comments -%}
|
||||||
|
Loading…
Reference in New Issue
Block a user