let roomListDiv = document.getElementById('room-list'); let messagesDiv = document.getElementById('messages'); let newMessageForm = document.getElementById('new-message'); let newRoomForm = document.getElementById('new-room'); let statusDiv = document.getElementById('status'); let roomTemplate = document.getElementById('room'); let messageTemplate = document.getElementById('message'); let messageField = newMessageForm.querySelector("#message"); let usernameField = newMessageForm.querySelector("#username"); let roomNameField = newRoomForm.querySelector("#name"); var STATE = { room: "lobby", rooms: {}, connected: false, } // Generate a color from a "hash" of a string. Thanks, internet. function hashColor(str) { let hash = 0; for (var i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); hash = hash & hash; } return `hsl(${hash % 360}, 100%, 70%)`; } // Add a new room `name` and change to it. Returns `true` if the room didn't // already exist and false otherwise. function addRoom(name) { if (STATE[name]) { changeRoom(name); return false; } var node = roomTemplate.content.cloneNode(true); var room = node.querySelector(".room"); room.addEventListener("click", () => changeRoom(name)); room.textContent = name; room.dataset.name = name; roomListDiv.appendChild(node); STATE[name] = []; changeRoom(name); return true; } // Change the current room to `name`, restoring its messages. function changeRoom(name) { if (STATE.room == name) return; var newRoom = roomListDiv.querySelector(`.room[data-name='${name}']`); var oldRoom = roomListDiv.querySelector(`.room[data-name='${STATE.room}']`); if (!newRoom || !oldRoom) return; STATE.room = name; oldRoom.classList.remove("active"); newRoom.classList.add("active"); messagesDiv.querySelectorAll(".message").forEach((msg) => { messagesDiv.removeChild(msg) }); STATE[name].forEach((data) => addMessage(name, data.username, data.message)) } // Add `message` from `username` to `room`. If `push`, then actually store the // message. If the current room is `room`, render the message. function addMessage(room, username, message, push = false) { if (push) { STATE[room].push({ username, message }) } if (STATE.room == room) { var node = messageTemplate.content.cloneNode(true); node.querySelector(".message .username").textContent = username; node.querySelector(".message .username").style.color = hashColor(username); node.querySelector(".message .text").textContent = message; messagesDiv.appendChild(node); } } // Subscribe to the event source at `uri` with exponential backoff reconnect. function subscribe(uri) { var retryTime = 1; function connect(uri) { const events = new EventSource(uri); events.addEventListener("message", (ev) => { console.log("raw data", JSON.stringify(ev.data)); console.log("decoded data", JSON.stringify(JSON.parse(ev.data))); const msg = JSON.parse(ev.data); if (!("message" in msg) || !("room" in msg) || !("username" in msg)) return; addMessage(msg.room, msg.username, msg.message, true); }); events.addEventListener("open", () => { setConnectedStatus(true); console.log(`connected to event stream at ${uri}`); retryTime = 1; }); events.addEventListener("error", () => { setConnectedStatus(false); events.close(); let timeout = retryTime; retryTime = Math.min(64, retryTime * 2); console.log(`connection lost. attempting to reconnect in ${timeout}s`); setTimeout(() => connect(uri), (() => timeout * 1000)()); }); } connect(uri); } // Set the connection status: `true` for connected, `false` for disconnected. function setConnectedStatus(status) { STATE.connected = status; statusDiv.className = (status) ? "connected" : "reconnecting"; } // Let's go! Initialize the world. function init() { // Initialize some rooms. addRoom("lobby"); addRoom("rocket"); changeRoom("lobby"); addMessage("lobby", "Rocket", "Hey! Open another browser tab, send a message.", true); addMessage("rocket", "Rocket", "This is another room. Neat, huh?", true); // Set up the form handler. newMessageForm.addEventListener("submit", (e) => { e.preventDefault(); const room = STATE.room; const message = messageField.value; const username = usernameField.value || "guest"; if (!message || !username) return; if (STATE.connected) { fetch("/message", { method: "POST", body: new URLSearchParams({ room, username, message }), }).then((response) => { if (response.ok) messageField.value = ""; }); } }) // Set up the new room handler. newRoomForm.addEventListener("submit", (e) => { e.preventDefault(); const room = roomNameField.value; if (!room) return; roomNameField.value = ""; if (!addRoom(room)) return; addMessage(room, "Rocket", `Look, your own "${room}" room! Nice.`, true); }) // Subscribe to server-sent events. subscribe("/events"); } init();