diff --git a/client/views/chat.tpl b/client/views/chat.tpl index 35ebce8c..b5b32d30 100644 --- a/client/views/chat.tpl +++ b/client/views/chat.tpl @@ -12,7 +12,7 @@ {{{parse topic}}}
-
+
diff --git a/src/client.js b/src/client.js index 65db1235..fe602bce 100644 --- a/src/client.js +++ b/src/client.js @@ -197,7 +197,7 @@ Client.prototype.connect = function(args) { client.networks.push(network); client.emit("network", { - networks: [network], + networks: [network.getFilteredClone(this.lastActiveChannel, -1)], }); if (config.lockNetwork) { diff --git a/src/models/chan.js b/src/models/chan.js index 63b45203..dbcdcd98 100644 --- a/src/models/chan.js +++ b/src/models/chan.js @@ -136,11 +136,39 @@ Chan.prototype.removeUser = function(user) { this.users.delete(user.nick.toLowerCase()); }; -Chan.prototype.toJSON = function() { - var clone = _.clone(this); - clone.users = []; // Do not send user list, the client will explicitly request it when needed - clone.messages = clone.messages.slice(-100); - return clone; +/** + * Get a clean clone of this channel that will be sent to the client. + * This function performs manual cloning of channel object for + * better control of performance and memory usage. + * + * @param {(int|bool)} lastActiveChannel - Last known active user channel id (needed to control how many messages are sent) + * If true, channel is assumed active. + * @param {int} lastMessage - Last message id seen by active client to avoid sending duplicates. + */ +Chan.prototype.getFilteredClone = function(lastActiveChannel, lastMessage) { + return Object.keys(this).reduce((newChannel, prop) => { + if (prop === "users") { + // Do not send users, client requests updated user list whenever needed + newChannel[prop] = []; + } else if (prop === "messages") { + // If channel is active, send up to 100 last messages, for all others send just 1 + // Client will automatically load more messages whenever needed based on last seen messages + const messagesToSend = lastActiveChannel === true || this.id === lastActiveChannel ? -100 : -1; + + // If client is reconnecting, only send new messages that client has not seen yet + if (lastMessage > -1) { + newChannel[prop] = this[prop] + .filter((m) => m.id > lastMessage) + .slice(messagesToSend); + } else { + newChannel[prop] = this[prop].slice(messagesToSend); + } + } else { + newChannel[prop] = this[prop]; + } + + return newChannel; + }, {}); }; function writeUserLog(client, msg) { diff --git a/src/models/network.js b/src/models/network.js index 7ad33776..61da4fed 100644 --- a/src/models/network.js +++ b/src/models/network.js @@ -7,6 +7,17 @@ module.exports = Network; let id = 1; +/** + * @type {Object} List of keys which should not be sent to the client. + */ +const filteredFromClient = { + awayMessage: true, + chanCache: true, + highlightRegex: true, + irc: true, + password: true, +}; + function Network(attr) { _.defaults(this, attr, { name: "", @@ -63,14 +74,27 @@ Network.prototype.setNick = function(nick) { ); }; -Network.prototype.toJSON = function() { - return _.omit(this, [ - "awayMessage", - "chanCache", - "highlightRegex", - "irc", - "password", - ]); +/** + * Get a clean clone of this network that will be sent to the client. + * This function performs manual cloning of network object for + * better control of performance and memory usage. + * + * Both of the parameters that are accepted by this function are passed into channels' getFilteredClone call. + * + * @see {@link Chan#getFilteredClone} + */ +Network.prototype.getFilteredClone = function(lastActiveChannel, lastMessage) { + return Object.keys(this).reduce((newNetwork, prop) => { + if (prop === "channels") { + // Channels objects perform their own cloning + newNetwork[prop] = this[prop].map((channel) => channel.getFilteredClone(lastActiveChannel, lastMessage)); + } else if (!filteredFromClient[prop]) { + // Some properties that are not useful for the client are skipped + newNetwork[prop] = this[prop]; + } + + return newNetwork; + }, {}); }; Network.prototype.export = function() { diff --git a/src/plugins/inputs/query.js b/src/plugins/inputs/query.js index 6d9d3a26..501b8bc1 100644 --- a/src/plugins/inputs/query.js +++ b/src/plugins/inputs/query.js @@ -47,6 +47,6 @@ exports.input = function(network, chan, cmd, args) { network.channels.push(newChan); this.emit("join", { network: network.id, - chan: newChan, + chan: newChan.getFilteredClone(true), }); }; diff --git a/src/plugins/irc-events/banlist.js b/src/plugins/irc-events/banlist.js index 9a7a119c..adac4cc0 100644 --- a/src/plugins/irc-events/banlist.js +++ b/src/plugins/irc-events/banlist.js @@ -29,7 +29,7 @@ module.exports = function(irc, network) { network.channels.push(chan); client.emit("join", { network: network.id, - chan: chan, + chan: chan.getFilteredClone(true), }); } diff --git a/src/plugins/irc-events/join.js b/src/plugins/irc-events/join.js index a781d059..6e53d42b 100644 --- a/src/plugins/irc-events/join.js +++ b/src/plugins/irc-events/join.js @@ -18,7 +18,7 @@ module.exports = function(irc, network) { client.save(); client.emit("join", { network: network.id, - chan: chan, + chan: chan.getFilteredClone(true), }); // Request channels' modes diff --git a/src/plugins/irc-events/list.js b/src/plugins/irc-events/list.js index 3122d1b8..0ddf2059 100644 --- a/src/plugins/irc-events/list.js +++ b/src/plugins/irc-events/list.js @@ -49,7 +49,7 @@ module.exports = function(irc, network) { network.channels.push(chan); client.emit("join", { network: network.id, - chan: chan, + chan: chan.getFilteredClone(true), }); } diff --git a/src/plugins/irc-events/message.js b/src/plugins/irc-events/message.js index 5ddcc0f3..894adaa0 100644 --- a/src/plugins/irc-events/message.js +++ b/src/plugins/irc-events/message.js @@ -68,7 +68,7 @@ module.exports = function(irc, network) { network.channels.push(chan); client.emit("join", { network: network.id, - chan: chan, + chan: chan.getFilteredClone(true), }); } } diff --git a/src/plugins/irc-events/whois.js b/src/plugins/irc-events/whois.js index 6747c2ed..d07836c0 100644 --- a/src/plugins/irc-events/whois.js +++ b/src/plugins/irc-events/whois.js @@ -17,7 +17,7 @@ module.exports = function(irc, network) { client.emit("join", { shouldOpen: true, network: network.id, - chan: chan, + chan: chan.getFilteredClone(true), }); } diff --git a/src/server.js b/src/server.js index 418fbc92..fc1089b1 100644 --- a/src/server.js +++ b/src/server.js @@ -420,24 +420,11 @@ function initializeClient(socket, client, token, lastMessage) { socket.join(client.id); const sendInitEvent = (tokenToSend) => { - let networks = client.networks; - - if (lastMessage > -1) { - // We need a deep cloned object because we are going to remove unneeded messages - networks = _.cloneDeep(networks); - - networks.forEach((network) => { - network.channels.forEach((channel) => { - channel.messages = channel.messages.filter((m) => m.id > lastMessage); - }); - }); - } - socket.emit("init", { applicationServerKey: manager.webPush.vapidKeys.publicKey, pushSubscription: client.config.sessions[token], active: client.lastActiveChannel, - networks: networks, + networks: client.networks.map((network) => network.getFilteredClone(client.lastActiveChannel, lastMessage)), token: tokenToSend, }); }; diff --git a/test/models/chan.js b/test/models/chan.js index 77dc1752..d9c2c182 100644 --- a/test/models/chan.js +++ b/test/models/chan.js @@ -145,4 +145,93 @@ describe("Chan", function() { ]); }); }); + + describe("#getFilteredClone(lastActiveChannel, lastMessage)", function() { + it("should send empty user list", function() { + const chan = new Chan(); + chan.setUser(new User({nick: "test"})); + + expect(chan.getFilteredClone().users).to.be.empty; + }); + + it("should keep necessary properties", function() { + const chan = new Chan(); + + expect(chan.getFilteredClone()).to.be.an("object").that.has.all.keys( + "firstUnread", + "highlight", + "id", + "key", + "messages", + "name", + "topic", + "type", + "unread", + "users" + ); + }); + + it("should send only last message for non active channel", function() { + const chan = new Chan({ + id: 1337, + messages: [ + new Msg({id: 10}), + new Msg({id: 11}), + new Msg({id: 12}), + new Msg({id: 13}), + ], + }); + + expect(chan.id).to.equal(1337); + + const messages = chan.getFilteredClone(999).messages; + + expect(messages).to.have.lengthOf(1); + expect(messages[0].id).to.equal(13); + }); + + it("should send more messages for active channel", function() { + const chan = new Chan({ + id: 1337, + messages: [ + new Msg({id: 10}), + new Msg({id: 11}), + new Msg({id: 12}), + new Msg({id: 13}), + ], + }); + + expect(chan.id).to.equal(1337); + + const messages = chan.getFilteredClone(1337).messages; + + expect(messages).to.have.lengthOf(4); + expect(messages[0].id).to.equal(10); + expect(messages[3].id).to.equal(13); + + expect(chan.getFilteredClone(true).messages).to.have.lengthOf(4); + }); + + it("should only send new messages", function() { + const chan = new Chan({ + id: 1337, + messages: [ + new Msg({id: 10}), + new Msg({id: 11}), + new Msg({id: 12}), + new Msg({id: 13}), + new Msg({id: 14}), + new Msg({id: 15}), + ], + }); + + expect(chan.id).to.equal(1337); + + const messages = chan.getFilteredClone(1337, 12).messages; + + expect(messages).to.have.lengthOf(3); + expect(messages[0].id).to.equal(13); + expect(messages[2].id).to.equal(15); + }); + }); }); diff --git a/test/models/network.js b/test/models/network.js index a8a97895..97354295 100644 --- a/test/models/network.js +++ b/test/models/network.js @@ -1,10 +1,10 @@ "use strict"; -var expect = require("chai").expect; - -var Chan = require("../../src/models/chan"); -var Msg = require("../../src/models/msg"); -var Network = require("../../src/models/network"); +const expect = require("chai").expect; +const Chan = require("../../src/models/chan"); +const Msg = require("../../src/models/msg"); +const User = require("../../src/models/user"); +const Network = require("../../src/models/network"); describe("Network", function() { describe("#export()", function() { @@ -91,4 +91,38 @@ describe("Network", function() { expect(network.channels[1].messages[2].text).to.equal("message after network creation"); }); }); + + describe("#getFilteredClone(lastActiveChannel, lastMessage)", function() { + it("should filter channels", function() { + const chan = new Chan(); + chan.setUser(new User({nick: "test"})); + + const network = new Network({ + channels: [ + chan, + ], + }); + + expect(network.channels[0].users).to.be.empty; + }); + + it("should keep necessary properties", function() { + const network = new Network(); + + expect(network.getFilteredClone()).to.be.an("object").that.has.all.keys( + "channels", + "commands", + "host", + "hostname", + "id", + "ip", + "name", + "port", + "realname", + "serverOptions", + "tls", + "username" + ); + }); + }); });