Give the web Server tab a name no nick can take (closes #200)
check / check (push) Waiting to run

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
This commit was merged in pull request #201.
This commit is contained in:
2026-10-27 06:29:27 +01:00
parent 2331c15aad
commit d3b169a641
+48 -43
View File
@@ -7,6 +7,11 @@ const RECONNECT_DELAY = 3000;
const MEMBER_REFRESH_INTERVAL = 10000; const MEMBER_REFRESH_INTERVAL = 10000;
const ACTION_PREFIX = "\x01ACTION "; const ACTION_PREFIX = "\x01ACTION ";
const ACTION_SUFFIX = "\x01"; 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. // Hashcash proof-of-work helpers using Web Crypto API.
@@ -270,9 +275,9 @@ function UserList({ members, onNickClick }) {
function App() { function App() {
const [loggedIn, setLoggedIn] = useState(false); const [loggedIn, setLoggedIn] = useState(false);
const [nick, setNick] = useState(""); 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 [activeTab, setActiveTab] = useState(0);
const [messages, setMessages] = useState({ Server: [] }); const [messages, setMessages] = useState({ [SERVER_TAB]: [] });
const [members, setMembers] = useState({}); const [members, setMembers] = useState({});
const [topics, setTopics] = useState({}); const [topics, setTopics] = useState({});
const [unread, setUnread] = useState({}); const [unread, setUnread] = useState({});
@@ -468,7 +473,7 @@ function App() {
// The channel's tab closes, so the Server tab gets the line // The channel's tab closes, so the Server tab gets the line
// too. It needs an id of its own: addMessage drops a message // too. It needs an id of its own: addMessage drops a message
// whose id it has already seen. // whose id it has already seen.
addMessage("Server", { addMessage(SERVER_TAB, {
...base, ...base,
id: msg.id + "-server", id: msg.id + "-server",
text, text,
@@ -568,7 +573,7 @@ function App() {
const chName = msg.params[0]; const chName = msg.params[0];
const count = msg.params[1]; const count = msg.params[1];
const chTopic = body || ""; const chTopic = body || "";
addMessage("Server", { addMessage(SERVER_TAB, {
...base, ...base,
text: `${chName} (${count} users): ${chTopic.trim()}`, text: `${chName} (${count} users): ${chTopic.trim()}`,
system: true, system: true,
@@ -578,7 +583,7 @@ function App() {
break; break;
} }
case 323: case 323:
addMessage("Server", { addMessage(SERVER_TAB, {
...base, ...base,
text: body || "End of channel list", text: body || "End of channel list",
system: true, system: true,
@@ -591,7 +596,7 @@ function App() {
const whoCh = msg.params[0]; const whoCh = msg.params[0];
const whoNick = msg.params[4]; const whoNick = msg.params[4];
const whoFlags = msg.params.length > 5 ? msg.params[5] : ""; const whoFlags = msg.params.length > 5 ? msg.params[5] : "";
addMessage("Server", { addMessage(SERVER_TAB, {
...base, ...base,
text: `${whoCh} ${whoNick} ${whoFlags}`, text: `${whoCh} ${whoNick} ${whoFlags}`,
system: true, system: true,
@@ -601,7 +606,7 @@ function App() {
break; break;
} }
case 315: case 315:
addMessage("Server", { addMessage(SERVER_TAB, {
...base, ...base,
text: body || "End of /WHO list", text: body || "End of /WHO list",
system: true, system: true,
@@ -612,7 +617,7 @@ function App() {
// RPL_WHOISUSER — params nick, user, host, *; body realname. // RPL_WHOISUSER — params nick, user, host, *; body realname.
if (Array.isArray(msg.params) && msg.params.length >= 3) { if (Array.isArray(msg.params) && msg.params.length >= 3) {
const [wiNick, wiUser, wiHost] = msg.params; const [wiNick, wiUser, wiHost] = msg.params;
addMessage("Server", { addMessage(SERVER_TAB, {
...base, ...base,
text: `${wiNick} is ${wiUser}@${wiHost} (${body})`, text: `${wiNick} is ${wiUser}@${wiHost} (${body})`,
system: true, system: true,
@@ -626,7 +631,7 @@ function App() {
if (Array.isArray(msg.params) && msg.params.length >= 2) { if (Array.isArray(msg.params) && msg.params.length >= 2) {
const wiNick = msg.params[0]; const wiNick = msg.params[0];
const wiServer = msg.params[1]; const wiServer = msg.params[1];
addMessage("Server", { addMessage(SERVER_TAB, {
...base, ...base,
text: `${wiNick} on ${wiServer}`, text: `${wiNick} on ${wiServer}`,
system: true, system: true,
@@ -638,7 +643,7 @@ function App() {
case 313: { case 313: {
// RPL_WHOISOPERATOR — nick; body "is an IRC operator". // RPL_WHOISOPERATOR — nick; body "is an IRC operator".
if (Array.isArray(msg.params) && msg.params.length >= 1) { if (Array.isArray(msg.params) && msg.params.length >= 1) {
addMessage("Server", { addMessage(SERVER_TAB, {
...base, ...base,
text: `${msg.params[0]} ${body}`, text: `${msg.params[0]} ${body}`,
system: true, system: true,
@@ -652,7 +657,7 @@ function App() {
if (Array.isArray(msg.params) && msg.params.length >= 3) { if (Array.isArray(msg.params) && msg.params.length >= 3) {
const [wiNick, idle, signon] = msg.params; const [wiNick, idle, signon] = msg.params;
const signedOn = new Date(Number(signon) * 1000).toLocaleString(); const signedOn = new Date(Number(signon) * 1000).toLocaleString();
addMessage("Server", { addMessage(SERVER_TAB, {
...base, ...base,
text: `${wiNick} has been idle ${idle}s, signed on ${signedOn}`, text: `${wiNick} has been idle ${idle}s, signed on ${signedOn}`,
system: true, system: true,
@@ -665,7 +670,7 @@ function App() {
// RPL_WHOISCHANNELS — nick, channels. // RPL_WHOISCHANNELS — nick, channels.
if (Array.isArray(msg.params) && msg.params.length >= 1) { if (Array.isArray(msg.params) && msg.params.length >= 1) {
const wiNick = msg.params[0]; const wiNick = msg.params[0];
addMessage("Server", { addMessage(SERVER_TAB, {
...base, ...base,
text: `${wiNick} is on: ${body}`, text: `${wiNick} is on: ${body}`,
system: true, system: true,
@@ -675,7 +680,7 @@ function App() {
break; break;
} }
case 318: case 318:
addMessage("Server", { addMessage(SERVER_TAB, {
...base, ...base,
text: body || "End of /WHOIS list", text: body || "End of /WHOIS list",
system: true, system: true,
@@ -685,7 +690,7 @@ function App() {
case 324: { case 324: {
// RPL_CHANNELMODEIS — channel, mode string. // RPL_CHANNELMODEIS — channel, mode string.
if (Array.isArray(msg.params) && msg.params.length >= 2) { if (Array.isArray(msg.params) && msg.params.length >= 2) {
addMessage("Server", { addMessage(SERVER_TAB, {
...base, ...base,
text: `${msg.params[0]} modes: ${msg.params[1]}`, text: `${msg.params[0]} modes: ${msg.params[1]}`,
system: true, system: true,
@@ -697,12 +702,12 @@ function App() {
case 375: case 375:
case 372: case 372:
case 376: case 376:
addMessage("Server", { ...base, text: body, system: true }); addMessage(SERVER_TAB, { ...base, text: body, system: true });
break; break;
default: default:
if (body) { 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); setNick(userNick);
setLoggedIn(true); setLoggedIn(true);
addSystemMessage("Server", `Connected as ${userNick}`); addSystemMessage(SERVER_TAB, `Connected as ${userNick}`);
if (isResumed) { if (isResumed) {
// Request MOTD on resumed sessions (new sessions // Request MOTD on resumed sessions (new sessions
@@ -912,7 +917,7 @@ function App() {
} }
} catch (err) { } catch (err) {
addSystemMessage( addSystemMessage(
"Server", SERVER_TAB,
`Failed to join ${name}: ${err.data?.error || "error"}`, `Failed to join ${name}: ${err.data?.error || "error"}`,
); );
} }
@@ -963,7 +968,7 @@ function App() {
switch (cmd) { switch (cmd) {
case "/join": { case "/join": {
if (parts[1]) joinChannel(parts[1]); if (parts[1]) joinChannel(parts[1]);
else addSystemMessage("Server", "Usage: /join #channel"); else addSystemMessage(SERVER_TAB, "Usage: /join #channel");
break; break;
} }
@@ -972,7 +977,7 @@ function App() {
const reason = parts.slice(1).join(" ") || undefined; const reason = parts.slice(1).join(" ") || undefined;
partChannel(tab.name, reason); partChannel(tab.name, reason);
} else { } else {
addSystemMessage("Server", "You are not in a channel"); addSystemMessage(SERVER_TAB, "You are not in a channel");
} }
break; break;
@@ -993,19 +998,19 @@ function App() {
openDM(target); openDM(target);
} catch (err) { } catch (err) {
addSystemMessage( addSystemMessage(
"Server", SERVER_TAB,
`Message failed: ${err.data?.error || "error"}`, `Message failed: ${err.data?.error || "error"}`,
); );
} }
} else { } else {
addSystemMessage("Server", "Usage: /msg <nick> <message>"); addSystemMessage(SERVER_TAB, "Usage: /msg <nick> <message>");
} }
break; break;
} }
case "/me": { case "/me": {
if (tab.type === "server") { if (tab.type === "server") {
addSystemMessage("Server", "Cannot use /me in server window"); addSystemMessage(SERVER_TAB, "Cannot use /me in server window");
break; break;
} }
@@ -1027,7 +1032,7 @@ function App() {
); );
} }
} else { } else {
addSystemMessage("Server", "Usage: /me <action>"); addSystemMessage(SERVER_TAB, "Usage: /me <action>");
} }
break; break;
@@ -1041,19 +1046,19 @@ function App() {
}); });
} catch (err) { } catch (err) {
addSystemMessage( addSystemMessage(
"Server", SERVER_TAB,
`Nick change failed: ${err.data?.error || "error"}`, `Nick change failed: ${err.data?.error || "error"}`,
); );
} }
} else { } else {
addSystemMessage("Server", "Usage: /nick <newnick>"); addSystemMessage(SERVER_TAB, "Usage: /nick <newnick>");
} }
break; break;
} }
case "/topic": { case "/topic": {
if (tab.type !== "channel") { if (tab.type !== "channel") {
addSystemMessage("Server", "You are not in a channel"); addSystemMessage(SERVER_TAB, "You are not in a channel");
break; break;
} }
@@ -1070,13 +1075,13 @@ function App() {
}); });
} catch (err) { } catch (err) {
addSystemMessage( addSystemMessage(
"Server", SERVER_TAB,
`Topic change failed: ${err.data?.error || "error"}`, `Topic change failed: ${err.data?.error || "error"}`,
); );
} }
} else { } else {
addSystemMessage( addSystemMessage(
"Server", SERVER_TAB,
`Current topic for ${tab.name}: ${topics[tab.name] || "(none)"}`, `Current topic for ${tab.name}: ${topics[tab.name] || "(none)"}`,
); );
} }
@@ -1085,7 +1090,7 @@ function App() {
} }
case "/mode": { case "/mode": {
if (tab.type !== "channel") { if (tab.type !== "channel") {
addSystemMessage("Server", "You are not in a channel"); addSystemMessage(SERVER_TAB, "You are not in a channel");
break; break;
} }
@@ -1103,7 +1108,7 @@ function App() {
}); });
} catch (err) { } catch (err) {
addSystemMessage( addSystemMessage(
"Server", SERVER_TAB,
`MODE failed: ${err.data?.error || "error"}`, `MODE failed: ${err.data?.error || "error"}`,
); );
} }
@@ -1134,7 +1139,7 @@ function App() {
}); });
} catch (err) { } catch (err) {
addSystemMessage( addSystemMessage(
"Server", SERVER_TAB,
`Failed to request MOTD: ${err.data?.error || "error"}`, `Failed to request MOTD: ${err.data?.error || "error"}`,
); );
} }
@@ -1158,13 +1163,13 @@ function App() {
}); });
} catch (err) { } catch (err) {
addSystemMessage( addSystemMessage(
"Server", SERVER_TAB,
`Message failed: ${err.data?.error || "error"}`, `Message failed: ${err.data?.error || "error"}`,
); );
} }
} }
} else { } else {
addSystemMessage("Server", "Usage: /query <nick> [message]"); addSystemMessage(SERVER_TAB, "Usage: /query <nick> [message]");
} }
break; break;
@@ -1177,7 +1182,7 @@ function App() {
}); });
} catch (err) { } catch (err) {
addSystemMessage( addSystemMessage(
"Server", SERVER_TAB,
`Failed to list channels: ${err.data?.error || "error"}`, `Failed to list channels: ${err.data?.error || "error"}`,
); );
} }
@@ -1194,12 +1199,12 @@ function App() {
}); });
} catch (err) { } catch (err) {
addSystemMessage( addSystemMessage(
"Server", SERVER_TAB,
`WHO failed: ${err.data?.error || "error"}`, `WHO failed: ${err.data?.error || "error"}`,
); );
} }
} else { } else {
addSystemMessage("Server", "Usage: /who #channel"); addSystemMessage(SERVER_TAB, "Usage: /who #channel");
} }
break; break;
@@ -1213,12 +1218,12 @@ function App() {
}); });
} catch (err) { } catch (err) {
addSystemMessage( addSystemMessage(
"Server", SERVER_TAB,
`WHOIS failed: ${err.data?.error || "error"}`, `WHOIS failed: ${err.data?.error || "error"}`,
); );
} }
} else { } else {
addSystemMessage("Server", "Usage: /whois <nick>"); addSystemMessage(SERVER_TAB, "Usage: /whois <nick>");
} }
break; break;
@@ -1249,13 +1254,13 @@ function App() {
" /help — Show this help", " /help — Show this help",
]; ];
for (const line of helpLines) { for (const line of helpLines) {
addSystemMessage("Server", line); addSystemMessage(SERVER_TAB, line);
} }
break; break;
} }
default: default:
addSystemMessage("Server", `Unknown command: ${cmd}`); addSystemMessage(SERVER_TAB, `Unknown command: ${cmd}`);
} }
}; };
@@ -1284,7 +1289,7 @@ function App() {
if (tab.type === "server") { if (tab.type === "server") {
addSystemMessage( addSystemMessage(
"Server", SERVER_TAB,
"Cannot send messages to the server window. Use /join #channel first.", "Cannot send messages to the server window. Use /join #channel first.",
); );
@@ -1355,7 +1360,7 @@ function App() {
key={tab.name} key={tab.name}
> >
<span class="tab-label"> <span class="tab-label">
{tab.type === "dm" ? tab.name : tab.name} {tab.type === "server" ? "Server" : tab.name}
</span> </span>
{unread[tab.name] > 0 && i !== activeTab && ( {unread[tab.name] > 0 && i !== activeTab && (
<span class="unread-count">({unread[tab.name]})</span> <span class="unread-count">({unread[tab.name]})</span>