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,
bumps INT NOT NULL DEFAULT 0,
replies INT NOT NULL DEFAULT 0,
quotes BIGINT[] NOT NULL DEFAULT '{{}}',
sticky BOOLEAN NOT NULL DEFAULT false,
locked BOOLEAN NOT NULL DEFAULT false,
reported TIMESTAMPTZ DEFAULT NULL,
reports JSONB NOT NULL DEFAULT '[]'::json,
reports JSONB NOT NULL DEFAULT '[]'::jsonb,
bumped 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 bumps: i32,
pub replies: i32,
pub quotes: Vec<i64>,
pub sticky: bool,
pub locked: bool,
pub reported: Option<DateTime<Utc>>,

Zobrazit soubor

@ -116,11 +116,13 @@ impl Post {
}
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")
.bind(board)
.bind(id)
.fetch_optional(ctx.db())
.await?;
let post = query_as(&format!(
"SELECT * FROM posts_{} WHERE id = $1",
board
))
.bind(id)
.fetch_optional(ctx.db())
.await?;
Ok(post)
}
@ -339,6 +341,21 @@ impl Post {
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> {
let mut files = self.files.clone();
@ -362,7 +379,7 @@ impl Post {
pub async fn delete(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
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
))
.bind(self.id)
@ -380,15 +397,31 @@ impl Post {
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>");
query(&format!(
"UPDATE posts_{} SET content = REPLACE(content, $1, $2)",
self.board
let posts = query_as(&format!(
"UPDATE posts_{} SET content = REPLACE(content, $1, $2) WHERE content LIKE '%{}%' RETURNING *",
self.board, live_quote
))
.bind(live_quote)
.bind(dead_quote)
.execute(ctx.db())
.fetch_all(ctx.db())
.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 content_key = format!("by_content:{}", digest(post.content_nomarkup.as_bytes()));

Zobrazit soubor

@ -24,7 +24,7 @@ pub enum NekrochanError {
CapcodeFormatError,
#[error("Obsah nesmí mít více než 10000 znaků.")]
ContentFormatError,
#[error("Popis musí mít 1-128 znaků.")]
#[error("Popis nesmí mít více než 128 znaků.")]
DescriptionFormatError,
#[error("E-mail nesmí mít více než 256 znaků.")]
EmailFormatError,
@ -36,11 +36,9 @@ pub enum NekrochanError {
FileLimitError(usize),
#[error("Tvůj příspěvek vypadá jako spam.")]
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.")]
HomePageError,
#[error("ID musí mít 1-16 znaků.")]
#[error("ID musí mít 1-16 znaků a obsahovat pouze alfanumerické znaky.")]
IdFormatError,
#[error("Nesprávné řešení CAPTCHA.")]
IncorrectCaptchaError,
@ -231,7 +229,6 @@ impl ResponseError for NekrochanError {
NekrochanError::FileError(_, _) => StatusCode::UNPROCESSABLE_ENTITY,
NekrochanError::FileLimitError(_) => StatusCode::BAD_REQUEST,
NekrochanError::FloodError => StatusCode::TOO_MANY_REQUESTS,
NekrochanError::HeaderError(_) => StatusCode::BAD_GATEWAY,
NekrochanError::HomePageError => StatusCode::NOT_FOUND,
NekrochanError::IdFormatError => StatusCode::BAD_REQUEST,
NekrochanError::IncorrectCaptchaError => StatusCode::UNAUTHORIZED,

Zobrazit soubor

@ -1,4 +1,7 @@
use askama::Template;
use db::models::{Board, Post};
use error::NekrochanError;
use web::tcx::TemplateCtx;
const GENERIC_PAGE_SIZE: i64 = 15;
@ -45,3 +48,14 @@ pub fn check_page(
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::{
db::models::{Board, Post},
filters,
web::tcx::TemplateCtx,
web::tcx::TemplateCtx, PostTemplate,
};
#[derive(Message)]
#[rtype(result = "()")]
pub struct SessionMessage(pub String);
pub enum SessionMessage {
Data(String),
Stop,
}
#[derive(Message)]
#[rtype(result = "()")]
@ -56,17 +58,6 @@ pub struct PostRemovedMessage {
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 cache: Connection,
pub recv_by_uuid: HashMap<Uuid, (TemplateCtx, Recipient<SessionMessage>)>,
@ -120,6 +111,10 @@ impl Handler<DisconnectMessage> for LiveHub {
.filter(|uuid| **uuid != msg.uuid)
.map(Uuid::clone)
.collect();
if recv_by_thread.is_empty() {
self.recv_by_thread.remove(&msg.thread);
}
}
}
@ -158,7 +153,7 @@ impl Handler<PostCreatedMessage> for LiveHub {
.render()
.unwrap_or_default();
recv.do_send(SessionMessage(
recv.do_send(SessionMessage::Data(
json!({ "type": "created", "id": id, "html": html }).to_string(),
));
}
@ -175,7 +170,7 @@ impl Handler<TargetedPostCreatedMessage> for LiveHub {
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;
};
@ -186,7 +181,7 @@ impl Handler<TargetedPostCreatedMessage> for LiveHub {
.render()
.unwrap_or_default();
recv.do_send(SessionMessage(
recv.do_send(SessionMessage::Data(
json!({ "type": "created", "id": id, "html": html }).to_string(),
));
}
@ -227,7 +222,7 @@ impl Handler<PostUpdatedMessage> for LiveHub {
.render()
.unwrap_or_default();
recv.do_send(SessionMessage(
recv.do_send(SessionMessage::Data(
json!({ "type": "updated", "id": id, "html": html }).to_string(),
));
}
@ -249,12 +244,24 @@ impl Handler<PostRemovedMessage> for LiveHub {
None => return,
};
if post.thread.is_none() {
for uuid in uuids {
let Some((_, recv)) = self.recv_by_uuid.get(uuid) else {
continue;
};
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(
recv.do_send(SessionMessage::Data(
json!({ "type": "removed", "id": post.id }).to_string(),
));
}

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

@ -111,10 +111,10 @@ pub async fn markup(
board: Option<String>,
op: Option<i64>,
text: &str,
) -> Result<String, NekrochanError> {
) -> Result<(String, Vec<Post>), NekrochanError> {
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 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 {
text
(text, Vec::new())
};
let text = GREENTEXT_REGEX.replace_all(&text, "<span class=\"greentext\">&gt;$1</span>");
@ -173,7 +178,7 @@ pub async fn markup(
text
};
Ok(text.to_string())
Ok((text.to_string(), quoted_posts))
}
fn escape_html(text: &str) -> String {

Zobrazit soubor

@ -168,30 +168,13 @@ pub async fn create_post(
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)
|| (thread.is_some() && board.config.0.require_reply_content)
{
return Err(NekrochanError::NoContentError);
if password_raw.len() < 8 {
return Err(NekrochanError::PasswordFormatError);
}
if content_nomarkup.len() > 10000 {
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?;
}
let password = hash(password_raw)?;
if form.files.len() > 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);
}
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() {
return Err(NekrochanError::EmptyPostError);
}
let password_raw = form.password.trim();
if password_raw.len() < 8 {
return Err(NekrochanError::PasswordFormatError);
if content_nomarkup.is_empty() && (thread.is_none() && board.config.0.require_thread_content)
|| (thread.is_some() && board.config.0.require_reply_content)
{
return Err(NekrochanError::NoContentError);
}
let password = hash(password_raw)?;
let thread_id = thread.as_ref().map(|t| t.id);
if content_nomarkup.len() > 10000 {
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(
&ctx,
@ -241,6 +241,10 @@ pub async fn create_post(
)
.await?;
for quoted_post in quoted_posts {
quoted_post.update_quotes(&ctx, post.id).await?;
}
let ts = thread.as_ref().map_or_else(
|| post.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 {
let post = &posts[&key];
let content_nomarkup = content_nomarkup.trim();
let content = markup(
let (content, quoted_posts) = markup(
&ctx,
&tcx.perms,
Some(post.board.clone()),
@ -49,6 +49,11 @@ pub async fn edit_posts(
post.update_content(&ctx, content, content_nomarkup.into())
.await?;
for quoted_post in quoted_posts {
quoted_post.update_quotes(&ctx, post.id).await?;
}
posts_edited += 1;
}

Zobrazit soubor

@ -1,4 +1,5 @@
use askama::Template;
use sqlx::query_as;
use super::tcx::TemplateCtx;
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 {
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);
}
}

Zobrazit soubor

@ -37,7 +37,7 @@ pub async fn captcha(
_ => 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 board = board.id;

Zobrazit soubor

@ -11,6 +11,7 @@ pub mod logout;
pub mod news;
pub mod overboard;
pub mod overboard_catalog;
pub mod post_json;
pub mod staff;
pub mod tcx;
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 lazy_static::lazy_static;
use regex::Regex;
use serde::Deserialize;
use crate::{
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)]
pub struct CreateBoardForm {
id: String,
@ -28,7 +34,7 @@ pub async fn create_board(
let name = form.name.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);
}
@ -36,7 +42,7 @@ pub async fn create_board(
return Err(NekrochanError::BoardNameFormatError);
}
if description.is_empty() || description.len() > 128 {
if description.len() > 128 {
return Err(NekrochanError::DescriptionFormatError);
}

Zobrazit soubor

@ -36,7 +36,7 @@ pub async fn create_news(
}
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?;

Zobrazit soubor

@ -52,11 +52,12 @@ pub async fn edit_news(
}
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
.update(&ctx, content, content_nomarkup.into())
.await?;
news_edited += 1;
}

Zobrazit soubor

@ -1,98 +1,103 @@
$(function () {
update_expandable($(".expandable"));
});
$(window).on("setup_post_events", function (event) {
setup_events($(`#${event.id}`).find(".expandable"));
});
function update_expandable(elements) {
elements.each(function() {
$(this).click(function() {
let src_link = $(this).attr("href");
setup_events($(".expandable"));
let is_video = [".mpeg", ".mov", ".mp4", ".webm", ".mkv", ".ogg"].some(
(ext) => src_link.endsWith(ext)
);
function setup_events(elements) {
elements.each(function() {
$(this).click(function() {
let src_link = $(this).attr("href");
if (!is_video) {
toggle_image($(this), src_link);
} else {
toggle_video($(this), src_link);
}
let is_video = [".mpeg", ".mov", ".mp4", ".webm", ".mkv", ".ogg"].some(
(ext) => src_link.endsWith(ext)
);
$(this).toggleClass("expanded");
if (!is_video) {
toggle_image($(this), src_link);
} else {
toggle_video($(this), src_link);
}
return false;
$(this).toggleClass("expanded");
return false;
})
})
})
}
function toggle_image(parent, src_link) {
let thumb = parent.find(".thumb");
let src = parent.find(".src");
if (src.length === 0) {
thumb.addClass("loading");
parent.append(`<img class="src" src="${src_link}">`);
}
function toggle_image(parent, src_link) {
let thumb = parent.find(".thumb");
let src = parent.find(".src");
src.hide();
src.on("load", function () {
thumb.removeClass("loading");
thumb.hide();
src.show();
if (src.length === 0) {
thumb.addClass("loading");
parent.closest(".post-files").addClass("float-none-b");
});
parent.append(`<img class="src" src="${src_link}">`);
return;
let src = parent.find(".src");
src.hide();
src.on("load", function () {
thumb.removeClass("loading");
thumb.hide();
src.show();
parent.closest(".post-files").addClass("float-none-b");
});
return;
}
thumb.toggle();
src.toggle();
parent.closest(".post-files").toggleClass("float-none-b");
}
thumb.toggle();
src.toggle();
parent.closest(".post-files").toggleClass("float-none-b");
}
function toggle_video(parent, src_link) {
let expanded = parent.hasClass("expanded");
let thumb = parent.find(".thumb");
let src = parent.parent().find(".src");
if (src.length === 0) {
thumb.addClass("loading");
parent.append('<b class="closer">[Zavřít]<br></b>');
parent
.parent()
.append(
`<video class="src" src="${src_link}" autoplay="" controls="" loop=""></video>`
);
function toggle_video(parent, src_link) {
let expanded = parent.hasClass("expanded");
let thumb = parent.find(".thumb");
let src = parent.parent().find(".src");
src.hide();
if (src.length === 0) {
thumb.addClass("loading");
src.on("loadstart", function () {
thumb.removeClass("loading");
thumb.hide();
src.show();
parent.append('<b class="closer">[Zavřít]<br></b>');
parent
.parent()
.append(
`<video class="src" src="${src_link}" controls="" loop=""></video>`
);
parent.closest(".post-files").addClass("float-none-b");
});
let src = parent.parent().find(".src");
return;
src.hide();
src.on("loadstart", function () {
thumb.removeClass("loading");
thumb.hide();
src.show();
src.get(0).play();
parent.closest(".post-files").addClass("float-none-b");
});
return;
}
thumb.toggle();
src.toggle();
if (expanded) {
src.get(0).pause();
src.get(0).currentTime = 0;
} else {
src.get(0).play();
}
parent.closest(".post-files").toggleClass("float-none-b");
parent.find(".closer").toggle();
}
thumb.toggle();
src.toggle();
if (expanded) {
src.get(0).pause();
src.get(0).currentTime = 0;
} else {
src.get(0).play();
}
parent.closest(".post-files").toggleClass("float-none-b");
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 () {
let main = $(".thread + hr");
let main = $(".thread + hr + .pagination + hr");
main.after(
'<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) {
case "created":
$(".thread").append(data.html + "<br>");
update_expandable($(`#${data.id} .expandable`));
update_reltimes($(`#${data.id}`).find("time"));
update_quote_links($(`#${data.id}`).find(".quote-link"));
$(window).trigger({
type: "setup_post_events",
id: data.id,
});
break;
case "updated":
$(`#${data.id}`).replaceWith(data.html);
update_expandable($(`#${data.id} .expandable`));
update_reltimes($(`#${data.id}`).find("time"));
update_quote_links($(`#${data.id}`)).find(".quote-link");
$(window).trigger({
type: "setup_post_events",
id: data.id,
});
break;
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}`).remove();
break;
case "thread_removed":
setTimeout(function () {
$("#live-indicator").css("background-color", "red");
$("#live-status").text("Vlákno bylo odstraněno");
}, 100);
break;
default:
break;
}

Zobrazit soubor

@ -1,4 +1,4 @@
$(function() {
$(function () {
let quoted_post = window.localStorage.getItem("quoted_post");
if (quoted_post) {
@ -7,26 +7,30 @@ $(function() {
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) {
elements.each(function() {
$(this).click(function () {
let post_id = $(this).text();
let thread_url = $(this).attr("data-thread-url");
let current_url = window.location.pathname;
setup_events($(".quote-link"));
function setup_events(elements) {
elements.each(function () {
$(this).click(function () {
let post_id = $(this).text();
let thread_url = $(this).attr("data-thread-url");
let current_url = window.location.pathname;
if (current_url !== thread_url) {
window.localStorage.setItem("quoted_post", post_id);
window.location.href = `${thread_url}#${post_id}`;
return false;
}
$("#post-form").attr("data-visible", true);
$("#content").append(`&gt;&gt;${post_id}\n`);
if (current_url !== thread_url) {
window.localStorage.setItem("quoted_post", post_id);
window.location.href = `${thread_url}#${post_id}`;
return false;
}
$("#post-form").attr("data-visible", true);
$("#content").append(`&gt;&gt;${post_id}\n`);
return false;
});
});
})
}
}
});

Zobrazit soubor

@ -1,25 +1,3 @@
$(function () {
update_reltimes($("time"));
setInterval(() => {
update_reltimes($("time"));
}, 60000);
});
function update_reltimes(elements) {
elements.each(function () {
let title = $(this).attr("title");
if (!title) {
$(this).attr("title", $(this).text());
}
let rel = reltime($(this).attr("datetime"));
$(this).text(rel);
});
}
const MINUTE = 60000,
HOUR = 3600000,
DAY = 86400000,
@ -27,86 +5,121 @@ const MINUTE = 60000,
MONTH = 2592000000,
YEAR = 31536000000;
function reltime(date) {
let delta = Date.now() - Date.parse(date);
let fut = false;
$(function () {
$(window).on("setup_post_events", function (event) {
setup_events($(`#${event.id}`).find("time"));
});
if (delta < 0) {
delta = Math.abs(delta);
fut = true;
setup_events($("time"));
setInterval(() => {
setup_events($("time"));
}, 60000);
function setup_events(elements) {
elements.each(function () {
let title = $(this).attr("title");
if (!title) {
$(this).attr("title", $(this).text());
}
let rel = reltime($(this).attr("datetime"));
$(this).text(rel);
});
}
let minutes = Math.floor(delta / MINUTE);
let hours = Math.floor(delta / HOUR);
let days = Math.floor(delta / DAY);
let weeks = Math.floor(delta / WEEK);
let months = Math.floor(delta / MONTH);
let years = Math.floor(delta / YEAR);
function reltime(date) {
let delta = Date.now() - Date.parse(date);
let fut = false;
let rt = "Teď";
if (delta < 0) {
delta = Math.abs(delta);
fut = true;
}
if (minutes > 0) {
if (fut) {
rt = `za ${minutes} ${plural("minutu|minuty|minut", minutes)}`;
let minutes = Math.floor(delta / MINUTE);
let hours = Math.floor(delta / HOUR);
let days = Math.floor(delta / DAY);
let weeks = Math.floor(delta / WEEK);
let months = Math.floor(delta / MONTH);
let years = Math.floor(delta / YEAR);
let rt = "Teď";
if (minutes > 0) {
if (fut) {
rt = `za ${minutes} ${plural("minutu|minuty|minut", minutes)}`;
} else {
rt = `před ${minutes} ${plural(
"minutou|minutami|minutami",
minutes
)}`;
}
}
if (hours > 0) {
if (fut) {
rt = `za ${hours} ${plural("hodinu|hodiny|hodin", hours)}`;
} else {
rt = `před ${hours} ${plural(
"hodinou|hodinami|hodinami",
hours
)}`;
}
}
if (days > 0) {
if (fut) {
rt = `za ${days} ${plural("den|dny|dnů", days)}`;
} else {
rt = `před ${days} ${plural("dnem|dny|dny", days)}`;
}
}
if (weeks > 0) {
if (fut) {
rt = `za ${weeks} ${plural("týden|týdny", weeks)}`;
} else {
rt = `před ${weeks} ${plural("týdnem|týdny", weeks)}`;
}
}
if (months > 0) {
if (fut) {
rt = `za ${months} ${plural("měsíc|měsíce|měsíců", months)}`;
} else {
rt = `před ${months} ${plural(
"měsícem|měsíci|měsíci",
months
)}`;
}
}
if (years > 0) {
if (fut) {
rt = `za ${years} ${plural("rok|roky|let", years)}`;
} else {
rt = `před ${years} ${plural("rokem|lety|lety", years)}`;
}
}
return rt;
}
function plural(plurals, count) {
let plurals_arr = plurals.split("|");
let one = plurals_arr[0];
let few = plurals_arr[1];
let other = plurals_arr[2];
if (count === 1) {
return one;
} else if (count < 5 && count !== 0) {
return few;
} else {
rt = `před ${minutes} ${plural("minutou|minutami|minutami", minutes)}`;
return other;
}
}
if (hours > 0) {
if (fut) {
rt = `za ${hours} ${plural("hodinu|hodiny|hodin", hours)}`;
} else {
rt = `před ${hours} ${plural("hodinou|hodinami|hodinami", hours)}`;
}
}
if (days > 0) {
if (fut) {
rt = `za ${days} ${plural("den|dny|dnů", days)}`;
} else {
rt = `před ${days} ${plural("dnem|dny|dny", days)}`;
}
}
if (weeks > 0) {
if (fut) {
rt = `za ${weeks} ${plural("týden|týdny", weeks)}`;
} else {
rt = `před ${weeks} ${plural("týdnem|týdny", weeks)}`;
}
}
if (months > 0) {
if (fut) {
rt = `za ${months} ${plural("měsíc|měsíce|měsíců", months)}`;
} else {
rt = `před ${months} ${plural("měsícem|měsíci|měsíci", months)}`;
}
}
if (years > 0) {
if (fut) {
rt = `za ${years} ${plural("rok|roky|let", years)}`;
} else {
rt = `před ${years} ${plural("rokem|lety|lety", years)}`;
}
}
return rt;
}
function plural(plurals, count) {
let plurals_arr = plurals.split("|");
let one = plurals_arr[0];
let few = plurals_arr[1];
let other = plurals_arr[2];
if (count === 1) {
return one;
} else if (count < 5 && count !== 0) {
return few;
} else {
return other;
}
}
});

Zobrazit soubor

@ -173,7 +173,8 @@ summary {
padding: 8px;
}
.box:target {
.box:target,
.box.highlighted {
background-color: var(--hl-box-color);
border-right: 1px solid var(--hl-box-border);
border-bottom: 1px solid var(--hl-box-border);
@ -314,12 +315,6 @@ summary {
margin-bottom: 0;
}
.post::after {
content: "";
display: block;
clear: both;
}
.board-links a,
.pagination a,
.post-number a {
@ -401,10 +396,6 @@ summary {
margin: 0;
}
.post .post-content {
margin: 1rem 2rem;
}
.post-content a {
color: var(--post-link-color);
}
@ -413,6 +404,14 @@ summary {
color: var(--post-link-hover);
}
.clearfix {
clear: both;
}
.post .post-content {
margin: 1rem 2rem;
}
.dead-quote {
color: var(--dead-quote-color);
text-decoration: line-through;
@ -483,7 +482,7 @@ summary {
.post.box {
display: block;
min-width: unset;
min-width: auto;
}
.thread > br {
@ -494,11 +493,6 @@ summary {
width: 140px;
height: 220px;
}
#post-form,
#post-form .form-table {
width: 100%;
}
}
/* Only in JS */
@ -533,3 +527,9 @@ summary {
vertical-align: middle;
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 -->
<script src="/static/js/autofill.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/time.js"></script>
{% block scripts %}{% endblock %}
</head>
<body>

Zobrazit soubor

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

Zobrazit soubor

@ -39,7 +39,7 @@
{% endif %}
{% if !boxed %}
<a href="{{ post.post_url_notarget() }}">[Odpovědět]</a>&#32;
<a href="{{ post.post_url_notarget() }}">[Otevřít]</a>&#32;
{% endif %}
</div>
{% if !post.files.0.is_empty() %}
@ -64,5 +64,14 @@
</div>
{% endif %}
<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>
{% endmacro %}

Zobrazit soubor

@ -1,5 +1,5 @@
{% 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/accounts">Účty</a>]&#32;

Zobrazit soubor

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

Zobrazit soubor

@ -69,7 +69,7 @@
</tr>
<tr>
<td class="label">Popis</td>
<td><input name="description" type="text" required=""></td>
<td><input name="description" type="text"></td>
</tr>
<tr>
<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) %}
</div>
<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">
<div class="thread">
{% call post::post(board, thread, false) %}
@ -38,6 +44,12 @@
{% endfor %}
</div>
<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() %}
</form>
{% endblock %}