feat: Implement comments, notifications, and custom emojis with new API routes, UI components, and database migrations.
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user