Extra fíčury pro odpovědi

Tento commit je obsažen v:
sneedmaster 2024-03-03 00:19:48 +01:00
rodič 28cefbd590
revize 1052f7c926
31 změnil soubory, kde provedl 638 přidání a 326 odebrání

Zobrazit soubor

@ -40,10 +40,11 @@ impl Board {
ip INET NOT NULL, ip INET NOT NULL,
bumps INT NOT NULL DEFAULT 0, bumps INT NOT NULL DEFAULT 0,
replies INT NOT NULL DEFAULT 0, replies INT NOT NULL DEFAULT 0,
quotes BIGINT[] NOT NULL DEFAULT '{{}}',
sticky BOOLEAN NOT NULL DEFAULT false, sticky BOOLEAN NOT NULL DEFAULT false,
locked BOOLEAN NOT NULL DEFAULT false, locked BOOLEAN NOT NULL DEFAULT false,
reported TIMESTAMPTZ DEFAULT NULL, reported TIMESTAMPTZ DEFAULT NULL,
reports JSONB NOT NULL DEFAULT '[]'::json, reports JSONB NOT NULL DEFAULT '[]'::jsonb,
bumped TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, bumped TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
)"#, )"#,

Zobrazit soubor

@ -63,6 +63,7 @@ pub struct Post {
pub ip: IpAddr, pub ip: IpAddr,
pub bumps: i32, pub bumps: i32,
pub replies: i32, pub replies: i32,
pub quotes: Vec<i64>,
pub sticky: bool, pub sticky: bool,
pub locked: bool, pub locked: bool,
pub reported: Option<DateTime<Utc>>, pub reported: Option<DateTime<Utc>>,

Zobrazit soubor

@ -116,8 +116,10 @@ impl Post {
} }
pub async fn read(ctx: &Ctx, board: String, id: i64) -> Result<Option<Self>, NekrochanError> { pub async fn read(ctx: &Ctx, board: String, id: i64) -> Result<Option<Self>, NekrochanError> {
let post = query_as("SELECT * FROM overboard WHERE board = $1 AND id = $2") let post = query_as(&format!(
.bind(board) "SELECT * FROM posts_{} WHERE id = $1",
board
))
.bind(id) .bind(id)
.fetch_optional(ctx.db()) .fetch_optional(ctx.db())
.await?; .await?;
@ -339,6 +341,21 @@ impl Post {
Ok(()) Ok(())
} }
pub async fn update_quotes(&self, ctx: &Ctx, id: i64) -> Result<(), NekrochanError> {
let post = query_as(&format!(
"UPDATE posts_{} SET quotes = array_append(quotes, $1) WHERE id = $2 RETURNING *",
self.board
))
.bind(id)
.bind(self.id)
.fetch_one(ctx.db())
.await?;
ctx.hub().send(PostUpdatedMessage { post }).await?;
Ok(())
}
pub async fn update_spoiler(&self, ctx: &Ctx) -> Result<(), NekrochanError> { pub async fn update_spoiler(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
let mut files = self.files.clone(); let mut files = self.files.clone();
@ -362,7 +379,7 @@ impl Post {
pub async fn delete(&self, ctx: &Ctx) -> Result<(), NekrochanError> { pub async fn delete(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
let to_be_deleted: Vec<Post> = query_as(&format!( let to_be_deleted: Vec<Post> = query_as(&format!(
"SELECT * FROM posts_{} WHERE id = $1 OR thread = $1", "SELECT * FROM posts_{} WHERE id = $1 OR thread = $1 ORDER BY id ASC",
self.board self.board
)) ))
.bind(self.id) .bind(self.id)
@ -380,15 +397,31 @@ impl Post {
let live_quote = format!("<a class=\"quote\" href=\"{url}\">&gt;&gt;{id}</a>"); let live_quote = format!("<a class=\"quote\" href=\"{url}\">&gt;&gt;{id}</a>");
let dead_quote = format!("<span class=\"dead-quote\">&gt;&gt;{id}</span>"); let dead_quote = format!("<span class=\"dead-quote\">&gt;&gt;{id}</span>");
query(&format!( let posts = query_as(&format!(
"UPDATE posts_{} SET content = REPLACE(content, $1, $2)", "UPDATE posts_{} SET content = REPLACE(content, $1, $2) WHERE content LIKE '%{}%' RETURNING *",
self.board self.board, live_quote
)) ))
.bind(live_quote) .bind(live_quote)
.bind(dead_quote) .bind(dead_quote)
.execute(ctx.db()) .fetch_all(ctx.db())
.await?; .await?;
for post in posts {
ctx.hub().send(PostUpdatedMessage { post }).await?;
}
let posts = query_as(&format!(
"UPDATE posts_{} SET quotes = array_remove(quotes, $1) WHERE $1 = ANY(quotes) RETURNING *",
self.board
))
.bind(id)
.fetch_all(ctx.db())
.await?;
for post in posts {
ctx.hub().send(PostUpdatedMessage { post }).await?;
}
let ip_key = format!("by_ip:{}", post.ip); let ip_key = format!("by_ip:{}", post.ip);
let content_key = format!("by_content:{}", digest(post.content_nomarkup.as_bytes())); let content_key = format!("by_content:{}", digest(post.content_nomarkup.as_bytes()));

Zobrazit soubor

@ -24,7 +24,7 @@ pub enum NekrochanError {
CapcodeFormatError, CapcodeFormatError,
#[error("Obsah nesmí mít více než 10000 znaků.")] #[error("Obsah nesmí mít více než 10000 znaků.")]
ContentFormatError, ContentFormatError,
#[error("Popis musí mít 1-128 znaků.")] #[error("Popis nesmí mít více než 128 znaků.")]
DescriptionFormatError, DescriptionFormatError,
#[error("E-mail nesmí mít více než 256 znaků.")] #[error("E-mail nesmí mít více než 256 znaků.")]
EmailFormatError, EmailFormatError,
@ -36,11 +36,9 @@ pub enum NekrochanError {
FileLimitError(usize), FileLimitError(usize),
#[error("Tvůj příspěvek vypadá jako spam.")] #[error("Tvůj příspěvek vypadá jako spam.")]
FloodError, FloodError,
#[error("Reverzní proxy nevrátilo vyžadovanou hlavičku '{}'.", .0)]
HeaderError(&'static str),
#[error("Domovní stránka vznikne po vytvoření nástěnky.")] #[error("Domovní stránka vznikne po vytvoření nástěnky.")]
HomePageError, HomePageError,
#[error("ID musí mít 1-16 znaků.")] #[error("ID musí mít 1-16 znaků a obsahovat pouze alfanumerické znaky.")]
IdFormatError, IdFormatError,
#[error("Nesprávné řešení CAPTCHA.")] #[error("Nesprávné řešení CAPTCHA.")]
IncorrectCaptchaError, IncorrectCaptchaError,
@ -231,7 +229,6 @@ impl ResponseError for NekrochanError {
NekrochanError::FileError(_, _) => StatusCode::UNPROCESSABLE_ENTITY, NekrochanError::FileError(_, _) => StatusCode::UNPROCESSABLE_ENTITY,
NekrochanError::FileLimitError(_) => StatusCode::BAD_REQUEST, NekrochanError::FileLimitError(_) => StatusCode::BAD_REQUEST,
NekrochanError::FloodError => StatusCode::TOO_MANY_REQUESTS, NekrochanError::FloodError => StatusCode::TOO_MANY_REQUESTS,
NekrochanError::HeaderError(_) => StatusCode::BAD_GATEWAY,
NekrochanError::HomePageError => StatusCode::NOT_FOUND, NekrochanError::HomePageError => StatusCode::NOT_FOUND,
NekrochanError::IdFormatError => StatusCode::BAD_REQUEST, NekrochanError::IdFormatError => StatusCode::BAD_REQUEST,
NekrochanError::IncorrectCaptchaError => StatusCode::UNAUTHORIZED, NekrochanError::IncorrectCaptchaError => StatusCode::UNAUTHORIZED,

Zobrazit soubor

@ -1,4 +1,7 @@
use askama::Template;
use db::models::{Board, Post};
use error::NekrochanError; use error::NekrochanError;
use web::tcx::TemplateCtx;
const GENERIC_PAGE_SIZE: i64 = 15; const GENERIC_PAGE_SIZE: i64 = 15;
@ -45,3 +48,14 @@ pub fn check_page(
Ok(()) Ok(())
} }
#[derive(Template)]
#[template(
ext = "html",
source = "{% import \"./macros/post.html\" as post %}{% call post::post(board, post, post.thread.is_some()) %}"
)]
pub struct PostTemplate {
tcx: TemplateCtx,
post: Post,
board: Board,
}

Zobrazit soubor

@ -7,13 +7,15 @@ use uuid::Uuid;
use crate::{ use crate::{
db::models::{Board, Post}, db::models::{Board, Post},
filters, web::tcx::TemplateCtx, PostTemplate,
web::tcx::TemplateCtx,
}; };
#[derive(Message)] #[derive(Message)]
#[rtype(result = "()")] #[rtype(result = "()")]
pub struct SessionMessage(pub String); pub enum SessionMessage {
Data(String),
Stop,
}
#[derive(Message)] #[derive(Message)]
#[rtype(result = "()")] #[rtype(result = "()")]
@ -56,17 +58,6 @@ pub struct PostRemovedMessage {
pub post: Post, pub post: Post,
} }
#[derive(Template)]
#[template(
ext = "html",
source = "{% import \"./macros/post.html\" as post %}{% call post::post(board, post, post.thread.is_some()) %}"
)]
struct PostTemplate {
tcx: TemplateCtx,
post: Post,
board: Board,
}
pub struct LiveHub { pub struct LiveHub {
pub cache: Connection, pub cache: Connection,
pub recv_by_uuid: HashMap<Uuid, (TemplateCtx, Recipient<SessionMessage>)>, pub recv_by_uuid: HashMap<Uuid, (TemplateCtx, Recipient<SessionMessage>)>,
@ -120,6 +111,10 @@ impl Handler<DisconnectMessage> for LiveHub {
.filter(|uuid| **uuid != msg.uuid) .filter(|uuid| **uuid != msg.uuid)
.map(Uuid::clone) .map(Uuid::clone)
.collect(); .collect();
if recv_by_thread.is_empty() {
self.recv_by_thread.remove(&msg.thread);
}
} }
} }
@ -158,7 +153,7 @@ impl Handler<PostCreatedMessage> for LiveHub {
.render() .render()
.unwrap_or_default(); .unwrap_or_default();
recv.do_send(SessionMessage( recv.do_send(SessionMessage::Data(
json!({ "type": "created", "id": id, "html": html }).to_string(), json!({ "type": "created", "id": id, "html": html }).to_string(),
)); ));
} }
@ -186,7 +181,7 @@ impl Handler<TargetedPostCreatedMessage> for LiveHub {
.render() .render()
.unwrap_or_default(); .unwrap_or_default();
recv.do_send(SessionMessage( recv.do_send(SessionMessage::Data(
json!({ "type": "created", "id": id, "html": html }).to_string(), json!({ "type": "created", "id": id, "html": html }).to_string(),
)); ));
} }
@ -227,7 +222,7 @@ impl Handler<PostUpdatedMessage> for LiveHub {
.render() .render()
.unwrap_or_default(); .unwrap_or_default();
recv.do_send(SessionMessage( recv.do_send(SessionMessage::Data(
json!({ "type": "updated", "id": id, "html": html }).to_string(), json!({ "type": "updated", "id": id, "html": html }).to_string(),
)); ));
} }
@ -249,12 +244,24 @@ impl Handler<PostRemovedMessage> for LiveHub {
None => return, None => return,
}; };
if post.thread.is_none() {
for uuid in uuids { for uuid in uuids {
let Some((_, recv)) = self.recv_by_uuid.get(uuid) else { let Some((_, recv)) = self.recv_by_uuid.get(uuid) else {
continue; continue;
}; };
recv.do_send(SessionMessage( recv.do_send(SessionMessage::Stop);
}
return;
}
for uuid in uuids {
let Some((_, recv)) = self.recv_by_uuid.get(uuid) else {
continue;
};
recv.do_send(SessionMessage::Data(
json!({ "type": "removed", "id": post.id }).to_string(), json!({ "type": "removed", "id": post.id }).to_string(),
)); ));
} }

