document.addEventListener("DOMContentLoaded", async () => { const params = new URLSearchParams(window.location.search); const room = params.get("room") || "Unknown Room"; const username = localStorage.getItem("username"); const userKey = localStorage.getItem("userKey"); let firstLoad = true; let isRefreshing = false; if (!room || !username || !userKey) { alert("Missing room or user credentials."); const url = `/index.html${room ? '?room=' + encodeURIComponent(room) : ''}`; window.location.href = url; return; } document.title = room; try { const res = await fetch('/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, key: userKey }) }); if (!res.ok) { localStorage.clear(); alert("User validation failed."); window.location.href = '/'; return; } } catch (err) { console.error('Validation error:', err); localStorage.clear(); alert("Validation failed."); const redirectUrl = `/index.html${room ? '?room=' + encodeURIComponent(room) : ''}`; window.location.href = redirectUrl; return; } // đ Security: Escape HTML to prevent XSS function escapeHtml(text) { if (!text) return text; return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } document.getElementById("room-title").innerHTML = `
`; const messagesDiv = document.getElementById("messages"); const form = document.getElementById("chat-form"); const input = document.getElementById("message-input"); const usersDiv = document.getElementById("user-list"); const soundToggleBtn = document.getElementById("sound-toggle"); const externalToggleBtn = document.getElementById("external-toggle"); // đ¤ Voice Message Setup const micBtn = document.createElement("button"); micBtn.textContent = "đ¤"; micBtn.type = "button"; micBtn.className = "mic-button"; micBtn.title = "Hold to record (max 15s)"; // Insert mic button after input (between input and send button) if (input && input.parentNode) { input.parentNode.insertBefore(micBtn, input.nextSibling); } // đŖī¸ Autoplay Toggle const autoplayToggleBtn = document.createElement("button"); autoplayToggleBtn.id = "autoplay-toggle"; // Insert after external toggle if (externalToggleBtn && externalToggleBtn.parentNode) { externalToggleBtn.parentNode.insertBefore(autoplayToggleBtn, externalToggleBtn.nextSibling); } // Audio setup const sounds = { message: new Audio('/assets/sounds/message.wav'), in: new Audio('/assets/sounds/user-in.wav'), out: new Audio('/assets/sounds/user-out.wav'), }; // Load saved preferences from localStorage soundEnabled = localStorage.getItem("soundEnabled") !== "false"; // default true externalContentEnabled = localStorage.getItem("externalContentEnabled") !== "false"; // default true let autoplayEnabled = localStorage.getItem("autoplayEnabled") !== "false"; // default true soundToggleBtn.textContent = soundEnabled ? "đ Sound: On" : "đ Sound: Off"; soundToggleBtn.classList.toggle("toggle-off", !soundEnabled); const updateAutoplayToggleBtn = () => { autoplayToggleBtn.textContent = autoplayEnabled ? "đŖī¸ Autoplay: On" : "đ Autoplay: Off"; autoplayToggleBtn.classList.toggle("toggle-off", !autoplayEnabled); }; updateAutoplayToggleBtn(); const updateExternalToggleBtn = () => { externalToggleBtn.textContent = externalContentEnabled ? "đĨī¸ External Content: On" : "đ External Content: Off"; externalToggleBtn.classList.toggle("toggle-off", !externalContentEnabled); }; updateExternalToggleBtn(); // Reset style on load // Mobile Toggle for User List & Settings const userListContainer = document.getElementById("user-list-container"); if (userListContainer) { const toggleBtn = document.createElement("button"); toggleBtn.id = "user-list-toggle"; toggleBtn.textContent = "Show Users & Settings"; userListContainer.prepend(toggleBtn); toggleBtn.addEventListener("click", () => { const isExpanded = userListContainer.classList.toggle("expanded"); toggleBtn.textContent = isExpanded ? "Hide Users & Settings" : "Show Users & Settings"; }); } soundToggleBtn.addEventListener("click", () => { soundEnabled = !soundEnabled; localStorage.setItem("soundEnabled", soundEnabled); // â Persist soundToggleBtn.textContent = soundEnabled ? "đ Sound: On" : "đ Sound: Off"; soundToggleBtn.classList.toggle("toggle-off", !soundEnabled); }); externalToggleBtn.addEventListener("click", () => { externalContentEnabled = !externalContentEnabled; localStorage.setItem("externalContentEnabled", externalContentEnabled); // â Persist updateExternalToggleBtn(); }); autoplayToggleBtn.addEventListener("click", () => { autoplayEnabled = !autoplayEnabled; localStorage.setItem("autoplayEnabled", autoplayEnabled); updateAutoplayToggleBtn(); }); // Recording Logic let mediaRecorder; let audioChunks = []; let isRecording = false; let recordingTimeout; async function startRecording() { if (isRecording) return; try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // Low bitrate preference (16kbps) const options = { mimeType: 'audio/webm;codecs=opus', bitsPerSecond: 16000 }; // Fallback if codec not supported if (!MediaRecorder.isTypeSupported(options.mimeType)) delete options.mimeType; mediaRecorder = new MediaRecorder(stream, options); audioChunks = []; isRecording = true; micBtn.classList.add("recording"); micBtn.textContent = "âšī¸"; mediaRecorder.ondataavailable = e => audioChunks.push(e.data); mediaRecorder.onstop = () => { micBtn.classList.remove("recording"); micBtn.textContent = "đ¤"; isRecording = false; clearTimeout(recordingTimeout); const blob = new Blob(audioChunks, { type: 'audio/webm' }); const reader = new FileReader(); reader.readAsDataURL(blob); reader.onloadend = () => { const base64Audio = reader.result; // Send immediately fetch(`/chat/${encodeURIComponent(room)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, text: "đ¤ Voice Message", audio: base64Audio }) }).then(() => refresh()); }; stream.getTracks().forEach(t => t.stop()); }; mediaRecorder.start(); recordingTimeout = setTimeout(() => { if (isRecording && mediaRecorder.state === "recording") mediaRecorder.stop(); }, 15000); // 15s limit } catch (err) { console.error("Mic error:", err); alert("Could not access microphone."); } } function stopRecording() { if (isRecording && mediaRecorder && mediaRecorder.state === "recording") { mediaRecorder.stop(); } } micBtn.addEventListener("mousedown", startRecording); micBtn.addEventListener("mouseup", stopRecording); micBtn.addEventListener("mouseleave", stopRecording); micBtn.addEventListener("touchstart", (e) => { e.preventDefault(); startRecording(); }); micBtn.addEventListener("touchend", (e) => { e.preventDefault(); stopRecording(); }); async function refreshUserPresence() { const res = await fetch(`/chat/${encodeURIComponent(room)}/users`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username }) }); if (!res.ok) { alert("You were removed from the room."); window.location.href = "/server_list.html"; // â go to server list return; } } let lastUserList = []; let lastTimestamp = null; const seenMessageTimestamps = new Set(); const audioQueue = []; let isPlayingAudio = false; function processAudioQueue() { if (isPlayingAudio || audioQueue.length === 0) return; const audio = audioQueue.shift(); isPlayingAudio = true; const onComplete = () => { isPlayingAudio = false; processAudioQueue(); }; audio.addEventListener('ended', onComplete, { once: true }); audio.addEventListener('error', () => { console.error("Audio playback error"); onComplete(); }, { once: true }); audio.play().catch(e => { console.log("Autoplay blocked or failed:", e); onComplete(); }); } let windowFocused = true; let unreadCount = 0; window.addEventListener('focus', () => { windowFocused = true; unreadCount = 0; document.title = `${room}`; }); window.addEventListener('blur', () => { windowFocused = false; }); function linkify(text) { // 1. Escape the raw text first to prevent XSS const safeText = escapeHtml(text); // 2. Replace URLs in the escaped text with HTML return safeText.replace(/(https?:\/\/[^\s]+)/g, (url) => { const imagePattern = /\.(png|jpe?g|gif)$/i; const youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]{11})/; const id = `preview-${Math.random().toString(36).substr(2, 9)}`; // â YouTube Video const ytMatch = url.match(youtubeRegex); if (ytMatch) { const videoId = ytMatch[1]; const embedUrl = `https://www.youtube.com/embed/${videoId}`; if (externalContentEnabled) { return ` ${url}