feat(example): add room support to browser chat client
Some checks failed
CI / test (push) Successful in 12s
CI / lint (push) Failing after 10s

Update example/index.html to allow users to specify a room via the UI:

- Add room name text input with '/' as default
- Add 'Join Room' button that (re)connects to the specified room
- Construct WebSocket URL using the room path (ws://host:port/room)
- Display connection status with current room name
- Derive initial room from browser URL hash (e.g., #/chat → /chat)
- Update URL hash on room change for shareable links
- Listen for hashchange events for URL-driven navigation
- Add Ctrl+Enter shortcut for sending messages
- Improve styling with flexbox layout for controls

Demo: open index.html#/chat in two tabs and index.html#/game in another.
Messages in /chat stay isolated from /game.

🤖 Assisted by the code-assist SOP
This commit is contained in:
savinmax 2026-06-13 13:35:26 +02:00
parent 18063fb3ef
commit 5288282ae7

View File

@ -9,14 +9,68 @@
<body>
<style>
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 20px;
}
.room-controls {
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.room-controls label {
font-weight: bold;
}
.room-controls input {
padding: 6px 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
width: 200px;
}
.room-controls button {
padding: 6px 14px;
border: none;
border-radius: 4px;
background-color: #4a90d9;
color: white;
font-size: 14px;
cursor: pointer;
}
.room-controls button:hover {
background-color: #357abd;
}
.room-status {
font-size: 13px;
color: #555;
padding: 4px 10px;
background-color: #e8f4e8;
border-radius: 4px;
}
.room-status.disconnected {
background-color: #f4e8e8;
}
.chat {
width: 600px;
height: 500px;
border: 1px solid black;
border: 1px solid #ccc;
border-radius: 6px;
overflow-y: scroll;
background-color: beige;
background-color: #fafaf5;
padding: 15px;
box-sizing: content-box;
}
.chat .bubble {
@ -28,58 +82,223 @@
padding: 5px 10px;
}
.chat .system {
width: calc(100% - 30px);
font-size: 11px;
color: #666;
font-style: italic;
margin-bottom: 8px;
padding: 3px 10px;
}
.chat .error {
color: red;
margin: 10px 0;
font-size: 12px;
}
.message-controls {
margin-top: 10px;
display: flex;
gap: 10px;
width: 600px;
}
.message-controls textarea {
flex: 1;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
resize: vertical;
min-height: 40px;
}
.message-controls button {
padding: 8px 18px;
border: none;
border-radius: 4px;
background-color: #4a90d9;
color: white;
font-size: 14px;
cursor: pointer;
align-self: flex-end;
}
.message-controls button:hover {
background-color: #357abd;
}
.message-controls button:disabled {
background-color: #aaa;
cursor: not-allowed;
}
</style>
<div>
<h1>P2P Chat</h1>
<div class="room-controls">
<label for="room-input">Room:</label>
<input type="text" id="room-input" placeholder="/" value="/">
<button id="join-btn">Join Room</button>
<span id="room-status" class="room-status disconnected">Disconnected</span>
</div>
<div class="chat" id="box"></div>
<textarea id="message"></textarea>
<br />
<button id="send" disabled>Send</button>
<div class="message-controls">
<textarea id="message" placeholder="Type a message..."></textarea>
<button id="send" disabled>Send</button>
</div>
</div>
<script>
const chat = document.getElementById("box");
const message = document.getElementById("message");
const btn = document.getElementById("send");
const sendBtn = document.getElementById("send");
const roomInput = document.getElementById("room-input");
const joinBtn = document.getElementById("join-btn");
const roomStatus = document.getElementById("room-status");
const name = Date.now().toString(36);
let retry = 1000;
let ws;
let currentRoom = "/";
function connect() {
ws = new WebSocket('ws://localhost:8443/');
// Derive initial room from URL hash (e.g., index.html#/chat → /chat)
function getRoomFromHash() {
const hash = window.location.hash.slice(1); // remove the '#'
if (hash && hash.startsWith("/")) {
return hash;
}
return "/";
}
ws.onmessage = (event) => {
console.log('Received:', event.data);
chat.innerHTML += `<div class="bubble">${event.data}</div>`;
function updateStatus(connected, room) {
if (connected) {
roomStatus.textContent = "Connected to " + room;
roomStatus.classList.remove("disconnected");
} else {
roomStatus.textContent = "Disconnected";
roomStatus.classList.add("disconnected");
}
}
function addSystemMessage(text) {
chat.innerHTML += '<div class="system">' + text + '</div>';
chat.scrollTop = chat.scrollHeight;
}
function connect(room) {
// Close existing connection if any
if (ws) {
ws.onmessage = null;
ws.onopen = null;
ws.onerror = null;
ws.onclose = null;
ws.close();
ws = null;
}
// Normalize room path
if (!room.startsWith("/")) {
room = "/" + room;
}
currentRoom = room;
roomInput.value = room;
// Update URL hash for shareable links
window.location.hash = room;
// Build WebSocket URL
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const host = window.location.hostname || "localhost";
const port = "8443";
const wsUrl = protocol + "//" + host + ":" + port + room;
addSystemMessage("Connecting to room <b>" + room + "</b>...");
sendBtn.setAttribute("disabled", "disabled");
updateStatus(false, room);
ws = new WebSocket(wsUrl);
ws.onmessage = function(event) {
chat.innerHTML += '<div class="bubble">' + event.data + '</div>';
chat.scrollTop = chat.scrollHeight;
};
ws.onopen = () => {
btn.removeAttribute("disabled");
ws.send(`${name} joined the chat`);
ws.onopen = function() {
retry = 1000;
sendBtn.removeAttribute("disabled");
updateStatus(true, room);
addSystemMessage("Joined room <b>" + room + "</b>");
ws.send(name + " joined the chat");
};
ws.onerror = (ev) => {
btn.setAttribute("disabled", "disabled");
chat.innerHTML += `<div class="error">Failed to connect to websocket, retrying...</div>`;
console.error(ev);
delete ws.onmessage;
delete ws.onopen;
setTimeout(connect, retry);
retry *= 2;
ws.onerror = function(ev) {
sendBtn.setAttribute("disabled", "disabled");
updateStatus(false, room);
chat.innerHTML += '<div class="error">Connection error, retrying in ' + (retry / 1000) + 's...</div>';
chat.scrollTop = chat.scrollHeight;
console.error("WebSocket error:", ev);
};
ws.onclose = function(ev) {
sendBtn.setAttribute("disabled", "disabled");
updateStatus(false, currentRoom);
if (!ev.wasClean) {
setTimeout(function() { connect(currentRoom); }, retry);
retry = Math.min(retry * 2, 30000);
}
};
}
btn.addEventListener("click", (ev) => {
// Join Room button handler
joinBtn.addEventListener("click", function() {
var room = roomInput.value.trim() || "/";
retry = 1000;
chat.innerHTML = "";
connect(room);
});
// Allow Enter key in room input to join
roomInput.addEventListener("keydown", function(ev) {
if (ev.key === "Enter") {
ev.preventDefault();
joinBtn.click();
}
});
// Send message handler
sendBtn.addEventListener("click", function(ev) {
ev.preventDefault();
const msg = message.value.trim();
var msg = message.value.trim();
if (!msg) {
return;
}
const data = `${name}<br>${msg}`;
var data = name + "<br>" + msg;
ws.send(data);
message.value = "";
});
connect();
// Allow Ctrl+Enter to send
message.addEventListener("keydown", function(ev) {
if (ev.key === "Enter" && (ev.ctrlKey || ev.metaKey)) {
ev.preventDefault();
sendBtn.click();
}
});
// Listen for hash changes (e.g., user edits URL)
window.addEventListener("hashchange", function() {
var room = getRoomFromHash();
if (room !== currentRoom) {
retry = 1000;
chat.innerHTML = "";
connect(room);
}
});
// Initial connection
var initialRoom = getRoomFromHash();
roomInput.value = initialRoom;
connect(initialRoom);
</script>
</body>