/**
* globalchat.js — Site-wide persistent chat widget
* Panel is always visible. Minimize-only.
* Features: emoji picker, :emoji autocomplete, @user autocomplete,
* greentext, [spoiler], [blur]
*/
(function () {
'use strict';
if (!window.f0ckSession?.enable_global_chat) return;
if (!window.f0ckSession?.logged_in) return;
if (window.location.pathname.startsWith('/abyss')) return;
const MAX_VISIBLE_MSGS = 100;
const RATE_LIMIT_MS = 800;
let isMinimized = localStorage.getItem('f0ck_chat_minimized') !== '0';
let isClosed = localStorage.getItem('f0ck_chat_closed') === '1';
let lastSent = 0;
let customEmojis = null; // name → url
let unreadCount = 0;
let chatFocused = document.hasFocus();
const ytOembedCache = new Map(); // videoId → {title, author_name}
function updateBadge() {
const badge = document.getElementById('gchat-badge');
const bubble = document.getElementById('gchat-reopen-bubble');
if (unreadCount > 0) {
const label = unreadCount > 99 ? '99+' : String(unreadCount);
if (badge) { badge.textContent = label; badge.style.display = 'inline-flex'; }
// Bubble badge — create it lazily if it doesn't exist yet
if (bubble) {
let bb = bubble.querySelector('.gchat-bubble-badge');
if (!bb) {
bb = document.createElement('span');
bb.className = 'gchat-bubble-badge';
bubble.appendChild(bb);
}
bb.textContent = label;
bb.style.display = '';
}
} else {
if (badge) badge.style.display = 'none';
const bb = document.getElementById('gchat-reopen-bubble')?.querySelector('.gchat-bubble-badge');
if (bb) bb.style.display = 'none';
}
}
function clearUnread() {
unreadCount = 0;
updateBadge();
}
window.addEventListener('focus', () => { chatFocused = true; if (!isMinimized) clearUnread(); });
window.addEventListener('blur', () => { chatFocused = false; });
// ── i18n ─────────────────────────────────────────────────────────────────
function t(key, fallback) { return window.f0ckI18n?.[key] || fallback; }
// ── Build widget ─────────────────────────────────────────────────────────
function buildWidget() {
const widget = document.createElement('div');
widget.id = 'gchat-widget';
widget.innerHTML = `
`;
document.body.appendChild(widget);
}
// ── Helpers ───────────────────────────────────────────────────────────────
function esc(str) {
const d = document.createElement('div');
d.textContent = str || '';
return d.innerHTML;
}
function avatarSrc(msg) {
if (msg.avatar_file) return `/a/${msg.avatar_file}`;
if (msg.avatar) return `/t/${msg.avatar}.webp`;
return '/a/default.png';
}
// ── Build allowed-hosts regex (mirrors comments.js logic) ─────────────────
function buildAllowedHostsRegex() {
const escapedSiteHost = window.location.host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const hosts = [escapedSiteHost];
if (window.f0ckAllowedImages && Array.isArray(window.f0ckAllowedImages)) {
window.f0ckAllowedImages.forEach(h => {
hosts.push(`(?:[a-z0-9-]+\\.)*${h.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
});
}
return hosts.join('|');
}
// ── Message rendering with formatting ────────────────────────────────────
function renderMsgContent(raw) {
// 1. Process line by line for greentext, then HTML-escape each line
const lines = raw.split('\n').map(line => {
const trimmed = line.trimStart();
if (trimmed.startsWith('>')) {
const content = esc(trimmed.slice(1));
return `>${content}`;
}
return esc(line);
});
// Join lines — skip
between consecutive greentext spans
let html = '';
for (let i = 0; i < lines.length; i++) {
html += lines[i];
if (i < lines.length - 1) {
const curGreen = lines[i].startsWith('');
const nextGreen = lines[i + 1].startsWith('');
if (!(curGreen && nextGreen)) html += '
';
}
}
// 2. [spoiler]...[/spoiler]
let prev, iter = 0;
do {
prev = html;
html = html.replace(/\[spoiler\]([\s\S]*?)\[\/spoiler\]/gi,
(_, c) => `${c}`);
} while (html !== prev && ++iter < 10);
// 3. [blur]...[/blur]
iter = 0;
do {
prev = html;
html = html.replace(/\[blur\]([\s\S]*?)\[\/blur\]/gi,
(_, c) => `${c}`);
} while (html !== prev && ++iter < 10);
// 4. @mention links
html = html.replace(/(? `@${u}`);
// 5. Custom emoji :name:
if (customEmojis) {
html = html.replace(/:([a-z0-9_]+):/g, (m, name) => {
if (customEmojis[name]) return `
`;
return m;
});
}
// 6. URL linkification + media embeds
const hostsRegex = buildAllowedHostsRegex();
// 6a. Raw image URLs from allowed hosts →
const imageRegex = new RegExp(
`(https?:\\/\\/(?:${hostsRegex})\\/(?:(?!https?:\\/\\/).)+?\\.(?:jpg|jpeg|png|gif|webp)(?:\\?(?:(?!https?:\\/\\/)\\S)+)?)`,
'gi'
);
html = html.replace(imageRegex, url =>
`
`
);
// 6b. Raw video URLs from allowed hosts →