Compare commits
24 Commits
svelte
...
chat-overh
| Author | SHA1 | Date | |
|---|---|---|---|
| f803ec27a5 | |||
| e61e24dfe8 | |||
| 44af6ae061 | |||
| 0739d72334 | |||
| 848055446a | |||
| 88e6b6efb9 | |||
| eea70ed62e | |||
| b6c906636c | |||
| 55b5a0ba9f | |||
| 5d60d80fc9 | |||
| 4dfb80b149 | |||
| b319f21cd4 | |||
| 28ed8d985c | |||
| 0f5e795793 | |||
| 78d14a1b56 | |||
| 3f149531da | |||
| 7a4432277d | |||
| 84eee7e13d | |||
| 2206b3eb86 | |||
| 5529724982 | |||
| 9c6070afff | |||
| 5bfa336773 | |||
| 517444f9ce | |||
| edb92ffa2a |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,3 +18,4 @@ public/tag_cache
|
||||
config.json
|
||||
bundle.css
|
||||
public/s/fonts/impact.woff
|
||||
f0ck-svelte/*
|
||||
@@ -99,6 +99,8 @@ services:
|
||||
container_name: f0ckm-nginx
|
||||
profiles:
|
||||
- f0ckm-nginx
|
||||
environment:
|
||||
- ENABLE_IPV6=true
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,8 +13,9 @@
|
||||
/* Meme Creator Styles */
|
||||
|
||||
.meme-creator-container {
|
||||
box-sizing: border-box !important;
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
max-width: 1100px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
@@ -111,6 +112,7 @@
|
||||
|
||||
.canvas-wrapper {
|
||||
flex: 1;
|
||||
max-width: 650px;
|
||||
min-width: 0; /* Allow shrinking */
|
||||
background: #000;
|
||||
border: 2px solid var(--nav-bg, #2b2b2b);
|
||||
@@ -238,22 +240,49 @@ canvas#memeCanvas {
|
||||
color: var(--white, #fff);
|
||||
}
|
||||
|
||||
/* Mobile Stacking - Simple 2-Row Layout */
|
||||
/* Mobile Stacking - Clean Flexbox Column Layout */
|
||||
@media (max-width: 950px) {
|
||||
.meme-editor-layout {
|
||||
display: grid !important;
|
||||
grid-template-columns: 1fr !important;
|
||||
grid-template-rows: 0.6fr auto !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: center !important;
|
||||
gap: 20px;
|
||||
}
|
||||
.meme-controls {
|
||||
box-sizing: border-box !important;
|
||||
width: 100% !important;
|
||||
grid-row: 2;
|
||||
max-width: 500px !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
.canvas-wrapper {
|
||||
box-sizing: border-box !important;
|
||||
width: 100% !important;
|
||||
grid-row: 1;
|
||||
max-width: 650px !important;
|
||||
margin-bottom: 10px;
|
||||
overflow: visible !important;
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Stack early when sidebar is open (sidebar is 300px, so 950 + 300 = 1250px)
|
||||
This prevents the canvas from shrinking too much when the sidebar takes space. */
|
||||
@media (max-width: 1250px) {
|
||||
body:not(.sidebar-right-hidden) .meme-editor-layout {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: center !important;
|
||||
gap: 20px;
|
||||
}
|
||||
body:not(.sidebar-right-hidden) .meme-controls {
|
||||
box-sizing: border-box !important;
|
||||
width: 100% !important;
|
||||
max-width: 500px !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
body:not(.sidebar-right-hidden) .canvas-wrapper {
|
||||
box-sizing: border-box !important;
|
||||
width: 100% !important;
|
||||
max-width: 650px !important;
|
||||
margin-bottom: 10px;
|
||||
overflow: visible !important;
|
||||
height: auto !important;
|
||||
@@ -294,20 +323,19 @@ canvas#memeCanvas {
|
||||
|
||||
/* Sidebar space reservation for meme pages */
|
||||
.meme-layout-wrapper {
|
||||
box-sizing: border-box !important;
|
||||
width: 100%;
|
||||
transition: padding-right 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
@media (min-width: 1000px) {
|
||||
/* Reserve space for the fixed sidebar so content doesn't flow behind it */
|
||||
.meme-layout-wrapper {
|
||||
body:not(.sidebar-right-hidden) .meme-layout-wrapper {
|
||||
padding-right: 300px;
|
||||
}
|
||||
|
||||
/* Collapse reserved space when sidebar is hidden */
|
||||
body.sidebar-right-hidden .meme-layout-wrapper {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Collapse reserved space when sidebar is hidden */
|
||||
body.sidebar-right-hidden .meme-layout-wrapper {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ class CommentSystem {
|
||||
document.body.classList.remove('sidebar-left-hidden');
|
||||
const layout = this.container.closest('.item-layout-container');
|
||||
if (layout) layout.classList.remove('sidebar-hidden');
|
||||
} else if (isHidden) {
|
||||
} else if (isHidden && (window.innerWidth >= 1000 || !document.body.classList.contains('layout-modern'))) {
|
||||
this.container.classList.add('faded-out');
|
||||
this.container.style.display = 'none';
|
||||
document.body.classList.add('sidebar-left-hidden');
|
||||
@@ -596,7 +596,7 @@ class CommentSystem {
|
||||
if (cached) cached.content = fullContent;
|
||||
}
|
||||
|
||||
contentEl.dataset.raw = this.escapeHtml(fullContent);
|
||||
contentEl.dataset.raw = fullContent;
|
||||
contentEl.innerHTML = this.renderCommentContent(fullContent, commentId);
|
||||
CommentSystem.autoplayConvertedGifs(contentEl);
|
||||
CommentSystem.playEmojiVideos(contentEl);
|
||||
@@ -622,6 +622,7 @@ class CommentSystem {
|
||||
|
||||
const contentEl = el.querySelector('.comment-content');
|
||||
if (contentEl) {
|
||||
contentEl.dataset.raw = data.content;
|
||||
contentEl.innerHTML = this.renderCommentContent(data.content, data.comment_id);
|
||||
CommentSystem.autoplayConvertedGifs(contentEl);
|
||||
CommentSystem.playEmojiVideos(contentEl);
|
||||
@@ -857,26 +858,14 @@ class CommentSystem {
|
||||
* border-right) is fully restored after the effect completes.
|
||||
*
|
||||
* @param {Element} el - The .comment element to animate.
|
||||
* @param {boolean} entering - True to also add 'comment-entering' (own post);
|
||||
* false for 'new-item-fade' only (live others).
|
||||
* @param {boolean} entering - Unused (kept for compatibility).
|
||||
*/
|
||||
static _animateNewComment(el, entering = false) {
|
||||
if (!el) return;
|
||||
|
||||
const classes = entering
|
||||
? ['comment-entering', 'new-item-fade']
|
||||
: ['new-item-fade'];
|
||||
|
||||
// Reset in case the element already has a stale animation
|
||||
classes.forEach(c => el.classList.remove(c));
|
||||
void el.offsetWidth; // force reflow so re-adding the class restarts the animation
|
||||
|
||||
classes.forEach(c => el.classList.add(c));
|
||||
|
||||
const cleanup = () => {
|
||||
classes.forEach(c => el.classList.remove(c));
|
||||
};
|
||||
el.addEventListener('animationend', cleanup, { once: true });
|
||||
el.classList.add('new-item-fade');
|
||||
setTimeout(() => {
|
||||
el.classList.remove('new-item-fade');
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
|
||||
@@ -892,7 +881,6 @@ class CommentSystem {
|
||||
if (el) {
|
||||
// Remove highlight from any previously highlighted or newly-posted comment
|
||||
document.querySelectorAll('.comment-highlighted').forEach(c => c.classList.remove('comment-highlighted'));
|
||||
document.querySelectorAll('.comment-entering').forEach(c => c.classList.remove('comment-entering'));
|
||||
document.querySelectorAll('.new-item-fade').forEach(c => c.classList.remove('new-item-fade'));
|
||||
|
||||
// Always smooth-scroll so there's no jarring jump
|
||||
@@ -1038,7 +1026,7 @@ class CommentSystem {
|
||||
preview.dataset.id = targetId;
|
||||
|
||||
// Remove temporary animation/highlight classes
|
||||
preview.classList.remove('new-item-fade', 'comment-highlighted', 'comment-entering');
|
||||
preview.classList.remove('new-item-fade', 'comment-highlighted');
|
||||
|
||||
// Remove input forms or action buttons
|
||||
const actions = preview.querySelector('.comment-actions');
|
||||
@@ -1173,7 +1161,12 @@ class CommentSystem {
|
||||
const contentEl = body.querySelector('.comment-content');
|
||||
if (contentEl) {
|
||||
const LINE_MAX = 200;
|
||||
const rawText = (contentEl.dataset.raw || '').trim();
|
||||
let rawText = (contentEl.dataset.raw || '').trim();
|
||||
if (rawText.includes('>') || rawText.includes('<') || rawText.includes('&')) {
|
||||
const txt = document.createElement('textarea');
|
||||
txt.innerHTML = rawText;
|
||||
rawText = txt.value;
|
||||
}
|
||||
// Preserve all lines but cap any single line exceeding LINE_MAX chars
|
||||
const lines = rawText.split('\n').map(line =>
|
||||
line.length > LINE_MAX ? line.substring(0, LINE_MAX) + '\u2026' : line
|
||||
@@ -1794,7 +1787,7 @@ class CommentSystem {
|
||||
const renderedContent = quoteEmojis
|
||||
? quoteContent.replace(/:([a-z0-9_]+):/g, (m, n) => this.renderEmoji(m, n))
|
||||
: quoteContent;
|
||||
return `<span class="greentext">>${renderedContent}</span>`;
|
||||
return `<span class="greentext">>${renderedContent.replace(/>/g, '>')}</span>`;
|
||||
}
|
||||
|
||||
// 2. Per-line limit to prevent marked.parse recursion on single giant lines
|
||||
@@ -2199,7 +2192,7 @@ class CommentSystem {
|
||||
if (!unsafe) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = unsafe;
|
||||
return div.innerHTML;
|
||||
return div.innerHTML.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
renderCommentAttachments(files, content = '') {
|
||||
@@ -3336,9 +3329,15 @@ class CommentSystem {
|
||||
params.append('has_poll', '1');
|
||||
}
|
||||
|
||||
const csrfToken = window.f0ckSession?.csrf_token || '';
|
||||
if (csrfToken) params.append('csrf_token', csrfToken);
|
||||
|
||||
const res = await fetch('/api/comments', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||
},
|
||||
body: params
|
||||
});
|
||||
|
||||
@@ -3808,10 +3807,13 @@ class CommentSystem {
|
||||
toggleComments() {
|
||||
if (!this.container) return;
|
||||
|
||||
if (document.body.classList.contains('layout-modern') && typeof window.toggleSidebarLeft === 'function') {
|
||||
if (document.body.classList.contains('layout-modern')) {
|
||||
if (window.innerWidth < 1000) return;
|
||||
if (typeof window.toggleSidebarLeft === 'function') {
|
||||
window.toggleSidebarLeft();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const layout = this.container.closest('.item-layout-container');
|
||||
const sidebar = this.container.closest('.item-sidebar-left');
|
||||
@@ -4457,20 +4459,71 @@ class CommentSystem {
|
||||
}, { passive: true });
|
||||
};
|
||||
|
||||
// Helper: reliable touch handling for tab buttons on mobile
|
||||
const addTabTouch = (btn, cb) => {
|
||||
// Helper: reliable touch handling for tab buttons on mobile (supports hold-to-action)
|
||||
let lastHoldTime = 0;
|
||||
const addTabTouch = (btn, cb, onHold) => {
|
||||
let ts = null;
|
||||
let holdTimer = null;
|
||||
let holdFired = false;
|
||||
btn.addEventListener('touchstart', (e) => {
|
||||
holdFired = false;
|
||||
ts = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
||||
if (onHold) {
|
||||
holdTimer = setTimeout(() => {
|
||||
holdFired = true;
|
||||
lastHoldTime = Date.now();
|
||||
try { navigator.vibrate?.(40); } catch(err) {}
|
||||
onHold();
|
||||
}, 500);
|
||||
}
|
||||
e.stopPropagation();
|
||||
}, { passive: true });
|
||||
|
||||
btn.addEventListener('touchmove', (e) => {
|
||||
if (!ts) return;
|
||||
const dx = Math.abs(e.touches[0].clientX - ts.x);
|
||||
const dy = Math.abs(e.touches[0].clientY - ts.y);
|
||||
if (dx > 10 || dy > 10) {
|
||||
clearTimeout(holdTimer);
|
||||
ts = null;
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
btn.addEventListener('touchend', (e) => {
|
||||
clearTimeout(holdTimer);
|
||||
if (holdFired) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
holdFired = false;
|
||||
ts = null;
|
||||
return;
|
||||
}
|
||||
if (!ts) return;
|
||||
const dx = Math.abs(e.changedTouches[0].clientX - ts.x);
|
||||
const dy = Math.abs(e.changedTouches[0].clientY - ts.y);
|
||||
ts = null;
|
||||
if (dx < 10 && dy < 10) { e.preventDefault(); cb(); }
|
||||
});
|
||||
|
||||
btn.addEventListener('touchcancel', () => {
|
||||
clearTimeout(holdTimer);
|
||||
holdFired = false;
|
||||
ts = null;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleFavDefault = (tabEl) => {
|
||||
const KEY = 'f0ck_emoji_default_tab';
|
||||
const isDefault = localStorage.getItem(KEY) === '__favs__';
|
||||
if (isDefault) {
|
||||
localStorage.removeItem(KEY);
|
||||
tabEl.classList.remove('ep-tab-default');
|
||||
window._showEmojiToast?.('Favorites unset as default tab');
|
||||
} else {
|
||||
localStorage.setItem(KEY, '__favs__');
|
||||
tabEl.classList.add('ep-tab-default');
|
||||
window._showEmojiToast?.('Favorites set as default tab');
|
||||
}
|
||||
};
|
||||
|
||||
// Build tab buttons — Favorites first
|
||||
@@ -4482,23 +4535,13 @@ class CommentSystem {
|
||||
favTab.innerHTML = '<i class="fa-solid fa-star" style="color:var(--emoji-fav-color,#f5c518);font-size:1.1em;"></i>';
|
||||
favTab.addEventListener('mousedown', e => e.preventDefault());
|
||||
favTab.addEventListener('click', () => showTab('__favs__'));
|
||||
addTabTouch(favTab, () => showTab('__favs__'));
|
||||
addTabTouch(favTab, () => showTab('__favs__'), () => toggleFavDefault(favTab));
|
||||
// Right-click on fav tab → set/unset as default
|
||||
favTab.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (navigator.maxTouchPoints > 0) return;
|
||||
const KEY = 'f0ck_emoji_default_tab';
|
||||
const isDefault = localStorage.getItem(KEY) === '__favs__';
|
||||
if (isDefault) {
|
||||
localStorage.removeItem(KEY);
|
||||
favTab.classList.remove('ep-tab-default');
|
||||
window._showEmojiToast?.('Favorites unset as default tab');
|
||||
} else {
|
||||
localStorage.setItem(KEY, '__favs__');
|
||||
favTab.classList.add('ep-tab-default');
|
||||
window._showEmojiToast?.('Favorites set as default tab');
|
||||
}
|
||||
if (Date.now() - lastHoldTime < 1000) return;
|
||||
toggleFavDefault(favTab);
|
||||
});
|
||||
// Mark as default if already set
|
||||
if (localStorage.getItem('f0ck_emoji_default_tab') === '__favs__') {
|
||||
|
||||
@@ -2307,9 +2307,49 @@ window.cancelAnimFrame = (function () {
|
||||
hidden = hiddenValue === 'true';
|
||||
}
|
||||
document.body.classList.toggle('sidebar-right-hidden', hidden);
|
||||
|
||||
// Always force-hide on very small screens regardless of saved preference
|
||||
if (window.innerWidth <= 599) {
|
||||
document.body.classList.add('sidebar-right-hidden');
|
||||
}
|
||||
};
|
||||
window.initSidebarRightToggle();
|
||||
|
||||
// Auto-hide sidebar on very small screens (≤599px), restore on wider
|
||||
(() => {
|
||||
const mq = window.matchMedia('(max-width: 599px)');
|
||||
const applyBreakpoint = (e) => {
|
||||
// Freeze transitions so the class change is instant (no sliding during resize)
|
||||
const sidebar = document.querySelector('.global-sidebar-right, .item-sidebar-right, .index-sidebar-right');
|
||||
const wrappers = document.querySelectorAll('.index-layout-wrapper, .item-layout-container, .pagination-container-fluid, .pagewrapper');
|
||||
if (sidebar) sidebar.style.setProperty('transition', 'none', 'important');
|
||||
wrappers.forEach(w => w.style.setProperty('transition', 'none', 'important'));
|
||||
|
||||
if (e.matches) {
|
||||
// Entering narrow: force-hide without touching localStorage
|
||||
document.body.classList.add('sidebar-right-hidden');
|
||||
} else {
|
||||
// Leaving narrow: restore from saved preference
|
||||
const saved = localStorage.getItem('sidebarRightHidden');
|
||||
const hidden = saved === 'true';
|
||||
document.body.classList.toggle('sidebar-right-hidden', hidden);
|
||||
}
|
||||
|
||||
// Force reflow then restore transitions
|
||||
if (sidebar) void sidebar.offsetWidth;
|
||||
requestAnimationFrame(() => {
|
||||
if (sidebar) sidebar.style.removeProperty('transition');
|
||||
wrappers.forEach(w => {
|
||||
w.style.removeProperty('transition');
|
||||
w.style.removeProperty('padding-right');
|
||||
w.style.removeProperty('right');
|
||||
});
|
||||
});
|
||||
};
|
||||
mq.addEventListener('change', applyBreakpoint);
|
||||
applyBreakpoint(mq); // run once on init
|
||||
})();
|
||||
|
||||
const loadPageAjax = async (url, replace = true, options = {}) => {
|
||||
if (isNavigating) return;
|
||||
|
||||
@@ -2398,7 +2438,7 @@ window.cancelAnimFrame = (function () {
|
||||
const isAdmin = !!pathname.match(/^\/admin/);
|
||||
const isMod = !!pathname.match(/^\/mod/);
|
||||
const isSettings = pathname.match(/\/settings\/?(?:$|\?)/);
|
||||
const isStatic = pathname.match(/\/(about|rules|terms|upload|subscriptions|stats|docs|discord|ranking|meme|memes)($|\/|\?)/);
|
||||
const isStatic = pathname.match(/\/(about|rules|terms|upload|subscriptions|stats|docs|discord|ranking|meme|memes|chat)($|\/|\?)/);
|
||||
const isUpload = pathname.match(/\/upload\/?(?:$|\?)/);
|
||||
const parts = pathname.split('/').filter(Boolean);
|
||||
const isItem = !pathname.match(/\/p\//) && (
|
||||
@@ -2406,8 +2446,9 @@ window.cancelAnimFrame = (function () {
|
||||
(parts.length >= 3 && (parts[0] === 'tag' || parts[0] === 'user' || parts[0] === 'h') && /^\d+$/.test(parts[parts.length - 1]))
|
||||
);
|
||||
const isMessages = !!pathname.match(/^\/messages(\/|$)/);
|
||||
const isChat = !!pathname.match(/^\/chat(\/|$)/);
|
||||
const isAbyss = !!pathname.match(/^\/abyss(\/|$|\?|#)/) || pathname === '/abyss';
|
||||
const isGrid = !isProfile && !isUserHall && !isUserHalls && !isHall && !isHalls && !isTags && !isComments && !isNotifs && !isItem && !isAdmin && !isMod && !isSettings && !isStatic && !isUpload && !isMessages && !isAbyss;
|
||||
const isGrid = !isProfile && !isUserHall && !isUserHalls && !isHall && !isHalls && !isTags && !isComments && !isNotifs && !isItem && !isAdmin && !isMod && !isSettings && !isStatic && !isUpload && !isMessages && !isChat && !isAbyss;
|
||||
|
||||
if (isItem) {
|
||||
isNavigating = false;
|
||||
@@ -4039,7 +4080,7 @@ window.cancelAnimFrame = (function () {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isSpecialLink && (pathname === '/' || pathname.startsWith('/halls') || pathname.startsWith('/h/') || pathname.startsWith('/notifications') || pathname.startsWith('/tag') || pathname.startsWith('/user/') || pathname.match(/^\/(image|video|audio)/) || pathname.match(/\/p\/\d+/) || pathname.match(/^\/\d+/) || pathname.match(/^\/(about|rules|terms|upload|subscriptions|stats|docs|settings|admin|mod|ranking|messages|meme|memes)/) || pathname.startsWith('/abyss'))) {
|
||||
if (!isSpecialLink && (pathname === '/' || pathname.startsWith('/halls') || pathname.startsWith('/h/') || pathname.startsWith('/notifications') || pathname.startsWith('/tag') || pathname.startsWith('/user/') || pathname.match(/^\/(image|video|audio)/) || pathname.match(/\/p\/\d+/) || pathname.match(/^\/\d+/) || pathname.match(/^\/(about|rules|terms|upload|subscriptions|stats|docs|settings|admin|mod|ranking|messages|meme|memes|chat)/) || pathname.startsWith('/abyss'))) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
@@ -4427,6 +4468,10 @@ window.cancelAnimFrame = (function () {
|
||||
if (window.renderTags) {
|
||||
window.renderTags(res.tags);
|
||||
}
|
||||
// Evict the cached item HTML so navigating back fetches fresh content
|
||||
if (window.invalidateItemCache) {
|
||||
window.invalidateItemCache(postid);
|
||||
}
|
||||
} else {
|
||||
revert();
|
||||
window.flashMessage('Error: ' + (res.msg || 'Failed to update rating'), 3000, 'error');
|
||||
@@ -4489,7 +4534,7 @@ window.cancelAnimFrame = (function () {
|
||||
(parts.length >= 3 && parts[0] === 'tag' && /^\d+$/.test(parts[parts.length - 1])) ||
|
||||
(parts.length >= 3 && parts[0] === 'user' && /^\d+$/.test(parts[parts.length - 1]))
|
||||
);
|
||||
const isSpecial = p.startsWith('/notifications') || p.startsWith('/tags') || p.startsWith('/user/') || p.startsWith('/subscriptions') || p.startsWith('/ranking');
|
||||
const isSpecial = p.startsWith('/notifications') || p.startsWith('/tags') || p.startsWith('/user/') || p.startsWith('/subscriptions') || p.startsWith('/ranking') || p.startsWith('/chat');
|
||||
const isGridLike = url.match(/\/p\/\d+/) || url.match(/[?&]page=\d+/) || p === '/';
|
||||
|
||||
window.f0ckDebug("[popstate] Navigation to:", url, { isItem, isSpecial, isGridLike });
|
||||
@@ -6277,8 +6322,10 @@ window.cancelAnimFrame = (function () {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
|
||||
// Freeze all transitions (override CSS !important) so the class toggle
|
||||
// doesn't trigger an additional right-property animation.
|
||||
// Freeze sidebar AND wrapper transitions so the class toggle
|
||||
// doesn't trigger a second padding-right animation.
|
||||
const wrappers = getSidebarWrappers();
|
||||
wrappers.forEach(w => w.style.setProperty('transition', 'none', 'important'));
|
||||
sidebar.style.setProperty('transition', 'none', 'important');
|
||||
|
||||
if (stateChanging) {
|
||||
@@ -6296,6 +6343,11 @@ window.cancelAnimFrame = (function () {
|
||||
// Restore CSS-controlled transitions on the next frame
|
||||
requestAnimationFrame(() => {
|
||||
sidebar.style.removeProperty('transition');
|
||||
wrappers.forEach(w => {
|
||||
w.style.removeProperty('transition');
|
||||
w.style.removeProperty('padding-right');
|
||||
w.style.removeProperty('right');
|
||||
});
|
||||
swipeRT.isDraggingSidebar = false;
|
||||
});
|
||||
};
|
||||
@@ -6533,6 +6585,10 @@ window.cancelAnimFrame = (function () {
|
||||
const settle = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
// Freeze sidebar AND wrapper transitions so the class toggle
|
||||
// doesn't trigger a second padding-right animation.
|
||||
const wrappers = getSidebarWrappers();
|
||||
wrappers.forEach(w => w.style.setProperty('transition', 'none', 'important'));
|
||||
sidebar.style.setProperty('transition', 'none', 'important');
|
||||
|
||||
if (stateChanging) {
|
||||
@@ -6545,6 +6601,11 @@ window.cancelAnimFrame = (function () {
|
||||
sidebar.style.transform = '';
|
||||
requestAnimationFrame(() => {
|
||||
sidebar.style.removeProperty('transition');
|
||||
wrappers.forEach(w => {
|
||||
w.style.removeProperty('transition');
|
||||
w.style.removeProperty('padding-right');
|
||||
w.style.removeProperty('right');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6612,15 +6673,55 @@ window.cancelAnimFrame = (function () {
|
||||
});
|
||||
window.addEventListener('resize', syncEdgeZone, { passive: true });
|
||||
|
||||
// Touchend on edgeZone: instant tap-to-toggle (no 300ms synthetic click delay)
|
||||
// Returns true if (clientX, clientY) is within the pill's hit area.
|
||||
// The pill is #sidebar-drag-zone::after — 4×40px, centered on pointer:fine,
|
||||
// right-aligned with padding-right:6px on pointer:coarse.
|
||||
const isWithinPillArea = (clientX, clientY) => {
|
||||
const rect = edgeZone.getBoundingClientRect();
|
||||
const pillH = 40;
|
||||
const hitPad = 12; // generous padding so it's easy to hit
|
||||
// Vertical: pill is always centered in the drag zone
|
||||
const pillCenterY = rect.top + rect.height / 2;
|
||||
if (clientY < pillCenterY - pillH / 2 - hitPad || clientY > pillCenterY + pillH / 2 + hitPad) return false;
|
||||
// Horizontal: right-aligned on touch, centered on mouse
|
||||
const isCoarse = window.matchMedia('(pointer: coarse)').matches;
|
||||
const pillCenterX = isCoarse ? rect.right - 6 - 2 : rect.left + rect.width / 2;
|
||||
return clientX >= pillCenterX - 2 - hitPad && clientX <= pillCenterX + 2 + hitPad;
|
||||
};
|
||||
|
||||
// Touchend: tap on the pill toggles sidebar; rest of drag zone only responds to swipe
|
||||
edgeZone.addEventListener('touchend', e => {
|
||||
// Only if it was a clean tap (no significant swipe movement)
|
||||
if (Math.abs(swipeRT.xDiff || 0) < 15 && Math.abs(swipeRT.yDiff || 0) < 15) {
|
||||
e.preventDefault();
|
||||
edgeZone.dispatchEvent(new Event('click'));
|
||||
e.preventDefault(); // always prevent 300ms synthetic click
|
||||
const touch = e.changedTouches[0];
|
||||
if (touch && isWithinPillArea(touch.clientX, touch.clientY)) {
|
||||
window.toggleSidebarRight();
|
||||
}
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
// Helper: all layout wrappers that have padding-right animated by sidebar state
|
||||
const getSidebarWrappers = () => {
|
||||
// At ≤999px: sidebar is a pure overlay — only pagination tracks it.
|
||||
if (window.innerWidth <= 599) {
|
||||
return document.querySelectorAll('.pagination-container-fluid');
|
||||
}
|
||||
// At ≥600px: exclude pagewrapper when a dedicated layout wrapper is present.
|
||||
// CSS already zeroes pagewrapper.padding-right via :has on those pages,
|
||||
// so including it here causes double-padding (pagewrapper + index-layout-wrapper both
|
||||
// get 300px snapped, making the grid 600px too narrow, then jumping back on settle).
|
||||
const hasDedicatedWrapper = !!(
|
||||
document.querySelector('.index-layout-wrapper') ||
|
||||
document.querySelector('.item-layout-container') ||
|
||||
document.querySelector('.meme-layout-wrapper') ||
|
||||
document.querySelector('.messages-convo-page')
|
||||
);
|
||||
const sel = hasDedicatedWrapper
|
||||
? '.index-layout-wrapper, .meme-layout-wrapper, .messages-convo-page, .pagination-container-fluid'
|
||||
: '.pagination-container-fluid, .pagewrapper';
|
||||
return document.querySelectorAll(sel);
|
||||
};
|
||||
|
||||
// Smoothly toggle sidebar open/closed
|
||||
window.toggleSidebarRight = () => {
|
||||
const sidebar = document.querySelector('.global-sidebar-right, .item-sidebar-right, .index-sidebar-right');
|
||||
@@ -6630,6 +6731,29 @@ window.cancelAnimFrame = (function () {
|
||||
const sidebarWidth = sidebar.offsetWidth || 300;
|
||||
const targetTranslate = isHidden ? -sidebarWidth : sidebarWidth;
|
||||
|
||||
// At ≤599px the sidebar is a pure overlay — never animate wrapper padding
|
||||
const animateWrappers = window.innerWidth > 599;
|
||||
|
||||
// Grid/pagewrapper: snap instantly in both directions (no animated reflow).
|
||||
// Pagination always animates — it tracks sidebar position with no reflow cost.
|
||||
const wrappers = getSidebarWrappers();
|
||||
if (animateWrappers) {
|
||||
wrappers.forEach(w => {
|
||||
const isPagination = w.classList.contains('pagination-container-fluid');
|
||||
const isMessages = w.classList.contains('messages-convo-page');
|
||||
|
||||
const prop = (isPagination || isMessages) ? 'right' : 'padding-right';
|
||||
const targetVal = isHidden ? sidebarWidth + 'px' : '0px';
|
||||
|
||||
// Lightweight layouts (pagination, messages) animate smoothly.
|
||||
// Heavy/reflow-intensive layouts snap instantly to avoid reflow lag.
|
||||
const shouldAnimate = (isPagination || isMessages);
|
||||
w.style.setProperty('transition', shouldAnimate ? `${prop} 0.3s ease-in-out` : 'none', 'important');
|
||||
w.style.setProperty(prop, targetVal, 'important');
|
||||
});
|
||||
void sidebar.offsetWidth; // settle layout before sidebar transform begins
|
||||
}
|
||||
|
||||
sidebar.style.setProperty('transition', 'transform 0.3s ease-in-out', 'important');
|
||||
sidebar.style.transform = `translateX(${targetTranslate}px)`;
|
||||
|
||||
@@ -6637,13 +6761,22 @@ window.cancelAnimFrame = (function () {
|
||||
const settle = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
// Freeze wrapper transitions so the class commit doesn't re-trigger them
|
||||
wrappers.forEach(w => w.style.setProperty('transition', 'none', 'important'));
|
||||
sidebar.style.setProperty('transition', 'none', 'important');
|
||||
const toHidden = !isHidden;
|
||||
document.body.classList.toggle('sidebar-right-hidden', toHidden);
|
||||
localStorage.setItem('sidebarRightHidden', toHidden);
|
||||
void sidebar.offsetWidth;
|
||||
sidebar.style.transform = '';
|
||||
requestAnimationFrame(() => sidebar.style.removeProperty('transition'));
|
||||
requestAnimationFrame(() => {
|
||||
sidebar.style.removeProperty('transition');
|
||||
wrappers.forEach(w => {
|
||||
w.style.removeProperty('transition');
|
||||
w.style.removeProperty('padding-right');
|
||||
w.style.removeProperty('right');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onEnd = ev => {
|
||||
@@ -6655,9 +6788,11 @@ window.cancelAnimFrame = (function () {
|
||||
setTimeout(settle, 350);
|
||||
};
|
||||
|
||||
// Click on edgeZone: smoothly toggle sidebar open/closed
|
||||
edgeZone.addEventListener('click', () => {
|
||||
// Click on edgeZone: only toggle when the click lands on the pill
|
||||
edgeZone.addEventListener('click', (e) => {
|
||||
if (isWithinPillArea(e.clientX, e.clientY)) {
|
||||
window.toggleSidebarRight();
|
||||
}
|
||||
});
|
||||
// </mouse-sidebar-drag>
|
||||
|
||||
@@ -6677,7 +6812,9 @@ window.cancelAnimFrame = (function () {
|
||||
});
|
||||
|
||||
const isLSActive = () =>
|
||||
document.body.classList.contains('layout-modern') && !!document.querySelector('.item-sidebar-left');
|
||||
document.body.classList.contains('layout-modern') &&
|
||||
!!document.querySelector('.item-sidebar-left') &&
|
||||
window.innerWidth >= 1000;
|
||||
|
||||
// Prepare sidebar for drag — keeps it in the CSS grid so overflow:hidden clips translateX.
|
||||
// OPEN: removes sidebar-hidden/display:none, places sidebar at translateX(-sw), reflows twice.
|
||||
@@ -6936,6 +7073,7 @@ window.cancelAnimFrame = (function () {
|
||||
};
|
||||
|
||||
window.toggleSidebarLeft = () => {
|
||||
if (window.innerWidth < 1000) return;
|
||||
const { sb, layout } = getLS();
|
||||
if (!sb || !layout) return;
|
||||
const isHidden = layout.classList.contains('sidebar-hidden');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1976,6 +1976,73 @@ if (window.__dmLoaded) {
|
||||
dmGridArea = document.createElement('div');
|
||||
dmGridArea.className = 'emoji-picker-grid';
|
||||
|
||||
// Helper: reliable touch handling for tab buttons on mobile (supports hold-to-action)
|
||||
let lastDmHoldTime = 0;
|
||||
const addDmTabTouch = (btn, cb, onHold) => {
|
||||
let ts = null;
|
||||
let holdTimer = null;
|
||||
let holdFired = false;
|
||||
btn.addEventListener('touchstart', (e) => {
|
||||
holdFired = false;
|
||||
ts = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
||||
if (onHold) {
|
||||
holdTimer = setTimeout(() => {
|
||||
holdFired = true;
|
||||
lastDmHoldTime = Date.now();
|
||||
try { navigator.vibrate?.(40); } catch(err) {}
|
||||
onHold();
|
||||
}, 500);
|
||||
}
|
||||
e.stopPropagation();
|
||||
}, { passive: true });
|
||||
|
||||
btn.addEventListener('touchmove', (e) => {
|
||||
if (!ts) return;
|
||||
const dx = Math.abs(e.touches[0].clientX - ts.x);
|
||||
const dy = Math.abs(e.touches[0].clientY - ts.y);
|
||||
if (dx > 10 || dy > 10) {
|
||||
clearTimeout(holdTimer);
|
||||
ts = null;
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
btn.addEventListener('touchend', (e) => {
|
||||
clearTimeout(holdTimer);
|
||||
if (holdFired) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
holdFired = false;
|
||||
ts = null;
|
||||
return;
|
||||
}
|
||||
if (!ts) return;
|
||||
const dx = Math.abs(e.changedTouches[0].clientX - ts.x);
|
||||
const dy = Math.abs(e.changedTouches[0].clientY - ts.y);
|
||||
ts = null;
|
||||
if (dx < 10 && dy < 10) { e.preventDefault(); cb(); }
|
||||
});
|
||||
|
||||
btn.addEventListener('touchcancel', () => {
|
||||
clearTimeout(holdTimer);
|
||||
holdFired = false;
|
||||
ts = null;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleDmFavDefault = (tabEl) => {
|
||||
const KEY = 'f0ck_emoji_default_tab';
|
||||
const isDefault = localStorage.getItem(KEY) === '__favs__';
|
||||
if (isDefault) {
|
||||
localStorage.removeItem(KEY);
|
||||
tabEl.classList.remove('ep-tab-default');
|
||||
window._showEmojiToast?.('Favorites unset as default tab');
|
||||
} else {
|
||||
localStorage.setItem(KEY, '__favs__');
|
||||
tabEl.classList.add('ep-tab-default');
|
||||
window._showEmojiToast?.('Favorites set as default tab');
|
||||
}
|
||||
};
|
||||
|
||||
// ── Favorites tab ──
|
||||
const favsTab = document.createElement('button');
|
||||
favsTab.className = 'ep-tab ep-tab-favs';
|
||||
@@ -1985,22 +2052,13 @@ if (window.__dmLoaded) {
|
||||
favsTab.innerHTML = '<i class="fa-solid fa-star" style="font-size:14px;color:var(--emoji-fav-color,#f5c518);"></i>';
|
||||
favsTab.addEventListener('mousedown', e => e.preventDefault());
|
||||
favsTab.addEventListener('click', () => showDmTab('__favs__', packs));
|
||||
addDmTabTouch(favsTab, () => showDmTab('__favs__', packs), () => toggleDmFavDefault(favsTab));
|
||||
// Right-click on fav tab → set/unset as default
|
||||
favsTab.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (navigator.maxTouchPoints > 0) return;
|
||||
const KEY = 'f0ck_emoji_default_tab';
|
||||
const isDefault = localStorage.getItem(KEY) === '__favs__';
|
||||
if (isDefault) {
|
||||
localStorage.removeItem(KEY);
|
||||
favsTab.classList.remove('ep-tab-default');
|
||||
window._showEmojiToast?.('Favorites unset as default tab');
|
||||
} else {
|
||||
localStorage.setItem(KEY, '__favs__');
|
||||
favsTab.classList.add('ep-tab-default');
|
||||
window._showEmojiToast?.('Favorites set as default tab');
|
||||
}
|
||||
if (Date.now() - lastDmHoldTime < 1000) return;
|
||||
toggleDmFavDefault(favsTab);
|
||||
});
|
||||
// Mark as default if already set
|
||||
if (localStorage.getItem('f0ck_emoji_default_tab') === '__favs__') {
|
||||
@@ -2028,6 +2086,7 @@ if (window.__dmLoaded) {
|
||||
}
|
||||
tab.addEventListener('mousedown', e => e.preventDefault());
|
||||
tab.addEventListener('click', () => showDmTab(pack.id ?? null, packs));
|
||||
addDmTabTouch(tab, () => showDmTab(pack.id ?? null, packs));
|
||||
dmTabBar.appendChild(tab);
|
||||
});
|
||||
|
||||
|
||||
@@ -1000,9 +1000,10 @@
|
||||
e.stopPropagation();
|
||||
const itemId = slide.dataset.id;
|
||||
try {
|
||||
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||
const resp = await fetch(`/api/v2/tags/${itemId}/${encodeURIComponent(tag)}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'x-csrf-token': window.scrollerCsrf || '' }
|
||||
headers: { ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) }
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) pill.remove();
|
||||
@@ -1157,10 +1158,14 @@
|
||||
if (nowFaved) flashFav(slide);
|
||||
}
|
||||
try {
|
||||
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||
const resp = await fetch('/api/v2/togglefav', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `postid=${id}`
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||
},
|
||||
body: `postid=${id}${csrfToken ? `&csrf_token=${encodeURIComponent(csrfToken)}` : ''}`
|
||||
});
|
||||
const data = await resp.json();
|
||||
// Sync count to server truth (handles race conditions)
|
||||
@@ -1582,9 +1587,10 @@
|
||||
e.stopPropagation();
|
||||
const itemId = slide.dataset.id;
|
||||
try {
|
||||
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||
const resp = await fetch(`/api/v2/tags/${itemId}/${encodeURIComponent(t)}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'x-csrf-token': window.scrollerCsrf || '' }
|
||||
headers: { ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) }
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) pill.remove();
|
||||
@@ -1850,11 +1856,12 @@
|
||||
else if (item.external_board === 'gif') rating = 'nsfw';
|
||||
|
||||
try {
|
||||
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||
const resp = await fetch('/api/v2/scroller/rehost', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'x-csrf-token': window.scrollerCsrf || ''
|
||||
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
url: item.external_media_url || item.dest,
|
||||
@@ -1966,9 +1973,10 @@
|
||||
showShareToast(info.toast);
|
||||
}
|
||||
|
||||
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||
fetch(`/api/v2/tags/${id}/cycle-rating`, {
|
||||
method: 'PUT',
|
||||
headers: { 'x-csrf-token': window.scrollerCsrf || '' }
|
||||
headers: { ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) }
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
@@ -2119,7 +2127,12 @@
|
||||
const contentEl = commentEl.querySelector('.comment-content');
|
||||
if (!contentEl) return;
|
||||
|
||||
const raw = (contentEl.dataset.raw || '').replace(/<br\s*\/?>/gi, '\n').trim();
|
||||
let raw = (contentEl.dataset.raw || '').replace(/<br\s*\/?>/gi, '\n').trim();
|
||||
if (raw.includes('>') || raw.includes('<') || raw.includes('&')) {
|
||||
const txt = document.createElement('textarea');
|
||||
txt.innerHTML = raw;
|
||||
raw = txt.value;
|
||||
}
|
||||
const lines = raw.split('\n');
|
||||
const quote = `>>${id}\n${lines.map(line => `>${line}`).join('\n')}\n`;
|
||||
|
||||
@@ -2787,11 +2800,16 @@
|
||||
if (!content || !commentsItemId || commentsPosting) return;
|
||||
commentsPosting = true; commentSendBtn.disabled = true;
|
||||
try {
|
||||
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||
let postBody = `item_id=${commentsItemId}&content=${encodeURIComponent(content)}`;
|
||||
if (replyToCommentId) postBody += `&parent_id=${replyToCommentId}`;
|
||||
if (csrfToken) postBody += `&csrf_token=${encodeURIComponent(csrfToken)}`;
|
||||
const resp = await fetch('/api/comments', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||
},
|
||||
body: postBody
|
||||
});
|
||||
const data = await resp.json();
|
||||
@@ -2899,10 +2917,14 @@
|
||||
if (addTagSendBtn) addTagSendBtn.disabled = true;
|
||||
closeSugg();
|
||||
try {
|
||||
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||
const resp = await fetch(`/api/v2/tags/${targetId}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `tagname=${encodeURIComponent(tag)}`
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||
},
|
||||
body: `tagname=${encodeURIComponent(tag)}${csrfToken ? `&csrf_token=${encodeURIComponent(csrfToken)}` : ''}`
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
@@ -3658,7 +3680,11 @@
|
||||
if (sMarkAll) {
|
||||
sMarkAll.addEventListener('click', async () => {
|
||||
try {
|
||||
await fetch('/api/notifications/read', { method: 'POST' });
|
||||
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||
await fetch('/api/notifications/read', {
|
||||
method: 'POST',
|
||||
headers: { ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) }
|
||||
});
|
||||
updateScrollerNotifBadge(0);
|
||||
sCachedNotifs = sCachedNotifs.map(n => ({ ...n, is_read: true }));
|
||||
updateScrollerTabBadges(sCachedNotifs);
|
||||
@@ -3673,7 +3699,12 @@
|
||||
if (!item) return;
|
||||
const nid = item.dataset.id;
|
||||
if (nid && item.classList.contains('unread')) {
|
||||
fetch(`/api/notifications/${nid}/read`, { method: 'POST', keepalive: true }).catch(() => {});
|
||||
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||
fetch(`/api/notifications/${nid}/read`, {
|
||||
method: 'POST',
|
||||
keepalive: true,
|
||||
headers: { ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) }
|
||||
}).catch(() => {});
|
||||
item.classList.remove('unread');
|
||||
// Update cache
|
||||
const cached = sCachedNotifs.find(n => String(n.id) === String(nid));
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
if (!unsafe) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = unsafe;
|
||||
return div.innerHTML;
|
||||
return div.innerHTML.replace(/"/g, '"').replace(/'/g, ''');
|
||||
};
|
||||
|
||||
const playSidebarEmojiVideos = (container) => {
|
||||
@@ -271,7 +271,7 @@
|
||||
const rendered = quoteEmojis
|
||||
? quoteContent.replace(/:([a-z0-9_]+):/g, (m, n) => renderEmoji(m, n))
|
||||
: quoteContent;
|
||||
return `<span class="greentext">>${rendered}</span>`;
|
||||
return `<span class="greentext">>${rendered.replace(/>/g, '>')}</span>`;
|
||||
}
|
||||
|
||||
// Per-line limit to prevent marked.parse recursion on single giant lines
|
||||
|
||||
@@ -306,7 +306,7 @@ if (!window.UserCommentSystem) {
|
||||
const trimmed = line.trimStart();
|
||||
if (trimmed.startsWith('>') && !trimmed.match(/^>>\d+/)) {
|
||||
const quoteContent = line.substring(line.indexOf('>') + 1);
|
||||
return `<span class="greentext">>${quoteContent}</span>`;
|
||||
return `<span class="greentext">>${quoteContent.replace(/>/g, '>')}</span>`;
|
||||
}
|
||||
|
||||
// Per-line limit
|
||||
@@ -450,7 +450,7 @@ if (!window.UserCommentSystem) {
|
||||
if (!unsafe) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = unsafe;
|
||||
return div.innerHTML;
|
||||
return div.innerHTML.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"meme": "Meme",
|
||||
"halls": "Hallen",
|
||||
"tags": "Tags",
|
||||
"chat": "Chat",
|
||||
"search": "Suchen",
|
||||
"random": "Zufall",
|
||||
"profile": "Profil",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"meme": "Meme",
|
||||
"halls": "Halls",
|
||||
"tags": "Tags",
|
||||
"chat": "Chat",
|
||||
"search": "Search",
|
||||
"random": "Random",
|
||||
"profile": "Profile",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"meme": "meme",
|
||||
"halls": "Hallen",
|
||||
"tags": "Tags",
|
||||
"chat": "Chat",
|
||||
"search": "Zoeken",
|
||||
"random": "Willekeurig",
|
||||
"profile": "profiel",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"meme": "Memen",
|
||||
"halls": "Hallen",
|
||||
"tags": "Etiketten",
|
||||
"chat": "Geschwafel",
|
||||
"search": "Suche",
|
||||
"random": "Zufall",
|
||||
"profile": "Profil",
|
||||
|
||||
@@ -25,7 +25,8 @@ export default router => {
|
||||
|
||||
group.post(/$/, lib.loggedin, async (req, res) => {
|
||||
// assign and/or create tag
|
||||
if (!req.params.postid || !req.post.tagname) {
|
||||
const rawTagname = req.post?.tagname || req.body?.tagname;
|
||||
if (!req.params.postid || !rawTagname) {
|
||||
return res.json({
|
||||
success: false,
|
||||
msg: 'missing postid or tag'
|
||||
@@ -33,7 +34,7 @@ export default router => {
|
||||
}
|
||||
|
||||
const postid = +req.params.postid;
|
||||
const tagname = req.post.tagname?.trim();
|
||||
const tagname = rawTagname.trim();
|
||||
const protectedTags = ['sfw', 'nsfw', 'nsfl'];
|
||||
|
||||
if (protectedTags.includes(tagname.toLowerCase())) {
|
||||
|
||||
@@ -47,6 +47,25 @@ function buildBgHostRegex() {
|
||||
|
||||
export default (router, tpl) => {
|
||||
|
||||
// GET /chat — Dedicated site chat page
|
||||
router.get('/chat', async (req, res) => {
|
||||
if (!cfg.websrv.enable_global_chat) {
|
||||
return res.reply({ code: 404, body: 'Not found' });
|
||||
}
|
||||
if (!req.session) {
|
||||
return res.redirect('/login');
|
||||
}
|
||||
if (req.session.banned) {
|
||||
return res.reply({ code: 403, body: 'Access denied: You are banned' });
|
||||
}
|
||||
return res.html(tpl.render('chat', {
|
||||
session: req.session,
|
||||
hidePagination: true,
|
||||
link: { main: '/chat', path: '/chat' },
|
||||
domain: cfg.main.url.domain
|
||||
}, req));
|
||||
});
|
||||
|
||||
// GET /api/chat — fetch recent messages
|
||||
router.get('/api/chat', async (req, res) => {
|
||||
if (!cfg.websrv.enable_global_chat) {
|
||||
|
||||
@@ -95,7 +95,7 @@ export default (router, tpl) => {
|
||||
if (req.headers['x-requested-with'] === 'XMLHttpRequest' || query.ajax) {
|
||||
return res.json({
|
||||
success: true,
|
||||
html: tpl.render('tag-cards', { toptags: tags, session: (req.session && req.session.user) ? { ...req.session } : false }, req),
|
||||
html: tpl.render('tag-cards', { toptags: tags, user: req.session?.user ? { user: req.session.user } : null, session: (req.session && req.session.user) ? { ...req.session } : false }, req),
|
||||
currentPage: page,
|
||||
hasMore: tags.length === TAGS_PER_PAGE
|
||||
});
|
||||
@@ -128,6 +128,7 @@ export default (router, tpl) => {
|
||||
phrase,
|
||||
tmp: null,
|
||||
hidePagination: true,
|
||||
user: req.session?.user ? { user: req.session.user } : null,
|
||||
session: (req.session && req.session.user) ? { ...req.session } : false,
|
||||
page_meta: {
|
||||
title: 'Tags',
|
||||
|
||||
9
views/chat.html
Normal file
9
views/chat.html
Normal file
@@ -0,0 +1,9 @@
|
||||
@include(snippets/header)
|
||||
<div class="pagewrapper">
|
||||
<div id="main" class="chat-page">
|
||||
<div id="site-chat-container" class="site-chat-container">
|
||||
<!-- Site chat widget is mounted here by globalchat.js -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@include(snippets/footer)
|
||||
@@ -8,7 +8,7 @@
|
||||
.meme-title { font-family: 'Impact', 'VCR', sans-serif; }
|
||||
|
||||
/* NUCLEAR VISIBILITY RESET: Override any and all parent constraints for this page */
|
||||
@media (max-width: 950px) {
|
||||
@media (max-width: 1250px) {
|
||||
/* Target all possible shell triggers */
|
||||
html, body, .pagewrapper, #main, .layout-modern react-wrapper, .layout-legacy react-wrapper {
|
||||
height: auto !important;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="{{ domain }}">
|
||||
<script>(function(){var h=document.documentElement;if(localStorage.getItem('hideItemRatings')==='true')h.classList.add('hide-item-ratings-active');if(localStorage.getItem('blurNsfw')==='true')h.classList.add('blur-nsfw-active');if(localStorage.getItem('blurNsfl')==='true')h.classList.add('blur-nsfl-active');if(localStorage.getItem('blurSfw')==='true')h.classList.add('blur-sfw-active');if(localStorage.getItem('blurUntagged')==='true')h.classList.add('blur-untagged-active');if(localStorage.getItem('blurDetail')!=='false')h.classList.add('blur-detail-active');if(localStorage.getItem('imageExpandOnClick')!=='false')h.classList.add('image-expand-active');})();</script>
|
||||
<style>
|
||||
html { background-color: #000; color: #fff; }
|
||||
@if(session && session.font)
|
||||
@@ -22,6 +23,7 @@
|
||||
}
|
||||
@endif
|
||||
:root {
|
||||
--favicon-url: url('@if(custom_favicon && custom_favicon.length > 0){{ custom_favicon }}@else/s/img/favicon.gif@endif');
|
||||
@if(session && session.font)
|
||||
--font: 'CustomUserFont', monospace !important;
|
||||
@elseif(typeof default_font !== 'undefined' && default_font && default_font.length > 0)
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
</div>
|
||||
@endif
|
||||
<a href="/tags" title="{{ t('nav.tags') }}"><i class="fa-solid fa-tags"></i></a>
|
||||
@if(enable_global_chat)
|
||||
<a href="/chat" title="{{ t('nav.chat') }}"><i class="fa-solid fa-comments"></i></a>
|
||||
@endif
|
||||
@if(abyss_enabled)
|
||||
<a href="/abyss" title="{{ t('nav.abyss') }}"><i class="fa-solid fa-dice-d6"></i></a>
|
||||
@endif
|
||||
|
||||
Reference in New Issue
Block a user