22 Commits

Author SHA1 Message Date
ca86cf7c6e possible tag deletion fix 2026-01-26 21:49:48 +01:00
734071beab possible fix for tag deletion modal 2026-01-26 20:53:08 +01:00
0e9fedc9b4 enhance error handling for register 2026-01-26 20:28:25 +01:00
8fe362c966 making better use of the notification system 2026-01-26 20:02:15 +01:00
8180cdd885 well 2026-01-26 19:34:47 +01:00
b71711aa96 smoother navbar black background when scrolled 2026-01-26 18:23:49 +01:00
588ab3c3de gdfsgfds 2026-01-26 10:21:44 +01:00
46fb7067ed revert 166ba96f0f
revert revert c4bb21bc31

revert possible fix for slow loading times for non logged in users
2026-01-26 09:15:37 +00:00
5815334e7b revert 6bfb200626
revert revert c4bb21bc31

revert possible fix for slow loading times for non logged in users
2026-01-26 09:15:17 +00:00
6bfb200626 revert c4bb21bc31
revert possible fix for slow loading times for non logged in users
2026-01-26 09:11:29 +00:00
166ba96f0f revert c4bb21bc31
revert possible fix for slow loading times for non logged in users
2026-01-26 09:10:48 +00:00
a2ebea2ba3 if this fails I revert 2026-01-26 10:09:11 +01:00
91546a1f0d lets try it out lmao 2026-01-26 10:04:53 +01:00
017ac4ca4c bruh 2026-01-26 10:00:59 +01:00
cb31cfd85b possible random performance improvement ? 2026-01-26 09:58:30 +01:00
a5acc22c4a possible performance improvement for random 2026-01-26 09:47:05 +01:00
dbcf39c3ba possible query optimization for random large datasets 2026-01-26 09:37:57 +01:00
4d2fd7561f possible performance optimization 2026-01-26 09:31:02 +01:00
c4bb21bc31 possible fix for slow loading times for non logged in users 2026-01-26 09:24:27 +01:00
7d58acd5ed visually enhancing comment section 2026-01-26 07:33:29 +01:00
286791de0d feat: Implement custom emojis, pinned comments, and comment locking with database schema changes and frontend rendering updates. 2026-01-25 23:01:31 +01:00
1b8a9185bb Merge pull request 'feat: Introduce custom emojis, pinned comments, and thread locking with database schema, client-side caching, and UI updates.' (#6) from comments-fix into f0bm
Reviewed-on: #6
2026-01-25 21:37:10 +00:00
16 changed files with 233 additions and 59 deletions

View File

@@ -0,0 +1 @@
CREATE INDEX CONCURRENTLY IF NOT EXISTS tags_assign_tag_id_idx ON public.tags_assign (tag_id);

View File

@@ -1596,7 +1596,8 @@ div.posts>a:hover::after {
} }
.navbar.scrolled { .navbar.scrolled {
background: #000 !important; background: black !important;
transition: background 0.1s ease;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
} }
@@ -3848,7 +3849,7 @@ input#s_avatar {
/* Comments System */ /* Comments System */
#comments-container { #comments-container {
padding: 0px 5px 5px 5px; padding: 0px 5px 5px 5px;
background: var(--metadata-bg); background: #00000087;
color: var(--white); color: var(--white);
font-family: var(--font); font-family: var(--font);
} }
@@ -3941,7 +3942,7 @@ input#s_avatar {
.comment-input textarea { .comment-input textarea {
width: 100%; width: 100%;
min-height: 80px; min-height: 80px;
background: var(--bg); background: #000000a1;
border: 1px solid var(--gray); border: 1px solid var(--gray);
color: var(--white); color: var(--white);
padding: 10px; padding: 10px;
@@ -4028,6 +4029,13 @@ input#s_avatar {
text-decoration: underline; text-decoration: underline;
} }
#comment-sort,
#subscribe-btn,
#lock-thread-btn {
background: none;
border: none;
}
/* Admin buttons */ /* Admin buttons */
.admin-edit-btn, .admin-edit-btn,
.admin-delete-btn { .admin-delete-btn {
@@ -4121,7 +4129,7 @@ input#s_avatar {
/* Two-level comment replies */ /* Two-level comment replies */
.comment-replies { .comment-replies {
margin-left: 40px; margin-left: 10px;
border-left: 2px solid var(--gray); border-left: 2px solid var(--gray);
padding-left: 15px; padding-left: 15px;
margin-top: 10px; margin-top: 10px;

View File

@@ -52,7 +52,7 @@
const delbutton = document.createElement("a"); const delbutton = document.createElement("a");
delbutton.innerHTML = " ×"; delbutton.innerHTML = " ×";
delbutton.href = "#"; delbutton.href = "javascript:void(0)";
// Class for delegation // Class for delegation
delbutton.classList.add("admin-deltag"); delbutton.classList.add("admin-deltag");
@@ -70,18 +70,62 @@
if (!ctx) return; if (!ctx) return;
const { postid } = ctx; const { postid } = ctx;
if (!confirm("Do you really want to delete this tag?")) // Use normalized target logic if possible, or assume e.target (wrapped in listener)
return; // In delegation, we passed 'e'. e.target might be text node if not normalized in the handler call?
const tagname = e.target.parentElement.querySelector('a:first-child').innerText; // In listener I normalized 'target', but passed 'e' to deleteEvent.
// So 'e.target' is still the raw event target.
// I should check nodeType here or use closest properly.
const res = await (await fetch("/api/v2/admin/" + postid + "/tags/" + encodeURIComponent(tagname), { let target = e.target;
method: 'DELETE' if (target.nodeType === 3) target = target.parentElement;
})).json();
if (!res.success) const badge = target.closest('.badge');
return alert("uff"); const tagLink = badge.querySelector('a:first-child');
const tagname = tagLink.innerText;
renderTags(res.tags); const modal = document.getElementById('delete-tag-modal');
const nameEl = document.getElementById('delete-tag-name');
const confirmBtn = document.getElementById('delete-tag-confirm');
const cancelBtn = document.getElementById('delete-tag-cancel');
if (modal) {
nameEl.textContent = tagname;
confirmBtn.textContent = 'Delete';
confirmBtn.disabled = false;
modal.style.display = 'flex';
const closeModal = () => {
modal.style.display = 'none';
confirmBtn.onclick = null;
cancelBtn.onclick = null;
};
cancelBtn.onclick = closeModal;
confirmBtn.onclick = async () => {
confirmBtn.textContent = 'Deleting...';
confirmBtn.disabled = true;
try {
const res = await (await fetch("/api/v2/admin/" + postid + "/tags/" + encodeURIComponent(tagname), {
method: 'DELETE'
})).json();
if (!res.success) {
alert(res.msg || "uff");
confirmBtn.textContent = 'Delete';
confirmBtn.disabled = false;
} else {
renderTags(res.tags);
closeModal();
}
} catch (err) {
console.error(err);
alert("Error deleting tag");
confirmBtn.textContent = 'Delete';
confirmBtn.disabled = false;
}
};
}
}; };
const addtagClick = (e) => { const addtagClick = (e) => {
@@ -314,29 +358,24 @@
return false; return false;
}; };
// Event Delegation
// Event Delegation // Event Delegation
document.addEventListener("click", e => { document.addEventListener("click", e => {
if (e.target.matches("a#a_addtag")) { const target = e.target.nodeType === 3 ? e.target.parentElement : e.target;
if (target.matches("a#a_addtag")) {
addtagClick(e); addtagClick(e);
} else if (e.target.matches("a#a_toggle")) { } else if (target.matches("a#a_toggle")) {
toggleEvent(e); toggleEvent(e);
} else if (e.target.closest("svg#a_favo")) { } else if (target.closest("svg#a_favo")) {
toggleFavEvent(e); toggleFavEvent(e);
} else if (e.target.closest("svg#a_delete")) { } else if (target.closest("svg#a_delete")) {
deleteButtonEvent(e); deleteButtonEvent(e);
} else if (e.target.matches("#tags > .badge > a:first-child")) { } else if (target.matches("#tags > .badge > a:first-child")) {
editTagEvent(e); editTagEvent(e);
} else if (e.target.innerText === " \u00d7" && e.target.closest(".badge")) { // check text " x" or similar for delete? } else if (target.matches("#tags > .badge > a:last-child") || target.closest('.admin-deltag')) {
// Original was " ×" which is × (\u00d7). // Match by structure OR class (added in renderTags)
// Logic in deleteEvent expects match. deleteEvent(e);
// Let's rely on class or structure.
// In renderTags I added class 'admin-deltag'.
// Existing tags in HTML might NOT have this class unless rendered by JS?
// But existing tags are just HTML. We should match structure.
// selector: "#tags > .badge > a:last-child"
if (e.target.matches("#tags > .badge > a:last-child")) {
deleteEvent(e);
}
} }
}); });

View File

@@ -53,9 +53,8 @@ class CommentSystem {
// If comments are already rendered, we might need to re-render them to show emojis // If comments are already rendered, we might need to re-render them to show emojis
// but usually loadComments also happens async. // but usually loadComments also happens async.
// To be safe, if we just got emojis, trigger a silent update if container exists // To be safe, if we just got emojis, trigger a silent update if container exists
if (this.container && this.container.querySelector('.comment-content')) { if (this.container && this.lastData) {
// This is a bit heavy, but ensures emojis appear if they loaded AFTER comments this.render(this.lastData, this.lastUserId, this.lastIsSubscribed);
// For now let's just let it be.
} }
} else { } else {
this.customEmojis = {}; this.customEmojis = {};
@@ -188,6 +187,11 @@ class CommentSystem {
} }
render(comments, currentUserId, isSubscribed) { render(comments, currentUserId, isSubscribed) {
// Store for re-rendering when emojis load
this.lastData = comments;
this.lastUserId = currentUserId;
this.lastIsSubscribed = isSubscribed;
// Build two-level tree: top-level comments + all replies at one level // Build two-level tree: top-level comments + all replies at one level
const map = new Map(); const map = new Map();
const roots = []; const roots = [];

View File

@@ -126,6 +126,58 @@ window.requestAnimFrame = (function () {
registerModal.style.display = 'none'; registerModal.style.display = 'none';
} }
}); });
const registerForm = registerModal.querySelector('form');
if (registerForm) {
registerForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(registerForm);
const params = new URLSearchParams(formData);
try {
const res = await fetch('/register', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params
});
if (res.redirected) {
window.location.href = res.url;
return;
}
const text = await res.text();
let json;
try {
json = JSON.parse(text);
} catch (e) { }
if (json && json.success === false) {
let errDiv = registerForm.querySelector('.flash-error');
if (!errDiv) {
errDiv = document.createElement('div');
errDiv.className = 'flash-error';
errDiv.style.color = '#ff4444';
errDiv.style.textAlign = 'center';
errDiv.style.marginBottom = '10px';
errDiv.style.fontWeight = 'bold';
registerForm.insertBefore(errDiv, registerForm.firstChild); // Insert before h2 or inputs
// Actually firstChild is text or h2. Let's insert after H2 if possible?
// The form has H2 as first element roughly.
// insertBefore firstChild is fine, it puts it at top.
}
errDiv.textContent = json.msg;
}
} catch (err) {
console.error('Registration error:', err);
}
});
}
} }
// Initialize background preference // Initialize background preference
@@ -295,7 +347,7 @@ window.requestAnimFrame = (function () {
const navbar = document.querySelector('.navbar'); const navbar = document.querySelector('.navbar');
if (navbar) { if (navbar) {
window.addEventListener('scroll', () => { window.addEventListener('scroll', () => {
if (window.scrollY > 50) { if (window.scrollY > 10) {
navbar.classList.add('scrolled'); navbar.classList.add('scrolled');
} else { } else {
navbar.classList.remove('scrolled'); navbar.classList.remove('scrolled');
@@ -466,9 +518,10 @@ window.requestAnimFrame = (function () {
// Intercept clicks // Intercept clicks
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
const target = e.target.nodeType === 3 ? e.target.parentElement : e.target;
// Check for thumbnail links on index page // Check for thumbnail links on index page
const thumbnail = e.target.closest('.posts > a'); const thumbnail = target.closest('.posts > a');
if (thumbnail && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) { if (thumbnail && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
e.preventDefault(); e.preventDefault();
// Thumbnails inherit context (e.g. from Tag Index) // Thumbnails inherit context (e.g. from Tag Index)
@@ -476,7 +529,7 @@ window.requestAnimFrame = (function () {
return; return;
} }
const link = e.target.closest('#next, #prev, #random, #nav-random, .id-link, .nav-next, .nav-prev'); const link = target.closest('#next, #prev, #random, #nav-random, .id-link, .nav-next, .nav-prev');
if (link && link.href && link.hostname === window.location.hostname && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) { if (link && link.href && link.hostname === window.location.hostname && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
// Special check for random // Special check for random
if (link.id === 'random' || link.id === 'nav-random') { if (link.id === 'random' || link.id === 'nav-random') {
@@ -527,7 +580,7 @@ window.requestAnimFrame = (function () {
} else { } else {
loadItemAjax(link.href, true); loadItemAjax(link.href, true);
} }
} else if (e.target.closest('#togglebg')) { } else if (target.closest('#togglebg')) {
e.preventDefault(); e.preventDefault();
background = !background; background = !background;
localStorage.setItem('background', background.toString()); localStorage.setItem('background', background.toString());
@@ -552,9 +605,9 @@ window.requestAnimFrame = (function () {
canvas.classList.add('fader-out'); canvas.classList.add('fader-out');
} }
} }
} else if (e.target.closest('.removetag')) { } else if (target.closest('.removetag')) {
e.preventDefault(); e.preventDefault();
const removeBtn = e.target.closest('.removetag'); const removeBtn = target.closest('.removetag');
const tagLink = removeBtn.previousElementSibling; const tagLink = removeBtn.previousElementSibling;
if (tagLink) { if (tagLink) {
@@ -570,8 +623,11 @@ window.requestAnimFrame = (function () {
if (modal) { if (modal) {
nameEl.textContent = tagName; nameEl.textContent = tagName;
confirmBtn.textContent = 'Delete';
confirmBtn.disabled = false;
modal.style.display = 'flex'; modal.style.display = 'flex';
const closeModal = () => { const closeModal = () => {
modal.style.display = 'none'; modal.style.display = 'none';
confirmBtn.onclick = null; confirmBtn.onclick = null;
@@ -988,7 +1044,7 @@ class NotificationSystem {
init() { init() {
this.bindEvents(); this.bindEvents();
this.poll(); this.poll();
setInterval(() => this.poll(), 60000); // Poll every minute setInterval(() => this.poll(), 10000); // Poll every 10 seconds
} }
bindEvents() { bindEvents() {
@@ -1065,6 +1121,30 @@ class NotificationSystem {
} }
renderItem(n) { renderItem(n) {
if (n.type === 'approve') {
const link = `/${n.item_id}`;
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'}" data-id="${n.id}">
<div>
<strong>Your Upload has been approved</strong>
</div>
<small class="notif-time">${new Date(n.created_at).toLocaleString()}</small>
</a>
`;
}
if (n.type === 'admin_pending') {
const link = '/admin/approve';
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'}" data-id="${n.id}">
<div>
<strong>A new upload needs approval</strong>
</div>
<small class="notif-time">${new Date(n.created_at).toLocaleString()}</small>
</a>
`;
}
let typeText = 'Start'; let typeText = 'Start';
if (n.type === 'comment_reply') typeText = 'replied to you'; if (n.type === 'comment_reply') typeText = 'replied to you';
else if (n.type === 'subscription') typeText = 'commented in a thread you follow'; else if (n.type === 'subscription') typeText = 'commented in a thread you follow';

View File

@@ -49,6 +49,17 @@
span.insertAdjacentElement("beforeend", a); span.insertAdjacentElement("beforeend", a);
if (document.querySelector('a[href^="/admin"]')) {
const space = document.createTextNode('\u00A0'); // &nbsp;
span.appendChild(space);
const del = document.createElement("a");
del.className = "removetag";
del.href = "javascript:void(0)";
del.innerHTML = "&#215;";
span.insertAdjacentElement("beforeend", del);
}
document.querySelector("#tags").insertAdjacentElement("afterbegin", span); document.querySelector("#tags").insertAdjacentElement("afterbegin", span);
}); });
}; };
@@ -209,11 +220,12 @@
// Event Delegation // Event Delegation
document.addEventListener("click", e => { document.addEventListener("click", e => {
if (e.target.matches("a#a_addtag")) { const target = e.target.nodeType === 3 ? e.target.parentElement : e.target;
if (target.matches("a#a_addtag")) {
addtagClick(e); addtagClick(e);
} else if (e.target.matches("a#a_toggle")) { } else if (target.matches("a#a_toggle")) {
toggleEvent(e); toggleEvent(e);
} else if (e.target.closest("svg#a_favo")) { } else if (target.closest("svg#a_favo")) {
toggleFavEvent(e); toggleFavEvent(e);
} }
}); });

View File

@@ -47,7 +47,7 @@ export default new class {
tmp = "items.id in (select item_id from tags_assign where tag_id = 2 group by item_id)"; tmp = "items.id in (select item_id from tags_assign where tag_id = 2 group by item_id)";
break; break;
case 2: // untagged case 2: // untagged
tmp = "items.id not in (select item_id from tags_assign group by item_id)"; tmp = "not exists (select 1 from tags_assign where item_id = items.id)";
break; break;
case 3: // all case 3: // all
tmp = "1 = 1"; tmp = "1 = 1";
@@ -94,7 +94,7 @@ export default new class {
const untagged = +(await db` const untagged = +(await db`
select count(*) as total select count(*) as total
from "items" from "items"
where id not in (select item_id from tags_assign group by item_id) and active = true where not exists (select 1 from tags_assign where item_id = items.id) and active = true
`)[0].total; `)[0].total;
const sfw = +(await db` const sfw = +(await db`
select count(*) as total select count(*) as total

View File

@@ -33,7 +33,7 @@ export default {
${o.fav ? db`and "user".user ilike ${'%' + user + '%'}` : db``} ${o.fav ? db`and "user".user ilike ${'%' + user + '%'}` : db``}
${!o.fav && user ? db`and items.username ilike ${'%' + user + '%'}` : db``} ${!o.fav && user ? db`and items.username ilike ${'%' + user + '%'}` : db``}
${mime ? db`and items.mime ilike ${smime}` : db``} ${mime ? db`and items.mime ilike ${smime}` : db``}
${!o.session && globalfilter ? db`and items.id not in (select item_id from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``} ${!o.session && globalfilter ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
group by items.id, tags.tag group by items.id, tags.tag
`)?.length || 0; `)?.length || 0;
@@ -67,7 +67,7 @@ export default {
${o.fav ? db`and "user".user ilike ${'%' + user + '%'}` : db``} ${o.fav ? db`and "user".user ilike ${'%' + user + '%'}` : db``}
${!o.fav && user ? db`and items.username ilike ${'%' + user + '%'}` : db``} ${!o.fav && user ? db`and items.username ilike ${'%' + user + '%'}` : db``}
${mime ? db`and items.mime ilike ${smime}` : db``} ${mime ? db`and items.mime ilike ${smime}` : db``}
${!o.session && globalfilter ? db`and items.id not in (select item_id from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``} ${!o.session && globalfilter ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
group by items.id, tags.tag, ta.tag_id group by items.id, tags.tag, ta.tag_id
order by items.id desc order by items.id desc
offset ${offset} offset ${offset}
@@ -131,7 +131,7 @@ export default {
${o.fav ? db`and "user"."user" ilike ${user}` : db``} ${o.fav ? db`and "user"."user" ilike ${user}` : db``}
${!o.fav && user ? db`and items.username ilike ${'%' + user + '%'}` : db``} ${!o.fav && user ? db`and items.username ilike ${'%' + user + '%'}` : db``}
${mime ? db`and items.mime ilike ${smime}` : db``} ${mime ? db`and items.mime ilike ${smime}` : db``}
${!o.session && globalfilter ? db`and items.id not in (select item_id from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``} ${!o.session && globalfilter ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
group by items.id, tags.tag, ta.tag_id group by items.id, tags.tag, ta.tag_id
order by items.id desc order by items.id desc
`; `;
@@ -234,7 +234,7 @@ export default {
and "user".user ilike ${'%' + user + '%'} and "user".user ilike ${'%' + user + '%'}
and items.active = 'true' and items.active = 'true'
${mime ? db`and items.mime ilike ${smime}` : db``} ${mime ? db`and items.mime ilike ${smime}` : db``}
${!o.session && globalfilter ? db`and items.id not in (select item_id from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``} ${!o.session && globalfilter ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
group by items.id group by items.id
order by random() order by random()
limit 1 limit 1
@@ -253,7 +253,7 @@ export default {
${tag ? db`and tags.normalized ilike '%' || slugify(${tag}) || '%'` : db``} ${tag ? db`and tags.normalized ilike '%' || slugify(${tag}) || '%'` : db``}
${user ? db`and items.username ilike ${'%' + user + '%'}` : db``} ${user ? db`and items.username ilike ${'%' + user + '%'}` : db``}
${mime ? db`and items.mime ilike ${smime}` : db``} ${mime ? db`and items.mime ilike ${smime}` : db``}
${!o.session && globalfilter ? db`and items.id not in (select item_id from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``} ${!o.session && globalfilter ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
group by items.id, tags.tag group by items.id, tags.tag
order by random() order by random()
limit 1 limit 1

View File

@@ -125,7 +125,7 @@ export default (router, tpl) => {
if (req.url.qs?.id) { if (req.url.qs?.id) {
const id = +req.url.qs.id; const id = +req.url.qs.id;
const f0ck = await db` const f0ck = await db`
select dest, mime select dest, mime, username, id
from "items" from "items"
where where
id = ${id} and id = ${id} and
@@ -138,6 +138,19 @@ export default (router, tpl) => {
}); });
} }
// Notify User
try {
const uploader = await db`select id from "user" where login = ${f0ck[0].username} limit 1`;
if (uploader.length > 0) {
await db`
INSERT INTO notifications (user_id, type, reference_id, item_id)
VALUES (${uploader[0].id}, 'approve', 0, ${id})
`;
}
} catch (err) {
console.error('[ADMIN APPROVE] Failed to notify user:', err);
}
await db`update "items" set active = 'true', is_deleted = false where id = ${id}`; await db`update "items" set active = 'true', is_deleted = false where id = ${id}`;
// Check if files need moving (if they are in deleted/) // Check if files need moving (if they are in deleted/)

View File

@@ -40,7 +40,7 @@ export default router => {
active = 'true' active = 'true'
${isFav ? db`and fu."user" = ${user}` : db`and items.username ilike ${user}`} ${isFav ? db`and fu."user" = ${user}` : db`and items.username ilike ${user}`}
${tag ? db`and tags.normalized ilike '%' || slugify(${tag}) || '%'` : db``} ${tag ? db`and tags.normalized ilike '%' || slugify(${tag}) || '%'` : db``}
${!hasSession && globalfilter ? db`and items.id not in (select item_id from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``} ${!hasSession && globalfilter ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
order by random() order by random()
limit 1 limit 1
`; `;

View File

@@ -9,10 +9,11 @@ export default (router, tpl) => {
try { try {
const notifications = await db` const notifications = await db`
SELECT n.id, n.type, n.item_id, n.reference_id, n.created_at, n.is_read, SELECT n.id, n.type, n.item_id, n.reference_id, n.created_at, n.is_read,
u.user as from_user, u.id as from_user_id COALESCE(u.user, 'System') as from_user,
COALESCE(u.id, 0) as from_user_id
FROM notifications n FROM notifications n
JOIN comments c ON n.reference_id = c.id LEFT JOIN comments c ON n.reference_id = c.id
JOIN "user" u ON c.user_id = u.id LEFT JOIN "user" u ON c.user_id = u.id
WHERE n.user_id = ${req.session.id} AND n.is_read = false WHERE n.user_id = ${req.session.id} AND n.is_read = false
ORDER BY n.created_at DESC ORDER BY n.created_at DESC
LIMIT 20 LIMIT 20

View File

@@ -15,6 +15,9 @@ export default (router, tpl) => {
const { username, password, password_confirm, token } = req.post; const { username, password, password_confirm, token } = req.post;
const renderError = (msg) => { const renderError = (msg) => {
if (req.headers['x-requested-with'] === 'XMLHttpRequest' || (req.headers.accept && req.headers.accept.includes('application/json'))) {
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg }));
}
return res.reply({ return res.reply({
body: tpl.render("register", { theme: req.cookies.theme ?? "f0ck", error: msg }) body: tpl.render("register", { theme: req.cookies.theme ?? "f0ck", error: msg })
}); });

View File

@@ -241,6 +241,19 @@ export const handleUpload = async (req, res) => {
`; `;
} }
// Notify Admins
try {
const admins = await db`select id from "user" where admin = true`;
for (const admin of admins) {
await db`
INSERT INTO notifications (user_id, type, reference_id, item_id)
VALUES (${admin.id}, 'admin_pending', 0, ${itemid})
`;
}
} catch (err) {
console.error('[UPLOAD HANDLER] Failed to notify admins:', err);
}
return sendJson(res, { return sendJson(res, {
success: true, success: true,
msg: 'Upload successful! Your upload is pending admin approval.', msg: 'Upload successful! Your upload is pending admin approval.',

View File

@@ -2,8 +2,8 @@
<div class="pagewrapper"> <div class="pagewrapper">
<div id="main"> <div id="main">
<div class="index-container"> <div class="index-container">
@if(tmp.user)<h2>user: <a href="/user/{{ tmp.user.toLowerCase() }}">{{ tmp.user.toLowerCase() }}</a>@if(tmp.mime) ({{ tmp.mime }}s)@else (all)@endif</h2>@endif @if(tmp.user)<h2>user: <a href="/user/{{ tmp.user.toLowerCase() }}">{!! tmp.user.toLowerCase() !!}</a>@if(tmp.mime) ({{ tmp.mime }}s)@else (all)@endif</h2>@endif
@if(tmp.tag)<h2>tag: @if(session)<a href="/search?tag={{ tmp.tag.toLowerCase() }}" target="_blank">{{ tmp.tag.toLowerCase() }}</a>@else{{ tmp.tag.toLowerCase() }}@endif@if(tmp.mime) ({{ tmp.mime }}s)@else (all)@endif</h2>@endif @if(tmp.tag)<h2>tag: @if(session)<a href="/search?tag={!! tmp.tag.toLowerCase() !!}" target="_blank">{!! tmp.tag.toLowerCase() !!}</a>@else{!! tmp.tag.toLowerCase() !!}@endif@if(tmp.mime) ({{ tmp.mime }}s)@else (all)@endif</h2>@endif
<div class="posts"> <div class="posts">
@each(items as item) @each(items as item)
<a href="{{ link.main }}{{ item.id }}" data-mime="{{ item.mime }}" data-mode="{{ item.tag_id ? ['','sfw','nsfw'][item.tag_id] : 'null' }}" style="background-image: url('/t/{{ item.id }}.webp')"><p></p></a> <a href="{{ link.main }}{{ item.id }}" data-mime="{{ item.mime }}" data-mode="{{ item.tag_id ? ['','sfw','nsfw'][item.tag_id] : 'null' }}" style="background-image: url('/t/{{ item.id }}.webp')"><p></p></a>

View File

@@ -105,7 +105,7 @@
@if(typeof item.tags !== "undefined") @if(typeof item.tags !== "undefined")
@each(item.tags as tag) @each(item.tags as tag)
<span @if(session)tooltip="{{ tag.user }}" @endif class="badge {{ tag.badge }} mr-2"> <span @if(session)tooltip="{{ tag.user }}" @endif class="badge {{ tag.badge }} mr-2">
<a href="/tag/{{ tag.normalized }}">{{ tag.tag }}</a>@if(session.admin)&nbsp;<a class="removetag" <a href="/tag/{{ tag.normalized }}">{!! tag.tag !!}</a>@if(session.admin)&nbsp;<a class="removetag"
href="#">&#215;</a>@endif href="#">&#215;</a>@endif
</span> </span>
@endeach @endeach

View File

@@ -24,7 +24,7 @@
</tr> </tr>
<tr> <tr>
<td>username</td> <td>username</td>
<td>{{ session.user }}</td> <td>{!! session.user !!}</td>
</tr> </tr>
<tr> <tr>
<td>@if(session.avatar)<a href="/{{ session.avatar }}"><img id="img_avatar" <td>@if(session.avatar)<a href="/{{ session.avatar }}"><img id="img_avatar"