feat: Implement comments, notifications, and custom emojis with new API routes, UI components, and database migrations.

This commit is contained in:
x
2026-01-25 03:48:24 +01:00
parent 595118c2c8
commit d903ce8b98
18 changed files with 1900 additions and 44 deletions

View File

@@ -411,9 +411,11 @@ window.requestAnimFrame = (function () {
const oldContent = container.querySelector('.content');
const oldMetadata = container.querySelector('.metadata');
const oldHeader = container.querySelector('._204863');
const oldComments = container.querySelector('#comments-container');
if (oldHeader) oldHeader.remove();
if (oldContent) oldContent.remove();
if (oldMetadata) oldMetadata.remove();
if (oldComments) oldComments.remove();
}
}
@@ -445,6 +447,9 @@ window.requestAnimFrame = (function () {
if (navbar) navbar.classList.remove("pbwork");
console.log("AJAX load complete");
// Notify extensions
document.dispatchEvent(new Event('f0ck:contentLoaded'));
} catch (err) {
console.error("AJAX load failed:", err);
}
@@ -658,6 +663,7 @@ window.requestAnimFrame = (function () {
// <wheeler>
const wheelEventListener = function (event) {
if (event.target.closest('.media-object, .steuerung')) {
event.preventDefault(); // Prevent default scroll
if (event.deltaY < 0) {
const el = document.getElementById('next');
if (el && el.href && !el.href.endsWith('#')) el.click();
@@ -668,7 +674,7 @@ window.requestAnimFrame = (function () {
}
};
window.addEventListener('wheel', wheelEventListener);
window.addEventListener('wheel', wheelEventListener, { passive: false });
// </wheeler>
@@ -687,7 +693,7 @@ window.requestAnimFrame = (function () {
f0ckimagescroll.removeAttribute("style");
f0ckimage.removeAttribute("style");
console.log("image is not expanded")
window.addEventListener('wheel', wheelEventListener);
window.addEventListener('wheel', wheelEventListener, { passive: false });
} else {
if (img.width > img.height) return;
isImageExpanded = true;
@@ -762,7 +768,8 @@ window.requestAnimFrame = (function () {
if (data.success && data.html) {
// Append new thumbnails
postsContainer.insertAdjacentHTML('beforeend', data.html);
const currentPosts = document.querySelector("div.posts");
if (currentPosts) currentPosts.insertAdjacentHTML('beforeend', data.html);
// Update state
infiniteState.currentPage = data.currentPage;
@@ -796,6 +803,9 @@ window.requestAnimFrame = (function () {
const PRELOAD_OFFSET = 500; // pixels before bottom to trigger load
window.addEventListener("scroll", () => {
const currentContainer = document.querySelector("div.posts");
// Only run if .posts exists (index/grid view)
if (!currentContainer) return;
if (!document.querySelector('#main')) return;
const scrollPosition = window.innerHeight + window.scrollY;
@@ -949,32 +959,6 @@ window.requestAnimFrame = (function () {
// </scroller>
})();
// disable default scroll event when mouse is on content div
// this is useful for items that have a lot of tags for example: 12536
const targetSelector = '.content';
let isMouseOver = true;
function isPageScrollable() {
return document.documentElement.scrollHeight > document.documentElement.clientHeight;
}
function onWheel(e) {
if (isMouseOver && isPageScrollable()) {
e.preventDefault();
}
}
function init() {
const el = document.querySelector(targetSelector);
if (!el) return;
el.addEventListener('mouseenter', () => isMouseOver = true);
el.addEventListener('mouseleave', () => isMouseOver = false);
window.addEventListener('wheel', onWheel, { passive: false });
}
window.addEventListener('load', init);
const sbtForm = document.getElementById('sbtForm');
if (sbtForm) {
sbtForm.addEventListener('submit', (e) => {
@@ -982,6 +966,105 @@ if (sbtForm) {
const input = document.getElementById('sbtInput').value.trim();
if (input) {
window.location.href = `/tag/${encodeURIComponent(input)}`;
}
});
}
// Notification System
class NotificationSystem {
constructor() {
this.bell = document.getElementById('nav-notif-btn');
this.dropdown = document.getElementById('notif-dropdown');
this.countBadge = this.bell ? this.bell.querySelector('.notif-count') : null;
this.list = this.dropdown ? this.dropdown.querySelector('.notif-list') : null;
this.markAllBtn = document.getElementById('mark-all-read');
if (this.bell && this.dropdown) {
this.init();
}
}
init() {
this.bindEvents();
this.poll();
setInterval(() => this.poll(), 60000); // Poll every minute
}
bindEvents() {
this.bell.addEventListener('click', (e) => {
e.preventDefault();
this.dropdown.classList.toggle('visible');
});
// Close on click outside
document.addEventListener('click', (e) => {
if (!this.bell.contains(e.target) && !this.dropdown.contains(e.target)) {
this.dropdown.classList.remove('visible');
}
});
if (this.markAllBtn) {
this.markAllBtn.addEventListener('click', async () => {
await fetch('/api/notifications/read', { method: 'POST' });
this.markAllReadUI();
});
}
}
async poll() {
try {
const res = await fetch('/api/notifications');
const data = await res.json();
if (data.success) {
this.updateUI(data.notifications);
}
} catch (e) {
console.error('Notification poll error', e);
}
}
updateUI(notifications) {
const unreadCount = notifications.filter(n => !n.is_read).length;
if (unreadCount > 0) {
this.countBadge.textContent = unreadCount;
this.countBadge.style.display = 'block';
} else {
this.countBadge.style.display = 'none';
}
if (notifications.length === 0) {
this.list.innerHTML = '<div class="notif-empty">No new notifications</div>';
return;
}
this.list.innerHTML = notifications.map(n => this.renderItem(n)).join('');
}
renderItem(n) {
const typeText = n.type === 'comment_reply' ? 'replied to your comment' : 'Start';
const cid = n.comment_id || n.reference_id;
const link = `/${n.item_id}#c${cid}`;
return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'}" onclick="window.location.href='${link}'; return false;">
<div>
<strong>${n.from_user}</strong> ${typeText}
</div>
<small class="notif-time">${new Date(n.created_at).toLocaleString()}</small>
</a>
`;
}
markAllReadUI() {
this.countBadge.style.display = 'none';
this.list.querySelectorAll('.notif-item.unread').forEach(el => el.classList.remove('unread'));
}
}
// Init Notifications
window.addEventListener('load', () => {
new NotificationSystem();
});