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
306 lines
8.9 KiB
HTML
306 lines
8.9 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Websocket relay example</title>
|
|
</head>
|
|
|
|
<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 #ccc;
|
|
border-radius: 6px;
|
|
overflow-y: scroll;
|
|
background-color: #fafaf5;
|
|
padding: 15px;
|
|
}
|
|
|
|
.chat .bubble {
|
|
width: calc(100% - 30px);
|
|
border-radius: 10px;
|
|
background-color: bisque;
|
|
font-size: 12px;
|
|
margin-bottom: 10px;
|
|
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>
|
|
<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 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 = "/";
|
|
|
|
// 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 "/";
|
|
}
|
|
|
|
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 = function() {
|
|
retry = 1000;
|
|
sendBtn.removeAttribute("disabled");
|
|
updateStatus(true, room);
|
|
addSystemMessage("Joined room <b>" + room + "</b>");
|
|
ws.send(name + " joined the chat");
|
|
};
|
|
|
|
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);
|
|
}
|
|
};
|
|
}
|
|
|
|
// 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();
|
|
var msg = message.value.trim();
|
|
if (!msg) {
|
|
return;
|
|
}
|
|
var data = name + "<br>" + msg;
|
|
ws.send(data);
|
|
message.value = "";
|
|
});
|
|
|
|
// 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>
|
|
|
|
</html>
|