Extra fíčury pro odpovědi
Tento commit je obsažen v:
rodič
28cefbd590
revize
1052f7c926
@ -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
|
||||||
)"#,
|
)"#,
|
||||||
|
@ -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>>,
|
||||||
|
@ -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}\">>>{id}</a>");
|
let live_quote = format!("<a class=\"quote\" href=\"{url}\">>>{id}</a>");
|
||||||
let dead_quote = format!("<span class=\"dead-quote\">>>{id}</span>");
|
let dead_quote = format!("<span class=\"dead-quote\">>>{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()));
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
14
src/lib.rs
14
src/lib.rs
@ -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,
|
||||||
|
}
|
||||||
|
@ -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(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -175,7 +170,7 @@ impl Handler<TargetedPostCreatedMessage> for LiveHub {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some((tcx, recv)) = self.recv_by_uuid.get(&msg.uuid)else {
|
let Some((tcx, recv)) = self.recv_by_uuid.get(&msg.uuid) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -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(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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\">>$1</span>");
|
let text = GREENTEXT_REGEX.replace_all(&text, "<span class=\"greentext\">>$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 {
|
||||||
|
@ -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(),
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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
47
src/web/post_json.rs
Normální 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))
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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?;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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");
|
||||||
@ -22,9 +25,9 @@ function update_expandable(elements) {
|
|||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle_image(parent, src_link) {
|
function toggle_image(parent, src_link) {
|
||||||
let thumb = parent.find(".thumb");
|
let thumb = parent.find(".thumb");
|
||||||
let src = parent.find(".src");
|
let src = parent.find(".src");
|
||||||
|
|
||||||
@ -51,9 +54,9 @@ function toggle_image(parent, src_link) {
|
|||||||
src.toggle();
|
src.toggle();
|
||||||
|
|
||||||
parent.closest(".post-files").toggleClass("float-none-b");
|
parent.closest(".post-files").toggleClass("float-none-b");
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle_video(parent, src_link) {
|
function toggle_video(parent, src_link) {
|
||||||
let expanded = parent.hasClass("expanded");
|
let expanded = parent.hasClass("expanded");
|
||||||
let thumb = parent.find(".thumb");
|
let thumb = parent.find(".thumb");
|
||||||
let src = parent.parent().find(".src");
|
let src = parent.parent().find(".src");
|
||||||
@ -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");
|
||||||
});
|
});
|
||||||
@ -95,4 +99,5 @@ 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
135
static/js/hover.js
Normální 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", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
$(function() {
|
$(function () {
|
||||||
let quoted_post = window.localStorage.getItem("quoted_post");
|
let quoted_post = window.localStorage.getItem("quoted_post");
|
||||||
|
|
||||||
if (quoted_post) {
|
if (quoted_post) {
|
||||||
@ -7,11 +7,14 @@ $(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"));
|
||||||
elements.each(function() {
|
|
||||||
|
function setup_events(elements) {
|
||||||
|
elements.each(function () {
|
||||||
$(this).click(function () {
|
$(this).click(function () {
|
||||||
let post_id = $(this).text();
|
let post_id = $(this).text();
|
||||||
let thread_url = $(this).attr("data-thread-url");
|
let thread_url = $(this).attr("data-thread-url");
|
||||||
@ -28,5 +31,6 @@ function update_quote_links(elements) {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
@ -1,12 +1,22 @@
|
|||||||
|
const MINUTE = 60000,
|
||||||
|
HOUR = 3600000,
|
||||||
|
DAY = 86400000,
|
||||||
|
WEEK = 604800000,
|
||||||
|
MONTH = 2592000000,
|
||||||
|
YEAR = 31536000000;
|
||||||
|
|
||||||
$(function () {
|
$(function () {
|
||||||
update_reltimes($("time"));
|
$(window).on("setup_post_events", function (event) {
|
||||||
|
setup_events($(`#${event.id}`).find("time"));
|
||||||
|
});
|
||||||
|
|
||||||
|
setup_events($("time"));
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
update_reltimes($("time"));
|
setup_events($("time"));
|
||||||
}, 60000);
|
}, 60000);
|
||||||
});
|
|
||||||
|
|
||||||
function update_reltimes(elements) {
|
function setup_events(elements) {
|
||||||
elements.each(function () {
|
elements.each(function () {
|
||||||
let title = $(this).attr("title");
|
let title = $(this).attr("title");
|
||||||
|
|
||||||
@ -18,16 +28,9 @@ function update_reltimes(elements) {
|
|||||||
|
|
||||||
$(this).text(rel);
|
$(this).text(rel);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const MINUTE = 60000,
|
function reltime(date) {
|
||||||
HOUR = 3600000,
|
|
||||||
DAY = 86400000,
|
|
||||||
WEEK = 604800000,
|
|
||||||
MONTH = 2592000000,
|
|
||||||
YEAR = 31536000000;
|
|
||||||
|
|
||||||
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
|
||||||
|
)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,9 +106,9 @@ function reltime(date) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return rt;
|
return rt;
|
||||||
}
|
}
|
||||||
|
|
||||||
function plural(plurals, count) {
|
function plural(plurals, count) {
|
||||||
let plurals_arr = plurals.split("|");
|
let plurals_arr = plurals.split("|");
|
||||||
let one = plurals_arr[0];
|
let one = plurals_arr[0];
|
||||||
let few = plurals_arr[1];
|
let few = plurals_arr[1];
|
||||||
@ -109,4 +121,5 @@ function plural(plurals, count) {
|
|||||||
} else {
|
} else {
|
||||||
return other;
|
return other;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
|
||||||
 
|
 
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% if current == pages %}
|
{% if current == pages %}
|
||||||
[Další]
|
[Další]
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if !boxed %}
|
{% if !boxed %}
|
||||||
<a href="{{ post.post_url_notarget() }}">[Odpovědět]</a> 
|
<a href="{{ post.post_url_notarget() }}">[Otevřít]</a> 
|
||||||
{% 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: 
|
||||||
|
{% for quote in post.quotes %}
|
||||||
|
<a class="quote" href="{{ post.thread_url() }}#{{ quote }}">>>{{ quote }}</a> 
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
@ -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>] 
|
[<a href="/staff/account">Účet</a>] 
|
||||||
[<a href="/staff/accounts">Účty</a>] 
|
[<a href="/staff/accounts">Účty</a>] 
|
||||||
|
|
||||||
|
@ -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 %}
|
||||||
|
@ -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>
|
||||||
|
@ -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> 
|
||||||
|
<a href="#bottom">[Dolů]</a> 
|
||||||
|
<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> 
|
||||||
|
<a href="#top">[Nahoru]</a> 
|
||||||
|
<a href="/boards/{{ board.id }}/catalog">[Katalog]</a>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
{% call post_actions::post_actions() %}
|
{% call post_actions::post_actions() %}
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Načítá se…
Odkázat v novém úkolu
Zablokovat Uživatele