Add RSS feeds (fix #57) (#90)

* Add RSS feeds

* feat(rss): feature-ify rss

* feat(rss): config-ify rss

* fix(rss): update info page

* feat(rss): conditionally add RSS feeds to user and sub pages

* feat(rss): implement URLs for RSS
This commit is contained in:
Matthew Esposito 2024-07-21 14:09:34 -04:00 committed by GitHub
parent 9bdb5c8966
commit 374238abc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1808 additions and 1185 deletions

234
Cargo.lock generated
View File

@ -98,7 +98,7 @@ dependencies = [
"mime_guess",
"proc-macro2",
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
@ -124,7 +124,20 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
name = "atom_syndication"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f34613907f31c9dbef0240156db3c9263f34842b6e1a8999d2304ea62c8a30"
dependencies = [
"chrono",
"derive_builder 0.20.0",
"diligent-date-parser",
"never",
"quick-xml 0.31.0",
]
[[package]]
@ -236,10 +249,10 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "771aa57f3b17da6c8bcacb187bb9ec9bc81c8160e72342e67c329e0e1651a669"
dependencies = [
"darling",
"darling 0.20.9",
"proc-macro2",
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
@ -260,6 +273,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"num-traits",
]
[[package]]
name = "clap"
version = "4.5.7"
@ -348,14 +370,38 @@ dependencies = [
"typenum",
]
[[package]]
name = "darling"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
dependencies = [
"darling_core 0.14.4",
"darling_macro 0.14.4",
]
[[package]]
name = "darling"
version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1"
dependencies = [
"darling_core",
"darling_macro",
"darling_core 0.20.9",
"darling_macro 0.20.9",
]
[[package]]
name = "darling_core"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 1.0.109",
]
[[package]]
@ -368,8 +414,19 @@ dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
"strsim 0.11.1",
"syn 2.0.68",
]
[[package]]
name = "darling_macro"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
dependencies = [
"darling_core 0.14.4",
"quote",
"syn 1.0.109",
]
[[package]]
@ -378,9 +435,9 @@ version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178"
dependencies = [
"darling_core",
"darling_core 0.20.9",
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
@ -398,6 +455,68 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "derive_builder"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8"
dependencies = [
"derive_builder_macro 0.12.0",
]
[[package]]
name = "derive_builder"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7"
dependencies = [
"derive_builder_macro 0.20.0",
]
[[package]]
name = "derive_builder_core"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f"
dependencies = [
"darling 0.14.4",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "derive_builder_core"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d"
dependencies = [
"darling 0.20.9",
"proc-macro2",
"quote",
"syn 2.0.68",
]
[[package]]
name = "derive_builder_macro"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e"
dependencies = [
"derive_builder_core 0.12.0",
"syn 1.0.109",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b"
dependencies = [
"derive_builder_core 0.20.0",
"syn 2.0.68",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -408,12 +527,30 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "diligent-date-parser"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6cf7fe294274a222363f84bcb63cdea762979a0443b4cf1f4f8fd17c86b1182"
dependencies = [
"chrono",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "encoding_rs"
version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
dependencies = [
"cfg-if",
]
[[package]]
name = "env_logger"
version = "0.10.2"
@ -862,6 +999,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "never"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
[[package]]
name = "nom"
version = "7.1.3"
@ -878,6 +1021,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
@ -1002,6 +1154,26 @@ version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-xml"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
dependencies = [
"encoding_rs",
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
"encoding_rs",
"memchr",
]
[[package]]
name = "quote"
version = "1.0.36"
@ -1061,6 +1233,7 @@ dependencies = [
"pretty_env_logger",
"regex",
"route-recognizer",
"rss",
"rust-embed",
"sealed_test",
"serde",
@ -1138,6 +1311,18 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
[[package]]
name = "rss"
version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7b2c77eb4450d7d5f98df52c381cd6c4e19b75dad9209a9530b85a44510219a"
dependencies = [
"atom_syndication",
"derive_builder 0.12.0",
"never",
"quick-xml 0.30.0",
]
[[package]]
name = "rust-embed"
version = "8.4.0"
@ -1158,7 +1343,7 @@ dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn",
"syn 2.0.68",
"walkdir",
]
@ -1307,7 +1492,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77253fb2d4451418d07025826028bcb96ee42d3e58859689a70ce62908009db6"
dependencies = [
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
@ -1350,7 +1535,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
@ -1437,6 +1622,12 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.1"
@ -1449,6 +1640,17 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.68"
@ -1498,7 +1700,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
@ -1576,7 +1778,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.68",
]
[[package]]
@ -1950,7 +2152,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.68",
]
[[package]]

View File

@ -42,8 +42,10 @@ fastrand = "2.0.1"
log = "0.4.20"
pretty_env_logger = "0.5.0"
dotenvy = "0.15.7"
rss = "2.0.7"
arc-swap = "1.7.1"
[dev-dependencies]
lipsum = "0.9.0"
sealed_test = "1.0.0"

View File

@ -381,7 +381,8 @@ Assign a default value for each instance-specific setting by passing environment
| `ROBOTS_DISABLE_INDEXING` | `["on", "off"]` | `off` | Disables indexing of the instance by search engines. |
| `PUSHSHIFT_FRONTEND` | String | `undelete.pullpush.io` | Allows the server to set the Pushshift frontend to be used with "removed" links. |
| `PORT` | Integer 0-65535 | `8080` | The **internal** port Redlib listens on. |
| `ENABLE_RSS` | `["on", "off"]` | `off` | Enables RSS feed generation. |
| `FULL_URL` | String | (empty) | Allows for proper URLs (for now, only needed by RSS)
## Default user settings
Assign a default value for each user-modifiable setting by passing environment variables to Redlib in the format `REDLIB_DEFAULT_{Y}`. Replace `{Y}` with the setting name (see list below) in capital letters.

View File

@ -70,6 +70,12 @@
},
"REDLIB_PUSHSHIFT_FRONTEND": {
"required": false
},
"REDLIB_ENABLE_RSS": {
"required": false
},
"REDLIB_FULL_URL": {
"required": false
}
}
}

View File

@ -103,6 +103,12 @@ pub struct Config {
#[serde(rename = "REDLIB_PUSHSHIFT_FRONTEND")]
#[serde(alias = "LIBREDDIT_PUSHSHIFT_FRONTEND")]
pub(crate) pushshift: Option<String>,
#[serde(rename = "REDLIB_ENABLE_RSS")]
pub(crate) enable_rss: Option<String>,
#[serde(rename = "REDLIB_FULL_URL")]
pub(crate) full_url: Option<String>,
}
impl Config {
@ -148,6 +154,8 @@ impl Config {
banner: parse("REDLIB_BANNER"),
robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"),
pushshift: parse("REDLIB_PUSHSHIFT_FRONTEND"),
enable_rss: parse("REDLIB_ENABLE_RSS"),
full_url: parse("REDLIB_FULL_URL"),
}
}
}
@ -175,6 +183,8 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
"REDLIB_BANNER" => config.banner.clone(),
"REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(),
"REDLIB_PUSHSHIFT_FRONTEND" => config.pushshift.clone(),
"REDLIB_ENABLE_RSS" => config.enable_rss.clone(),
"REDLIB_FULL_URL" => config.full_url.clone(),
_ => None,
}
}

