diff --git a/components/app.js b/components/app.js index 9c1cb50..4e57b12 100644 --- a/components/app.js +++ b/components/app.js @@ -9,7 +9,7 @@ import Composer from "/components/composer.js"; import ScrollManager from "/components/scroll-manager.js"; import { html, Component, createRef } from "/lib/index.js"; import { strip as stripANSI } from "/lib/ansi.js"; -import { SERVER_BUFFER, BufferType, ReceiptType, Status, Unread } from "/state.js"; +import { SERVER_BUFFER, BufferType, ReceiptType, NetworkStatus, Unread } from "/state.js"; import commands from "/commands.js"; import { setup as setupKeybindings } from "/keybindings.js"; @@ -150,7 +150,6 @@ export default class App extends Component { autoconnect: false, autojoin: [], }, - status: Status.DISCONNECTED, networks: new Map(), buffers: new Map(), activeBuffer: null, @@ -451,7 +450,7 @@ export default class App extends Component { var networks = new Map(state.networks); networks.set(netID, { id: netID, - status: Status.CONNECTING, + status: NetworkStatus.CONNECTING, isupport: new Map(), }); return { networks }; @@ -469,8 +468,8 @@ export default class App extends Component { this.clients.set(netID, client); - client.addEventListener("close", () => { - this.handleClose(netID); + client.addEventListener("status", () => { + this.handleStatus(netID, client.status); }); client.addEventListener("message", (event) => { @@ -487,18 +486,24 @@ export default class App extends Component { this.switchBuffer({ network: netID, name: SERVER_BUFFER }); } - handleClose(netID) { + handleStatus(netID, status) { this.setNetworkState(netID, (state) => { - if (state.status == Status.DISCONNECTED) { + if (status !== Client.Status.DISCONNECTED) { + return { status }; + } + + if (state.status === Client.Status.DISCONNECTED) { // User decided to logout return null; } + console.log("Reconnecting to server in " + RECONNECT_DELAY_SEC + " seconds"); clearTimeout(this.reconnectTimeoutID); this.reconnectTimeoutID = setTimeout(() => { this.connect(netID, this.state.connectParams); }, RECONNECT_DELAY_SEC * 1000); - return { status: Status.DISCONNECTED }; + + return { status }; }); } @@ -515,12 +520,10 @@ export default class App extends Component { var client = this.clients.get(netID); if (client) { - // Prevent auto-reconnect from kicking in - client.removeEventListener("close", this.handleClose); - client.close(); + client.disconnect(); } - this.setNetworkState(netID, { status: Status.DISCONNECTED }); + this.setNetworkState(netID, { status: NetworkStatus.DISCONNECTED }); } reconnect(netID) { @@ -538,8 +541,6 @@ export default class App extends Component { var client = this.clients.get(netID); switch (msg.command) { case irc.RPL_WELCOME: - this.setNetworkState(netID, { status: Status.REGISTERED }); - if (this.state.connectParams.autojoin.length > 0) { client.send({ command: "JOIN", @@ -1009,10 +1010,10 @@ export default class App extends Component { activeNetwork = this.state.networks.get(activeBuffer.network); } - if (!activeNetwork || (activeNetwork.status != Status.REGISTERED && !activeBuffer)) { + if (!activeNetwork || (activeNetwork.status !== NetworkStatus.REGISTERED && !activeBuffer)) { return html`
- <${Connect} error=${this.state.error} params=${this.state.connectParams} disabled=${this.state.status != Status.DISCONNECTED} onSubmit=${this.handleConnectSubmit}/> + <${Connect} error=${this.state.error} params=${this.state.connectParams} disabled=${activeNetwork} onSubmit=${this.handleConnectSubmit}/>
`; } diff --git a/components/buffer-header.js b/components/buffer-header.js index 3c53e79..4a4c4fc 100644 --- a/components/buffer-header.js +++ b/components/buffer-header.js @@ -1,7 +1,7 @@ import { html, Component } from "/lib/index.js"; import linkify from "/lib/linkify.js"; import { strip as stripANSI } from "/lib/ansi.js"; -import { BufferType, Status } from "/state.js"; +import { BufferType, NetworkStatus } from "/state.js"; const UserStatus = { HERE: "here", @@ -28,13 +28,16 @@ export default function BufferHeader(props) { var description = null; if (props.buffer.serverInfo) { switch (props.network.status) { - case Status.DISCONNECTED: + case NetworkStatus.DISCONNECTED: description = "Disconnected"; break; - case Status.CONNECTING: + case NetworkStatus.CONNECTING: description = "Connecting..."; break; - case Status.REGISTERED: + case NetworkStatus.REGISTERING: + description = "Logging in..."; + break; + case NetworkStatus.REGISTERED: var serverInfo = props.buffer.serverInfo; description = `Connected to ${serverInfo.name}`; break; diff --git a/lib/client.js b/lib/client.js index d72976a..8d8d714 100644 --- a/lib/client.js +++ b/lib/client.js @@ -13,6 +13,14 @@ const permanentCaps = [ ]; export default class Client extends EventTarget { + static Status = { + DISCONNECTED: "disconnected", + CONNECTING: "connecting", + REGISTERING: "registering", + REGISTERED: "registered", + }; + + status = Client.Status.DISCONNECTED; ws = null; nick = null; params = { @@ -23,7 +31,6 @@ export default class Client extends EventTarget { pass: null, saslPlain: null, }; - registered = false; availableCaps = {}; enabledCaps = {}; batches = new Map(); @@ -33,12 +40,19 @@ export default class Client extends EventTarget { this.params = Object.assign(this.params, params); + this.reconnect(); + } + + reconnect() { + this.disconnect(); + this.setStatus(Client.Status.CONNECTING); + try { - this.ws = new WebSocket(params.url); + this.ws = new WebSocket(this.params.url); } catch (err) { setTimeout(() => { this.dispatchEvent(new CustomEvent("error", { detail: "Failed to create connection: " + err })); - this.dispatchEvent(new CustomEvent("close")); + this.setStatus(Client.Status.DISCONNECTED); }, 0); return; } @@ -47,7 +61,8 @@ export default class Client extends EventTarget { this.ws.addEventListener("close", () => { console.log("Connection closed"); - this.dispatchEvent(new CustomEvent("close")); + this.ws = null; + this.setStatus(Client.Status.DISCONNECTED); }); this.ws.addEventListener("error", () => { @@ -55,8 +70,23 @@ export default class Client extends EventTarget { }); } + disconnect() { + if (this.ws) { + this.ws.close(1000); + } + } + + setStatus(status) { + if (this.status === status) { + return; + } + this.status = status; + this.dispatchEvent(new CustomEvent("status")); + } + handleOpen() { console.log("Connection opened"); + this.setStatus(Client.Status.REGISTERING); this.nick = this.params.nick; @@ -88,12 +118,12 @@ export default class Client extends EventTarget { case irc.RPL_WELCOME: if (this.params.saslPlain && this.availableCaps["sasl"] === undefined) { console.error("Server doesn't support SASL PLAIN"); - this.close(); + this.disconnect(); return; } console.log("Registration complete"); - this.registered = true; + this.setStatus(Client.Status.REGISTERED); break; case "CAP": this.handleCap(msg); @@ -109,7 +139,7 @@ export default class Client extends EventTarget { break; case irc.RPL_SASLSUCCESS: console.log("SASL authentication success"); - if (!this.registered) { + if (this.status != Client.Status.REGISTERED) { this.send({ command: "CAP", params: ["END"] }); } break; @@ -119,7 +149,7 @@ export default class Client extends EventTarget { case irc.ERR_SASLABORTED: case irc.ERR_SASLALREADY: this.dispatchEvent(new CustomEvent("error", { detail: "SASL error (" + msg.command + "): " + msg.params[1] })); - this.close(); + this.disconnect(); break; case "PING": this.send({ command: "PONG", params: [msg.params[0]] }); @@ -148,7 +178,7 @@ export default class Client extends EventTarget { break; case "ERROR": this.dispatchEvent(new CustomEvent("error", { detail: "Fatal IRC error: " + msg.params[0] })); - this.close(); + this.disconnect(); break; case irc.ERR_PASSWDMISMATCH: case irc.ERR_ERRONEUSNICKNAME: @@ -158,8 +188,8 @@ export default class Client extends EventTarget { case irc.ERR_NOPERMFORHOST: case irc.ERR_YOUREBANNEDCREEP: this.dispatchEvent(new CustomEvent("error", { detail: "Error (" + msg.command + "): " + msg.params[msg.params.length - 1] })); - if (!this.registered) { - this.close(); + if (this.status != Client.Status.REGISTERED) { + this.disconnect(); } break; } @@ -229,7 +259,7 @@ export default class Client extends EventTarget { this.requestCaps(reqCaps); - if (!this.registered && capEnd) { + if (this.status != Client.Status.REGISTERED && capEnd) { this.send({ command: "CAP", params: ["END"] }); } } @@ -261,7 +291,7 @@ export default class Client extends EventTarget { break; case "NAK": console.log("Server nak'ed caps:", args[0]); - if (!this.registered) { + if (this.status != Client.Status.REGISTERED) { this.send({ command: "CAP", params: ["END"] }); } break; @@ -287,11 +317,6 @@ export default class Client extends EventTarget { console.log("Sent:", msg); } - close() { - this.ws.close(1000); - this.registered = false; - } - /* Execute a command that expects a response. `done` is called with message * events until it returns a truthy value. */ roundtrip(msg, done) { diff --git a/state.js b/state.js index 0409e0d..9049d9c 100644 --- a/state.js +++ b/state.js @@ -1,3 +1,5 @@ +import Client from "/lib/client.js"; + export const SERVER_BUFFER = "*"; export const BufferType = { @@ -6,11 +8,7 @@ export const BufferType = { NICK: "nick", }; -export const Status = { - DISCONNECTED: "disconnected", - CONNECTING: "connecting", - REGISTERED: "registered", -}; +export const NetworkStatus = Client.Status; export const Unread = { NONE: "",