fix: replay channel state on SPA reconnect
Some checks failed
check / check (push) Has been cancelled
Some checks failed
check / check (push) Has been cancelled
When a client reconnects to an existing session (e.g. browser tab closed and reopened), the server now enqueues synthetic JOIN messages plus TOPIC/NAMES numerics for every channel the session belongs to. These are delivered only to the reconnecting client, not broadcast to other users. Server changes: - Add replayChannelState() to handlers that enqueues per-channel JOIN + join-numerics (332/353/366) to a specific client. - HandleState accepts ?replay=1 query parameter to trigger replay. - HandleLogin (password auth) also replays channel state for the new client since it creates a fresh client for an existing session. SPA changes: - On resume, call /state?replay=1 instead of /state so the server enqueues channel state into the message queue. - processMessage now creates channel tabs when receiving a JOIN where msg.from matches the current nick (handles both live joins and replayed joins on reconnect). - onLogin no longer re-sends JOIN commands for saved channels on resume — the server handles it via the replay mechanism, avoiding spurious JOIN broadcasts to other channel members. Closes #60
This commit is contained in:
@@ -70,7 +70,7 @@ function LoginScreen({ onLogin }) {
|
||||
.catch(() => {});
|
||||
const saved = localStorage.getItem("neoirc_token");
|
||||
if (saved) {
|
||||
api("/state")
|
||||
api("/state?replay=1")
|
||||
.then((u) => onLogin(u.nick, true))
|
||||
.catch(() => localStorage.removeItem("neoirc_token"));
|
||||
}
|
||||
@@ -333,7 +333,24 @@ function App() {
|
||||
case "JOIN": {
|
||||
const text = `${msg.from} has joined ${msg.to}`;
|
||||
if (msg.to) addMessage(msg.to, { ...base, text, system: true });
|
||||
if (msg.to && msg.to.startsWith("#")) refreshMembers(msg.to);
|
||||
if (msg.to && msg.to.startsWith("#")) {
|
||||
// Create a tab when the current user joins a channel
|
||||
// (including replayed JOINs on reconnect).
|
||||
if (msg.from === nickRef.current) {
|
||||
setTabs((prev) => {
|
||||
if (
|
||||
prev.find(
|
||||
(t) => t.type === "channel" && t.name === msg.to,
|
||||
)
|
||||
)
|
||||
return prev;
|
||||
|
||||
return [...prev, { type: "channel", name: msg.to }];
|
||||
});
|
||||
}
|
||||
|
||||
refreshMembers(msg.to);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -636,9 +653,12 @@ function App() {
|
||||
setLoggedIn(true);
|
||||
addSystemMessage("Server", `Connected as ${userNick}`);
|
||||
|
||||
// Request MOTD on resumed sessions (new sessions get
|
||||
// it automatically from the server during creation).
|
||||
if (isResumed) {
|
||||
// Request MOTD on resumed sessions (new sessions
|
||||
// get it automatically from the server during
|
||||
// creation). Channel state is replayed by the
|
||||
// server via the message queue (?replay=1), so we
|
||||
// do not need to re-JOIN channels here.
|
||||
try {
|
||||
await api("/messages", {
|
||||
method: "POST",
|
||||
@@ -647,8 +667,11 @@ function App() {
|
||||
} catch (e) {
|
||||
// MOTD is non-critical.
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Fresh session — join any previously saved channels.
|
||||
const saved = JSON.parse(
|
||||
localStorage.getItem("neoirc_channels") || "[]",
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user