diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css
index 60c9d2a..e6b4588 100644
--- a/public/s/css/f0ckm.css
+++ b/public/s/css/f0ckm.css
@@ -10032,6 +10032,7 @@ html[theme="orange"] .image-brand {
.pagewrapper:has(.index-layout-wrapper),
.pagewrapper:has(.item-layout-container),
.pagewrapper:has(.meme-layout-wrapper),
+ .pagewrapper:has(.chat-page),
body.private-gate-active .pagewrapper {
padding-right: 0 !important;
}
@@ -14704,6 +14705,7 @@ body.layout-modern .xd-score-wrapper {
z-index: 950;
display: flex;
flex-direction: column;
+ justify-content: flex-end;
align-items: flex-end;
pointer-events: none;
font-family: var(--font, monospace);
@@ -14788,8 +14790,9 @@ body.scroller-active #gchat-reopen-bubble {
display: flex;
flex-direction: column;
width: 300px;
- height: 40dvh;
- max-height: calc(100dvh - var(--navbar-h, 50px));
+ height: 400px;
+ max-height: calc(100dvh - var(--navbar-h, 50px) - 15px);
+ min-height: 180px;
background: var(--bg, #1a1a1a);
border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.08));
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.65);
@@ -14974,22 +14977,12 @@ body.scroller-active #gchat-reopen-bubble {
white-space: nowrap;
}
-#gchat-topic {
- padding: 5px 10px;
- font-size: 0.78em;
- font-weight: 600;
- color: var(--bg, #111);
- background: var(--accent, #f2ef0b);
- border-bottom: 1px solid rgba(0, 0, 0, 0.15);
- word-break: break-word;
- white-space: pre-wrap;
- flex-shrink: 0;
- opacity: 0.95;
}
#gchat-messages {
- flex: 1;
- overflow-y: auto;
+ flex: 1 1 0%;
+ min-height: 0;
+ overflow-y: auto !important;
overflow-x: hidden;
padding: 8px;
display: flex;
@@ -15096,7 +15089,9 @@ body.scroller-active #gchat-reopen-bubble {
/* Emoji images inside chat bubbles */
.gchat-bubble .emoji {
- height: 10.4em;
+ height: 1.4em;
+ max-height: 28px;
+ width: auto;
vertical-align: middle;
display: inline-block;
}
@@ -15204,33 +15199,15 @@ body.scroller-active #gchat-reopen-bubble {
transform: scale(0.92);
}
-/* Emoji picker grid (above input area, inside panel) */
#gchat-emoji-picker {
display: none;
- grid-template-columns: repeat(6, 1fr);
- gap: 4px;
- padding: 8px;
- overflow-y: auto;
- max-height: 140px;
+ flex-direction: column;
+ max-height: 220px;
background: var(--nav-bg, #111);
border-top: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.08));
scrollbar-width: thin;
}
-#gchat-emoji-picker img {
- width: 36px;
- height: 36px;
- object-fit: contain;
- cursor: pointer;
- border-radius: 4px;
- padding: 2px;
- transition: background 0.1s;
-}
-
-#gchat-emoji-picker img:hover {
- background: rgba(255, 255, 255, 0.12);
-}
-
/* :emoji inline autocomplete — portaled to body */
#gchat-emoji-ac {
position: fixed;
@@ -15743,14 +15720,319 @@ body.scroller-active #gchat-reopen-bubble {
white-space: nowrap;
}
-.gchat-item-card-meta {
- font-size: 0.7rem;
- opacity: 0.5;
+/* ── Dedicated /chat Page ── */
+html:has(.chat-page),
+body:has(.chat-page) {
+ height: 100% !important;
+ overflow: hidden !important;
+}
+
+body:has(.chat-page) .pagewrapper {
+ height: calc(100dvh - var(--navbar-h, 60px)) !important;
+ min-height: 0 !important;
+ max-width: 100% !important;
+ width: 100% !important;
+ padding: 0 !important;
+ margin: 0 !important;
+ overflow: hidden !important;
+ display: flex !important;
+ flex-direction: column !important;
+ transition: padding-right 0.3s ease-in-out !important;
+}
+
+@media (min-width: 1000px) {
+ body:has(.chat-page):not(.sidebar-right-hidden) .pagewrapper {
+ padding-right: 300px !important;
+ }
+}
+
+body:has(.chat-page) footer,
+body:has(.chat-page) .footer {
+ display: none !important;
+}
+
+.chat-page {
+ width: 100%;
+ height: 100%;
+ max-width: 100%;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+.site-chat-container {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ width: 100%;
+ height: 100%;
+ position: relative;
+ border-radius: 0;
+ overflow: hidden;
+ border: none;
+ background: var(--bg, #1a1a1a);
+ box-shadow: none;
+}
+
+.gchat-page-panel {
+ position: relative !important;
+ inset: auto !important;
+ width: 100% !important;
+ height: 100% !important;
+ max-width: 100% !important;
+ min-width: 0 !important;
+ border: none !important;
+ border-radius: 0 !important;
+ box-shadow: none !important;
+ display: flex !important;
+ flex-direction: column !important;
+ transform: none !important;
+}
+
+.gchat-page-panel #gchat-resize-h,
+.gchat-page-panel #gchat-resize-w,
+.gchat-page-panel #gchat-resize-e,
+.gchat-page-panel .gchat-header-btns {
+ display: none !important;
+}
+
+/* Flex layout containers for chat body */
+.gchat-body-wrap {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
+
+#gchat-panel:not(.gchat-page-panel) .gchat-body-wrap {
+ flex-direction: column;
+}
+
+.gchat-page-panel .gchat-body-wrap {
+ flex-direction: row;
+}
+
+.gchat-main-area {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-width: 0;
+ min-height: 0;
+ height: 100%;
+}
+
+.gchat-page-panel #gchat-messages {
+ flex: 1;
+ min-height: 0;
+ padding: 14px;
+ overflow-y: auto;
+}
+
+.gchat-page-panel .gchat-msg {
+ max-width: 100% !important;
+}
+
+/* Right-side user list in page mode */
+.gchat-page-panel #gchat-online {
+ width: 250px;
+ min-width: 200px;
+ max-width: 300px;
+ border-left: 1px solid var(--border-color, rgba(255, 255, 255, 0.1));
+ background: rgba(0, 0, 0, 0.25);
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+ flex-shrink: 0;
+}
+
+.gchat-page-panel .gchat-online-header {
+ padding: 12px 14px 4px 14px;
+ font-size: 0.92rem;
+ font-weight: 700;
+ color: var(--accent, #f2ef0b);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.gchat-page-panel .gchat-online-subhead {
+ padding: 2px 14px 10px 14px;
+ font-size: 0.75rem;
+ opacity: 0.6;
+ border-bottom: 1px solid var(--border-color, rgba(255, 255, 255, 0.08));
+}
+
+.gchat-page-panel .gchat-online-list {
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
+ padding: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.gchat-page-panel .gchat-online-user-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 6px 10px;
+ border-radius: 6px;
+ text-decoration: none;
+ color: var(--fg, #e0e0e0);
+ transition: background 0.15s ease;
+}
+
+.gchat-page-panel .gchat-online-user-item:hover {
+ background: rgba(255, 255, 255, 0.08);
+}
+
+.gchat-page-panel .gchat-online-user-avatar {
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ object-fit: cover;
+ flex-shrink: 0;
+ border: 1px solid rgba(255, 255, 255, 0.15);
+}
+
+.gchat-page-panel .gchat-online-user-avatar.guest-avatar {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255, 255, 255, 0.08);
+ font-size: 0.8rem;
+ color: #aaa;
+}
+
+.gchat-page-panel .gchat-online-user-name {
+ font-size: 0.88rem;
+ font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
+@media (max-width: 768px) {
+ .gchat-page-panel .gchat-body-wrap {
+ flex-direction: column !important;
+ flex: 1 !important;
+ min-height: 0 !important;
+ height: 100% !important;
+ overflow: hidden !important;
+ }
+
+ .gchat-page-panel #gchat-online {
+ width: 100% !important;
+ max-width: 100% !important;
+ height: 40px !important;
+ min-height: 40px !important;
+ max-height: 40px !important;
+ border-left: none !important;
+ border-bottom: 1px solid var(--border-color, rgba(255, 255, 255, 0.12)) !important;
+ order: -1 !important;
+ flex-shrink: 0 !important;
+ display: flex !important;
+ flex-direction: row !important;
+ align-items: center !important;
+ padding: 0 10px !important;
+ overflow: hidden !important;
+ background: rgba(0, 0, 0, 0.3) !important;
+ }
+
+ .gchat-page-panel .gchat-online-header {
+ padding: 0 !important;
+ font-size: 0.8rem !important;
+ white-space: nowrap !important;
+ flex-shrink: 0 !important;
+ gap: 4px !important;
+ }
+
+ .gchat-page-panel .gchat-online-header span {
+ font-size: 0.78rem !important;
+ }
+
+ .gchat-page-panel .gchat-online-subhead {
+ display: none !important;
+ }
+
+ .gchat-page-panel .gchat-online-list {
+ flex: 1 !important;
+ min-width: 0 !important;
+ display: flex !important;
+ flex-direction: row !important;
+ flex-wrap: nowrap !important;
+ overflow-x: auto !important;
+ overflow-y: hidden !important;
+ gap: 6px !important;
+ padding: 0 4px !important;
+ align-items: center !important;
+ scrollbar-width: none !important;
+ -webkit-overflow-scrolling: touch !important;
+ touch-action: pan-x !important;
+ }
+
+ .gchat-page-panel .gchat-online-list::-webkit-scrollbar {
+ display: none !important;
+ }
+
+ .gchat-page-panel .gchat-online-user-item {
+ flex-shrink: 0 !important;
+ padding: 2px 8px !important;
+ gap: 6px !important;
+ border-radius: 12px !important;
+ background: rgba(255, 255, 255, 0.08) !important;
+ height: 28px !important;
+ align-items: center !important;
+ }
+
+ .gchat-page-panel .gchat-online-user-avatar {
+ width: 20px !important;
+ height: 20px !important;
+ }
+
+ .gchat-page-panel .gchat-online-user-name {
+ font-size: 0.75rem !important;
+ max-width: 110px !important;
+ }
+
+ .gchat-page-panel .gchat-main-area {
+ flex: 1 !important;
+ min-height: 0 !important;
+ height: auto !important;
+ display: flex !important;
+ flex-direction: column !important;
+ overflow: hidden !important;
+ }
+
+ .gchat-page-panel #gchat-messages {
+ flex: 1 !important;
+ min-height: 0 !important;
+ overflow-y: auto !important;
+ padding: 10px !important;
+ -webkit-overflow-scrolling: touch !important;
+ }
+
+ .gchat-page-panel .gchat-msg {
+ max-width: 92% !important;
+ }
+
+ .gchat-page-panel #gchat-input-area {
+ flex-shrink: 0 !important;
+ background: var(--nav-bg, #0d0d0d) !important;
+ padding-bottom: env(safe-area-inset-bottom, 0px) !important;
+ }
+}
+
/* =============================================
SETTINGS PAGE – MOBILE OVERFLOW FIXES
Prevents long i18n strings (Deutsch / Zange)
diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js
index 2cd2d53..6c15c7b 100644
--- a/public/s/js/f0ckm.js
+++ b/public/s/js/f0ckm.js
@@ -2438,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\//) && (
@@ -2446,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;
@@ -4079,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();
@@ -4533,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 });
diff --git a/public/s/js/globalchat.js b/public/s/js/globalchat.js
index ac55bd9..f07874e 100644
--- a/public/s/js/globalchat.js
+++ b/public/s/js/globalchat.js
@@ -15,7 +15,7 @@
const MAX_VISIBLE_MSGS = 100;
const RATE_LIMIT_MS = 800;
- let isMinimized = localStorage.getItem('f0ck_chat_minimized') !== '0';
+ let isMinimized = localStorage.getItem('f0ck_chat_minimized') === '1';
let isClosed = localStorage.getItem('f0ck_chat_closed') === '1';
let lastSent = 0;
let customEmojis = null; // name → url
@@ -77,10 +77,18 @@
// ── Build widget ─────────────────────────────────────────────────────────
function buildWidget() {
- const widget = document.createElement('div');
- widget.id = 'gchat-widget';
+ const isPageMode = (window.location.pathname === '/chat');
+ const containerEl = isPageMode ? document.getElementById('site-chat-container') : null;
+
+ if (isPageMode && !containerEl) return;
+
+ const widget = isPageMode ? containerEl : (document.getElementById('gchat-widget') || document.createElement('div'));
+ if (!isPageMode) widget.id = 'gchat-widget';
+
+ const panelClass = isPageMode ? 'gchat-page-panel' : (isMinimized ? 'gchat-minimized' : '');
+
widget.innerHTML = `
-
-
`;
- document.body.appendChild(widget);
+ if (!isPageMode && !document.getElementById('gchat-widget')) {
+ document.body.appendChild(widget);
+ }
}
// ── Helpers ───────────────────────────────────────────────────────────────
@@ -651,73 +658,486 @@
}
}
- // ── Emoji picker ──────────────────────────────────────────────────────────
+ // ── Tabbed Emoji & Sticker Picker ──────────────────────────────────────────
function setupEmojiPicker(textarea) {
- const btn = document.getElementById('gchat-emoji-btn');
- if (!btn) return;
+ if (!window._emojiPickerFavUtils) {
+ window._emojiPickerFavUtils = {
+ KEY: 'f0ck_emoji_favs',
+ get() { try { return JSON.parse(localStorage.getItem(this.KEY) || '[]'); } catch(e) { return []; } },
+ has(name) { return this.get().some(f => f.name === name); },
+ toggle(name, url) {
+ const favs = this.get();
+ const idx = favs.findIndex(f => f.name === name);
+ const action = idx >= 0 ? 'remove' : 'add';
+ if (idx >= 0) favs.splice(idx, 1); else favs.push({ name, url });
+ localStorage.setItem(this.KEY, JSON.stringify(favs));
+ const csrf = window.f0ckSession?.csrf_token;
+ if (csrf) {
+ fetch('/api/v2/user/emoji-favorites/toggle', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf, 'X-Requested-With': 'XMLHttpRequest' },
+ body: JSON.stringify({ name, url, action })
+ }).catch(() => {});
+ }
+ },
+ syncFromDB() {
+ fetch('/api/v2/user/emoji-favorites', { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
+ .then(r => r.ok ? r.json() : null)
+ .then(data => {
+ if (data?.success && Array.isArray(data.favorites)) {
+ localStorage.setItem(this.KEY, JSON.stringify(data.favorites));
+ window._emojiPickerRefresh?.();
+ }
+ }).catch(() => {});
+ }
+ };
+ if (window.f0ckSession?.logged_in) window._emojiPickerFavUtils.syncFromDB();
+ }
+
+ if (!window.stickerPreview) {
+ const toastEl = document.createElement('div');
+ toastEl.className = 'emoji-fav-toast';
+ document.body.appendChild(toastEl);
+ let _toastTimer = null;
+ const showToast = (msg) => {
+ clearTimeout(_toastTimer);
+ toastEl.textContent = msg;
+ toastEl.classList.remove('toast-out');
+ toastEl.classList.add('toast-in');
+ _toastTimer = setTimeout(() => {
+ toastEl.classList.replace('toast-in', 'toast-out');
+ }, 2000);
+ };
+
+ const overlay = document.createElement('div');
+ overlay.className = 'sticker-preview-overlay';
+ document.body.appendChild(overlay);
+ let _hideTimer = null;
+ window.stickerPreview = {
+ isShowing: false,
+ _autoHideTimer: null,
+ show(src, isVideo, name, url) {
+ clearTimeout(this._autoHideTimer);
+ clearTimeout(_hideTimer);
+ overlay.innerHTML = '';
+ const el = isVideo ? document.createElement('video') : document.createElement('img');
+ el.src = src;
+ if (isVideo) { el.autoplay = true; el.loop = true; el.muted = true; el.playsInline = true; }
+ overlay.appendChild(el);
+ if (name && url && window._emojiPickerFavUtils) {
+ const isFav = window._emojiPickerFavUtils.has(name);
+ const favBtn = document.createElement('button');
+ favBtn.className = 'sticker-preview-fav-btn' + (isFav ? ' is-fav' : '');
+ favBtn.innerHTML = '';
+ favBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ window._emojiPickerFavUtils.toggle(name, url);
+ const nowFav = window._emojiPickerFavUtils.has(name);
+ window.stickerPreview.hide();
+ showToast(nowFav ? 'Emoji added to Favorites' : 'Emoji removed from Favorites');
+ window._emojiPickerRefresh?.();
+ });
+ favBtn.addEventListener('touchend', (e) => { e.preventDefault(); e.stopPropagation(); favBtn.click(); });
+ overlay.appendChild(favBtn);
+ }
+ overlay.classList.add('visible');
+ this.isShowing = true;
+ if (isVideo) el.play().catch(() => {});
+ },
+ hide() {
+ clearTimeout(this._autoHideTimer);
+ this._autoHideTimer = null;
+ this.isShowing = false;
+ overlay.classList.remove('visible');
+ _hideTimer = setTimeout(() => { overlay.innerHTML = ''; }, 200);
+ }
+ };
+ window._showEmojiToast = showToast;
+ overlay.addEventListener('click', () => window.stickerPreview.hide());
+ document.addEventListener('mouseup', () => {
+ if (window.stickerPreview?.isShowing && !window.stickerPreview._autoHideTimer) {
+ window.stickerPreview.hide();
+ }
+ });
+ }
+
+ if (!window._favContextMenu) {
+ const menu = document.createElement('div');
+ menu.style.cssText = 'position:fixed;z-index:99999;background:var(--dropdown-bg,#222);border:1px solid var(--nav-border-color,#444);border-radius:7px;padding:4px 0;box-shadow:0 4px 20px rgba(0,0,0,0.5);display:none;min-width:180px;';
+ document.body.appendChild(menu);
+ window._favContextMenu = {
+ el: menu,
+ show(x, y, name, url, onToggle) {
+ const isFav = window._emojiPickerFavUtils.has(name);
+ menu.innerHTML = '';
+ const item = document.createElement('div');
+ item.style.cssText = 'padding:9px 16px;cursor:pointer;font-size:0.875em;color:var(--white,#fff);display:flex;align-items:center;gap:8px;border-radius:4px;margin:2px 4px;';
+ item.innerHTML = '' + (isFav ? '★' : '☆') + '' + (isFav ? 'Remove from Favorites' : 'Add to Favorites');
+ item.addEventListener('mouseenter', () => { item.style.background = 'rgba(255,255,255,0.09)'; });
+ item.addEventListener('mouseleave', () => { item.style.background = ''; });
+ item.addEventListener('click', (e) => { e.stopPropagation(); window._emojiPickerFavUtils.toggle(name, url); menu.style.display = 'none'; if (onToggle) onToggle(); });
+ menu.appendChild(item);
+ menu.style.left = x + 'px'; menu.style.top = y + 'px'; menu.style.display = 'block';
+ requestAnimationFrame(() => {
+ const r = menu.getBoundingClientRect();
+ if (r.right > window.innerWidth) menu.style.left = (x - r.width) + 'px';
+ if (r.bottom > window.innerHeight) menu.style.top = (y - r.height) + 'px';
+ });
+ },
+ hide() { menu.style.display = 'none'; }
+ };
+ document.addEventListener('click', () => window._favContextMenu.hide());
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') window._favContextMenu.hide(); });
+ }
let picker = null;
let closeHandler = null;
+ let activeTabId = null;
+ let tabBar = null;
+ let gridArea = null;
+ let currentPacks = [];
- const buildPicker = () => {
- if (picker) { picker.remove(); picker = null; }
- picker = document.createElement('div');
- picker.id = 'gchat-emoji-picker';
+ function insertEmoji(name) {
+ const input = document.getElementById('gchat-input') || textarea;
+ if (!input) return;
+ const pos = input.selectionStart ?? input.value.length;
+ const val = input.value;
+ input.value = val.slice(0, pos) + `:${name}:` + val.slice(pos);
+ const newPos = pos + name.length + 2;
+ input.setSelectionRange(newPos, newPos);
+ input.focus();
+ if (picker) picker.style.display = 'none';
+ if (closeHandler) { document.removeEventListener('click', closeHandler); closeHandler = null; }
+ }
- if (customEmojis && Object.keys(customEmojis).length > 0) {
- Object.keys(customEmojis).forEach(name => {
- const img = document.createElement('img');
- img.src = customEmojis[name];
- img.title = `:${name}:`;
- img.loading = 'lazy';
- img.onerror = () => { img.style.display = 'none'; };
- img.addEventListener('click', (e) => {
- e.stopPropagation();
- const pos = textarea.selectionStart ?? textarea.value.length;
- const v = textarea.value;
- textarea.value = v.slice(0, pos) + `:${name}:` + v.slice(pos);
- const newPos = pos + name.length + 2;
- textarea.setSelectionRange(newPos, newPos);
- textarea.focus();
- picker.style.display = 'none';
- if (closeHandler) { document.removeEventListener('click', closeHandler); closeHandler = null; }
- });
- picker.appendChild(img);
- });
+ function makeChatEmojiEl(url, name) {
+ const isVideo = url && url.endsWith('.webm');
+ let media;
+ if (isVideo) {
+ media = document.createElement('video');
+ media.src = url;
+ media.autoplay = true;
+ media.loop = true;
+ media.muted = true;
+ media.playsInline = true;
} else {
- picker.innerHTML = 'No custom emojis
';
+ media = document.createElement('img');
+ media.src = url;
+ media.loading = 'lazy';
+ }
+ media.title = `:${name}:`;
+ media.onerror = () => { media.style.display = 'none'; };
+
+ const el = document.createElement('div');
+ el.className = 'ep-sticker-wrap';
+ el.appendChild(media);
+
+ if (window._emojiPickerFavUtils?.has(name)) {
+ const badge = document.createElement('i');
+ badge.className = 'fa-solid fa-star ep-fav-badge';
+ el.appendChild(badge);
}
- // Position above input area
- const panel = document.getElementById('gchat-panel');
- panel.appendChild(picker);
- };
+ let holdTimer = null;
+ let holdFired = false;
+ let isTouchHold = false;
+ const startHold = (fromTouch) => {
+ holdFired = false;
+ isTouchHold = !!fromTouch;
+ holdTimer = setTimeout(() => {
+ holdFired = true;
+ window.stickerPreview?.show(url, isVideo, name, url);
+ }, 800);
+ };
+ const cancelHold = () => { clearTimeout(holdTimer); isTouchHold = false; };
+ const endHold = () => {
+ clearTimeout(holdTimer);
+ if (holdFired) {
+ holdFired = false;
+ el.addEventListener('click', (e) => {
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ }, { once: true, capture: true });
+ if (!isTouchHold) window.stickerPreview?.hide();
+ } else {
+ window.stickerPreview?.hide();
+ }
+ isTouchHold = false;
+ };
- btn.addEventListener('click', (e) => {
- e.preventDefault();
- if (!picker) buildPicker();
+ media.addEventListener('mousedown', () => startHold(false));
+ media.addEventListener('mouseup', endHold);
+ media.addEventListener('mouseleave', cancelHold);
+ media.addEventListener('mouseenter', () => {
+ if (window.stickerPreview?.isShowing) window.stickerPreview.show(url, isVideo, name, url);
+ });
+ media.addEventListener('touchstart', () => startHold(true), { passive: true });
+ media.addEventListener('touchmove', cancelHold, { passive: true });
+ media.addEventListener('touchend', endHold);
+ media.addEventListener('touchcancel', cancelHold);
+ media.addEventListener('contextmenu', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (navigator.maxTouchPoints > 0) return;
+ if (holdFired) return;
+ window._favContextMenu?.show(e.clientX, e.clientY, name, url, () => {
+ if (activeTabId === '__favs__') showTab('__favs__', currentPacks);
+ });
+ });
+ return el;
+ }
- const isVisible = picker.style.display !== 'none' && picker.style.display !== '';
- if (isVisible) {
+ function showTab(packId, packs) {
+ activeTabId = packId;
+ window._emojiPickerRefresh = () => showTab(activeTabId, currentPacks);
+ if (tabBar) {
+ tabBar.querySelectorAll('.ep-tab').forEach(t =>
+ t.classList.toggle('active', t.dataset.packId === String(packId ?? 'null'))
+ );
+ }
+ if (!gridArea) return;
+ gridArea.innerHTML = '';
+
+ if (packId === '__favs__') {
+ const favs = window._emojiPickerFavUtils?.get() || [];
+ if (!favs.length) {
+ gridArea.innerHTML = 'No favorites yet. Right-click or hold any sticker to add it here.
';
+ return;
+ }
+ favs.forEach(({ name, url }) => {
+ const el = makeChatEmojiEl(url, name);
+ el.addEventListener('click', (ev) => {
+ ev.stopPropagation();
+ insertEmoji(name);
+ });
+ gridArea.appendChild(el);
+ });
+ return;
+ }
+
+ const pack = (packs || []).find(p => String(p.id ?? null) === String(packId ?? null)) || packs?.[0];
+ if (!pack) return;
+ pack.emojis.forEach(({ name, url }) => {
+ const el = makeChatEmojiEl(url, name);
+ el.addEventListener('click', (ev) => {
+ ev.stopPropagation();
+ insertEmoji(name);
+ });
+ gridArea.appendChild(el);
+ });
+ gridArea.querySelectorAll('video').forEach(v =>
+ v.play().catch(() => v.addEventListener('canplay', () => v.play().catch(() => {}), { once: true }))
+ );
+ }
+
+ function buildPicker(emojis, packs) {
+ const inputArea = document.getElementById('gchat-input-area');
+ if (!inputArea) return;
+
+ if (!picker) {
+ picker = document.createElement('div');
+ picker.id = 'gchat-emoji-picker';
+ picker.className = 'emoji-picker gchat-emoji-picker';
picker.style.display = 'none';
- if (closeHandler) { document.removeEventListener('click', closeHandler); closeHandler = null; }
- } else {
- picker.style.display = 'grid';
- closeHandler = (ev) => {
- if (!picker.contains(ev.target) && ev.target !== btn) {
- picker.style.display = 'none';
- document.removeEventListener('click', closeHandler);
- closeHandler = null;
+ inputArea.insertBefore(picker, inputArea.firstChild);
+ }
+
+ picker.innerHTML = '';
+ currentPacks = packs || [];
+
+ if (!currentPacks || !currentPacks.length) {
+ picker.classList.remove('has-tabs');
+ Object.entries(emojis || {}).forEach(([name, url]) => {
+ const el = makeChatEmojiEl(url, name);
+ el.addEventListener('click', (ev) => {
+ ev.stopPropagation();
+ insertEmoji(name);
+ });
+ picker.appendChild(el);
+ });
+ return;
+ }
+
+ picker.classList.add('has-tabs');
+ tabBar = document.createElement('div');
+ tabBar.className = 'emoji-picker-tabs';
+
+ gridArea = document.createElement('div');
+ gridArea.className = 'emoji-picker-grid';
+
+ let lastHoldTime = 0;
+ const addTabTouch = (tabBtn, cb, onHold) => {
+ let ts = null, holdTimer = null, holdFired = false;
+ tabBtn.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);
}
- };
- setTimeout(() => document.addEventListener('click', closeHandler), 0);
+ e.stopPropagation();
+ }, { passive: true });
+
+ tabBtn.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 });
+
+ tabBtn.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(); }
+ });
+ };
+
+ 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');
+ }
+ };
+
+ // Favs Tab
+ const favTab = document.createElement('button');
+ favTab.className = 'ep-tab ep-tab-favs';
+ favTab.dataset.packId = '__favs__';
+ favTab.title = 'Favorites';
+ favTab.type = 'button';
+ favTab.innerHTML = '';
+ favTab.addEventListener('mousedown', e => e.preventDefault());
+ favTab.addEventListener('click', () => showTab('__favs__', currentPacks));
+ addTabTouch(favTab, () => showTab('__favs__', currentPacks), () => toggleFavDefault(favTab));
+ favTab.addEventListener('contextmenu', (e) => {
+ e.preventDefault(); e.stopPropagation();
+ if (Date.now() - lastHoldTime < 1000) return;
+ toggleFavDefault(favTab);
+ });
+ if (localStorage.getItem('f0ck_emoji_default_tab') === '__favs__') {
+ favTab.classList.add('ep-tab-default');
+ }
+ tabBar.appendChild(favTab);
+
+ // Pack Tabs
+ currentPacks.forEach((pack) => {
+ const tab = document.createElement('button');
+ tab.className = 'ep-tab';
+ tab.dataset.packId = String(pack.id ?? null);
+ tab.title = pack.name || 'Emojis';
+ tab.type = 'button';
+ if (pack.emojis && pack.emojis.length > 0) {
+ const iconUrl = pack.thumb_url || (pack.emojis.find(e => !e.url.endsWith('.webm')) || pack.emojis[0]).url;
+ const icon = document.createElement('img');
+ icon.src = iconUrl;
+ icon.alt = pack.name;
+ icon.loading = 'lazy';
+ tab.appendChild(icon);
+ } else {
+ tab.textContent = '☺';
+ }
+ tab.addEventListener('mousedown', e => e.preventDefault());
+ tab.addEventListener('click', () => showTab(pack.id ?? null, currentPacks));
+ addTabTouch(tab, () => showTab(pack.id ?? null, currentPacks));
+ tabBar.appendChild(tab);
+ });
+
+ picker.appendChild(tabBar);
+ picker.appendChild(gridArea);
+
+ const defaultTab = localStorage.getItem('f0ck_emoji_default_tab');
+ showTab(defaultTab === '__favs__' ? '__favs__' : (currentPacks[0]?.id ?? null), currentPacks);
+ }
+
+ async function populatePicker() {
+ const cachedPacks = (typeof CommentSystem !== 'undefined' && CommentSystem.emojiPacks) || null;
+ const cachedEmojis = (typeof CommentSystem !== 'undefined' && CommentSystem.emojiCache) || null;
+
+ if (cachedEmojis && Object.keys(cachedEmojis).length && cachedPacks && cachedPacks.length > 0) {
+ buildPicker(cachedEmojis, cachedPacks);
+ return;
+ }
+
+ try {
+ const res = await fetch('/api/v2/emojis');
+ const data = await res.json();
+ if (data.success && data.emojis?.length) {
+ const map = {};
+ data.emojis.forEach(e => { map[e.name] = e.url; });
+ if (typeof CommentSystem !== 'undefined') {
+ CommentSystem.emojiCache = map;
+ CommentSystem.emojiPacks = data.packs || [];
+ }
+ customEmojis = map;
+ buildPicker(map, data.packs || []);
+ }
+ } catch (e) {
+ console.warn('[Chat] Failed to load emoji packs:', e);
+ }
+ }
+
+ // Toggle via button click
+ document.addEventListener('click', (e) => {
+ const btn = e.target.closest('#gchat-emoji-btn');
+ if (btn) {
+ e.preventDefault();
+ const inputArea = document.getElementById('gchat-input-area');
+ if (!inputArea) return;
+ picker = document.getElementById('gchat-emoji-picker');
+ if (!picker) {
+ populatePicker().then(() => {
+ picker = document.getElementById('gchat-emoji-picker');
+ if (picker) {
+ picker.style.display = 'flex';
+ closeHandler = (ev) => {
+ if (!picker.contains(ev.target) && !ev.target.closest('#gchat-emoji-btn')) {
+ picker.style.display = 'none';
+ document.removeEventListener('click', closeHandler);
+ closeHandler = null;
+ }
+ };
+ setTimeout(() => document.addEventListener('click', closeHandler), 0);
+ }
+ });
+ return;
+ }
+ const isHidden = picker.style.display === 'none' || picker.style.display === '';
+ if (isHidden) {
+ populatePicker();
+ picker.style.display = 'flex';
+ closeHandler = (ev) => {
+ if (!picker.contains(ev.target) && !ev.target.closest('#gchat-emoji-btn')) {
+ picker.style.display = 'none';
+ document.removeEventListener('click', closeHandler);
+ closeHandler = null;
+ }
+ };
+ setTimeout(() => document.addEventListener('click', closeHandler), 0);
+ } else {
+ picker.style.display = 'none';
+ if (closeHandler) { document.removeEventListener('click', closeHandler); closeHandler = null; }
+ }
}
});
- // Rebuild picker if emojis load after init
window.addEventListener('f0ck:emojis_ready', (e) => {
customEmojis = e.detail;
- if (picker) { picker.remove(); picker = null; } // force rebuild on next open
+ if (picker) { picker.remove(); picker = null; }
});
}
@@ -986,30 +1406,35 @@
buildWidget();
// Gate chat on SSE connection — block input until stream is live
- const _panel = document.getElementById('gchat-panel');
- const _inputArea = document.getElementById('gchat-input-area');
- const _messages = document.getElementById('gchat-messages');
let _sseReady = false;
function setConnecting(connecting) {
- if (_inputArea) _inputArea.style.opacity = connecting ? '0.35' : '1';
- if (_inputArea) _inputArea.style.pointerEvents = connecting ? 'none' : '';
- if (_messages) _messages.style.opacity = connecting ? '0.35' : '1';
- const sendBtn = document.getElementById('gchat-send-btn');
- if (sendBtn) sendBtn.disabled = connecting;
+ const inputArea = document.getElementById('gchat-input-area');
+ const messages = document.getElementById('gchat-messages');
+ const sendBtn = document.getElementById('gchat-send-btn');
+ if (inputArea) {
+ inputArea.style.opacity = connecting ? '0.35' : '1';
+ inputArea.style.pointerEvents = connecting ? 'none' : 'auto';
+ }
+ if (messages) messages.style.opacity = connecting ? '0.35' : '1';
+ if (sendBtn) sendBtn.disabled = connecting;
+ }
+
+ // Failsafe check if SSE is already active on init
+ if (document.documentElement.dataset.sseReady === '1') {
+ _sseReady = true;
+ setConnecting(false);
+ } else {
+ setConnecting(true);
}
- setConnecting(true);
const onSseReady = () => {
- if (_sseReady) return;
_sseReady = true;
clearTimeout(_sseUnlockTimer);
- clearInterval(_pollFallback);
+ if (_pollFallback) clearInterval(_pollFallback);
setConnecting(false);
};
document.addEventListener('f0ck:sse_ready', onSseReady);
- // Failsafe: if SSE already fired before this ran (e.g. fast reconnect), check flag
- if (document.documentElement.dataset.sseReady === '1') onSseReady();
// Absolute fallback: if SSE hasn't connected within 10s, unlock anyway and poll
let _pollFallback = null;
@@ -1040,6 +1465,9 @@
+ const isPageMode = (window.location.pathname === '/chat');
+ if (isPageMode) isMinimized = false;
+
// Returns min top position for float panel (below navbar + MOTD)
function getTopBound() {
const nav = document.querySelector('nav.navbar');
@@ -1051,142 +1479,160 @@
return Math.max(0, bound);
}
- // Restore saved height
- const panel = document.getElementById('gchat-panel');
- const savedH = localStorage.getItem('f0ck_chat_height');
- if (savedH) panel.style.height = savedH + 'px';
+ function getMaxPanelHeight() {
+ const topB = typeof getTopBound === 'function' ? getTopBound() : 50;
+ return Math.max(180, window.innerHeight - topB - 15);
+ }
- // Restore saved width
- const savedW = localStorage.getItem('f0ck_chat_width');
- if (savedW) panel.style.width = savedW + 'px';
+ const panel = document.getElementById('gchat-panel');
+
+ if (!isPageMode && panel) {
+ // Restore saved height
+ const savedH = localStorage.getItem('f0ck_chat_height');
+ if (savedH) {
+ const parsedH = parseInt(savedH, 10);
+ const maxH = getMaxPanelHeight();
+ panel.style.height = (parsedH ? Math.min(parsedH, maxH) : 400) + 'px';
+ }
+
+ // Restore saved width
+ const savedW = localStorage.getItem('f0ck_chat_width');
+ if (savedW) panel.style.width = savedW + 'px';
+ }
// Drag-to-resize height via top invisible strip
const resizeH = document.getElementById('gchat-resize-h');
- function startResizeH(startY, startH, startTop) {
- const onMove = (clientY) => {
- const delta = startY - clientY;
- const maxH = window.innerHeight * 0.95;
- const newH = Math.min(Math.max(startH + delta, 180), maxH);
- panel.style.height = newH + 'px';
- if (isFloating) {
- const newTop = startTop - (newH - startH);
- panel.style.top = Math.max(getTopBound(), newTop) + 'px';
- }
- };
- const onEnd = () => {
- const h = parseInt(panel.style.height);
- if (h) localStorage.setItem('f0ck_chat_height', h);
- if (isFloating) localStorage.setItem('f0ck_chat_float_y', parseInt(panel.style.top));
- document.removeEventListener('mousemove', mouseMove);
- document.removeEventListener('mouseup', mouseEnd);
- document.removeEventListener('touchmove', touchMove);
- document.removeEventListener('touchend', touchEnd);
- };
- const mouseMove = mv => onMove(mv.clientY);
- const mouseEnd = onEnd;
- const touchMove = mv => { mv.preventDefault(); onMove(mv.touches[0].clientY); };
- const touchEnd = onEnd;
- document.addEventListener('mousemove', mouseMove);
- document.addEventListener('mouseup', mouseEnd);
- document.addEventListener('touchmove', touchMove, { passive: false });
- document.addEventListener('touchend', touchEnd);
+ if (resizeH && !isPageMode) {
+ function startResizeH(startY, startH, startTop) {
+ const onMove = (clientY) => {
+ const delta = startY - clientY;
+ const maxH = isFloating ? (window.innerHeight * 0.95) : getMaxPanelHeight();
+ const newH = Math.min(Math.max(startH + delta, 180), maxH);
+ panel.style.height = newH + 'px';
+ if (isFloating) {
+ const newTop = startTop - (newH - startH);
+ panel.style.top = Math.max(getTopBound(), newTop) + 'px';
+ }
+ };
+ const onEnd = () => {
+ const h = parseInt(panel.style.height);
+ if (h) localStorage.setItem('f0ck_chat_height', h);
+ if (isFloating) localStorage.setItem('f0ck_chat_float_y', parseInt(panel.style.top));
+ document.removeEventListener('mousemove', mouseMove);
+ document.removeEventListener('mouseup', mouseEnd);
+ document.removeEventListener('touchmove', touchMove);
+ document.removeEventListener('touchend', touchEnd);
+ };
+ const mouseMove = mv => onMove(mv.clientY);
+ const mouseEnd = onEnd;
+ const touchMove = mv => { mv.preventDefault(); onMove(mv.touches[0].clientY); };
+ const touchEnd = onEnd;
+ document.addEventListener('mousemove', mouseMove);
+ document.addEventListener('mouseup', mouseEnd);
+ document.addEventListener('touchmove', touchMove, { passive: false });
+ document.addEventListener('touchend', touchEnd);
+ }
+ resizeH.addEventListener('mousedown', (e) => {
+ if (e.button !== 0) return;
+ if (isMinimized) return;
+ e.preventDefault();
+ startResizeH(e.clientY, panel.getBoundingClientRect().height, panel.getBoundingClientRect().top);
+ });
+ resizeH.addEventListener('touchstart', (e) => {
+ if (isMinimized) return;
+ e.preventDefault();
+ startResizeH(e.touches[0].clientY, panel.getBoundingClientRect().height, panel.getBoundingClientRect().top);
+ }, { passive: false });
}
- resizeH.addEventListener('mousedown', (e) => {
- if (e.button !== 0) return;
- if (isMinimized) return;
- e.preventDefault();
- startResizeH(e.clientY, panel.getBoundingClientRect().height, panel.getBoundingClientRect().top);
- });
- resizeH.addEventListener('touchstart', (e) => {
- if (isMinimized) return;
- e.preventDefault();
- startResizeH(e.touches[0].clientY, panel.getBoundingClientRect().height, panel.getBoundingClientRect().top);
- }, { passive: false });
-
// Drag-to-resize width from the left handle
const resizeW = document.getElementById('gchat-resize-w');
- function startResizeW(startX, startW, startLeft) {
- const onMove = (clientX) => {
- const delta = startX - clientX;
- const newW = Math.min(Math.max(startW + delta, 200), window.innerWidth * 0.6);
- panel.style.width = newW + 'px';
- if (isFloating) {
- const newLeft = startLeft - (newW - startW);
- panel.style.left = Math.max(0, newLeft) + 'px';
- }
- };
- const onEnd = () => {
- const w = parseInt(panel.style.width);
- if (w) localStorage.setItem('f0ck_chat_width', w);
- if (isFloating) localStorage.setItem('f0ck_chat_float_x', parseInt(panel.style.left));
- document.removeEventListener('mousemove', mouseMove);
- document.removeEventListener('mouseup', mouseEnd);
- document.removeEventListener('touchmove', touchMove);
- document.removeEventListener('touchend', touchEnd);
- };
- const mouseMove = mv => onMove(mv.clientX);
- const mouseEnd = onEnd;
- const touchMove = mv => { mv.preventDefault(); onMove(mv.touches[0].clientX); };
- const touchEnd = onEnd;
- document.addEventListener('mousemove', mouseMove);
- document.addEventListener('mouseup', mouseEnd);
- document.addEventListener('touchmove', touchMove, { passive: false });
- document.addEventListener('touchend', touchEnd);
+ if (resizeW && !isPageMode) {
+ function startResizeW(startX, startW, startLeft) {
+ const onMove = (clientX) => {
+ const delta = startX - clientX;
+ const newW = Math.min(Math.max(startW + delta, 200), window.innerWidth * 0.6);
+ panel.style.width = newW + 'px';
+ if (isFloating) {
+ const newLeft = startLeft - (newW - startW);
+ panel.style.left = Math.max(0, newLeft) + 'px';
+ }
+ };
+ const onEnd = () => {
+ const w = parseInt(panel.style.width);
+ if (w) localStorage.setItem('f0ck_chat_width', w);
+ if (isFloating) localStorage.setItem('f0ck_chat_float_x', parseInt(panel.style.left));
+ document.removeEventListener('mousemove', mouseMove);
+ document.removeEventListener('mouseup', mouseEnd);
+ document.removeEventListener('touchmove', touchMove);
+ document.removeEventListener('touchend', touchEnd);
+ };
+ const mouseMove = mv => onMove(mv.clientX);
+ const mouseEnd = onEnd;
+ const touchMove = mv => { mv.preventDefault(); onMove(mv.touches[0].clientX); };
+ const touchEnd = onEnd;
+ document.addEventListener('mousemove', mouseMove);
+ document.addEventListener('mouseup', mouseEnd);
+ document.addEventListener('touchmove', touchMove, { passive: false });
+ document.addEventListener('touchend', touchEnd);
+ }
+ resizeW.addEventListener('mousedown', (e) => {
+ if (e.button !== 0) return;
+ e.preventDefault();
+ const r = panel.getBoundingClientRect();
+ startResizeW(e.clientX, r.width, r.left);
+ });
+ resizeW.addEventListener('touchstart', (e) => {
+ e.preventDefault();
+ const r = panel.getBoundingClientRect();
+ startResizeW(e.touches[0].clientX, r.width, r.left);
+ }, { passive: false });
}
- resizeW.addEventListener('mousedown', (e) => {
- if (e.button !== 0) return;
- e.preventDefault();
- const r = panel.getBoundingClientRect();
- startResizeW(e.clientX, r.width, r.left);
- });
- resizeW.addEventListener('touchstart', (e) => {
- e.preventDefault();
- const r = panel.getBoundingClientRect();
- startResizeW(e.touches[0].clientX, r.width, r.left);
- }, { passive: false });
// Drag-to-resize width from the RIGHT handle (left edge stays fixed)
const resizeE = document.getElementById('gchat-resize-e');
- function startResizeE(startX, startW) {
- const onMove = (clientX) => {
- const delta = clientX - startX;
- const newW = Math.min(Math.max(startW + delta, 200), window.innerWidth * 0.6);
- panel.style.width = newW + 'px';
- };
- const onEnd = () => {
- const w = parseInt(panel.style.width);
- if (w) localStorage.setItem('f0ck_chat_width', w);
- document.removeEventListener('mousemove', mouseMove);
- document.removeEventListener('mouseup', mouseEnd);
- document.removeEventListener('touchmove', touchMove);
- document.removeEventListener('touchend', touchEnd);
- };
- const mouseMove = mv => onMove(mv.clientX);
- const mouseEnd = onEnd;
- const touchMove = mv => { mv.preventDefault(); onMove(mv.touches[0].clientX); };
- const touchEnd = onEnd;
- document.addEventListener('mousemove', mouseMove);
- document.addEventListener('mouseup', mouseEnd);
- document.addEventListener('touchmove', touchMove, { passive: false });
- document.addEventListener('touchend', touchEnd);
+ if (resizeE && !isPageMode) {
+ function startResizeE(startX, startW) {
+ const onMove = (clientX) => {
+ const delta = clientX - startX;
+ const newW = Math.min(Math.max(startW + delta, 200), window.innerWidth * 0.6);
+ panel.style.width = newW + 'px';
+ };
+ const onEnd = () => {
+ const w = parseInt(panel.style.width);
+ if (w) localStorage.setItem('f0ck_chat_width', w);
+ document.removeEventListener('mousemove', mouseMove);
+ document.removeEventListener('mouseup', mouseEnd);
+ document.removeEventListener('touchmove', touchMove);
+ document.removeEventListener('touchend', touchEnd);
+ };
+ const mouseMove = mv => onMove(mv.clientX);
+ const mouseEnd = onEnd;
+ const touchMove = mv => { mv.preventDefault(); onMove(mv.touches[0].clientX); };
+ const touchEnd = onEnd;
+ document.addEventListener('mousemove', mouseMove);
+ document.addEventListener('mouseup', mouseEnd);
+ document.addEventListener('touchmove', touchMove, { passive: false });
+ document.addEventListener('touchend', touchEnd);
+ }
+ resizeE.addEventListener('mousedown', (e) => {
+ if (e.button !== 0) return;
+ e.preventDefault();
+ const r = panel.getBoundingClientRect();
+ startResizeE(e.clientX, r.width);
+ });
+ resizeE.addEventListener('touchstart', (e) => {
+ e.preventDefault();
+ const r = panel.getBoundingClientRect();
+ startResizeE(e.touches[0].clientX, r.width);
+ }, { passive: false });
}
- resizeE.addEventListener('mousedown', (e) => {
- if (e.button !== 0) return;
- e.preventDefault();
- const r = panel.getBoundingClientRect();
- startResizeE(e.clientX, r.width);
- });
- resizeE.addEventListener('touchstart', (e) => {
- e.preventDefault();
- const r = panel.getBoundingClientRect();
- startResizeE(e.touches[0].clientX, r.width);
- }, { passive: false });
// ── Float / Dock toggle ───────────────────────────────────────────────
let isFloating = localStorage.getItem('f0ck_chat_floating') === '1';
function applyFloatMode() {
+ if (isPageMode) return;
const widget = document.getElementById('gchat-widget');
if (isFloating) {
panel.classList.add('gchat-floating');
@@ -1196,31 +1642,40 @@
if (fy) panel.style.top = Math.max(getTopBound(), parseInt(fy)) + 'px';
panel.style.right = 'auto';
panel.style.bottom = 'auto';
- document.getElementById('gchat-float-btn').innerHTML = '';
+ const floatBtn = document.getElementById('gchat-float-btn');
+ if (floatBtn) floatBtn.innerHTML = '';
} else {
panel.classList.remove('gchat-floating');
panel.style.left = '';
panel.style.top = '';
panel.style.right = '';
panel.style.bottom = '';
- document.getElementById('gchat-float-btn').innerHTML = '';
+ const maxH = window.innerHeight - getTopBound() - 10;
+ const curH = parseInt(panel.style.height);
+ if (curH && curH > maxH) {
+ panel.style.height = maxH + 'px';
+ }
+ const floatBtn = document.getElementById('gchat-float-btn');
+ if (floatBtn) floatBtn.innerHTML = '';
}
}
applyFloatMode();
- document.getElementById('gchat-float-btn').addEventListener('click', () => {
- isFloating = !isFloating;
- localStorage.setItem('f0ck_chat_floating', isFloating ? '1' : '0');
- if (isFloating) {
- // Snap to current visual position so it doesn't jump
- const r = panel.getBoundingClientRect();
- panel.style.left = r.left + 'px';
- panel.style.top = r.top + 'px';
- localStorage.setItem('f0ck_chat_float_x', Math.round(r.left));
- localStorage.setItem('f0ck_chat_float_y', Math.round(r.top));
- }
- applyFloatMode();
- });
+ const floatBtn = document.getElementById('gchat-float-btn');
+ if (floatBtn) {
+ floatBtn.addEventListener('click', () => {
+ isFloating = !isFloating;
+ localStorage.setItem('f0ck_chat_floating', isFloating ? '1' : '0');
+ if (isFloating) {
+ const r = panel.getBoundingClientRect();
+ panel.style.left = r.left + 'px';
+ panel.style.top = r.top + 'px';
+ localStorage.setItem('f0ck_chat_float_x', Math.round(r.left));
+ localStorage.setItem('f0ck_chat_float_y', Math.round(r.top));
+ }
+ applyFloatMode();
+ });
+ }
// Drag-to-move in float mode — drag the header
const header = document.getElementById('gchat-header');
@@ -1259,25 +1714,28 @@
document.addEventListener('touchmove', touchMove, { passive: false });
document.addEventListener('touchend', touchEnd);
}
- header.addEventListener('mousedown', (e) => {
- if (e.button !== 0) return;
- if (!isFloating) return;
- if (e.target.closest('button')) return;
- e.preventDefault();
- const r = panel.getBoundingClientRect();
- startDragMove(e.clientX, e.clientY, e.clientX - r.left, e.clientY - r.top);
- });
- header.addEventListener('touchstart', (e) => {
- if (!isFloating) return;
- if (e.target.closest('button')) return;
- e.preventDefault();
- const r = panel.getBoundingClientRect();
- const t = e.touches[0];
- startDragMove(t.clientX, t.clientY, t.clientX - r.left, t.clientY - r.top);
- }, { passive: false });
+ if (header && !isPageMode) {
+ header.addEventListener('mousedown', (e) => {
+ if (e.button !== 0) return;
+ if (!isFloating) return;
+ if (e.target.closest('button')) return;
+ e.preventDefault();
+ const r = panel.getBoundingClientRect();
+ startDragMove(e.clientX, e.clientY, e.clientX - r.left, e.clientY - r.top);
+ });
+ header.addEventListener('touchstart', (e) => {
+ if (!isFloating) return;
+ if (e.target.closest('button')) return;
+ e.preventDefault();
+ const r = panel.getBoundingClientRect();
+ const t = e.touches[0];
+ startDragMove(t.clientX, t.clientY, t.clientX - r.left, t.clientY - r.top);
+ }, { passive: false });
+ }
// Minimize toggle — in float mode anchor to bottom edge in both directions
function toggleMinimized() {
+ if (isPageMode) return;
const willExpand = isMinimized; // about to expand
if (isFloating) {
const r = panel.getBoundingClientRect();
@@ -1285,7 +1743,6 @@
const bottomBound = window.innerHeight;
if (!willExpand) {
- // About to minimize: decide anchor based on proximity
const distToTop = Math.abs(r.top - topBound);
const distToBottom = Math.abs(bottomBound - (r.top + r.height));
const anchorTop = distToTop < distToBottom;
@@ -1298,17 +1755,14 @@
requestAnimationFrame(() => {
let newTop;
if (anchorTop) {
- // Anchor to top: keep current top
newTop = Math.max(topBound, curTop);
} else {
- // Anchor to bottom: keep current bottom (existing behavior)
newTop = Math.min(bottomBound - 42, Math.max(topBound, curBottom - 42));
}
panel.style.top = newTop + 'px';
localStorage.setItem('f0ck_chat_float_y', newTop);
});
} else {
- // Expanding: use saved anchor or default to bottom if not set
const wasAnchorTop = localStorage.getItem('f0ck_chat_anchor_top') === '1';
setMinimized(false);
@@ -1317,10 +1771,8 @@
const curTop = parseInt(panel.style.top) || 0;
let newTop;
if (wasAnchorTop) {
- // Expanding from top anchor: top stays same
newTop = Math.max(topBound, curTop);
} else {
- // Expanding from bottom anchor: shift top UP (existing behavior)
newTop = Math.max(topBound, curTop - (fullH - 42));
}
panel.style.top = newTop + 'px';
@@ -1330,21 +1782,26 @@
} else {
setMinimized(!isMinimized);
if (!isMinimized) {
- // Normal docked expand
loadHistory();
}
}
}
- document.getElementById('gchat-minimize-btn').addEventListener('click', e => {
- e.stopPropagation(); toggleMinimized();
- });
- document.getElementById('gchat-header').addEventListener('dblclick', e => {
- if (!e.target.closest('.gchat-icon-btn')) toggleMinimized();
- });
+ const minBtn = document.getElementById('gchat-minimize-btn');
+ if (minBtn) {
+ minBtn.addEventListener('click', e => {
+ e.stopPropagation(); toggleMinimized();
+ });
+ }
+ if (header && !isPageMode) {
+ header.addEventListener('dblclick', e => {
+ if (!e.target.closest('.gchat-icon-btn')) toggleMinimized();
+ });
+ }
// Close button — hide widget and show reopen bubble
function closeChat() {
+ if (isPageMode) return;
isClosed = true;
localStorage.setItem('f0ck_chat_closed', '1');
const widget = document.getElementById('gchat-widget');
@@ -1352,6 +1809,7 @@
buildReopenBubble();
}
function reopenChat() {
+ if (isPageMode) return;
isClosed = false;
localStorage.removeItem('f0ck_chat_closed');
const widget = document.getElementById('gchat-widget');
@@ -1361,6 +1819,7 @@
if (isMinimized) { setMinimized(false); } else { loadHistory(); }
}
function buildReopenBubble() {
+ if (isPageMode) return;
if (document.getElementById('gchat-reopen-bubble')) return;
const el = document.createElement('button');
el.id = 'gchat-reopen-bubble';
@@ -1369,40 +1828,47 @@
el.addEventListener('click', reopenChat);
document.body.appendChild(el);
}
- document.getElementById('gchat-close-btn').addEventListener('click', e => {
- e.stopPropagation(); closeChat();
- });
+ const closeBtn = document.getElementById('gchat-close-btn');
+ if (closeBtn) {
+ closeBtn.addEventListener('click', e => {
+ e.stopPropagation(); closeChat();
+ });
+ }
// Expose globally for settings page
window.reopenChat = reopenChat;
window.closeChatWidget = closeChat;
- // Send
- document.getElementById('gchat-send-btn').addEventListener('click', sendMessage);
- textarea.addEventListener('keydown', e => {
- if (e.key === 'Enter' && !e.shiftKey) {
- // Don't send while emoji or mention autocomplete is active
- const emojiAC = document.getElementById('gchat-emoji-ac');
- const mentionAC = document.getElementById('gchat-mention-ac');
- if ((emojiAC && emojiAC.style.display !== 'none') ||
- (mentionAC && mentionAC.style.display !== 'none')) return;
+ // Delegated Send & Toolbar Click Listeners
+ document.addEventListener('click', e => {
+ const sendBtn = e.target.closest('#gchat-send-btn');
+ if (sendBtn) {
e.preventDefault();
sendMessage();
+ return;
}
});
- // Auto-grow
- textarea.addEventListener('input', function () {
- this.style.height = 'auto';
- this.style.height = Math.min(this.scrollHeight, 80) + 'px';
+ // Delegated Enter Key press on chat input
+ document.addEventListener('keydown', e => {
+ if (e.target && e.target.id === 'gchat-input') {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ const emojiAC = document.getElementById('gchat-emoji-ac');
+ const mentionAC = document.getElementById('gchat-mention-ac');
+ if ((emojiAC && emojiAC.style.display !== 'none') ||
+ (mentionAC && mentionAC.style.display !== 'none')) return;
+ e.preventDefault();
+ sendMessage();
+ }
+ }
});
- // Toolbar
- document.getElementById('gchat-spoiler-btn').addEventListener('click', e => {
- e.preventDefault(); insertWrap(textarea, '[spoiler]', '[/spoiler]');
- });
- document.getElementById('gchat-blur-btn').addEventListener('click', e => {
- e.preventDefault(); insertWrap(textarea, '[blur]', '[/blur]');
+ // Delegated Auto-grow for chat input
+ document.addEventListener('input', e => {
+ if (e.target && e.target.id === 'gchat-input') {
+ e.target.style.height = 'auto';
+ e.target.style.height = Math.min(e.target.scrollHeight, 80) + 'px';
+ }
});
// @mention autocomplete (always opens upward)
@@ -1458,62 +1924,199 @@
applyBackground(e.detail?.background || null);
});
- // ── Chat topic helpers ────────────────────────────────────────────────
- function applyTopic(text) {
- const el = document.getElementById('gchat-topic');
- if (!el) return;
- if (text) {
- el.textContent = text;
- el.style.display = '';
- } else {
- el.style.display = 'none';
- el.textContent = '';
- }
- }
-
- // Restore topic on load
- fetch('/api/chat/topic').then(r => r.json()).then(d => {
- if (d.success && d.topic) applyTopic(d.topic);
- }).catch(() => {});
-
- // SSE topic change — apply instantly to all clients
- document.addEventListener('f0ck:global_chat_topic', (e) => {
- applyTopic(e.detail?.topic || null);
- });
-
// ── Online users bar ─────────────────────────────────────────────────
function renderOnline(users, guestCount = 0) {
const el = document.getElementById('gchat-online');
if (!el) return;
+ const isPageMode = (window.location.pathname === '/chat');
+
if ((!users || users.length === 0) && guestCount === 0) {
- el.innerHTML = '';
- el.style.display = 'none';
+ if (isPageMode) {
+ el.style.display = 'flex';
+ el.innerHTML = ``;
+ } else {
+ el.innerHTML = '';
+ el.style.display = 'none';
+ }
return;
}
- el.style.display = 'block';
- // Show up to 8 avatars, then "+N more" pill
- const MAX_SHOWN = 8;
- const shown = users.slice(0, MAX_SHOWN);
- const extra = users.length - shown.length;
- const avatarHTML = shown.map(u => {
- const name = esc(u.display_name || u.username || '?');
- const src = avatarSrc(u);
- const colorStyle = u.username_color ? `style="border-color:${esc(u.username_color)}"` : '';
- return `
`;
- }).join('');
- const extraPill = extra > 0 ? `` : '';
-
- let countText = `${users.length} online`;
- if (guestCount > 0 && (window.f0ckSession.is_admin || window.f0ckSession.is_moderator)) {
- countText += ` (${guestCount} guests)`;
+
+ if (isPageMode) {
+ el.style.display = 'flex';
+ let totalOnline = users.length + guestCount;
+ let countText = `${users.length} registered`;
+ if (guestCount > 0) {
+ countText += `, ${guestCount} guest${guestCount > 1 ? 's' : ''}`;
+ }
+
+ const userItemsHTML = users.map(u => {
+ const name = esc(u.display_name || u.username || '?');
+ const uname = esc(u.username || '');
+ const src = avatarSrc(u);
+ const colorStyle = u.username_color ? `style="color:${esc(u.username_color)}"` : '';
+ return `
+
+
+ ${name}
+
+ `;
+ }).join('');
+
+ const guestHTML = guestCount > 0 ? `
+
+
+
${guestCount} Guest${guestCount > 1 ? 's' : ''}
+
+ ` : '';
+
+ el.innerHTML = `
+
+ ${countText}
+
+ ${userItemsHTML}
+ ${guestHTML}
+
+ `;
+ } else {
+ el.style.display = 'block';
+ // Show up to 8 avatars, then "+N more" pill
+ const MAX_SHOWN = 8;
+ const shown = users.slice(0, MAX_SHOWN);
+ const extra = users.length - shown.length;
+ const avatarHTML = shown.map(u => {
+ const name = esc(u.display_name || u.username || '?');
+ const src = avatarSrc(u);
+ const colorStyle = u.username_color ? `style="border-color:${esc(u.username_color)}"` : '';
+ return `
`;
+ }).join('');
+ const extraPill = extra > 0 ? `` : '';
+
+ let countText = `${users.length} online`;
+ if (guestCount > 0 && (window.f0ckSession?.is_admin || window.f0ckSession?.is_moderator)) {
+ countText += ` (${guestCount} guests)`;
+ }
+ const countLabel = `${countText}`;
+ el.innerHTML = `${countLabel}
${avatarHTML}${extraPill}
`;
}
- const countLabel = `${countText}`;
- el.innerHTML = `${countLabel}
${avatarHTML}${extraPill}
`;
}
+ let lastPresenceUsers = [];
+ let lastPresenceGuests = 0;
+
+ function syncChatLocation() {
+ const isPageMode = (window.location.pathname === '/chat');
+ const container = isPageMode ? document.getElementById('site-chat-container') : null;
+ let widget = document.getElementById('gchat-widget');
+ let panel = document.getElementById('gchat-panel');
+
+ if (!panel) {
+ buildWidget();
+ panel = document.getElementById('gchat-panel');
+ }
+
+ const messagesEl = document.getElementById('gchat-messages');
+ const inputAreaEl = document.getElementById('gchat-input-area');
+ const onlineEl = document.getElementById('gchat-online');
+
+ if (isPageMode) {
+ if (!container) return;
+ isMinimized = false;
+ isClosed = false;
+
+ const reopenBubble = document.getElementById('gchat-reopen-bubble');
+ if (reopenBubble) reopenBubble.remove();
+
+ // Build page-mode wrapper if not present
+ let bodyWrap = panel.querySelector('.gchat-body-wrap');
+ if (!bodyWrap) {
+ bodyWrap = document.createElement('div');
+ bodyWrap.className = 'gchat-body-wrap';
+ const mainArea = document.createElement('div');
+ mainArea.className = 'gchat-main-area';
+ bodyWrap.appendChild(mainArea);
+ panel.appendChild(bodyWrap);
+ }
+ const mainArea = bodyWrap.querySelector('.gchat-main-area');
+ if (messagesEl && mainArea) mainArea.appendChild(messagesEl);
+ if (inputAreaEl && mainArea) mainArea.appendChild(inputAreaEl);
+ if (onlineEl && bodyWrap) bodyWrap.appendChild(onlineEl);
+
+ if (!container.contains(panel)) {
+ container.appendChild(panel);
+ }
+ if (widget && widget.parentElement) {
+ widget.remove();
+ }
+
+ if (panel) {
+ panel.className = 'gchat-page-panel';
+ panel.style.height = '';
+ panel.style.width = '';
+ panel.style.left = '';
+ panel.style.top = '';
+ panel.style.display = '';
+ }
+
+ renderOnline(lastPresenceUsers, lastPresenceGuests);
+ if (_sseReady || document.documentElement.dataset.sseReady === '1') {
+ setConnecting(false);
+ }
+ loadHistory();
+ } else {
+ if (!widget) {
+ widget = document.createElement('div');
+ widget.id = 'gchat-widget';
+ document.body.appendChild(widget);
+ }
+ widget.style.display = isClosed ? 'none' : '';
+
+ // Remove page-mode wrapper if present, moving elements back as direct children of panel
+ const bodyWrap = panel.querySelector('.gchat-body-wrap');
+ if (bodyWrap) {
+ if (onlineEl) panel.appendChild(onlineEl);
+ if (messagesEl) panel.appendChild(messagesEl);
+ if (inputAreaEl) panel.appendChild(inputAreaEl);
+ bodyWrap.remove();
+ }
+
+ if (panel.parentElement !== widget) {
+ widget.appendChild(panel);
+ }
+
+ if (panel) {
+ panel.className = isMinimized ? 'gchat-minimized' : '';
+ const savedH = localStorage.getItem('f0ck_chat_height');
+ if (savedH) {
+ const parsedH = parseInt(savedH, 10);
+ const maxH = getMaxPanelHeight();
+ panel.style.height = (parsedH ? Math.min(parsedH, maxH) : 400) + 'px';
+ }
+ const savedW = localStorage.getItem('f0ck_chat_width');
+ if (savedW) panel.style.width = savedW + 'px';
+ applyFloatMode();
+ }
+
+ if (isClosed) {
+ buildReopenBubble();
+ } else {
+ const reopenBubble = document.getElementById('gchat-reopen-bubble');
+ if (reopenBubble) reopenBubble.remove();
+ }
+
+ renderOnline(lastPresenceUsers, lastPresenceGuests);
+ }
+ }
+
document.addEventListener('f0ck:global_chat_presence', (e) => {
- renderOnline(e.detail?.users || [], e.detail?.guestCount || 0);
+ lastPresenceUsers = e.detail?.users || [];
+ lastPresenceGuests = e.detail?.guestCount || 0;
+ renderOnline(lastPresenceUsers, lastPresenceGuests);
});
+ document.addEventListener('f0ck:contentLoaded', syncChatLocation);
+ window.addEventListener('popstate', syncChatLocation);
+
// Event delegation: reply + admin delete buttons inside #gchat-messages
const msgArea = document.getElementById('gchat-messages');
const csrf = () => window.f0ckSession?.csrf_token;
@@ -1573,11 +2176,11 @@
}
} catch (_) {}
}
- if (!isMinimized) loadHistory();
+ if (!isMinimized || (window.location.pathname === '/chat')) loadHistory();
})();
// If user had previously closed the chat, hide on load and show bubble
- if (isClosed) {
+ if (isClosed && (window.location.pathname !== '/chat')) {
const widget = document.getElementById('gchat-widget');
if (widget) widget.style.display = 'none';
buildReopenBubble();
diff --git a/src/inc/locales/de.json b/src/inc/locales/de.json
index ec8ed36..1ef011f 100644
--- a/src/inc/locales/de.json
+++ b/src/inc/locales/de.json
@@ -4,6 +4,7 @@
"meme": "Meme",
"halls": "Hallen",
"tags": "Tags",
+ "chat": "Chat",
"search": "Suchen",
"random": "Zufall",
"profile": "Profil",
diff --git a/src/inc/locales/en.json b/src/inc/locales/en.json
index fddd032..1fd5c3f 100644
--- a/src/inc/locales/en.json
+++ b/src/inc/locales/en.json
@@ -4,6 +4,7 @@
"meme": "Meme",
"halls": "Halls",
"tags": "Tags",
+ "chat": "Chat",
"search": "Search",
"random": "Random",
"profile": "Profile",
diff --git a/src/inc/locales/nl.json b/src/inc/locales/nl.json
index d0ead57..e7a833a 100644
--- a/src/inc/locales/nl.json
+++ b/src/inc/locales/nl.json
@@ -4,6 +4,7 @@
"meme": "meme",
"halls": "Hallen",
"tags": "Tags",
+ "chat": "Chat",
"search": "Zoeken",
"random": "Willekeurig",
"profile": "profiel",
diff --git a/src/inc/locales/zange.json b/src/inc/locales/zange.json
index 168ffbc..1fe9f4e 100644
--- a/src/inc/locales/zange.json
+++ b/src/inc/locales/zange.json
@@ -4,6 +4,7 @@
"meme": "Memen",
"halls": "Hallen",
"tags": "Etiketten",
+ "chat": "Geschwafel",
"search": "Suche",
"random": "Zufall",
"profile": "Profil",
diff --git a/src/inc/routes/chat.mjs b/src/inc/routes/chat.mjs
index 47bac63..a02c0d5 100644
--- a/src/inc/routes/chat.mjs
+++ b/src/inc/routes/chat.mjs
@@ -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) {
diff --git a/views/chat.html b/views/chat.html
new file mode 100644
index 0000000..d48dc12
--- /dev/null
+++ b/views/chat.html
@@ -0,0 +1,9 @@
+@include(snippets/header)
+
+@include(snippets/footer)
diff --git a/views/snippets/navbar.html b/views/snippets/navbar.html
index d1a00b0..87495fa 100644
--- a/views/snippets/navbar.html
+++ b/views/snippets/navbar.html
@@ -26,6 +26,9 @@
@endif
+ @if(enable_global_chat)
+
+ @endif
@if(abyss_enabled)
@endif