feat(example): add room support to browser chat client
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:
parent
18063fb3ef
commit
5288282ae7
@ -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 />
|
||||
<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();
|
||||
const msg = message.value.trim();
|
||||
joinBtn.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Send message handler
|
||||
sendBtn.addEventListener("click", function(ev) {
|
||||
ev.preventDefault();
|
||||
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>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user