From bae20c9abe4826b6a9f54c521ed6f45d82a6ca7c Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 27 Oct 2026 04:55:35 +0000 Subject: [PATCH] Give the web Server tab a name no nick can take (closes #200) The web client stored each tab's messages and unread count under the tab's name, and the Server tab was named Server. A user with the nick Server got a direct-message tab with the same name, so the two tabs showed one message list. The Server tab is now stored under "*server*". No nick or channel name can contain *, so that user's tab keeps its own messages. The tab bar still labels the tab Server. Model: opus-5-5 --- web/src/app.jsx | 91 ++++++++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/web/src/app.jsx b/web/src/app.jsx index 364f11e..6c1d112 100644 --- a/web/src/app.jsx +++ b/web/src/app.jsx @@ -7,6 +7,11 @@ const RECONNECT_DELAY = 3000; const MEMBER_REFRESH_INTERVAL = 10000; const ACTION_PREFIX = "\x01ACTION "; const ACTION_SUFFIX = "\x01"; +// SERVER_TAB is the Server tab's name, under which its messages and +// unread count are stored. No nick or channel name can contain *, so a +// user with the nick Server gets a direct-message tab of its own. The +// tab bar labels this tab Server. +const SERVER_TAB = "*server*"; // Hashcash proof-of-work helpers using Web Crypto API. @@ -270,9 +275,9 @@ function UserList({ members, onNickClick }) { function App() { const [loggedIn, setLoggedIn] = useState(false); const [nick, setNick] = useState(""); - const [tabs, setTabs] = useState([{ type: "server", name: "Server" }]); + const [tabs, setTabs] = useState([{ type: "server", name: SERVER_TAB }]); const [activeTab, setActiveTab] = useState(0); - const [messages, setMessages] = useState({ Server: [] }); + const [messages, setMessages] = useState({ [SERVER_TAB]: [] }); const [members, setMembers] = useState({}); const [topics, setTopics] = useState({}); const [unread, setUnread] = useState({}); @@ -468,7 +473,7 @@ function App() { // The channel's tab closes, so the Server tab gets the line // too. It needs an id of its own: addMessage drops a message // whose id it has already seen. - addMessage("Server", { + addMessage(SERVER_TAB, { ...base, id: msg.id + "-server", text, @@ -568,7 +573,7 @@ function App() { const chName = msg.params[0]; const count = msg.params[1]; const chTopic = body || ""; - addMessage("Server", { + addMessage(SERVER_TAB, { ...base, text: `${chName} (${count} users): ${chTopic.trim()}`, system: true, @@ -578,7 +583,7 @@ function App() { break; } case 323: - addMessage("Server", { + addMessage(SERVER_TAB, { ...base, text: body || "End of channel list", system: true, @@ -591,7 +596,7 @@ function App() { const whoCh = msg.params[0]; const whoNick = msg.params[4]; const whoFlags = msg.params.length > 5 ? msg.params[5] : ""; - addMessage("Server", { + addMessage(SERVER_TAB, { ...base, text: `${whoCh} ${whoNick} ${whoFlags}`, system: true, @@ -601,7 +606,7 @@ function App() { break; } case 315: - addMessage("Server", { + addMessage(SERVER_TAB, { ...base, text: body || "End of /WHO list", system: true, @@ -612,7 +617,7 @@ function App() { // RPL_WHOISUSER — params nick, user, host, *; body realname. if (Array.isArray(msg.params) && msg.params.length >= 3) { const [wiNick, wiUser, wiHost] = msg.params; - addMessage("Server", { + addMessage(SERVER_TAB, { ...base, text: `${wiNick} is ${wiUser}@${wiHost} (${body})`, system: true, @@ -626,7 +631,7 @@ function App() { if (Array.isArray(msg.params) && msg.params.length >= 2) { const wiNick = msg.params[0]; const wiServer = msg.params[1]; - addMessage("Server", { + addMessage(SERVER_TAB, { ...base, text: `${wiNick} on ${wiServer}`, system: true, @@ -638,7 +643,7 @@ function App() { case 313: { // RPL_WHOISOPERATOR — nick; body "is an IRC operator". if (Array.isArray(msg.params) && msg.params.length >= 1) { - addMessage("Server", { + addMessage(SERVER_TAB, { ...base, text: `${msg.params[0]} ${body}`, system: true, @@ -652,7 +657,7 @@ function App() { if (Array.isArray(msg.params) && msg.params.length >= 3) { const [wiNick, idle, signon] = msg.params; const signedOn = new Date(Number(signon) * 1000).toLocaleString(); - addMessage("Server", { + addMessage(SERVER_TAB, { ...base, text: `${wiNick} has been idle ${idle}s, signed on ${signedOn}`, system: true, @@ -665,7 +670,7 @@ function App() { // RPL_WHOISCHANNELS — nick, channels. if (Array.isArray(msg.params) && msg.params.length >= 1) { const wiNick = msg.params[0]; - addMessage("Server", { + addMessage(SERVER_TAB, { ...base, text: `${wiNick} is on: ${body}`, system: true, @@ -675,7 +680,7 @@ function App() { break; } case 318: - addMessage("Server", { + addMessage(SERVER_TAB, { ...base, text: body || "End of /WHOIS list", system: true, @@ -685,7 +690,7 @@ function App() { case 324: { // RPL_CHANNELMODEIS — channel, mode string. if (Array.isArray(msg.params) && msg.params.length >= 2) { - addMessage("Server", { + addMessage(SERVER_TAB, { ...base, text: `${msg.params[0]} modes: ${msg.params[1]}`, system: true, @@ -697,12 +702,12 @@ function App() { case 375: case 372: case 376: - addMessage("Server", { ...base, text: body, system: true }); + addMessage(SERVER_TAB, { ...base, text: body, system: true }); break; default: if (body) { - addMessage("Server", { ...base, text: body, system: true }); + addMessage(SERVER_TAB, { ...base, text: body, system: true }); } } }, @@ -828,7 +833,7 @@ function App() { ); setNick(userNick); setLoggedIn(true); - addSystemMessage("Server", `Connected as ${userNick}`); + addSystemMessage(SERVER_TAB, `Connected as ${userNick}`); if (isResumed) { // Request MOTD on resumed sessions (new sessions @@ -912,7 +917,7 @@ function App() { } } catch (err) { addSystemMessage( - "Server", + SERVER_TAB, `Failed to join ${name}: ${err.data?.error || "error"}`, ); } @@ -963,7 +968,7 @@ function App() { switch (cmd) { case "/join": { if (parts[1]) joinChannel(parts[1]); - else addSystemMessage("Server", "Usage: /join #channel"); + else addSystemMessage(SERVER_TAB, "Usage: /join #channel"); break; } @@ -972,7 +977,7 @@ function App() { const reason = parts.slice(1).join(" ") || undefined; partChannel(tab.name, reason); } else { - addSystemMessage("Server", "You are not in a channel"); + addSystemMessage(SERVER_TAB, "You are not in a channel"); } break; @@ -993,19 +998,19 @@ function App() { openDM(target); } catch (err) { addSystemMessage( - "Server", + SERVER_TAB, `Message failed: ${err.data?.error || "error"}`, ); } } else { - addSystemMessage("Server", "Usage: /msg "); + addSystemMessage(SERVER_TAB, "Usage: /msg "); } break; } case "/me": { if (tab.type === "server") { - addSystemMessage("Server", "Cannot use /me in server window"); + addSystemMessage(SERVER_TAB, "Cannot use /me in server window"); break; } @@ -1027,7 +1032,7 @@ function App() { ); } } else { - addSystemMessage("Server", "Usage: /me "); + addSystemMessage(SERVER_TAB, "Usage: /me "); } break; @@ -1041,19 +1046,19 @@ function App() { }); } catch (err) { addSystemMessage( - "Server", + SERVER_TAB, `Nick change failed: ${err.data?.error || "error"}`, ); } } else { - addSystemMessage("Server", "Usage: /nick "); + addSystemMessage(SERVER_TAB, "Usage: /nick "); } break; } case "/topic": { if (tab.type !== "channel") { - addSystemMessage("Server", "You are not in a channel"); + addSystemMessage(SERVER_TAB, "You are not in a channel"); break; } @@ -1070,13 +1075,13 @@ function App() { }); } catch (err) { addSystemMessage( - "Server", + SERVER_TAB, `Topic change failed: ${err.data?.error || "error"}`, ); } } else { addSystemMessage( - "Server", + SERVER_TAB, `Current topic for ${tab.name}: ${topics[tab.name] || "(none)"}`, ); } @@ -1085,7 +1090,7 @@ function App() { } case "/mode": { if (tab.type !== "channel") { - addSystemMessage("Server", "You are not in a channel"); + addSystemMessage(SERVER_TAB, "You are not in a channel"); break; } @@ -1103,7 +1108,7 @@ function App() { }); } catch (err) { addSystemMessage( - "Server", + SERVER_TAB, `MODE failed: ${err.data?.error || "error"}`, ); } @@ -1134,7 +1139,7 @@ function App() { }); } catch (err) { addSystemMessage( - "Server", + SERVER_TAB, `Failed to request MOTD: ${err.data?.error || "error"}`, ); } @@ -1158,13 +1163,13 @@ function App() { }); } catch (err) { addSystemMessage( - "Server", + SERVER_TAB, `Message failed: ${err.data?.error || "error"}`, ); } } } else { - addSystemMessage("Server", "Usage: /query [message]"); + addSystemMessage(SERVER_TAB, "Usage: /query [message]"); } break; @@ -1177,7 +1182,7 @@ function App() { }); } catch (err) { addSystemMessage( - "Server", + SERVER_TAB, `Failed to list channels: ${err.data?.error || "error"}`, ); } @@ -1194,12 +1199,12 @@ function App() { }); } catch (err) { addSystemMessage( - "Server", + SERVER_TAB, `WHO failed: ${err.data?.error || "error"}`, ); } } else { - addSystemMessage("Server", "Usage: /who #channel"); + addSystemMessage(SERVER_TAB, "Usage: /who #channel"); } break; @@ -1213,12 +1218,12 @@ function App() { }); } catch (err) { addSystemMessage( - "Server", + SERVER_TAB, `WHOIS failed: ${err.data?.error || "error"}`, ); } } else { - addSystemMessage("Server", "Usage: /whois "); + addSystemMessage(SERVER_TAB, "Usage: /whois "); } break; @@ -1249,13 +1254,13 @@ function App() { " /help — Show this help", ]; for (const line of helpLines) { - addSystemMessage("Server", line); + addSystemMessage(SERVER_TAB, line); } break; } default: - addSystemMessage("Server", `Unknown command: ${cmd}`); + addSystemMessage(SERVER_TAB, `Unknown command: ${cmd}`); } }; @@ -1284,7 +1289,7 @@ function App() { if (tab.type === "server") { addSystemMessage( - "Server", + SERVER_TAB, "Cannot send messages to the server window. Use /join #channel first.", ); @@ -1355,7 +1360,7 @@ function App() { key={tab.name} > - {tab.type === "dm" ? tab.name : tab.name} + {tab.type === "server" ? "Server" : tab.name} {unread[tab.name] > 0 && i !== activeTab && ( ({unread[tab.name]}) -- 2.54.0