View File

@ -126,6 +126,8 @@ impl InstanceInfo {
["Compile mode", &self.compile_mode],
["SFW only", &convert(&self.config.sfw_only)],
["Pushshift frontend", &convert(&self.config.pushshift)],
["RSS enabled", &convert(&self.config.enable_rss)],
["Full URL", &convert(&self.config.full_url)],
//TODO: fallback to crate::config::DEFAULT_PUSHSHIFT_FRONTEND
])
.with_header_row(["Settings"]),
@ -165,6 +167,8 @@ impl InstanceInfo {
Compile mode: {}\n
SFW only: {:?}\n
Pushshift frontend: {:?}\n
RSS enabled: {:?}\n
Full URL: {:?}\n
Config:\n
Banner: {:?}\n
Hide awards: {:?}\n
@ -189,6 +193,8 @@ impl InstanceInfo {
self.deploy_unix_ts,
self.compile_mode,
self.config.sfw_only,
self.config.enable_rss,
self.config.full_url,
self.config.pushshift,
self.config.banner,
self.config.default_hide_awards,

View File

@ -254,6 +254,7 @@ async fn main() {
app.at("/u/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed());
app.at("/user/[deleted]").get(|req| error(req, "User has deleted their account").boxed());
app.at("/user/:name.rss").get(|r| user::rss(r).boxed());
app.at("/user/:name").get(|r| user::profile(r).boxed());
app.at("/user/:name/:listing").get(|r| user::profile(r).boxed());
app.at("/user/:name/comments/:id").get(|r| post::item(r).boxed());
@ -265,6 +266,9 @@ async fn main() {
app.at("/settings/restore").get(|r| settings::restore(r).boxed());
app.at("/settings/update").get(|r| settings::update(r).boxed());
// RSS Subscriptions
app.at("/r/:sub.rss").get(|r| subreddit::rss(r).boxed());
// Subreddit services
app
.at("/r/:sub")

View File

@ -1,3 +1,4 @@
use crate::{config, utils};
// CRATES
use crate::utils::{
catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit,
@ -459,6 +460,56 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
})
}
pub async fn rss(req: Request<Body>) -> Result<Response<Body>, String> {
if config::get_setting("REDLIB_ENABLE_RSS").is_none() {
return Ok(error(req, "RSS is disabled on this instance.").await.unwrap_or_default());
}
use hyper::header::CONTENT_TYPE;
use rss::{ChannelBuilder, Item};
// Get subreddit
let sub = req.param("sub").unwrap_or_default();
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));
// Get path
let path = format!("/r/{sub}/{sort}.json?{}", req.uri().query().unwrap_or_default());
// Get subreddit data
let subreddit = subreddit(&sub, false).await?;
// Get posts
let (posts, _) = Post::fetch(&path, false).await?;
// Build the RSS feed
let channel = ChannelBuilder::default()
.title(&subreddit.title)
.description(&subreddit.description)
.items(
posts
.into_iter()
.map(|post| Item {
title: Some(post.title.to_string()),
link: Some(utils::get_post_url(&post)),
author: Some(post.author.name),
content: Some(rewrite_urls(&post.body)),
..Default::default()
})
.collect::<Vec<_>>(),
)
.build();
// Serialize the feed to RSS
let body = channel.to_string().into_bytes();
// Create the HTTP response
let mut res = Response::new(Body::from(body));
res.headers_mut().insert(CONTENT_TYPE, hyper::header::HeaderValue::from_static("application/rss+xml"));
Ok(res)
}
#[tokio::test(flavor = "multi_thread")]
async fn test_fetching_subreddit() {
let subreddit = subreddit("rust", false).await;

View File

@ -2,6 +2,7 @@
use crate::client::json;
use crate::server::RequestExt;
use crate::utils::{error, filter_posts, format_url, get_filters, nsfw_landing, param, setting, template, Post, Preferences, User};
use crate::{config, utils};
use askama::Template;
use hyper::{Body, Request, Response};
use time::{macros::format_description, OffsetDateTime};
@ -129,6 +130,56 @@ async fn user(name: &str) -> Result<User, String> {
})
}
pub async fn rss(req: Request<Body>) -> Result<Response<Body>, String> {
if config::get_setting("REDLIB_ENABLE_RSS").is_none() {
return Ok(error(req, "RSS is disabled on this instance.").await.unwrap_or_default());
}
use crate::utils::rewrite_urls;
use hyper::header::CONTENT_TYPE;
use rss::{ChannelBuilder, Item};
// Get user
let user_str = req.param("name").unwrap_or_default();
let listing = req.param("listing").unwrap_or_else(|| "overview".to_string());
// Get path
let path = format!("/user/{user_str}/{listing}.json?{}&raw_json=1", req.uri().query().unwrap_or_default(),);
// Get user
let user_obj = user(&user_str).await.unwrap_or_default();
// Get posts
let (posts, _) = Post::fetch(&path, false).await?;
// Build the RSS feed
let channel = ChannelBuilder::default()
.title(user_str)
.description(user_obj.description)
.items(
posts
.into_iter()
.map(|post| Item {
title: Some(post.title.to_string()),
link: Some(utils::get_post_url(&post)),
author: Some(post.author.name),
content: Some(rewrite_urls(&post.body)),
..Default::default()
})
.collect::<Vec<_>>(),
)
.build();
// Serialize the feed to RSS
let body = channel.to_string().into_bytes();
// Create the HTTP response
let mut res = Response::new(Body::from(body));
res.headers_mut().insert(CONTENT_TYPE, hyper::header::HeaderValue::from_static("application/rss+xml"));
Ok(res)
}
#[tokio::test(flavor = "multi_thread")]
async fn test_fetching_user() {
let user = user("spez").await;

View File

@ -1,5 +1,5 @@
#![allow(dead_code)]
use crate::config::get_setting;
use crate::config::{self, get_setting};
//
// CRATES
//
@ -15,6 +15,7 @@ use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::env;
use std::str::FromStr;
use std::string::ToString;
use time::{macros::format_description, Duration, OffsetDateTime};
use url::Url;
@ -327,6 +328,7 @@ pub struct Post {
pub gallery: Vec<GalleryMedia>,
pub awards: Awards,
pub nsfw: bool,
pub out_url: Option<String>,
pub ws_url: String,
}
@ -435,6 +437,7 @@ impl Post {
awards,
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
ws_url: val(post, "websocket_url"),
out_url: post["data"]["url_overridden_by_dest"].as_str().map(|a| a.to_string()),
});
}
Ok((posts, res["data"]["after"].as_str().unwrap_or_default().to_string()))
@ -770,6 +773,7 @@ pub async fn parse_post(post: &Value) -> Post {
awards,
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
ws_url: val(post, "websocket_url"),
out_url: post["data"]["url_overridden_by_dest"].as_str().map(|a| a.to_string()),
}
}
@ -1082,6 +1086,16 @@ pub fn sfw_only() -> bool {
}
}
/// Returns true if the config/env variable REDLIB_ENABLE_RSS is set to "on".
/// If this variable is set as such, the instance will enable RSS feeds.
/// Otherwise, the instance will not provide RSS feeds.
pub fn enable_rss() -> bool {
match get_setting("REDLIB_ENABLE_RSS") {
Some(val) => val == "on",
None => false,
}
}
// Determines if a request shoud redirect to a nsfw landing gate.
pub fn should_be_nsfw_gated(req: &Request<Body>, req_url: &str) -> bool {
let sfw_instance = sfw_only();
@ -1137,6 +1151,20 @@ pub fn url_path_basename(path: &str) -> String {
}
}
// Returns the URL of a post, as needed by RSS feeds
pub fn get_post_url(post: &Post) -> String {
if let Some(out_url) = &post.out_url {
// Handle cross post
if out_url.starts_with("/r/") {
format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), out_url)
} else {
out_url.to_string()
}
} else {
format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), post.permalink)
}
}
#[cfg(test)]
mod tests {
use super::{format_num, format_url, rewrite_urls};

View File

@ -31,14 +31,15 @@
}
@font-face {
font-family: 'Inter';
src: url('/Inter.var.woff2') format('woff2-variations');
font-family: "Inter";
src: url("/Inter.var.woff2") format("woff2-variations");
font-style: normal;
font-weight: 100 900;
}
/* Automatic theme selection */
:root, .dark{
:root,
.dark {
/* Default & fallback theme (dark) */
--accent: aqua;
--green: #5cff85;
@ -107,8 +108,31 @@
outline: 2px solid var(--accent);
}
html, body, div, h1, h2, h3, h4, h5, h6, ul, ol, dl, li, dt, dd, p, blockquote,
pre, form, fieldset, table, th, td, select, input {
html,
body,
div,
h1,
h2,
h3,
h4,
h5,
h6,
ul,
ol,
dl,
li,
dt,
dd,
p,
blockquote,
pre,
form,
fieldset,
table,
th,
td,
select,
input {
accent-color: var(--accent);
margin: 0;
color: var(--text);
@ -166,9 +190,16 @@ nav.fixed_navbar {
position: fixed;
}
nav * { color: var(--text); }
nav #reddit, #code > span { color: var(--accent); }
nav #code > svg { stroke: var(--accent); }
nav * {
color: var(--text);
}
nav #reddit,
#code > span {
color: var(--accent);
}
nav #code > svg {
stroke: var(--accent);
}
nav #logo {
grid-area: logo;
@ -224,8 +255,8 @@ figcaption {
/* all other browsers */
@supports ((-webkit-backdrop-filter: none) or (backdrop-filter: none)) {
.popup {
-webkit-backdrop-filter: blur(.25rem) brightness(15%);
backdrop-filter: blur(.25rem) brightness(15%);
-webkit-backdrop-filter: blur(0.25rem) brightness(15%);
backdrop-filter: blur(0.25rem) brightness(15%);
}
}
@ -345,7 +376,7 @@ body > footer {
.footer-button {
align-items: center;
border-radius: .25rem;
border-radius: 0.25rem;
box-sizing: border-box;
color: var(--text);
cursor: pointer;
@ -403,7 +434,8 @@ aside {
max-width: 350px;
}
.post, .panel {
.post,
.panel {
border: var(--panel-border);
}
@ -414,7 +446,9 @@ aside {
/* User & Subreddit */
#user, #subreddit, #sidebar {
#user,
#subreddit,
#sidebar {
margin: 40px auto 0 auto;
display: flex;
flex-direction: column;
@ -424,19 +458,34 @@ aside {
border-radius: 5px;
overflow: hidden;
}
#subreddit, #sidebar { min-width: 350px; }
#subreddit,
#sidebar {
min-width: 350px;
}
#user *, #subreddit * { text-align: center; }
#user *,
#subreddit * {
text-align: center;
}
#user, #sub_meta, #sidebar_contents { padding: 20px; }
#user,
#sub_meta,
#sidebar_contents {
padding: 20px;
}
#sidebar, #sidebar_contents { margin-top: 10px; }
#sidebar_label, #subreddit_label {
#sidebar,
#sidebar_contents {
margin-top: 10px;
}
#sidebar_label,
#subreddit_label {
padding: 10px;
text-align: left;
}
#user_icon, #sub_icon {
#user_icon,
#sub_icon {
width: 100px;
height: 100px;
border: 2px solid var(--accent);
@ -445,35 +494,50 @@ aside {
margin: 10px;
}
#user_title, #sub_title {
#user_title,
#sub_title {
font-size: 20px;
font-weight: bold;
}
#user_description, #sub_description {
#user_description,
#sub_description {
margin: 0 15px;
text-align: left;
overflow-wrap: anywhere;
}
#user_name, #user_description:not(:empty), #user_icon,
#sub_name, #sub_icon, #sub_description:not(:empty) {
#user_name,
#user_description:not(:empty),
#user_icon,
#sub_name,
#sub_icon,
#sub_description:not(:empty) {
margin-bottom: 20px;
}
#user_details, #sub_details, #sub_actions, #user_actions {
#user_details,
#sub_details,
#sub_actions,
#user_actions {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 20px;
}
#user_details > label, #sub_details > label {
#user_details > label,
#sub_details > label {
color: var(--accent);
}
/* Subscriptions */
#sub_subscription, #user_subscription, #user_filter, #sub_filter {
#sub_subscription,
#user_subscription,
#sub_filter,
#user_filter,
#sub_rss,
#user_rss {
margin-top: 20px;
}
@ -481,18 +545,23 @@ aside {
margin-bottom: 20px;
}
.subscribe, .unsubscribe, .filter, .unfilter {
.subscribe,
.unsubscribe,
.filter,
.unfilter {
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
}
.subscribe, .filter {
.subscribe,
.filter {
color: var(--foreground);
background-color: var(--accent);
}
.unsubscribe, .unfilter {
.unsubscribe,
.unfilter {
color: var(--text);
background-color: var(--highlighted);
}
@ -575,7 +644,13 @@ aside {
/* Sorting and Search */
select, #search, #sort_options, #listing_options, #inside, #searchbox > *, #sort_submit {
select,
#search,
#sort_options,
#listing_options,
#inside,
#searchbox > *,
#sort_submit {
height: 38px;
}
@ -592,7 +667,8 @@ select {
cursor: pointer;
}
select, #search {
select,
#search {
border: none;
padding: 0 10px;
@ -613,7 +689,10 @@ select, #search {
border-radius: 5px;
}
#searchbox > *, #sort_submit { background: var(--highlighted); }
#searchbox > *,
#sort_submit {
background: var(--highlighted);
}
#search {
border-right: 2px var(--outside) solid;
@ -629,9 +708,12 @@ select, #search {
max-width: 50%;
}
#restrict_sr { margin-right: 5px; }
#restrict_sr {
margin-right: 5px;
}
input[type="submit"], button.submit {
input[type="submit"],
button.submit {
border: 0;
border-radius: 0px 5px 5px 0px;
}
@ -641,10 +723,16 @@ button.submit {
align-items: center;
}
select:hover { background: var(--foreground); }
select:hover {
background: var(--foreground);
}
input[type="submit"]:hover { color: var(--accent); }
button.submit:hover > svg { stroke: var(--accent); }
input[type="submit"]:hover {
color: var(--accent);
}
button.submit:hover > svg {
stroke: var(--accent);
}
#timeframe {
margin: 0 2px;
@ -665,7 +753,9 @@ button.submit:hover > svg { stroke: var(--accent); }
overflow: auto;
}
#search_sort > *, .search_widget_divider_box > *, .search_widget_divider_box #sort_options {
#search_sort > *,
.search_widget_divider_box > *,
.search_widget_divider_box #sort_options {
background: var(--highlighted);
font-size: 15px;
}
@ -684,7 +774,8 @@ button.submit:hover > svg { stroke: var(--accent); }
color: var(--green);
}
#sort, #search_sort {
#sort,
#search_sort {
display: flex;
align-items: center;
margin-bottom: 20px;
@ -750,7 +841,9 @@ button.submit:hover > svg { stroke: var(--accent); }
}
}
#sort_options, #listing_options, main > * > footer > a {
#sort_options,
#listing_options,
main > * > footer > a {
border-radius: 5px;
align-items: center;
box-shadow: var(--shadow);
@ -759,7 +852,9 @@ button.submit:hover > svg { stroke: var(--accent); }
overflow-y: hidden;
}
#sort_options > a, #listing_options > a, main > * > footer > a {
#sort_options > a,
#listing_options > a,
main > * > footer > a {
color: var(--text);
padding: 10px 20px;
text-align: center;
@ -767,12 +862,14 @@ button.submit:hover > svg { stroke: var(--accent); }
transition: 0.2s background;
}
#sort_options > a.selected, #listing_options > a.selected {
#sort_options > a.selected,
#listing_options > a.selected {
background: var(--accent);
color: var(--foreground);
}
#sort_options > a:not(.selected):hover, #listing_options > a:not(.selected):hover {
#sort_options > a:not(.selected):hover,
#listing_options > a:not(.selected):hover {
background: var(--foreground);
}
@ -863,7 +960,8 @@ a.search_subreddit:hover {
box-shadow: var(--shadow);
display: grid;
transition: 0.2s background;
grid-template: "post_score post_header post_thumbnail" auto
grid-template:
"post_score post_header post_thumbnail" auto
"post_score post_title post_thumbnail" 1fr
"post_score post_media post_thumbnail" auto
"post_score post_body post_thumbnail" auto
@ -873,7 +971,9 @@ a.search_subreddit:hover {
/ minmax(40px, auto) minmax(0, 1fr) fit-content(min(20%, 152px));
}
.post:not(:last-child) { margin-bottom: 10px; }
.post:not(:last-child) {
margin-bottom: 10px;
}
.post:hover {
background: var(--foreground);
@ -959,7 +1059,8 @@ a.search_subreddit:hover {
vertical-align: middle;
}
.author_flair:empty, .post_flair:empty {
.author_flair:empty,
.post_flair:empty {
display: none;
}
@ -992,7 +1093,9 @@ a.search_subreddit:hover {
border-radius: 5px;
}
.post_media_content, .post .__NoScript_PlaceHolder__, .gallery {
.post_media_content,
.post .__NoScript_PlaceHolder__,
.gallery {
max-width: calc(100% - 40px);
grid-area: post_media;
margin: 15px auto 5px auto;
@ -1010,7 +1113,8 @@ a.search_subreddit:hover {
margin: auto;
}
.post_media_image.short svg, .post_media_image.short img{
.post_media_image.short svg,
.post_media_image.short img {
width: auto;
height: auto;
max-width: 100%;
@ -1134,7 +1238,7 @@ a.search_subreddit:hover {
/* Used only for text post preview */
.post_preview {
-webkit-mask-image: linear-gradient(180deg,#000 60%,transparent);;
-webkit-mask-image: linear-gradient(180deg, #000 60%, transparent);
mask-image: linear-gradient(180deg, #000 60%, transparent);
opacity: 0.8;
max-height: 250px;
@ -1269,7 +1373,8 @@ a.search_subreddit:hover {
margin: 0;
}
.comment_left, .comment_right {
.comment_left,
.comment_right {
display: flex;
flex-direction: column;
}
@ -1281,9 +1386,15 @@ a.search_subreddit:hover {
align-items: center;
}
.comment_title { font-size: 20px; }
.comment_link { text-decoration: underline; }
.comment_author { opacity: 0.9; }
.comment_title {
font-size: 20px;
}
.comment_link {
text-decoration: underline;
}
.comment_author {
opacity: 0.9;
}
.author_flair {
background: var(--highlighted);
@ -1348,7 +1459,8 @@ a.search_subreddit:hover {
overflow: auto;
}
.comment_body.highlighted, .comment_body_filtered.highlighted {
.comment_body.highlighted,
.comment_body_filtered.highlighted {
background: var(--highlighted);
}
@ -1413,12 +1525,25 @@ summary.comment_data {
display: none;
}
.moderator, .admin { opacity: 1; }
.op, .moderator, .admin { font-weight: bold; }
.moderator,
.admin {
opacity: 1;
}
.op,
.moderator,
.admin {
font-weight: bold;
}
.op { color: var(--accent); }
.moderator { color: var(--green); }
.admin { color: var(--admin); }
.op {
color: var(--accent);
}
.moderator {
color: var(--green);
}
.admin {
color: var(--admin);
}
/* Layouts */
@ -1438,8 +1563,12 @@ summary.comment_data {
overflow: hidden;
}
.compact .post.highlighted { border-radius: 5px; }
.compact .post:not(:last-of-type):not(.highlighted):not(.stickied) { border-bottom: 0; }
.compact .post.highlighted {
border-radius: 5px;
}
.compact .post:not(:last-of-type):not(.highlighted):not(.stickied) {
border-bottom: 0;
}
.compact .post_score {
padding-top: 15px;
@ -1451,7 +1580,9 @@ summary.comment_data {
font-size: 14px;
}
.compact .post_title, .compact #post_url, .compact .post_body {
.compact .post_title,
.compact #post_url,
.compact .post_body {
margin: 2.5px 15px;
}
@ -1548,7 +1679,7 @@ aside.prefs {
padding: 10px 15px;
border-radius: 5px;
margin-top: 5px;
width: 100%
width: 100%;
}
input[type="submit"] {
@ -1602,12 +1733,24 @@ input[type="submit"] {
margin-top: 10px;
}
.md h1 { font-size: 22px; }
.md h2 { font-size: 20px; }
.md h3 { font-size: 18px; }
.md h4 { font-size: 16px; }
.md h5 { font-size: 14px; }
.md h6 { font-size: 12px; }
.md h1 {
font-size: 22px;
}
.md h2 {
font-size: 20px;
}
.md h3 {
font-size: 18px;
}
.md h4 {
font-size: 16px;
}
.md h5 {
font-size: 14px;
}
.md h6 {
font-size: 12px;
}
.md blockquote {
padding: 10px;
@ -1616,11 +1759,13 @@ input[type="submit"] {
background: var(--post);
}
.md a, .md a * {
.md a,
.md a * {
color: var(--accent);
}
.md .md-spoiler-text, .md-spoiler-text a {
.md .md-spoiler-text,
.md-spoiler-text a {
background: var(--highlighted);
color: transparent;
}
@ -1635,8 +1780,12 @@ input[type="submit"] {
color: var(--accent);
}
.md li { margin: 10px 0; }
.toc_child { list-style: none; }
.md li {
margin: 10px 0;
}
.toc_child {
list-style: none;
}
.md pre {
background: var(--outside);
@ -1659,27 +1808,42 @@ input[type="submit"] {
font-size: 14px;
}
.md code:not(.md pre > code) { background: var(--highlighted); }
.md code:not(.md pre > code) {
background: var(--highlighted);
}
/* Tables */
table, td, th { border: var(--panel-border); }
table,
td,
th {
border: var(--panel-border);
}
table {
border-width: 3px;
border-spacing: 0;
}
td, th {
td,
th {
padding: 10px;
}
/* Errors */
#error { text-align: center; }
#error h1 { margin-bottom: 10px; }
#error h3 { opacity: 0.85; }
#error a { color: var(--accent); }
#error {
text-align: center;
}
#error h1 {
margin-bottom: 10px;
}
#error h3 {
opacity: 0.85;
}
#error a {
color: var(--accent);
}
/* Messages */
@ -1732,7 +1896,9 @@ td, th {
/* Mobile */
@media screen and (max-width: 800px) {
body.fixed_navbar { padding-top: 120px }
body.fixed_navbar {
padding-top: 120px;
}
main {
flex-direction: column-reverse;
@ -1742,16 +1908,24 @@ td, th {
}
nav {
grid-template-areas: 'logo links' 'searchbox searchbox';
grid-template-areas: "logo links" "searchbox searchbox";
padding: 10px;
width: calc(100% - 20px);
}
nav #links { margin-left: auto; }
nav #links span { display: none; }
nav #links svg { display: block; }
nav #links {
margin-left: auto;
}
nav #links span {
display: none;
}
nav #links svg {
display: block;
}
#subscriptions { position: unset; }
#subscriptions {
position: unset;
}
#sub_list {
left: 10px;
@ -1763,23 +1937,37 @@ td, th {
max-width: unset;
}
aside, #subreddit, #user {
aside,
#subreddit,
#user {
margin: 0;
max-width: 100%;
}
#user, #sidebar { margin: 20px 0; }
#logo, #links { margin-bottom: 5px; }
#searchbox { width: calc(100vw - 35px); }
#user,
#sidebar {
margin: 20px 0;
}
#logo,
#links {
margin-bottom: 5px;
}
#searchbox {
width: calc(100vw - 35px);
}
}
@media screen and (max-width: 480px) {
body.fixed_navbar { padding-top: 100px; }
#version { display: none; }
body.fixed_navbar {
padding-top: 100px;
}
#version {
display: none;
}
.post {
grid-template: "post_header post_header post_thumbnail" auto
grid-template:
"post_header post_header post_thumbnail" auto
"post_title post_title post_thumbnail" 1fr
"post_media post_media post_thumbnail" auto
"post_body post_body post_thumbnail" auto
@ -1798,12 +1986,20 @@ td, th {
padding: 5px 15px 10px 12px;
}
.compact .post_score { padding: 0; }
.compact .post_score {
padding: 0;
}
.post_score::before { content: "↑" }
.post_score::before {
content: "↑";
}
.post_header { font-size: 14px; }
.post_footer { margin-left: 15px; }
.post_header {
font-size: 14px;
}
.post_footer {
margin-left: 15px;
}
.replies > .comment {
margin-left: -12px;
@ -1822,9 +2018,15 @@ td, th {
}
/* .thread { margin-left: -5px; } */
.comment_right { padding: 5px 0 10px 2px; }
.comment_author { margin-left: 12px; }
.comment_data { margin-left: 12px; }
.comment_right {
padding: 5px 0 10px 2px;
}
.comment_author {
margin-left: 12px;
}
.comment_data {
margin-left: 12px;
}
.user-comment .comment_data {
flex-direction: column;
@ -1832,16 +2034,24 @@ td, th {
row-gap: 5px;
}
.comment_data::marker { font-size: 25px; }
.user-comment .comment_data > .comment_link { order: 2 }
.user_comment_data_divider { order: 1; }
.comment_data::marker {
font-size: 25px;
}
.user-comment .comment_data > .comment_link {
order: 2;
}
.user_comment_data_divider {
order: 1;
}
.user_comment_data_divider .dot {
display: unset;
margin-left: 5px;
}
.created-in { display: none; }
.created-in {
display: none;
}
.comment_score {
min-width: 32px;
@ -1851,11 +2061,19 @@ td, th {
margin-right: -5px;
}
#post_links > li { margin-right: 10px }
.post_footer > p > span#upvoted { display: none }
#post_links > li {
margin-right: 10px;
}
.post_footer > p > span#upvoted {
display: none;
}
.desktop_item { display: none }
.mobile_item { display: auto }
.desktop_item {
display: none;
}
.mobile_item {
display: auto;
}
.popup {
width: auto;

View File

@ -130,7 +130,13 @@
</form>
{% endif %}
</div>
{% if crate::utils::enable_rss() %}
<div id="sub_rss">
<a href="/r/{{ sub.name }}.rss" title="RSS feed for r/{{ sub.name }}">
<button class="subscribe">RSS feed</button >
</a>
</div>
{% endif %}
</div>
</details>
<details class="panel" id="sidebar">

View File

@ -1,30 +1,33 @@
{% extends "base.html" %}
{% import "utils.html" as utils %}
{% block search %}
{% call utils::search("".to_owned(), "") %}
{% endblock %}
{% block title %}{{ user.name.replace("u/", "") }} (u/{{ user.name }}) - Redlib{% endblock %}
{% block subscriptions %}
{% call utils::sub_list("") %}
{% endblock %}
{% block body %}
{% extends "base.html" %} {% import "utils.html" as utils %} {% block search %}
{% call utils::search("".to_owned(), "") %} {% endblock %} {% block title %}{{
user.name.replace("u/", "") }} (u/{{ user.name }}) - Redlib{% endblock %} {%
block subscriptions %} {% call utils::sub_list("") %} {% endblock %} {% block
body %}
<main>
{% if !is_filtered %}
<div id="column_one">
<form id="sort">
<div id="listing_options">
{% call utils::sort(["/user/", user.name.as_str()].concat(), ["overview", "comments", "submitted"], listing) %}
{% call utils::sort(["/user/", user.name.as_str()].concat(),
["overview", "comments", "submitted"], listing) %}
</div>
<select id="sort_select" name="sort">
{% call utils::options(sort.0, ["hot", "new", "top", "controversial"], "") %}
</select>{% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t">
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "all") %}
</select>{% endif %}<button id="sort_submit" class="submit">
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
{% call utils::options(sort.0, ["hot", "new", "top",
"controversial"], "") %}</select
>{% if sort.0 == "top" || sort.0 == "controversial" %}<select
id="timeframe"
name="t"
>
{% call utils::options(sort.1, ["hour", "day", "week", "month",
"year", "all"], "all") %}</select
>{% endif %}<button id="sort_submit" class="submit">
<svg
width="15"
viewBox="0 0 110 100"
fill="none"
stroke-width="10"
stroke-linecap="round"
>
<path d="M20 50 H100" />
<path d="M75 15 L100 50 L75 85" />
&rarr;
@ -33,50 +36,52 @@
</form>
{% if all_posts_hidden_nsfw %}
<center>All posts are hidden because they are NSFW. Enable "Show NSFW posts" in settings to view.</center>
{% endif %}
{% if no_posts %}
<center>
All posts are hidden because they are NSFW. Enable "Show NSFW posts"
in settings to view.
</center>
{% endif %} {% if no_posts %}
<center>No posts were found.</center>
{% endif %}
{% if all_posts_filtered %}
{% endif %} {% if all_posts_filtered %}
<center>(All content on this page has been filtered)</center>
{% else %}
<div id="posts">
{% for post in posts %}
{% if post.flags.nsfw && prefs.show_nsfw != "on" %}
{% else if !post.title.is_empty() %}
{% call utils::post_in_list(post) %}
{% else %}
{% for post in posts %} {% if post.flags.nsfw && prefs.show_nsfw !=
"on" %} {% else if !post.title.is_empty() %} {% call
utils::post_in_list(post) %} {% else %}
<div class="comment user-comment">
<div class="comment_left">
<p class="comment_score" title="{{ post.score.1 }}">
{% if prefs.hide_score != "on" %}
{{ post.score.0 }}
{% else %}
&#x2022;
{% endif %}
{% if prefs.hide_score != "on" %} {{ post.score.0 }} {%
else %} &#x2022; {% endif %}
</p>
<div class="line"></div>
</div>
<details class="comment_right" open>
<summary class="comment_data">
<a class="comment_link" href="{{ post.permalink }}" title="{{ post.link_title }}">{{ post.link_title }}</a>
<a
class="comment_link"
href="{{ post.permalink }}"
title="{{ post.link_title }}"
>{{ post.link_title }}</a
>
<div class="user_comment_data_divider">
<span class="created-in">&nbsp;in&nbsp;</span>
<a class="comment_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
<a
class="comment_subreddit"
href="/r/{{ post.community }}"
>r/{{ post.community }}</a
>
<span class="dot">&bull;</span>
<span class="created" title="{{ post.created }}">&nbsp;{{ post.rel_time }}</span>
<span class="created" title="{{ post.created }}"
>&nbsp;{{ post.rel_time }}</span
>
</div>
</summary>
<p class="comment_body">{{ post.body|safe }}</p>
</details>
</div>
{% endif %}
{% endfor %}
{% if prefs.use_hls == "on" %}
{% endif %} {% endfor %} {% if prefs.use_hls == "on" %}
<script src="/hls.min.js"></script>
<script src="/playHLSVideo.js"></script>
{% endif %}
@ -85,11 +90,17 @@
<footer>
{% if ends.0 != "" %}
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}" accesskey="P">PREV</a>
{% endif %}
{% if ends.1 != "" %}
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}" accesskey="N">NEXT</a>
<a
href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}"
accesskey="P"
>PREV</a
>
{% endif %} {% if ends.1 != "" %}
<a
href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}"
accesskey="N"
>NEXT</a
>
{% endif %}
</footer>
</div>
@ -99,7 +110,12 @@
<center>(Content from u/{{ user.name }} has been filtered)</center>
{% endif %}
<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"
/>
<h1 id="user_title">{{ user.title }}</h1>
<p id="user_name">u/{{ user.name }}</p>
<div id="user_description">{{ user.description }}</div>
@ -113,26 +129,48 @@
{% let name = ["u_", user.name.as_str()].join("") %}
<div id="user_subscription">
{% if prefs.subscriptions.contains(name) %}
<form action="/r/{{ name }}/unsubscribe?redirect={{ redirect_url }}" method="POST">
<form
action="/r/{{ name }}/unsubscribe?redirect={{ redirect_url }}"
method="POST"
>
<button class="unsubscribe">Unfollow</button>
</form>
{% else %}
<form action="/r/{{ name }}/subscribe?redirect={{ redirect_url }}" method="POST">
<form
action="/r/{{ name }}/subscribe?redirect={{ redirect_url }}"
method="POST"
>
<button class="subscribe">Follow</button>
</form>
{% endif %}
</div>
<div id="user_filter">
{% if prefs.filters.contains(name) %}
<form action="/r/{{ name }}/unfilter?redirect={{ redirect_url }}" method="POST">
<form
action="/r/{{ name }}/unfilter?redirect={{ redirect_url }}"
method="POST"
>
<button class="unfilter">Unfilter</button>
</form>
{% else %}
<form action="/r/{{ name }}/filter?redirect={{ redirect_url }}" method="POST">
<form
action="/r/{{ name }}/filter?redirect={{ redirect_url }}"
method="POST"
>
<button class="filter">Filter</button>
</form>
{% endif %}
</div>
{% if crate::utils::enable_rss() %}
<div id="user_rss">
<a
href="/u/{{ user.name }}.rss"
title="RSS feed for u/{{ user.name }}"
>
<button class="subscribe">RSS feed</button>
</a>
</div>
{% endif %}
</div>
</div>
</aside>