const chatContainer = document.getElementById("chat"); const end = new URLSearchParams(location.search).get("end") !== null; class ChatMessage { get username() { return this._username; } set username(username) { this._username = username; this._usernameEl.innerText = username; } get content() { return this._content; } set content(content) { this._content = content; this._contentEl.innerText = content; } get colour() { return this._colour; } set colour(colour) { this._colour = colour; if (colour != undefined) this._container.style.setProperty("--colour", colour); } get element() { return this._container; } destroy() { this._container.remove(); } constructor(message) { this._headerEl = document.createElement('div'); this._headerEl.className = "header"; // TODO: assemble badges in header, before username this._usernameEl = document.createElement('p'); this._usernameEl.className = "username"; this.username = message.username; this._headerEl.appendChild(this._usernameEl); this._contentEl = document.createElement('p'); this._contentEl.className = "content"; // TODO: build message content from fragments // TODO: 7TV emotes (stretch goal: FFZ emotes) this.content = message.text; this._container = document.createElement("div"); this._container.className = "message"; this._container.dataset.id = message.id; this._container.dataset.username = message.username; // TODO: generate and cache random colour for users that do not have one this.colour = message.colour; this._container.appendChild(this._headerEl); this._container.appendChild(this._contentEl); } } class SystemMessage { get text() { return this._text; } set text(text) { this._text = text; this._textEl.innerText = text; } get type() { return this._type; } set type(type) { this._type = type; this._container.className = ["message", "system", type].join(" ").trim(); } get element() { return this._container; } destroy() { this._container.remove(); } constructor(text, type) { // TODO: actual icon designs for system messages this._iconEl = document.createElement('div'); this._iconEl.className = "icon"; this._textEl = document.createElement('p'); this._textEl.className = "text"; this.text = text; this._container = document.createElement("div"); this.type = type; this._container.appendChild(this._iconEl); this._container.appendChild(this._textEl); } } async function sync() { let eventSource; try { eventSource = new EventSource("/twitch/sse/chat"); } catch (err) { console.error("Failed to connect to event source", err); return; } eventSource.addEventListener("error", () => { eventSource.close(); console.error("Connection lost, or an error has occurred."); console.log("Attempting to reconnect..."); setTimeout(() => { sync(); }, 1000); }); eventSource.addEventListener("chat", event => { handleChatMessage(JSON.parse(event.data)); }); eventSource.addEventListener("system", event => { handleSystemMessage(JSON.parse(event.data)); }); eventSource.addEventListener("delete", event => { const data = JSON.parse(event.data); const username = data.username; const message_id = data.id; handleDeleteMessage(username, message_id); }); eventSource.addEventListener("open", () => { console.log("Connected."); }); } function handleChatMessage(data) { console.log(`[CHAT] ${data.username}: ${data.text}`); const chatMessage = new ChatMessage(data); addMessageAndTruncateChat(chatMessage.element); } function handleSystemMessage(data) { if (data.type) console.log(`[SYSTEM] [${data.type}] ${data.text}`); else console.log(`[SYSTEM] ${data.text}`); const systemMessage = new SystemMessage(data.text, data.type || ""); addMessageAndTruncateChat(systemMessage.element); } function handleDeleteMessage(username, message_id) { if (username == "*" && message_id == "*") { chatContainer.innerHTML = ""; return; } if (username.length > 0 && message_id == "*") { chatContainer.querySelectorAll(`.message[data-username=${username}]`) .forEach(msg => { msg.remove(); }); return; } if (username.length == 0 && message_id.length > 0) { const msg = chatContainer.querySelector(`.message[data-id=${message_id}]`); if (msg) msg.remove(); } } const CHAT_LIMIT = 50; function addMessageAndTruncateChat(element) { chatContainer.appendChild(element); window.scrollTo(0, document.body.scrollHeight); if (chatContainer.childElementCount <= CHAT_LIMIT) return; chatContainer.children[0].remove(); } function twitchEmoteURL(emoteID) { return `https://static-cdn.jtvnw.net/emoticons/v2/${emoteID}/default/dark/4.0#e=0` } function twitchBadgeURL(badgeID) { return `https://static-cdn.jtvnw.net/badges/v1/${badgeID}/3` } if (end) chatContainer.classList.add("end") sync();