Zobrazit soubor

@ -1,5 +1,6 @@
use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, StreamHandler}; use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, StreamHandler};
use actix_web_actors::ws::{Message as WsMessage, ProtocolError, WebsocketContext}; use actix_web_actors::ws::{Message as WsMessage, ProtocolError, WebsocketContext};
use serde_json::json;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
@ -21,12 +22,14 @@ impl Actor for LiveSession {
impl Handler<SessionMessage> for LiveSession { impl Handler<SessionMessage> for LiveSession {
type Result = (); type Result = ();
fn handle( fn handle(&mut self, msg: SessionMessage, ctx: &mut Self::Context) -> Self::Result {
&mut self, match msg {
SessionMessage(msg): SessionMessage, SessionMessage::Data(data) => ctx.text(data),
ctx: &mut Self::Context, SessionMessage::Stop => {
) -> Self::Result { ctx.text(json!({ "type": "thread_removed" }).to_string());
ctx.text(msg) self.finished(ctx)
}
};
} }
} }

Zobrazit soubor

@ -73,6 +73,7 @@ async fn run() -> Result<(), Error> {
.service(web::news::news) .service(web::news::news)
.service(web::overboard::overboard) .service(web::overboard::overboard)
.service(web::overboard_catalog::overboard_catalog) .service(web::overboard_catalog::overboard_catalog)
.service(web::post_json::post_json)
.service(web::thread::thread) .service(web::thread::thread)
.service(web::actions::appeal_ban::appeal_ban) .service(web::actions::appeal_ban::appeal_ban)
.service(web::actions::create_post::create_post) .service(web::actions::create_post::create_post)

Zobrazit soubor

@ -111,10 +111,10 @@ pub async fn markup(
board: Option<String>, board: Option<String>,
op: Option<i64>, op: Option<i64>,
text: &str, text: &str,
) -> Result<String, NekrochanError> { ) -> Result<(String, Vec<Post>), NekrochanError> {
let text = escape_html(text); let text = escape_html(text);
let text = if let Some(board) = board { let (text, quoted_posts) = if let Some(board) = board {
let quoted_posts = get_quoted_posts(ctx, &board, &text).await?; let quoted_posts = get_quoted_posts(ctx, &board, &text).await?;
let text = QUOTE_REGEX.replace_all(&text, |captures: &Captures| { let text = QUOTE_REGEX.replace_all(&text, |captures: &Captures| {
@ -142,9 +142,14 @@ pub async fn markup(
} }
}); });
text.to_string() let quoted_posts = quoted_posts
.into_values()
.filter(|post| op == Some(post.thread.unwrap_or(post.id)))
.collect();
(text.to_string(), quoted_posts)
} else { } else {
text (text, Vec::new())
}; };
let text = GREENTEXT_REGEX.replace_all(&text, "<span class=\"greentext\">&gt;$1</span>"); let text = GREENTEXT_REGEX.replace_all(&text, "<span class=\"greentext\">&gt;$1</span>");
@ -173,7 +178,7 @@ pub async fn markup(
text text
}; };
Ok(text.to_string()) Ok((text.to_string(), quoted_posts))
} }
fn escape_html(text: &str) -> String { fn escape_html(text: &str) -> String {

Zobrazit soubor

@ -168,30 +168,13 @@ pub async fn create_post(
Some(email_raw.into()) Some(email_raw.into())
}; };
let content_nomarkup = form.content.0.trim().to_owned(); let password_raw = form.password.trim();
if content_nomarkup.is_empty() && (thread.is_none() && board.config.0.require_thread_content) if password_raw.len() < 8 {
|| (thread.is_some() && board.config.0.require_reply_content) return Err(NekrochanError::PasswordFormatError);
{
return Err(NekrochanError::NoContentError);
} }
if content_nomarkup.len() > 10000 { let password = hash(password_raw)?;
return Err(NekrochanError::ContentFormatError);
}
let content = markup(
&ctx,
&perms,
Some(board.id.clone()),
thread.as_ref().map(|t| t.id),
&content_nomarkup,
)
.await?;
if board.config.antispam {
check_spam(&ctx, &board, ip, content_nomarkup.clone()).await?;
}
if form.files.len() > board.config.0.file_limit { if form.files.len() > board.config.0.file_limit {
return Err(NekrochanError::FileLimitError(board.config.0.file_limit)); return Err(NekrochanError::FileLimitError(board.config.0.file_limit));
@ -210,18 +193,35 @@ pub async fn create_post(
files.push(file); files.push(file);
} }
let thread_id = thread.as_ref().map(|t| t.id);
let content_nomarkup = form.content.0.trim().to_owned();
if board.config.antispam && !(perms.owner() || perms.bypass_antispam()) {
check_spam(&ctx, &board, ip, content_nomarkup.clone()).await?;
}
if content_nomarkup.is_empty() && files.is_empty() { if content_nomarkup.is_empty() && files.is_empty() {
return Err(NekrochanError::EmptyPostError); return Err(NekrochanError::EmptyPostError);
} }
let password_raw = form.password.trim(); if content_nomarkup.is_empty() && (thread.is_none() && board.config.0.require_thread_content)
|| (thread.is_some() && board.config.0.require_reply_content)
if password_raw.len() < 8 { {
return Err(NekrochanError::PasswordFormatError); return Err(NekrochanError::NoContentError);
} }
let password = hash(password_raw)?; if content_nomarkup.len() > 10000 {
let thread_id = thread.as_ref().map(|t| t.id); return Err(NekrochanError::ContentFormatError);
}
let (content, quoted_posts) = markup(
&ctx,
&perms,
Some(board.id.clone()),
thread.as_ref().map(|t| t.id),
&content_nomarkup,
)
.await?;
let post = Post::create( let post = Post::create(
&ctx, &ctx,
@ -241,6 +241,10 @@ pub async fn create_post(
) )
.await?; .await?;
for quoted_post in quoted_posts {
quoted_post.update_quotes(&ctx, post.id).await?;
}
let ts = thread.as_ref().map_or_else( let ts = thread.as_ref().map_or_else(
|| post.created.timestamp_micros(), || post.created.timestamp_micros(),
|thread| thread.created.timestamp_micros(), |thread| thread.created.timestamp_micros(),

Zobrazit soubor

@ -38,7 +38,7 @@ pub async fn edit_posts(
for (key, content_nomarkup) in edits { for (key, content_nomarkup) in edits {
let post = &posts[&key]; let post = &posts[&key];
let content_nomarkup = content_nomarkup.trim(); let content_nomarkup = content_nomarkup.trim();
let content = markup( let (content, quoted_posts) = markup(
&ctx, &ctx,
&tcx.perms, &tcx.perms,
Some(post.board.clone()), Some(post.board.clone()),
@ -49,6 +49,11 @@ pub async fn edit_posts(
post.update_content(&ctx, content, content_nomarkup.into()) post.update_content(&ctx, content, content_nomarkup.into())
.await?; .await?;
for quoted_post in quoted_posts {
quoted_post.update_quotes(&ctx, post.id).await?;
}
posts_edited += 1; posts_edited += 1;
} }

Zobrazit soubor

@ -1,4 +1,5 @@
use askama::Template; use askama::Template;
use sqlx::query_as;
use super::tcx::TemplateCtx; use super::tcx::TemplateCtx;
use crate::{ctx::Ctx, db::models::Post}; use crate::{ctx::Ctx, db::models::Post};
@ -22,7 +23,12 @@ pub async fn get_posts_from_ids(ctx: &Ctx, ids: &Vec<String>) -> Vec<Post> {
for id in ids { for id in ids {
if let Some((board, id)) = parse_id(id) { if let Some((board, id)) = parse_id(id) {
if let Ok(Some(post)) = Post::read(ctx, board, id).await { if let Ok(Some(post)) = query_as("SELECT * FROM overboard WHERE board = $1 AND id = $2")
.bind(board)
.bind(id)
.fetch_optional(ctx.db())
.await
{
posts.push(post); posts.push(post);
} }
} }

Zobrazit soubor

@ -37,7 +37,7 @@ pub async fn captcha(
_ => return Err(NekrochanError::NoCaptchaError), _ => return Err(NekrochanError::NoCaptchaError),
}; };
// >NOOOOOOOO YOU NEED TO MAKE A NEW ERROR TYPE FOR THIS ERROR THAT CAN ONLY HAPPEN ONCE IN THE CODE NOOOOOOOOOO // >YOU NEED TO MAKE A NEW ERROR TYPE FOR THIS ERROR THAT CAN ONLY HAPPEN ONCE IN THE CODE OR HOWEVER THE TRANS CHILDREN ARE PROTECTED
let png = captcha.as_base64().ok_or(NekrochanError::NoCaptchaError)?; let png = captcha.as_base64().ok_or(NekrochanError::NoCaptchaError)?;
let board = board.id; let board = board.id;

Zobrazit soubor

@ -11,6 +11,7 @@ pub mod logout;
pub mod news; pub mod news;
pub mod overboard; pub mod overboard;
pub mod overboard_catalog; pub mod overboard_catalog;
pub mod post_json;
pub mod staff; pub mod staff;
pub mod tcx; pub mod tcx;
pub mod thread; pub mod thread;

47
src/web/post_json.rs Normální soubor
Zobrazit soubor

@ -0,0 +1,47 @@
use actix_web::{
get,
web::{Data, Json, Path},
HttpRequest,
};
use askama::Template;
use serde::Serialize;
use crate::{
ctx::Ctx,
db::models::{Board, Post},
error::NekrochanError,
web::tcx::TemplateCtx,
PostTemplate,
};
#[derive(Serialize)]
pub struct PostJsonResponse {
pub html: String,
}
#[get("/post-json/{board}/{id}")]
pub async fn post_json(
ctx: Data<Ctx>,
req: HttpRequest,
path: Path<(String, i64)>,
) -> Result<Json<PostJsonResponse>, NekrochanError> {
let (board, id) = path.into_inner();
let tcx = TemplateCtx::new(&ctx, &req).await?;
let board = Board::read(&ctx, board.clone())
.await?
.ok_or(NekrochanError::BoardNotFound(board))?;
let post = Post::read(&ctx, board.id.clone(), id)
.await?
.ok_or(NekrochanError::PostNotFound(board.id.clone(), id))?;
let html = PostTemplate { tcx, board, post }
.render()
.unwrap_or_default();
let res = PostJsonResponse { html };
Ok(Json(res))
}

Zobrazit soubor

@ -1,10 +1,16 @@
use actix_web::{post, web::Data, HttpRequest, HttpResponse}; use actix_web::{post, web::Data, HttpRequest, HttpResponse};
use lazy_static::lazy_static;
use regex::Regex;
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
ctx::Ctx, db::models::Board, error::NekrochanError, qsform::QsForm, web::tcx::account_from_auth, ctx::Ctx, db::models::Board, error::NekrochanError, qsform::QsForm, web::tcx::account_from_auth,
}; };
lazy_static! {
static ref ID_REGEX: Regex = Regex::new(r#"^\w{1,16}$"#).unwrap();
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CreateBoardForm { pub struct CreateBoardForm {
id: String, id: String,
@ -28,7 +34,7 @@ pub async fn create_board(
let name = form.name.trim().to_owned(); let name = form.name.trim().to_owned();
let description = form.description.trim().to_owned(); let description = form.description.trim().to_owned();
if id.is_empty() || id.len() > 16 { if !ID_REGEX.is_match(&id) {
return Err(NekrochanError::IdFormatError); return Err(NekrochanError::IdFormatError);
} }
@ -36,7 +42,7 @@ pub async fn create_board(
return Err(NekrochanError::BoardNameFormatError); return Err(NekrochanError::BoardNameFormatError);
} }
if description.is_empty() || description.len() > 128 { if description.len() > 128 {
return Err(NekrochanError::DescriptionFormatError); return Err(NekrochanError::DescriptionFormatError);
} }

Zobrazit soubor

@ -36,7 +36,7 @@ pub async fn create_news(
} }
let content_nomarkup = content; let content_nomarkup = content;
let content = markup(&ctx, &account.perms(), None, None, &content_nomarkup).await?; let (content, _) = markup(&ctx, &account.perms(), None, None, &content_nomarkup).await?;
NewsPost::create(&ctx, title, content, content_nomarkup, account.username).await?; NewsPost::create(&ctx, title, content, content_nomarkup, account.username).await?;

Zobrazit soubor

@ -52,11 +52,12 @@ pub async fn edit_news(
} }
let content_nomarkup = content_nomarkup.trim(); let content_nomarkup = content_nomarkup.trim();
let content = markup(&ctx, &tcx.perms, None, None, content_nomarkup).await?; let (content, _) = markup(&ctx, &tcx.perms, None, None, content_nomarkup).await?;
newspost newspost
.update(&ctx, content, content_nomarkup.into()) .update(&ctx, content, content_nomarkup.into())
.await?; .await?;
news_edited += 1; news_edited += 1;
} }

Zobrazit soubor

@ -1,8 +1,11 @@
$(function () { $(function () {
update_expandable($(".expandable")); $(window).on("setup_post_events", function (event) {
setup_events($(`#${event.id}`).find(".expandable"));
}); });
function update_expandable(elements) { setup_events($(".expandable"));
function setup_events(elements) {
elements.each(function() { elements.each(function() {
$(this).click(function() { $(this).click(function() {
let src_link = $(this).attr("href"); let src_link = $(this).attr("href");
@ -65,7 +68,7 @@ function toggle_video(parent, src_link) {
parent parent
.parent() .parent()
.append( .append(
`<video class="src" src="${src_link}" autoplay="" controls="" loop=""></video>` `<video class="src" src="${src_link}" controls="" loop=""></video>`
); );
let src = parent.parent().find(".src"); let src = parent.parent().find(".src");
@ -76,6 +79,7 @@ function toggle_video(parent, src_link) {
thumb.removeClass("loading"); thumb.removeClass("loading");
thumb.hide(); thumb.hide();
src.show(); src.show();
src.get(0).play();
parent.closest(".post-files").addClass("float-none-b"); parent.closest(".post-files").addClass("float-none-b");
}); });
@ -96,3 +100,4 @@ function toggle_video(parent, src_link) {
parent.closest(".post-files").toggleClass("float-none-b"); parent.closest(".post-files").toggleClass("float-none-b");
parent.find(".closer").toggle(); parent.find(".closer").toggle();
} }
});

135
static/js/hover.js Normální soubor
Zobrazit soubor

@ -0,0 +1,135 @@
$.fn.isInViewport = function () {
let element_top = $(this).offset().top;
let element_bottom = element_top + $(this).outerHeight();
let viewport_top = $(window).scrollTop();
let viewport_bottom = viewport_top + $(window).height();
return element_bottom > viewport_top && element_top < viewport_bottom;
};
$(function () {
let cache = {};
let hovering = false;
let preview_w = 0;
let preview_h = 0;
$(window).on("setup_post_events", function (event) {
setup_events($(`#${event.id}`).find(".quote"));
});
setup_events($(".quote"));
function setup_events(elements) {
elements.on("mouseenter", function (event) {
toggle_hover($(this), event);
});
elements.on("mouseout", function (event) {
toggle_hover($(this), event);
});
elements.on("click", function (event) {
toggle_hover($(this), event);
});
elements.on("mousemove", move_preview);
}
function toggle_hover(quote, event) {
hovering = event.type === "mouseenter";
let path_segments = quote.prop("pathname").split("/");
let board = path_segments[2];
let id = quote.prop("hash").slice(1);
let post = $(`#${id}[data-board="${board}"]`);
if (post.length > 0) {
if (post.isInViewport()) {
post.toggleClass("highlighted", hovering);
return;
}
if (hovering) {
create_preview(post.clone(), event.clientX, event.clientY);
return;
}
}
if ($("#preview").length > 0 && !hovering) {
remove_preview();
return;
}
let html = cache[`${board}/${id}`];
if (html) {
post = $($.parseHTML(html));
create_preview(post, event.clientX, event.clientY);
return;
}
quote.css("cursor", "wait");
try {
$.get(`/post-json/${board}/${id}`, function (data) {
quote.css("cursor", "");
html = data.html;
cache[`${board}/${id}`] = html;
post = $($.parseHTML(html));
if (hovering)
create_preview(post, event.clientX, event.clientY);
});
} catch (e) {
quote.css("cursor", "");
console.error(e);
}
}
function move_preview(event) {
position_preview($("#preview"), event.clientX, event.clientY);
}
function create_preview(preview, x, y) {
preview.attr("id", "preview");
preview.addClass("box");
preview.removeClass("highlighted");
preview.css("position", "fixed");
let existing = $("#preview");
if (existing.length > 0) {
existing.replaceWith(preview);
} else {
preview.appendTo("body");
}
preview_w = preview.outerWidth();
preview_h = preview.outerHeight();
position_preview(preview, x, y);
$(window).trigger({ type: "setup_post_events", id: "preview" });
}
function remove_preview() {
$("#preview").remove();
}
function position_preview(preview, x, y) {
let ww = $(window).width();
let wh = $(window).height();
preview.css("left", `${Math.min(x + 4, ww - preview_w)}px`);
if (preview_h + y < wh) {
preview.css("top", `${y + 4}px`);
preview.css("bottom", "");
} else {
preview.css("bottom", `${wh - y + 4}px`);
preview.css("top", "");
}
}
});

Zobrazit soubor

@ -1,5 +1,5 @@
$(function () { $(function () {
let main = $(".thread + hr"); let main = $(".thread + hr + .pagination + hr");
main.after( main.after(
'<div id="live-info" class="box inline-block"><span id="live-indicator"></span> <span id="live-status"></span></div><br>' '<div id="live-info" class="box inline-block"><span id="live-indicator"></span> <span id="live-status"></span></div><br>'
@ -33,27 +33,28 @@ $(function () {
switch (data.type) { switch (data.type) {
case "created": case "created":
$(".thread").append(data.html + "<br>"); $(".thread").append(data.html + "<br>");
update_expandable($(`#${data.id} .expandable`)); $(window).trigger({
update_reltimes($(`#${data.id}`).find("time")); type: "setup_post_events",
update_quote_links($(`#${data.id}`).find(".quote-link")); id: data.id,
});
break; break;
case "updated": case "updated":
$(`#${data.id}`).replaceWith(data.html); $(`#${data.id}`).replaceWith(data.html);
update_expandable($(`#${data.id} .expandable`)); $(window).trigger({
update_reltimes($(`#${data.id}`).find("time")); type: "setup_post_events",
update_quote_links($(`#${data.id}`)).find(".quote-link"); id: data.id,
});
break; break;
case "removed": case "removed":
if (data.id === parseInt(thread[1])) {
ws.close();
$("#live-indicator").css("background-color", "red");
$("#live-status").text("Vlákno bylo odstraněno");
return;
}
$(`#${data.id}`).next("br").remove(); $(`#${data.id}`).next("br").remove();
$(`#${data.id}`).remove(); $(`#${data.id}`).remove();
break; break;
case "thread_removed":
setTimeout(function () {
$("#live-indicator").css("background-color", "red");
$("#live-status").text("Vlákno bylo odstraněno");
}, 100);
break;
default: default:
break; break;
} }

Zobrazit soubor

@ -7,10 +7,13 @@ $(function() {
window.localStorage.removeItem("quoted_post"); window.localStorage.removeItem("quoted_post");
} }
update_quote_links($(".quote-link")); $(window).on("setup_post_events", function (event) {
setup_events($(`#${event.id}`).find(".quote-link"));
}); });
function update_quote_links(elements) { setup_events($(".quote-link"));
function setup_events(elements) {
elements.each(function () { elements.each(function () {
$(this).click(function () { $(this).click(function () {
let post_id = $(this).text(); let post_id = $(this).text();
@ -28,5 +31,6 @@ function update_quote_links(elements) {
return false; return false;
}); });
}) });
} }
});

Zobrazit soubor

@ -1,12 +1,22 @@
$(function () { const MINUTE = 60000,
update_reltimes($("time")); HOUR = 3600000,
DAY = 86400000,
WEEK = 604800000,
MONTH = 2592000000,
YEAR = 31536000000;
setInterval(() => { $(function () {
update_reltimes($("time")); $(window).on("setup_post_events", function (event) {
}, 60000); setup_events($(`#${event.id}`).find("time"));
}); });
function update_reltimes(elements) { setup_events($("time"));
setInterval(() => {
setup_events($("time"));
}, 60000);
function setup_events(elements) {
elements.each(function () { elements.each(function () {
let title = $(this).attr("title"); let title = $(this).attr("title");
@ -20,13 +30,6 @@ function update_reltimes(elements) {
}); });
} }
const MINUTE = 60000,
HOUR = 3600000,
DAY = 86400000,
WEEK = 604800000,
MONTH = 2592000000,
YEAR = 31536000000;
function reltime(date) { function reltime(date) {
let delta = Date.now() - Date.parse(date); let delta = Date.now() - Date.parse(date);
let fut = false; let fut = false;
@ -49,7 +52,10 @@ function reltime(date) {
if (fut) { if (fut) {
rt = `za ${minutes} ${plural("minutu|minuty|minut", minutes)}`; rt = `za ${minutes} ${plural("minutu|minuty|minut", minutes)}`;
} else { } else {
rt = `před ${minutes} ${plural("minutou|minutami|minutami", minutes)}`; rt = `před ${minutes} ${plural(
"minutou|minutami|minutami",
minutes
)}`;
} }
} }
@ -57,7 +63,10 @@ function reltime(date) {
if (fut) { if (fut) {
rt = `za ${hours} ${plural("hodinu|hodiny|hodin", hours)}`; rt = `za ${hours} ${plural("hodinu|hodiny|hodin", hours)}`;
} else { } else {
rt = `před ${hours} ${plural("hodinou|hodinami|hodinami", hours)}`; rt = `před ${hours} ${plural(
"hodinou|hodinami|hodinami",
hours
)}`;
} }
} }
@ -81,7 +90,10 @@ function reltime(date) {
if (fut) { if (fut) {
rt = `za ${months} ${plural("měsíc|měsíce|měsíců", months)}`; rt = `za ${months} ${plural("měsíc|měsíce|měsíců", months)}`;
} else { } else {
rt = `před ${months} ${plural("měsícem|měsíci|měsíci", months)}`; rt = `před ${months} ${plural(
"měsícem|měsíci|měsíci",
months
)}`;
} }
} }
@ -110,3 +122,4 @@ function plural(plurals, count) {
return other; return other;
} }
} }
});

Zobrazit soubor

@ -173,7 +173,8 @@ summary {
padding: 8px; padding: 8px;
} }
.box:target { .box:target,
.box.highlighted {
background-color: var(--hl-box-color); background-color: var(--hl-box-color);
border-right: 1px solid var(--hl-box-border); border-right: 1px solid var(--hl-box-border);
border-bottom: 1px solid var(--hl-box-border); border-bottom: 1px solid var(--hl-box-border);
@ -314,12 +315,6 @@ summary {
margin-bottom: 0; margin-bottom: 0;
} }
.post::after {
content: "";
display: block;
clear: both;
}
.board-links a, .board-links a,
.pagination a, .pagination a,
.post-number a { .post-number a {
@ -401,10 +396,6 @@ summary {
margin: 0; margin: 0;
} }
.post .post-content {
margin: 1rem 2rem;
}
.post-content a { .post-content a {
color: var(--post-link-color); color: var(--post-link-color);
} }
@ -413,6 +404,14 @@ summary {
color: var(--post-link-hover); color: var(--post-link-hover);
} }
.clearfix {
clear: both;
}
.post .post-content {
margin: 1rem 2rem;
}
.dead-quote { .dead-quote {
color: var(--dead-quote-color); color: var(--dead-quote-color);
text-decoration: line-through; text-decoration: line-through;
@ -483,7 +482,7 @@ summary {
.post.box { .post.box {
display: block; display: block;
min-width: unset; min-width: auto;
} }
.thread > br { .thread > br {
@ -494,11 +493,6 @@ summary {
width: 140px; width: 140px;
height: 220px; height: 220px;
} }
#post-form,
#post-form .form-table {
width: 100%;
}
} }
/* Only in JS */ /* Only in JS */
@ -533,3 +527,9 @@ summary {
vertical-align: middle; vertical-align: middle;
border-radius: 50%; border-radius: 50%;
} }
#preview {
-webkit-box-shadow: 0px 0px 2.5px 2.5px rgba(0, 0, 0, 0.25);
-moz-box-shadow: 0px 0px 2.5px 2.5px rgba(0, 0, 0, 0.25);
box-shadow: 0px 0px 2.5px 2.5px rgba(0, 0, 0, 0.25);
}

Zobrazit soubor

@ -13,8 +13,9 @@
<!-- UX scripts --> <!-- UX scripts -->
<script src="/static/js/autofill.js"></script> <script src="/static/js/autofill.js"></script>
<script src="/static/js/expand.js"></script> <script src="/static/js/expand.js"></script>
<script src="/static/js/time.js"></script> <script src="/static/js/hover.js"></script>
<script src="/static/js/quote.js"></script> <script src="/static/js/quote.js"></script>
<script src="/static/js/time.js"></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</head> </head>
<body> <body>

Zobrazit soubor

@ -1,5 +1,5 @@
{% macro pagination(base, pages, current) %} {% macro pagination(base, pages, current) %}
<div class="box inline-block pagination"> <div class="pagination box inline-block">
{% if current == 1 %} {% if current == 1 %}
[Předchozí] [Předchozí]
{% else %} {% else %}
@ -13,8 +13,8 @@
{% else %} {% else %}
[<a href="{{ base }}?page={{ page }}">{{ page }}</a>] [<a href="{{ base }}?page={{ page }}">{{ page }}</a>]
{% endif %} {% endif %}
{% endfor %}
&#32; &#32;
{% endfor %}
{% if current == pages %} {% if current == pages %}
[Další] [Další]

Zobrazit soubor

@ -39,7 +39,7 @@
{% endif %} {% endif %}
{% if !boxed %} {% if !boxed %}
<a href="{{ post.post_url_notarget() }}">[Odpovědět]</a>&#32; <a href="{{ post.post_url_notarget() }}">[Otevřít]</a>&#32;
{% endif %} {% endif %}
</div> </div>
{% if !post.files.0.is_empty() %} {% if !post.files.0.is_empty() %}
@ -64,5 +64,14 @@
</div> </div>
{% endif %} {% endif %}
<div class="post-content">{{ post.content|add_yous(post.board, tcx.yous)|safe }}</div> <div class="post-content">{{ post.content|add_yous(post.board, tcx.yous)|safe }}</div>
<div class="clearfix"></div>
{% if !post.quotes.is_empty() %}
<div class="small">
Odpovědi:&#32;
{% for quote in post.quotes %}
<a class="quote" href="{{ post.thread_url() }}#{{ quote }}">&gt;&gt;{{ quote }}</a>&#32;
{% endfor %}
</div>
{% endif %}
</div> </div>
{% endmacro %} {% endmacro %}

Zobrazit soubor

@ -1,5 +1,5 @@
{% macro staff_nav() %} {% macro staff_nav() %}
<div class="box inline-block pagination"> <div class="pagination box inline-block">
[<a href="/staff/account">Účet</a>]&#32; [<a href="/staff/account">Účet</a>]&#32;
[<a href="/staff/accounts">Účty</a>]&#32; [<a href="/staff/accounts">Účty</a>]&#32;

Zobrazit soubor

@ -1,5 +1,5 @@
{% macro static_pagination(base, current) %} {% macro static_pagination(base, current) %}
<div class="box inline-block pagination"> <div class="pagination box inline-block">
{% if current == 1 %} {% if current == 1 %}
[Předchozí] [Předchozí]
{% else %} {% else %}

Zobrazit soubor

@ -69,7 +69,7 @@
</tr> </tr>
<tr> <tr>
<td class="label">Popis</td> <td class="label">Popis</td>
<td><input name="description" type="text" required=""></td> <td><input name="description" type="text"></td>
</tr> </tr>
<tr> <tr>
<td colspan="2"><input class="button" type="submit" value="Vytvořit nástěnku"></td> <td colspan="2"><input class="button" type="submit" value="Vytvořit nástěnku"></td>

Zobrazit soubor

@ -29,6 +29,12 @@
{% call post_form::post_form(board, true, thread.id) %} {% call post_form::post_form(board, true, thread.id) %}
</div> </div>
<hr> <hr>
<div class="pagination box inline-block">
<a href="/boards/{{ board.id }}">[Zpět]</a>&#32;
<a href="#bottom">[Dolů]</a>&#32;
<a href="/boards/{{ board.id }}/catalog">[Katalog]</a>
</div>
<hr>
<form method="post"> <form method="post">
<div class="thread"> <div class="thread">
{% call post::post(board, thread, false) %} {% call post::post(board, thread, false) %}
@ -38,6 +44,12 @@
{% endfor %} {% endfor %}
</div> </div>
<hr> <hr>
<div class="pagination box inline-block">
<a href="/boards/{{ board.id }}">[Zpět]</a>&#32;
<a href="#top">[Nahoru]</a>&#32;
<a href="/boards/{{ board.id }}/catalog">[Katalog]</a>
</div>
<hr>
{% call post_actions::post_actions() %} {% call post_actions::post_actions() %}
</form> </form>
{% endblock %} {% endblock %}