diff --git a/client/js/socket-events/join.js b/client/js/socket-events/join.js index 1eaab574..31adfb19 100644 --- a/client/js/socket-events/join.js +++ b/client/js/socket-events/join.js @@ -10,11 +10,12 @@ const sidebar = $("#sidebar"); socket.on("join", function(data) { const id = data.network; const network = sidebar.find(`#network-${id}`); - network.append( - templates.chan({ - channels: [data.chan], - }) - ); + const channels = network.children(); + const position = $(channels[data.index || channels.length - 1]); // Put channel in correct position, or the end if we don't have one + const sidebarEntry = templates.chan({ + channels: [data.chan], + }); + $(sidebarEntry).insertAfter(position); chat.append( templates.chat({ channels: [data.chan], diff --git a/src/models/network.js b/src/models/network.js index aba3218b..f3c2a28b 100644 --- a/src/models/network.js +++ b/src/models/network.js @@ -128,6 +128,28 @@ Network.prototype.getNetworkStatus = function() { return status; }; +Network.prototype.addChannel = function(newChan) { + let index = this.channels.length; // Default to putting as the last item in the array + + // Don't sort special channels in amongst channels/users. + if (newChan.type === Chan.Type.CHANNEL || newChan.type === Chan.Type.QUERY) { + // We start at 1 so we don't test against the lobby + for (let i = 1; i < this.channels.length; i++) { + const compareChan = this.channels[i]; + + // Negative if the new chan is alphabetically before the next chan in the list, positive if after + if (newChan.name.localeCompare(compareChan.name, {sensitivity: "base"}) <= 0 + || (compareChan.type !== Chan.Type.CHANNEL && compareChan.type !== Chan.Type.QUERY)) { + index = i; + break; + } + } + } + + this.channels.splice(index, 0, newChan); + return index; +}; + Network.prototype.export = function() { const network = _.pick(this, [ "uuid", diff --git a/src/plugins/inputs/query.js b/src/plugins/inputs/query.js index 7c7875c1..7d8c787a 100644 --- a/src/plugins/inputs/query.js +++ b/src/plugins/inputs/query.js @@ -47,11 +47,12 @@ exports.input = function(network, chan, cmd, args) { type: Chan.Type.QUERY, name: target, }); - network.channels.push(newChan); + this.emit("join", { network: network.id, chan: newChan.getFilteredClone(true), shouldOpen: true, + index: network.addChannel(newChan), }); this.save(); newChan.loadMessages(this, network); diff --git a/src/plugins/irc-events/banlist.js b/src/plugins/irc-events/banlist.js index a0ee92b9..cd61843e 100644 --- a/src/plugins/irc-events/banlist.js +++ b/src/plugins/irc-events/banlist.js @@ -37,10 +37,10 @@ module.exports = function(irc, network) { type: Chan.Type.SPECIAL, name: chanName, }); - network.channels.push(chan); client.emit("join", { network: network.id, chan: chan.getFilteredClone(true), + index: network.addChannel(chan), }); } diff --git a/src/plugins/irc-events/join.js b/src/plugins/irc-events/join.js index 23d1b7e0..cba391a2 100644 --- a/src/plugins/irc-events/join.js +++ b/src/plugins/irc-events/join.js @@ -15,12 +15,13 @@ module.exports = function(irc, network) { name: data.channel, state: Chan.State.JOINED, }); - network.channels.push(chan); - client.save(); + client.emit("join", { network: network.id, chan: chan.getFilteredClone(true), + index: network.addChannel(chan), }); + client.save(); chan.loadMessages(client, network); diff --git a/src/plugins/irc-events/list.js b/src/plugins/irc-events/list.js index 0ddf2059..98cc6a83 100644 --- a/src/plugins/irc-events/list.js +++ b/src/plugins/irc-events/list.js @@ -46,10 +46,11 @@ module.exports = function(irc, network) { type: Chan.Type.SPECIAL, name: "Channel List", }); - network.channels.push(chan); + client.emit("join", { network: network.id, chan: chan.getFilteredClone(true), + index: network.addChannel(chan), }); } diff --git a/src/plugins/irc-events/message.js b/src/plugins/irc-events/message.js index 4187f646..f4fd2221 100644 --- a/src/plugins/irc-events/message.js +++ b/src/plugins/irc-events/message.js @@ -67,11 +67,13 @@ module.exports = function(irc, network) { type: Chan.Type.QUERY, name: target, }); - network.channels.push(chan); + client.emit("join", { network: network.id, chan: chan.getFilteredClone(true), + index: network.addChannel(chan), }); + client.save(); chan.loadMessages(client, network); } } diff --git a/src/plugins/irc-events/whois.js b/src/plugins/irc-events/whois.js index cdc02a7e..3f7e7355 100644 --- a/src/plugins/irc-events/whois.js +++ b/src/plugins/irc-events/whois.js @@ -13,13 +13,15 @@ module.exports = function(irc, network) { type: Chan.Type.QUERY, name: data.nick, }); - network.channels.push(chan); + client.emit("join", { shouldOpen: true, network: network.id, chan: chan.getFilteredClone(true), + index: network.addChannel(chan), }); chan.loadMessages(client, network); + client.save(); } let msg; diff --git a/test/models/network.js b/test/models/network.js index 95987258..900e4f8e 100644 --- a/test/models/network.js +++ b/test/models/network.js @@ -147,4 +147,194 @@ describe("Network", function() { ); }); }); + + describe("#addChannel(newChan)", function() { + it("should add channel", function() { + const chan = new Chan({name: "#thelounge"}); + + const network = new Network({ + channels: [ + chan, + ], + }); + // Lobby and initial channel + expect(network.channels.length).to.equal(2); + + const newChan = new Chan({name: "#freenode"}); + network.addChannel(newChan); + + expect(network.channels.length).to.equal(3); + }); + + it("should add channel alphabetically", function() { + const chan1 = new Chan({name: "#abc"}); + const chan2 = new Chan({name: "#thelounge"}); + const chan3 = new Chan({name: "#zero"}); + + const network = new Network({ + channels: [ + chan1, + chan2, + chan3, + ], + name: "freenode", + }); + + const newChan = new Chan({name: "#freenode"}); + network.addChannel(newChan); + + expect(network.channels[0].name).to.equal("freenode"); + expect(network.channels[1]).to.equal(chan1); + expect(network.channels[2]).to.equal(newChan); + expect(network.channels[3]).to.equal(chan2); + expect(network.channels[4]).to.equal(chan3); + }); + + it("should sort case-insensitively", function() { + const chan1 = new Chan({name: "#abc"}); + const chan2 = new Chan({name: "#THELOUNGE"}); + + const network = new Network({ + channels: [ + chan1, + chan2, + ], + }); + + const newChan = new Chan({name: "#freenode"}); + network.addChannel(newChan); + + expect(network.channels[1]).to.equal(chan1); + expect(network.channels[2]).to.equal(newChan); + expect(network.channels[3]).to.equal(chan2); + }); + + it("should sort users separately from channels", function() { + const chan1 = new Chan({name: "#abc"}); + const chan2 = new Chan({name: "#THELOUNGE"}); + + const network = new Network({ + channels: [ + chan1, + chan2, + ], + }); + + const newUser = new Chan({name: "mcinkay", type: Chan.Type.QUERY}); + network.addChannel(newUser); + + expect(network.channels[1]).to.equal(chan1); + expect(network.channels[2]).to.equal(chan2); + expect(network.channels[3]).to.equal(newUser); + }); + + it("should sort users alphabetically", function() { + const chan1 = new Chan({name: "#abc"}); + const chan2 = new Chan({name: "#THELOUNGE"}); + const user1 = new Chan({name: "astorije", type: Chan.Type.QUERY}); + const user2 = new Chan({name: "xpaw", type: Chan.Type.QUERY}); + + const network = new Network({ + channels: [ + chan1, + chan2, + user1, + user2, + ], + }); + + const newUser = new Chan({name: "mcinkay", type: Chan.Type.QUERY}); + network.addChannel(newUser); + + expect(network.channels[1]).to.equal(chan1); + expect(network.channels[2]).to.equal(chan2); + expect(network.channels[3]).to.equal(user1); + expect(network.channels[4]).to.equal(newUser); + expect(network.channels[5]).to.equal(user2); + }); + + it("should not sort special channels", function() { + const chan1 = new Chan({name: "#abc"}); + const chan2 = new Chan({name: "#THELOUNGE"}); + const user1 = new Chan({name: "astorije", type: Chan.Type.QUERY}); + const user2 = new Chan({name: "xpaw", type: Chan.Type.QUERY}); + + const network = new Network({ + channels: [ + chan1, + chan2, + user1, + user2, + ], + }); + + const newBanlist = new Chan({name: "Banlist for #THELOUNGE", type: Chan.Type.SPECIAL}); + network.addChannel(newBanlist); + + expect(network.channels[1]).to.equal(chan1); + expect(network.channels[2]).to.equal(chan2); + expect(network.channels[3]).to.equal(user1); + expect(network.channels[4]).to.equal(user2); + expect(network.channels[5]).to.equal(newBanlist); + }); + + it("should not compare against special channels", function() { + const chan1 = new Chan({name: "#abc"}); + const chan2 = new Chan({name: "#THELOUNGE"}); + const user1 = new Chan({name: "astorije", type: Chan.Type.QUERY}); + + const network = new Network({ + channels: [ + chan1, + chan2, + user1, + ], + }); + + const newBanlist = new Chan({name: "Banlist for #THELOUNGE", type: Chan.Type.SPECIAL}); + network.addChannel(newBanlist); + const newUser = new Chan({name: "mcinkay", type: Chan.Type.QUERY}); + network.addChannel(newUser); + + expect(network.channels[1]).to.equal(chan1); + expect(network.channels[2]).to.equal(chan2); + expect(network.channels[3]).to.equal(user1); + expect(network.channels[4]).to.equal(newUser); + expect(network.channels[5]).to.equal(newBanlist); + }); + + it("should insert before first special channel", function() { + const banlist = new Chan({name: "Banlist for #THELOUNGE", type: Chan.Type.SPECIAL}); + const chan1 = new Chan({name: "#thelounge"}); + const user1 = new Chan({name: "astorije", type: Chan.Type.QUERY}); + + const network = new Network({ + channels: [ + banlist, + chan1, + user1, + ], + }); + + const newChan = new Chan({name: "#freenode"}); + network.addChannel(newChan); + + expect(network.channels[1]).to.equal(newChan); + expect(network.channels[2]).to.equal(banlist); + expect(network.channels[3]).to.equal(chan1); + expect(network.channels[4]).to.equal(user1); + }); + + it("should never add something in front of the lobby", function() { + const network = new Network({ + name: "freenode", + channels: [], + }); + + const newUser = new Chan({name: "astorije"}); + network.addChannel(newUser); + + expect(network.channels[1]).to.equal(newUser); + }); + }); });