Filter subreddits and users (#317)
* Initial work on filtering subreddits and users * Fix doubly-prefixed subreddit name in search alt text (e.g. r/r/pics) * Don't set post title to "Comment" if empty - this could throw off actual posts with the title "Comment" * Filter search results * Fix filtering to differentiate between "this subject itself is filtered" vs "all posts on this current page have been filtered" * Remove unnecessary check * Clean up * Cargo format * Collapse comments from filtered users Co-authored-by: spikecodes <19519553+spikecodes@users.noreply.github.com>
This commit is contained in:
parent
beada1f2b2
commit
888e7b302d
@ -215,8 +215,10 @@ async fn main() {
|
|||||||
.at("/r/u_:name")
|
.at("/r/u_:name")
|
||||||
.get(|r| async move { Ok(redirect(format!("/user/{}", r.param("name").unwrap_or_default()))) }.boxed());
|
.get(|r| async move { Ok(redirect(format!("/user/{}", r.param("name").unwrap_or_default()))) }.boxed());
|
||||||
|
|
||||||
app.at("/r/:sub/subscribe").post(|r| subreddit::subscriptions(r).boxed());
|
app.at("/r/:sub/subscribe").post(|r| subreddit::subscriptions_filters(r).boxed());
|
||||||
app.at("/r/:sub/unsubscribe").post(|r| subreddit::subscriptions(r).boxed());
|
app.at("/r/:sub/unsubscribe").post(|r| subreddit::subscriptions_filters(r).boxed());
|
||||||
|
app.at("/r/:sub/filter").post(|r| subreddit::subscriptions_filters(r).boxed());
|
||||||
|
app.at("/r/:sub/unfilter").post(|r| subreddit::subscriptions_filters(r).boxed());
|
||||||
|
|
||||||
app.at("/r/:sub/comments/:id").get(|r| post::item(r).boxed());
|
app.at("/r/:sub/comments/:id").get(|r| post::item(r).boxed());
|
||||||
app.at("/r/:sub/comments/:id/:title").get(|r| post::item(r).boxed());
|
app.at("/r/:sub/comments/:id/:title").get(|r| post::item(r).boxed());
|
||||||
|
49
src/post.rs
49
src/post.rs
@ -4,11 +4,12 @@ use crate::esc;
|
|||||||
use crate::server::RequestExt;
|
use crate::server::RequestExt;
|
||||||
use crate::subreddit::{can_access_quarantine, quarantine};
|
use crate::subreddit::{can_access_quarantine, quarantine};
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
error, format_num, format_url, param, rewrite_urls, setting, template, time, val, Author, Awards, Comment, Flags, Flair, FlairPart, Media, Post, Preferences,
|
error, format_num, format_url, get_filters, param, rewrite_urls, setting, template, time, val, Author, Awards, Comment, Flags, Flair, FlairPart, Media, Post, Preferences,
|
||||||
};
|
};
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
@ -55,7 +56,7 @@ pub async fn item(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
// Parse the JSON into Post and Comment structs
|
// Parse the JSON into Post and Comment structs
|
||||||
let post = parse_post(&response[0]).await;
|
let post = parse_post(&response[0]).await;
|
||||||
let comments = parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment);
|
let comments = parse_comments(&response[1], &post.permalink, &post.author.name, highlighted_comment, &get_filters(&req));
|
||||||
let url = req.uri().to_string();
|
let url = req.uri().to_string();
|
||||||
|
|
||||||
// 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
|
||||||
@ -156,7 +157,7 @@ async fn parse_post(json: &serde_json::Value) -> Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// COMMENTS
|
// COMMENTS
|
||||||
fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, highlighted_comment: &str) -> Vec<Comment> {
|
fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str, highlighted_comment: &str, filters: &HashSet<String>) -> 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);
|
||||||
|
|
||||||
@ -177,7 +178,7 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
|
|||||||
|
|
||||||
// If this comment contains replies, handle those too
|
// If this comment contains replies, handle those too
|
||||||
let replies: Vec<Comment> = if data["replies"].is_object() {
|
let replies: Vec<Comment> = if data["replies"].is_object() {
|
||||||
parse_comments(&data["replies"], post_link, post_author, highlighted_comment)
|
parse_comments(&data["replies"], post_link, post_author, highlighted_comment, filters)
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
@ -190,23 +191,7 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
|
|||||||
let id = val(&comment, "id");
|
let id = val(&comment, "id");
|
||||||
let highlighted = id == highlighted_comment;
|
let highlighted = id == highlighted_comment;
|
||||||
|
|
||||||
// Many subreddits have a default comment posted about the sub's rules etc.
|
let author = Author {
|
||||||
// Many libreddit users do not wish to see this kind of comment by default.
|
|
||||||
// Reddit does not tell us which users are "bots", so a good heuristic is to
|
|
||||||
// collapse stickied moderator comments.
|
|
||||||
let is_moderator_comment = data["distinguished"].as_str().unwrap_or_default() == "moderator";
|
|
||||||
let is_stickied = data["stickied"].as_bool().unwrap_or_default();
|
|
||||||
let collapsed = is_moderator_comment && is_stickied;
|
|
||||||
|
|
||||||
Comment {
|
|
||||||
id,
|
|
||||||
kind,
|
|
||||||
parent_id: parent_info[1].to_string(),
|
|
||||||
parent_kind: parent_info[0].to_string(),
|
|
||||||
post_link: post_link.to_string(),
|
|
||||||
post_author: post_author.to_string(),
|
|
||||||
body,
|
|
||||||
author: Author {
|
|
||||||
name: val(&comment, "author"),
|
name: val(&comment, "author"),
|
||||||
flair: Flair {
|
flair: Flair {
|
||||||
flair_parts: FlairPart::parse(
|
flair_parts: FlairPart::parse(
|
||||||
@ -219,7 +204,26 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
|
|||||||
foreground_color: val(&comment, "author_flair_text_color"),
|
foreground_color: val(&comment, "author_flair_text_color"),
|
||||||
},
|
},
|
||||||
distinguished: val(&comment, "distinguished"),
|
distinguished: val(&comment, "distinguished"),
|
||||||
},
|
};
|
||||||
|
let is_filtered = filters.contains(&["u_", author.name.as_str()].concat());
|
||||||
|
|
||||||
|
// Many subreddits have a default comment posted about the sub's rules etc.
|
||||||
|
// Many libreddit users do not wish to see this kind of comment by default.
|
||||||
|
// Reddit does not tell us which users are "bots", so a good heuristic is to
|
||||||
|
// collapse stickied moderator comments.
|
||||||
|
let is_moderator_comment = data["distinguished"].as_str().unwrap_or_default() == "moderator";
|
||||||
|
let is_stickied = data["stickied"].as_bool().unwrap_or_default();
|
||||||
|
let collapsed = (is_moderator_comment && is_stickied) || is_filtered;
|
||||||
|
|
||||||
|
Comment {
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
parent_id: parent_info[1].to_string(),
|
||||||
|
parent_kind: parent_info[0].to_string(),
|
||||||
|
post_link: post_link.to_string(),
|
||||||
|
post_author: post_author.to_string(),
|
||||||
|
body,
|
||||||
|
author,
|
||||||
score: if data["score_hidden"].as_bool().unwrap_or_default() {
|
score: if data["score_hidden"].as_bool().unwrap_or_default() {
|
||||||
("\u{2022}".to_string(), "Hidden".to_string())
|
("\u{2022}".to_string(), "Hidden".to_string())
|
||||||
} else {
|
} else {
|
||||||
@ -232,6 +236,7 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
|
|||||||
highlighted,
|
highlighted,
|
||||||
awards,
|
awards,
|
||||||
collapsed,
|
collapsed,
|
||||||
|
is_filtered,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// CRATES
|
// CRATES
|
||||||
use crate::utils::{catch_random, error, format_num, format_url, param, redirect, setting, template, val, Post, Preferences};
|
use crate::utils::{catch_random, error, filter_posts, format_num, format_url, get_filters, param, redirect, setting, template, val, Post, Preferences};
|
||||||
use crate::{
|
use crate::{
|
||||||
client::json,
|
client::json,
|
||||||
subreddit::{can_access_quarantine, quarantine},
|
subreddit::{can_access_quarantine, quarantine},
|
||||||
@ -37,6 +37,11 @@ struct SearchTemplate {
|
|||||||
params: SearchParams,
|
params: SearchParams,
|
||||||
prefs: Preferences,
|
prefs: Preferences,
|
||||||
url: String,
|
url: String,
|
||||||
|
/// Whether the subreddit itself is filtered.
|
||||||
|
is_filtered: bool,
|
||||||
|
/// Whether all fetched posts are filtered (to differentiate between no posts fetched in the first place,
|
||||||
|
/// and all fetched posts being filtered).
|
||||||
|
all_posts_filtered: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// SERVICES
|
// SERVICES
|
||||||
@ -59,14 +64,45 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
let typed = param(&path, "type").unwrap_or_default();
|
let typed = param(&path, "type").unwrap_or_default();
|
||||||
|
|
||||||
let sort = param(&path, "sort").unwrap_or_else(|| "relevance".to_string());
|
let sort = param(&path, "sort").unwrap_or_else(|| "relevance".to_string());
|
||||||
|
let filters = get_filters(&req);
|
||||||
|
|
||||||
// If search is not restricted to this subreddit, show other subreddits in search results
|
// If search is not restricted to this subreddit, show other subreddits in search results
|
||||||
let subreddits = param(&path, "restrict_sr").map_or(search_subreddits(&query, &typed).await, |_| Vec::new());
|
let subreddits = if param(&path, "restrict_sr").is_none() {
|
||||||
|
let mut subreddits = search_subreddits(&query, &typed).await;
|
||||||
|
subreddits.retain(|s| !filters.contains(s.name.as_str()));
|
||||||
|
subreddits
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
||||||
|
|
||||||
match Post::fetch(&path, String::new(), quarantined).await {
|
// If all requested subs are filtered, we don't need to fetch posts.
|
||||||
Ok((posts, after)) => template(SearchTemplate {
|
if sub.split("+").all(|s| filters.contains(s)) {
|
||||||
|
template(SearchTemplate {
|
||||||
|
posts: Vec::new(),
|
||||||
|
subreddits,
|
||||||
|
sub,
|
||||||
|
params: SearchParams {
|
||||||
|
q: query.replace('"', """),
|
||||||
|
sort,
|
||||||
|
t: param(&path, "t").unwrap_or_default(),
|
||||||
|
before: param(&path, "after").unwrap_or_default(),
|
||||||
|
after: "".to_string(),
|
||||||
|
restrict_sr: param(&path, "restrict_sr").unwrap_or_default(),
|
||||||
|
typed,
|
||||||
|
},
|
||||||
|
prefs: Preferences::new(req),
|
||||||
|
url,
|
||||||
|
is_filtered: true,
|
||||||
|
all_posts_filtered: false,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
match Post::fetch(&path, quarantined).await {
|
||||||
|
Ok((mut posts, after)) => {
|
||||||
|
let all_posts_filtered = filter_posts(&mut posts, &filters);
|
||||||
|
|
||||||
|
template(SearchTemplate {
|
||||||
posts,
|
posts,
|
||||||
subreddits,
|
subreddits,
|
||||||
sub,
|
sub,
|
||||||
@ -81,7 +117,10 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
},
|
},
|
||||||
prefs: Preferences::new(req),
|
prefs: Preferences::new(req),
|
||||||
url,
|
url,
|
||||||
}),
|
is_filtered: false,
|
||||||
|
all_posts_filtered,
|
||||||
|
})
|
||||||
|
}
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
if msg == "quarantined" {
|
if msg == "quarantined" {
|
||||||
let sub = req.param("sub").unwrap_or_default();
|
let sub = req.param("sub").unwrap_or_default();
|
||||||
@ -91,6 +130,7 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn search_subreddits(q: &str, typed: &str) -> Vec<Subreddit> {
|
async fn search_subreddits(q: &str, typed: &str) -> Vec<Subreddit> {
|
||||||
@ -109,7 +149,7 @@ async fn search_subreddits(q: &str, typed: &str) -> Vec<Subreddit> {
|
|||||||
let icon = subreddit["data"]["community_icon"].as_str().map_or_else(|| val(subreddit, "icon_img"), ToString::to_string);
|
let icon = subreddit["data"]["community_icon"].as_str().map_or_else(|| val(subreddit, "icon_img"), ToString::to_string);
|
||||||
|
|
||||||
Subreddit {
|
Subreddit {
|
||||||
name: val(subreddit, "display_name_prefixed"),
|
name: val(subreddit, "display_name"),
|
||||||
url: val(subreddit, "url"),
|
url: val(subreddit, "url"),
|
||||||
icon: format_url(&icon),
|
icon: format_url(&icon),
|
||||||
description: val(subreddit, "public_description"),
|
description: val(subreddit, "public_description"),
|
||||||
|
@ -109,7 +109,7 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body
|
|||||||
|
|
||||||
let mut response = redirect(path);
|
let mut response = redirect(path);
|
||||||
|
|
||||||
for name in [PREFS.to_vec(), vec!["subscriptions"]].concat() {
|
for name in [PREFS.to_vec(), vec!["subscriptions", "filters"]].concat() {
|
||||||
match form.get(name) {
|
match form.get(name) {
|
||||||
Some(value) => response.insert_cookie(
|
Some(value) => response.insert_cookie(
|
||||||
Cookie::build(name.to_owned(), value.clone())
|
Cookie::build(name.to_owned(), value.clone())
|
||||||
|
113
src/subreddit.rs
113
src/subreddit.rs
@ -1,6 +1,8 @@
|
|||||||
// CRATES
|
// CRATES
|
||||||
use crate::esc;
|
use crate::esc;
|
||||||
use crate::utils::{catch_random, error, format_num, format_url, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit};
|
use crate::utils::{
|
||||||
|
catch_random, error, filter_posts, format_num, format_url, get_filters, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit,
|
||||||
|
};
|
||||||
use crate::{client::json, server::ResponseExt, RequestExt};
|
use crate::{client::json, server::ResponseExt, RequestExt};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use cookie::Cookie;
|
use cookie::Cookie;
|
||||||
@ -17,6 +19,11 @@ struct SubredditTemplate {
|
|||||||
ends: (String, String),
|
ends: (String, String),
|
||||||
prefs: Preferences,
|
prefs: Preferences,
|
||||||
url: String,
|
url: String,
|
||||||
|
/// Whether the subreddit itself is filtered.
|
||||||
|
is_filtered: bool,
|
||||||
|
/// Whether all fetched posts are filtered (to differentiate between no posts fetched in the first place,
|
||||||
|
/// and all fetched posts being filtered).
|
||||||
|
all_posts_filtered: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
@ -48,7 +55,7 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
|
let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string());
|
||||||
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort));
|
let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort));
|
||||||
|
|
||||||
let sub = req.param("sub").unwrap_or(if front_page == "default" || front_page.is_empty() {
|
let sub_name = req.param("sub").unwrap_or(if front_page == "default" || front_page.is_empty() {
|
||||||
if subscribed.is_empty() {
|
if subscribed.is_empty() {
|
||||||
"popular".to_string()
|
"popular".to_string()
|
||||||
} else {
|
} else {
|
||||||
@ -57,43 +64,58 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
} else {
|
} else {
|
||||||
front_page.clone()
|
front_page.clone()
|
||||||
});
|
});
|
||||||
let quarantined = can_access_quarantine(&req, &sub) || root;
|
let quarantined = can_access_quarantine(&req, &sub_name) || root;
|
||||||
|
|
||||||
// Handle random subreddits
|
// Handle random subreddits
|
||||||
if let Ok(random) = catch_random(&sub, "").await {
|
if let Ok(random) = catch_random(&sub_name, "").await {
|
||||||
return Ok(random);
|
return Ok(random);
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.param("sub").is_some() && sub.starts_with("u_") {
|
if req.param("sub").is_some() && sub_name.starts_with("u_") {
|
||||||
return Ok(redirect(["/user/", &sub[2..]].concat()));
|
return Ok(redirect(["/user/", &sub_name[2..]].concat()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = format!("/r/{}/{}.json?{}&raw_json=1", sub, sort, req.uri().query().unwrap_or_default());
|
// Request subreddit metadata
|
||||||
|
let sub = if !sub_name.contains('+') && sub_name != subscribed && sub_name != "popular" && sub_name != "all" {
|
||||||
match Post::fetch(&path, String::new(), quarantined).await {
|
|
||||||
Ok((posts, after)) => {
|
|
||||||
// If you can get subreddit posts, also request subreddit metadata
|
|
||||||
let sub = if !sub.contains('+') && sub != subscribed && sub != "popular" && sub != "all" {
|
|
||||||
// Regular subreddit
|
// Regular subreddit
|
||||||
subreddit(&sub, quarantined).await.unwrap_or_default()
|
subreddit(&sub_name, quarantined).await.unwrap_or_default()
|
||||||
} else if sub == subscribed {
|
} else if sub_name == subscribed {
|
||||||
// Subscription feed
|
// Subscription feed
|
||||||
if req.uri().path().starts_with("/r/") {
|
if req.uri().path().starts_with("/r/") {
|
||||||
subreddit(&sub, quarantined).await.unwrap_or_default()
|
subreddit(&sub_name, quarantined).await.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
Subreddit::default()
|
Subreddit::default()
|
||||||
}
|
}
|
||||||
} else if sub.contains('+') {
|
} else if sub_name.contains('+') {
|
||||||
// Multireddit
|
// Multireddit
|
||||||
Subreddit {
|
Subreddit {
|
||||||
name: sub,
|
name: sub_name.clone(),
|
||||||
..Subreddit::default()
|
..Subreddit::default()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Subreddit::default()
|
Subreddit::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let path = format!("/r/{}/{}.json?{}&raw_json=1", sub_name.clone(), sort, req.uri().query().unwrap_or_default());
|
||||||
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
||||||
|
let filters = get_filters(&req);
|
||||||
|
|
||||||
|
// If all requested subs are filtered, we don't need to fetch posts.
|
||||||
|
if sub_name.split("+").all(|s| filters.contains(s)) {
|
||||||
|
template(SubredditTemplate {
|
||||||
|
sub,
|
||||||
|
posts: Vec::new(),
|
||||||
|
sort: (sort, param(&path, "t").unwrap_or_default()),
|
||||||
|
ends: (param(&path, "after").unwrap_or_default(), "".to_string()),
|
||||||
|
prefs: Preferences::new(req),
|
||||||
|
url,
|
||||||
|
is_filtered: true,
|
||||||
|
all_posts_filtered: false,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
match Post::fetch(&path, quarantined).await {
|
||||||
|
Ok((mut posts, after)) => {
|
||||||
|
let all_posts_filtered = filter_posts(&mut posts, &filters);
|
||||||
|
|
||||||
template(SubredditTemplate {
|
template(SubredditTemplate {
|
||||||
sub,
|
sub,
|
||||||
@ -102,15 +124,18 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
ends: (param(&path, "after").unwrap_or_default(), after),
|
ends: (param(&path, "after").unwrap_or_default(), after),
|
||||||
prefs: Preferences::new(req),
|
prefs: Preferences::new(req),
|
||||||
url,
|
url,
|
||||||
|
is_filtered: false,
|
||||||
|
all_posts_filtered,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(msg) => match msg.as_str() {
|
Err(msg) => match msg.as_str() {
|
||||||
"quarantined" => quarantine(req, sub),
|
"quarantined" => quarantine(req, sub_name),
|
||||||
"private" => error(req, format!("r/{} is a private community", sub)).await,
|
"private" => error(req, format!("r/{} is a private community", sub_name)).await,
|
||||||
"banned" => error(req, format!("r/{} has been banned from Reddit", sub)).await,
|
"banned" => error(req, format!("r/{} has been banned from Reddit", sub_name)).await,
|
||||||
_ => error(req, msg).await,
|
_ => error(req, msg).await,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn quarantine(req: Request<Body>, sub: String) -> Result<Response<Body>, String> {
|
pub fn quarantine(req: Request<Body>, sub: String) -> Result<Response<Body>, String> {
|
||||||
@ -150,18 +175,25 @@ pub fn can_access_quarantine(req: &Request<Body>, sub: &str) -> bool {
|
|||||||
setting(req, &format!("allow_quaran_{}", sub.to_lowercase())).parse().unwrap_or_default()
|
setting(req, &format!("allow_quaran_{}", sub.to_lowercase())).parse().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sub or unsub by setting subscription cookie using response "Set-Cookie" header
|
// Sub, filter, unfilter, or unsub by setting subscription cookie using response "Set-Cookie" header
|
||||||
pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String> {
|
pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
let sub = req.param("sub").unwrap_or_default();
|
let sub = req.param("sub").unwrap_or_default();
|
||||||
|
let action: Vec<String> = req.uri().path().split('/').map(String::from).collect();
|
||||||
|
|
||||||
// Handle random subreddits
|
// Handle random subreddits
|
||||||
if sub == "random" || sub == "randnsfw" {
|
if sub == "random" || sub == "randnsfw" {
|
||||||
|
if action.contains(&"filter".to_string()) || action.contains(&"unfilter".to_string()) {
|
||||||
|
return Err("Can't filter random subreddit!".to_string());
|
||||||
|
} else {
|
||||||
return Err("Can't subscribe to random subreddit!".to_string());
|
return Err("Can't subscribe to random subreddit!".to_string());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let query = req.uri().query().unwrap_or_default().to_string();
|
let query = req.uri().query().unwrap_or_default().to_string();
|
||||||
let action: Vec<String> = req.uri().path().split('/').map(String::from).collect();
|
|
||||||
|
|
||||||
let mut sub_list = Preferences::new(req).subscriptions;
|
let preferences = Preferences::new(req);
|
||||||
|
let mut sub_list = preferences.subscriptions;
|
||||||
|
let mut filters = preferences.filters;
|
||||||
|
|
||||||
// Retrieve list of posts for these subreddits to extract display names
|
// Retrieve list of posts for these subreddits to extract display names
|
||||||
let posts = json(format!("/r/{}/hot.json?raw_json=1", sub), true).await?;
|
let posts = json(format!("/r/{}/hot.json?raw_json=1", sub), true).await?;
|
||||||
@ -182,8 +214,10 @@ pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String>
|
|||||||
for part in sub.split('+') {
|
for part in sub.split('+') {
|
||||||
// Retrieve display name for the subreddit
|
// Retrieve display name for the subreddit
|
||||||
let display;
|
let display;
|
||||||
let part = if let Some(&(_, display)) = display_lookup.iter().find(|x| x.0 == part.to_lowercase()) {
|
let part = if part.starts_with("u_") {
|
||||||
// This is already known, doesn't require seperate request
|
part
|
||||||
|
} else if let Some(&(_, display)) = display_lookup.iter().find(|x| x.0 == part.to_lowercase()) {
|
||||||
|
// This is already known, doesn't require separate request
|
||||||
display
|
display
|
||||||
} else {
|
} else {
|
||||||
// This subreddit display name isn't known, retrieve it
|
// This subreddit display name isn't known, retrieve it
|
||||||
@ -196,16 +230,28 @@ pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String>
|
|||||||
if action.contains(&"subscribe".to_string()) && !sub_list.contains(&part.to_owned()) {
|
if action.contains(&"subscribe".to_string()) && !sub_list.contains(&part.to_owned()) {
|
||||||
// Add each sub name to the subscribed list
|
// Add each sub name to the subscribed list
|
||||||
sub_list.push(part.to_owned());
|
sub_list.push(part.to_owned());
|
||||||
// Reorder sub names alphabettically
|
filters.retain(|s| s.to_lowercase() != part.to_lowercase());
|
||||||
|
// Reorder sub names alphabetically
|
||||||
sub_list.sort_by_key(|a| a.to_lowercase());
|
sub_list.sort_by_key(|a| a.to_lowercase());
|
||||||
|
filters.sort_by_key(|a| a.to_lowercase());
|
||||||
} else if action.contains(&"unsubscribe".to_string()) {
|
} else if action.contains(&"unsubscribe".to_string()) {
|
||||||
// Remove sub name from subscribed list
|
// Remove sub name from subscribed list
|
||||||
sub_list.retain(|s| s.to_lowercase() != part.to_lowercase());
|
sub_list.retain(|s| s.to_lowercase() != part.to_lowercase());
|
||||||
|
} else if action.contains(&"filter".to_string()) && !filters.contains(&part.to_owned()) {
|
||||||
|
// Add each sub name to the filtered list
|
||||||
|
filters.push(part.to_owned());
|
||||||
|
sub_list.retain(|s| s.to_lowercase() != part.to_lowercase());
|
||||||
|
// Reorder sub names alphabetically
|
||||||
|
filters.sort_by_key(|a| a.to_lowercase());
|
||||||
|
sub_list.sort_by_key(|a| a.to_lowercase());
|
||||||
|
} else if action.contains(&"unfilter".to_string()) {
|
||||||
|
// Remove sub name from filtered list
|
||||||
|
filters.retain(|s| s.to_lowercase() != part.to_lowercase());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect back to subreddit
|
// Redirect back to subreddit
|
||||||
// check for redirect parameter if unsubscribing from outside sidebar
|
// check for redirect parameter if unsubscribing/unfiltering from outside sidebar
|
||||||
let path = if let Some(redirect_path) = param(&format!("?{}", query), "redirect") {
|
let path = if let Some(redirect_path) = param(&format!("?{}", query), "redirect") {
|
||||||
format!("/{}/", redirect_path)
|
format!("/{}/", redirect_path)
|
||||||
} else {
|
} else {
|
||||||
@ -226,6 +272,17 @@ pub async fn subscriptions(req: Request<Body>) -> Result<Response<Body>, String>
|
|||||||
.finish(),
|
.finish(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if filters.is_empty() {
|
||||||
|
response.remove_cookie("filters".to_string());
|
||||||
|
} else {
|
||||||
|
response.insert_cookie(
|
||||||
|
Cookie::build("filters", filters.join("+"))
|
||||||
|
.path("/")
|
||||||
|
.http_only(true)
|
||||||
|
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
37
src/user.rs
37
src/user.rs
@ -2,7 +2,7 @@
|
|||||||
use crate::client::json;
|
use crate::client::json;
|
||||||
use crate::esc;
|
use crate::esc;
|
||||||
use crate::server::RequestExt;
|
use crate::server::RequestExt;
|
||||||
use crate::utils::{error, format_url, param, template, Post, Preferences, User};
|
use crate::utils::{error, filter_posts, format_url, get_filters, param, template, Post, Preferences, User};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
@ -17,6 +17,11 @@ struct UserTemplate {
|
|||||||
ends: (String, String),
|
ends: (String, String),
|
||||||
prefs: Preferences,
|
prefs: Preferences,
|
||||||
url: String,
|
url: String,
|
||||||
|
/// Whether the user themself is filtered.
|
||||||
|
is_filtered: bool,
|
||||||
|
/// Whether all fetched posts are filtered (to differentiate between no posts fetched in the first place,
|
||||||
|
/// and all fetched posts being filtered).
|
||||||
|
all_posts_filtered: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// FUNCTIONS
|
// FUNCTIONS
|
||||||
@ -27,20 +32,31 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
req.param("name").unwrap_or_else(|| "reddit".to_string()),
|
req.param("name").unwrap_or_else(|| "reddit".to_string()),
|
||||||
req.uri().query().unwrap_or_default()
|
req.uri().query().unwrap_or_default()
|
||||||
);
|
);
|
||||||
|
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
||||||
|
|
||||||
// Retrieve other variables from Libreddit request
|
// Retrieve other variables from Libreddit request
|
||||||
let sort = param(&path, "sort").unwrap_or_default();
|
let sort = param(&path, "sort").unwrap_or_default();
|
||||||
let username = req.param("name").unwrap_or_default();
|
let username = req.param("name").unwrap_or_default();
|
||||||
|
|
||||||
// Request user posts/comments from Reddit
|
|
||||||
let posts = Post::fetch(&path, "Comment".to_string(), false).await;
|
|
||||||
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
|
||||||
|
|
||||||
match posts {
|
|
||||||
Ok((posts, after)) => {
|
|
||||||
// If you can get user posts, also request user data
|
|
||||||
let user = user(&username).await.unwrap_or_default();
|
let user = user(&username).await.unwrap_or_default();
|
||||||
|
|
||||||
|
let filters = get_filters(&req);
|
||||||
|
if filters.contains(&["u_", &username].concat()) {
|
||||||
|
template(UserTemplate {
|
||||||
|
user,
|
||||||
|
posts: Vec::new(),
|
||||||
|
sort: (sort, param(&path, "t").unwrap_or_default()),
|
||||||
|
ends: (param(&path, "after").unwrap_or_default(), "".to_string()),
|
||||||
|
prefs: Preferences::new(req),
|
||||||
|
url,
|
||||||
|
is_filtered: true,
|
||||||
|
all_posts_filtered: false,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Request user posts/comments from Reddit
|
||||||
|
match Post::fetch(&path, false).await {
|
||||||
|
Ok((mut posts, after)) => {
|
||||||
|
let all_posts_filtered = filter_posts(&mut posts, &filters);
|
||||||
|
|
||||||
template(UserTemplate {
|
template(UserTemplate {
|
||||||
user,
|
user,
|
||||||
posts,
|
posts,
|
||||||
@ -48,11 +64,14 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
ends: (param(&path, "after").unwrap_or_default(), after),
|
ends: (param(&path, "after").unwrap_or_default(), after),
|
||||||
prefs: Preferences::new(req),
|
prefs: Preferences::new(req),
|
||||||
url,
|
url,
|
||||||
|
is_filtered: false,
|
||||||
|
all_posts_filtered,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// If there is an error show error page
|
// If there is an error show error page
|
||||||
Err(msg) => error(req, msg).await,
|
Err(msg) => error(req, msg).await,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// USER
|
// USER
|
||||||
|
28
src/utils.rs
28
src/utils.rs
@ -7,7 +7,7 @@ use cookie::Cookie;
|
|||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use time::{Duration, OffsetDateTime};
|
use time::{Duration, OffsetDateTime};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
@ -219,7 +219,7 @@ pub struct Post {
|
|||||||
|
|
||||||
impl Post {
|
impl Post {
|
||||||
// Fetch posts of a user or subreddit and return a vector of posts and the "after" value
|
// Fetch posts of a user or subreddit and return a vector of posts and the "after" value
|
||||||
pub async fn fetch(path: &str, fallback_title: String, quarantine: bool) -> Result<(Vec<Self>, String), String> {
|
pub async fn fetch(path: &str, quarantine: bool) -> Result<(Vec<Self>, String), String> {
|
||||||
let res;
|
let res;
|
||||||
let post_list;
|
let post_list;
|
||||||
|
|
||||||
@ -262,7 +262,7 @@ impl Post {
|
|||||||
|
|
||||||
posts.push(Self {
|
posts.push(Self {
|
||||||
id: val(post, "id"),
|
id: val(post, "id"),
|
||||||
title: esc!(if title.is_empty() { fallback_title.clone() } else { title }),
|
title,
|
||||||
community: val(post, "subreddit"),
|
community: val(post, "subreddit"),
|
||||||
body,
|
body,
|
||||||
author: Author {
|
author: Author {
|
||||||
@ -346,6 +346,7 @@ pub struct Comment {
|
|||||||
pub highlighted: bool,
|
pub highlighted: bool,
|
||||||
pub awards: Awards,
|
pub awards: Awards,
|
||||||
pub collapsed: bool,
|
pub collapsed: bool,
|
||||||
|
pub is_filtered: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
@ -458,6 +459,7 @@ pub struct Preferences {
|
|||||||
pub comment_sort: String,
|
pub comment_sort: String,
|
||||||
pub post_sort: String,
|
pub post_sort: String,
|
||||||
pub subscriptions: Vec<String>,
|
pub subscriptions: Vec<String>,
|
||||||
|
pub filters: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Preferences {
|
impl Preferences {
|
||||||
@ -475,10 +477,28 @@ impl Preferences {
|
|||||||
comment_sort: setting(&req, "comment_sort"),
|
comment_sort: setting(&req, "comment_sort"),
|
||||||
post_sort: setting(&req, "post_sort"),
|
post_sort: setting(&req, "post_sort"),
|
||||||
subscriptions: setting(&req, "subscriptions").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
|
subscriptions: setting(&req, "subscriptions").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
|
||||||
|
filters: setting(&req, "filters").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets a `HashSet` of filters from the cookie in the given `Request`.
|
||||||
|
pub fn get_filters(req: &Request<Body>) -> HashSet<String> {
|
||||||
|
setting(&req, "filters").split('+').map(String::from).filter(|s| !s.is_empty()).collect::<HashSet<String>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filters a `Vec<Post>` by the given `HashSet` of filters (each filter being a subreddit name or a user name). If a
|
||||||
|
/// `Post`'s subreddit or author is found in the filters, it is removed. Returns `true` if _all_ posts were filtered
|
||||||
|
/// out, or `false` otherwise.
|
||||||
|
pub fn filter_posts(posts: &mut Vec<Post>, filters: &HashSet<String>) -> bool {
|
||||||
|
if posts.is_empty() {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
posts.retain(|p| !filters.contains(&p.community) && !filters.contains(&["u_", &p.author.name].concat()));
|
||||||
|
posts.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// FORMATTING
|
// FORMATTING
|
||||||
//
|
//
|
||||||
@ -515,7 +535,7 @@ pub fn setting(req: &Request<Body>, name: &str) -> String {
|
|||||||
|
|
||||||
// Detect and redirect in the event of a random subreddit
|
// Detect and redirect in the event of a random subreddit
|
||||||
pub async fn catch_random(sub: &str, additional: &str) -> Result<Response<Body>, String> {
|
pub async fn catch_random(sub: &str, additional: &str) -> Result<Response<Body>, String> {
|
||||||
if (sub == "random" || sub == "randnsfw") && !sub.contains('+') {
|
if sub == "random" || sub == "randnsfw" {
|
||||||
let new_sub = json(format!("/r/{}/about.json?raw_json=1", sub), false).await?["data"]["display_name"]
|
let new_sub = json(format!("/r/{}/about.json?raw_json=1", sub), false).await?["data"]["display_name"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
|
@ -372,7 +372,7 @@ aside {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#user_details, #sub_details {
|
#user_details, #sub_details, #sub_actions, #user_actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
grid-column-gap: 20px;
|
grid-column-gap: 20px;
|
||||||
@ -384,7 +384,7 @@ aside {
|
|||||||
|
|
||||||
/* Subscriptions */
|
/* Subscriptions */
|
||||||
|
|
||||||
#sub_subscription, #user_subscription {
|
#sub_subscription, #user_subscription, #user_filter, #sub_filter {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -392,18 +392,18 @@ aside {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscribe, .unsubscribe {
|
.subscribe, .unsubscribe, .filter, .unfilter {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscribe {
|
.subscribe, .filter {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
background-color: var(--accent);
|
background-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.unsubscribe {
|
.unsubscribe, .unfilter {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background-color: var(--highlighted);
|
background-color: var(--highlighted);
|
||||||
}
|
}
|
||||||
@ -1042,7 +1042,7 @@ a.search_subreddit:hover {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment_body.highlighted {
|
.comment_body.highlighted, .comment_body_filtered.highlighted {
|
||||||
background: var(--highlighted);
|
background: var(--highlighted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1055,6 +1055,15 @@ a.search_subreddit:hover {
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment_body_filtered {
|
||||||
|
opacity: 0.4;
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 5px 5px;
|
||||||
|
margin: 5px 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.deeper_replies {
|
.deeper_replies {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
@ -1226,6 +1235,14 @@ input[type="submit"] {
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#settings_filters .unsubscribe {
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings_filters a {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.helper {
|
.helper {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
width: 250px;
|
width: 250px;
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<p class="comment_score" title="{{ score.1 }}">{{ score.0 }}</p>
|
<p class="comment_score" title="{{ score.1 }}">{{ score.0 }}</p>
|
||||||
<div class="line"></div>
|
<div class="line"></div>
|
||||||
</div>
|
</div>
|
||||||
<details class="comment_right" {% if collapsed == false %}open{% endif %}>
|
<details class="comment_right" {% if !collapsed || highlighted %}open{% endif %}>
|
||||||
<summary class="comment_data">
|
<summary class="comment_data">
|
||||||
<a class="comment_author {{ author.distinguished }} {% if author.name == post_author %}op{% endif %}" href="/user/{{ author.name }}">u/{{ author.name }}</a>
|
<a class="comment_author {{ author.distinguished }} {% if author.name == post_author %}op{% endif %}" href="/user/{{ author.name }}">u/{{ author.name }}</a>
|
||||||
{% if author.flair.flair_parts.len() > 0 %}
|
{% if author.flair.flair_parts.len() > 0 %}
|
||||||
@ -25,7 +25,11 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</summary>
|
</summary>
|
||||||
|
{% if is_filtered %}
|
||||||
|
<div class="comment_body_filtered {% if highlighted %}highlighted{% endif %}">(Filtered content)</div>
|
||||||
|
{% else %}
|
||||||
<div class="comment_body {% if highlighted %}highlighted{% endif %}">{{ body }}</div>
|
<div class="comment_body {% if highlighted %}highlighted{% endif %}">{{ body }}</div>
|
||||||
|
{% endif %}
|
||||||
<blockquote class="replies">{% for c in replies -%}{{ c.render().unwrap() }}{%- endfor %}
|
<blockquote class="replies">{% for c in replies -%}{{ c.render().unwrap() }}{%- endfor %}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</details>
|
</details>
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{% if !is_filtered %}
|
||||||
{% if subreddits.len() > 0 || params.typed == "sr_user" %}
|
{% if subreddits.len() > 0 || params.typed == "sr_user" %}
|
||||||
<div id="search_subreddits">
|
<div id="search_subreddits">
|
||||||
{% if params.typed == "sr_user" %}
|
{% if params.typed == "sr_user" %}
|
||||||
@ -41,7 +42,7 @@
|
|||||||
<div class="search_subreddit_left">{% if subreddit.icon != "" %}<img loading="lazy" src="{{ subreddit.icon }}" alt="r/{{ subreddit.name }} icon">{% endif %}</div>
|
<div class="search_subreddit_left">{% if subreddit.icon != "" %}<img loading="lazy" src="{{ subreddit.icon }}" alt="r/{{ subreddit.name }} icon">{% endif %}</div>
|
||||||
<div class="search_subreddit_right">
|
<div class="search_subreddit_right">
|
||||||
<p class="search_subreddit_header">
|
<p class="search_subreddit_header">
|
||||||
<span class="search_subreddit_name">{{ subreddit.name }}</span>
|
<span class="search_subreddit_name">r/{{ subreddit.name }}</span>
|
||||||
<span class="dot">•</span>
|
<span class="dot">•</span>
|
||||||
<span class="search_subreddit_members" title="{{ subreddit.subscribers.1 }} Members">{{ subreddit.subscribers.0 }} Members</span>
|
<span class="search_subreddit_members" title="{{ subreddit.subscribers.1 }} Members">{{ subreddit.subscribers.0 }} Members</span>
|
||||||
</p>
|
</p>
|
||||||
@ -54,10 +55,15 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if params.typed != "sr_user" %}
|
{% endif %}
|
||||||
|
{% if all_posts_filtered %}
|
||||||
|
<center>(All content on this page has been filtered)</center>
|
||||||
|
{% else if is_filtered %}
|
||||||
|
<center>(Content from r/{{ sub }} has been filtered)</center>
|
||||||
|
{% else if params.typed != "sr_user" %}
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
{% if post.flags.nsfw && prefs.show_nsfw != "on" %}
|
{% if post.flags.nsfw && prefs.show_nsfw != "on" %}
|
||||||
{% else if post.title != "Comment" %}
|
{% else if !post.title.is_empty() %}
|
||||||
{% call utils::post_in_list(post) %}
|
{% call utils::post_in_list(post) %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="comment">
|
<div class="comment">
|
||||||
|
@ -92,10 +92,25 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if !prefs.filters.is_empty() %}
|
||||||
|
<div class="prefs" id="settings_filters">
|
||||||
|
<p>Filtered Feeds</p>
|
||||||
|
{% for sub in prefs.filters %}
|
||||||
|
<div>
|
||||||
|
{% let feed -%}
|
||||||
|
{% if sub.starts_with("u_") -%}{% let feed = format!("u/{}", &sub[2..]) -%}{% else -%}{% let feed = format!("r/{}", sub) -%}{% endif -%}
|
||||||
|
<a href="/{{ feed }}">{{ feed }}</a>
|
||||||
|
<form action="/r/{{ sub }}/unfilter/?redirect=settings" method="POST">
|
||||||
|
<button class="unfilter">Unfilter</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div id="settings_note">
|
<div id="settings_note">
|
||||||
<p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br>
|
<p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br>
|
||||||
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&use_hls={{ prefs.use_hls }}&hide_hls_notification={{ prefs.hide_hls_notification }}&subscriptions={{ prefs.subscriptions.join("%2B") }}">this link</a>.</p>
|
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&use_hls={{ prefs.use_hls }}&hide_hls_notification={{ prefs.hide_hls_notification }}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<main>
|
<main>
|
||||||
|
{% if !is_filtered %}
|
||||||
<div id="column_one">
|
<div id="column_one">
|
||||||
<form id="sort">
|
<form id="sort">
|
||||||
<div id="sort_options">
|
<div id="sort_options">
|
||||||
@ -45,6 +46,9 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if all_posts_filtered %}
|
||||||
|
<center>(All content on this page has been filtered)</center>
|
||||||
|
{% else %}
|
||||||
<div id="posts">
|
<div id="posts">
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
{% if !(post.flags.nsfw && prefs.show_nsfw != "on") %}
|
{% if !(post.flags.nsfw && prefs.show_nsfw != "on") %}
|
||||||
@ -57,6 +61,7 @@
|
|||||||
<script src="/playHLSVideo.js"></script>
|
<script src="/playHLSVideo.js"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
{% if ends.0 != "" %}
|
{% if ends.0 != "" %}
|
||||||
@ -68,8 +73,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
{% if sub.name != "" && !sub.name.contains("+") %}
|
{% endif %}
|
||||||
|
{% if is_filtered || (sub.name != "" && !sub.name.contains("+")) %}
|
||||||
<aside>
|
<aside>
|
||||||
|
{% if is_filtered %}
|
||||||
|
<center>(Content from r/{{ sub.name }} has been filtered)</center>
|
||||||
|
{% endif %}
|
||||||
|
{% if sub.name != "" && !sub.name.contains("+") %}
|
||||||
<div class="panel" id="subreddit">
|
<div class="panel" id="subreddit">
|
||||||
{% if sub.wiki %}
|
{% if sub.wiki %}
|
||||||
<div id="top">
|
<div id="top">
|
||||||
@ -88,6 +98,7 @@
|
|||||||
<div title="{{ sub.members.1 }}">{{ sub.members.0 }}</div>
|
<div title="{{ sub.members.1 }}">{{ sub.members.0 }}</div>
|
||||||
<div title="{{ sub.active.1 }}">{{ sub.active.0 }}</div>
|
<div title="{{ sub.active.1 }}">{{ sub.active.0 }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="sub_actions">
|
||||||
<div id="sub_subscription">
|
<div id="sub_subscription">
|
||||||
{% if prefs.subscriptions.contains(sub.name) %}
|
{% if prefs.subscriptions.contains(sub.name) %}
|
||||||
<form action="/r/{{ sub.name }}/unsubscribe" method="POST">
|
<form action="/r/{{ sub.name }}/unsubscribe" method="POST">
|
||||||
@ -99,6 +110,18 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<div id="sub_filter">
|
||||||
|
{% if prefs.filters.contains(sub.name) %}
|
||||||
|
<form action="/r/{{ sub.name }}/unfilter" method="POST">
|
||||||
|
<button class="unfilter">Unfilter</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form action="/r/{{ sub.name }}/filter" method="POST">
|
||||||
|
<button class="filter">Filter</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<details class="panel" id="sidebar">
|
<details class="panel" id="sidebar">
|
||||||
@ -115,6 +138,7 @@
|
|||||||
</ul> #}
|
</ul> #}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
{% endif %}
|
||||||
</aside>
|
</aside>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</main>
|
</main>
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<main>
|
<main>
|
||||||
|
{% if !is_filtered %}
|
||||||
<div id="column_one">
|
<div id="column_one">
|
||||||
<form id="sort">
|
<form id="sort">
|
||||||
<select name="sort">
|
<select name="sort">
|
||||||
@ -28,11 +29,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{% if all_posts_filtered %}
|
||||||
|
<center>(All content on this page has been filtered)</center>
|
||||||
|
{% else %}
|
||||||
<div id="posts">
|
<div id="posts">
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
|
|
||||||
{% if post.flags.nsfw && prefs.show_nsfw != "on" %}
|
{% if post.flags.nsfw && prefs.show_nsfw != "on" %}
|
||||||
{% else if post.title != "Comment" %}
|
{% else if !post.title.is_empty() %}
|
||||||
{% call utils::post_in_list(post) %}
|
{% call utils::post_in_list(post) %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="comment">
|
<div class="comment">
|
||||||
@ -55,6 +59,7 @@
|
|||||||
<script src="/playHLSVideo.js"></script>
|
<script src="/playHLSVideo.js"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
{% if ends.0 != "" %}
|
{% if ends.0 != "" %}
|
||||||
@ -66,7 +71,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<aside>
|
<aside>
|
||||||
|
{% if is_filtered %}
|
||||||
|
<center>(Content from u/{{ user.name }} has been filtered)</center>
|
||||||
|
{% endif %}
|
||||||
<div class="panel" id="user">
|
<div class="panel" id="user">
|
||||||
<img loading="lazy" id="user_icon" src="{{ user.icon }}" alt="User icon">
|
<img loading="lazy" id="user_icon" src="{{ user.icon }}" alt="User icon">
|
||||||
<p id="user_title">{{ user.title }}</p>
|
<p id="user_title">{{ user.title }}</p>
|
||||||
@ -78,18 +87,31 @@
|
|||||||
<div>{{ user.karma }}</div>
|
<div>{{ user.karma }}</div>
|
||||||
<div>{{ user.created }}</div>
|
<div>{{ user.created }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="user_subscription">
|
<div id="user_actions">
|
||||||
{% let name = ["u_", user.name.as_str()].join("") %}
|
{% let name = ["u_", user.name.as_str()].join("") %}
|
||||||
|
<div id="user_subscription">
|
||||||
{% if prefs.subscriptions.contains(name) %}
|
{% if prefs.subscriptions.contains(name) %}
|
||||||
<form action="/r/u_{{ user.name }}/unsubscribe" method="POST">
|
<form action="/r/{{ name }}/unsubscribe" method="POST">
|
||||||
<button class="unsubscribe">Unfollow</button>
|
<button class="unsubscribe">Unfollow</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form action="/r/u_{{ user.name }}/subscribe" method="POST">
|
<form action="/r/{{ name }}/subscribe" method="POST">
|
||||||
<button class="subscribe">Follow</button>
|
<button class="subscribe">Follow</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<div id="user_filter">
|
||||||
|
{% if prefs.filters.contains(name) %}
|
||||||
|
<form action="/r/{{ name }}/unfilter" method="POST">
|
||||||
|
<button class="unfilter">Unfilter</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form action="/r/{{ name }}/filter" method="POST">
|
||||||
|
<button class="filter">Filter</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
|
Loading…
Reference in New Issue
Block a user