diff --git a/components/app.js b/components/app.js index 46b9cdf..5615824 100644 --- a/components/app.js +++ b/components/app.js @@ -577,6 +577,7 @@ export default class App extends Component { } this.createBuffer(serverID, buf.name); client.who(buf.name); + client.monitor(buf.name); } let lastReceipt = this.latestReceipt(ReceiptType.DELIVERED); @@ -785,6 +786,8 @@ export default class App extends Component { case irc.RPL_TOPICWHOTIME: case irc.RPL_NAMREPLY: case irc.RPL_ENDOFNAMES: + case irc.RPL_MONONLINE: + case irc.RPL_MONOFFLINE: case "AWAY": case "SETNAME": case "CAP": @@ -853,6 +856,7 @@ export default class App extends Component { client.send({ command: "JOIN", params: [target] }); } else { client.who(target); + client.monitor(target); this.createBuffer(serverID, target); this.switchBuffer({ server: serverID, name: target }); } @@ -928,6 +932,8 @@ export default class App extends Component { return { buffers }; }); + client.unmonitor(buf.name); + this.receipts.delete(buf.name); this.saveReceipts(); diff --git a/lib/client.js b/lib/client.js index 1622479..6bf4cbd 100644 --- a/lib/client.js +++ b/lib/client.js @@ -54,6 +54,7 @@ export default class Client extends EventTarget { pingIntervalID = null; pendingHistory = Promise.resolve(null); cm = irc.CaseMapping.RFC1459; + monitored = new irc.CaseMapMap(null, irc.CaseMapping.RFC1459); whoisDB = new irc.CaseMapMap(null, irc.CaseMapping.RFC1459); constructor(params) { @@ -97,6 +98,7 @@ export default class Client extends EventTarget { this.batches = new Map(); this.pendingHistory = Promise.resolve(null); this.isupport = new Map(); + this.monitored = new irc.CaseMapMap(null, irc.CaseMapping.RFC1459); if (this.autoReconnect) { if (!navigator.onLine) { @@ -199,6 +201,10 @@ export default class Client extends EventTarget { if (changed.indexOf("CASEMAPPING") >= 0) { this.setCaseMapping(this.isupport.get("CASEMAPPING")); } + if (changed.indexOf("MONITOR") >= 0 && this.isupport.has("MONITOR")) { + let targets = Array.from(this.monitored.keys()).slice(0, this.maxMonitorTargets()); + this.send({ command: "MONITOR", params: ["+", targets.join(",")] }); + } break; case irc.RPL_ENDOFMOTD: case irc.ERR_NOMOTD: @@ -478,6 +484,7 @@ export default class Client extends EventTarget { } this.whoisDB = new irc.CaseMapMap(this.whoisDB, this.cm); + this.monitored = new irc.CaseMapMap(this.monitored, this.cm); } isServer(name) { @@ -683,4 +690,40 @@ export default class Client extends EventTarget { return networks; }); } + + maxMonitorTargets() { + if (!this.isupport.has("MONITOR")) { + return 0; + } + return parseInt(this.isupport.get("MONITOR"), 10); + } + + monitor(target) { + if (this.monitored.has(target)) { + return; + } + + this.monitored.set(target, true); + + // TODO: add poll-based fallback when MONITOR is not supported + if (this.monitored.size + 1 > this.maxMonitorTargets()) { + return; + } + + this.send({ command: "MONITOR", params: ["+", target] }); + } + + unmonitor(target) { + if (!this.monitored.has(target)) { + return; + } + + this.monitored.delete(target); + + if (!this.isupport.has("MONITOR")) { + return; + } + + this.send({ command: "MONITOR", params: ["-", target] }); + } } diff --git a/lib/irc.js b/lib/irc.js index a45f970..5024e7d 100644 --- a/lib/irc.js +++ b/lib/irc.js @@ -42,6 +42,12 @@ export const ERR_UNAVAILRESOURCE = "437"; // Other export const RPL_QUIETLIST = "728"; export const RPL_ENDOFQUIETLIST = "729"; +// IRCv3 MONITOR: https://ircv3.net/specs/extensions/monitor +export const RPL_MONONLINE = "730"; +export const RPL_MONOFFLINE = "731"; +export const RPL_MONLIST = "732"; +export const RPL_ENDOFMONLIST = "733"; +export const ERR_MONLISTFULL = "734"; // IRCv3 SASL: https://ircv3.net/specs/extensions/sasl-3.1 export const RPL_LOGGEDIN = "900"; export const RPL_LOGGEDOUT = "901"; @@ -107,7 +113,7 @@ export function formatTags(tags) { return l.join(";"); } -function parsePrefix(s) { +export function parsePrefix(s) { let prefix = { name: null, user: null, @@ -306,6 +312,7 @@ export function isError(cmd) { case ERR_SASLTOOLONG: case ERR_SASLABORTED: case ERR_SASLALREADY: + case ERR_MONLISTFULL: return true; case "FAIL": return true; diff --git a/state.js b/state.js index c0f70d4..ab7ccbe 100644 --- a/state.js +++ b/state.js @@ -293,7 +293,7 @@ export const State = { return; } - let target, channel, topic; + let target, channel, topic, targets; switch (msg.command) { case irc.RPL_MYINFO: // TODO: parse available modes @@ -437,6 +437,30 @@ export const State = { return { members }; }); + case irc.RPL_MONONLINE: + targets = msg.params[1].split(","); + + for (let target of targets) { + let prefix = irc.parsePrefix(target); + let update = updateBuffer(prefix.name, (buf) => { + return { offline: false }; + }); + state = { ...state, ...update }; + } + + return state; + case irc.RPL_MONOFFLINE: + targets = msg.params[1].split(","); + + for (let target of targets) { + let prefix = irc.parsePrefix(target); + let update = updateBuffer(prefix.name, (buf) => { + return { offline: true }; + }); + state = { ...state, ...update }; + } + + return state; } }, addMessage(state, msg, bufID) {