From 0eef5d92404bc2f153f516f3ec6f440699a657ee Mon Sep 17 00:00:00 2001 From: Maxime Poulin Date: Sat, 11 Jun 2016 22:16:17 -0400 Subject: [PATCH 0001/3926] Add touch slideout menu for mobile --- client/css/style.css | 31 +++++++++--- client/js/libs/slideout.js | 99 ++++++++++++++++++++++++++++++++++++++ client/js/lounge.js | 22 +++++---- 3 files changed, 137 insertions(+), 15 deletions(-) create mode 100644 client/js/libs/slideout.js diff --git a/client/css/style.css b/client/css/style.css index cb3d324a..c9cc8e02 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -303,13 +303,26 @@ button { #viewport { height: 100%; - transition: all .4s; + transition: transform 160ms, -webkit-transform 160ms; -webkit-transform: translateZ(0); transform: translateZ(0); -webkit-perspective: 1000; perspective: 1000; } +#viewport.menu-open { + -webkit-transform: translate3d(220px, 0, 0); + transform: translate3d(220px, 0, 0); +} + +#viewport.menu-dragging { + transition: none !important; +} + +#viewport.menu-open .messages { + pointer-events: none; +} + #viewport .lt, #viewport .rt, #chat button.menu { @@ -1595,11 +1608,6 @@ button { margin-top: 60px !important; } - #viewport.lt { - -webkit-transform: translate3d(220px, 0, 0); - transform: translate3d(220px, 0, 0); - } - #viewport.rt #chat .sidebar { -webkit-transform: translate3d(-180px, 0, 0); transform: translate3d(-180px, 0, 0); @@ -1645,6 +1653,17 @@ button { } } +@media (min-width: 769px) { + #viewport { + -webkit-transform: none !important; + transform: none !important; + } + + #viewport.menu-open { + transition: none; + } +} + @media (max-width: 479px) { .container { margin: 40px 0 !important; diff --git a/client/js/libs/slideout.js b/client/js/libs/slideout.js new file mode 100644 index 00000000..330c89b3 --- /dev/null +++ b/client/js/libs/slideout.js @@ -0,0 +1,99 @@ +/** + * Simple slideout menu implementation. + */ +function slideoutMenu(viewport, menu) { + var touchStartPos = null; + var touchCurPos = null; + var touchStartTime = 0; + var menuWidth = parseFloat(window.getComputedStyle(menu).width); + var menuIsOpen = false; + var menuIsMoving = false; + + function toggleMenu(state) { + menuIsOpen = state; + viewport.classList.toggle("menu-open", state); + } + + function disableSlideout() { + viewport.removeEventListener("ontouchstart", onTouchStart); + } + + function onTouchStart(e) { + if (e.touches.length !== 1) { + onTouchEnd(); + return false; + } + + var touch = e.touches.item(0); + viewport.classList.toggle("menu-dragging", true); + + if ((!menuIsOpen && touch.screenX < 50) || (menuIsOpen && touch.screenX > menuWidth)) { + touchStartPos = touch; + touchCurPos = touch; + touchStartTime = Date.now(); + + viewport.addEventListener("touchmove", onTouchMove); + viewport.addEventListener("touchend", onTouchEnd); + } + } + + function onTouchMove(e) { + var touch = touchCurPos = e.touches.item(0); + var setX = touch.screenX - touchStartPos.screenX; + + if (Math.abs(setX > 30)) { + menuIsMoving = true; + } + + if (!menuIsMoving && Math.abs(touch.screenY - touchStartPos.screenY) > 30) { + onTouchEnd(); + return; + } + + if (menuIsOpen) { + setX += menuWidth; + } + + if (setX > menuWidth) { + setX = menuWidth; + } else if (setX < 0) { + setX = 0; + } + + viewport.style.transform = "translate3d(" + setX + "px, 0, 0)"; + + if (menuIsMoving) { + e.preventDefault(); + e.stopPropagation(); + } + } + + function onTouchEnd() { + var diff = touchCurPos.screenX - touchStartPos.screenX; + var absDiff = Math.abs(diff); + + if (absDiff > menuWidth / 2 || Date.now() - touchStartTime < 180 && absDiff > 50) { + toggleMenu(diff > 0); + } + + viewport.removeEventListener("touchmove", onTouchMove); + viewport.removeEventListener("touchend", onTouchEnd); + viewport.classList.toggle("menu-dragging", false); + viewport.style.transform = null; + + touchStartPos = null; + touchCurPos = null; + touchStartTime = 0; + menuIsMoving = false; + } + + viewport.addEventListener("touchstart", onTouchStart); + + return { + disable: disableSlideout, + toggle: toggleMenu, + isOpen: function() { + return menuIsOpen; + } + }; +} diff --git a/client/js/lounge.js b/client/js/lounge.js index b537541a..5c2ac8f0 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -542,19 +542,22 @@ $(function() { }); var viewport = $("#viewport"); + var sidebarSlide = window.slideoutMenu(viewport[0], sidebar[0]); var contextMenuContainer = $("#context-menu-container"); var contextMenu = $("#context-menu"); - viewport.on("click", ".lt, .rt", function(e) { + $("#main").on("click", function(e) { + if ($(e.target).is(".lt")) { + sidebarSlide.toggle(!sidebarSlide.isOpen()); + } else if (sidebarSlide.isOpen()) { + sidebarSlide.toggle(false); + } + }); + + viewport.on("click", ".rt", function(e) { var self = $(this); viewport.toggleClass(self.attr("class")); - if (viewport.is(".lt, .rt")) { - e.stopPropagation(); - chat.find(".chat").one("click", function(e) { - e.stopPropagation(); - viewport.removeClass("lt"); - }); - } + e.stopPropagation(); }); function positionContextMenu(that, e) { @@ -761,7 +764,8 @@ $(function() { toggleNotificationMarkers(false); } - viewport.removeClass("lt"); + sidebarSlide.toggle(false); + var lastActive = $("#windows > .active"); lastActive From 1d47290ada0a01cc0144444da6760bfac12821c2 Mon Sep 17 00:00:00 2001 From: Maxime Poulin Date: Wed, 9 Mar 2016 22:04:07 +0200 Subject: [PATCH 0002/3926] Implement /list Thanks to @xPaw for the base of this code --- client/css/style.css | 54 +++++++++++++++++++++++++ client/js/lounge.js | 3 +- client/views/actions/channel_list.tpl | 18 +++++++++ src/client.js | 1 + src/models/chan.js | 3 +- src/plugins/irc-events/list.js | 57 +++++++++++++++++++++++++++ 6 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 client/views/actions/channel_list.tpl create mode 100644 src/plugins/irc-events/list.js diff --git a/client/css/style.css b/client/css/style.css index cb3d324a..b2defc9a 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -222,6 +222,9 @@ button { #sidebar .chan.channel:before, #chat .channel .title:before { content: "\f0f6"; /* http://fontawesome.io/icon/file-text-o/ */ } +#sidebar .chan.special:before, +#chat .channel .title:before { content: "\f03a"; /* http://fontawesome.io/icon/list/ */ } + #footer .sign-in:before { content: "\f023"; /* http://fontawesome.io/icon/lock/ */ } #footer .connect:before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ } #footer .settings:before { content: "\f013"; /* http://fontawesome.io/icon/cog/ */ } @@ -750,6 +753,10 @@ button { right: 180px; } +#chat .special { + bottom: -47px; +} + #viewport.rt .chat { right: 0; } @@ -769,11 +776,13 @@ button { } #chat .lobby .chat, +#chat .special .chat, #chat .query .chat { right: 0; } #chat .lobby .sidebar, +#chat .special .sidebar, #chat .query .sidebar { display: none; } @@ -876,6 +885,11 @@ button { } #loading a, +#chat .special .time, +#chat .special .from { + display: none; +} + #chat a { color: #50a656; } @@ -941,6 +955,46 @@ button { overflow: hidden; } +#chat .msg.channel_list_loading .text { + color: #999; + font-style: italic; + padding-left: 20px; +} + +#chat .msg.channel_list_truncated .text { + color: #f00; + padding-left: 20px; +} + +#chat table.channel-list { + margin: 5px 10px; + width: calc(100% - 30px); +} + +#chat table.channel-list th, +#chat table.channel-list td { + padding: 5px; + vertical-align: top; + border-bottom: #eee 1px solid; +} + +#chat table.channel-list .channel, +#chat table.channel-list .topic { + text-align: left; +} + +#chat table.channel-list .users { + text-align: center; +} + +#chat table.channel-list td.channel .inline-channel { + color: #428bca; +} + +#chat table.channel-list td { + color: #555; +} + #chat.hide-join .join, #chat.hide-mode .mode, #chat.hide-motd .motd, diff --git a/client/js/lounge.js b/client/js/lounge.js index b537541a..b8008858 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -243,6 +243,7 @@ $(function() { "action", "whois", "ctcp", + "channel_list", ].indexOf(type) !== -1) { data.msg.template = "actions/" + type; template = "msg_action"; @@ -601,7 +602,7 @@ $(function() { output += render("contextmenu_divider"); output += render("contextmenu_item", { class: "close", - text: target.hasClass("lobby") ? "Disconnect" : target.hasClass("query") ? "Close" : "Leave", + text: target.hasClass("lobby") ? "Disconnect" : target.hasClass("channel") ? "Leave" : "Close", data: target.data("target") }); } diff --git a/client/views/actions/channel_list.tpl b/client/views/actions/channel_list.tpl new file mode 100644 index 00000000..8fab73d0 --- /dev/null +++ b/client/views/actions/channel_list.tpl @@ -0,0 +1,18 @@ + + + + + + + + + + {{#each channels}} + + + + + + {{/each}} + +
ChannelUsersTopic
{{{parse channel}}}{{num_users}}{{{parse topic}}}
diff --git a/src/client.js b/src/client.js index e682dfad..28c4da3d 100644 --- a/src/client.js +++ b/src/client.js @@ -29,6 +29,7 @@ var events = [ "quit", "topic", "welcome", + "list", "whois" ]; var inputs = [ diff --git a/src/models/chan.js b/src/models/chan.js index e64ac8ac..750e798a 100644 --- a/src/models/chan.js +++ b/src/models/chan.js @@ -6,7 +6,8 @@ module.exports = Chan; Chan.Type = { CHANNEL: "channel", LOBBY: "lobby", - QUERY: "query" + QUERY: "query", + SPECIAL: "special", }; var id = 0; diff --git a/src/plugins/irc-events/list.js b/src/plugins/irc-events/list.js new file mode 100644 index 00000000..66c482a1 --- /dev/null +++ b/src/plugins/irc-events/list.js @@ -0,0 +1,57 @@ +var Chan = require("../../models/chan"); +var Msg = require("../../models/msg"); + +module.exports = function(irc, network) { + var client = this; + var chanCache = {}; + var MAX_CHANS = 1000; + + irc.on("channel list start", function() { + chanCache[network.id] = []; + + updateListStatus(new Msg({ + text: "Loading channel list, this can take a moment...", + type: "channel_list_loading" + })); + }); + + irc.on("channel list", function(channels) { + Array.prototype.push.apply(chanCache[network.id], channels); + }); + + irc.on("channel list end", function() { + updateListStatus(new Msg({ + type: "channel_list", + channels: chanCache[network.id].slice(0, MAX_CHANS) + })); + + if (chanCache[network.id].length > MAX_CHANS) { + updateListStatus(new Msg({ + type: "channel_list_truncated", + text: "Channel list is too large: truncated to " + MAX_CHANS + " channels." + })); + } + + chanCache[network.id] = []; + }); + + function updateListStatus(msg) { + var chan = network.getChannel("Channel List"); + if (typeof chan === "undefined") { + chan = new Chan({ + type: Chan.Type.SPECIAL, + name: "Channel List" + }); + network.channels.push(chan); + client.emit("join", { + network: network.id, + chan: chan + }); + } + + client.emit("msg", { + chan: chan.id, + msg: msg + }); + } +}; From 0f439545d48b14fea1b5dd71c274391d34813ec2 Mon Sep 17 00:00:00 2001 From: Maxime Poulin Date: Sat, 28 May 2016 22:07:34 -0400 Subject: [PATCH 0003/3926] Fix broken IRC servers with /list --- src/client.js | 1 + src/models/network.js | 1 + src/plugins/inputs/list.js | 7 +++++++ src/plugins/irc-events/list.js | 11 +++++------ 4 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 src/plugins/inputs/list.js diff --git a/src/client.js b/src/client.js index 28c4da3d..0f46db1e 100644 --- a/src/client.js +++ b/src/client.js @@ -47,6 +47,7 @@ var inputs = [ "quit", "raw", "topic", + "list", ].reduce(function(plugins, name) { var path = "./plugins/inputs/" + name; var plugin = require(path); diff --git a/src/models/network.js b/src/models/network.js index 65303756..cbeea7d4 100644 --- a/src/models/network.js +++ b/src/models/network.js @@ -23,6 +23,7 @@ function Network(attr) { serverOptions: { PREFIX: [], }, + chanCache: [], }, attr)); this.name = attr.name || prettify(attr.host); this.channels.unshift( diff --git a/src/plugins/inputs/list.js b/src/plugins/inputs/list.js new file mode 100644 index 00000000..70a4a65f --- /dev/null +++ b/src/plugins/inputs/list.js @@ -0,0 +1,7 @@ +exports.commands = ["list"]; + +exports.input = function(network, chan, cmd, args) { + network.chanCache = []; + network.irc.list(args); + return true; +}; diff --git a/src/plugins/irc-events/list.js b/src/plugins/irc-events/list.js index 66c482a1..db6fdd3c 100644 --- a/src/plugins/irc-events/list.js +++ b/src/plugins/irc-events/list.js @@ -3,11 +3,10 @@ var Msg = require("../../models/msg"); module.exports = function(irc, network) { var client = this; - var chanCache = {}; var MAX_CHANS = 1000; irc.on("channel list start", function() { - chanCache[network.id] = []; + network.chanCache = []; updateListStatus(new Msg({ text: "Loading channel list, this can take a moment...", @@ -16,23 +15,23 @@ module.exports = function(irc, network) { }); irc.on("channel list", function(channels) { - Array.prototype.push.apply(chanCache[network.id], channels); + Array.prototype.push.apply(network.chanCache, channels); }); irc.on("channel list end", function() { updateListStatus(new Msg({ type: "channel_list", - channels: chanCache[network.id].slice(0, MAX_CHANS) + channels: network.chanCache.slice(0, MAX_CHANS) })); - if (chanCache[network.id].length > MAX_CHANS) { + if (network.chanCache.length > MAX_CHANS) { updateListStatus(new Msg({ type: "channel_list_truncated", text: "Channel list is too large: truncated to " + MAX_CHANS + " channels." })); } - chanCache[network.id] = []; + network.chanCache = []; }); function updateListStatus(msg) { From 987474cfc1ef5e6d4138ad65baf57e9097cb7549 Mon Sep 17 00:00:00 2001 From: Johan Lindskogen Date: Fri, 29 Jul 2016 21:20:38 -0400 Subject: [PATCH 0004/3926] implementing LDAP support --- client/index.html | 2 +- defaults/config.js | 38 +++++++++++++++++ package.json | 3 +- src/clientManager.js | 4 +- src/command-line/start.js | 2 +- src/server.js | 86 +++++++++++++++++++++++++++++++-------- 6 files changed, 112 insertions(+), 23 deletions(-) diff --git a/client/index.html b/client/index.html index edf9ee33..f59043b9 100644 --- a/client/index.html +++ b/client/index.html @@ -292,7 +292,7 @@ - <% if (!public) { %> + <% if (!public && !ldap.enable) { %>
diff --git a/defaults/config.js b/defaults/config.js index 241d0e94..254d2a4a 100644 --- a/defaults/config.js +++ b/defaults/config.js @@ -321,4 +321,42 @@ module.exports = { // @default null // oidentd: null, + + // + // LDAP authentication settings (only available if public=false) + // @type object + // @default {} + // + ldap: { + // + // Enable LDAP user authentication + // + // @type boolean + // @default false + // + enable: false, + + // + // LDAP server URL + // + // @type string + // + url: "ldaps://example.com", + + // + // LDAP base dn + // + // @type string + // + baseDN: "ou=accounts,dc=example,dc=com", + + // + // LDAP primary key + // + // @type string + // @default "uid" + // + primaryKey: "uid" + } + }; diff --git a/package.json b/package.json index 7f6432e0..ea48cb7a 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "request": "2.72.0", "semver": "5.1.0", "socket.io": "1.4.5", - "spdy": "3.3.2" + "spdy": "3.3.2", + "ldapjs": "1.0.0" }, "devDependencies": { "chai": "3.5.0", diff --git a/src/clientManager.js b/src/clientManager.js index a9b61bfd..dadaa774 100644 --- a/src/clientManager.js +++ b/src/clientManager.js @@ -14,10 +14,10 @@ function ClientManager() { } } -ClientManager.prototype.findClient = function(name) { +ClientManager.prototype.findClient = function(name, token) { for (var i in this.clients) { var client = this.clients[i]; - if (client.name === name) { + if (client.name === name || (token && token === client.token)) { return client; } } diff --git a/src/command-line/start.js b/src/command-line/start.js index e6c37ef1..4386a2fb 100644 --- a/src/command-line/start.js +++ b/src/command-line/start.js @@ -21,7 +21,7 @@ program mode = false; } - if (!mode && !users.length) { + if (!mode && !users.length && !Helper.config.ldap.enable) { log.warn("No users found!"); log.info("Create a new user with 'lounge add '."); diff --git a/src/server.js b/src/server.js index c542c020..3199966d 100644 --- a/src/server.js +++ b/src/server.js @@ -10,8 +10,11 @@ var fs = require("fs"); var io = require("socket.io"); var dns = require("dns"); var Helper = require("./helper"); +var ldap = require("ldapjs"); var manager = null; +var ldapclient = null; +var authFunction = localAuth; module.exports = function() { manager = new ClientManager(); @@ -24,6 +27,10 @@ module.exports = function() { var config = Helper.config; var server = null; + if (config.public && (config.ldap || {}).enable) { + throw "Server is public and set to use LDAP. Please disable public if trying to use LDAP authentication."; + } + if (!config.https.enable) { server = require("http"); server = server.createServer(app).listen(config.port, config.host); @@ -43,6 +50,13 @@ module.exports = function() { require("./identd").start(config.identd.port); } + if ((config.ldap || {}).enable) { + ldapclient = ldap.createClient({ + url: config.ldap.url + }); + authFunction = ldapAuth; + } + var sockets = io(server, { transports: config.transports }); @@ -138,7 +152,7 @@ function init(socket, client) { client.connect(data); } ); - if (!Helper.config.public) { + if (!Helper.config.public && !Helper.config.ldap.enable) { socket.on( "change-password", function(data) { @@ -223,10 +237,39 @@ function reverseDnsLookup(socket, client) { }); } +function localAuth(client, user, password, callback) { + var result = false; + try { + result = bcrypt.compareSync(password || "", client.config.password); + } catch (error) { + if (error === "Not a valid BCrypt hash.") { + console.error("User (" + user + ") with no local password set tried signed in. (Probably a ldap user)"); + } + result = false; + } finally { + callback(result); + } +} + +function ldapAuth(client, user, password, callback) { + var userDN = user.replace(/([,\\\/#+<>;"= ])/g, "\\$1"); + var bindDN = Helper.config.ldap.primaryKey + "=" + userDN + "," + Helper.config.ldap.baseDN; + + ldapclient.bind(bindDN, password, function(err) { + if (!err && !client) { + if (!manager.addUser(user, null)) { + console.log("Unable to create new user", user); + } + } + callback(!err); + }); +} + function auth(data) { var socket = this; + var client; if (Helper.config.public) { - var client = new Client(manager); + client = new Client(manager); manager.clients.push(client); socket.on("disconnect", function() { manager.clients = _.without(manager.clients, client); @@ -238,28 +281,35 @@ function auth(data) { init(socket, client); } } else { - var success = false; - _.each(manager.clients, function(client) { - if (data.token) { - if (data.token === client.config.token) { - success = true; - } - } else if (client.config.user === data.user) { - if (bcrypt.compareSync(data.password || "", client.config.password)) { - success = true; - } - } + client = manager.findClient(data.user, data.token); + var signedIn = data.token && client && client.token === data.token; + var token; + + if (data.remember || data.token) { + token = client.token; + } + + var authCallback = function(success) { if (success) { + if (!client) { + // LDAP just created a user + manager.loadUser(data.user); + client = manager.findClient(data.user); + } if (Helper.config.webirc !== null && !client.config["ip"]) { reverseDnsLookup(socket, client); } else { - init(socket, client); + init(socket, client, token); } - return false; + } else { + socket.emit("auth", {success: success}); } - }); - if (!success) { - socket.emit("auth", {success: success}); + }; + + if (signedIn) { + authCallback(true); + } else { + authFunction(client, data.user, data.password, authCallback); } } } From d42ac23c55d36b5307e039024fdbe8708f8ed888 Mon Sep 17 00:00:00 2001 From: Maxime Poulin Date: Sat, 30 Jul 2016 20:54:09 -0400 Subject: [PATCH 0005/3926] Fix webirc and 4-in-6 addresses --- src/server.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/server.js b/src/server.js index c542c020..67d7d847 100644 --- a/src/server.js +++ b/src/server.js @@ -70,11 +70,15 @@ module.exports = function() { }; function getClientIp(req) { + var ip; + if (!Helper.config.reverseProxy) { - return req.connection.remoteAddress; + ip = req.connection.remoteAddress; } else { - return req.headers["x-forwarded-for"] || req.connection.remoteAddress; + ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; } + + return ip.replace(/^::ffff:/, ""); } function allRequests(req, res, next) { From 2041c936b2806cc6d389a6612475f926dc254dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 3 Aug 2016 01:10:22 -0400 Subject: [PATCH 0006/3926] Add tooltips to user list and submit buttons --- client/css/style.css | 5 ++++- client/index.html | 4 +++- client/views/chat.tpl | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index a9449a68..876f065f 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -346,9 +346,12 @@ button { opacity: 1; } +#viewport .rt-tooltip { + float: right; +} + #viewport .rt { display: block; - float: right; margin: 6px -12px 0 0; } diff --git a/client/index.html b/client/index.html index edf9ee33..75e57613 100644 --- a/client/index.html +++ b/client/index.html @@ -61,7 +61,9 @@
- + + +
diff --git a/client/views/chat.tpl b/client/views/chat.tpl index 94ace5e1..9ccb6391 100644 --- a/client/views/chat.tpl +++ b/client/views/chat.tpl @@ -3,7 +3,9 @@
{{#equal type "channel"}} - + + + {{/equal}} {{name}} From e99bf9ac0a16735189e477e8b9d476c07f27e4fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 3 Aug 2016 01:11:10 -0400 Subject: [PATCH 0007/3926] Make sure body fonts are used for tooltips --- client/css/style.css | 2 +- client/themes/crypto.css | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/client/css/style.css b/client/css/style.css index 876f065f..0a36f62e 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1371,7 +1371,7 @@ button { visibility: hidden; opacity: 0; padding: 5px 8px; - font-size: 12px; + font: 12px Lato; line-height: 1.2; color: #262c36; text-align: center; diff --git a/client/themes/crypto.css b/client/themes/crypto.css index 51046c54..f561d7b2 100644 --- a/client/themes/crypto.css +++ b/client/themes/crypto.css @@ -129,6 +129,10 @@ a:hover, color: #666; } +.tooltipped:after { + font-family: Inconsolata-g, monospace; +} + @media (max-width: 768px) { #main { left: 0; From 83d1a99608bacdab7bb1bbbd8b700a5a7a64fe3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 3 Aug 2016 01:11:50 -0400 Subject: [PATCH 0008/3926] Set tooltip colors to be readable on both light and dark backgrounds --- client/css/style.css | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 0a36f62e..7663d889 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1373,7 +1373,7 @@ button { padding: 5px 8px; font: 12px Lato; line-height: 1.2; - color: #262c36; + color: #fff; text-align: center; text-decoration: none; text-shadow: none; @@ -1383,7 +1383,7 @@ button { white-space: pre; pointer-events: none; content: attr(aria-label); - background: #fff; + background: #222; border-radius: 3px; -webkit-font-smoothing: subpixel-antialiased; transition: .2s; @@ -1397,7 +1397,7 @@ button { opacity: 0; width: 0; height: 0; - color: #262c36; + color: #fff; pointer-events: none; content: ""; border: 5px solid transparent; @@ -1430,7 +1430,7 @@ button { right: 50%; bottom: -5px; margin-right: -5px; - border-bottom-color: #fff; + border-bottom-color: #222; } .tooltipped-se:after { @@ -1458,7 +1458,7 @@ button { right: 50%; bottom: auto; margin-right: -5px; - border-top-color: #fff; + border-top-color: #222; } .tooltipped-ne:after { @@ -1492,7 +1492,7 @@ button { bottom: 50%; left: -5px; margin-top: -5px; - border-left-color: #fff; + border-left-color: #222; } .tooltipped-e:after { @@ -1509,9 +1509,11 @@ button { right: -5px; bottom: 50%; margin-top: -5px; - border-right-color: #fff; + border-right-color: #222; } +/* End tooltips */ + /** * IRC Message Styling * https://github.com/megawac/irc-style-parser From a6ffffa71533bc7bb9a7a08e1569bb686be6b3ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sun, 7 Aug 2016 18:02:30 -0400 Subject: [PATCH 0009/3926] Fix super minor typo in change log entry title --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af6ca99d..97d1c741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ All sections are explained on the link above, they are all optional, and each of Don't forget to thank the PR authors in a commit comment, and copy/paste the release content as-is in GitHub releases: https://github.com/thelounge/lounge/releases --> -## 2.0.0-pre.5 - 2016-08-07 [Pre-release] +## v2.0.0-pre.5 - 2016-08-07 [Pre-release] [See the full changelog](https://github.com/thelounge/lounge/compare/v2.0.0-pre.4...v2.0.0-pre.5) From e6800c5aceb15082a6be516d2883eb084092839f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 10 Aug 2016 01:05:53 -0400 Subject: [PATCH 0010/3926] Add change log entry for upcoming v2.0.0-pre.6 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97d1c741..f30591df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,18 @@ All sections are explained on the link above, they are all optional, and each of Don't forget to thank the PR authors in a commit comment, and copy/paste the release content as-is in GitHub releases: https://github.com/thelounge/lounge/releases --> +## v2.0.0-pre.6 - 2016-08-10 [Pre-release] + +[See the full changelog](https://github.com/thelounge/lounge/compare/v2.0.0-pre.5...v2.0.0-pre.6) + +LDAP! That's all there is to be found in this pre-release, but it should please some administrators out there. Big thanks to [@thisisdarshan](https://github.com/thisisdarshan) and [@lindskogen](https://github.com/lindskogen) for sticking with us on this one. + +This feature will remain in beta version until the official v2.0.0 release. + +### Added + +- LDAP support ([#477](https://github.com/thelounge/lounge/pull/477) by [@thisisdarshan](https://github.com/thisisdarshan) and [@lindskogen](https://github.com/lindskogen)) + ## v2.0.0-pre.5 - 2016-08-07 [Pre-release] [See the full changelog](https://github.com/thelounge/lounge/compare/v2.0.0-pre.4...v2.0.0-pre.5) From facf30604538258f5cc3a5166b3e14df052aba3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 10 Aug 2016 01:06:05 -0400 Subject: [PATCH 0011/3926] 2.0.0-pre.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ea7bf984..723ca233 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "thelounge", "description": "The self-hosted Web IRC client", - "version": "2.0.0-pre.5", + "version": "2.0.0-pre.6", "publishConfig": { "tag": "next" }, From cf64cb04c493041756f1f3639f5b8d35dff21b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 10 Aug 2016 02:26:47 -0400 Subject: [PATCH 0012/3926] Fix token persistency across server restarts This fixes a regression introduced by LDAP support addition (https://github.com/thelounge/lounge/pull/477), which forces users to re-login when the server restarts. This was originally implemented in https://github.com/thelounge/lounge/pull/370. --- src/clientManager.js | 2 +- src/server.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/clientManager.js b/src/clientManager.js index dadaa774..4024fbea 100644 --- a/src/clientManager.js +++ b/src/clientManager.js @@ -17,7 +17,7 @@ function ClientManager() { ClientManager.prototype.findClient = function(name, token) { for (var i in this.clients) { var client = this.clients[i]; - if (client.name === name || (token && token === client.token)) { + if (client.name === name || (token && token === client.config.token)) { return client; } } diff --git a/src/server.js b/src/server.js index 3199966d..aa80788a 100644 --- a/src/server.js +++ b/src/server.js @@ -282,11 +282,11 @@ function auth(data) { } } else { client = manager.findClient(data.user, data.token); - var signedIn = data.token && client && client.token === data.token; + var signedIn = data.token && data.token === client.config.token; var token; if (data.remember || data.token) { - token = client.token; + token = client.config.token; } var authCallback = function(success) { From 14782a56b73aea3549d171200e1e2bd44abc12ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 10 Aug 2016 02:14:09 -0400 Subject: [PATCH 0013/3926] Use our logger instead of console.{log,error} --- src/server.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/server.js b/src/server.js index 3199966d..5c1d63f3 100644 --- a/src/server.js +++ b/src/server.js @@ -28,7 +28,7 @@ module.exports = function() { var server = null; if (config.public && (config.ldap || {}).enable) { - throw "Server is public and set to use LDAP. Please disable public if trying to use LDAP authentication."; + log.warn("Server is public and set to use LDAP. Set to private mode if trying to use LDAP authentication."); } if (!config.https.enable) { @@ -50,7 +50,7 @@ module.exports = function() { require("./identd").start(config.identd.port); } - if ((config.ldap || {}).enable) { + if (!config.public && (config.ldap || {}).enable) { ldapclient = ldap.createClient({ url: config.ldap.url }); @@ -243,7 +243,7 @@ function localAuth(client, user, password, callback) { result = bcrypt.compareSync(password || "", client.config.password); } catch (error) { if (error === "Not a valid BCrypt hash.") { - console.error("User (" + user + ") with no local password set tried signed in. (Probably a ldap user)"); + log.error("User (" + user + ") with no local password set tried to sign in. (Probably a LDAP user)"); } result = false; } finally { @@ -258,7 +258,7 @@ function ldapAuth(client, user, password, callback) { ldapclient.bind(bindDN, password, function(err) { if (!err && !client) { if (!manager.addUser(user, null)) { - console.log("Unable to create new user", user); + log.error("Unable to create new user", user); } } callback(!err); From dd02f0f029c5914e09df47d47c328f66d0efcf59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Thu, 11 Aug 2016 01:13:41 -0400 Subject: [PATCH 0014/3926] Make sure input height is reset when submitting with icon This is especially noticeable on mobile, where clicking Send icon is more natural. --- client/js/lounge.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 26656a4a..b0193469 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -631,6 +631,10 @@ $(function() { return false; }); + function resetInputHeight(input) { + input.style.height = input.style.minHeight; + } + var input = $("#input") .history() .on("input keyup", function() { @@ -638,7 +642,7 @@ $(function() { // Start by resetting height before computing as scrollHeight does not // decrease when deleting characters - this.style.height = this.style.minHeight; + resetInputHeight(this); this.style.height = Math.min( Math.round(window.innerHeight - 100), // prevent overflow @@ -662,6 +666,7 @@ $(function() { } input.val(""); + resetInputHeight(input.get(0)); if (text.indexOf("/clear") === 0) { clear(); From f2a0bc5d23c711d9381f1801a8d2096fb2d6cada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sun, 14 Aug 2016 17:28:47 -0400 Subject: [PATCH 0015/3926] Remove font family redundancy, fix missed fonts, remove Open Sans --- client/css/fonts/Open-Sans-300/LICENSE.txt | 202 -- .../css/fonts/Open-Sans-300/Open-Sans-300.eot | Bin 18759 -> 0 bytes .../css/fonts/Open-Sans-300/Open-Sans-300.svg | 1633 ---------------- .../css/fonts/Open-Sans-300/Open-Sans-300.ttf | Bin 35340 -> 0 bytes .../fonts/Open-Sans-300/Open-Sans-300.woff | Bin 14140 -> 0 bytes .../fonts/Open-Sans-300/Open-Sans-300.woff2 | Bin 10200 -> 0 bytes client/css/fonts/Open-Sans-700/LICENSE.txt | 202 -- .../css/fonts/Open-Sans-700/Open-Sans-700.eot | Bin 18866 -> 0 bytes .../css/fonts/Open-Sans-700/Open-Sans-700.svg | 1635 ---------------- .../css/fonts/Open-Sans-700/Open-Sans-700.ttf | Bin 35924 -> 0 bytes .../fonts/Open-Sans-700/Open-Sans-700.woff | Bin 14192 -> 0 bytes .../fonts/Open-Sans-700/Open-Sans-700.woff2 | Bin 10284 -> 0 bytes .../css/fonts/Open-Sans-regular/LICENSE.txt | 202 -- .../Open-Sans-regular/Open-Sans-regular.eot | Bin 18233 -> 0 bytes .../Open-Sans-regular/Open-Sans-regular.svg | 1637 ----------------- .../Open-Sans-regular/Open-Sans-regular.ttf | Bin 34156 -> 0 bytes .../Open-Sans-regular/Open-Sans-regular.woff | Bin 14260 -> 0 bytes .../Open-Sans-regular/Open-Sans-regular.woff2 | Bin 10352 -> 0 bytes client/css/style.css | 66 +- client/themes/crypto.css | 18 +- client/themes/morning.css | 13 +- client/themes/zenburn.css | 13 +- 22 files changed, 32 insertions(+), 5589 deletions(-) delete mode 100755 client/css/fonts/Open-Sans-300/LICENSE.txt delete mode 100755 client/css/fonts/Open-Sans-300/Open-Sans-300.eot delete mode 100755 client/css/fonts/Open-Sans-300/Open-Sans-300.svg delete mode 100755 client/css/fonts/Open-Sans-300/Open-Sans-300.ttf delete mode 100755 client/css/fonts/Open-Sans-300/Open-Sans-300.woff delete mode 100755 client/css/fonts/Open-Sans-300/Open-Sans-300.woff2 delete mode 100755 client/css/fonts/Open-Sans-700/LICENSE.txt delete mode 100755 client/css/fonts/Open-Sans-700/Open-Sans-700.eot delete mode 100755 client/css/fonts/Open-Sans-700/Open-Sans-700.svg delete mode 100755 client/css/fonts/Open-Sans-700/Open-Sans-700.ttf delete mode 100755 client/css/fonts/Open-Sans-700/Open-Sans-700.woff delete mode 100755 client/css/fonts/Open-Sans-700/Open-Sans-700.woff2 delete mode 100755 client/css/fonts/Open-Sans-regular/LICENSE.txt delete mode 100755 client/css/fonts/Open-Sans-regular/Open-Sans-regular.eot delete mode 100755 client/css/fonts/Open-Sans-regular/Open-Sans-regular.svg delete mode 100755 client/css/fonts/Open-Sans-regular/Open-Sans-regular.ttf delete mode 100755 client/css/fonts/Open-Sans-regular/Open-Sans-regular.woff delete mode 100755 client/css/fonts/Open-Sans-regular/Open-Sans-regular.woff2 diff --git a/client/css/fonts/Open-Sans-300/LICENSE.txt b/client/css/fonts/Open-Sans-300/LICENSE.txt deleted file mode 100755 index d6456956..00000000 --- a/client/css/fonts/Open-Sans-300/LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/client/css/fonts/Open-Sans-300/Open-Sans-300.eot b/client/css/fonts/Open-Sans-300/Open-Sans-300.eot deleted file mode 100755 index 5219d9e2dc2870d220a7f420d0b7712b616e2518..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18759 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

_ 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 bz3f`cPwP*XX%O3Q=ahEpk8mSauS51=8}dlE diff --git a/client/css/fonts/Open-Sans-300/Open-Sans-300.svg b/client/css/fonts/Open-Sans-300/Open-Sans-300.svg deleted file mode 100755 index 851d83c7..00000000 --- a/client/css/fonts/Open-Sans-300/Open-Sans-300.svg +++ /dev/null @@ -1,1633 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/css/fonts/Open-Sans-300/Open-Sans-300.ttf b/client/css/fonts/Open-Sans-300/Open-Sans-300.ttf deleted file mode 100755 index d7d7cd1fcc97d7aaeb1ffa373ea86d0c2ee13b5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35340 zcmbTf349bq_6J_o-P3d5Gnw3($>bn}5RxGYAwY&BAtVq&2#^U^xWf%%M7%%|kstyp z9)O65h=?#8A|MKgsHn)gF6(-Nx31{Au8Rsu=l`wh$$_i?-{=1i_M1Mct6sf&^{#r= z5=sbh07%3;cu+|xv5}7nS#>?`Mh&hSK9abJ0oV87+BA6Nr~x0p{mCX=KaA_^hmXw8 z-Fkl02tvlO=k-(OPFk?neBe=BU&8fUQ*OB4>-f(3G$GS>;(FKg1vBQ}HtCR^knx8J z(H@*JY0(0bNc^a8DxTYB%vmzMN9|ib5kk8WGU`~vv`JHcDLTE4klI)9Jih@q%y-Il zsJ{W%DGhV4zwy4K<1gcRGa=%f+0(9@_r!PGKPO~H4Izo^=FFcmX;IIWiG+;Nq0VD- zC*8O}c$EH1$mnnIzIWcFxznOo=B^`T#TR%!dcpif*FS8T1^ia_Aw=l1;JRrGKDl?> zctWPl!SmikASXd6gXRzsiIyY=O&UQG1yQe)L@g!Rf$V_YK?@7*0ec{)htp^G72r52 z4rzYAL0HnVQgi4^d4u#7swUf8FVRw7kC9{qojMWq2xb$d20=8)BSHozDb6I(#g4*k zR;4|_tE7H0AodL8x?TQc>gt=YcuiGxbroGA^}X`&nktc!^bxHAV$)&fO zOnQgaVw4O{rz=h|O-R4#l|b z#pdDWrRH;Hsn|Z;zSb_9?Pj~i;}i`Vkgqu4C@jp*9zP!4&9oEN=g2Rost45N#C3QI z?d-s{r!Y5H{mtn?MV}k}aq=V2=k(zSi2QI3b;Fgu^b|XW(|fDvt4eQnR4LcddolLgTUUt}WC!t* zbkd#N985R6qZ3mzWl7TbNEZju7#y*h9@#0D#LP@fWhm7w%CdP_NIu+p;y_eP8T$=- z%(k*_3GRepp=h^R&UCvoWwYebcu00OdX3d*UZQpSq$vc3BoLyt;S9xc7q&^WG-`eqCBuA+aD4Q0BhiqBt>4O$=1oY7DA1%E=|L)j={>T#XUF?I=^7)k z8|}XItZX?qC$VeS9-_sP-9yff*15E?DKX$m{9}wBu(PA1gKQe{iR6k3_AHewZ%Ct&{A|18eSn@cz}S z9#)#0&)18Yl5q3|+IaZLk;9*T_wSD`{PW+A8EJr?2!K?X54qj~Z5<9JlV7x$pdozP9ZA3?_slv3hkKHOjz0+=&E41QGc3Hz3IM%4{V`5=HI_j$f0`r_MSsZpp_^I z%A8&FRn*%T^?uk^uc$}8GW3_lEa^My4WMQqWfQc1Vz&wDfhY(4aMOMBX^$-rEMyfc zzbU(gbIGP_}Uy0UvPy;)f?nAW5(Z=f5{!hCW? z@=JS&k@O1s^#meAV>XFeowhneth#Jnv2LPnscx-qukN_+f=;g^+2gq!P{|Adh~^>Z z?0!4sn>`>br2Cc1YNd*XtA%Fum+nV9gOp!sfc!vYVt-DeI%on&7FG!=NcDg!jslu) zs+iJ$nC}O-^2oPz`Q(2SH2_fQ$@5wPi-$bmsZoI%q@#Owo)8$&A-kbaQ$XMAX$brl~5KlEQm2P%qm!C&dR%BB6qSmPAo-km6$A zkUH@x=CZJ@#&drk;0~# zigKhT8zvCA%Mr+RP!7QoxFaQf_rsSze0}NbpPEnXf9}O+pMCMgUBU%rsd6v9mEJ&a zqYISf%35W&@&c`*L+Bt{rG$b1Yd~Zd&@m8i(1IbN)MV7^X|$x5nK*N>xy^Jo7|`dp z_biZQO*#z-yC$2gUgg6=x@NSLv3|2P{>#A>vV)R^(6mf`Mw8rNtk&UyDd32fVq*jk zwHk(ntXk@+4AJQJevuC0ffjsuv>)U?y>}MrYyzSQ8EFPCBUK!WLu`>i6>62-$b+~ zkJL3o^h8Gk2WvDniqX-Aw6vsQp){?*HY{Z5AU7$D&Yn17JP6+jK`5#=JrxDiBwsRD zRk>Vz`SSwmNwN&w8S>)0&#Vg{R=!oPT-Z_b&bW_1`TV0TTMwOo_MW*17vzlk@#Z%^ zk|x}JG**r}`0&|JM^5RMd*F%no9mx_;HLZgmJb{}@;Epo6Vz}=2&lao$znU5d!^O;6b#Amk^_#uv^#^wnv zHsSn|W@U#(=bF*dOdFNev~i#$H=F4;+Wl+qH94>8~7xi^bRK3#JDI?9~C1d zCJ^Va5FxV16I&VbsHVZSRAjuX>UqreTnS`@{O?3JjI@&(JNZJ)KdD*y=JMQE2F%$x z@x(Wuy?n>LeR>NgTDD|u7UzHV>ZeySyUZxaeDm2Sj;5yzkJh&|CxAlBftMe1D4L{^ z%3#-cr{83P4s(iK(#^IgThj25)wa|o7(|;bN|N-$Ly|TsW>_fdS|tBqsJS3d3FLa* z;6@?__>h7haRKe6Dl8caENb~Cns8=9<&YV_DBs)7LtmMD_Ur$={fW{_FKj<_aMSd? z4O5;L=FxRZ(mee=*X*YAK!VbZbRk#vp0gmb1~LQps-9bgKn8n z4G}b=4)RC?9fTy?ij16x4M(N$$?zi;YjY*Eg2;0hPlwK8wi(*(bSsahlNakLZEA{ z8aAYu39^>Whlo8>!1aDU6uUYh(!sCEoKmWy7fO$QzGlmb%`N9YzO-Y{<9E(F{?iLP zx4-`A#lsuvv^CEZFTUrAd!N>vduiK(-9v^SyX(1sym4~*tEEGqzG3ELqpv>F^zh?N zw`>|Uc(=HE^^(%rV|o`qJp0Cbm^7V_{+hrEv6%JXgjBPcB)W*!C3elQcKZk zqPB;WkLZ;1NA?`7+c2#0twpRyTqj9c&`EYu7)&|ne} zM0WxAUZ9Ysyeqa>AqAMk$K1l?XR`{&d^CmT#K!7srGZ-t*I-OFf<^|$@*iUCR8gKU z764t;Lub(`bi+g?t*=rKd<$DIi7sfVB=}zagFc-*#S2+$b$g|ZE>di)gOD_b!&nt^ zNNLFlG|XTBQ2lsW)#94extn<0sXaxel}aCr?61$VF((Q8AO92H#IXc z7e*~Y3s6wwnM=&PByM4-(u{`&_3zE9%6qipz}0u>EpBz#hF`ZKES&#Bxin||)M+R8yg_M((j&29=C?1rOj90x zO?lvw@{;m+O3x`(bUkJ-p^tpFUwKY>Z0$N)NBwbU86VU7thHH9qNJlkLpl~J(TcDX+f{alF$7v*Na6p}08B@pn3gVU#kDPa-dZRO?Joou zwde%nYcc*fuOPKFYmQwRB-9Jme)H-p!TsmZ{(5KUuEvslb z&4L!Z9a8)-=0p@p3%W#$OJ{+-<{(2c35;@i$k;InxC#V~7z2PABD~O(@d6?SVzKhQ z@(S%r-G?`>e_we^+43~)``5oeov~FjU)id>t-Pk}$gT^}d+D?bwB|_FxGFYspqwTZ zl+z)02#eez!K)#5hsHcKq!A^{(2#`D`d^Ej_z)cP5)pX7kMU|!mD`jiWgA^hC(ya` zm6OVw%71lB2}l>3xNX>Z#1ioz@_v?>08HjShtm?|3#6hdA{MYBoPmxrt} z#s~^j5H#f>L8ONA5bZ#rV~is902>{K3!Ex`h(9!cDkilI7Vc}ACmhinn$YszgqAgJ ztulXB4-WOoxFcz)XfUd67R$mltukjuH9aCmafkT<-%V|d=1muUEhWMOEpu2q4@_jO zU^F!uTqnb7bZVpF4a91txOfs<0jp6m8f`;EMoEeSVLJwv_39d{(Px)IU3=Tm@+r?o z-0l-CLfqe!Uz8on+UM!0kN@>j#!GK07nEn|0NU$~HOesI)0SyUd&pE%_Ug7HwU8asK`Lm2#=(`dh9XYopg?;9(`*gKi3&P%q0`7*saVsI}-ss%;xw?&va| z%KtHMx1-^-JPhyU8O>XCrpeRANwR*5#_`P!%|p@Lj|aDjMFr*8(QM|^n`x+Rn7 z7`?k9q?bf%MM&&m@by^3CWv?nv(;C{B_Msz`-KsQ`iqUsH#;a#A)Mhj-YMG`eSovS>gQ|V%RHNe9h zZI%Ex0}%0o^3z5p{O@Vhe1Q(zp?u{_`acQvfwD9ov@93?U&s}q*ZrWEnM9H5V0VMr zWOQgP6v9_)G)vJQ1N1pGr6tKyY#9#SfatGi5*#AL7mETiy}+adcP0_%zLu3iO>nQz z3lEsZL1}74Y5E~J={Rbm(xxqR%}vTmWdrpbdF%DheiCkNS*|&B=8gN`=+-h_xV84l z$Deu_GYfu>co`Zp1!EeQ;4l*6acHE}G-HD3c0-f9VL_L7W;X(f)rJcnQ@@wX8Sb(0 zD6Oh8VK*^<){`iRmzDoqP<~#!<-Z5tE#30S#b+pe@DRov5D`R;dLpB1^BAi#GE z+A)x>usZ|Vzvbh)mJ4&~f|>+Zks9P{ zPWW_T#TN7;fDj4OwsB-oFqsmMPHS^HrPw%wUeenlT1E?3gkvrD z3fHxiwzXJ@7Imas(2amR3?7|UfW>Rj6AjghOa=dr{5%wg1~91*k4&Pw=P5BW6;Gq^ zpy+L0+VZB5C9XnWs?iq~Zq*}3oq*W1AV^xhWHe}@HKhtzb%KskU4?2@anDK>h3#Z# z234LKWbsMm%{sBR`J8yPd55@gsK!t~^a_@2DcPeO6Y9{47Mjv5X=SRV2q42bRQ;XK z{28&#ggT{$?p6-ba^;wOWx{W#P|G#3WC1}w*dBinOLl^|O9lUomQPeJ!MBkdKG%D8#IpVtJ1)`_SQ0rDmtjg3M4TaXF zv-QCMq?D*DsXyYoH+Frg^#1T8+4dgChBD1F8Pf57sT#CQf#=hWni3LXl6ATmra4@x!4r^+Se-^vH|wcD`*w4}Ct)Z}q>AGO#o|NXbG=zEOo7lYO-q#9rf zU(F(lI;ZG~l8vPy*+%R@u?@M%T*~R8NoKwp_s>*i6JTze(C{t7PL3be+#a{4;q|xQ zKJviQ1{Y1k9J0`?oZ9SPzWM04ym1hr0&7t+fi_O~wV;hLL6-H1?dg49$z;LoGq{P1 z(Z&)}7P7Q8j{&`wHr%d6;L;OdhMnDyK~VXd{+uNcD$4T^r#XZnqDM<2>>qi&$y!0nxeN zE5A_k{)tbT|1F$7K7P%dahq>`^d+0Hb-RnEfQk|=RDS-d>f)u?%(^ZKlI`577tdq# z5vE3a5Slq{GBE)odF5EmRjo!+I7 za+~!CZkLAaHke;}Hw!GaYqL&b!KY>*aIebFBtpPzw~M=*)}MNA$`p;Z@6jfDop5sd zhSMLn?8MZ!uby11ENUqPZ|}skXa2khIRv|oMMo<Ix;IBuj#oqRHaov*Ve*;kF95 z>v*1zN>>^)Wy-?hB)>nYSSqI7>vOwj^#mKXZc$vkUK3;nJQat*%pB!t4{=kg=nh&X zot0=XO(FwZ9D5cc8EJ)&oKtn3x!ET>5$RUjORXv>>GRN(r53eK#T7H!71nsK{IK#q zZ79+@rzUb>TImmR6+#!PuC+)# zLgtOo@9!*DrtF_GMN2;xCM!K!)(ffIlv`z2^Uun8COLm6ILo-WlOYETwTiA5+e8Dnm<+4c5hF~uL}vPV0tL~nOXrf5@XC^6cG;a81p zxYrTg&|#(F2I}u%1eScl8N$Q)fkP0{E=#v8dur>n=4Uofsdmd*JEl@PM9MpG-TwW; znaA!se7a@1@HKV5_F>CTsd&SP(8=*rUpfa?Y2#7g>mWJ7Xhu=u&^w$iliq3ti&||4 zVEacb$|hnb!~#GCbFGa(Yn<{dYVF`pw7QEkXGA*`qD2$po`~L=wN9vnm{jUd)6(;5qXs`f1fa)~KcJm#K2?JYE3YnsjW-1}S#bHI zumAJ6r=C}KytMxr*Rh)NT4l1Fxhafb*d^s><^9@UFVG9`T=KSlz~}@lO?+gy7LC%I z&87s=B>KF1JCO}mbDRu=2%5>+fg_XqZCuojBtub6Meqa)BEcCM7~0k4QHW5X@b8x{ zeRQc&cGOIk2#j5 z)~*AQ*aT0y_LH|hShoGp<4>)4_=!6Qo;@7ilDc)(ip@*H_un;;#@+Ymz}eNaL)8oN zi$={JcGJBB>IT$Sj+$PaH);;Bi)pJdRAMh7A4i75Hj6dr9d;>>xOG~@b1d#yc;X_`1ftv)(QMYm*!5PT z<3cLH+;Jp$F{z=p4!xuP#|0J^X#83&Tdr|(T%77J^US5VA3Q0tJL{*(%9&Yv`a0#J zxQlh?%WSf)Y{yjPec`?GwdLzuGJ$E+Kj-~(nD9``?C*y5r*XmppiEirs(6}HBoK^6 z@{>lO1|z}~veqbrDxy)MR-I8rQuP2jq&$X!5ryj}sHc<4itCX1(|;~%nd4Aya$>c?kCKcQgyS2N?~t`onSuEZ2o%^q4<+IwK2vUsEkz22gHJ^6M@ zgmG8x8|UP^N85T#`_RYmfbTBWq33_-yMybk+;=YvF`cOzcwXV+;nP(Jo33yXr`@ z)XsLhk=wtl%Nk5|H=AHR)AO+YYPY4jVL#egJ}Xwp=!(T_Pd)k7O><~N(=D}=Cxzms zPZP&JxS5VPD^&@TryzivDc5OcH7MuqSmxHbmfj4kXtN>p|ie2-p~p%)LL7m2JFqRSX()g>l+T%I`5nd?s-N|Wcf-$mMJq#-CWUTZz31rMC0{*xYRwXwe)Ce7&b4eg&6zP< zQ@80sW!uzALX~t@*}LhXvBI<&z-qqGfED?^n6}-5(OAig5*-$!V;Pw*FO?sYMLDQ9 zm&phSj1R<~&3(njk`j4VAUvBqD7+{Sv#lY%n3zqYl{q)`nsaO4In!>N&}(+T9t($D zzws8~+rC8;=EZgE-8WdD6D)(}A&?!)Dq04q&a70Hk&eKFC|WV9%w;knt1qHGFq$eg zIpbo+`a5zL-a|^dOHEr4Uc36TW@dZF!n@EJXAyr*AQOW{I&FMBRHr9FN=notV-Inc zu>OSpFM7#={Llm~iGw8{vc-Ahdc=wKaSP&>#ffoo30iFe(oR^S*7$`yC)%Oro55h~ zEV*Hp^mMQs=uS$7yUc#S+b`4|J$v?OwPpFbq`PLk@aCC=%kQ4|sHys;M4J5FH#F0? zXGcLz;J=qX{jK-7UTY< zatz{0`zCOd{Z$)qsWwCZ=%Hll+na{n;t?RL&E;CWrsZe4}sEh7AUQvampTA zt?Zep?8REqUK*uTHwqiY_~uL1XRDQSGzUMT3v;QH9MVD~J469tQzX@hqEA2UNL<{hm4G6Q8vcJ zC8p|v#eGD#m=~8N`t*A1kdU7E(uah6Ovb|oWz6U)1oL&608H<&5oJl|k(_gwfsx^Z z2!Siuo}boktfgV$qkye;K(rxo3l9JKX|)a8C*5}*=b$p zRIgm0m^!g?%GiSb1G*1cwXlC>S#id7!G2Zcxt;}#s#O|~I8<{A@oqcYd!x5#u(afK zm_(Zvn@6tQ+QKFi+a;6Qc0#L?yun~Gy_NrH=63Hgdh}=pKtnRM^^!aY(#cIyNgndx zvPDbx#Iz`##Tk{TDac2AcANd8{YN{N;;hCbZ=6eP61w@f@vlG&Ub9VusZ}Ntc)X`meAjJHRiivC!Yh!N znj0oA*!uXyp@YWE^)+7ig)+6YZ{5t=>u(aX%4#OpO`V~WGKW><4C(cr()VC~zp3Mb z8o4jjGa*v%v>7$wREvmhDuddVzH7Au7DMC9)?iS8HF&8)$xCMd_=&fd!CNnaT#`!A!a`uH%&1H}k1QvIxycYzPFs0-}MkmQ7+L z!D+HJ#hQtlrJA*xy_(~i3mQGPF0rh>h$2>HJCh!grMmfm(5yJ<=4#T~+Ll`{$Z*6U z*4nvF8{xYt!F;1d!cq}IfP(X%$zqR6lne5WR_Gmv*>1Hb*##`VJ8TZY?yx)lg#Gvc zb!I|!vUALM*v;?9Mj9d=%!(1-!J*=&S~$i-O?=mpWG;X zI6k!dso_hKmBq9{d01RmP`w{?Yn}BwziRg|wN@TP-#U zpJMK^90GsP=8#Qbm6&L2v7YMPSb-}JyfVJ?t~Ut0wm=t?W$j_e)z98`+q2K!cE{6o zH;oy6^Npdp#nRS0pL*)fJD=LIV(e|Vj30OFGK}GT7U9F>5d@RnBVk*DB}s^)USI(U(JmG+V|;l9T!?lg?OO!u6B=7i{B-Mt@x$iK z%~|}=E%(Ick5v9kj|=6M-!EA=Z1BkH{xg=&&#oCWUcrV(N-~5(;dZp@N9Ja#O%p?m zNiJl`$f+r!%Qf6R(LLWS8r^Yjq%!K_eMx4WFCwY1$F9SeRTVqZk$E<9yRi3nB{Xwd{uMJDym&Ub<8{J1T3(9c%m4_M1FrWM$2m zVFO20mQH{_eC-Mql1s6WC@s@pyF$fwI>BPq`su16Yh%}>JljwzeLMU}4|~tuV^~iQ zA^Kv0R|8EROyWK@w2;RT8{>>Nnc-PS!?X6p*$^YMwfWZExmMSL+Z=3lZmNThIlB%{afEi%VO6z$aBt{bFjc`X6t8@Qsk9G|NYSD-{Ed zD_5zcfZMhq2Y!$kKtRq81-KmTW+LW`1Swo&z0pW)W+I!BYiveFqE3vnl5B40T+<>; zF>f2x^nw5kwN)#Z42n(cS6O{>eEoo(%Ewcu8EXx_dQOPj)0m1jhQi)C3Y$w0Eo&Bt z2yfY-6TLEwV8jvH0&Wp+itTRErqc=ICYxfWUd3j9wBjrSMvs(5zFH3*sWfzGu%4+uuR#udi%s|Qa zFssuDw%zes-&1Q9OCz;rs2@`TNB|uRn<->fBa$ViNZ3NHH;qi4LDH zZA8dt<0;XQ6K&=oOyG{FjH)jwOR|Bk=F6M~1uz2Cp~x@5_M*HFFHa~OKB;a~y%c;CgtZ+WJZr`1GvK(J}jNM_z2lxT!j<|U&4ti%bDpq7ZgwnSWeD=!mC zEfI5V33q#mnIs7%3B%!^`Nkqgp^6^bXZ~)_r)fl9uYJSD|S`o*d8!VEY zPDK4I7OI1A&=6{}IW%sU!Hn!w1F}<*u`1idp&^@%${L5>DLczU$PTe|&Qw)XebjA9 zs>h-_y4-T-aY}XESneufXv}Hh2U}az{Mr&wQa_(BtP$t7JR$^JP6#uu-cI*V<(bcs zlvY6yeg&OxhipamFnX7ULD7;WuuvEuo`dE010SI-krCR*X-Ca%wEKODMpCMlaQ|6} z6U3&LXs5qg2cy5*%1nHXfYD!|>LBKU%df!&BURLijt5R=R$PNEo+hE z?67K!wG*}TwQIGHX^(5QMyw%7dcA#U2-_D}sw|RWI`KYM!mjcDn1+BXKx!d~2WQ;p zjU*;BigqL?Z{2!LdNMMzXSJs&3p*wV``faVTXs%pIjGW5x~ygV*MrmWWzwkjKemlR zJO`3f8SWgL2V^cUA+%HI_az!(((-xW`?ESukdtbO)N4yHJk(ZRCR^1K$=8-(ov2@`zn~ZOBqp0JFRG5Thhh<(rc+uV zl+j0eD-HC)-pWj6Rw3P>%q&D&|HJIEp^!ez0ez@9EIu_K^b%%J9Bi6V!5pjxAP^Vt zBJl>I(HX1`m^Qc-CHZ2k4jr;>Y-U+B`zk_aNsQ^7F3NVy^F7v`onCdvJjPfJU+@fg z9=Z^1=`f}v1sY*bRkPBUGi|BN6sM~vFMqr|Cz-yOO@B3kqpz7-QWhEm&eB%@o7HRg?tpK zvuzZ9U!sxxsFsNPvl1uBdbNZquG(8+BdNABlb2vKN*(MriB`xlfer&EjU+>@#lhRb zoWs22d9X&Tmtz+tbhn--!bZ9EE=(%DtTiDaHXDoBCOMK0%OZ1X+$*wQn8_y|WoXNZ zWfSXvvSOzrjNP6VD;oaxnX(({{=Y^NV`2MYJ!KGbE_z~hFj?bGBeCWzY?|?QcWLtT zV$*E7-QBqvWuaVR^OUule{8P*&-yK9xJy-VY?(>jdoFj! zV#@4+o1aNdoSoKV?yQkFRgAfDbf2lc3n%p+KecJ;)dA9>XOFGNIPt72*@tl&sB%N= z4UoUkfNA!K2E4X}Od64Xg6G};?D>hm@aMVLJlC9wv>=n0pXJZ9uYG>1{rSu{2-kdh zya3}96@%sNB~JcTEzw0SfjNs3GF!c6a-)FU5gd}2AecOfQqP{5**cvw!Jo;#iy&y+ zrp!)RWEywRL@dPhOzMfWo}N85?pzzoDvNQrZQgWEIuUJgAk?zxZ zX_3u$2yu-Udq4gs9N}egN|p)4Qk7V6yi@KO^@>Odl0H=4pFZgIhd1xO(tPITRS!Ix z`s}k*6P$nP@VhN}blj*r=8n6sdFj(HzwqI)o5Wj`FA{}un_T!R!p%>v+qhpB$xM6q zyz-wftCXJ6S#=rlb?qr?;vr?;vA=_!SVd;pU!Z4+s%vRE*R?Dw><7*ZgTV_j8So?@ zsf4zX`u+1pOj~e5>YqJ7@e6;Ram{lL8)>!vnOD_u?52)mYisK#Xm>ov;6;@il~J4< zsiv)t*|`7DP9++Z&(sp;NQu^0VF;Nzl{lenQcKv|N_>YpzUFwSt<2;lm~3*Xm_YJ0 zV!kKAw~B{FfT+7UA%XbeH`ra#nywiRt0z{9w%Ob!EERZ5A&A6iB)_J0EI70H6qiD5 zd4TJHPWycDJqTp2VS#|HlN1PGwSu(t0-Pd%?ZC%U)bKTe0#eeipZ@)7%RlBkRw4z@ zDnI4?JbGc){nI|pd1K=vGhg0yIHF|e zs+uJaDRaKQ@5wW79X)rMSyP7~Pc3{dR)GeXi!gz82WaOJmI^uTM)2nYKunBqY#a4i z7GLM*jkEakgOoJEX@cH{Fte}N)c55cgx1=P*A2awq>)Oqs369OX=Fk z{!y%tvfYdUbw4buF|6oxTEyWSN!z#Hc5K4M#?qGas+$pUHg;84(^XTZ^z7cf<@(5K zlZcGRkEKGX;dgdKyepo#I_!uf=m06kr_os*%#JW4%fxJxMbQ^wC7x=})|vj)j%Zs3 z;n|stgETH?Kj1SpJ()cLCSutz&)zV)Z2S@=#2mP(zPw>U^Nva(Y4QWeiTSCl<gq7&@I7AGJ>MzHCS!KrIYD*B%c8P#53!kTB{wkPx0 zwQPZ@EsKZ7T&Ey;=dWrm50b?eA&qDI$!DJtAAG4TeMe2`(Gg|IFU@;cD$iekb0C!m zwBl>^ysv6cX5OXtwT;&!S!Regb;>eR6ZOEQT+3jKPk?2ap$uE0`BC1FIEM><;e=pM ztJCQ*lLQv)ut`3z)d8o^3a5_&BV0VANa5~LhaOvcu+Is*o!a*~vBjFmf=#=&YZqTk zyZL8f@!hb}KAed4G#(=v2PBXFScVoGEMI&!qw6po$I>-+b#tS|A^l`i_9?F`r{CN$nKen3UVfM5mk z{3(q?Yv-|=9fRC+Ta-s{@EGD^qs(yPmO1fxpxjRR1d)&s281sPYW@6E9#|KISi_f^ zWYLsT+0Wb8o7KlL#4xL-M^)G4U_$qP_Mx^}(ub*ev0X~W%wL+D9N%ro$m?%H+oT%7 zxIM!E`nZCmLC@gy}NKApq}qQMUpKdGaXwXoay+;0zNRM z%`kbVWcE)iF^VT$3ux5XY0B%hz^<(ZEw6!v4zQMw8rX*vBC!GX%_LMp3I9F=zPoV9 zh0i9{Q~!04D1(&WD89A?PwG{Yezg|~>?2zCwYP+XGXCpys=pSC`fD<&zj}Qcg1h*} zTV^JUA~8G!w)2LqaV7_C20ON~>MVB2=^$9~0z&FKC$n^Uz@n{NzP1xO-IP4J>wv@o zR;992S8t@2v6Gc|r+4(Zqb)7gqtKS#MzxaGY(w6TS+cRt2VU)9Y`9Jl0{A>2<296^ z`^_nnyRbGpXyRI?Tplx-W=&`E7Ss_Ia_XGn)XCdV|G$$5^r7Tye4Q#z+{z;mHklCQ zaesBWdFc{Djbi0}6y*uggSL>3#>4}q}}aQ<#}vGbZDYC%kz9&N7$ zBh0?kK_ijqj#?r?XnORIV$Q%uIfz&Lf-gi88#H2x2iq&#H#xC=L0HAZ2q9Zd=-YgL zf|wcE$y`@Ac;Jy?Y;0Ld}hM2*oAm+0-H?UpHfecs_A(Z_O%e6c}@gSg=# zb8+m_*uAmh(%7}J=VJdA+ZwAOv9?%bSj5IgrKXZ$p;T=YKK=B+-vb)Ikc<1+XxrPF zxvIqH+KVUs0YN6R-9D|`#dH^wC2E=y5fO7s%PRnMAfuRlyioM-`67c}Rs=~x23|?x zcZVNXJ!19XFO+Y)9^5TSyAP*R?+3N_RixjkX}tQc>W1k4QPuDNr~icH7`DHpI;~=Q zHMP;)CVya-GE0;tR52qI2B!cM6Z`_#TU?7M zL>k7F@kxaM11oZWX{Y;@>^^343|0i!YmQ-Wq#gSg{aT|QTSe?h-!fpm#v*EEJJo3P zd_@;|+S)`Bi-bJ4iP7rXofgoIEI5huC1j+q(e@+rM#gr(^|#k4Q|LqWw|}UUlaz17 zqJRIpk5JNb_TxLJnzEG}RO~dD+p$Y0WgI(TFP&|}Uh~x-Jg@BZyr})THu66I3IN}q zFB1D91h}!ooozN`gj4smpPSA&5cFw13GxSdr0E|xLwr-|rAC@GZ4oUjhY0;Tm1u0u zS4*_TnOiTSL|Ug3CzR*Z685$_G$=u}Eas)QGLx6!ekCu$qL&0;wrwI&q!z3C0%Tq` z83&B=$QR%W_B18(uZrA2MZ|d(;WptR;gs+rzLGjhTp+F#pAb(Y_Z#Co^ZZ{k>e27GmY~v#1Q^tRrx|>#+ zJ~C&UN19if4_VBXQp+OC4$B$KS5}=h$2!D1+xnKxXREXAM#{+m`zMZAN2z0l9yTX05`)^U^s1Z?jMxBfL+LP;w|5y*JU?=f7+l zC;K}y!1W>j{ifpQtLIzZ*C**F<64i==YQaFa3}n7mb8U~ULxCtcgYG3zV9imC)?#l zvQTr7+>9016+#v)m@Lv4US$KG&8LUSAmM&8j(&|*+N1dV?Rv5Z$7?ti;+TLV6USN{ z%W=%bu>{9_RvyPtcHVY~6G)-fgZD5ek+OPD`jy z962X{jr*tZn?PAO^L-sIy@f$$< zwPJZ|v$O?k^nJ->=@dC6o+XR%yI7h^7DHAfNZBMyI!q4Hb7Uty*SbZ#z|Rk9ZR8NU zFNJyeL+l+f0q-9rHNqay<1pDJ?nZykkR-906tHtKjP~CJ4QpsKe#=<@BXr+^^LKGn z;#i6!la-Nt=-U=rCGR9tQD>#tNUHH{KBGIkCy~}G;#{(t-wPuLBTO=QUrv)ln!aQq zr}cJ{iTgu^&#@ZaNXj&qND&SrjttPfoYDM`4!N;aVdKEZfQ>`s&>SV%ZFFa}?|cA{ z#q65VzVpFpk1>*VfnJQxe{jgB$Yf4?(4En~?EuX`0qtkw_f;Ib+>yS2^+^&5P<+i}P*#aMA3p@itsxfPjQHnKGcANr#WBl2Hub2!H z1@blezD+P;3{B91mz9I)Z#kpAlm|Y+0XoOXo8kE408JAF@Gj*4ama71fFfA1re!Cl zU((g{$`P#nGnw1@?|poEY$ff6vO3L14Ysb*B=uK(hfJrJunQgE*I}QX6{L$q_*am4 z;k~m}Lqc-;k3euKQ%dv_;0pi^00Lr6KU6t497M_KzbhG3O9tCZQk%CA>BE}v zd&6f3`Mrl}-H2M8uNmYIdBflH^I`m4O5+!1T=;x=gJZ?8^_GN7Z)j*LsRxd9pV2VT zKX95Ms~g#8FyhpRvv3#xf_=10Kgv&pE+xJ92}EaR&7!9zlct8NM%0!Jiu3tGS>1+& zEq>I5M`R$cGAs`aYk8I4ndnuRtoH8fcB1K?!!|OxKGQVSKXuZ$+ORkY?>31gO-(Dq z_RMgGe^5B%rZ1vFqiNx8{y`<-Ojc_}b$dG%9Zk}(Ce`NmHvN~NJO1x4p&y;@Op4r* zQ*Hkx>|t0K7^c;=KK2(^icx85D)oCyo9de;9d2DV+3&UaoA#MZO$$muZc16U-#~iWZ*)#Ms)fkXq_(m;_?rzhb|ySDeq@UIrEX@nZtAfHJ;ou-pEI28`BqjTL zWaEuV^%$_3Y+OgwhO_+(!Y=;+m5XqLp;a<-WG#P1y;~A?4GiP|j1YM%oLz!;dU0P*Z`^?Z7CJ+Na623|5L}j4(o{QjdN`>*ZYt*8bZ>2(FB}YE zghT$?X(7gTpiss|G>1QQ3%pWOTQSmKF`}*(qv{LsC+q>MPD(BLT{ZsNIJFuuU|5%` z^VY(_4xyk8H@u}d@ek+&7}lohaM(a_ewQ)sfIi+@8b{hn1C4Now`AI&NI7@it zKjd%lhJ#hLj6{q)oXQbmaGI;5Qc+Xe`KlAKKqlhDV@|mUF~g;q?KEV>4G-qmZL|i@ zT>E%P+hcE&&R;RIiH(FmQXv86L&AiyaIlvh-Nwvk6WR~i>$PD@^9kLwFBoK#xuG|> zvB^JVYLkCtZ6975^5oXIn^@ZpQb8+f24r=EXdAH4PwyVFFG%kmSyy|+1`Tz0P3?Yx z3Ipp0g!ZN2S?v)oAwhmuV0YO~R>aFLSWVTq(DCwdM}mYb<4+}igI`ZMObNfI7Q#JB zrW_X3dp2HfUm9;H2>W`Vo>9NiYj>`+zkt z!=0nhh*&5(V4`Lkw-0XtN8CP4m+jLB4VNYkV0Y7dwQUgHoA4Pbv@ zm*@t}4CtE@?^MPpw}cv+>O*X@kthre{-a^)?}uUX_X8rbDQxgh8xS`72eA9a?0&I& zUuO5U{sCbcMe+JF%)6>E1*{oc>%)}v#-535vVG4+Dg>d@WcxBJctT%O>Ps3~-I`Qg zTANf=x;)8O<{jmw;;5uDaa5w%nlyY^ZBpg1>ZD<%Ba+esX`|Atxk;&klugK-{RPfP0jkT1VLe)=^gLaI27HU5d>z=d6+yyJlLg2zF623gOIA^T|@O zm;4J~fhBZV6mldF({=l5MrLMK9M-l%ljy6)hUwkm)R7DZN7RMoyTfEu-PqcFl-?U! zv1Sb!kWdlM9a$Uhkr1i~PsNEhVP6y(5L&b-GjqbC>l=CG^ZE(6Vwe2te?jyF>_T&4 z&5?ay$wL|eW5_jsScP#TBagIRVz)b>;%fZ~J_yP;KhXMuvbOc2@-*UM6H&|muRqnV zBz0s08G{Jn1Tu+?B_UFe^U>rLz-qF9R}9Wgya)SD-O*TCl5c}eo6@=vFdx~S8q$ly zLJo_tYNR0pTlWB#w5|Xw#?%;4a zhj((gg2THw+{oK~l-IC@xBeK1TRD83!)+Ws!QpmZ@<|SNaQGC5J2~9TVVJ{x9Pa1v z0Ef?Uc!=ZnJcoxlJi_5|JT-9o7&v_loIVD^=wm>RtqYLR$H3`h;Pf#NMjr!V^f7Sy z7zm?}fiU_Q2&0dIF!~snAi!!#7-C{cET{R6NZVMFih-( zVPYo?6FXs;*a^eLj!e3Zh}LP)Rthn2n1-h*z=y$Z6#N9N*m;0MhE)pi*#lU}A;TvH z_^>i-Ib6r#dJga7@O}*P z>6kSPGMv*fYZzoWr(<+jNrrPeVK{dI-`yQb>tQ3(9UR3lIRlu(M<<7mP7bG14j-KyJ~}ykbaMFU4{*!_9PYINZ%^P!s< zKBst3U*V9=)_gu&3ou6SAt%(%VG4M@0AtPI9US8SLW1)6q|?1k}VFo(kchXeUrB^;J=IGDpB{ONEGt2i9N?^pBlk^CIrwLxv8 zI2^;_?Yyl!I9$%*ogA*<@GcJT<|XledcnKvI9$)+eH`A;;RX&L<*jVt@G%aza`-rh z+c9zTpW<*QhkH2;bGVPg{Tv?P@Hq~VGYx#6=kPFxM>s@&Iqo0lWnSY{ zeFLzN)47n-xscDJLQdyGPUk{S=R!{BLd>H}D9`9zh#ABnqjMqbF$Ni(3t^8j$mm>% z8N}{0Iv2toW9N*{g`CcXu*cXrqjMp+f-LU09xjLwCaBMdS+7h;Yu$mm>%Il>^L zb0OvkgN)9Fm?NweM(0A{GW)p*%H1LnhgZc)yBxzlwOjig>?@c)yBxzltzwtgS5^KE~lz4j<=m8;4JD zxc&d>>s({pxT-jQw@n&|fC}*ip(?5nLMAJ{n>=o=)*@O5zh|hy;e~|4{WP=phAVoGv zG4@l8`V^xw#fVEW;!=#b6eBLhh)XfzQjEA1BQC{=OEKb7jJOnIY6cB6XqZ963>uEm zeuVZTv>%~8O?#U5G;v51hct0W6NfZ$NE3%N*fiJ-SlyAPIA_3Sz-GW^z-GW^z-GW^ zz

d#(9dNS<9IoQSykAN0dBrrbnziV&xGlj|}FK!8|gUM+WoAU>+IFBc2}d^oXZN zJU!y+5l@eJdc@Nso*wb^h^I$9J@pw=cvbc&ed#ED=_q~asM>F5)r9t+eVJAMxun-t zLClhWX7Mn~3~d%av&_?UABTL-fztOIYWq1*_B{@_>$UODftmv~2P#XHvP3CMl(NJp zOMJ4#C(FKP+4n5_o@L*&?0c4d&$91X_C3qKXW92G`<`Xr=TSJ1!g&v>qjKtF=FKWl9}PYld^GrI@X_F-!AFCS1|JPR8hkYPXzh=vdiAsRw7glGuS5TYSOLx_eD4IvsrG=yjf(Ga2` zL_>&%5Dg(3boNe1MCWp_)*td{$fF^ThCCWHM`j!A4|z1`(?2K&=8^FajO0T>;>2h@D|`L;J<+X0{#p5FW|p`{{sFCczyzJPhy|KK8-ChT8oU< zqUJx>Tv2=Uf~=i;DQb_*-bK5e$0$;77O6Lj)SE@>%_8+?k$SU8y;;rP-`iK(RENwF)RE*Tv+zm|Pc=>tb?UOsxz-R<0mW26F%?iuzKu06 zvTdvaiWTj=TjxCCI_C-373WwjmRHy5$?J-Pwb*&Wb;ZG~ohMvZ9Lz>oJ5RXo>^$K! z`h>bJYcuL+^r>Ois+hC1pQZgQ?PqB}NBcS2&(VI4_Vcu#r~N$b=V`w{`vuxB(0+mT zi?m;){UYrbX>ZfsroGJ!zs(H4%?!WI48P3`zs(H44Ym!o1GWRU1GWRU1GWRU1GWRU z1GWRU3$_ck3$_ck3$_ck3$_ck3$_ck2et>c2et>c2et>c2et>c2et=x1MCLa4X_(v zH^6Ry-2l4*b_47N*gn`k*gn`k*gn`k*gn`k*gn`k*a6r9*a6r9*a6r9*a6r9*a6r9 z*df><*df><*df><*df><*df><*vrCRbC-oRdj)${c7)FnK1cW*;d6w~5k5!w9N}|> z&k;U1sTww^8a9=SF1bzRBD0o@HmMpmHG-_gs;W(mqf2fJ#w{4PVBCUn3&t%Nw_x0Y zaSO&R7`I`x`w22`8^&!Iw_)6daT~^M7`I`3j+v6(uaLEskNRrH2|X*wjbGoOr5TEx9slGyT{r3VcBJExmUmc-@~nX9?GixEx3oZ?LIfDpQE5x)Vc)o zK3zGncV6-IijuvrQetY(Nb6l%^?!%^6hH5hsz>yDTv|`a$C5Oi(z^@t{Ibp!wXdk< zey#W1uQMx$wU-B^`c7>FesYWMH`$}RNpICt8Q-dBp}b935#O$B zYPYF(y+cn$-75umNW(s9u?1uA)j9X~>CEo?wTDwK;qKP6LOsF>d?c&PiKQ{ zmFq!wNc)r0zRb8IF0EtqfR5yYit2||%g?Ew%nMD}eNsnb zQ7dhi+@m^H%Q_mXI*yO&`2E3s+I`l2MrWkH;6ATw;}^76<%{l1s*10>KWn^Q)4g3o z-3{`R`?Y&k`1b{YBppxJwUp(YIAh*j4tQYE|!# z-A{Hbt}ZO>T3tLnIeAa=wM&nkSYA24w0vU6fz{LVwX zPcJQx8{a+t`<@$4p46DTQAd8aj_@w6AHG@t*JFgo-Nn9B)4AVZ?bU96)Ny#dM~WsE zj-6Q)UQ4vOXUE=|SU7#^nA%?6P7k2t-3hf!s3+K8eI=)Q)h+5hw<)qVx(_S5_Vt%+^hww+9piEY~x+qOBu#J25U=Dqj+zWc84uKU;5yQ=!B z=k%^q-K*Eyr}paOCMPNi00Mk9b{+uy-(HvF%m0V@>;3m8CM+Tf0060facKW20$^QC zUQX$Yv;Xp8{!vr_l$er=;1}opT(-*h=)e0OVh>fWPJhVjbV6^I&D-XbS*Pq5uF;=wCx) z3r-7nt?i7y?q#O=%CX{0J9uWq3vCSC?EwIm<*!%-e-$_aDB0G)#^j58`tn=9#;Ib2 zFvZ&2IXMFWtPNj&@YnZHq{3Mu{NMU$9`o2fcobGxC7x1v-Sx90eY@C z%jp^ZG>A-9<*skjrjLr;+w9%I12K)Y1=x5lzasbFQhu+|p;h#I%2t27&-s3NDlDt3 zpffLHtbg!wzpP`vMZ@BRga!Q#jDG$d7U%0L+<}brO)bn84D?O5^s@#nCwh7);PcYx znF<-Zw)G8nOihhUO$-bTJ$icXHv9z$)BXJg0)Zf4a09da6Z<~9t#3YsF*G?m(}++o zFb)Iv)Xy}U{r50Zfm&1GRsJ)+zP_U0e^qckz4&M<5DAzN&D<= z{sKZHhIz(fEonCtVU$9YJ(NV01(XUDZ4^nAMGDCGPxyr*fb#iZ&5v!Ee$d<#f>KKZ zP)JbB8cV64C!aBxFQ5D`ezE?KpBtaNpH3eiFRyQx_2Afe6+Sfwy<_0xc+ws-?*!Za zW1a8c-Qoa%?_%sbUuz5kfcRQpGywSLCjj0ZC`c1XJ8J2bf7)a7b;Fc)y{$P}R7Xvx z611wVK*c|qq&1#ILP3Qz_S+xR*yL}3s72tgq9C+zpa$OqvsHZqZ0kw8`uE%m-uJHn zt1ivm(}%-{8?O%?hdhUz(;nG}-qW`@KNW}tz0oyjcVck}#To@GOL9bQ3$MkgrkE>Z ze>Vjc-8>@LXl%?v5gLxGAp*me?1G(ul_sWV=fWIiYoS;$2n*uj&opr|GR8IP4XJE} z%1-rIaIn4us^R4OL0?GAS>L12S3;V8D^Y&r+vvn$Ta~NMHbw6YtI_&8XFe(Tr^inR z3F*xt;9?80i46-Uji2hraci7Q-yL&8d{oHIz8PhRJ!Z!0(rmd;z~-WUsEc#cZ`QG zb#_#R0A6x@*jjG63>4EzQ@>HTy3R71wpKy+akV*&Bn5>ImliXlADs*=7=Pes z}oOI7NDQ2uw}ZFQn1xPAG0~X23)UnhAmk|T#X)KX4?<>@(D7rl<(&C zjML@Ojf6=)pq+@Ri5?eX=VBa{9aD}Z1{9Z&CrPvSKnmBr^OxoNm)Al@NnAtL+FKdT z*2!4$QLVwWCN7LnoJOeHYOAT&q+vuYA#vE@E%^X(ipAh^dp<7l+cv%3gj^rg8Zls- z;+D0)p5`oF@;qhDHwA@Jx7HM>Gzl&W7u}dz9f+me!RM%}RnBrm&&+VR^Zxf5B;}RU z(Vd4;__%w2>OSV~l3dtyTEq_xf~)*B2;tTGR9sR5kg|}?V=XN%crS1&Hr3_8A!O{P z%B;pNloylg)$rGT>MIo$+F2KZxh&(0!1@X^z`2Le3FL!%l>n4i#cxi|o+ulu`aHek z7mW&P4_UBF|NARq0E>@r>|F`3k^`n0O5#Yg#NIZIQ=(Jm;;az;u6-4_wR~J=Nrdbo zjjuEshhUhhe0=0Wk*4A^9SzC&Mi9mMobqxH$%v>;M{qR~ZyZ;IU_q{I{>-+9ybKWx z2@)J;jlxTiMyWB?HBo^a_m$-1AcQ_nBW9?mrK2K}?7sor5 z@|`U(Q7{lMB~c$g4z2+n4ZDXIrh_ZQ{Av(K0f>||R#>balj3CMv#9`ZQliKM=Y|{G zIx?#2`ch^YxnI`c>i}?sZd9Tmx7YjG>6nY=4L@t^(vP<%e~{sr%YIstoQmK<2KeM2 z>AFOe1j!pv;XZK;xVSw8+*cf2eSF+vXhF*;!(3eiW6haupRgvK_0cE+d6Y@7BNbmf7s4y>&P6Ak@&&XAj(SjyFSX^Mya^ zBmARYEys*flIgK)?{Bt|ZiYGIr_oqa=Z&TOIyWfPIBFDQfKfbv(YyeHv#LAg+{ zBih^iCi4pR2raD2LfeqsPDAkRk}oPcUAGD<($n6DKfA7WWcYkdbl7{#Or7Sl93<>u zIo>2jAAW+IqE|peU_V^6BuH0pIMZ>~Av<5d(U3*B0=R-?D{GdlO7U+vhujl*yOPaF zP+i%^P!bRw#R>Odc0qTodDzm_M??ml!5)+&>}L4Eu9Q7by3npg-Z|<@&q8#3 z{g*}p$u;!gCfAsYwtLr=@GE@H7;G7mUaSx6F0(u557xOwE`5%B$XzsPH{$e}TuU73 ziq=@`$bLD=&8~VLgK?Bos0|#yO?a00jc~D&%!FTx&k1XvjQ% zzMX*20nzR^vaZI%7n7Y2uFT*j|K=WUi9au7a`;29w@yZevGnnKl=r?Co2V}1fcU!x zgu-gMNmDf#YopZ<2plV(ewBzz^Zcke#TRx)&&`^jI3fkQm7d~A{PhTrNL41 zrlO$m19SU?bus7wwDR*Tnus!ALlGg~C*jY~D5gB^H`eFpJ1 zaZJ!Ec{F4yeq6UgLDboyZ?U8M&Cgn&cQP{BM&c!PhC%1YM|3LVq!Dz`dh>uZVolmc z<`Sk1f{UW#QKKYY#d#Z5QuDPq!8@7_Aa!UlvDqXlzD+j9SJaJm8YiC>{1VklHz>@E zx`xEH(rz(V+z^Nv8NSw=Ih7kojVoT1Mjz+`xMsbqxYccae$$6wp{GA`g{)3D4aZR` z=s+Ff-FRgd@ZB7sovxY0r&^&Mvh2j)>4}tpQ>y|g1&n@n-0|7&gVU>{Y!|bCc zaoTIw6uBU{=127Rc${k1zuI`9L*RRRout{Geml`&sku#LMAp4DZF2y*!DhxG2&mPk z-B9FxtR}q_WA~R{$Wlo~{+8#V5P@wJ%yU&+F3%`q(HU)m1?MOA<0b4JWwRRzpwm6L`oVBz&~(jr-&Ei{n016Nou%ApItgc- zUc?X!P1GGnzbzw=yRLb|+bS*jDA=>Kd3OJYa-58NRBaEZ>#W!-RQ8)}3 zAe*1ArX4L}iA&|;#sy4%En-95nHk4A9g8d=_zAy|mW$5Me>;32Uh>J34OTHq*2J+k1jQLLUqS_sW7x$; zblm5HbQ#u`Jv%!(V!}Q3G^FfO%Xl3@u$gfDX_(b4VEXASjichkG_Swf_3c)xH>k^{ zP+MTP(9qidLye&(4`@o?vbI)&W-#DKP$gv(Td`JMtO$Msei6(cV?iwpd{#FFEncSa zN8tuzvYeo#68fyq1QesPC>uj2b`^J0iGwTsY{pEcr_SQYav^o?v~&6^WJ4)pUoHOe zrUXaJACrYR#ozfc0JY}|S6mSKAX6}=v934FT_UT-Jz$|Bqw<1vrJFgVUW8D|gZMktS(*8t-OBxZsm|5oz7X|x+Phhsmhs^CQ1)=vDb9km><1=U2>C;Crg zzkQ4m?wcsTn;N;4nDKC2j+d+8n}g8QtEkHu8C>>d{`OF~zAKvQqWV~=>>a62w2%@6 z_%812+!m3BN#q4O2Acx?TV)~_IbjQfO0A7qu6u?;FeSQxytD4KdN4-y$fg z@^K@?LtV~#IbS)s!eQRNlSf1lk$X{}zmG8>=c}_yFDLpOp zg&|)weTr&egUR6X7e7kMJG6>!%bS{Gexfl+q0eH{{)iR~F@VF36C#p0qA@2GI`~eQ zXpHFBF^v+nYTs$Wnr#Ec_=rm~q%Hi)zzo234IIN9AdTWU+=|LJ)qt+K@$v0u%KZudh%4Gz~POZI2O4xA@nNS|Su&Zui(}UyZKob@qsR(UQsB0eK z=lgKofIdMAF8C$h66&G|GJ6YW}EWS#Ge2E3C49VrHLA$Lf)jp`6a z09FMPGCnU3mlpNvWEIKGX|^kbMH{sTi9U-sCTfJdMHH!$>Cas=; zyfvf1y>Bf_f}>P@p^DV!$A6%%C(`|5f6yMbu()&$w%1poV*`ZWdFOdr5Z@Pwl~%3& z?NmE8B5J#*BkT8-q}OeH3|o>14xjrIbADx;{m$Wh1ob>wVv4u7IN48I>ZtJOU))AF zkerVi!IR}4m!#AAyT9jj*tO+}P!*MqmmTDRc^zSyOX{+Zr@UcKnw05pl67{U+`FVD*0e2 zMyp=z7vw9Y?Dd8pR-(GF?5qf~yE8YPBVt}nA_rY3vp_wDN>&J0(HpzUmo4mUM@rJz zqs06=hS2DaEBVo7_tg6YZp+2lO2KUV+J{Sic0J`tdY6y9Wq|N}s=If-XuDC{*?#*l zSrVgX#C0r7?j&Px#0AtuY~_6{_^N>-v@5F)F4^Ypuzsu2|#3#Vz!VKuRw!(P{n@BELh~@%X6_RroyIJx#5fLuQuV0!Lo)Xehs~J@t^u z$Xd4r*=t{39saY2F+t8-RjwIm&bfr4x!Z3$@$Ntf^P^Z512$=#T{XdKx2dDK&ba>u zp6yD2_{@}U(?Qcd;)J_K?^-NLFA;50`|=kSFM(C2tJ?v&neut{8kmS73|~{B{qMQH zK3E8OTEPxn9Wl~ISIXgHq*@SWo~(AkUX>~;&UX*RcxgQOW+>C$p&M8;+q4IHXeAV~ zp;*phfeEY!f^RUq4o9qvh7r#S5EW%^J>pkRu14D8!;5Hc#F_xegsGVlaoMVb^|3DMn^J_dj8Blx$I^+ zqM?lNo(vq&L+@$#ICVVqzM23-)mvhS)g!*~cy)GS>Ak(%C^ns%yz{!{ei3W|kKbT$ zHZU|gFU!Kbo~HIAdRLz}dPMJC@KYgLkjuI8D2H){oJ#LzK%k+U)>CIdjKy1DZ&t47 zPcGQ(WX;oyc8{B_8wT~%!a^penvxS%`6#lZ4eD0s3~0xQ%A9L^=lesJnH*w-%tCQL zrP=JT32!5oS&8gy;XqPy5J@9S_Eq=;r(?8P{FJxj?MKe%hR;vs%+ihOiWsN}!mxnH zCZWP0(bDdbN5*N*+W)MQTtCkJs`+2v4@2UkuaUXHxcwQH znskLTISeCrui}D!ODO2<^|Re7xGJGkvPuu?P31_wiq2x72)$aG>zO8@-4&oZ?Wr;* z8swzn)9LwDcepNHm}Kxj+pEvd3KijTLnqrzb2r#NGuZ328>t5q8s!@l=X^dz&O)og zN8w^2GxBkcxOtPUGIDD=WYhc{B!WkaUYY6#j@Ko(t(PQkLBmJF>Mqt_ssiGIAc|t_ zKPT4Pe4*rHTt0;Kz_SV0=E>1m;Il+bg-2qvCMSp&WUi;E(NS?c5jRW>jFoMUAmSTA z(eBzF*WxJdblByi=rbdI>rqjJgYjZe7rq~WLzb7_iSOM%Xa7cTXD>nK(BSm=`b8`}BO-D(@jc z#(a?FhWCT5f}>(!Po}l{#bk)S;F?TnsCrwd2X(rzA28nKWJYp4kqaUGV?jK5GI*PT zBm$)`ozob9B_Zh5&}#=AU8Xyus%Ls2X)=mpWi7Rt!Z{kwH14d%!vv(3?^*W`c3+0l z1t~K6=Sbw00AXOrF;T$KilT?)==;DU&~}UK;G#6YN`p`LG5dmepS(8B{tG+I;2>7c zwn?$ila^;OHO$V#z&A{z2@Pj$igJ_on8-g-;k0+(E35H!D7@g4zDLb_kA;}D4wmqD zHWMH#s?ml^WOB{2dT_B@?7CvC4v6gxkc5KU5vnFpLQ^?hP6mR4R9rMX9{G56 z62bojJxgNmdr9T_Ne0unu8JzM*-%CAWsLONWRPtiQyTh0mfT>`eP5F~9y%L0EO0th zl|em&M0`K&%xdKzaC(V~21Qor_8kpXB4_BKI`-aHmh&BsDN$SNB;!DSH#k z0`A~oews~?3Pi3g)T$5>2ffiFCUmv#`_0#U(s9efq4croF26guJxGNrs#F840oCLD zFfn+VsuERL%htCRx>MQoB2|V>GL&w~R%vZ}n!JHfnw`wpLYFjd0aNZ%Kg^`8^Za@v zqqLY*pV5cr4pk_*n~G3p%;KBgp2AGV<#2}8*o#m*>pgGweqW*#*=eV&KWEDD$f8y7 zxLEFwfqgS1^TBF_A|K5;+EY&MgL;MM!Wo)^i`$a?)${$=+=u{%6u<01Y+$pa9flUH z4A>mZ`!Rfp@bVb&empGIgzE^+>_CPu+x5=q#z_X$}8(s`qNVmcMvwW72J!@C>9^|wc?#RUgR>&+i;Z?{&(;FbKw zp^zJOPS!|=%vCe>myxxc;{n88t{EI-cEY)C_5wHl$9ASzs)=`3sYr1w68y&IIdP!% z!NNbmJSO4bB4Mz%c#sKU;+V|nv|X`Q2!f8p6kp~GZh?r_*8&3r^=qGq$4}qGn%>6D6$IWKk9tDU!8Ks-5}4|rQ2)h_?kuvxk~ zzM!Z0mX1%}iL+F3z+a=U9HLOZsxeb?^FA{lsp-J_ls_gEPs7I@So%I)`?&nxVB~x3 zdR@Bp-}N3LtA(a)cMn#6E5|gPR!yM(U7?10$N7TU8p#m8wcy@zhU17NN3K~PxoAe{ ziDGyoRYDl4oqr&c*6OsBc(sjOx4>ENM>H=8Y&dUtK)2;BSb$_yQnY-vQ6P4Y|NPKQ zk~>6iuc=)l4WW;x|2={e-xeN8*bm%nN&yaUE(XXO1QL=Yg#h&p7y^<2y%rnAJptCZ zfW5-r=bFN_<=mayTA0b{p+BXW83W#k*x|={aCGz(7d{#W+V>6<-o0d0L|W;BI0g0+ zg%Hvo<_NEP)B)2InTejJVAj~UFIpQ*Ud4K}bgVa@P7+s9qsY{5F{7svj)Y0p2c~pH zdLPvy0yDE@Gxfo-YaWaHK@Z)_8q$#@LE3O#LWjhr<2Y%&X+C0hpkUDOG#ZXuJ@N1l zLv*PKo%SCC%i6w&A?XEN4-FN+ZYiFs(qu(!=$0 zI>VSbc&nnfar_P7pIQy`NrhI4ZF9-5v4UCV`81qP3emavR1BF5EMF zj7n!n!=U7+=Yi;!dzOGGzunri(&1~s11=egtav@!WxZANWgur~cRLPmxO*CTJh*?~ zN_a!+VkPPyE!hd&aWe!)CXb8=O=s|2UJi|Ydj6aMsWIeTPuRnvz|dH$EM4+nK={~B zHF`$@p(dYMB6?^C1kR#4w$rz~Gk(u)fdKQ-*iXXn z2+S4MDbgf;r{_xO%#oeME`DdBUA|f`I)$&nW`uH?7UIYf(&E1QSt*D)6}+7(`-^0| zh(;Hg6oud?%IEh&&Ccix1W@XmV7F653pEJd8`tq}^UqwCMg^ z*|CRI3JJ}N4tXAR5#~xD>SCpDQe6XXETTE z#dFefHw5qfv4)BY3-76_{v@w4IF+OXGyQrN{u~FWqL0esJ3BxnaS3?ZUBAT$>A}{P z6kD9sL~k!TG|3Y79ASUPUTc@T@ynH%I9tNlWf) z4!HBv?`K`H6F;ptC6r2BLG2K`ggb@9f?YGg`=yRJDPD{2z!uiSFD&zN z1Upc5O!7(y*{MCM-a35+$6 zZzvlj=lBs>w&!T9a!TWs*%FSe=%u5Nc~bI%^XN+2eUDOgR*R#%-n0-=(>SuI;2pmK zI?#D`Ja*oLApoBNK60S2#*|hx*wv30odfR$i)Rb151KriUJ$6!cngpTL?m}eZk$tb z6Ty-&LqanVfrbome9*L?t2GQ}cDLj@rLV*n3mIIjry2^(O3PazuZNadPiC#<5`8@u zeA}&qrXORtJ$yaP3DKyCj%$9-qMbAbdAhXwWX?im9(jKEPBB)LXrwlI7O6a%)WrD~ zoHFAyb4g#so{L1?HYO;SaHLqQZGCBXunP)SE;&;!{+y3g=%qnRfjJXlh_JN3=_XNB zk<7!K68L@M3a$im$e*YVovWM#O?Wj;2BDXJRhpKQfN{<-D#+-)Jp@)FXV0nBd2HAx zQ$9yQ0<*vA^6@Bcdxf6bXDvVh z`9Q3j+2$&@ERbAID`}H2v6@$2Pmq<`qz#lc>zSc`b5iIthb}ksg1&(5ZCo%(r$$~5 z*Ij_uCa+k>yVsyB=+6_YAWi`k&G0})YE|S!7V-!Gn6ukM!d%ybVyj+_WOpF9*lqV4 z?2XUO)on(zS`#Kc4UKwt|85UZET~yCUZ>?vP|G#G@n9wE!6ongVAKof8qtjsJWjI= zr1?sp+>&)=6C?RywJx#W2aW_-*9G$Fy=Vog)b#Io%=^t5%o%Ca@tk?g$XFtbpNvrw z8kP#|m*LFVFURAhoF89F^EAqvl+Z3TxJsTs0THIsH1X0^B4E--rwAN(tIs-(=nVYH z5rJ6MoW&kti~FFq@zdL*o1MqNHAw8YEmn}e0XtduV3ui9*sU1W#JqQ_+>p3)1#dM@ zAcEUj&t88nUcb{h7$$z&BD-n%)SIpMUXE9cyG+nZP8wO`#B{#;TO(^Lb)OzqCxyd;6P_Ynw9N? z$x8livJgy%_G?5fKl|kz^u8e|eKAPGfNfc+uw7w-jU&TuM*YDxXg_SDlu;+a)%h6v zk{KhHfYL%ghp)I*5ewMCIZ}&-IKvW^pL_b|VZ=K+g)+;(Oqop{I$u%4^P#8XXlB&o z$0d$#EJ8BZygH))I?~cS$@#AQeAW{yt68D7%}zz{Z8MMA^PdHgmjD;PEWIDtMmZb2 zaaLHC`mV(LpTa)UvChq}CJUK+DGJzUG|DR$(5z7kMH6YuuJHrgO{7vZst9YCRTmv| z=A&rTd-_RaU@0pFs?xJw%6{V~Sold0&F z3kE{8cv73E$+Y|v84=~6Ap~p|%jO8LeP#eF?Jpr;7~2z-1@gI^?JYW$z+5P)`KsEA zjZeCvvsmlRCMBz`cq2OnchbF7UEy?xr${TM+V-Bj@p*PefM9F2>?=IZ%pr`zlE{NX2; zh?Fj1;|_&IO1@xeqJ$v*VP2kaAL|N^=1vkv7qyOn(g}?h7PcIZt-IIP^W|(5q95l2 z?2wSgWy5z4}NMN&7YN8q5p7lfAkT{*Wf)|ml73LPNobMtnK3snN z{mojJh{$!f;CJ4;m2tZVbO=s07!E4*7gty<9LI3gPolQ(qP8CMSytb)$`mh3?~$to z$2+ap6FNffFs`YVN;4s46N(dV85eCdWXHV=;A7om=+z0!Rpzk0A#>9v&zw0t=r%Z$ z3+lyJKiK*z=uMOCD9si)Fj-w5%lJJ7>{>-ar_~5jAie;=fod{ zKkz1;=bwy)U3>$hSMNDOJ6>%y`Hj6!%5^|z2H&SQX7dc;yz#4hhh()1yuemTQ->^c z)2Y$cN~Z)xbfWr3OB1r~L}K)vf5t(>^^?|lE{HT>rZ+z%*>YHUugKW-kX{bQlPo7* zeZXS(M)7RDeLn5t^7Jh@pY~fc`Hb>})t`k0)uRRN7+V;3l{p!&bf$E~)OcRnk*3|@ zVQ_)zS>TrPgdZ3;eM7NeQS__{E$(I}PHq-SoTRe`*PM0Ydc)`pv?0h=y#u>xUDg#n zCiwd(j{NX5;c!a)I%@TG2nh%?T{fPOLl2(k9smvTMF0S!f+(wh`&nbpiI0X4Fz_mH zr!N=s>kQUE?eDoLWBsk8_jX`$rnl7_JMveqTnMTbNI(uNIM3g6Q#NtTZx{jyL7Nv- zLrp6KY`>P9Y*xR(mzBW9wuv0ic*P!k#9piHit}pu28f5O8Sw5^ef?JNt)QG}vYc*= z68v0{s()aROdEYr*mftP=ZkG9n_sNsAAP{fwmLsDwE^K62t%ADp-}g8f*^2&gGu<4 z9F{ev&qZ{->N_jL)w$te9SCh|>; z%W-;6y!Uf@+>^jUATep^hccPfbFj#A^`i;Rc>QYIOE12L-GPLQa|$)b#f3qnHX$$L z(1WNgW!R1*((ok684G;C{RJlre>Mwl^tgnh{HNI-R!YXHTtU&g96ovN>N%+UW5)$N zmE{H8%^SJ%^4yz2MY&zfU02pK@?5Gkp~yY{XpBXN);tOKY=Re?Yr%~6QmNZHWPqM6 z0@}~cKngji5WCjk-7c=+psMD4idWcktA&G?wh!oE@ri{Dol9zmVR6RFS8|8B%%M^I zQSk~JyZv5~v!xeC1s5)&b=bqdoeW7@11|DjvmNqo&F*-ATys6slrvI|c>x~@6tu-e zwJl*_VYE?6ipFLFv&e}`8|=Zg2BdTO4Fa)d4SIF?)XM}N6w-CQbC2Dg zBoHd%wW4}XWgF8vQai>pZVA{!q6eT~eFE`;?)QG}5_ce+Io3!LHH4vSv!ysuL{ zE(^}vBQL`#rRC9T8LPG1ac1bO){6aqYHfJ0@-2uIM)U$zPmh zeEAd)i6hhnvtkyWM+%hj`gcoj<2si^-uJ*m!NWpA<+d^JQumF15Q<_Ljjc&=Ni+Ij znc1Tq+T)>@Y7dZV521Dhs9j^}4r9AR+tsh)c0@cna^^IeR<~NQrNd>6tsmt^fO6A# zo&BQuBc5Cn(5;}<&&5^~1{u9YfjeZ6L(Wh%ZS>oEu43HOsVi(P3a+hGrjRY}kMO_7 z=CZ{ZaYQ4h&XI2xPI?=M>|>qPlsmFt_}E1`hw9MCD>bB;KsLs_MafyI4}-BW_99A` z`ANT}+g=*Cub9)1PcECPne=wWimb+y(j-(PsLc}Z{>Pjo_p!Om>WDw19oe~#cI?#u zD>gEa4;mT){wd=MDMJ`5$Q#`+24l#Nw<>1HxfM`quG&kOIW zyP08tP$a_;J5SQ6&oku6-s4GX@c9_sGXRtXjc5fx%hhY|CPe#3}?b-TGZz| zHM>!UOc9sVed2%Rw|Qkvw(jlJirLt@aG;nmj+8vA7FvE3cg@&?Kpe?f^hHUdu~5fn zgt8-_cE;Pds-MX2+d9t-0<(ski+CiC8M))g8CJ`feC;mG|JYrF1Izp4PhTfXlT(QPwTZJ~CAFI{nM^k}(f z*tvmU?(lX@lf_&^Cec3?(_h1qCye5)&;DAAv}q#cz56G(YuNiVnqNXx>;)HUO)KMd z{Y`s+^XlhuL5bOR;2ACXpLgc2o$_K$G_18@05QjCB4y?fX67Jg<}m30wLzJ$;Wh@n zo-jTSbGs@rAb={vSaeopd^x zlwv8RACx^P`cMp@sld`e2b;L}+-+_hOV3W~DejoxfB=II?T~tcR46!57i&KQg!8 zp>PYP{T=Z%EucdzjPMNz#Rlg^yTy%m>bHH&B6~JOJL;g+#u0uF9|IOnqKs{A?XXtY zRg#_eAvidW;{YRhX023=cE3W|+m4;5cCz~O;ATS-sp^4~IK9W z9;}VI5cZ#$FG&6jLtHt*$h^Kq*4$z=ZQ3=gwQ-#d@4bhd^W3M_b)AKeV?Tm8oM}H4 ztygu$&ilIC0%uJE5JU)f8()>S?swhoajB*?HJm4L`yBzp$iRs_H;^2Y%|2uxaRxESYnX!$r4a?l5 zo}`}N-^=|~K`OyCAvON{1>3;%>+eAL5U1}w1b}|P#B0+-9XtsX6WlJt|0*?br~kXy z!yW$bf)K(nKnUj$DulE1KSA6R`4Fy!JUEw9q5lDPhwQ$H#uJC;QH<N?e&(A*j%=?`2YvEtZ;pbBRJ8zB4_V zoXN;z{}*w!81l%gF)FEA7oSvNOiKMPNTpgEu2Nx$TCP6$Z65 z)=Iq}Yq2Jnwc_7UV_b8wF1fM7gx2a`kZZ9v+NHt>_f&oO-w;pc-TXvMiT{(MDuD_j z7bDo<4f3stj5`A2DV2xu-sog_A*KvpiNDxa`aRVXMbzM5qH2KpFWE+s{kIV1LQu%z z0+~3~ajs-q%YH)v68VlO|G#xjbXqBuO4cDaR- zCaW9%r-|m+2_^my^YofN2Xc9l9QXCRkhT|O0)r6)FjKGK)9I?gvf9*a9&Nh%w?IIg zU<+Jnn#8}P3P)x#f7D;_)Ej=atBtJ69=x(_V$8SH&p=eSpRFKzg*N$<%YET5&H zoVfx~xk?P2>iBC=zQHSfbp|Chel2_*SD*C|`M+jM&7GVnrZ7^%ONPmJ(68YO^F&KQ z(u2|RjE=CQ?HRS<+514Qojbrc2k5Wmb`r1(`7A`a;(U!!(nKHP3-*XwC9>mVSePf} zq~=c=@N6Wx=Opf+I)(IR=%tMisV9Q#!mQ!6rr2sb&mbU1fS>bFl!N}vK>QJ)#{d)C z2G$M{yF&A|PdMDpa@XSvkUO}2lJw2u-?@y}9_=xft>gDd_C`xbAgZPy!wSzc%v2|8 z3N6@abEk6}N2&|2plu)Dc5Kt8SsGuzso|lo{si%B^bG!|K3oR~06+o!fNx*b9|C|L zp#4=cvI6$L{viV74&)QfstNKPK@|U@q2-Q)6!qdlp3vb@n+SZA@ph-|rt-as86@(0 z5&@(i8Kwq`mS?1*!oQP##d>=?djjGXMbkaego>7DoLU8M1c79bi~jB9K%`k15;?Dj zSwJD~r*1Uyx}P-i)l}Nr&0Qq{UonLg0>WkOsps(KL4M(6d0mujiDLVdsvK?%bw3by z6%$?4qzTVwn}9|-j=}o3*~Ltc%Z@QxwXa7SFa3trS;j^s5VjFL%RlvrbuUO{@>c~s z^Z0()ZvA)v9pu<7)#(U*A?lckRI6)(Y_&6=7|4bD2mb=qHwC>79>fkq6Vfhs*(;U zs$Ye5RKI=|w_#cSDr(|7`Bl;dhGkh;OO|d~+=`}YS=7k($Figoj_ahbp04erxSf|R z!=Sic)x#`V03fFB^toYF;q5L1LUJ_HJP95Vl!>1R0k> z0VP?6Wx8A!$9@_JoaaRpIvnR&>TVUsPHDO6>Urkv)Hj7Luvr0V1ndFOYk>g4i}u|r zug{@)we*C3-SiZTuh#*6X?n?;%9)vZiDNsOnu+j8u1-u0Kj%bwES`%*28%$;MADu` VYwr9g3a_u^h$ik;$8+}+*Xy|}x}LCbBw|K`8ReKM0h zldL`Su32l6y_5A#S&9P!65=nw6$rq;r@_33DV>t zNT#|GhuXpOts&R0Q5RSmj5C+}&`e_TudM;~C@>dkM$MXM>#u=-E=d#S?qUtQ(V?rd zPf{7}G*w-`*jYp05(q zCiqj_Z~2Pjz5q?2_Cc=Jw!=h80dR8*oT*o#0y>stWv9 zEHHq>zpiuE0>zzqdBl;ztoU;~0YB_6KGw9JAef+bOwqbPjpe?mU7 zsIOK`d1KS#^C5&8Y&Tf1_2>ECa%*d+XKW|uv11;)90g2`(I&w{z@Ceyc4;KA--e!l zh)sECY|c}KDYPgS}+DTNs+@L^~3G&pW~R>CL5>voN>p-@s%?CVsen7VBl zN83VomavPQe5t0sxn-?1c2X><9P3g5pzl-R^-)7>l5Lhl z2VQAL^frr@7%yL6V4hE?x2K?20DR74izq#hH(*=?r!if%Y=bq(Svb0UB}V83F$w?k zXDcm*Pbl!HF)kmgd?!&t(#9=^I-AqsYy8e{Em(Z2nbb;nmT~eus8Lmd0=KhkHH!{w z8V6=?glNR9N;992{D}=U;)+cX=V#yOh!Q>m-u)iH2SMR*uimF}?*I>xu7!F0&Qjb! zC~qSO2RCVHO=PcttJn<4t{@yNqj%&$O@NwT3XRNlWfar|^;}{kic^A$&MT@Tr`4R% za_n{GMmTs<%1YWiSyZh}^z?W(0keVI-eerA4q|I%o(=CL$`V0TNSK10l!zeGT@bak zFbB^?@__4|FcXD2OK#Uwpb}tUT%C@>qK{d~weV$F^zbdaDf??yJyZGMgA7Bi;A^q< znSi?~(I# zK3IcAzqzJ)b)0g*ac^aImdWO0ugZp0^yj#V<8Irr^oSKk5!#{FVoevwmgj1X;$1`_ zG}@hdyls(yfwfq$YM4Igb9j-F)UJIAMVeL5z5wR4Mc!h8h&RnrG)DcUx7_Mx;Ee}a2aLR^_8 z^*K_7LpAW%c3njDvM`t|U6v&+2=cxcrt&dR%o$bn-KsI3@V1p*wEA=}&1?67f1_{Q zw&P$8jc#MvQ<=F|;M2;5~XdtT)?^Da!Ouy^~&^ezel4!gAk-8XFfV zjwoQD;atooMOqy7b716?xFlNt$YBMNyBv@w%og6c?vK--UyF0Pa)n|lF-4UULF|qC zG2rUx^I_~!MAX!*2Hb5e)hz^)#hpC0I*6ciWml7tg&_B|l4g9!d-XG&H|`cZO@pNm zax%HXPE+W~V|L8k5B2SFM&X0D-RF?9_Fa1o7)Ifv?Zp6k3{LeM%czdIJwI3@r!=2G zqH;qoeN`3di78B)b2j865k9DcRbjd82-A?bW)AI-5FU3 zqzxL+JQ&lTQD0e}0B=8^thB;EvWy)1&AHZaH=`!{%GT~oD zwPaF0Uxmb(vyKVOq!I5S+~uRxFStV7!~`@j_XC)>7xefU)GZ|Z>C=0Ot`NYD3nnfS&J3nlccvx^%H%|pcoHwS7e6Wu(npp_jS_Yh zq7E#U3kb&ueyt#u!i-?@wjprwhvbDrPK?APt6cyd^syJ746=aCgMElkjUSEOo?|em zOX+2tUbHgx)spx)hONgdJHb6;m@#{9ADEGeQx54Lfw#}(>w4ABW!DVRWp|~>TEHWa z^`%hrBV|1(1pzu%!|FhiqT7b%u1XL4ruyvfkMvTXl!tIhZ=bOwB_ zBD`&C#mBLC4rN(WP187KW1mCDhYOT3q|R4|U3>6TFV7lk!R};iX_-G)agcRHLOu#4 zNkjx;u@7g^%4h{AM$f@v+M+-k21OPv_e}7zQIaMn6^iIP=7`c20VcE< zO1?-uXV<5$@twYD!|mJ1MzI3~>D($*YL4A3w~ ztIWYo#}?!g+Z0Ef{b0tOs~3@o6@#k%9gz4K*|9VBM%{i@<@JO)qY7-OgC9a3uJ;tx zr9zKW1)tQbYo{2m;Qv{KCtMsWBE?_e&~bVYoqkyS9qHqET0T(;_oQuLDGllTbBXRY zkKz|=sEI0JsLbfSGfyOxAskHqJf1o+tZ{`yZj|CkEK-^1H)(y&Kc=%PWZ!OgZd7>= zgY$?%6odH8s=wUw?>2Lh@n^DBf)oodoAw0_-(3&BqVJ@<3a{=+=do$$&^Vj8WmV^f zM3wOm_lEA;b#^ROaad*z9QyAHw~L!|S<$O<=a@R51W^_h6M7ryAW71gEh#iHOh|4q~B5y_{*6!B>0l0 z1ABj#zAfJM>s{#^ypmtaeH5-l>1$BDl_oMiaLGN6q@LODX}M`@<%!rvP$d(@5c_z| z7gR>3pJo$iP`d+DYMC}{dZ|F^^tI^&iHRr*ry~S?F&}EX>f6|(IcyJ3wSu zt!qb5x1C^M-rjrVX&`BH&X&5gL8pa(a~X?Ab99%lexND@h7KPLD6cYq*MHX9Q+iOb zyBGx7dlsn^>Cb=jz`10c=dRMsiQg2OMmGd|Ur5+ZUa#u^klV$sF3)#5OqTI?RczLi zAyOW%dD~5A9nJnCnNSgux@G!Qi}5ycrAXQzvLBA9`_dyw`*_|`yzLtLaWk`%Tj!&s z8B9ASiHtKi)3fg+@@juRj7P?isMHnX|8XiGCP2AlzwoyEmYWpo_3miy-!+Z+0&QLv z&+GTb{6}xCPT@E7OrwEUar=wQFlt+eD|}lN(C3zG!rBtdy3x&Hpx^K7#Y~SpiBc|5 zS`yHf2=EEAb$OdW14DXz5DSlJT$Ul-%;witofazsbl=ykdEw9I;B3cA_FiUr#)=$3 zIoBt;0NkE2nj2+G`kVT+U?QG}ZjsHbD228AQ;LTYX!O!p|M4fY3&iPZ#|3k3EPA=n zLoe;_!LPdF`pQMDN6GSYZs~aM&k z9lrHGP7&*VC{M%PZLF7irzAP{NCdPhr^NZt%|1!Js=Jxt*&nH~0}MC=Az+K_sc%Ec zs0~_%E_Uh6El)>NmC{!{KV7CP(X<=Z4jyU0e{W=IsgL*~XF=%QzPu*Skxm5OpkF5!Aa$n5rl(>bnWy9kdN@Uxu zTL5?m0D34Bd_Xh-K#+7<+jLIZ`7wxD2q*0fwIoPGPSIl`TfQeG1ponQvlgflHp{h5)2I6@TksGcg1y& z?lh1u9YGc2UZTNH1)jgI%GBBOj8l)LH0AKOAchUb>E}@|A)i)Bg$}Pv8f$x&-Of^I z*~xEW6)MbD<0oN`{u*rSLSG*Dmr2%U*>&e_AyXE{_Nkg^qf4j))=!Yva{VHP48;bc zs;AN-Ob>UvUr7v2S?~&PUl9c%_P*+Hk8h2peTnUK|FmMz_QC!FfhT?r zcG<2GC(Oj?upNOlq+%Y zVg^mtpDqhq-oNWc8mRXZcd2sGtenJm%cZyYGf|}LRcO83Or?>N>-kIEmwwT(1|yID zR4=TME2-7LT|BtynR9I?Ja!c{Wq+$OdK7t8+yUMzN8U2B(S4)f?G`!CJYPHO2pDi>0kfq}v#q!R%sgIj_diC*)OB?e;Cx(9zQyK?<@o zDW>_6fYD7{N1(EY_7 z{ul9hPd3z0=R=R-$Oj~!{LIjw2t=blh)ATU%|i|yUOunCqkjsz8oi|8GM9(ALn$rIck8+Pu$2~Y zfx#C5pIBcwoR2i?OJZ>*^a(h~SKA+E$Xd|4xvz3(N*t2v!Ej~M0K}6y_Tb_=bm^4V zZ5N!hZ<*m>0zip~anI2fBi+$P{k3amx`{=(Re5@24Yckir1#GmvG{_=*{V~X7v%1l zcKoxV2~b@a+E-^V`!ez#)qFYcr4EW?MXc1sypUx}W+F}^2J~E}!?a=cwi$o2XDZu8 z@s5{5?l^i)rr37_I-&xiEU}&%WhCR9^^k76EjUNTxY!R*FL*j)$7AZ_)YW|vkR|<{ z4~Ra@;oR5f-RWr9Z9~?bQW9riaw8FLeUeT~!NSIvy!wNfw&Un{zSvOYxu<{RL+LP( zjpy-hkHy?(dx^J6q!U1qxCh;dPx3;yVCd`iR>+BL67ae~v&GScpzTOjN-NfexCDCE z2aR9c>GO(poS}9gde;}W(_c2Ja=LD2^h{q|4xA-#8!q6ZI3?PdIrm6JuvoQvdks7; z)dZC%w&f>Y^&Q(uu9AaMT#j-JKNS`5M3Pn7R?9K>CQYerXOV}W0+#DWTYP{Ef47}yxeU&J%~(^HW1r>qZiJTKfZ@95|ZB?TMl9! zp`(-P-ICFgP!&^HK^WNL5BE-WJ?4#pfWw2qf9y+IypD3JB?ODF1||n}maC3V?&g!U z_L)glFyXcQ84c-Xw`(f=YMJGjp*M-*KX_xAYkd*)^#?|FJsRVc?^JY5je$ft)^PCd zkkk=)Nswn?B(@WcXlZz^c-?}V5(Qz^G6fv9hv6sfZ~}z?*yk3gfoJTdr7e5j9{mm0 zxV+`1+kPV9l@E85T=i^}>jodJuMpMF;&lo6M(&w?O77V4%6{|<*zz&HKlNEx4Ipda zcj(44+V=#0T>JH*gDzr-p8fDw(!)jvXs14|_0BrOjoyBLbF>J3e7}pXT_&f$wAdLL zP~tFcq$ly?oo!zps5KXu--ipB8Nn^_TIlfmNVb;viCmcv%nY24c9=O{m~`hv{U)=< zl0#_Cjsa{)!#?8m5@;)iF=VSyo&Lrd8<=51jf`u_NUEkWyXuNe(spsnV7Pc3^C?D( zLntZ8eb5Rc#_Ol`xcW9q)4avr_;PN>);e7%xKy)M6koGVg$D#C-#<=4fg*EoU5n5# z;nEi#5O z7GOYfgP!1{?CDY?>|09mmuGnkBf)vHcn@I=IfQin{es`#vj7t8^sy=9tOFYUobEr#i;p_hT8G7NO8*&_q-Sa zlQwhu&&&i6s&6oYvO^Hi5>E_(@BB(a@=6C~Dsz4EGn@VMu@BTP1p!EX&#*Sbg{AHO ztHme@pNj(fZBr!Pxzu2|=gEU}n0}Uu&!H`Efj@{%&61p!81#u8s#Cx}ITfU|LKji1 ziTmCfW|~I+el*n0w;1~ZTpbDz5tn^6#?F^Mu5#?&Qb(8D|7Tf~IIFu*)sUl&cUSQet8u zTfsyyerOCW;nW?lY_P9I2@6 zm=~AYWZOB(JtnG}tB%ZYt?Ca6K^Z<3*u<_-?z3Eeo6YO0XPuFjAABkpmHqC1Hb&_Z z6PYJoxd!M_Q3?z!rC2w*u~Lks<9(iDO4vqSOdnd%hcV+5RNXoGJ+(CG)G33wwDB@G4)C_ zQ9k~bt=4+;eLX#bToG31k#WNXry=8r55Ham`ai-pzOv?C+hW^qyiK(iszMAYfA|jY z3T_)8_{RXg%i!z9+MGMrDc53p876zj&Jht3yXM!?Y*@*5Cl$ts^AX&{sL=ktgY!|! zw)?z!tDQ9O6ZhjNDgERm0dy&^Fh%YO%nF7fG96~U;#BixB&mq-3?GrfGv6!t5f(F- zG%M#@MM&yN34MwwbJ+Rq9;Nv(9dtEHFzOwzTbKPcPfW>IEwI=&QQW0U*H|kxJS45U z-c|Eg$S=tz$K}#8v+YVB(+e&3Ma`)q9zM~V8knZG9wTiH^+ErvRDAUU`q)S37=P~? zbg?8z!8>9R&QTk~>u=5ef26ivUv==KBt~KP)$gRLeZt~cbWRH-{da$K%l3IWZ+nYT zNx?P{<3|~VxCUCZKGagKGA5vUmb)w6~5J_pOz!6uhMM633{{wk-9NdufdpMh`+FPyj z{fonm-Vi6MCDExeZ)T|@e)g|6UUfq~GF;!S^z&@He0e_pc2eDqG~W#n=B!a6p1XxH zFN0onfnmZ#GG5b{lfVDHoHLuT<(&Jlj-WWs4^yLcr5Ms|1DY4T&r}{(W_e%fDT{6lT8t-JLT{X&neG;|OCZNz;qR@S8h@G2VL+rb|pM1z| znP8MMGt6w5hvm@`%h#ZOgktsUgw^_Nk0P(!_OhF!W5>2wIV%kvd1w8*N>VqHK79T7 zwrBrSfj#6S#x)7BUAByuBiV@!q`l@17V-)=GBT8VPC6Gm@&`6Jm#*>JLs~8U3#W<~sE>r@`WpkeBS50t5TWoFem$qc`eN zhHbI5g>-FF`R^a7;R{&NkfCkKSx4hZu;m3$RULCA8i%Y{(AMKNE+2i1X@Ot#u zSZUOpK3*yoRD;)1G-QwnhQ#)EfFN#NlyYGXpjKg#J^6v8RE;(no6xY-c0z&MLU294 z@0Q=@?4}K3C$Rq6ILIRIvUG8`{$YR=^{ntWjk-fZ-vysPg+WCJ{ujL}TXjNR6vMZV zgWmi2-n^!k97S=T3HgJ{u2aK zQX&>81QR-&R&-tL?Qq&XGdP<+BYd*9q$bH>Oo?F;s{1=O zYK8XABh!s>>9$L#$n{+WZZE3D5=$(yG`!2GcLqYjWF!8kARKX`kNRin{%M(HZrqj- z(2!&zbgEg0;&&*8{j`wb2rz*zt{{$+!r0!kZs^Y|gsDv)o~bU(DGQB1ILKdv3bDFpSfR#j+fP-_ia^D{U6iP*(MK$ zjGK5|q9(?0HGnF}H!uv7!jB0RNDe$|ii8}#Ok~+a4@)Vp3J&JlF*9?DAbmB5 zL23?$VtADuB#K^cCTHm1G$0!0a0QYbBw<~1P0qB$?5G5c$$@}mE<%)}4AIZ=T3usN z6u8Y6_h-KblPxIiA~kM2`G8C(wQqiK0nv9tnU^_&9s8$SU#dR16^oA7H8wEzTs~F6 zg8H;Aj0WNRF!#JZWri1`gVG2%gqcXxO(A0(}=lD-1)G#ln;;pI~0(ut{XqP`&me`RuQRMV?Mz=mXU(~^e zwxJE0a(K8Ah%%J}(P5%jWyV~)r*F;05r<7y3KQayQ8CrTv^u|;@>faA%ob3sj6|60^Av8^|X zJOwgzfOyKc@SJ@(xWNyt-bN^C&WPdu5tDT#+L0It7qv=c1zl)LK)Y&7zU^U=aZ=oy0!Rq29hF;}|!zPA4}*&wnOrq46(8)AX60vUYr zzFX=m9=l^G1iCK(CFwQZ@MY^Ho)yy5E&UD>BA&JQf|(Xqt1bMDh57wrWB@`-?N2wa zNr}q*NdRm8#M$p!7&@gNF!o)A zWXx9qSGX7S#8I&p|9?M0-mI2pBWmm@TDPdXJx()s$C z!*5hux>19>WLw_zQtp~Co;rGS*LRgMD~6jUiNP?wrgykghSon5Ghx8(#TOJlMr>z7zWr3=i~f`XOKg`WMdi&&bULy!{=9s0Ug&`|Zx$UPkAiumYc zwQRYvaBvNKvAK6YNk76N>)BDDajD4Ql+r%GSP0RTt238sOcQcT3eM0Xz(|v4G%NfV z7?KTA$eWv^fu@bNAFdL5MI&}!{|e-De>@8lwSfNLs*3K6Y8HY8pFzv+w^qnju?}=` zEv_3BFf`oui`HB38s)Y1E@p3b?kJ;2_NGYJIvA|K2wOZThE@){Iz!IWT zmb=ojn>axHk*@WNm#|?#ryIP-7A6D*A5>cTdo|$0zy_wt*5|Wh4A7KdTq}x5j*>>d zNsS<-Onv;6kh@edSCQ@WU%hfT|Am8wOD0sJNX6O>E@`mIbUE7(K2E^l-!83}Ve65? zhA+exD1HAM*Z&3=;)!XE8;%whOOJYf+vNZ+f!WFE0_EMq+z#c3X)R-1THq}k zZ-O8q{Ky1_Jbczc%3oH!(4xXZJ53ZV#RxkK@CxZiXo6Y4kNU zxWD2hAV^@QeJ-3OP{3V_)AxBK;e+ZLJ-PH+_$?K;cORGc<><#SV>Lji^H3nXzT?v= z@hm02n>z-&ZyQ3Jm$FRblvHC}W!XR8H%`v(cJ`K!@^h|)g;#ldh8Pj+UKfE*|0RyB z^?$WUf%a=n|Fx(8N|gRd{h!2=Rr?>a!~z4uo93Y<9i2NKPEOmZ&a4*+!|p$VivjD< zm$O%qQ~GlHuDoqq_qmo3IQ7M+u6_YA|2&3ZBmM>{^KXrQXT1N`;v)W*@YnOd*zsMO z!1e9j{Iv??<={~JoA>#g{t!gU5a_{FKU#kq3P%7rG5*2%kLZZMjO>h!`6pibJ^a6` z{co4Q1DG5W({=I@JFT9P={*MJW1b zIr_q6HMxJCzA!cIFO5q&wv~;-9~kUff*G+n>_hmbG1lECHA^uBM@upGO=+%E{70Gd zgBhHbQFx$1H9>BG9lv4so6vh zwZoxoZ_|!n1W>N!w8CM<%q`M-6f}ynD$bI`;4g*cS==O-=bSFB_7`k& zi`n2%q^_1CD`&c738b@xI95F6NFgE%CO((gcbpQng14OUDi=MRUBxpW^bgbuLPEAc zSb^Md(;kT755Ix4cml0J93l^5KIfgG5Iiz}C>TV3G5k0LRg(ePAJmfRDB3RAbZWm8 zzB6#2a#_v&QVeGidO^YGFqsIY=R9Q6s(T4=nda8@IOliTA4o&~^wnYA>w5otPX>U} zklOzZ7Jq?8xy$RpfKeq+Jz97pNDcG2`nLfu+Iz8H2<15G>o1M&>OWw`Vv393p0o1Shu8@P4iw^B4AeAd$6(+q@1oebp4bixa7M&k|tCVCC&nvO5)e6Ux zjpf3>D2*jvbjDk(vzsjF$FFl*5Kk81V6o_ESoH+UA42VYF*1##spSFO+vv-__eqh)rJJOFSZ2LJ&70uA`FuLFwj6CG!&p42mrtXl+=C#0Xv3d z05Q71^?$ZPC;-_%ep$7c_81epKzo_`zQ0TKXLfD^##Z;S`f z`CIq+o45a)`=^f`!18xz`&%RWzsx54x48g-gr=<8f1VKl00I1F0er;(DOUiC9p=<& z?5O3ME`l9<2X8xH3g#ycFk>d>>i+t|^-k-Dy7oG`5NCOKO6ude<=nVBY2UWt?@BVt zwLF?~Z7-kIsy_A5@oXzobWycFd6ad+XCm1f75L*gEm)`|{ur`zmWjVL?*5`9$GRBX zfM(V6z{OH#7KN*Owp3%<1aLpuZ@wvOwlg{Uh&hOQe#fl`3yyqdMKSYJKO82nUp|5O z3?u*MF&?KT=ixciO*fVO0fIXDo-QLY7C!_)4KoV`mt5%~9^}iX4bbo@Y>4 zH-1>4;7+%~iK~S}?4&n?E=~i%#B5`+15(IqnNnW&yauHVwh3M^j`xZ66nfg+_BGzs zoZgqyl3^`w3s*NXwb!WA@+?@e-e_z`rPui}?zaap1z7%HdeN{N_b@-fPx^CWVFSqSXBb^dKF7Yx-ojR*)sHsMPuER6y}_I|h7 zkfkRPKmdRXTC1YmV6AW*6huI|>~t7+jx)upS%>bZd}4_?N+dTm*dJb|sM>HAW7&jL zv$R4NZs{czixwNqxyqBu7D4I92<6arc$R6d+fpzO`xX4cXxb{FZ z4OR6{ksNYYX3B^$+DjNmXFe`8c8oROR|*V8RHD0!hNGhrzI!K`0|0#tFw%}-!pMku zP)R5QLp9y`B&-N^(8J14Ig)VDCFQRcan#O~0oW2nmGSceZv2InR&mLicEuKv>cBwp zRly`|+#R=?kFIj_^GC!?JRQD350n{{dNyQuYt*1PCoo1!gIrcvJVFRHBD0?_21}Ub zY@y-63EKhq+XcKR^1zPhJCIM1P2fl~I=T_kX2vU(ay=|j(JStNTq}35FHtL-PK&qR zw5ZSV%X*}Assvq5!iZz8d>3=aTgpH8#A!tC9f9mxPinA;0tyZKf&~aR-2W5b+vb$P zToh8Z)%hU#DIJ>R{$@EGUY5G0JYl-FMfQkZm=DD`f9Qm66ff!!KRH0o>X8!SYmz>i zP|NZ~Kx4&(`0JFzmKB_JjY+}pG*{Bp#uN&c#$7y5BS@F1c&w*Sc7^+DI&ypx_EGlL z9lzurO57l#C1-Vh>V+bV_=G-@MpgmXV1@o^@V43z#Q3Orf4MzC%R{;Lf-*_frP$D-Hz~sL3q5W z6jW@%x8M95C}+T74nOm@uLyJB+N?IZ{j?vIX^20V$T9)yqP!GGy=Y$uznrt0v&KZ{ z#`%fE!QirM@JEF2SpD3ae`bEK8Nf=_&`r+Z$qf(CtkHmGFZhFI)^`liZP(aEvrz$p zrZVrJH>Ldruk?E8)2%)AQ5d##;0N3V+>`mO#E(x@T$mAfY{TW1)Yr6P!A`bR*mAPK zq87?S;1ZrLmsU|6F1jSwy3Sxg))8@B*=u4~Ffy;4SVHx|=y(vkL`q-Qcf+(WG<-4w zUm#{F)u+J;1|v+RH1PUY#oLepro~Q#NM|2>IY05Mbh)$0PEfNR_(?E2t=}dB{vrmZciEmDoyHHeyXQ8L#9>+$Co#?atSI zY^Ha*6rTY0m93&#euj?Gu1{|LMU^8+OK($NE?7rHCr*@As9Uuwh$7!9uZCj<_i z^7o+RJX}oas1_tNP&m zU>+R+#xj9T_-sxE{GlDeR2dXyAx1As+IL{_T_j?3m<`ulf@#9Nayv?7mKk12v-$@Ztg$=N z4TkNeA8@LA(8(g4l8MO{=7#=SHPNF)_xeaNZif((f$jbBRSG*prP`Ze!#cn25+Mh1 z|IK}j8l&^@v>`FE%7nm)U7#35AW-zfQ5R*ve3=X-Q=nCE==)G@miI^7?B!^BK}%{B@<|e!@Z3r2b^@WS7nQAN;Ch@kpW5-+Ieo0WlfU8j2WdpeJ9xv?+TMABDWA&Gmr=nLstII<>K~c{WUm z8p^W4DaVLhXIZ(G34Iqk&A7jwFkVd!$hK3x0`dFaB|f{gHbJXBpq*{XhGhjGFzzuw zzFVL`6fDgTh>5-+J*trs9Esy~pF_@tC!{2rQw|z6(s zHr4C@un#Gpa6^;(J%<9)xlyxMv%}m#5-Dr(!oO{;vi{%?Ogh9FM@IgDS}73vQZ7gP z38q{1YqUyeS(~pBc3%=#vfb50>tGu_(_Eg0`mxckVzcQtVT5e^@%nL01fzUFUbvc8 zXMeR02NJiNN-SH$3y{>^satgHUJ}*z_sORJ41_4b-R#STBaSlsr2W(cAI-t6Mgo@T zA~tE!GAzxyk`;g_^m~qNc4L~KK>_vn+T|^t-bYXPo3I2Hw?p_0GL9cZ=*7Q>E7kY8WRr#dly=3Z>Ib9!S8TWTCV$nqq$vfN;L|g z%@F3JRO`e3tH^h?6uVpp^gif~?-$oMvA(<1P}~W8u)%d<*!1vY{fb26YYx|3JJv`U zK3~Ep2UK*;`LoEof{7NA;i**lTD-5IaT=RZvyG)}uEQj_OlM%RdCeOoJ$$(4N%^;$ z+z)Nvi);|3s1yBiDs_hEkR^XNbq;o5^_D5qLI*z>L*a$3(cbvdpFbZG+iblciEz}u zeDo)g7E6v!)_UXxc!~hzOIz{)$gBG^h2X`g@;?i`eeZhsyq^rok|Xzup~*~PL}Y!_ zd6j?4wL3kxtX{VIG0hpq0Q$P1vq4?ui;jkA1ns|)DdbcJC9=J;CSsfw9_$M|-%<8_ zGdfPsPQJ;fH2g@{w+epI1O0pxX`arj3U|&pCSY@rPAuXep$(QGKm9sV>>n|Y{U}ec zN_u?|cjh%f9r0sNLUs+IQOTN}iA@E}b14@sL2fnhyOqz4ArP=j18_*^`Aqx=Um@hZ z3;O1fZ@sO#j-m>^=lB`w2#bww_B!%rrzzrwiGfYJS=!t01rrNKbVPc13eE1t7#cD2 zTRS(rMe~nuhL0wO`EqrHMHN5#eE1T{yx1&C;mu!4p&ApVutLhr$e^=0%4%T_1XK(j zu2<~Pyyn2gwN&MKlh2+CAk3`;&%G|V;Icu>Q)Tl%9H8V^*ju)brGy18jTY%XLyt{f zWBbHgayh{WuL+6%hw*Mlvo2;OI2A_n;{6kCRpPYDib+85aSy4B1b~6@qmQf)mF&^s z0~#kZi;ItKgI*l3+|SnF1XNZ*V~pKM)(^4pX?%xGxQr`~e(}ufuB3CJ&@(N8A}v6= z3|yK%n6(f1V!Fj1clyZ_u<&Z1en?Tvj9{xv z9EZV38z)6sR3?C)=hB16_1zBCK+1#VxzXsX)>3vk+>CVXk-maA6B0k9D2wxCF^glZ z}T%j;ZI|+1O<6_Z-#H3Sfq6MWA>n4IW@Izh0j5;Fm#)M&L zFqv0T3>@g76T6fj*xSk-ObN|vNbP%t6Whwu`*r()1SQd!{iI>n9lcuHvy>aNYN-+R zgk%ia0V-9x4ARGXTJN$$J}`#H-EUESab4I$YSR4TT-DbufJEDM?K%xNB|~#5RMdlo z+)y#}oS3r))rp^s;%v1;>Xm8V92YrBWsZiMW9>8>*?M|e*zi=)9Xm|I70MtewjU~g zDuav8Ap35cCzT2dNtwwB2N5gOTY}?MD3m_noR-oY^}c(tE88XIm$KN52y&A^7zmdz zTy+wWsEx)~4faAsCz_OgrIIyqF__4aFJ-}-|% z#?1Yg=;tR3$M6avynH(}sz^^6pyGWtuofJ9gj>3)!`X`n?bqmbgXqY96g3tCC639= z97fxNKA9+Mwl1P66<);1^j*pNECkFj)NJ=kf3<^Ek}0CSpwX3HRBn(NO(QI~J&-?U z-Y2`VkhcnQG{pwGug2g-ZY4&t>;^%%CrWM}hiIizHh`iP*2b zd67<-o=QVgi|1MQ+)U$8NsZ?(wnz$|&YL_!s5=D7Z@qd5#6r&)jnYyG%!))2>B>XZ5V z7RrI9ioe3O!a+g8fSe znF=#>g@*(B?6h`hXui5yv1=+!5x{9VCm~No2SsrJJES=CUP|w4{-R0TN7PrX$|pw@?#^z`!a5CI~Er0t|ZNalBDFaRrTxIhi4;qv6BTv?2TVa0ZvlX zq}{$LOG2`V>Q0F*CjLeDS-|TU7n%#_$KpAlvp+Xghjq0an|n%Sh8Ek84NQ_$>Lc&U zGuEg2Wj|HK_p&d*%!WbElxM;*ON{`bu|!husJCAi2x1rP8gs&>4_sU`8y}DMqrOR8 zddO@5$-s-n^~Vi%CYZGGuwec|6(8jE%cqGhCp%B2MT66-S4o2vRFmtS7xYs?O2!!E zs=LV{r-42S}ag#;`Ms2;NQ)ce}9i;AbHW&8Ez;oQ?#Djs23)$ z!2=G9f43$iTE)g9N<*k`amJmMw<&8;eK2%FrI-3= zJnU&ceIzn)RG9SGn3NdAQboK>_hihCu2|FH)hJKJ0i$kk@v&`?**MZHitETuQbxpr zE*UoZbj6JuL&cv6D&!{GD%HF#Pl2Z6I#1(RA<0=qU{yctGvR=at^$mk+*Nv1?wqVz zL|`2=|B(IW=_Pi0G}#CT<7%--^`?=9|?1`fY+vUoRa5tY*VlRJO-?fO8?#}1duxQR(3 zD`&9f7bQNzrv&6z6P*mcZVd};mX$T>;<7wyiVIfc_GTrJx|pIb{aMqLx#!rx>mm$Z z;-PbEg?}}2xk36vfY&=Q{D_>i%+ccN>1JsI9;TY3sX;NKSqSJD>W%g zXAcr?Sd%-cO_i`i2D{lOZg|etj+9}@ItNpK@Bp=Sr_JIFT=C~0Po$_*@r+TT9JgSn zgvJ)vkw3NP;s1dLIY$pDhE(>FQV-SX{HfmKZHVgNaPBoyHA>7K>$v^i6Of@njgb%gx z-re^&5WDf`9G5%p^LI)156u+6uluLH+7$?tJtC3S=3DN_U`g}V4`dXlYXLOXw4b5E z7S)m|7`&6}6uf&31mD#j)DVl+Vz5IHuxhZ8pzrJY4G2xky;E{r$uZg|My?22bD&1^ z3XNIMF$r!BZxX&S)Ck2ECSvkp*3LSA8loBxoaHveBb5xQGn~zAYwQQ5o}LBy@C{EA zYnNmWtW)TV6;g3|hlJn@E@lkb8M0}SA#P0!5&?)bMv{L8u_~H)P1O1Dp4k0%N?;&C z<9HM@8LU}W+cO*Va=ii%@0$Bl;jX%$-#T#O5+5I6gt%T@G;yt6YJ5Mxq{$sfA%WmDwAAMG&Y|c zNv4s7S>i`|lAYGxm3JNQ6TobUIrxDUVV!LG$RjJ-q;xKlY4Z|Wn}8R00p3uy)j4mF z*-sDSNAuh6`Ub6sstl+yzD>_FE0Gs(F2oW$DaESAGH~j<4P2IUKSL?Prj6Y2gyqVk zcvF!dP^a%ci~>{)UXr~~5J*OPx-tXecHTu}e%Zs#9+;SVePSy$C)>&0yT`7mrw@XV zxv^~Q$c&DNzKJ2t=K9wtdZEW~0NdX-p(9vj;pLY5?{f|Ue%#ug0+Kjsp|v^Dr`zq< zH|4r7VtOKw3W^bCV3kSlgE;VL28n+3l}+LrLUzL@ag>dLQM|NuKm1174lB>31nk|a z5L&d06A#;h-{PZvSc+`AXal~FF2*OOYurhrNq5Bfau@Cn+7TxUPal027Z1}R>t_ zh?uXJe(tajTm=m1^?4?N`JYq}Oyp!{En>r>h$Ts7TDtw!*}h$8!G}5#9|b{u6%K6Vo8st-SnW0>ekV_38AD!~_EBs`I$dFy z`O^`al!p$T2d=w%sM&O8{{sD%-=9Gv8&`^LaBxpqB0~~$%&Aeww1$Fh)exq=wkf?`Y=ps+=5hBK344fZ~I~+M_Ecxb3{sXEKWRo*PS9&u>0)2s}SgEMX#&{N5 zzIo@xJD{0#J_LWU*E)*97RT1qWUdaWIakU~3Mp_UlNe7#4@nLPAugI2jqKr#uBMkN zHF#9EPs$I0OH@+h)~fp&F4Dg8mj77k*}fwpve^Ufj*InmoUSQ3Db(?UuJ6!I|@q}qQeb-x(_T7!??6+5ZJ66x%gDN z!$-15Pi~}zmz|DJr0bUWWl?6N!@{?rWL#_bUI@Hd-ZcL5rn7PC1nru z5=q8KB;)kxq!x!8r4c0P< zptU%DiFjFly4Y4$)wn8Qxq%+Ie#E)IM|%WXl4k~f9o&faVf>L)oPk3MIXWIij_4id&= z|Lt#|jN18GRal&Bme1Z<%t|PCT+ev$YrjnKT%wK;{KnP6i;$VsdA`>gGH&PwyPWs{ zZE%fyKd zmUth(J`;Udn=9eysU!Ei0}+&gs;!?scDE>Y{yF2T3pB+rZpJ6_*kbj2bfgbJB zJrn2gi~a7Nsp|i2q^KKera*1JU!4`*I!gAMf;WU;Iun3~iqGhP&u-DZvn!TxdA>^i z#afF_p(B|W72{&lytp~m90OI@&Pi@RYxBUyxW9xvV-{SyXd>AL_MUjcpz{j$u}NTl~BWuis#)1Y*` zo`U82+8YIpkoP0aGaFfc-LqyT8ERM+3H32vX5hP&t<|Ug^P;FfBwC>lZMu?6u>{vQ zt{Z^hTl`;2H6dHcF%F^aLjVx|PRaep5Fu;RM0HPz?;*u>HLRcM8WcnjvkYF|0w-#2 zdt)2O3)cAAqFWi$-i7#L-6-QgXD2pr6>Jj=OmLBx8RlZS2M|rm-9W*>6zL(3{tNiB zl8<{323RtEJYwm)aCnkbz;r2cPieFtU7!r^uThTHqv;rXgtW&_#M+_{>q1R4^deC{ zc~=A~X1C@@6v~yoJ3mxiS=jIUo<6zDC6oL5x)xI0AEDS26YiMC$af&&pAD63GrpEB zu=yf7;qUpAk@m3Tjbhj2IR)|-TXprB# zzTG3A;+`*O@flR2eT-w~ZKB;~!aFDGa*u1aQ-rvLQdwvdD1UD7`UGP3-)NQE#GDZ+ z`lI^byi|fPOy!c zs-_e|X<4VLniOI3^nG;Dfr=E&5%}@6IP}7Uevh-kboG{*S(D3v5TZ4-FgnUWm`zN6 z3pZ=df!+i;0gT$i*{eRFdWo0~J7gpf2hkrk>Zsl=;|Q@Ke5DnHJ>Y#jD)LsV(|g7@ zyyH2Up-M~5X<~#>0pl4dkN$^d&`c@*skD631X?LeaG;YZ`Zarfpxoa!ro4a4 z?*m3Co*2?nIJ!GG<7zfl-M6Z|gO>Xka#V61Y}shepXv9hH18DHSx3s1>?xBAWXeiY zSs473>K=Er>gbt2^g@viki?X}KGmmRA9$feHh;%>l2yxjkX?YiOf$R9qsv|(KTZ1{2);>O zY>|V4FEmvY)xzv1FwHDD0&`Bp{>kORtECSZMg~1&XP3lB#x8{BAC^JQH}_3}_7i7> z1_oMx1@#fe`B~MrOHPXMqnQD>n2B2RPRj8Bxg7T}zBtPXur{jl95u(GYBeMg@ggCLZne=-TqE!V$ zYO8J%&pSKdbTGcee2yZFl0EyxzW;&SN^b?x)UFiBV=HOv%pM(=mxsB~5wT?@S#A^Y zP|59et$sTJYnPCZysGbgZu)v1fmj|r9BzAo^puUWx!BTl35%$j4v(d$4aE(I?Vt#1 z#be<6L>rqY8E!vz-S8zIe`86V-kNff3 zl!u$B<~JLE*{Xfs3F*>VI4ZRrjy?&|UpvIP$|P0*{{Djsz1RtYl01AxzJ=Xn?1LeU zu<+3YUFJd{(>6mBZ3)T>rz@|2JAw?qph1x{lq~dUXCL+OpAI>w9as)!NFc-6JtI@G zzKJ72J3wE4Xl8W5l>eP%0LiRVr+Kzxo^Ag+&q_ z%6B-pS3{JuxH(mH$vDtKy9{y`ojHGg4M0f_i>rI#n_~qX&@vk}6Y;|{HXR1fplMG@ zl&#nB6#njMg<3Qnx=Vr8(+Ga&HAn6X z3*CmxjQmPUOht&);v+D(W-QbL%%^EOq2gHh&)(s}FVyqmgHg*g;G z7z^HsHhRS}^Xw`jo(d3sHx3i=l#I@A**W2hsK)C&%515H4mpE|9%E`nk*;q?` zb9Qm)2L*$q9h>r#2UE%z*w2o7gUf}-8I>KHM4JI_;Nr7AEl*n>D#?FNnu2l%m6>8K zl}3wMZR37~DBIN39`(H{3!%r~Cxn)$pheaBXBQ$Vz|?%kMV0Dn8-Uq7s$_s~$_8(T zO$RZQS>-H2=_l;4%{RcopMPR|PWDP1)|if3q=Z0|xYS|9wwPdPeby3aBI_mX7Rg*h z@9=l+aVRSeEb0w=!L8Eb|323DF?ia7g43cBNR9vf@Z0poZ#RbKMHk9CTr@eyPh#aH zD{w~Cv2dn!`+FvkC3P+-8TuHXjg&4(_(K_AJ6j1`q&6}n2dROY@h;go{O^}$&ht|m zRE%=wkI`w7ipy9zIxNh_I+`|plR=o$)!q?fNRNw3t;U^*S}d0aB!j-J*2$jxlo?6C z5>l7?tIp3|nrlH{_p`^>=?8J03)7{{;k6M zs0NT2!m{hj)1DKj0xnR~?`O5#YW!mu-d(w4tGPiH+g}kIX$=P=prFWqBWEkn(%4cU zdxmz#sNCUPf1JzJn9Xf`S)@?P&5h?MT)54CPsr2m(SaSIK7;jXkz{i(Sf!0z+LAh{ zZ2R6jGvV8qqRbc-T?+3{zZGjOj`d0B!|T*q!d*bH#a?!ir187pw?)rEyqLA!ng*UA z?%!yhEqHXU9_l!qr}8n`g{>~`goR4mO~$3mB53hmW%nQd!~x@&UziTziwv`o@y6jlntoOxBd{E;I_mS#}1=l{k-4 zoV*AtMV9JQG<_5?N@?GO_pz~_`|EX*SxW7>&F$CYMO03SwwI~rqLoKzxryoe9b&tQ zt5TiX%IWtv9g5`Ln)7+eVtZ?=@W00%i36%AVS~M0 zM|sJQ%-A*kYJ00s<*`1Q^aOe(_GW?Q{zn-jxjNbQI{M}MHW^j~+Nkzg{N-Xt>H7ql z@%A#z<>;;H;%jPA_F^dI0G_lR1JzJ_1;pDTpgE#gS+`t4@vWK_F?5t#joi^yFQv@A zAXPw!=AUmPAvc8G&->KvGnYD^N}5xs_12#|sKc;7b+xL`c3LB<5JEDkoC+!1d3VCfgUKzYjA@isU_iOnguvEL!8Ocb#yX7nY&O^UY)b#Ps z#A;A#-ntsw8eI6m8>MqjOMy3~lE9ynLDK3)j#dJd3buTuQH^O^gVxUHH!o3LTJ6pB zTXF3}=Z5dq85j;y+A)|d<%|j%>80rzL}gV62eQ-z{B(0;GKmswa+c^r0U(TPWLYtG zV!)8+AjTJDnQnIQ$&g_f26Ma=4?FtGkfbFB3bGUzJL>F^APD@f_7tQpZ-bIzGhVXI z3Ozh~p^iF?em;lVf_|=!+=9VVWxV?=m1At-R+AQ?4aU&Ylg2Q|mJU}2V{qw7shiD8 zhlzuc?+LQvg{TlI9qhQ*3&k+*p-9E#Be2!lHlo@0*ybg9Og})Mn<=+KPq=0|-(L1; zw~e=w4Q043Qek=ospKWO?goUZH^oDwHT;E5X=@MK{0hEcEbR$&%IeCUoxB+n)|PsE zxzxuk^?zt*)rC3R`DTPwrA4r}n!}KTthb`KfhlOAhUpaRv%qQuj-jTe7wl6JE4^lk zC5w)_j0{?_{YPXkLxNT?4FGRNw059}5K%j*haQoc@2l4@BE-jo-H$%dqPldDG0exk zo-n#E>P_~0==W2ElqeyC-uR)jDJRib!09@xt_Xl=_;D zH&jOU;$$0WAfr62O8!c^{g{Yo7JW}Qb6qGxn z94Gv8S0}HlLF|xkL#QWTx-;)IFQbS@6GL4xGzw}uv)0{*oJJalQw`{t=drEuRYO_l zHd*T1GEHF1F_85VJuVu{H%cvO$Rkiirlw0%kfo?9MbT?!;cNrc)4eJ$Ws&p2{?ZOO&-tLjHCIc1@i$Xk%5~R9OEx+or%bJM5Vjz^W&}^U~9byK{h87Rn z1qe@ekde%f*2>kuXYcCt;L-6?;8S7ion#Bx;X~!$-SVtZoWvx7eMRBTJWCJJSDPrWYp zeApq5y0yH^KYv>Ychm9&Ea+cn=No)OZt*{L*oQ2Q*qaZJnGeU94~_j}B{AkD_U6UN zB_b#RQZ5})KZvN#dCzDOMZQPdRiglrPyk3Mk6^+JCd>s$Xy1JqG4&o7im;lQT3T&$ zl^$6UB+aaBg`WFN`9qpX*>W}anbLJa1uHj8Q3F@sH|@s4@(Bu&tbY@ zDX+Mehk7hG^_*RVv2|V7y0w1L+1O{Tlexu<(BXt4lWYv_-P|T=GA%|`3r0%(q#WYc6ZzuU@3$zl1Sy-Ec?)$6Fl0QX0;HRvX+w~!dU zg0KDgi#h^<+$_0C%90g%^W&#hUX9Osfn_FRx=q>_8GgVo0PP*b#?e}$*y7O|qUhq$ z*ncF3C_3f8#Ga^p0fxY=R0-Q2lE9{3fymV_6HPYewV+e_y)pVY^oArfk|FWLGX`YVv>{p zn_$e76I1+;Ak33}qWB;ABp`)FCC>5hLI%YF!1{M#0ALmm=w9Qz!URK^_Q_sjymI^_ z(7{lo{~G;w015_d_win%y#8Bo)BCvn>Uu0L`dIj?`cC@?>rc#|p+9@UH~&f;{rljR z_8se~BzRZ!x^T}Tc&GQ;^pW)y_Z{gE%pb^~{{NjQ4xa5jF}-Jf#&t$&wyy8%lQW&6*VJmKwE*k06jpx0Nz4^`u--GxBrvWg13UV zp#LV|?LYbO_TkOoRdM<3|HIny+QPrHSYPm?;79IX2>xGm2>u5Zf`8y|n0IRW|DnOy zI_-avV*PJOr~D;6O@0jdn2Iw$xyM7|X$-NFiXlI_z(c}*455;WIzNZkbI^bH>-D^! zy%1Xgo+{!|gr`JIvCwz|ru6>>4!YF;CgtS6NH~$(i6xl+YmspRc#ix>|^Q!17A|Rb~#2 z1pzF6WkC!v( z$4JgW+}$|0+A`RG0W2ru{uemaP}NXnWjd)T1`5)4r#Pi$I*BO;=`Hk(^O@yLDo9w6 zvZ87_1_{I@Nb_v-Y-~Y9K}05S0?In1sZZaGsR8VvsN` zxe>Yl+4{EzZSh|X|8@X8BxInS91|576A1w(95NtoPS%ht7cUpjBxNCGAt6pqmYggn zM@5B+iGTnd9^pOd_6Lb8?`zmcuoIJeL>XGD!0vwZ9^^4$0_oylk{opa1evldKS2QZinF` zrD!Mzenm(p3p7@+u96KqWaLTgn<@%`;-sIQAH*}7}bSO*|aFHIe)25y`85H=laM0FD2E{rkB_Wc)(9hlr+xX%4R*gOMJQvNJp@B~mJOg1^WA1;+qa`UUN@QOi z)D^_jK@NFPwD9qB$%-mvjOO;Wt7t+UZ)+M9U{E>yy!C5HZeTayHzm*WBJUwxizW|BTs2x6j@WIE`86)N*ez!h@b z!DBK`F1s~WPChOGYzP}I|FYYk68b1SKX-nD8x|Md4lE7^kw0Xtx!3LN!nxTQX)u*d zHQ3*U&b-(37k}=b2hiXsroZY5D2z zL6zt?J$CrX$BI!+2BIqXzAKwSdbw+xZs%E2zqzUjM2yh6skV(I-y8s`xXjwny&%x# zK`I{`5xUbXbft59H-$TH^tSo}Ob1(9Br4>%j}w^hmnM{_&)xW2d6T7B`S1Ua=BbmT zc{M#ed9ia1I$Fx|^rE^HSO(Lpec4^piRr zE&MogL}woDwq!_u&Vs^$#7cFBE}Le&SL}v1+=K$_533q_CRr>IixML~PZbI&P7cld zazCjsF9z1~yy4Q&TogNx`eDoxgW5*}oqmx;wcqJw{0pcG?Wsp$AI2D9iI{CWTUmzg zLlrjK@&~Oa+~Mr(wU9$*Q*$=eXd|8OUJwH|%)iWbaeqtP5Uz8A`p`HE?X`mC=fp>P z^YnaBtuJ>M1$0Gi-MWqylE} zYT`7i{zkx!NG1|I)>f+))h%gz|XrCYc6o7g(uAM8L`@1eH(b%~$z-!te#?!S~ z7-3f0%JPV@sNJD8FrWI8s)CL0v@fvK{>L)p)DCD9#qhg@&HoeUy;eoUjVz2z-|5`iB~^%qae4tV_yHqX8NjS~#TU4x{(C+I#XW4~tuuF4VH^cv%Fr-@fS#dt@b z&v~yzY(ZYN>JQ1GDtD~=RdFhLRelj8FfjLq6Z+*`qR~m4;k>o+Eh~XTAKNRaoXy)c zB+WBNQ^YY8II&uOTmchRCICiCWQ0OSM_*jMHEcpHVlxPF3V#3 zMoQZIOX?Yl8P$PiG6p4H$VYepvhSN59p0dF_$&;6SVu>>1F(C7Ef}Ugr^>yXXZj|z z=-TTXO0hmAz!#0da(4F~e(9A8QA>&=>Qu)Aa@vr2CWZamA>(xw>^Xtv)zpg8BvZ^O z=^sEelQUP~A{#S+(z7%% z=*`s}gJbf8eGdF*)e6&t)XPosI=JV%%zKbe-kB;H(#H1TPrG!%FZlTs{*6QBbE@Ja z2+b#@OQf{tyO<_Pwm}Pts!)78X2($J#yfwO!vfHezf^kH{Hl)X`c3YbVHp&$gkx&O zg2FP1(AJ;3Ms~JS;a|XA#3M#h037z8Y8>;khd7jJz*HPNNvN;EY9;$g-B^kzSw_n9 zZRx=p%!j~tuRIp;3}CXc(6i1Yx7yT_QJ9c+k*@ zK#!0=4c4oR7u${2h>^nwuTQV8Sp0?vf9Im$JAt?w5}PB65EO^ z*^sRif|zn}by_2HD)qta^v6MXCH60{llIKT?89R(<6&>`#vL0}f_}LkVi_2D&Lv}r za683C_7|k6sjAT5Ll3|H{B?+I*o&2d4l$ciMqIkNF*~UrH5$*$z-?vgh|T9TZA@Vl zI~?GIqEM~OEycuF$m+v7gXUOKvt>3}=V-tX3$?0FqjlHUj8qxDq%|IjlqIl3t^wmi z|9x*strNJ8UD!DLxQ|d!w0|QB!^fTxu;jEVC&P9!@G%hUnWbjLMhr<`?c=4iLcFBG z3VHUVsR9o^qNVVN^G4KOsN@n_T@Gd?B5WYoN&eMf4@ zJcK55XNvzPxd%r0gs9R;W-*O4D2KoyAy_hMO~4TtXhMg7Vh4-nQ&|p9Nel4zXwz;< zq3jnZ1FIkRU%O>zo=b$Xd8?8%mqB59X}WPn#So-2DC`Y{|#K9kgb=c!%7cG-2%6a)Vh&<}?6UDWjSo)_HC?{TeHFn%wA&NvQh?tlhV^ z*4ha{O|%^ht3pq+J%ph_IQ1pQ#V}exaz_^dv_!vVWF{vWq_;YqSPZZm=lf+IiJt+ztvL* zr@m5kfY12SYHZ3492zLQhz(lWYL%*?hgOpza2v9zeW_L+l-fPKW_ft};VIUS4gTP# zzoqp9-ba~FS16;SA}4Csm&#j{kX~m%1C|{~+ksCWGlh&`sVsx^OaW7fc*PP~PZ^*I z$@GBPD)Znu6aBv1k*&(UgdZwCa8N*wmVo{;e0oyNC6FzT!aie^wZd6rIQW8`K)z(S zQD4_J&HOs72uI$uDO_>0&n!X389+rG8F``w_}5MJFcrbbdJk_>4Jfk=jc;f<)7lOh7HdFwyp+_nL?UqO(P9b7>PlSaY$Kp?AON83*iy zid>XLRRfQPN?>@xkSS8vYp4o?SGI=7O@b6S*H_mAJ6gS~Ji9yN>lP&^N-@*8pdM+V zFkngmMW({f3i2q|9Qz0vM2w5tLrWQiGYu0(s~9=5L7fUyG5YBYu&1paU_#1J^^Tn@i1CsR z^aE1%38@$W00>)()|XDaTrP_l5{}C17Z%OhlT69g4Tw;xI3=BY9HBiL5$?erR*B;U z#|=~9Et~;?S~M*UL>V!V7_6ikK&G6=$-~%~4kS*(qD&Vu0kCh%CSfN~iV%m%J@Js= z0bp!kHTAm#bikFIl*dSGYROoi)wtI*c-TN8dH2;^mKAnY+Oh+Oijr+SPBO&s?lfx8 zvt}C0Sfql-h2FA{pwmmon)c&>M^I?Q9XnF&y|0cldFY$9O-NiH3b zvvC9rRO?<=Fm_8|%-@dPE)K*HKhEnj5to8ce6M2P`3REM`!D4v)fc@3mVFEw5MwYf zRs%v?1&abA2)UNRNGTI2gKr3StKhZ-jKM+kpao2*KG+mdf5=!m!%9lhhORei;Dor| zlGO}}DFi0CuojfhE=C=Wc_pcFW-MkYPcD_q{RAdaxo{s-7QxY|4h6>0SYk&2whYmU z^^P(Mk`RkDND0UswrD}Kq0G_aX0{jKOe8>0Gl}Mu!WZgoX@KCjSP+UH?qZ&dx;I0) zhDHB!W>EWviie<*NMT!vGL8O^0!NUm^L>%{C}Ze8H^UJvAofBK!4m30TO&Kk#Iziw zwuVJSsRH<{N*}o@NqsO=HMc}~lvNog&1uHA#-h;^LK{64WTGVr9H&MVY|CSV)J%z> z`4Ll6T_kNSYt-VPUBIy@D&&C+QcWmnTN(z~o6|6jg|eJ7a&?QCWXW_4z#wAEs~VQm=(F&W!}1osnQ&@(ViOTva*>x^usPR zj`DF5H0}zfGQ1@TuTc#=8KqJ{dlH@zYrc-Up&vIa?$&+PXmYaN3LC}8cAi)1HR)LZ z#*1vt7#{F7vb2NmYhC%<4*?>zMBCEB4Q9rhyIlO2JZtnu;2oUp5drb7!-L50{5TBz zLN1^?w+LBr{+8EE3-mIt37jlDrJjtrn`SG(xF@013s~rw9**q@)xEpZCBgjUQ(0K< zH?|Keu@)GB$ppBpt5XS8nWHQlJD&8hM1rymM@7~af4VD_{~GgJl0u;f!!v+$sb&ds z!7Laqb}{0#@$;}o@f9-4p|SN;w;&oKHlA}i4mPn62wa9r&=5z;a{m zVPe0lg~I#rG1Meg3gDBuwiaRYdkK*1SITnF3LcUj?i_>gbJmMy3Yy@`q9s5eL5A{> z3Wf{yie@fC!89B%D&x4)@FaOPjp4b@n%ARylrM6CO+;4>UvLD5zX%0Ryguk5#m~C` z`hwr^T|A>%j2^Lu2T3lLWQD_md`Aa=gmq99xw$(x0$zaM&s(9j#w!tZrW1$&zr+_( Qnna<*fIl{EU=ljrXq - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/css/fonts/Open-Sans-700/Open-Sans-700.ttf b/client/css/fonts/Open-Sans-700/Open-Sans-700.ttf deleted file mode 100755 index 974a7c5edd7f14486c43561220b54244da886885..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35924 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 J*BSue{{xGvL4g1O diff --git a/client/css/fonts/Open-Sans-700/Open-Sans-700.woff2 b/client/css/fonts/Open-Sans-700/Open-Sans-700.woff2 deleted file mode 100755 index 51c75cadf14fc03d3971457f5525730a7a21c12f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10284 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 VjUyFw_tG>yE$3x9SE1bvTd - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/css/fonts/Open-Sans-regular/Open-Sans-regular.ttf b/client/css/fonts/Open-Sans-regular/Open-Sans-regular.ttf deleted file mode 100755 index 0dae9c3bbc0b52ccd98b060849e631661a9bebdc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34156 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 diff --git a/client/css/fonts/Open-Sans-regular/Open-Sans-regular.woff b/client/css/fonts/Open-Sans-regular/Open-Sans-regular.woff deleted file mode 100755 index ac2b2c65e3d1fbefea04e2caf9e2242f0d997254..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14260 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 diff --git a/client/css/fonts/Open-Sans-regular/Open-Sans-regular.woff2 b/client/css/fonts/Open-Sans-regular/Open-Sans-regular.woff2 deleted file mode 100755 index 402dfd77bc2b8b5fd212abadf5d24f6c9cfa5d8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10352 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 diff --git a/client/css/style.css b/client/css/style.css index a9449a68..cd24aad7 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -28,51 +28,6 @@ url("fonts/Lato-700/Lato-700.svg#Lato") format("svg"); } -@font-face { - font-family: "Open Sans"; - font-weight: 300; - font-style: normal; - src: url("fonts/Open-Sans-300/Open-Sans-300.eot"); - src: - url("fonts/Open-Sans-300/Open-Sans-300.eot?#iefix") format("embedded-opentype"), - local("Open Sans Light"), - local("Open-Sans-300"), - url("fonts/Open-Sans-300/Open-Sans-300.woff2") format("woff2"), - url("fonts/Open-Sans-300/Open-Sans-300.woff") format("woff"), - url("fonts/Open-Sans-300/Open-Sans-300.ttf") format("truetype"), - url("fonts/Open-Sans-300/Open-Sans-300.svg#OpenSans") format("svg"); -} - -@font-face { - font-family: "Open Sans"; - font-weight: 400; - font-style: normal; - src: url("fonts/Open-Sans-regular/Open-Sans-regular.eot"); - src: - url("fonts/Open-Sans-regular/Open-Sans-regular.eot?#iefix") format("embedded-opentype"), - local("Open Sans"), - local("Open-Sans-regular"), - url("fonts/Open-Sans-regular/Open-Sans-regular.woff2") format("woff2"), - url("fonts/Open-Sans-regular/Open-Sans-regular.woff") format("woff"), - url("fonts/Open-Sans-regular/Open-Sans-regular.ttf") format("truetype"), - url("fonts/Open-Sans-regular/Open-Sans-regular.svg#OpenSans") format("svg"); -} - -@font-face { - font-family: "Open Sans"; - font-weight: 700; - font-style: normal; - src: url("fonts/Open-Sans-700/Open-Sans-700.eot"); - src: - url("fonts/Open-Sans-700/Open-Sans-700.eot?#iefix") format("embedded-opentype"), - local("Open Sans Bold"), - local("Open-Sans-700"), - url("fonts/Open-Sans-700/Open-Sans-700.woff2") format("woff2"), - url("fonts/Open-Sans-700/Open-Sans-700.woff") format("woff"), - url("fonts/Open-Sans-700/Open-Sans-700.ttf") format("truetype"), - url("fonts/Open-Sans-700/Open-Sans-700.svg#OpenSans") format("svg"); -} - @font-face { font-family: "FontAwesome"; src: url("../fonts/fontawesome-webfont.eot?v=4.6.3"); @@ -130,7 +85,8 @@ button { border-radius: 3px; color: #84ce88; display: inline-block; - font: bold 12px Lato, sans-serif; + font-size: 12px; + font-weight: bold; letter-spacing: 1px; margin-bottom: 10px; padding: 9px 17px; @@ -662,7 +618,7 @@ button { } #windows .window h1 { - font: 36px Lato; + font-size: 36px; } #windows .window h2 { @@ -679,14 +635,14 @@ button { #windows .header { border-bottom: 1px solid #e7e7e7; - line-height: 50px !important; + line-height: 48px; height: 48px; padding: 0 20px; overflow: hidden; } #windows .header .title { - font: 14px Lato; + font-size: 14px; } #windows .header .topic { @@ -727,8 +683,9 @@ button { display: block; } -#chat, -#windows .header { +#windows .header .topic, +.messages .msg, +.sidebar { font: 12px Consolas, Menlo, Monaco, "Lucida Console", "DejaVu Sans Mono", "Courier New", monospace; line-height: 1.4; } @@ -793,7 +750,7 @@ button { border-bottom-color: #b7b7b7; border-radius: 2px; color: #555; - font: 12px Lato, sans-serif; + font-size: 12px; height: 34px; line-height: 0; width: 100%; @@ -820,6 +777,8 @@ button { opacity: .5; margin: 0 10px; z-index: 0; + font-weight: bold; + font-size: 12px; } #chat .unread-marker:before { @@ -837,7 +796,6 @@ button { background-color: white; color: #e74c3c; padding: 0 10px; - font: bold 12px Lato; } #chat .unread-marker:last-child { @@ -1009,7 +967,7 @@ button { border-radius: 2px; display: none; color: #222; - font: 12px Lato; + font-size: 12px; max-width: 100%; padding: 6px 8px; margin-top: 2px; diff --git a/client/themes/crypto.css b/client/themes/crypto.css index 51046c54..ad72ce6b 100644 --- a/client/themes/crypto.css +++ b/client/themes/crypto.css @@ -30,7 +30,7 @@ a:hover, #windows .window h2 { color: #666; - font: regular 14px Leto, sans-serif; + font: regular 14px Lato, sans-serif; border-bottom: none; } @@ -52,7 +52,7 @@ a:hover, #sign-in label input { margin-top: 10px !important; - font: 14px Inconsolata-g, monospace; + font-size: 14px; } .btn { @@ -76,7 +76,7 @@ a:hover, } #sign-in .remember { - font: 12px Inconsolata-g, monospace; + font-size: 12px; line-height: 30px; } @@ -101,8 +101,9 @@ a:hover, color: #fff; } -#chat, -#windows .header { +#windows .header .topic, +.messages .msg, +.sidebar { font: 12px Inconsolata-g, monospace; line-height: 1.8; } @@ -117,12 +118,9 @@ a:hover, font-weight: bold; } -#chat .unread-marker-text:before { - font: bold 12px Inconsolata-g, monospace; -} - #windows #form .input { - font: 12px Inconsolata-g, monospace; + font-family: inherit; + font-size: 12px; } #footer .icon { diff --git a/client/themes/morning.css b/client/themes/morning.css index c9af079b..14b6c230 100644 --- a/client/themes/morning.css +++ b/client/themes/morning.css @@ -29,12 +29,10 @@ body { background: #333c4a; } -#main #chat, -#main #form, -#windows #form .input, -#chat, -#windows .header { - font-family: "Open Sans", sans-serif !important; +#windows .header .topic, +.messages .msg, +.sidebar { + font-family: inherit; font-size: 13px; } @@ -145,7 +143,8 @@ body { border-color: #242a33; } -#form .input { +#windows #form .input { + font-family: inherit; background-color: #2e3642 !important; border-color: #242a33 !important; color: #ccc; diff --git a/client/themes/zenburn.css b/client/themes/zenburn.css index 64f83b42..51acedae 100644 --- a/client/themes/zenburn.css +++ b/client/themes/zenburn.css @@ -30,12 +30,10 @@ body { background: #3f3f3f; } -#main #chat, -#main #form, -#windows #form .input, -#chat, -#windows .header { - font-family: "Open Sans", sans-serif !important; +#windows .header .topic, +.messages .msg, +.sidebar { + font-family: inherit; font-size: 13px; } @@ -172,7 +170,8 @@ body { border-color: #101010; } -#form .input { +#windows #form .input { + font-family: inherit; background-color: #434443 !important; border-color: #101010 !important; color: #dcdccc !important; From a959e0ae44e985fbac01a9aa4e96ee829bcbedf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Mon, 15 Aug 2016 00:10:30 -0400 Subject: [PATCH 0016/3926] Bump request to 2.74.0 Diff at https://github.com/request/request/compare/v2.72.0...v2.74.0 This suppresses security vulnerability warning. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 723ca233..3e7ad02c 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "lodash": "4.11.2", "moment": "2.13.0", "read": "1.0.7", - "request": "2.72.0", + "request": "2.74.0", "semver": "5.1.0", "socket.io": "1.4.5", "spdy": "3.3.2", From d48830a1fdbacb15e490816f7f753e709661ea37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 16 Aug 2016 00:24:20 -0400 Subject: [PATCH 0017/3926] Make custom highlights case-insensitive --- client/js/lounge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index b0193469..167ba0b3 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -204,7 +204,7 @@ $(function() { var template = "msg"; if (!data.msg.highlight && !data.msg.self && (type === "message" || type === "notice") && highlights.some(function(h) { - return data.msg.text.indexOf(h) > -1; + return data.msg.text.toLocaleLowerCase().indexOf(h.toLocaleLowerCase()) > -1; })) { data.msg.highlight = true; } From 40b8f0c2939ee6bdf2e796f3998af83c5bfce15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Thu, 18 Aug 2016 00:02:40 -0400 Subject: [PATCH 0018/3926] Make sure users with wrong tokens are locked out instead of crashing the app --- src/server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server.js b/src/server.js index 88401bb8..872e4308 100644 --- a/src/server.js +++ b/src/server.js @@ -286,10 +286,10 @@ function auth(data) { } } else { client = manager.findClient(data.user, data.token); - var signedIn = data.token && data.token === client.config.token; + var signedIn = data.token && client && data.token === client.config.token; var token; - if (data.remember || data.token) { + if (client && (data.remember || data.token)) { token = client.config.token; } From f509e9fe5ab7e9e37bf4954dbe98ffa626c01e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Fri, 19 Aug 2016 01:16:54 -0400 Subject: [PATCH 0019/3926] Move border-radius from #main to .window elements to fix radius once and for all https://github.com/thelounge/lounge/pull/537 only fixed it on chat windows, but for some browser-specific display bug/reason not on settings, login, ... --- client/css/style.css | 4 +--- client/themes/crypto.css | 3 +++ client/themes/morning.css | 4 ++++ client/themes/zenburn.css | 4 ++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index a9449a68..fc257e9d 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -587,11 +587,8 @@ button { } #main { - background: #fff; - border-radius: 2px; bottom: 4px; left: 220px; - overflow: hidden; /* Without this, border-radius has no effect */ position: absolute; right: 5px; top: 4px; @@ -651,6 +648,7 @@ button { #windows .window { background: #fff; + border-radius: 2px; bottom: 0; display: none; left: 0; diff --git a/client/themes/crypto.css b/client/themes/crypto.css index 51046c54..acbb0366 100644 --- a/client/themes/crypto.css +++ b/client/themes/crypto.css @@ -38,6 +38,9 @@ a:hover, right: 0; bottom: 0; top: 0; +} + +#windows .window { border-radius: 0; } diff --git a/client/themes/morning.css b/client/themes/morning.css index c9af079b..0910ca77 100644 --- a/client/themes/morning.css +++ b/client/themes/morning.css @@ -70,12 +70,16 @@ body { left: 0; bottom: 0; width: 220px; + border-radius: 0; } #main { top: 0; bottom: 0; right: 0; +} + +#windows .window { border-radius: 0; } diff --git a/client/themes/zenburn.css b/client/themes/zenburn.css index 64f83b42..124afa0b 100644 --- a/client/themes/zenburn.css +++ b/client/themes/zenburn.css @@ -97,12 +97,16 @@ body { left: 0; bottom: 0; width: 220px; + border-radius: 0; } #main { top: 0; bottom: 0; right: 0; +} + +#windows .window { border-radius: 0; } From 66f6a623d82e314ea85cdefef10ca9947a74bfc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sat, 20 Aug 2016 00:23:56 -0400 Subject: [PATCH 0020/3926] Allow long URLs to break onto next line on Chrome This fixes a bug that displays a horizontal scrollbar and messes up with the layout when URLs (or text) is too long. Fix is Chrome-specific but so is the bug. --- client/css/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/client/css/style.css b/client/css/style.css index 7663d889..7e531b8e 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -815,6 +815,7 @@ button { #chat .msg { word-wrap: break-word; + word-break: break-word; /* Webkit-specific */ } #chat .unread-marker { From 727e7deb5eed9ac7c08ab888aa6c61cb9db8a621 Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Sat, 6 Aug 2016 22:43:57 -0700 Subject: [PATCH 0021/3926] Mention wiki in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc9ae834..88d0f6e4 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ When the install is complete, go ahead and run this in your terminal: lounge --help ``` -For more information, read the [documentation](https://thelounge.github.io/docs/). +For more information, read the [documentation](https://thelounge.github.io/docs/) or [wiki](https://github.com/thelounge/lounge/wiki). ## Development setup From 0c3dc31e31fa85efcfc445b8a65bfd8817a040b6 Mon Sep 17 00:00:00 2001 From: Maxime Poulin Date: Sat, 6 Aug 2016 14:39:39 -0400 Subject: [PATCH 0022/3926] Add debug config option for irc-fw debug log --- defaults/config.js | 9 ++++++++- src/plugins/irc-events/connection.js | 10 ++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/defaults/config.js b/defaults/config.js index 254d2a4a..09cd40b6 100644 --- a/defaults/config.js +++ b/defaults/config.js @@ -357,6 +357,13 @@ module.exports = { // @default "uid" // primaryKey: "uid" - } + }, + // Enables extra debugging output. Turn this on if you experience + // IRC connection issues and want to file a bug report. + // + // @type boolean + // @default false + // + debug: false, }; diff --git a/src/plugins/irc-events/connection.js b/src/plugins/irc-events/connection.js index 72b4b7cd..fe356102 100644 --- a/src/plugins/irc-events/connection.js +++ b/src/plugins/irc-events/connection.js @@ -2,6 +2,7 @@ var _ = require("lodash"); var identd = require("../../identd"); var Msg = require("../../models/msg"); var Chan = require("../../models/chan"); +var Helper = require("../../helper"); module.exports = function(irc, network) { var client = this; @@ -74,10 +75,11 @@ module.exports = function(irc, network) { }); } - // TODO Add a debug mode. See https://github.com/thelounge/lounge/issues/459 - // irc.on("debug", function(message) { - // log.debug("[" + client.name + " (#" + client.id + ") on " + network.name + " (#" + network.id + ")]", message); - // }); + if (Helper.config.debug) { + irc.on("debug", function(message) { + log.debug("[" + client.name + " (#" + client.id + ") on " + network.name + " (#" + network.id + ")]", message); + }); + } irc.on("socket error", function(err) { network.channels[0].pushMessage(client, new Msg({ From d0ed6826be3dd325765e40d57f5c2ba99bdea168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 30 Aug 2016 00:40:17 -0400 Subject: [PATCH 0023/3926] Do not set app orientation in manifest to use user setting at OS level --- client/manifest.json | 1 - 1 file changed, 1 deletion(-) diff --git a/client/manifest.json b/client/manifest.json index 51780d6c..5c047d55 100644 --- a/client/manifest.json +++ b/client/manifest.json @@ -3,7 +3,6 @@ "short_name": "The Lounge", "description": "Self-hosted web IRC client", "display": "standalone", - "orientation": "any", "theme_color": "#455164", "background_color": "#455164", "icons": From 34036a4b7a333609c25bc77479add45df793783c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 17 Aug 2016 01:43:46 -0400 Subject: [PATCH 0024/3926] Allow ourselves to have decent looking select elements in the settings --- client/css/style.css | 5 ++++- client/js/lounge.js | 2 +- client/themes/crypto.css | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index e5036c7f..9d1d7225 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -581,10 +581,13 @@ button { outline: 0; padding: 8px 10px; transition: border-color .2s; - -webkit-appearance: none; width: 100%; } +#windows select.input { + height: 35px; +} + #user-specified-css-input { resize: vertical; } diff --git a/client/js/lounge.js b/client/js/lounge.js index 167ba0b3..edcde75b 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -493,7 +493,7 @@ $(function() { var highlights = []; - settings.on("change", "input, textarea", function() { + settings.on("change", "input, select, textarea", function() { var self = $(this); var name = self.attr("name"); diff --git a/client/themes/crypto.css b/client/themes/crypto.css index 3179c5a3..b4251dec 100644 --- a/client/themes/crypto.css +++ b/client/themes/crypto.css @@ -126,6 +126,10 @@ a:hover, font-size: 12px; } +#windows select.input { + height: 38px; +} + #footer .icon { color: #666; } From c4cfd7e4b56457e6feb35d24d40c752ff86fe1be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 17 Aug 2016 01:46:26 -0400 Subject: [PATCH 0025/3926] Alphabetically order default user settings, remove unnecessary continue statement, transform ifs into else-ifs --- client/js/lounge.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index edcde75b..2b8b0ee6 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -462,18 +462,18 @@ $(function() { var userStyles = $("#user-specified-css"); var settings = $("#settings"); var options = $.extend({ - desktopNotifications: false, coloredNicks: true, + desktopNotifications: false, join: true, links: true, mode: true, motd: false, nick: true, notification: true, - part: true, - thumbnails: true, - quit: true, notifyAllMessages: false, + part: true, + quit: true, + thumbnails: true, userStyles: userStyles.text(), }, JSON.parse(window.localStorage.getItem("settings"))); @@ -483,7 +483,6 @@ $(function() { $(document.head).find("#user-specified-css").html(options[i]); } settings.find("#user-specified-css-input").val(options[i]); - continue; } else if (i === "highlights") { settings.find("input[name=" + i + "]").val(options[i]); } else if (options[i]) { @@ -515,14 +514,11 @@ $(function() { "notifyAllMessages", ].indexOf(name) !== -1) { chat.toggleClass("hide-" + name, !self.prop("checked")); - } - if (name === "coloredNicks") { + } else if (name === "coloredNicks") { chat.toggleClass("colored-nicks", self.prop("checked")); - } - if (name === "userStyles") { + } else if (name === "userStyles") { $(document.head).find("#user-specified-css").html(options[name]); - } - if (name === "highlights") { + } else if (name === "highlights") { var highlightString = options[name]; highlights = highlightString.split(",").map(function(h) { return h.trim(); From b153d568a093485d17a63142788ddfd462253172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 17 Aug 2016 01:52:29 -0400 Subject: [PATCH 0026/3926] Add a theme selector in the settings Power to the people! There is now 2 ways to set the theme: on the app config file (defaults for all users) and in the user settings. All CSS files present in the `client/themes` folder will be given as choices to the users. This is temporary (as in, temporary for a fairly long time) until we have proper theme management. --- client/index.html | 15 ++++++++++++++- client/js/lounge.js | 6 ++++++ src/server.js | 5 +++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/client/index.html b/client/index.html index 74940d40..1276b99b 100644 --- a/client/index.html +++ b/client/index.html @@ -15,7 +15,7 @@ - "> + @@ -241,6 +241,19 @@ Enable colored nicknames

+
+

Theme

+
+
+ + +
<% if (typeof prefetch === "undefined" || prefetch !== false) { %>

Links and URLs

diff --git a/client/js/lounge.js b/client/js/lounge.js index 2b8b0ee6..a5105f1b 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -473,6 +473,7 @@ $(function() { notifyAllMessages: false, part: true, quit: true, + theme: $("#theme").attr("href").replace(/^themes\/(.*).css$/, "$1"), // Extracts default theme name, set on the server configuration thumbnails: true, userStyles: userStyles.text(), }, JSON.parse(window.localStorage.getItem("settings"))); @@ -485,6 +486,9 @@ $(function() { settings.find("#user-specified-css-input").val(options[i]); } else if (i === "highlights") { settings.find("input[name=" + i + "]").val(options[i]); + } else if (i === "theme") { + $("#theme").attr("href", "themes/" + options[i] + ".css"); + settings.find("select[name=" + i + "]").val(options[i]); } else if (options[i]) { settings.find("input[name=" + i + "]").prop("checked", true); } @@ -516,6 +520,8 @@ $(function() { chat.toggleClass("hide-" + name, !self.prop("checked")); } else if (name === "coloredNicks") { chat.toggleClass("colored-nicks", self.prop("checked")); + } else if (name === "theme") { + $("#theme").attr("href", "themes/" + options[name] + ".css"); } else if (name === "userStyles") { $(document.head).find("#user-specified-css").html(options[name]); } else if (name === "highlights") { diff --git a/src/server.js b/src/server.js index 872e4308..4dcade40 100644 --- a/src/server.js +++ b/src/server.js @@ -122,6 +122,11 @@ function index(req, res, next) { Helper.config ); data.gitCommit = gitCommit; + data.themes = fs.readdirSync("client/themes/").filter(function(file) { + return file.endsWith(".css"); + }).map(function(css) { + return css.slice(0, -4); + }); var template = _.template(file); res.setHeader("Content-Security-Policy", "default-src *; style-src * 'unsafe-inline'; script-src 'self'; child-src 'none'; object-src 'none'; form-action 'none'; referrer no-referrer;"); res.setHeader("Content-Type", "text/html"); From 6d72f023faed99b87520f26dc47e3c127db74a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 17 Aug 2016 01:54:03 -0400 Subject: [PATCH 0027/3926] Fix wrong loading of Crypto font Turns out, this theme probably never loaded its font right, fail... `GET https://.../themes/fonts/inconsolatag.woff` --> 404 --- client/themes/crypto.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/themes/crypto.css b/client/themes/crypto.css index b4251dec..d86c6070 100644 --- a/client/themes/crypto.css +++ b/client/themes/crypto.css @@ -10,7 +10,7 @@ GitHub: https://github.com/aynik @font-face { font-family: Inconsolata-g; - src: url("fonts/inconsolatag.woff") format("woff"), url("fonts/inconsolatag.ttf") format("ttf"); + src: url("../css/fonts/inconsolatag.woff") format("woff"), url("../css/fonts/inconsolatag.ttf") format("ttf"); } body { From d5f234bdb5c8b374b03c6e85f4245c45f4695a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 6 Sep 2016 01:18:21 -0400 Subject: [PATCH 0028/3926] Make all window form inputs white so selects match text inputs --- client/css/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/client/css/style.css b/client/css/style.css index 9d1d7225..321d215f 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -572,6 +572,7 @@ button { } #windows .input { + background-color: white; border: 1px solid #cdd3da; border-radius: 2px; color: #222; From 204e5e4ee47f419d0c7d7ab93bf0a0b7c03d2fc0 Mon Sep 17 00:00:00 2001 From: William Boman Date: Mon, 5 Sep 2016 15:59:30 +0200 Subject: [PATCH 0029/3926] lint: default to ecmaVersion: 6, keep ecmaVersion: 5 for client/ --- .eslintrc.yml | 3 +++ package.json | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 971713e2..223901ec 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -7,6 +7,9 @@ env: mocha: true node: true +parserOptions: + ecmaVersion: 6 + rules: block-spacing: [2, always] brace-style: [2, 1tbs] diff --git a/package.json b/package.json index 3e7ad02c..8c5e4f26 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "test": "npm-run-all -c test:mocha lint", "test:mocha": "mocha -r test/fixtures/env.js test/**/*.js", "lint": "npm-run-all -c lint:js lint:css", - "lint:js": "eslint .", + "lint:js": "npm-run-all -c lint:js:es5 lint:js:es6", + "lint:js:es5": "eslint --parser-options=\"ecmaVersion:5\" client/", + "lint:js:es6": "eslint --ignore-pattern client/ .", "lint:css": "stylelint \"**/*.css\"", "prepublish": "npm run build" }, From 3b20b1bcaba5bda8f91e9df63db00f443f21b98f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Thu, 8 Sep 2016 23:52:29 -0400 Subject: [PATCH 0030/3926] Add change log entry for upcoming v2.0.0-pre.7 --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f30591df..5259ae1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,37 @@ All sections are explained on the link above, they are all optional, and each of Don't forget to thank the PR authors in a commit comment, and copy/paste the release content as-is in GitHub releases: https://github.com/thelounge/lounge/releases --> +## v2.0.0-pre.7 - 2016-08-08 [Pre-release] + +[See the full changelog](https://github.com/thelounge/lounge/compare/v2.0.0-pre.6...v2.0.0-pre.7) + +This prerelease fixes a lot of bugs on both the server and the client. It also adds a theme selector on the client and connection debug log level on the server. Additionally, custom highlights are now case-insensitive. + +### Added + +- Add tooltips on every clickable icons ([#540](https://github.com/thelounge/lounge/pull/540) by [@astorije](https://github.com/astorije)) +- Add debug config option for `irc-framework` debug log ([#547](https://github.com/thelounge/lounge/pull/547) by [@maxpoulin64](https://github.com/maxpoulin64)) +- Client-side theme selector ([#568](https://github.com/thelounge/lounge/pull/568) by [@astorije](https://github.com/astorije)) + +### Changed + +- Use our logger instead of `console.log` and `console.error` for LDAP logs ([#552](https://github.com/thelounge/lounge/pull/552) by [@astorije](https://github.com/astorije)) +- Make custom highlights case-insensitive ([#565](https://github.com/thelounge/lounge/pull/565) by [@astorije](https://github.com/astorije)) +- Bump `request` dependency to 2.74.0 ([#563](https://github.com/thelounge/lounge/pull/563) by [@astorije](https://github.com/astorije)) +- Mention wiki in README ([#548](https://github.com/thelounge/lounge/pull/548) by [@MaxLeiter](https://github.com/MaxLeiter)) +- Support ES6 features in JS linting outside of client code ([#593](https://github.com/thelounge/lounge/pull/593) by [@williamboman](https://github.com/williamboman)) + +### Fixed + +- Fix token persistency across server refreshes ([#553](https://github.com/thelounge/lounge/pull/553) by [@astorije](https://github.com/astorije)) +- Make sure input height is reset when submitting with icon ([#555](https://github.com/thelounge/lounge/pull/555) by [@astorije](https://github.com/astorije)) +- Fix webirc and 4-in-6 addresses ([#535](https://github.com/thelounge/lounge/pull/535) by [@maxpoulin64](https://github.com/maxpoulin64)) +- Allow long URLs to break onto next line on Chrome ([#576](https://github.com/thelounge/lounge/pull/576) by [@astorije](https://github.com/astorije)) +- Make sure users with wrong tokens are locked out instead of crashing the app ([#570](https://github.com/thelounge/lounge/pull/570) by [@astorije](https://github.com/astorije)) +- Remove font family redundancy, fix missed fonts, remove Open Sans ([#562](https://github.com/thelounge/lounge/pull/562) by [@astorije](https://github.com/astorije)) +- Do not set app orientation in manifest to use user setting at OS level ([#587](https://github.com/thelounge/lounge/pull/587) by [@astorije](https://github.com/astorije)) +- Move border-radius from `#main` to `.window elements` to fix radius once and for all ([#572](https://github.com/thelounge/lounge/pull/572) by [@astorije](https://github.com/astorije)) + ## v2.0.0-pre.6 - 2016-08-10 [Pre-release] [See the full changelog](https://github.com/thelounge/lounge/compare/v2.0.0-pre.5...v2.0.0-pre.6) From 8d838aa08dbb0de9ebefcd6000433991efee94e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Thu, 8 Sep 2016 23:52:42 -0400 Subject: [PATCH 0031/3926] 2.0.0-pre.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8c5e4f26..2cb94bef 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "thelounge", "description": "The self-hosted Web IRC client", - "version": "2.0.0-pre.6", + "version": "2.0.0-pre.7", "publishConfig": { "tag": "next" }, From 2b3b4ea92450cc229c4297ea6160f51daa6787c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Fri, 9 Sep 2016 01:17:31 -0400 Subject: [PATCH 0032/3926] Explicitly authorize websockets in CSP header This follows a recent change in WebKit (see https://webkit.org/blog/6830/a-refined-content-security-policy/, section "More restrictive wildcard *") to remove websocket schemes from the connect-src directive. Users of Safari v10 (to be publicly released in a few days) would be affected by this and could not load the app. --- src/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.js b/src/server.js index 4dcade40..4dbbc1af 100644 --- a/src/server.js +++ b/src/server.js @@ -128,7 +128,7 @@ function index(req, res, next) { return css.slice(0, -4); }); var template = _.template(file); - res.setHeader("Content-Security-Policy", "default-src *; style-src * 'unsafe-inline'; script-src 'self'; child-src 'none'; object-src 'none'; form-action 'none'; referrer no-referrer;"); + res.setHeader("Content-Security-Policy", "default-src *; connect-src 'self' ws: wss:; style-src * 'unsafe-inline'; script-src 'self'; child-src 'none'; object-src 'none'; form-action 'none'; referrer no-referrer;"); res.setHeader("Content-Type", "text/html"); res.writeHead(200); res.end(template(data)); From 687a5846b63b4df4abb6028d5b283007f00e224b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Mon, 12 Sep 2016 01:25:09 -0400 Subject: [PATCH 0033/3926] Fix small input text on Morning and Zenburn --- client/themes/morning.css | 2 +- client/themes/zenburn.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/themes/morning.css b/client/themes/morning.css index 7ec20035..bc0b76fb 100644 --- a/client/themes/morning.css +++ b/client/themes/morning.css @@ -30,6 +30,7 @@ body { } #windows .header .topic, +#windows #form .input, .messages .msg, .sidebar { font-family: inherit; @@ -148,7 +149,6 @@ body { } #windows #form .input { - font-family: inherit; background-color: #2e3642 !important; border-color: #242a33 !important; color: #ccc; diff --git a/client/themes/zenburn.css b/client/themes/zenburn.css index 5d77c6ca..88b5ce87 100644 --- a/client/themes/zenburn.css +++ b/client/themes/zenburn.css @@ -31,6 +31,7 @@ body { } #windows .header .topic, +#windows #form .input, .messages .msg, .sidebar { font-family: inherit; @@ -175,7 +176,6 @@ body { } #windows #form .input { - font-family: inherit; background-color: #434443 !important; border-color: #101010 !important; color: #dcdccc !important; From 87dfe2cc22c60bdbe1ab3e8729ae21c45f82fdb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 14 Sep 2016 23:50:57 -0400 Subject: [PATCH 0034/3926] Disable tooltips on mobile to prevent them to stay after clicking --- client/css/style.css | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/client/css/style.css b/client/css/style.css index 321d215f..dab0d048 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1557,6 +1557,24 @@ button { } @media (max-width: 768px) { + /** + * TODO Replace this with `@media (hover: hover)` when Firefox supports it + * See: + * - http://stackoverflow.com/a/28058919/1935861 + * - http://caniuse.com/#feat=css-media-interaction + * - https://www.w3.org/TR/mediaqueries-4/ + * - https://developer.mozilla.org/en-US/docs/Web/CSS/@media/hover + */ + .tooltipped:hover:before, + .tooltipped:hover:after, + .tooltipped:active:before, + .tooltipped:active:after, + .tooltipped:focus:before, + .tooltipped:focus:after { + visibility: hidden; + opacity: 0; + } + .container { margin-top: 60px !important; } From 8be62e4f552f25c4eb204ec29a8b2bd6a9abfec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sat, 17 Sep 2016 13:13:01 -0400 Subject: [PATCH 0035/3926] Fix a left margin appearing on all non-default themes --- client/css/style.css | 4 ---- client/themes/example.css | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index dab0d048..a209e3bb 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -555,10 +555,6 @@ button { flex-direction: column; } -.signed-out #main { - left: 5px; -} - #header { display: none; height: 40px; diff --git a/client/themes/example.css b/client/themes/example.css index 7a2206bf..e318b675 100644 --- a/client/themes/example.css +++ b/client/themes/example.css @@ -5,3 +5,7 @@ body { margin: 0; } + +.signed-out #main { + left: 5px; +} From 71f0455c911f6053c2244d5c397b2df99e9b497b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sat, 17 Sep 2016 13:25:12 -0400 Subject: [PATCH 0036/3926] Add change log entry for upcoming v2.0.0-rc.1 --- CHANGELOG.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5259ae1c..fe1164c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,30 @@ All sections are explained on the link above, they are all optional, and each of Don't forget to thank the PR authors in a commit comment, and copy/paste the release content as-is in GitHub releases: https://github.com/thelounge/lounge/releases --> -## v2.0.0-pre.7 - 2016-08-08 [Pre-release] +## v2.0.0-rc.1 - 2016-09-17 [Pre-release] + +[See the full changelog](https://github.com/thelounge/lounge/compare/v2.0.0-pre.7...v2.0.0-rc.1) + +Prior to this release, users of Safari 10 were not able to access The Lounge anymore, because of a conscious change the WebKit made to their support of CSP, as [explained here](https://webkit.org/blog/6830/a-refined-content-security-policy/). This release addresses this issue. + +Another notable change is the removal of tooltips on mobiles, as hovering states on mobile devices breaks in different kind of ways. Hopefully there will be a better solution in the future, or better support across mobiles. + +This is also the first release candidate for v2.0.0. This means only critical bug fixes will be merged before releasing v2.0.0. + +### Changed + +- Explicitly authorize websockets in CSP header ([#597](https://github.com/thelounge/lounge/pull/597) by [@astorije](https://github.com/astorije)) + +### Removed + +- Disable tooltips on mobile to prevent them to stay after clicking ([#612](https://github.com/thelounge/lounge/pull/612) by [@astorije](https://github.com/astorije)) + +### Fixed + +- Fix small input text on Morning and Zenburn ([#601](https://github.com/thelounge/lounge/pull/601) by [@astorije](https://github.com/astorije)) +- Fix a left margin appearing on all non-default themes ([#615](https://github.com/thelounge/lounge/pull/615) by [@astorije](https://github.com/astorije)) + +## v2.0.0-pre.7 - 2016-09-08 [Pre-release] [See the full changelog](https://github.com/thelounge/lounge/compare/v2.0.0-pre.6...v2.0.0-pre.7) From c090ab065f09b23408790d2d58cbf38ed0442a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sat, 17 Sep 2016 13:25:27 -0400 Subject: [PATCH 0037/3926] 2.0.0-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2cb94bef..8977ae62 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "thelounge", "description": "The self-hosted Web IRC client", - "version": "2.0.0-pre.7", + "version": "2.0.0-rc.1", "publishConfig": { "tag": "next" }, From 701e333d78331410dcc38ee982486656c90ba0e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sat, 17 Sep 2016 20:43:29 -0400 Subject: [PATCH 0038/3926] Hide sidebar when app is loading in themes This change was originally made in #420 then I broke it for themes in #615, sigh... --- client/css/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/css/style.css b/client/css/style.css index a209e3bb..0656ef37 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -555,6 +555,10 @@ button { flex-direction: column; } +.signed-out #main { + left: 0; /* Hide the sidebar when user is signed out */ +} + #header { display: none; height: 40px; From 578328d20846e0032b43ed76563a1cb541b3dbbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sun, 18 Sep 2016 19:28:27 -0400 Subject: [PATCH 0039/3926] Disable pull-to-refresh on mobile that conflicts with scrolling the message list See http://stackoverflow.com/a/29313685/1935861 --- client/css/style.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/css/style.css b/client/css/style.css index a209e3bb..ed3ec8ab 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -46,6 +46,12 @@ body { color: #222; font: 16px Lato, sans-serif; margin: 0; + + /** + * Disable pull-to-refresh on mobile that conflicts with scrolling the message list. + * See http://stackoverflow.com/a/29313685/1935861 + */ + overflow-y: hidden; } a { From f1c3e376c533c7ea767b7737801faa846fcc57fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 20 Sep 2016 00:06:54 -0400 Subject: [PATCH 0040/3926] Add information on running from source and clean up README a bit --- README.md | 48 +++++++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 88d0f6e4..d89d16c8 100644 --- a/README.md +++ b/README.md @@ -34,39 +34,53 @@ This fork aims to be community managed, meaning that the decisions are taken in a collegial fashion, and that a bunch of maintainers should be able to make the review process quicker and more streamlined. -## Install +## Installation and usage -To use The Lounge you must have [Node.js](https://nodejs.org/en/download/) installed. -The oldest Node.js version we support is 4.2.0. +The Lounge requires [Node.js](https://nodejs.org/) v4 or more recent. -If you still use 0.10 or 0.12 we strongly advise you to upgrade before installing The Lounge. -For more information on how to upgrade, read the [documentation](https://nodejs.org/en/download/package-manager/). +### Running stable releases from npm (recommended) -``` -sudo npm install -g thelounge +Run this in a terminal to install (or upgrade) the latest stable release from +[npm](https://www.npmjs.com/): + +```sh +[sudo] npm install -g thelounge ``` -## Usage - -When the install is complete, go ahead and run this in your terminal: +When installation is complete, run: +```sh +lounge start ``` + +For more information, read the [documentation](https://thelounge.github.io/docs/), [wiki](https://github.com/thelounge/lounge/wiki), or run: + +```sh lounge --help ``` -For more information, read the [documentation](https://thelounge.github.io/docs/) or [wiki](https://github.com/thelounge/lounge/wiki). +### Running from source -## Development setup +The following commands install the development version of The Lounge. A word of +caution: while it is the most recent codebase, this is not production-ready! -To run the app from source, just clone the code and run this in your terminal: - -``` +```sh +git clone https://github.com/thelounge/lounge.git +cd lounge npm install npm start ``` -You will have to run `npm run build` if you change or add anything in -`client/js/libs` or `client/views`. +## Development setup + +Simply follow the instructions to run The Lounge from source above, on your own +fork. + +Before submitting any change, make sure to: + +- Read the [Contributing instructions](https://github.com/thelounge/lounge/blob/master/CONTRIBUTING.md#contributing) +- Run `npm test` to execute linters and test suite +- Run `npm run build` if you change or add anything in `client/js/libs` or `client/views` ## License From ae2b27ba5f42548ed931957e4fbe0c3c3403736a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 20 Sep 2016 00:08:06 -0400 Subject: [PATCH 0041/3926] Remove license info from the README This is already on the badge and at the top of the page, on GitHub UI, as well as in 2 places in npm. --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index d89d16c8..aea710cd 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,3 @@ Before submitting any change, make sure to: - Read the [Contributing instructions](https://github.com/thelounge/lounge/blob/master/CONTRIBUTING.md#contributing) - Run `npm test` to execute linters and test suite - Run `npm run build` if you change or add anything in `client/js/libs` or `client/views` - -## License - -Available under the [MIT License](LICENSE). - -Some fonts licensed under [SIL OFL](http://scripts.sil.org/OFL) and the [Apache License](http://www.apache.org/licenses/). From 813572de4734f980951e8c5d91a7bb9b095aa50c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 20 Sep 2016 23:43:23 -0400 Subject: [PATCH 0042/3926] Ensure localStorage cannot fail because of quota or Safari private browsing See http://stackoverflow.com/q/14555347/1935861 --- client/js/lounge.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index a5105f1b..cca024b9 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -54,6 +54,16 @@ $(function() { return Handlebars.templates[name](data); } + function setLocalStorageItem(key, value) { + try { + window.localStorage.setItem(key, value); + } catch (e) { + // Do nothing. If we end up here, web storage quota exceeded, or user is + // in Safari's private browsing where localStorage's setItem is not + // available. See http://stackoverflow.com/q/14555347/1935861. + } + } + Handlebars.registerHelper( "partial", function(id) { return new Handlebars.SafeString(render(id, this)); @@ -125,7 +135,7 @@ $(function() { } if (data.token && window.localStorage.getItem("token") !== null) { - window.localStorage.setItem("token", data.token); + setLocalStorageItem("token", data.token); } passwordForm @@ -144,7 +154,7 @@ $(function() { } if (data.token && $("#sign-in-remember").is(":checked")) { - window.localStorage.setItem("token", data.token); + setLocalStorageItem("token", data.token); } else { window.localStorage.removeItem("token"); } @@ -506,7 +516,7 @@ $(function() { options[name] = self.val(); } - window.localStorage.setItem("settings", JSON.stringify(options)); + setLocalStorageItem("settings", JSON.stringify(options)); if ([ "join", @@ -1005,7 +1015,7 @@ $(function() { } }); if (values.user) { - window.localStorage.setItem("user", values.user); + setLocalStorageItem("user", values.user); } socket.emit( event, values From 8bd5d800d0d113de0b0052274192fc8892246af2 Mon Sep 17 00:00:00 2001 From: Gilles Gauthier Date: Tue, 20 Sep 2016 21:26:22 -0700 Subject: [PATCH 0043/3926] Fixing display: flex for iOS 8 --- client/css/style.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/client/css/style.css b/client/css/style.css index a209e3bb..a733e5e0 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -551,7 +551,9 @@ button { position: absolute; right: 5px; top: 4px; + display: -webkit-flex; display: flex; + -webkit-flex-direction: column; flex-direction: column; } @@ -666,13 +668,16 @@ button { } #windows #chat-container.active { + display: -webkit-flex; display: flex; + -webkit-flex-direction: column; flex-direction: column; } #chat { position: relative; overflow: hidden; + -webkit-flex: 1; flex: 1; } @@ -1196,6 +1201,7 @@ button { #form { background: #eee; border-top: 1px solid #ddd; + -webkit-flex: 0 0 auto; flex: 0 0 auto; padding: 5px; } @@ -1207,7 +1213,9 @@ button { margin: 0; padding: 0; background: white; + display: -webkit-flex; display: flex; + -webkit-align-items: flex-end; align-items: flex-end; } @@ -1225,6 +1233,7 @@ button { -moz-user-select: none; -ms-user-select: none; user-select: none; + -webkit-flex: 0 0 auto; flex: 0 0 auto; } @@ -1248,6 +1257,7 @@ button { margin: 5px; padding: 0; resize: none; + -webkit-flex: 1 0 auto; flex: 1 0 auto; align-self: center; } @@ -1258,6 +1268,7 @@ button { height: 32px; transition: opacity .2s; width: 32px; + -webkit-flex: 0 0 auto; flex: 0 0 auto; } From 4439303f90bbb7d9d3241d8fe2397921182a8fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 21 Sep 2016 01:14:42 -0400 Subject: [PATCH 0044/3926] Ignore consecutive duplicates in stylelint for prefixed values This is already necessary globally and will become even more so as we are adding flexbox pieces to the UI. See http://stylelint.io/user-guide/rules/declaration-block-no-duplicate-properties/. --- .stylelintrc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.stylelintrc b/.stylelintrc index d346c411..3351866c 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -55,7 +55,9 @@ "number-leading-zero": "never", "number-no-trailing-zeros": true, "length-zero-no-unit": true, - "declaration-block-no-duplicate-properties": true, + "declaration-block-no-duplicate-properties": [true, { + "ignore": ["consecutive-duplicates"] + }], "declaration-block-no-shorthand-property-overrides": true, "rule-non-nested-empty-line-before": ["always-multi-line", { "ignore": ["after-comment"] From a85d6458901fb5b6afba48f3748c1cd930aba2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 21 Sep 2016 01:35:18 -0400 Subject: [PATCH 0045/3926] Add change log entry for upcoming v2.0.0-rc.2 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe1164c3..d2459770 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,16 @@ All sections are explained on the link above, they are all optional, and each of Don't forget to thank the PR authors in a commit comment, and copy/paste the release content as-is in GitHub releases: https://github.com/thelounge/lounge/releases --> +## v2.0.0-rc.2 - 2016-09-21 [Pre-release] + +[See the full changelog](https://github.com/thelounge/lounge/compare/v2.0.0-rc.1...v2.0.0-rc.2) + +This release candidate only fixes a UI bug affecting iOS 8 users, introduced in v2.0.0-pre.5. + +### Fixed + +- Fix flexboxes to work on iOS 8 ([#626](https://github.com/thelounge/lounge/pull/626) by [@Gilles123](https://github.com/Gilles123)) + ## v2.0.0-rc.1 - 2016-09-17 [Pre-release] [See the full changelog](https://github.com/thelounge/lounge/compare/v2.0.0-pre.7...v2.0.0-rc.1) From f7466cb5566b5449ea16e9e0154339ae91c37e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 21 Sep 2016 01:35:27 -0400 Subject: [PATCH 0046/3926] 2.0.0-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8977ae62..3322bc4f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "thelounge", "description": "The self-hosted Web IRC client", - "version": "2.0.0-rc.1", + "version": "2.0.0-rc.2", "publishConfig": { "tag": "next" }, From 62edd07c23f72eeac692bda6da7d0bd79cee9ced Mon Sep 17 00:00:00 2001 From: Jeremy Apthorp Date: Thu, 22 Sep 2016 08:58:47 -0700 Subject: [PATCH 0047/3926] Move uglify invocation into npm scripts and remove grunt --- Gruntfile.js | 26 -------------------------- package.json | 10 ++++------ 2 files changed, 4 insertions(+), 32 deletions(-) delete mode 100644 Gruntfile.js diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 49f9477b..00000000 --- a/Gruntfile.js +++ /dev/null @@ -1,26 +0,0 @@ -module.exports = function(grunt) { - var libs = "client/js/libs/**/*.js"; - grunt.initConfig({ - watch: { - files: libs, - tasks: ["uglify"] - }, - uglify: { - options: { - sourceMap: true, - compress: false - }, - js: { - files: { - "client/js/libs.min.js": libs - } - } - } - }); - grunt.loadNpmTasks("grunt-contrib-uglify"); - grunt.loadNpmTasks("grunt-contrib-watch"); - grunt.registerTask( - "default", - ["uglify"] - ); -}; diff --git a/package.json b/package.json index 8977ae62..45139ef8 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,9 @@ "scripts": { "coverage": "istanbul cover node_modules/mocha/bin/_mocha -r test/fixtures/env.js test/**/*.js", "start": "node index", - "build": "npm run build:font-awesome && npm run build:grunt && npm run build:handlebars", + "build": "npm-run-all build:*", "build:font-awesome": "node scripts/build-fontawesome.js", - "build:grunt": "grunt", + "build:libs": "uglifyjs client/js/libs/*.js client/js/libs/jquery/*.js client/js/libs/handlebars/*.js -o client/js/libs.min.js --source-map client/js/libs.min.js.map --source-map-url libs.min.js.map -p relative", "build:handlebars": "handlebars client/views/ -e tpl -f client/js/lounge.templates.js", "test": "npm-run-all -c test:mocha lint", "test:mocha": "mocha -r test/fixtures/env.js test/**/*.js", @@ -64,13 +64,11 @@ "chai": "3.5.0", "eslint": "2.11.1", "font-awesome": "4.6.3", - "grunt": "1.0.1", - "grunt-contrib-uglify": "1.0.1", - "grunt-contrib-watch": "1.0.0", "handlebars": "4.0.5", "istanbul": "0.4.3", "mocha": "2.4.5", "npm-run-all": "2.1.1", - "stylelint": "6.6.0" + "stylelint": "6.6.0", + "uglify-js": "2.7.3" } } From 62ee13833baeea04e43a62aef6bb99ae68c0ae9a Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sun, 29 May 2016 23:21:45 +0300 Subject: [PATCH 0048/3926] Move Shout theme borders to example theme --- client/css/style.css | 38 +++++++++---------------------- client/themes/crypto.css | 26 ---------------------- client/themes/example.css | 47 +++++++++++++++++++++++++++++++++++++++ client/themes/morning.css | 38 ------------------------------- client/themes/zenburn.css | 39 -------------------------------- 5 files changed, 57 insertions(+), 131 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 0234bb2d..eec221e9 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -323,7 +323,7 @@ button { } #sidebar { - bottom: 52px; + bottom: 48px; left: 0; overflow: auto; overflow-x: hidden; @@ -505,15 +505,14 @@ button { #footer { background: rgba(0, 0, 0, .06); - border-radius: 2px; - bottom: 4px; + bottom: 0; height: 45px; - left: 5px; + left: 0; font-size: 14px; line-height: 45px; position: absolute; text-align: center; - width: 210px; + width: 220px; } #footer button.active { @@ -546,11 +545,11 @@ button { } #main { - bottom: 4px; + bottom: 0; left: 220px; position: absolute; - right: 5px; - top: 4px; + right: 0; + top: 0; display: -webkit-flex; display: flex; -webkit-flex-direction: column; @@ -600,20 +599,8 @@ button { border-color: #79838c; } -#windows .window:before { - background: #f4f4f4; - background-image: linear-gradient(#f4f4f4, #ececec); - border-bottom: 1px solid #d7d7d7; - content: " "; - display: block; - height: 10px; - position: relative; - z-index: 10; -} - #windows .window { background: #fff; - border-radius: 2px; bottom: 0; display: none; left: 0; @@ -1600,13 +1587,9 @@ button { transform: translate3d(-180px, 0, 0); } - #sidebar { - left: -220px; - } - + #sidebar, #footer { - left: -215px; - width: 215px; + left: -220px; } #sidebar .empty:before { @@ -1614,8 +1597,7 @@ button { } #main { - left: 5px; - right: 5px; + left: 0; } #chat .chat { diff --git a/client/themes/crypto.css b/client/themes/crypto.css index d86c6070..61a7c31d 100644 --- a/client/themes/crypto.css +++ b/client/themes/crypto.css @@ -34,16 +34,6 @@ a:hover, border-bottom: none; } -#main { - right: 0; - bottom: 0; - top: 0; -} - -#windows .window { - border-radius: 0; -} - .container { margin: 40px auto; } @@ -68,11 +58,6 @@ a:hover, background: #00ff0e; } -#windows .window:before, -#windows .chan:before { - content: none; -} - #settings .opt { line-height: 20px; font-size: 12px; @@ -111,11 +96,6 @@ a:hover, line-height: 1.8; } -#chat .chat, -#chat .sidebar { - top: 48px; -} - #chat .user { color: black; font-weight: bold; @@ -137,9 +117,3 @@ a:hover, .tooltipped:after { font-family: Inconsolata-g, monospace; } - -@media (max-width: 768px) { - #main { - left: 0; - } -} diff --git a/client/themes/example.css b/client/themes/example.css index e318b675..d85df9fa 100644 --- a/client/themes/example.css +++ b/client/themes/example.css @@ -9,3 +9,50 @@ body { .signed-out #main { left: 5px; } + +#sidebar { + bottom: 52px; +} + +#footer { + border-radius: 2px; + bottom: 4px; + left: 5px; + width: 210px; +} + +#main { + bottom: 4px; + right: 5px; + top: 4px; +} + +#windows .window:before { + background: #f4f4f4; + background-image: linear-gradient(#f4f4f4, #ececec); + border-bottom: 1px solid #d7d7d7; + content: " "; + display: block; + height: 10px; + position: relative; + z-index: 10; +} + +#windows .window { + border-radius: 2px; +} + +@media (max-width: 768px) { + #sidebar { + left: -220px; + } + + #footer { + left: -215px; + width: 215px; + } + + #main { + left: 5px; + } +} diff --git a/client/themes/morning.css b/client/themes/morning.css index bc0b76fb..f6731ed8 100644 --- a/client/themes/morning.css +++ b/client/themes/morning.css @@ -59,34 +59,6 @@ body { border-color: #2a323d; } -/* Attach chat to window borders */ -#windows .window:before, -#windows .chan:before { - display: none; -} - -#footer { - left: 0; - bottom: 0; - width: 220px; - border-radius: 0; -} - -#main { - top: 0; - bottom: 0; - right: 0; -} - -#windows .window { - border-radius: 0; -} - -#chat .chat, -#chat .sidebar { - top: 48px; -} - /* User list */ #chat .user-mode { color: #fefefe; @@ -244,13 +216,3 @@ body { #chat .toggle-content .body { color: #99a2b4; } - -@media (max-width: 768px) { - #main { - left: 0; - } - #footer { - left: -220px; - width: 225px; - } -} diff --git a/client/themes/zenburn.css b/client/themes/zenburn.css index 88b5ce87..a75afab7 100644 --- a/client/themes/zenburn.css +++ b/client/themes/zenburn.css @@ -52,7 +52,6 @@ body { #sidebar { background: #2b2b2b; - bottom: 48px; } #sidebar .chan .name:after { @@ -86,34 +85,6 @@ body { border-color: #333; } -/* Attach chat to window borders */ -#windows .window:before, -#windows .chan:before { - display: none; -} - -#footer { - left: 0; - bottom: 0; - width: 220px; - border-radius: 0; -} - -#main { - top: 0; - bottom: 0; - right: 0; -} - -#windows .window { - border-radius: 0; -} - -#chat .chat, -#chat .sidebar { - top: 48px; -} - /* User list */ #chat .user-mode { color: #dcdccc; @@ -271,13 +242,3 @@ body { #chat .toggle-content .body { color: #d2d39b; } - -@media (max-width: 768px) { - #main { - left: 0; - } - #footer { - left: -220px; - width: 225px; - } -} From bdb4d0de6a0bae097c7ad886f524c426b4ffffbe Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 24 Sep 2016 10:46:02 +0300 Subject: [PATCH 0049/3926] Remove -ms-transform and add missed -webkit-transform --- client/css/style.css | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 0234bb2d..113611e7 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -195,6 +195,7 @@ button { content: "\f08b"; /* http://fontawesome.io/icon/sign-out/ */ color: #ff4136; display: inline-block; + -webkit-transform: rotate(180deg); transform: rotate(180deg); } @@ -1446,7 +1447,6 @@ button { .tooltipped-s:after, .tooltipped-n:after { -webkit-transform: translateX(50%); - -ms-transform: translateX(50%); transform: translateX(50%); } @@ -1455,7 +1455,6 @@ button { bottom: 50%; margin-right: 5px; -webkit-transform: translateY(50%); - -ms-transform: translateY(50%); transform: translateY(50%); } @@ -1472,7 +1471,6 @@ button { left: 100%; margin-left: 5px; -webkit-transform: translateY(50%); - -ms-transform: translateY(50%); transform: translateY(50%); } From e6bf20de2fdb598c02865aef8cbf38ff18fd390b Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Mon, 19 Sep 2016 22:33:09 -0700 Subject: [PATCH 0050/3926] Handle stderr when using edit or config command, fixes #164 --- src/command-line/config.js | 5 ++++- src/command-line/edit.js | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/command-line/config.js b/src/command-line/config.js index 1fef7dde..9488d507 100644 --- a/src/command-line/config.js +++ b/src/command-line/config.js @@ -6,9 +6,12 @@ program .command("config") .description("Edit config: " + Helper.CONFIG_PATH) .action(function() { - child.spawn( + var child_spawn = child.spawn( process.env.EDITOR || "vi", [Helper.CONFIG_PATH], {stdio: "inherit"} ); + child_spawn.on("error", function() { + log.error("Unable to open " + Helper.CONFIG_PATH + ". $EDITOR is not set, and vi was not found."); + }); }); diff --git a/src/command-line/edit.js b/src/command-line/edit.js index 5d849eb9..7b2ca558 100644 --- a/src/command-line/edit.js +++ b/src/command-line/edit.js @@ -12,9 +12,12 @@ program log.error("User '" + name + "' doesn't exist."); return; } - child.spawn( + var child_spawn = child.spawn( process.env.EDITOR || "vi", [Helper.getUserConfigPath(name)], {stdio: "inherit"} ); + child_spawn.on("error", function() { + log.error("Unable to open " + Helper.getUserConfigPath(name) + ". $EDITOR is not set, and vi was not found."); + }); }); From 1dc3e74f7f2c391e512a6c5df9e4f02dd22d9dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sat, 24 Sep 2016 23:56:49 -0400 Subject: [PATCH 0051/3926] Remove `next` tag used for 2.0.0 prereleases --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index 3322bc4f..fe6daece 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,6 @@ "name": "thelounge", "description": "The self-hosted Web IRC client", "version": "2.0.0-rc.2", - "publishConfig": { - "tag": "next" - }, "preferGlobal": true, "bin": { "lounge": "index.js" From e9ab8e3b1f2cd639b83a732e7e9d90197cd49039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sat, 24 Sep 2016 23:57:08 -0400 Subject: [PATCH 0052/3926] Add change log entry for upcoming v2.0.0 --- CHANGELOG.md | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2459770..ddce00d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,108 @@ All sections are explained on the link above, they are all optional, and each of Don't forget to thank the PR authors in a commit comment, and copy/paste the release content as-is in GitHub releases: https://github.com/thelounge/lounge/releases --> +## v2.0.0 - 2016-09-24 + +[See the full changelog](https://github.com/thelounge/lounge/compare/v1.5.0...v2.0.0) + +After more than 5 months in the works, v2.0.0 is finally happening, and it's shipping with lots of new and enhanced features! 🎉 + +First of all, the backend IRC library is completely different, which was the first step to deciding on a major release. +This change brings many improvements and fixes, including support for auto-reconnection! This also allows us to easily improve our [IRCv3 compliance](http://ircv3.net/software/clients.html#web-clients). + +Main changes on the server include support for WEBIRC, oidentd and LDAP. On the client, users will notice a lot of improvements about reporting unseen activity (notifications, markers, etc.), support for custom highlights, a new loading page, an auto-expanding message input, a theme selector, and more. + +Administrators should note that the channel list format in user configuration files has changed. The old format is deprecated, but it will be automatically converted when the server starts (support may or may not be removed later). Additionally, The Lounge now only runs on Node v4 and up. + +The above is only a small subset of changes. A more detailed list can be found below. +The following list features the most noticeable changes only, and more details can be found on all [v2.0.0 pre-releases](https://www.github.com/thelounge/lounge/releases). + +### Added + +- Add tooltips on every clickable icons ([#540](https://github.com/thelounge/lounge/pull/540) by [@astorije](https://github.com/astorije)) +- Add debug config option for `irc-framework` debug log ([#547](https://github.com/thelounge/lounge/pull/547) by [@maxpoulin64](https://github.com/maxpoulin64)) +- Client-side theme selector ([#568](https://github.com/thelounge/lounge/pull/568) by [@astorije](https://github.com/astorije)) +- LDAP support ([#477](https://github.com/thelounge/lounge/pull/477) by [@thisisdarshan](https://github.com/thisisdarshan) and [@lindskogen](https://github.com/lindskogen)) +- Add custom highlights ([#425](https://github.com/thelounge/lounge/pull/425) by [@YaManicKill](https://github.com/YaManicKill)) +- Add auto-grow textarea support ([#379](https://github.com/thelounge/lounge/pull/379) by [@maxpoulin64](https://github.com/maxpoulin64)) +- Display unhandled numerics on the client ([#286](https://github.com/thelounge/lounge/pull/286) by [@xPaw](https://github.com/xPaw)) +- A proper unread marker ([#332](https://github.com/thelounge/lounge/pull/332) by [@xPaw](https://github.com/xPaw)) +- Add information on the About section of the client ([#497](https://github.com/thelounge/lounge/pull/497) by [@astorije](https://github.com/astorije)) +- Add a red dot to the mobile menu icon when being notified ([#486](https://github.com/thelounge/lounge/pull/486) by [@astorije](https://github.com/astorije)) +- Add "The Lounge" label to the landing pages ([#487](https://github.com/thelounge/lounge/pull/487) by [@astorije](https://github.com/astorije)) +- Display network name on Connect page when network is locked and info is hidden ([#488](https://github.com/thelounge/lounge/pull/488) by [@astorije](https://github.com/astorije)) +- Display a loading message instead of blank page ([#386](https://github.com/thelounge/lounge/pull/386) by [@xPaw](https://github.com/xPaw)) +- Fall back to LOUNGE_HOME env variable when using the CLI ([#402](https://github.com/thelounge/lounge/pull/402) by [@williamboman](https://github.com/williamboman)) +- Enable auto reconnection ([#254](https://github.com/thelounge/lounge/pull/254) by [@xPaw](https://github.com/xPaw)) +- Add "!" modechar for admin ([#354](https://github.com/thelounge/lounge/pull/354) by [@omnicons](https://github.com/omnicons)) +- Add support for oidentd spoofing ([#256](https://github.com/thelounge/lounge/pull/256) by [@maxpoulin64](https://github.com/maxpoulin64)) +- Log enabled capabilities ([#272](https://github.com/thelounge/lounge/pull/272) by [@xPaw](https://github.com/xPaw)) +- Add support for `~` home folder expansion ([#284](https://github.com/thelounge/lounge/pull/284) by [@maxpoulin64](https://github.com/maxpoulin64)) +- Document supported node version ([#280](https://github.com/thelounge/lounge/pull/280) by [@xPaw](https://github.com/xPaw)) +- Implement WEBIRC ([#240](https://github.com/thelounge/lounge/pull/240) by [@maxpoulin64](https://github.com/maxpoulin64)) +- Add `manifest.json` for nicer mobile experience ([#310](https://github.com/thelounge/lounge/pull/310) by [@xPaw](https://github.com/xPaw)) + +### Changed + +- Cache loaded config and merge it with defaults ([#387](https://github.com/thelounge/lounge/pull/387) by [@xPaw](https://github.com/xPaw)) +- Ignore unnecessary files at release time ([#499](https://github.com/thelounge/lounge/pull/499) by [@astorije](https://github.com/astorije)) +- Improve font icon management, sizing and sharpness ([#493](https://github.com/thelounge/lounge/pull/493) by [@astorije](https://github.com/astorije)) +- Maintain scroll position after loading previous messages ([#496](https://github.com/thelounge/lounge/pull/496) by [@davibe](https://github.com/davibe)) +- Perform node version check as soon as possible ([#409](https://github.com/thelounge/lounge/pull/409) by [@xPaw](https://github.com/xPaw)) +- Prepend http protocol to www. links in chat ([#410](https://github.com/thelounge/lounge/pull/410) by [@xPaw](https://github.com/xPaw)) +- Change default configuration for `host` to allow OS to decide and use both IPv4 and IPv6 ([#432](https://github.com/thelounge/lounge/pull/432) by [@maxpoulin64](https://github.com/maxpoulin64)) +- Do not hide timestamps on small viewports ([#376](https://github.com/thelounge/lounge/pull/376) by [@xPaw](https://github.com/xPaw)) +- Drop `slate-irc`, switch to `irc-framework` ([#167](https://github.com/thelounge/lounge/pull/167) by [@xPaw](https://github.com/xPaw)) +- Improve sticky scroll ([#262](https://github.com/thelounge/lounge/pull/262) by [@xPaw](https://github.com/xPaw)) +- Minor wording changes for better clarity ([#305](https://github.com/thelounge/lounge/pull/305) by [@astorije](https://github.com/astorije)) +- Improve nick highlights ([#327](https://github.com/thelounge/lounge/pull/327) by [@xPaw](https://github.com/xPaw)) +- CSS classes in themes for nick colors ([#325](https://github.com/thelounge/lounge/pull/325) by [@astorije](https://github.com/astorije)) +- Replace all concatenated paths with Node's path.join ([#307](https://github.com/thelounge/lounge/pull/307) by [@astorije](https://github.com/astorije)) + +### Deprecated + +- Store channels in array format in user configuration files, deprecating previous format ([#417](https://github.com/thelounge/lounge/pull/417) by [@xPaw](https://github.com/xPaw)) + +### Removed + +- Disable tooltips on mobile to prevent them to stay after clicking ([#612](https://github.com/thelounge/lounge/pull/612) by [@astorije](https://github.com/astorije)) +- Remove Docker-related files now that we have a dedicated repository ([#288](https://github.com/thelounge/lounge/pull/288) by [@astorije](https://github.com/astorije)) +- Remove JavaScript scrollbar library ([#429](https://github.com/thelounge/lounge/pull/429) by [@xPaw](https://github.com/xPaw)) +- Remove navigator.standalone detection ([#427](https://github.com/thelounge/lounge/pull/427) by [@xPaw](https://github.com/xPaw)) +- Do not increase font size on highlight in morning theme ([#321](https://github.com/thelounge/lounge/pull/321) by [@xPaw](https://github.com/xPaw)) + +### Fixed + +- Remove font family redundancy, fix missed fonts, remove Open Sans ([#562](https://github.com/thelounge/lounge/pull/562) by [@astorije](https://github.com/astorije)) +- Stop propagation when hiding the chat through click/tapping the chat ([#455](https://github.com/thelounge/lounge/pull/455) by [@williamboman](https://github.com/williamboman)) +- Improve click handling on users and inline channels ([#366](https://github.com/thelounge/lounge/pull/366) by [@xPaw](https://github.com/xPaw)) +- Only load config if it exists ([#461](https://github.com/thelounge/lounge/pull/461) by [@xPaw](https://github.com/xPaw)) +- Send user to lobby of deleted chan when parting from active chan ([#489](https://github.com/thelounge/lounge/pull/489) by [@astorije](https://github.com/astorije)) +- Set title attribute on topic on initial page load ([#515](https://github.com/thelounge/lounge/pull/515) by [@williamboman](https://github.com/williamboman)) +- Save user's channels when they sort the channel list ([#401](https://github.com/thelounge/lounge/pull/401) by [@xPaw](https://github.com/xPaw)) +- Turn favicon red on page load if there are highlights ([#344](https://github.com/thelounge/lounge/pull/344) by [@xPaw](https://github.com/xPaw)) +- Keep chat stickied to the bottom on resize ([#346](https://github.com/thelounge/lounge/pull/346) by [@maxpoulin64](https://github.com/maxpoulin64)) +- Only increase unread counter for whitelisted actions ([#273](https://github.com/thelounge/lounge/pull/273) by [@xPaw](https://github.com/xPaw)) +- Parse CTCP replies ([#278](https://github.com/thelounge/lounge/pull/278) by [@xPaw](https://github.com/xPaw)) +- Do not count your own messages as unread ([#279](https://github.com/thelounge/lounge/pull/279) by [@xPaw](https://github.com/xPaw)) +- Do not display incorrect nick when switching to a non connected network ([#252](https://github.com/thelounge/lounge/pull/252) by [@xPaw](https://github.com/xPaw)) +- Keep autocompletion sort whenever user list updates ([#217](https://github.com/thelounge/lounge/pull/217) by [@xPaw](https://github.com/xPaw)) +- Save user when parting channels ([#297](https://github.com/thelounge/lounge/pull/297) by [@xPaw](https://github.com/xPaw)) +- Add labels in connect window ([#300](https://github.com/thelounge/lounge/pull/300) by [@xPaw](https://github.com/xPaw)) +- Add missing `aria-label` on icon buttons ([#303](https://github.com/thelounge/lounge/pull/303) by [@astorije](https://github.com/astorije)) +- Fix missing colors in action messages ([#317](https://github.com/thelounge/lounge/pull/317) by [@astorije](https://github.com/astorije)) +- Don't falsely report failed write if it didn't fail ([`e6990e0`](https://github.com/thelounge/lounge/commit/e6990e0fc7641d18a5bcbabddca1aacf2254ae52) by [@xPaw](https://github.com/xPaw)) +- Fix sending messages starting with a space ([#320](https://github.com/thelounge/lounge/pull/320) by [@maxpoulin64](https://github.com/maxpoulin64)) +- Fix notifications in query windows ([#334](https://github.com/thelounge/lounge/pull/334) by [@xPaw](https://github.com/xPaw)) + +### Security + +- Implement user token persistency ([#370](https://github.com/thelounge/lounge/pull/370) by [@xPaw](https://github.com/xPaw)) +- Restrict access to the home directory by default ([#205](https://github.com/thelounge/lounge/pull/205) by [@maxpoulin64](https://github.com/maxpoulin64)) +- Add security headers to minimize XSS damage ([#292](https://github.com/thelounge/lounge/pull/292) by [@xPaw](https://github.com/xPaw)) +- Do not write user configs outside of the app's users directory ([#238](https://github.com/thelounge/lounge/pull/238) by [@williamboman](https://github.com/williamboman)) +- Don't check for existing password emptiness ([#315](https://github.com/thelounge/lounge/pull/315) by [@maxpoulin64](https://github.com/maxpoulin64)) + ## v2.0.0-rc.2 - 2016-09-21 [Pre-release] [See the full changelog](https://github.com/thelounge/lounge/compare/v2.0.0-rc.1...v2.0.0-rc.2) From 213384ded988ef5327960e17d45342eb710d3373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sat, 24 Sep 2016 23:58:59 -0400 Subject: [PATCH 0053/3926] 2.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fe6daece..46fe6054 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "thelounge", "description": "The self-hosted Web IRC client", - "version": "2.0.0-rc.2", + "version": "2.0.0", "preferGlobal": true, "bin": { "lounge": "index.js" From 396a9cffb1a86b25a3ee5f55d3556136c283c7ee Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sun, 25 Sep 2016 09:52:16 +0300 Subject: [PATCH 0054/3926] Display extra loading messages --- client/js/lounge.js | 6 ++++++ src/server.js | 2 ++ 2 files changed, 8 insertions(+) diff --git a/client/js/lounge.js b/client/js/lounge.js index cca024b9..b537fa05 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -80,6 +80,10 @@ $(function() { }); }); + socket.on("authorized", function() { + $("#loading-page-message").text("Authorized, loading messages…"); + }); + socket.on("auth", function(data) { var login = $("#sign-in"); @@ -147,6 +151,8 @@ $(function() { }); socket.on("init", function(data) { + $("#loading-page-message").text("Rendering…"); + if (data.networks.length === 0) { $("#footer").find(".connect").trigger("click"); } else { diff --git a/src/server.js b/src/server.js index 4dbbc1af..95ec2e9e 100644 --- a/src/server.js +++ b/src/server.js @@ -140,6 +140,8 @@ function init(socket, client) { socket.emit("auth", {success: true}); socket.on("auth", auth); } else { + socket.emit("authorized"); + socket.on( "input", function(data) { From 1d08e909ccd7fcad9069b57bfeeda6e03630f4a2 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sun, 25 Sep 2016 14:53:03 +0300 Subject: [PATCH 0055/3926] Update developer dependencies --- .stylelintrc | 4 ++-- package.json | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.stylelintrc b/.stylelintrc index 3351866c..5f32a858 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -49,9 +49,9 @@ "media-query-list-comma-newline-after": "always-multi-line", "media-query-list-comma-space-after": "always-single-line", "media-query-list-comma-space-before": "never", - "media-query-parentheses-space-inside": "never", + "media-feature-parentheses-space-inside": "never", "no-eol-whitespace": true, - "no-missing-eof-newline": true, + "no-missing-end-of-source-newline": true, "number-leading-zero": "never", "number-no-trailing-zeros": true, "length-zero-no-unit": true, diff --git a/package.json b/package.json index c252b554..2c46415f 100644 --- a/package.json +++ b/package.json @@ -59,13 +59,13 @@ }, "devDependencies": { "chai": "3.5.0", - "eslint": "2.11.1", + "eslint": "3.6.0", "font-awesome": "4.6.3", "handlebars": "4.0.5", - "istanbul": "0.4.3", - "mocha": "2.4.5", - "npm-run-all": "2.1.1", - "stylelint": "6.6.0", + "istanbul": "0.4.5", + "mocha": "3.0.2", + "npm-run-all": "3.1.0", + "stylelint": "7.3.1", "uglify-js": "2.7.3" } } From 7858504c4cf2c0a0426d1874a7d292f4a901ec61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 27 Sep 2016 01:24:47 -0400 Subject: [PATCH 0056/3926] Fix devDependency URL in David badge Because they **needed** to break backward compatibility, didn't they... --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aea710cd..54ea3c3e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Travis CI Build Status](https://travis-ci.org/thelounge/lounge.svg?branch=master)](https://travis-ci.org/thelounge/lounge) [![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/deymtp0lldq78s8t/branch/master?svg=true)](https://ci.appveyor.com/project/astorije/lounge/branch/master) [![Dependency Status](https://david-dm.org/thelounge/lounge.svg)](https://david-dm.org/thelounge/lounge) -[![devDependency Status](https://david-dm.org/thelounge/lounge/dev-status.svg)](https://david-dm.org/thelounge/lounge#info=devDependencies) +[![devDependency Status](https://david-dm.org/thelounge/lounge/dev-status.svg)](https://david-dm.org/thelounge/lounge?type=dev) # The Lounge From 7c02ef53b001eb59fb8002b982995a35a2b957b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 28 Sep 2016 00:20:44 -0400 Subject: [PATCH 0057/3926] Add change log entry for upcoming v2.0.1 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddce00d7..8dafdfeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,30 @@ All sections are explained on the link above, they are all optional, and each of Don't forget to thank the PR authors in a commit comment, and copy/paste the release content as-is in GitHub releases: https://github.com/thelounge/lounge/releases --> +## v2.0.1 - 2016-09-28 + +[See the full changelog](https://github.com/thelounge/lounge/compare/v2.0.0...v2.0.1) + +This is a minor house-keeping release with mostly two sets of changes. + +First, a few bugs were fixed, including one simply preventing The Lounge to run in Safari's private browsing. + +Additionally, the developer experience has been made a tiny bit better, with better documentation, lighter dependencies and simpler theme creation. + +### Changed + +- Add info on README about how to run from source, how to upgrade ([#621](https://github.com/thelounge/lounge/pull/621) by [@astorije](https://github.com/astorije)) +- Move uglify invocation into npm scripts and remove grunt ([#628](https://github.com/thelounge/lounge/pull/628) by [@nornagon](https://github.com/nornagon)) +- Move Shout theme borders to example theme ([#359](https://github.com/thelounge/lounge/pull/359) by [@xPaw](https://github.com/xPaw)) +- Update developer dependencies ([#639](https://github.com/thelounge/lounge/pull/639) by [@xPaw](https://github.com/xPaw)) + +### Fixed + +- Remove -ms-transform and add missed -webkit-transform ([#629](https://github.com/thelounge/lounge/pull/629) by [@xPaw](https://github.com/xPaw)) +- Ensure localStorage cannot fail because of quota or Safari private browsing ([#625](https://github.com/thelounge/lounge/pull/625) by [@astorije](https://github.com/astorije)) +- Disable pull-to-refresh on mobile that conflicts with scrolling the message list ([#618](https://github.com/thelounge/lounge/pull/618) by [@astorije](https://github.com/astorije)) +- Handle stderr when using edit or config command ([#622](https://github.com/thelounge/lounge/pull/622) by [@MaxLeiter](https://github.com/MaxLeiter)) + ## v2.0.0 - 2016-09-24 [See the full changelog](https://github.com/thelounge/lounge/compare/v1.5.0...v2.0.0) From 4541309988c5fee292eeaf41c76d9bbdc899e1c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 28 Sep 2016 00:21:01 -0400 Subject: [PATCH 0058/3926] 2.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2c46415f..177bd846 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "thelounge", "description": "The self-hosted Web IRC client", - "version": "2.0.0", + "version": "2.0.1", "preferGlobal": true, "bin": { "lounge": "index.js" From 65ba8af6604723d5035f4ccc4ff726463ac00f35 Mon Sep 17 00:00:00 2001 From: Alexander Schittler Date: Wed, 28 Sep 2016 20:23:06 +0200 Subject: [PATCH 0059/3926] Themes: Fixed CSS rule selectors for highlight messages --- client/themes/morning.css | 3 ++- client/themes/zenburn.css | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/client/themes/morning.css b/client/themes/morning.css index f6731ed8..a837d62c 100644 --- a/client/themes/morning.css +++ b/client/themes/morning.css @@ -179,7 +179,8 @@ body { #chat .error, #chat .error .from, #chat .channel .highlight, -#chat .channel .highlight .from { +#chat .channel .highlight .from, +#chat .channel .highlight .text { color: #f92772; } diff --git a/client/themes/zenburn.css b/client/themes/zenburn.css index a75afab7..cd95b652 100644 --- a/client/themes/zenburn.css +++ b/client/themes/zenburn.css @@ -205,7 +205,8 @@ body { #chat .error, #chat .error .from, #chat .channel .highlight, -#chat .channel .highlight .from { +#chat .channel .highlight .from, +#chat .channel .highlight .text { color: #bc6c4c; } From f2c4d08801903d2fcbfed46cf5d6aee85bc838eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Fri, 30 Sep 2016 01:50:54 -0400 Subject: [PATCH 0060/3926] Fix title icons for channels and channel lists This bug slipped when adding `/list` support in #258. --- client/css/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/css/style.css b/client/css/style.css index 9bd5569a..65bba699 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -185,7 +185,7 @@ button { #chat .channel .title:before { content: "\f0f6"; /* http://fontawesome.io/icon/file-text-o/ */ } #sidebar .chan.special:before, -#chat .channel .title:before { content: "\f03a"; /* http://fontawesome.io/icon/list/ */ } +#chat .special .title:before { content: "\f03a"; /* http://fontawesome.io/icon/list/ */ } #footer .sign-in:before { content: "\f023"; /* http://fontawesome.io/icon/lock/ */ } #footer .connect:before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ } From 63f4fc39c941f45039f65678d9765c9274588d65 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 1 Oct 2016 00:29:49 +0300 Subject: [PATCH 0061/3926] Display wallops in server window Fixes #225 --- src/plugins/irc-events/message.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/plugins/irc-events/message.js b/src/plugins/irc-events/message.js index bd1b5291..cbe36fae 100644 --- a/src/plugins/irc-events/message.js +++ b/src/plugins/irc-events/message.js @@ -25,6 +25,12 @@ module.exports = function(irc, network) { handleMessage(data); }); + irc.on("wallops", function(data) { + data.from_server = true; + data.type = Msg.Type.NOTICE; + handleMessage(data); + }); + function handleMessage(data) { var highlight = false; var self = data.nick === irc.user.nick; From fc03a338fc692304dd6f2753efc10666c175064f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sat, 1 Oct 2016 02:46:19 -0400 Subject: [PATCH 0062/3926] Display localized timestamp in title of message times --- client/js/libs/handlebars/localetime.js | 5 +++++ client/views/msg.tpl | 2 +- client/views/msg_action.tpl | 2 +- client/views/msg_unhandled.tpl | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 client/js/libs/handlebars/localetime.js diff --git a/client/js/libs/handlebars/localetime.js b/client/js/libs/handlebars/localetime.js new file mode 100644 index 00000000..c8dc097a --- /dev/null +++ b/client/js/libs/handlebars/localetime.js @@ -0,0 +1,5 @@ +"use strict"; + +Handlebars.registerHelper("localetime", function(time) { + return new Date(time).toLocaleString(); +}); diff --git a/client/views/msg.tpl b/client/views/msg.tpl index 65737198..8817f2be 100644 --- a/client/views/msg.tpl +++ b/client/views/msg.tpl @@ -1,5 +1,5 @@
- + {{tz time}} diff --git a/client/views/msg_action.tpl b/client/views/msg_action.tpl index 9b56fdad..9acc07d0 100644 --- a/client/views/msg_action.tpl +++ b/client/views/msg_action.tpl @@ -1,5 +1,5 @@
- + {{tz time}} diff --git a/client/views/msg_unhandled.tpl b/client/views/msg_unhandled.tpl index 25b568f9..24838975 100644 --- a/client/views/msg_unhandled.tpl +++ b/client/views/msg_unhandled.tpl @@ -1,5 +1,5 @@
- + {{tz time}} [{{command}}] From 743d4b61d5f426613249412da3901a7cf581d21b Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 1 Oct 2016 13:09:57 +0300 Subject: [PATCH 0063/3926] Do not trigger a DOM event on every message --- client/js/lounge.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 58cf0c17..0af95638 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -352,17 +352,14 @@ $(function() { var target = "#chan-" + data.chan; var container = chat.find(target + " .messages"); - container - .append(msg) - .trigger("msg", [ - target, - data.msg - ]); + container.append(msg); if (data.msg.self) { container .find(".unread-marker") .appendTo(container); + } else { + chatMessageShown(target, data.msg); } }); @@ -892,11 +889,7 @@ $(function() { }); }); - chat.on("msg", ".messages", function(e, target, msg) { - if (msg.self) { - return; - } - + function chatMessageShown(target, msg) { var button = sidebar.find(".chan[data-target='" + target + "']"); if (msg.highlight || (options.notifyAllMessages && msg.type === "message")) { if (!document.hasFocus() || !$(target).hasClass("active")) { @@ -960,7 +953,7 @@ $(function() { badge.addClass("highlight"); } } - }); + } chat.on("click", ".show-more-button", function() { var self = $(this); From 4e1d89f5676bbdd1296c4ab057f8255f5ba54a80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sat, 1 Oct 2016 15:38:06 -0400 Subject: [PATCH 0064/3926] Consolidate locale time helpers When working on #660, I missed that helper already existed, added in #167. --- client/js/libs/handlebars/date.js | 7 ------- client/views/actions/topic_set_by.tpl | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 client/js/libs/handlebars/date.js diff --git a/client/js/libs/handlebars/date.js b/client/js/libs/handlebars/date.js deleted file mode 100644 index 751e713d..00000000 --- a/client/js/libs/handlebars/date.js +++ /dev/null @@ -1,7 +0,0 @@ -Handlebars.registerHelper( - "localeDate", function(date) { - date = new Date(date); - - return date.toLocaleString(); - } -); diff --git a/client/views/actions/topic_set_by.tpl b/client/views/actions/topic_set_by.tpl index 03364afc..e80c26a2 100644 --- a/client/views/actions/topic_set_by.tpl +++ b/client/views/actions/topic_set_by.tpl @@ -1,3 +1,3 @@ Topic set by {{mode}}{{nick}} -on {{localeDate when}} +on {{localetime when}} From 12839af6849f52f07311904201c0511212f20e21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Fri, 29 Jul 2016 02:10:29 -0400 Subject: [PATCH 0065/3926] Make nick badge editable to set it in the UI --- client/css/style.css | 58 ++++++++++++++++++++++++++++++++++++------ client/index.html | 8 +++++- client/js/lounge.js | 60 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 116 insertions(+), 10 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 65bba699..9596efd7 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -160,7 +160,8 @@ button { #chat .whois .from:before, #chat .nick .from:before, #chat .action .from:before, -.context-menu-item:before { +.context-menu-item:before, +#nick button:before { font: normal normal normal 14px/1 FontAwesome; font-size: inherit; /* Can't have font-size inherit on line above, so need to override */ -webkit-font-smoothing: antialiased; @@ -260,6 +261,18 @@ button { margin-right: 9px; } +#set-nick:before { + content: "\f040"; /* http://fontawesome.io/icon/pencil/ */ +} + +#submit-nick:before { + content: "\f00c"; /* http://fontawesome.io/icon/check/ */ +} + +#cancel-nick:before { + content: "\f00d"; /* http://fontawesome.io/icon/times/ */ +} + /* End icons */ #wrap { @@ -1284,32 +1297,61 @@ button { align-items: flex-end; } +[contenteditable]:focus { + outline: none; +} + +/* Nick editor */ + #form #nick { background: #f6f6f6; color: #666; font: inherit; font-size: 11px; margin: 4px; - line-height: 26px; + line-height: 22px; height: 24px; - padding: 0 9px; - border-radius: 1px; + padding-left: 9px; + padding-right: 5px; + border-radius: 2px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; -webkit-flex: 0 0 auto; flex: 0 0 auto; + border: 1px solid transparent; + transition: border-color .2s; } -#form #nick:empty { - visibility: hidden; +#form #nick-value { + padding-right: 5px; } -#form #nick:after { - content: ":"; +#form #nick.editable { + border-color: black; } +#nick button#set-nick, +#nick button#submit-nick, +#nick button#cancel-nick { + color: #aaa; + width: 18px; +} + +#nick.editable button#set-nick, +#nick button#submit-nick, +#nick button#cancel-nick { + display: none; +} + +#nick.editable button#submit-nick, +#nick.editable button#cancel-nick { + display: inline-block; +} + +/* End nick editor */ + #form #input { background: transparent; border: none; diff --git a/client/index.html b/client/index.html index 1276b99b..bc3c7e0e 100644 --- a/client/index.html +++ b/client/index.html @@ -59,7 +59,13 @@
- + + + + diff --git a/client/js/lounge.js b/client/js/lounge.js index 58cf0c17..1b65f4b0 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -713,6 +713,61 @@ $(function() { .first(); } + $("button#set-nick").on("click", function() { + toggleNickEditor(true); + + // Selects existing nick in the editable text field + var element = document.querySelector("#nick-value"); + element.focus(); + var range = document.createRange(); + range.selectNodeContents(element); + var selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + }); + + $("button#cancel-nick").on("click", cancelNick); + $("button#submit-nick").on("click", submitNick); + + function toggleNickEditor(toggle) { + $("#nick").toggleClass("editable", toggle); + $("#nick-value").attr("contenteditable", toggle); + } + + // FIXME Reset content when new nick is invalid (already in use, forbidden chars, ...) + function submitNick() { + var newNick = $("#nick-value").text(); + + socket.emit("input", { + target: chat.data("id"), + text: "/nick " + newNick + }); + + toggleNickEditor(false); + } + + function cancelNick() { + setNick(sidebar.find(".chan.active").closest(".network").data("nick")); + } + + $("#nick-value").keypress(function(e) { + switch (e.keyCode ? e.keyCode : e.which) { + case 13: // Enter + // Ensures a new line is not added when pressing Enter + e.preventDefault(); + break; + } + }).keyup(function(e) { + switch (e.keyCode ? e.keyCode : e.which) { + case 13: // Enter + submitNick(); + break; + case 27: // Escape + cancelNick(); + break; + } + }); + chat.on("click", ".inline-channel", function() { var name = $(this).data("chan"); var chan = findCurrentNetworkChan(name); @@ -1198,7 +1253,10 @@ $(function() { } function setNick(nick) { - $("#nick").text(nick); + $("#nick-value").text(nick); + // Closes the nick editor when canceling, changing channel, or when a nick + // is set in a different tab / browser / device. + toggleNickEditor(false); } function move(array, old_index, new_index) { From 4328946f80023318c389c053727211e952f6de51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 3 Aug 2016 01:06:58 -0400 Subject: [PATCH 0066/3926] Add tooltips to nick editor buttons --- client/css/style.css | 5 ++++- client/index.html | 7 +++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 9596efd7..695a0b75 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1340,8 +1340,11 @@ button { } #nick.editable button#set-nick, +#nick.editable #set-nick-tooltip, #nick button#submit-nick, -#nick button#cancel-nick { +#nick:not(.editable) #save-nick-tooltip, +#nick button#cancel-nick, +#nick:not(.editable) #cancel-nick-tooltip { display: none; } diff --git a/client/index.html b/client/index.html index bc3c7e0e..25be00ab 100644 --- a/client/index.html +++ b/client/index.html @@ -61,10 +61,9 @@
- + --> From beb6d1ea5b2cb2e301e076fb7f3978fc97faf775 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 1 Oct 2016 19:46:36 +0300 Subject: [PATCH 0067/3926] Reset nickname in UI back to previous one on error --- src/plugins/irc-events/error.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/plugins/irc-events/error.js b/src/plugins/irc-events/error.js index b7709738..3a92ebb7 100644 --- a/src/plugins/irc-events/error.js +++ b/src/plugins/irc-events/error.js @@ -28,6 +28,11 @@ module.exports = function(irc, network) { var random = (data.nick || irc.user.nick) + Math.floor(10 + (Math.random() * 89)); irc.changeNick(random); } + + client.emit("nick", { + network: network.id, + nick: irc.user.nick + }); }); irc.on("nick invalid", function(data) { @@ -42,5 +47,10 @@ module.exports = function(irc, network) { var random = "i" + Math.random().toString(36).substr(2, 10); // 'i' so it never begins with a number irc.changeNick(random); } + + client.emit("nick", { + network: network.id, + nick: irc.user.nick + }); }); }; From 024369d4c32c0c29037ba5d15dbb3d226ec6892c Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 1 Oct 2016 20:04:03 +0300 Subject: [PATCH 0068/3926] Implement our own /nick command to allow editing nick when not connected --- client/js/lounge.js | 15 ++++++++++----- src/client.js | 1 + src/plugins/inputs/nick.js | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 src/plugins/inputs/nick.js diff --git a/client/js/lounge.js b/client/js/lounge.js index 1b65f4b0..914d9c2c 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -734,16 +734,20 @@ $(function() { $("#nick-value").attr("contenteditable", toggle); } - // FIXME Reset content when new nick is invalid (already in use, forbidden chars, ...) function submitNick() { - var newNick = $("#nick-value").text(); + var newNick = $("#nick-value").text().trim(); + + if (newNick.length === 0) { + cancelNick(); + return; + } + + toggleNickEditor(false); socket.emit("input", { target: chat.data("id"), text: "/nick " + newNick }); - - toggleNickEditor(false); } function cancelNick() { @@ -1253,10 +1257,11 @@ $(function() { } function setNick(nick) { - $("#nick-value").text(nick); // Closes the nick editor when canceling, changing channel, or when a nick // is set in a different tab / browser / device. toggleNickEditor(false); + + $("#nick-value").text(nick); } function move(array, old_index, new_index) { diff --git a/src/client.js b/src/client.js index 222b7628..f8be844a 100644 --- a/src/client.js +++ b/src/client.js @@ -42,6 +42,7 @@ var inputs = [ "invite", "kick", "mode", + "nick", "notice", "query", "quit", diff --git a/src/plugins/inputs/nick.js b/src/plugins/inputs/nick.js new file mode 100644 index 00000000..a267b1e4 --- /dev/null +++ b/src/plugins/inputs/nick.js @@ -0,0 +1,37 @@ +var Msg = require("../../models/msg"); + +exports.commands = ["nick"]; +exports.allowDisconnected = true; + +exports.input = function(network, chan, cmd, args) { + if (args.length === 0) { + chan.pushMessage(this, new Msg({ + type: Msg.Type.ERROR, + text: "Usage: /nick " + })); + return; + } + + if (args.length !== 1) { + chan.pushMessage(this, new Msg({ + type: Msg.Type.ERROR, + text: "Nicknames may not contain spaces." + })); + return; + } + + var newNick = args[0]; + + // If connected to IRC, send to server and wait for ACK + // otherwise update the nick and UI straight away + if (network.irc && network.irc.connection) { + network.irc.raw("NICK", newNick); + } else { + network.setNick(newNick); + + this.emit("nick", { + network: network.id, + nick: newNick + }); + } +}; From 565e37e873b73a10aa652c96efd731fa202f051a Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Wed, 28 Sep 2016 11:46:00 -0700 Subject: [PATCH 0069/3926] Fix unhandled message color in Crypto theme --- client/css/style.css | 4 ---- client/themes/crypto.css | 4 ++++ client/themes/example.css | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 801a2ae6..73fae1e0 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -949,10 +949,6 @@ button { color: #f00; } -#chat .unhandled .from { - color: #eee; -} - #chat .msg.toggle .time { visibility: hidden; } diff --git a/client/themes/crypto.css b/client/themes/crypto.css index 61a7c31d..50b01388 100644 --- a/client/themes/crypto.css +++ b/client/themes/crypto.css @@ -85,6 +85,10 @@ a:hover, color: #666; } +#chat .unhandled .from { + color: #ddd; +} + #sidebar .active { color: #fff; } diff --git a/client/themes/example.css b/client/themes/example.css index d85df9fa..f59944fe 100644 --- a/client/themes/example.css +++ b/client/themes/example.css @@ -27,6 +27,10 @@ body { top: 4px; } +#chat .unhandled .from { + color: #ddd; +} + #windows .window:before { background: #f4f4f4; background-image: linear-gradient(#f4f4f4, #ececec); From 41525ec20ce048953f90be00490f8a25376a4670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Mon, 3 Oct 2016 19:40:26 -0400 Subject: [PATCH 0070/3926] Add hostmasks in logs when possible This will augment logs for `join`/`part`/`quit` with something similar to: ``` [2016-10-03 23:19:29] * astorije2 (~lounge-us@123.45.67.89) join [2016-10-03 23:22:04] * foobar (~foo@irc.example.com) join [2016-10-03 23:22:00] * foo (foo@gateway/web/freenode/ip.12.34.56.789) quit Quit: Page closed [2016-10-03 23:22:12] * bar (~foo@unaffiliated/bar) quit Ping timeout: 252 seconds [2016-10-03 23:31:23] * astorije (~astorije@128.30.0.0) part ``` --- src/userLog.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/userLog.js b/src/userLog.js index 2d4e4ba1..1e10f4d4 100644 --- a/src/userLog.js +++ b/src/userLog.js @@ -16,19 +16,26 @@ module.exports.write = function(user, network, chan, msg) { var tz = Helper.config.logs.timezone || "UTC+00:00"; var time = moment().utcOffset(tz).format(format); - var line = "[" + time + "] "; + var line = `[${time}] `; var type = msg.type.trim(); if (type === "message" || type === "highlight") { // Format: // [2014-01-01 00:00:00] Put that cookie down.. Now!! - line += "<" + msg.from + "> " + msg.text; + line += `<${msg.from}> ${msg.text}`; } else { // Format: // [2014-01-01 00:00:00] * Arnold quit - line += "* " + msg.from + " " + msg.type; + line += `* ${msg.from} `; + + if (msg.hostmask) { + line += `(${msg.hostmask}) `; + } + + line += msg.type; + if (msg.text) { - line += " " + msg.text; + line += ` ${msg.text}`; } } From 5b6f5d5dcef869abc5d88403632ad51e547f7a2c Mon Sep 17 00:00:00 2001 From: toXel Date: Wed, 5 Oct 2016 00:35:04 +0200 Subject: [PATCH 0071/3926] Check if SSL key and certificate files exist --- src/server.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/server.js b/src/server.js index 95ec2e9e..980b68a4 100644 --- a/src/server.js +++ b/src/server.js @@ -36,9 +36,19 @@ module.exports = function() { server = server.createServer(app).listen(config.port, config.host); } else { server = require("spdy"); + const keyPath = Helper.expandHome(config.https.key); + const certPath = Helper.expandHome(config.https.certificate); + if (!config.https.key.length || !fs.existsSync(keyPath)) { + log.error("Path to SSL key is invalid. Stopping server..."); + process.exit(); + } + if (!config.https.certificate.length || !fs.existsSync(certPath)) { + log.error("Path to SSL certificate is invalid. Stopping server..."); + process.exit(); + } server = server.createServer({ - key: fs.readFileSync(Helper.expandHome(config.https.key)), - cert: fs.readFileSync(Helper.expandHome(config.https.certificate)) + key: fs.readFileSync(keyPath), + cert: fs.readFileSync(certPath) }, app).listen(config.port, config.host); } From 2e82c6b5c6986dd1217ba57389706c62e832ae7a Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 24 Sep 2016 19:34:35 +0300 Subject: [PATCH 0072/3926] Make use of multi-prefix cap and remove NAMES spam on mode changes --- src/models/user.js | 17 ++++--- src/plugins/irc-events/join.js | 2 +- src/plugins/irc-events/mode.js | 54 ++++++++++++++++++++--- src/plugins/irc-events/names.js | 13 +++--- test/models/chan.js | 78 +++++++++++++++++++-------------- 5 files changed, 110 insertions(+), 54 deletions(-) diff --git a/src/models/user.js b/src/models/user.js index 68d251d1..e5898253 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -2,13 +2,16 @@ var _ = require("lodash"); module.exports = User; -function User(attr) { - // TODO: Remove this - attr.name = attr.name || attr.nick; - attr.mode = attr.mode || (attr.modes && attr.modes[0]) || ""; - +function User(attr, prefixLookup) { _.merge(this, _.extend({ - mode: "", - name: "" + modes: [], + nick: "" }, attr)); + + // irc-framework sets character mode, but lounge works with symbols + this.modes = this.modes.map(mode => prefixLookup[mode]); + + // TODO: Remove this + this.name = this.nick; + this.mode = (this.modes && this.modes[0]) || ""; } diff --git a/src/plugins/irc-events/join.js b/src/plugins/irc-events/join.js index d2984db2..69198904 100644 --- a/src/plugins/irc-events/join.js +++ b/src/plugins/irc-events/join.js @@ -17,7 +17,7 @@ module.exports = function(irc, network) { chan: chan }); } - chan.users.push(new User({nick: data.nick, modes: ""})); + chan.users.push(new User({nick: data.nick})); chan.sortUsers(irc); client.emit("users", { chan: chan.id diff --git a/src/plugins/irc-events/mode.js b/src/plugins/irc-events/mode.js index a466be2d..886c42fb 100644 --- a/src/plugins/irc-events/mode.js +++ b/src/plugins/irc-events/mode.js @@ -17,17 +17,19 @@ module.exports = function(irc, network) { } var usersUpdated; + var supportsMultiPrefix = network.irc.network.cap.isEnabled("multi-prefix"); + var userModeSortPriority = {}; + + irc.network.options.PREFIX.forEach(function(prefix, index) { + userModeSortPriority[prefix.symbol] = index; + }); for (var i = 0; i < data.modes.length; i++) { var mode = data.modes[i]; var text = mode.mode; + if (mode.param) { text += " " + mode.param; - - var user = _.find(targetChan.users, {name: mode.param}); - if (typeof user !== "undefined") { - usersUpdated = true; - } } var msg = new Msg({ @@ -39,11 +41,51 @@ module.exports = function(irc, network) { self: data.nick === irc.user.nick }); targetChan.pushMessage(client, msg); + + if (!mode.param) { + continue; + } + + var user = _.find(targetChan.users, {name: mode.param}); + if (!user) { + continue; + } + + usersUpdated = true; + + if (!supportsMultiPrefix) { + continue; + } + + var add = mode.mode[0] === "+"; + var changedMode = network.prefixLookup[mode.mode[1]]; + + if (!add) { + _.pull(user.modes, changedMode); + } else if (user.modes.indexOf(changedMode) === -1) { + user.modes.push(changedMode); + user.modes.sort(function(a, b) { + return userModeSortPriority[a] - userModeSortPriority[b]; + }); + } + + // TODO: remove in future + user.mode = (user.modes && user.modes[0]) || ""; } - if (usersUpdated) { + if (!usersUpdated) { + return; + } + + if (!supportsMultiPrefix) { // TODO: This is horrible irc.raw("NAMES", data.target); + } else { + targetChan.sortUsers(irc); + + client.emit("users", { + chan: targetChan.id + }); } }); }; diff --git a/src/plugins/irc-events/names.js b/src/plugins/irc-events/names.js index 4307ff22..6bee2696 100644 --- a/src/plugins/irc-events/names.js +++ b/src/plugins/irc-events/names.js @@ -8,18 +8,17 @@ module.exports = function(irc, network) { if (typeof chan === "undefined") { return; } - chan.users = []; - _.each(data.users, function(u) { - var user = new User(u); - // irc-framework sets characater mode, but lounge works with symbols - if (user.mode) { - user.mode = network.prefixLookup[user.mode]; - } + chan.users = []; + + _.each(data.users, function(u) { + var user = new User(u, network.prefixLookup); chan.users.push(user); }); + chan.sortUsers(irc); + client.emit("users", { chan: chan.id }); diff --git a/test/models/chan.js b/test/models/chan.js index fe85f927..a12335c0 100644 --- a/test/models/chan.js +++ b/test/models/chan.js @@ -1,36 +1,48 @@ "use strict"; +var _ = require("lodash"); var expect = require("chai").expect; var Chan = require("../../src/models/chan"); var User = require("../../src/models/user"); -function makeUser(name) { - // TODO Update/Fix this when User constructor gets reworked (see its TODO) - return new User({nick: name, mode: ""}); -} - -function getUserNames(chan) { - return chan.users.map(function(u) { - return u.name; - }); -} - describe("Chan", function() { describe("#sortUsers(irc)", function() { - var fullNetworkPrefix = [ - {symbol: "~", mode: "q"}, - {symbol: "&", mode: "a"}, - {symbol: "@", mode: "o"}, - {symbol: "%", mode: "h"}, - {symbol: "+", mode: "v"} - ]; + var network = { + network: { + options: { + PREFIX: [ + {symbol: "~", mode: "q"}, + {symbol: "&", mode: "a"}, + {symbol: "@", mode: "o"}, + {symbol: "%", mode: "h"}, + {symbol: "+", mode: "v"} + ] + } + } + }; + + var prefixLookup = {}; + + _.each(network.network.options.PREFIX, function(mode) { + prefixLookup[mode.mode] = mode.symbol; + }); + + var makeUser = function(nick) { + return new User({nick: nick}, prefixLookup); + }; + + var getUserNames = function(chan) { + return chan.users.map(function(u) { + return u.name; + }); + }; it("should sort a simple user list", function() { var chan = new Chan({users: [ "JocelynD", "YaManicKill", "astorije", "xPaw", "Max-P" ].map(makeUser)}); - chan.sortUsers({network: {options: {PREFIX: fullNetworkPrefix}}}); + chan.sortUsers(network); expect(getUserNames(chan)).to.deep.equal([ "astorije", "JocelynD", "Max-P", "xPaw", "YaManicKill" @@ -39,13 +51,13 @@ describe("Chan", function() { it("should group users by modes", function() { var chan = new Chan({users: [ - new User({name: "JocelynD", mode: "&"}), - new User({name: "YaManicKill", mode: "+"}), - new User({name: "astorije", mode: "%"}), - new User({name: "xPaw", mode: "~"}), - new User({name: "Max-P", mode: "@"}), + new User({nick: "JocelynD", modes: ["a", "o"]}, prefixLookup), + new User({nick: "YaManicKill", modes: ["v"]}, prefixLookup), + new User({nick: "astorije", modes: ["h"]}, prefixLookup), + new User({nick: "xPaw", modes: ["q"]}, prefixLookup), + new User({nick: "Max-P", modes: ["o"]}, prefixLookup), ]}); - chan.sortUsers({network: {options: {PREFIX: fullNetworkPrefix}}}); + chan.sortUsers(network); expect(getUserNames(chan)).to.deep.equal([ "xPaw", "JocelynD", "Max-P", "astorije", "YaManicKill" @@ -54,13 +66,13 @@ describe("Chan", function() { it("should sort a mix of users and modes", function() { var chan = new Chan({users: [ - new User({name: "JocelynD"}), - new User({name: "YaManicKill", mode: "@"}), - new User({name: "astorije"}), - new User({name: "xPaw"}), - new User({name: "Max-P", mode: "@"}), + new User({nick: "JocelynD"}, prefixLookup), + new User({nick: "YaManicKill", modes: ["o"]}, prefixLookup), + new User({nick: "astorije"}, prefixLookup), + new User({nick: "xPaw"}, prefixLookup), + new User({nick: "Max-P", modes: ["o"]}, prefixLookup), ]}); - chan.sortUsers({network: {options: {PREFIX: fullNetworkPrefix}}}); + chan.sortUsers(network); expect(getUserNames(chan)).to.deep.equal( ["Max-P", "YaManicKill", "astorije", "JocelynD", "xPaw"] @@ -69,7 +81,7 @@ describe("Chan", function() { it("should be case-insensitive", function() { var chan = new Chan({users: ["aB", "Ad", "AA", "ac"].map(makeUser)}); - chan.sortUsers({network: {options: {PREFIX: fullNetworkPrefix}}}); + chan.sortUsers(network); expect(getUserNames(chan)).to.deep.equal(["AA", "aB", "ac", "Ad"]); }); @@ -79,7 +91,7 @@ describe("Chan", function() { "[foo", "]foo", "(foo)", "{foo}", "", "_foo", "@foo", "^foo", "&foo", "!foo", "+foo", "Foo" ].map(makeUser)}); - chan.sortUsers({network: {options: {PREFIX: fullNetworkPrefix}}}); + chan.sortUsers(network); expect(getUserNames(chan)).to.deep.equal([ "!foo", "&foo", "(foo)", "+foo", "", "@foo", "[foo", "]foo", From 8f3f1ca0b17e78e9916ac7825171469e855e7eca Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sun, 2 Oct 2016 10:37:37 +0300 Subject: [PATCH 0073/3926] Fix memory and reference shuffling when creating models --- src/models/chan.js | 4 +-- src/models/msg.js | 4 +-- src/models/network.js | 8 +++--- src/models/user.js | 4 +-- test/models/network.js | 61 ++++++++++++++++++++++++++++++++++++++---- 5 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/models/chan.js b/src/models/chan.js index 750e798a..828c74c3 100644 --- a/src/models/chan.js +++ b/src/models/chan.js @@ -13,7 +13,7 @@ Chan.Type = { var id = 0; function Chan(attr) { - _.merge(this, _.extend({ + _.defaults(this, attr, { id: id++, messages: [], name: "", @@ -23,7 +23,7 @@ function Chan(attr) { unread: 0, highlight: false, users: [] - }, attr)); + }); } Chan.prototype.pushMessage = function(client, msg) { diff --git a/src/models/msg.js b/src/models/msg.js index 73c19ff1..0a80e736 100644 --- a/src/models/msg.js +++ b/src/models/msg.js @@ -26,13 +26,13 @@ module.exports = Msg; var id = 0; function Msg(attr) { - _.merge(this, _.extend({ + _.defaults(this, attr, { from: "", id: id++, text: "", type: Msg.Type.MESSAGE, self: false - }, attr)); + }); if (this.time > 0) { this.time = new Date(this.time); diff --git a/src/models/network.js b/src/models/network.js index cbeea7d4..0d492382 100644 --- a/src/models/network.js +++ b/src/models/network.js @@ -6,7 +6,7 @@ module.exports = Network; var id = 0; function Network(attr) { - _.merge(this, _.extend({ + _.defaults(this, attr, { name: "", host: "", port: 6667, @@ -24,7 +24,8 @@ function Network(attr) { PREFIX: [], }, chanCache: [], - }, attr)); + }); + this.name = attr.name || prettify(attr.host); this.channels.unshift( new Chan({ @@ -53,9 +54,10 @@ Network.prototype.setNick = function(nick) { Network.prototype.toJSON = function() { return _.omit(this, [ + "chanCache", + "highlightRegex", "irc", "password", - "highlightRegex" ]); }; diff --git a/src/models/user.js b/src/models/user.js index e5898253..ba81f28a 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -3,10 +3,10 @@ var _ = require("lodash"); module.exports = User; function User(attr, prefixLookup) { - _.merge(this, _.extend({ + _.defaults(this, attr, { modes: [], nick: "" - }, attr)); + }); // irc-framework sets character mode, but lounge works with symbols this.modes = this.modes.map(mode => prefixLookup[mode]); diff --git a/test/models/network.js b/test/models/network.js index 04314b3d..bb9cfb3f 100644 --- a/test/models/network.js +++ b/test/models/network.js @@ -3,18 +3,23 @@ var expect = require("chai").expect; var Chan = require("../../src/models/chan"); +var Msg = require("../../src/models/msg"); var Network = require("../../src/models/network"); describe("Network", function() { describe("#export()", function() { it("should produce an valid object", function() { - var network = new Network({name: "networkName"}); + var network = new Network({ + name: "networkName", + channels: [ + new Chan({name: "#thelounge"}), + new Chan({name: "&foobar"}), + new Chan({name: "Channel List", type: Chan.Type.SPECIAL}), + new Chan({name: "PrivateChat", type: Chan.Type.QUERY}), + ] + }); network.setNick("chillin`"); - network.channels.push(new Chan({name: "#thelounge"})); - network.channels.push(new Chan({name: "&foobar"})); - network.channels.push(new Chan({name: "Lobby", type: Chan.Type.LOBBY})); - network.channels.push(new Chan({name: "PrivateChat", type: Chan.Type.QUERY})); expect(network.export()).to.deep.equal({ name: "networkName", @@ -34,5 +39,51 @@ describe("Network", function() { ] }); }); + + it("lobby should be at the top", function() { + var network = new Network({ + name: "Super Nice Network", + channels: [ + new Chan({name: "AAAA!", type: Chan.Type.QUERY}), + new Chan({name: "#thelounge"}), + new Chan({name: "&foobar"}), + ] + }); + network.channels.push(new Chan({name: "#swag"})); + + expect(network.channels[0].name).to.equal("Super Nice Network"); + expect(network.channels[0].type).to.equal(Chan.Type.LOBBY); + }); + + it("should maintain channel reference", function() { + var chan = new Chan({ + name: "#506-bug-fix", + messages: [ + new Msg({ + text: "message in constructor" + }) + ] + }); + + var network = new Network({ + name: "networkName", + channels: [ + chan + ] + }); + + chan.messages.push(new Msg({ + text: "message in original instance" + })); + + network.channels[1].messages.push(new Msg({ + text: "message after network creation" + })); + + expect(network.channels[1].messages).to.have.lengthOf(3); + expect(network.channels[1].messages[0].text).to.equal("message in constructor"); + expect(network.channels[1].messages[1].text).to.equal("message in original instance"); + expect(network.channels[1].messages[2].text).to.equal("message after network creation"); + }); }); }); From 3b8a478e343aaa900fa991369d0c5a1a44e3a2b8 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sun, 9 Oct 2016 12:29:17 +0300 Subject: [PATCH 0074/3926] Fix loading fonts in Microsoft Edge --- src/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.js b/src/server.js index 980b68a4..80598fd0 100644 --- a/src/server.js +++ b/src/server.js @@ -138,7 +138,7 @@ function index(req, res, next) { return css.slice(0, -4); }); var template = _.template(file); - res.setHeader("Content-Security-Policy", "default-src *; connect-src 'self' ws: wss:; style-src * 'unsafe-inline'; script-src 'self'; child-src 'none'; object-src 'none'; form-action 'none'; referrer no-referrer;"); + res.setHeader("Content-Security-Policy", "default-src *; connect-src 'self' ws: wss:; style-src * 'unsafe-inline'; script-src 'self'; child-src 'self'; object-src 'none'; form-action 'none'; referrer no-referrer;"); res.setHeader("Content-Type", "text/html"); res.writeHead(200); res.end(template(data)); From caa46042bf2ee5d5b8c8b6b1bafb3b34cfa93d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sun, 9 Oct 2016 15:14:02 -0400 Subject: [PATCH 0075/3926] Enforce strict mode across all JS files with ESLint Several ES6 additions are only available in strict mode. Example: > SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode Strict mode was also enabled in a few of our files already, and it is a good thing to have anyway. --- .eslintrc.yml | 1 + client/js/libs/handlebars/diff.js | 2 ++ client/js/libs/handlebars/equal.js | 2 ++ client/js/libs/handlebars/modes.js | 2 ++ client/js/libs/handlebars/parse.js | 2 ++ client/js/libs/handlebars/roundBadgeNumber.js | 2 ++ client/js/libs/handlebars/tz.js | 2 ++ client/js/libs/handlebars/users.js | 2 ++ client/js/loading-slow-alert.js | 2 ++ client/js/lounge.js | 2 ++ defaults/config.js | 2 ++ index.js | 3 +++ src/client.js | 2 ++ src/clientManager.js | 2 ++ src/command-line/add.js | 2 ++ src/command-line/config.js | 2 ++ src/command-line/edit.js | 2 ++ src/command-line/index.js | 2 ++ src/command-line/list.js | 2 ++ src/command-line/remove.js | 2 ++ src/command-line/reset.js | 2 ++ src/command-line/start.js | 2 ++ src/helper.js | 2 ++ src/identd.js | 2 ++ src/log.js | 2 ++ src/models/chan.js | 2 ++ src/models/msg.js | 2 ++ src/models/network.js | 2 ++ src/models/user.js | 2 ++ src/oidentd.js | 2 ++ src/plugins/inputs/action.js | 2 ++ src/plugins/inputs/connect.js | 2 ++ src/plugins/inputs/ctcp.js | 2 ++ src/plugins/inputs/disconnect.js | 2 ++ src/plugins/inputs/invite.js | 2 ++ src/plugins/inputs/kick.js | 2 ++ src/plugins/inputs/list.js | 2 ++ src/plugins/inputs/mode.js | 2 ++ src/plugins/inputs/msg.js | 2 ++ src/plugins/inputs/nick.js | 2 ++ src/plugins/inputs/notice.js | 2 ++ src/plugins/inputs/part.js | 2 ++ src/plugins/inputs/query.js | 2 ++ src/plugins/inputs/quit.js | 2 ++ src/plugins/inputs/raw.js | 2 ++ src/plugins/inputs/topic.js | 2 ++ src/plugins/irc-events/connection.js | 2 ++ src/plugins/irc-events/ctcp.js | 2 ++ src/plugins/irc-events/error.js | 2 ++ src/plugins/irc-events/invite.js | 2 ++ src/plugins/irc-events/join.js | 2 ++ src/plugins/irc-events/kick.js | 2 ++ src/plugins/irc-events/link.js | 2 ++ src/plugins/irc-events/list.js | 2 ++ src/plugins/irc-events/message.js | 2 ++ src/plugins/irc-events/mode.js | 2 ++ src/plugins/irc-events/motd.js | 2 ++ src/plugins/irc-events/names.js | 2 ++ src/plugins/irc-events/nick.js | 2 ++ src/plugins/irc-events/part.js | 2 ++ src/plugins/irc-events/quit.js | 2 ++ src/plugins/irc-events/topic.js | 2 ++ src/plugins/irc-events/unhandled.js | 2 ++ src/plugins/irc-events/welcome.js | 2 ++ src/plugins/irc-events/whois.js | 2 ++ src/userLog.js | 2 ++ test/fixtures/.lounge/config.js | 2 ++ test/fixtures/env.js | 2 ++ test/plugins/link.js | 2 ++ test/util.js | 2 ++ 70 files changed, 140 insertions(+) diff --git a/.eslintrc.yml b/.eslintrc.yml index 223901ec..354baa99 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -34,6 +34,7 @@ rules: space-before-blocks: 2 space-infix-ops: 2 spaced-comment: [2, always] + strict: 2 globals: log: false diff --git a/client/js/libs/handlebars/diff.js b/client/js/libs/handlebars/diff.js index 21ee523c..8cc0d135 100644 --- a/client/js/libs/handlebars/diff.js +++ b/client/js/libs/handlebars/diff.js @@ -1,3 +1,5 @@ +"use strict"; + var diff; Handlebars.registerHelper( diff --git a/client/js/libs/handlebars/equal.js b/client/js/libs/handlebars/equal.js index 15fdc033..426f54bd 100644 --- a/client/js/libs/handlebars/equal.js +++ b/client/js/libs/handlebars/equal.js @@ -1,3 +1,5 @@ +"use strict"; + Handlebars.registerHelper( "equal", function(a, b, opt) { a = a.toString(); diff --git a/client/js/libs/handlebars/modes.js b/client/js/libs/handlebars/modes.js index f2907846..bf97b033 100644 --- a/client/js/libs/handlebars/modes.js +++ b/client/js/libs/handlebars/modes.js @@ -1,3 +1,5 @@ +"use strict"; + Handlebars.registerHelper( "modes", function(mode) { var modes = { diff --git a/client/js/libs/handlebars/parse.js b/client/js/libs/handlebars/parse.js index 637fb0e7..b09086a6 100644 --- a/client/js/libs/handlebars/parse.js +++ b/client/js/libs/handlebars/parse.js @@ -1,3 +1,5 @@ +"use strict"; + Handlebars.registerHelper( "parse", function(text) { text = Handlebars.Utils.escapeExpression(text); diff --git a/client/js/libs/handlebars/roundBadgeNumber.js b/client/js/libs/handlebars/roundBadgeNumber.js index 1a6ad031..5982acdd 100644 --- a/client/js/libs/handlebars/roundBadgeNumber.js +++ b/client/js/libs/handlebars/roundBadgeNumber.js @@ -1,3 +1,5 @@ +"use strict"; + Handlebars.registerHelper( "roundBadgeNumber", function(count) { if (count < 1000) { diff --git a/client/js/libs/handlebars/tz.js b/client/js/libs/handlebars/tz.js index e38fff94..9b46be61 100644 --- a/client/js/libs/handlebars/tz.js +++ b/client/js/libs/handlebars/tz.js @@ -1,3 +1,5 @@ +"use strict"; + Handlebars.registerHelper( "tz", function(time) { time = new Date(time); diff --git a/client/js/libs/handlebars/users.js b/client/js/libs/handlebars/users.js index 1aa6ac08..977fbf90 100644 --- a/client/js/libs/handlebars/users.js +++ b/client/js/libs/handlebars/users.js @@ -1,3 +1,5 @@ +"use strict"; + Handlebars.registerHelper( "users", function(count) { return count + " " + (count === 1 ? "user" : "users"); diff --git a/client/js/loading-slow-alert.js b/client/js/loading-slow-alert.js index b86b6409..dcca1847 100644 --- a/client/js/loading-slow-alert.js +++ b/client/js/loading-slow-alert.js @@ -1,3 +1,5 @@ +"use strict"; + /* * This is a separate file for two reasons: * 1. CSP policy does not allow inline javascript diff --git a/client/js/lounge.js b/client/js/lounge.js index f6c8c56d..9b0cbaf1 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -1,3 +1,5 @@ +"use strict"; + $(function() { $("#loading-page-message").text("Connecting…"); diff --git a/defaults/config.js b/defaults/config.js index 09cd40b6..f84196e9 100644 --- a/defaults/config.js +++ b/defaults/config.js @@ -1,3 +1,5 @@ +"use strict"; + module.exports = { // // Set the server mode. diff --git a/index.js b/index.js index f78f00e4..f9ed254a 100755 --- a/index.js +++ b/index.js @@ -1,4 +1,7 @@ #!/usr/bin/env node + +"use strict"; + process.chdir(__dirname); // Perform node version check before loading any other files or modules diff --git a/src/client.js b/src/client.js index f8be844a..94730e53 100644 --- a/src/client.js +++ b/src/client.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); var package = require("../package.json"); var Chan = require("./models/chan"); diff --git a/src/clientManager.js b/src/clientManager.js index 4024fbea..1ec9f6ee 100644 --- a/src/clientManager.js +++ b/src/clientManager.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); var fs = require("fs"); var Client = require("./client"); diff --git a/src/command-line/add.js b/src/command-line/add.js index fc5f8265..2e3723e0 100644 --- a/src/command-line/add.js +++ b/src/command-line/add.js @@ -1,3 +1,5 @@ +"use strict"; + var ClientManager = new require("../clientManager"); var bcrypt = require("bcrypt-nodejs"); var program = require("commander"); diff --git a/src/command-line/config.js b/src/command-line/config.js index 9488d507..d5ad2e1b 100644 --- a/src/command-line/config.js +++ b/src/command-line/config.js @@ -1,3 +1,5 @@ +"use strict"; + var program = require("commander"); var child = require("child_process"); var Helper = require("../helper"); diff --git a/src/command-line/edit.js b/src/command-line/edit.js index 7b2ca558..dadd0e52 100644 --- a/src/command-line/edit.js +++ b/src/command-line/edit.js @@ -1,3 +1,5 @@ +"use strict"; + var ClientManager = new require("../clientManager"); var program = require("commander"); var child = require("child_process"); diff --git a/src/command-line/index.js b/src/command-line/index.js index dfc41cd1..5b6ea728 100644 --- a/src/command-line/index.js +++ b/src/command-line/index.js @@ -1,3 +1,5 @@ +"use strict"; + global.log = require("../log.js"); var program = require("commander"); diff --git a/src/command-line/list.js b/src/command-line/list.js index 4e8f8bc3..d8c07189 100644 --- a/src/command-line/list.js +++ b/src/command-line/list.js @@ -1,3 +1,5 @@ +"use strict"; + var ClientManager = new require("../clientManager"); var program = require("commander"); diff --git a/src/command-line/remove.js b/src/command-line/remove.js index 0d3b2ce9..efd9092d 100644 --- a/src/command-line/remove.js +++ b/src/command-line/remove.js @@ -1,3 +1,5 @@ +"use strict"; + var ClientManager = new require("../clientManager"); var program = require("commander"); diff --git a/src/command-line/reset.js b/src/command-line/reset.js index 3c046629..e5cf6609 100644 --- a/src/command-line/reset.js +++ b/src/command-line/reset.js @@ -1,3 +1,5 @@ +"use strict"; + var bcrypt = require("bcrypt-nodejs"); var ClientManager = new require("../clientManager"); var fs = require("fs"); diff --git a/src/command-line/start.js b/src/command-line/start.js index 4386a2fb..a277e319 100644 --- a/src/command-line/start.js +++ b/src/command-line/start.js @@ -1,3 +1,5 @@ +"use strict"; + var ClientManager = new require("../clientManager"); var program = require("commander"); var server = require("../server"); diff --git a/src/helper.js b/src/helper.js index c2a69c83..21e99fcb 100644 --- a/src/helper.js +++ b/src/helper.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); var path = require("path"); var os = require("os"); diff --git a/src/identd.js b/src/identd.js index f598052a..e8928074 100644 --- a/src/identd.js +++ b/src/identd.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); var net = require("net"); diff --git a/src/log.js b/src/log.js index 153f3a3d..483d5345 100644 --- a/src/log.js +++ b/src/log.js @@ -1,3 +1,5 @@ +"use strict"; + var colors = require("colors/safe"); var moment = require("moment"); var Helper = require("./helper"); diff --git a/src/models/chan.js b/src/models/chan.js index 750e798a..0eddf4a1 100644 --- a/src/models/chan.js +++ b/src/models/chan.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); var Helper = require("../helper"); diff --git a/src/models/msg.js b/src/models/msg.js index 73c19ff1..e59d4dd3 100644 --- a/src/models/msg.js +++ b/src/models/msg.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); Msg.Type = { diff --git a/src/models/network.js b/src/models/network.js index cbeea7d4..3f5a3472 100644 --- a/src/models/network.js +++ b/src/models/network.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); var Chan = require("./chan"); diff --git a/src/models/user.js b/src/models/user.js index e5898253..c3a3be1a 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); module.exports = User; diff --git a/src/oidentd.js b/src/oidentd.js index aa3819ad..9412d6cd 100644 --- a/src/oidentd.js +++ b/src/oidentd.js @@ -1,3 +1,5 @@ +"use strict"; + var fs = require("fs"); var Helper = require("./helper"); diff --git a/src/plugins/inputs/action.js b/src/plugins/inputs/action.js index 4f8a4000..4b625854 100644 --- a/src/plugins/inputs/action.js +++ b/src/plugins/inputs/action.js @@ -1,3 +1,5 @@ +"use strict"; + exports.commands = ["slap", "me"]; exports.input = function(network, chan, cmd, args) { diff --git a/src/plugins/inputs/connect.js b/src/plugins/inputs/connect.js index 04296247..885b6112 100644 --- a/src/plugins/inputs/connect.js +++ b/src/plugins/inputs/connect.js @@ -1,3 +1,5 @@ +"use strict"; + var Msg = require("../../models/msg"); exports.commands = ["connect", "server"]; diff --git a/src/plugins/inputs/ctcp.js b/src/plugins/inputs/ctcp.js index 75906b79..0acc6aec 100644 --- a/src/plugins/inputs/ctcp.js +++ b/src/plugins/inputs/ctcp.js @@ -1,3 +1,5 @@ +"use strict"; + exports.commands = ["ctcp"]; exports.input = function(network, chan, cmd, args) { diff --git a/src/plugins/inputs/disconnect.js b/src/plugins/inputs/disconnect.js index a0ad99fb..bd43e40d 100644 --- a/src/plugins/inputs/disconnect.js +++ b/src/plugins/inputs/disconnect.js @@ -1,3 +1,5 @@ +"use strict"; + exports.commands = ["disconnect"]; exports.input = function(network, chan, cmd, args) { diff --git a/src/plugins/inputs/invite.js b/src/plugins/inputs/invite.js index d4543ee3..f7ac7a07 100644 --- a/src/plugins/inputs/invite.js +++ b/src/plugins/inputs/invite.js @@ -1,3 +1,5 @@ +"use strict"; + var Chan = require("../../models/chan"); exports.commands = ["invite"]; diff --git a/src/plugins/inputs/kick.js b/src/plugins/inputs/kick.js index d0b74802..2ce955a1 100644 --- a/src/plugins/inputs/kick.js +++ b/src/plugins/inputs/kick.js @@ -1,3 +1,5 @@ +"use strict"; + exports.commands = ["kick"]; exports.input = function(network, chan, cmd, args) { diff --git a/src/plugins/inputs/list.js b/src/plugins/inputs/list.js index 70a4a65f..79512a48 100644 --- a/src/plugins/inputs/list.js +++ b/src/plugins/inputs/list.js @@ -1,3 +1,5 @@ +"use strict"; + exports.commands = ["list"]; exports.input = function(network, chan, cmd, args) { diff --git a/src/plugins/inputs/mode.js b/src/plugins/inputs/mode.js index b476256d..cc26b80b 100644 --- a/src/plugins/inputs/mode.js +++ b/src/plugins/inputs/mode.js @@ -1,3 +1,5 @@ +"use strict"; + exports.commands = ["mode", "op", "voice", "deop", "devoice"]; exports.input = function(network, chan, cmd, args) { diff --git a/src/plugins/inputs/msg.js b/src/plugins/inputs/msg.js index 379dea61..45dee5f8 100644 --- a/src/plugins/inputs/msg.js +++ b/src/plugins/inputs/msg.js @@ -1,3 +1,5 @@ +"use strict"; + exports.commands = ["msg", "say"]; exports.input = function(network, chan, cmd, args) { diff --git a/src/plugins/inputs/nick.js b/src/plugins/inputs/nick.js index a267b1e4..b53126de 100644 --- a/src/plugins/inputs/nick.js +++ b/src/plugins/inputs/nick.js @@ -1,3 +1,5 @@ +"use strict"; + var Msg = require("../../models/msg"); exports.commands = ["nick"]; diff --git a/src/plugins/inputs/notice.js b/src/plugins/inputs/notice.js index aa20f237..a2c160eb 100644 --- a/src/plugins/inputs/notice.js +++ b/src/plugins/inputs/notice.js @@ -1,3 +1,5 @@ +"use strict"; + exports.commands = ["notice"]; exports.input = function(network, chan, cmd, args) { diff --git a/src/plugins/inputs/part.js b/src/plugins/inputs/part.js index 5a480b3f..6a837271 100644 --- a/src/plugins/inputs/part.js +++ b/src/plugins/inputs/part.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); var Msg = require("../../models/msg"); var Chan = require("../../models/chan"); diff --git a/src/plugins/inputs/query.js b/src/plugins/inputs/query.js index 1aa7f840..2cf23e8a 100644 --- a/src/plugins/inputs/query.js +++ b/src/plugins/inputs/query.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); var Chan = require("../../models/chan"); var Msg = require("../../models/msg"); diff --git a/src/plugins/inputs/quit.js b/src/plugins/inputs/quit.js index 9039c704..be41378a 100644 --- a/src/plugins/inputs/quit.js +++ b/src/plugins/inputs/quit.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); exports.commands = ["quit"]; diff --git a/src/plugins/inputs/raw.js b/src/plugins/inputs/raw.js index b58a993c..b18fbc4d 100644 --- a/src/plugins/inputs/raw.js +++ b/src/plugins/inputs/raw.js @@ -1,3 +1,5 @@ +"use strict"; + exports.commands = ["raw", "send", "quote"]; exports.input = function(network, chan, cmd, args) { diff --git a/src/plugins/inputs/topic.js b/src/plugins/inputs/topic.js index d9e66852..1d403eb3 100644 --- a/src/plugins/inputs/topic.js +++ b/src/plugins/inputs/topic.js @@ -1,3 +1,5 @@ +"use strict"; + exports.commands = ["topic"]; exports.input = function(network, chan, cmd, args) { diff --git a/src/plugins/irc-events/connection.js b/src/plugins/irc-events/connection.js index fe356102..306872e6 100644 --- a/src/plugins/irc-events/connection.js +++ b/src/plugins/irc-events/connection.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); var identd = require("../../identd"); var Msg = require("../../models/msg"); diff --git a/src/plugins/irc-events/ctcp.js b/src/plugins/irc-events/ctcp.js index f853d60b..a4ed7033 100644 --- a/src/plugins/irc-events/ctcp.js +++ b/src/plugins/irc-events/ctcp.js @@ -1,3 +1,5 @@ +"use strict"; + var Msg = require("../../models/msg"); module.exports = function(irc, network) { diff --git a/src/plugins/irc-events/error.js b/src/plugins/irc-events/error.js index 3a92ebb7..477645fb 100644 --- a/src/plugins/irc-events/error.js +++ b/src/plugins/irc-events/error.js @@ -1,3 +1,5 @@ +"use strict"; + var Msg = require("../../models/msg"); module.exports = function(irc, network) { diff --git a/src/plugins/irc-events/invite.js b/src/plugins/irc-events/invite.js index 2d5a6181..13e22c8a 100644 --- a/src/plugins/irc-events/invite.js +++ b/src/plugins/irc-events/invite.js @@ -1,3 +1,5 @@ +"use strict"; + var Msg = require("../../models/msg"); module.exports = function(irc, network) { diff --git a/src/plugins/irc-events/join.js b/src/plugins/irc-events/join.js index 69198904..c7f91ec9 100644 --- a/src/plugins/irc-events/join.js +++ b/src/plugins/irc-events/join.js @@ -1,3 +1,5 @@ +"use strict"; + var Chan = require("../../models/chan"); var Msg = require("../../models/msg"); var User = require("../../models/user"); diff --git a/src/plugins/irc-events/kick.js b/src/plugins/irc-events/kick.js index 16b182ec..a02c0363 100644 --- a/src/plugins/irc-events/kick.js +++ b/src/plugins/irc-events/kick.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); var Msg = require("../../models/msg"); diff --git a/src/plugins/irc-events/link.js b/src/plugins/irc-events/link.js index a9e2399f..0aff8bc4 100644 --- a/src/plugins/irc-events/link.js +++ b/src/plugins/irc-events/link.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); var cheerio = require("cheerio"); var Msg = require("../../models/msg"); diff --git a/src/plugins/irc-events/list.js b/src/plugins/irc-events/list.js index db6fdd3c..0dbf2881 100644 --- a/src/plugins/irc-events/list.js +++ b/src/plugins/irc-events/list.js @@ -1,3 +1,5 @@ +"use strict"; + var Chan = require("../../models/chan"); var Msg = require("../../models/msg"); diff --git a/src/plugins/irc-events/message.js b/src/plugins/irc-events/message.js index cbe36fae..0f345cd7 100644 --- a/src/plugins/irc-events/message.js +++ b/src/plugins/irc-events/message.js @@ -1,3 +1,5 @@ +"use strict"; + var Chan = require("../../models/chan"); var Msg = require("../../models/msg"); diff --git a/src/plugins/irc-events/mode.js b/src/plugins/irc-events/mode.js index 886c42fb..6cadabee 100644 --- a/src/plugins/irc-events/mode.js +++ b/src/plugins/irc-events/mode.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); var Chan = require("../../models/chan"); var Msg = require("../../models/msg"); diff --git a/src/plugins/irc-events/motd.js b/src/plugins/irc-events/motd.js index 0b1f7afd..1075d229 100644 --- a/src/plugins/irc-events/motd.js +++ b/src/plugins/irc-events/motd.js @@ -1,3 +1,5 @@ +"use strict"; + var Msg = require("../../models/msg"); module.exports = function(irc, network) { diff --git a/src/plugins/irc-events/names.js b/src/plugins/irc-events/names.js index 6bee2696..3c16149c 100644 --- a/src/plugins/irc-events/names.js +++ b/src/plugins/irc-events/names.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); var User = require("../../models/user"); diff --git a/src/plugins/irc-events/nick.js b/src/plugins/irc-events/nick.js index c84b1b27..8b3e9b20 100644 --- a/src/plugins/irc-events/nick.js +++ b/src/plugins/irc-events/nick.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); var Msg = require("../../models/msg"); diff --git a/src/plugins/irc-events/part.js b/src/plugins/irc-events/part.js index 706de66f..6711aced 100644 --- a/src/plugins/irc-events/part.js +++ b/src/plugins/irc-events/part.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); var Msg = require("../../models/msg"); diff --git a/src/plugins/irc-events/quit.js b/src/plugins/irc-events/quit.js index f3e473f6..24104852 100644 --- a/src/plugins/irc-events/quit.js +++ b/src/plugins/irc-events/quit.js @@ -1,3 +1,5 @@ +"use strict"; + var _ = require("lodash"); var Msg = require("../../models/msg"); diff --git a/src/plugins/irc-events/topic.js b/src/plugins/irc-events/topic.js index 286325f7..0ee6ba6e 100644 --- a/src/plugins/irc-events/topic.js +++ b/src/plugins/irc-events/topic.js @@ -1,3 +1,5 @@ +"use strict"; + var Msg = require("../../models/msg"); module.exports = function(irc, network) { diff --git a/src/plugins/irc-events/unhandled.js b/src/plugins/irc-events/unhandled.js index c089d0cf..46b98f22 100644 --- a/src/plugins/irc-events/unhandled.js +++ b/src/plugins/irc-events/unhandled.js @@ -1,3 +1,5 @@ +"use strict"; + var Msg = require("../../models/msg"); module.exports = function(irc, network) { diff --git a/src/plugins/irc-events/welcome.js b/src/plugins/irc-events/welcome.js index 5d86e686..311d5667 100644 --- a/src/plugins/irc-events/welcome.js +++ b/src/plugins/irc-events/welcome.js @@ -1,3 +1,5 @@ +"use strict"; + var Msg = require("../../models/msg"); module.exports = function(irc, network) { diff --git a/src/plugins/irc-events/whois.js b/src/plugins/irc-events/whois.js index 6b531a35..86cdcaa9 100644 --- a/src/plugins/irc-events/whois.js +++ b/src/plugins/irc-events/whois.js @@ -1,3 +1,5 @@ +"use strict"; + var Chan = require("../../models/chan"); var Msg = require("../../models/msg"); diff --git a/src/userLog.js b/src/userLog.js index 1e10f4d4..e35df410 100644 --- a/src/userLog.js +++ b/src/userLog.js @@ -1,3 +1,5 @@ +"use strict"; + var fs = require("fs"); var fsextra = require("fs-extra"); var moment = require("moment"); diff --git a/test/fixtures/.lounge/config.js b/test/fixtures/.lounge/config.js index 3f900589..7e5efe76 100644 --- a/test/fixtures/.lounge/config.js +++ b/test/fixtures/.lounge/config.js @@ -1,3 +1,5 @@ +"use strict"; + var config = require("../../../defaults/config.js"); config.prefetch = true; diff --git a/test/fixtures/env.js b/test/fixtures/env.js index 08aafaf0..f3c51b0d 100644 --- a/test/fixtures/env.js +++ b/test/fixtures/env.js @@ -1,2 +1,4 @@ +"use strict"; + var home = require("path").join(__dirname, ".lounge"); require("../../src/helper").setHome(home); diff --git a/test/plugins/link.js b/test/plugins/link.js index 278a2713..5267dd58 100644 --- a/test/plugins/link.js +++ b/test/plugins/link.js @@ -1,3 +1,5 @@ +"use strict"; + var assert = require("assert"); var util = require("../util"); diff --git a/test/util.js b/test/util.js index 58edddd7..0b14f3f1 100644 --- a/test/util.js +++ b/test/util.js @@ -1,3 +1,5 @@ +"use strict"; + var EventEmitter = require("events").EventEmitter; var util = require("util"); var _ = require("lodash"); From e416d74f571634252f7c4d8a3c6bdde0fcab79a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sun, 9 Oct 2016 15:15:20 -0400 Subject: [PATCH 0076/3926] Rename package variable, reserved in strict mode This has been renamed similarly in other files of the projects already. --- src/client.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client.js b/src/client.js index 94730e53..6dc06c04 100644 --- a/src/client.js +++ b/src/client.js @@ -1,7 +1,7 @@ "use strict"; var _ = require("lodash"); -var package = require("../package.json"); +var pkg = require("../package.json"); var Chan = require("./models/chan"); var crypto = require("crypto"); var userLog = require("./userLog"); @@ -230,7 +230,7 @@ Client.prototype.connect = function(args) { } else { webirc = { password: config.webirc[network.host], - username: package.name, + username: pkg.name, address: args.ip, hostname: args.hostname }; @@ -257,7 +257,7 @@ Client.prototype.connect = function(args) { }); network.irc.connect({ - version: package.name + " " + package.version + " -- " + package.homepage, + version: pkg.name + " " + pkg.version + " -- " + pkg.homepage, host: network.host, port: network.port, nick: nick, From b28bba6dd4370cb00f980fb764542355cb38dc1f Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sun, 9 Oct 2016 12:05:42 +0300 Subject: [PATCH 0077/3926] Remove svg and ttf font formats --- client/css/fonts/Lato-700/Lato-700.eot | Bin 35184 -> 0 bytes client/css/fonts/Lato-700/Lato-700.svg | 4457 ----------------- client/css/fonts/Lato-700/Lato-700.ttf | Bin 82368 -> 0 bytes .../css/fonts/Lato-regular/Lato-regular.eot | Bin 34943 -> 0 bytes .../css/fonts/Lato-regular/Lato-regular.svg | 4148 --------------- .../css/fonts/Lato-regular/Lato-regular.ttf | Bin 81980 -> 0 bytes client/css/fonts/inconsolatag.ttf | Bin 33680 -> 0 bytes client/css/style.css | 17 +- client/themes/crypto.css | 2 +- scripts/build-fontawesome.js | 3 - 10 files changed, 6 insertions(+), 8621 deletions(-) delete mode 100755 client/css/fonts/Lato-700/Lato-700.eot delete mode 100755 client/css/fonts/Lato-700/Lato-700.svg delete mode 100755 client/css/fonts/Lato-700/Lato-700.ttf delete mode 100755 client/css/fonts/Lato-regular/Lato-regular.eot delete mode 100755 client/css/fonts/Lato-regular/Lato-regular.svg delete mode 100755 client/css/fonts/Lato-regular/Lato-regular.ttf delete mode 100644 client/css/fonts/inconsolatag.ttf diff --git a/client/css/fonts/Lato-700/Lato-700.eot b/client/css/fonts/Lato-700/Lato-700.eot deleted file mode 100755 index 30b5dffec482620689c539da804ec365c22c70d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35184 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

l*VGR|FAx@tzNU_l0Q>ntRUbnzQ$< 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 MR>BSyvjtUxI5f*nJpcdz diff --git a/client/css/fonts/Lato-700/Lato-700.svg b/client/css/fonts/Lato-700/Lato-700.svg deleted file mode 100755 index bac8d6da..00000000 --- a/client/css/fonts/Lato-700/Lato-700.svg +++ /dev/null @@ -1,4457 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/css/fonts/Lato-700/Lato-700.ttf b/client/css/fonts/Lato-700/Lato-700.ttf deleted file mode 100755 index e8b9bf6a20e1f67fd0e8b42b25e3f0e627babe39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82368 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 WwHG^1yNvPL2mL_!h-%}4hyNEi*y0iZ diff --git a/client/css/fonts/Lato-regular/Lato-regular.eot b/client/css/fonts/Lato-regular/Lato-regular.eot deleted file mode 100755 index 28343da023e3e010ce54aabdac6da04570fe0c8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34943 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 cYqen+a diff --git a/client/css/fonts/Lato-regular/Lato-regular.svg b/client/css/fonts/Lato-regular/Lato-regular.svg deleted file mode 100755 index f7678d37..00000000 --- a/client/css/fonts/Lato-regular/Lato-regular.svg +++ /dev/null @@ -1,4148 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/css/fonts/Lato-regular/Lato-regular.ttf b/client/css/fonts/Lato-regular/Lato-regular.ttf deleted file mode 100755 index 7608bc3e0fd03fc3cf3a41501ed9c1b49b773ac4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81980 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 diff --git a/client/css/fonts/inconsolatag.ttf b/client/css/fonts/inconsolatag.ttf deleted file mode 100644 index 1a212c65f1a8cfb4424aaf0024899e725a123e1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33680 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 Date: Sun, 9 Oct 2016 11:54:44 +0300 Subject: [PATCH 0078/3926] Enforce more eslint rules --- .eslintrc.yml | 17 +++- client/js/libs/handlebars/diff.js | 4 +- client/js/libs/handlebars/equal.js | 4 +- client/js/libs/handlebars/parse.js | 8 +- client/js/lounge.js | 153 ++++++++++++++--------------- scripts/build-fontawesome.js | 10 +- src/client.js | 14 ++- src/clientManager.js | 3 +- src/command-line/start.js | 10 +- src/models/chan.js | 4 +- src/oidentd.js | 2 +- src/plugins/inputs/mode.js | 8 +- src/plugins/irc-events/link.js | 3 +- src/plugins/irc-events/message.js | 7 +- src/plugins/irc-events/nick.js | 5 +- src/server.js | 10 +- src/userLog.js | 3 +- test/models/network.js | 4 +- 18 files changed, 144 insertions(+), 125 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 354baa99..de36b033 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -11,27 +11,34 @@ parserOptions: ecmaVersion: 6 rules: + block-scoped-var: 2 block-spacing: [2, always] brace-style: [2, 1tbs] comma-dangle: 0 curly: [2, all] + dot-notation: 2 eqeqeq: 2 + handle-callback-err: 2 indent: [2, tab] key-spacing: [2, {beforeColon: false, afterColon: true}] keyword-spacing: [2, {before: true, after: true}] linebreak-style: [2, unix] no-console: 0 no-control-regex: 0 - no-inner-declarations: 2 - no-invalid-regexp: 2 - no-irregular-whitespace: 2 + no-else-return: 2 + no-implicit-globals: 2 + no-multi-spaces: 2 + no-shadow: 2 + no-template-curly-in-string: 2 no-trailing-spaces: 2 - no-unexpected-multiline: 2 - no-unreachable: 2 + no-unsafe-negation: 2 + no-useless-escape: 2 object-curly-spacing: [2, never] + quote-props: [2, as-needed] quotes: [2, double, avoid-escape] semi: [2, always] space-before-blocks: 2 + space-before-function-paren: [2, never] space-infix-ops: 2 spaced-comment: [2, always] strict: 2 diff --git a/client/js/libs/handlebars/diff.js b/client/js/libs/handlebars/diff.js index 8cc0d135..17f366ab 100644 --- a/client/js/libs/handlebars/diff.js +++ b/client/js/libs/handlebars/diff.js @@ -7,8 +7,8 @@ Handlebars.registerHelper( if (a !== diff) { diff = a; return opt.fn(this); - } else { - return opt.inverse(this); } + + return opt.inverse(this); } ); diff --git a/client/js/libs/handlebars/equal.js b/client/js/libs/handlebars/equal.js index 426f54bd..4d8f34ef 100644 --- a/client/js/libs/handlebars/equal.js +++ b/client/js/libs/handlebars/equal.js @@ -6,8 +6,8 @@ Handlebars.registerHelper( b = b.toString(); if (a === b) { return opt.fn(this); - } else { - return opt.inverse(this); } + + return opt.inverse(this); } ); diff --git a/client/js/libs/handlebars/parse.js b/client/js/libs/handlebars/parse.js index b09086a6..71a45820 100644 --- a/client/js/libs/handlebars/parse.js +++ b/client/js/libs/handlebars/parse.js @@ -33,7 +33,7 @@ function uri(text) { */ function channels(text) { return text.replace( - /(^|\s|\x07|,)((?:#|&)[^\x07\s\,]{1,49})/g, + /(^|\s|\x07|,)((?:#|&)[^\x07\s,]{1,49})/g, '$1$2' ); } @@ -114,10 +114,10 @@ function colors(line) { return; } - result = result.replace(style.keyregex, function(match, text) { + result = result.replace(style.keyregex, function(matchedTrash, matchedText) { return styleTemplate({ - "style": style.style, - "text": text + style: style.style, + text: matchedText }); }); }); diff --git a/client/js/lounge.js b/client/js/lounge.js index 9b0cbaf1..34e55817 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -88,6 +88,7 @@ $(function() { socket.on("auth", function(data) { var login = $("#sign-in"); + var token; login.find(".btn").prop("disabled", false); @@ -99,7 +100,7 @@ $(function() { error.hide(); }); } else { - var token = window.localStorage.getItem("token"); + token = window.localStorage.getItem("token"); if (token) { $("#loading-page-message").text("Authorizing…"); socket.emit("auth", {token: token}); @@ -476,7 +477,7 @@ $(function() { socket.on("names", renderChannelUsers); var userStyles = $("#user-specified-css"); - var settings = $("#settings"); + var highlights = []; var options = $.extend({ coloredNicks: true, desktopNotifications: false, @@ -494,73 +495,75 @@ $(function() { userStyles: userStyles.text(), }, JSON.parse(window.localStorage.getItem("settings"))); - for (var i in options) { - if (i === "userStyles") { - if (!/[\?&]nocss/.test(window.location.search)) { - $(document.head).find("#user-specified-css").html(options[i]); - } - settings.find("#user-specified-css-input").val(options[i]); - } else if (i === "highlights") { - settings.find("input[name=" + i + "]").val(options[i]); - } else if (i === "theme") { - $("#theme").attr("href", "themes/" + options[i] + ".css"); - settings.find("select[name=" + i + "]").val(options[i]); - } else if (options[i]) { - settings.find("input[name=" + i + "]").prop("checked", true); - } - } + (function SettingsScope() { + var settings = $("#settings"); - var highlights = []; - - settings.on("change", "input, select, textarea", function() { - var self = $(this); - var name = self.attr("name"); - - if (self.attr("type") === "checkbox") { - options[name] = self.prop("checked"); - } else { - options[name] = self.val(); - } - - setLocalStorageItem("settings", JSON.stringify(options)); - - if ([ - "join", - "mode", - "motd", - "nick", - "part", - "quit", - "notifyAllMessages", - ].indexOf(name) !== -1) { - chat.toggleClass("hide-" + name, !self.prop("checked")); - } else if (name === "coloredNicks") { - chat.toggleClass("colored-nicks", self.prop("checked")); - } else if (name === "theme") { - $("#theme").attr("href", "themes/" + options[name] + ".css"); - } else if (name === "userStyles") { - $(document.head).find("#user-specified-css").html(options[name]); - } else if (name === "highlights") { - var highlightString = options[name]; - highlights = highlightString.split(",").map(function(h) { - return h.trim(); - }).filter(function(h) { - // Ensure we don't have empty string in the list of highlights - // otherwise, users get notifications for everything - return h !== ""; - }); - } - }).find("input") - .trigger("change"); - - $("#desktopNotifications").on("change", function() { - var self = $(this); - if (self.prop("checked")) { - if (Notification.permission !== "granted") { - Notification.requestPermission(updateDesktopNotificationStatus); + for (var i in options) { + if (i === "userStyles") { + if (!/[\?&]nocss/.test(window.location.search)) { + $(document.head).find("#user-specified-css").html(options[i]); + } + settings.find("#user-specified-css-input").val(options[i]); + } else if (i === "highlights") { + settings.find("input[name=" + i + "]").val(options[i]); + } else if (i === "theme") { + $("#theme").attr("href", "themes/" + options[i] + ".css"); + settings.find("select[name=" + i + "]").val(options[i]); + } else if (options[i]) { + settings.find("input[name=" + i + "]").prop("checked", true); } } - }); + + settings.on("change", "input, select, textarea", function() { + var self = $(this); + var name = self.attr("name"); + + if (self.attr("type") === "checkbox") { + options[name] = self.prop("checked"); + } else { + options[name] = self.val(); + } + + setLocalStorageItem("settings", JSON.stringify(options)); + + if ([ + "join", + "mode", + "motd", + "nick", + "part", + "quit", + "notifyAllMessages", + ].indexOf(name) !== -1) { + chat.toggleClass("hide-" + name, !self.prop("checked")); + } else if (name === "coloredNicks") { + chat.toggleClass("colored-nicks", self.prop("checked")); + } else if (name === "theme") { + $("#theme").attr("href", "themes/" + options[name] + ".css"); + } else if (name === "userStyles") { + userStyles.html(options[name]); + } else if (name === "highlights") { + var highlightString = options[name]; + highlights = highlightString.split(",").map(function(h) { + return h.trim(); + }).filter(function(h) { + // Ensure we don't have empty string in the list of highlights + // otherwise, users get notifications for everything + return h !== ""; + }); + } + }).find("input") + .trigger("change"); + + $("#desktopNotifications").on("change", function() { + var self = $(this); + if (self.prop("checked")) { + if (Notification.permission !== "granted") { + Notification.requestPermission(updateDesktopNotificationStatus); + } + } + }); + }()); var viewport = $("#viewport"); var sidebarSlide = window.slideoutMenu(viewport[0], sidebar[0]); @@ -676,9 +679,7 @@ $(function() { }) .tab(complete, {hint: false}); - var form = $("#form"); - - form.on("submit", function(e) { + $("#form").on("submit", function(e) { e.preventDefault(); var text = input.val(); @@ -804,7 +805,7 @@ $(function() { var text = ""; if (window.getSelection) { text = window.getSelection().toString(); - } else if (document.selection && document.selection.type !== "Control") { + } else if (document.selection && document.selection.type !== "Control") { text = document.selection.createRange().text; } if (!text) { @@ -1027,20 +1028,20 @@ $(function() { chat.on("click", ".toggle-button", function() { var self = $(this); - var chat = self.closest(".chat"); - var bottom = chat.isScrollBottom(); + var localChat = self.closest(".chat"); + var bottom = localChat.isScrollBottom(); var content = self.parent().next(".toggle-content"); if (bottom && !content.hasClass("show")) { var img = content.find("img"); if (img.length !== 0 && !img.width()) { img.on("load", function() { - chat.scrollBottom(); + localChat.scrollBottom(); }); } } content.toggleClass("show"); if (bottom) { - chat.scrollBottom(); + localChat.scrollBottom(); } }); @@ -1048,9 +1049,7 @@ $(function() { var forms = $("#sign-in, #connect, #change-password"); windows.on("show", "#sign-in", function() { - var self = $(this); - var inputs = self.find("input"); - inputs.each(function() { + $(this).find("input").each(function() { var self = $(this); if (self.val() === "") { self.focus(); diff --git a/scripts/build-fontawesome.js b/scripts/build-fontawesome.js index b7ce06f9..6c55863a 100644 --- a/scripts/build-fontawesome.js +++ b/scripts/build-fontawesome.js @@ -9,13 +9,13 @@ var fonts = [ "fontawesome-webfont.woff2" ]; -fs.ensureDir(destDir, function (err) { - if (err) { - console.error(err); +fs.ensureDir(destDir, function(dirErr) { + if (dirErr) { + console.error(dirErr); } - fonts.forEach(function (font) { - fs.copy(srcDir + font, destDir + font, function (err) { + fonts.forEach(function(font) { + fs.copy(srcDir + font, destDir + font, function(err) { if (err) { console.error(err); } else { diff --git a/src/client.js b/src/client.js index 6dc06c04..e18ed33a 100644 --- a/src/client.js +++ b/src/client.js @@ -118,12 +118,12 @@ Client.prototype.emit = function(event, data) { } }; -Client.prototype.find = function(id) { +Client.prototype.find = function(channelId) { var network = null; var chan = null; for (var i in this.networks) { var n = this.networks[i]; - chan = _.find(n.channels, {id: id}); + chan = _.find(n.channels, {id: channelId}); if (chan) { network = n; break; @@ -134,9 +134,9 @@ Client.prototype.find = function(id) { network: network, chan: chan }; - } else { - return false; } + + return false; }; Client.prototype.connect = function(args) { @@ -168,7 +168,7 @@ Client.prototype.connect = function(args) { // also used by the "connect" window } else if (args.join) { channels = args.join - .replace(/\,/g, " ") + .replace(/,/g, " ") .split(/\s+/g) .map(function(chan) { return new Chan({ @@ -278,6 +278,10 @@ Client.prototype.updateToken = function(callback) { var client = this; crypto.randomBytes(48, function(err, buf) { + if (err) { + throw err; + } + callback(client.config.token = buf.toString("hex")); }); }; diff --git a/src/clientManager.js b/src/clientManager.js index 1ec9f6ee..56774b63 100644 --- a/src/clientManager.js +++ b/src/clientManager.js @@ -34,8 +34,9 @@ ClientManager.prototype.loadUsers = function() { }; ClientManager.prototype.loadUser = function(name) { + let json; try { - var json = this.readUserConfig(name); + json = this.readUserConfig(name); } catch (e) { log.error("Failed to read user config", e); return; diff --git a/src/command-line/start.js b/src/command-line/start.js index a277e319..7bd40a79 100644 --- a/src/command-line/start.js +++ b/src/command-line/start.js @@ -6,11 +6,11 @@ var server = require("../server"); var Helper = require("../helper"); program - .option("-H, --host " , "host") - .option("-P, --port " , "port") - .option("-B, --bind " , "bind") - .option(" --public" , "mode") - .option(" --private" , "mode") + .option("-H, --host ", "host") + .option("-P, --port ", "port") + .option("-B, --bind ", "bind") + .option(" --public", "mode") + .option(" --private", "mode") .command("start") .description("Start the server") .action(function() { diff --git a/src/models/chan.js b/src/models/chan.js index 0eddf4a1..6a71eda4 100644 --- a/src/models/chan.js +++ b/src/models/chan.js @@ -78,9 +78,9 @@ Chan.prototype.getMode = function(name) { var user = _.find(this.users, {name: name}); if (user) { return user.mode; - } else { - return ""; } + + return ""; }; Chan.prototype.toJSON = function() { diff --git a/src/oidentd.js b/src/oidentd.js index 9412d6cd..80c42161 100644 --- a/src/oidentd.js +++ b/src/oidentd.js @@ -53,7 +53,7 @@ OidentdFile.prototype = { var file = "# Warning: file generated by The Lounge: changes will be overwritten!\n"; function makeRule(connection) { - return "to " + connection.socket.remoteAddress + return "to " + connection.socket.remoteAddress + " lport " + connection.socket.localPort + " from " + connection.socket.localAddress + " fport " + connection.socket.remotePort diff --git a/src/plugins/inputs/mode.js b/src/plugins/inputs/mode.js index cc26b80b..37bc490c 100644 --- a/src/plugins/inputs/mode.js +++ b/src/plugins/inputs/mode.js @@ -12,10 +12,10 @@ exports.input = function(network, chan, cmd, args) { if (cmd !== "mode") { user = args[0]; mode = { - "op": "+o", - "voice": "+v", - "deop": "-o", - "devoice": "-v" + op: "+o", + voice: "+v", + deop: "-o", + devoice: "-v" }[cmd]; } else if (args.length === 1) { return true; diff --git a/src/plugins/irc-events/link.js b/src/plugins/irc-events/link.js index 0aff8bc4..76cbfcba 100644 --- a/src/plugins/irc-events/link.js +++ b/src/plugins/irc-events/link.js @@ -90,8 +90,9 @@ function parse(msg, url, res, client) { } function fetch(url, cb) { + let req; try { - var req = request.get({ + req = request.get({ url: url, maxRedirects: 5, timeout: 5000, diff --git a/src/plugins/irc-events/message.js b/src/plugins/irc-events/message.js index 0f345cd7..558964e6 100644 --- a/src/plugins/irc-events/message.js +++ b/src/plugins/irc-events/message.js @@ -34,8 +34,9 @@ module.exports = function(irc, network) { }); function handleMessage(data) { - var highlight = false; - var self = data.nick === irc.user.nick; + let chan; + let highlight = false; + const self = data.nick === irc.user.nick; // Server messages go to server window, no questions asked if (data.from_server) { @@ -48,7 +49,7 @@ module.exports = function(irc, network) { target = data.nick; } - var chan = network.getChannel(target); + chan = network.getChannel(target); if (typeof chan === "undefined") { // Send notices that are not targeted at us into the server window if (data.type === Msg.Type.NOTICE) { diff --git a/src/plugins/irc-events/nick.js b/src/plugins/irc-events/nick.js index 8b3e9b20..51929f4c 100644 --- a/src/plugins/irc-events/nick.js +++ b/src/plugins/irc-events/nick.js @@ -6,12 +6,13 @@ var Msg = require("../../models/msg"); module.exports = function(irc, network) { var client = this; irc.on("nick", function(data) { + let msg; var self = false; if (data.nick === irc.user.nick) { network.setNick(data.new_nick); var lobby = network.channels[0]; - var msg = new Msg({ + msg = new Msg({ text: "You're now known as " + data.new_nick, }); lobby.pushMessage(client, msg); @@ -33,7 +34,7 @@ module.exports = function(irc, network) { client.emit("users", { chan: chan.id }); - var msg = new Msg({ + msg = new Msg({ time: data.time, type: Msg.Type.NICK, mode: chan.getMode(data.new_nick), diff --git a/src/server.js b/src/server.js index 80598fd0..c95102b2 100644 --- a/src/server.js +++ b/src/server.js @@ -127,13 +127,17 @@ function index(req, res, next) { } return fs.readFile("client/index.html", "utf-8", function(err, file) { + if (err) { + throw err; + } + var data = _.merge( pkg, Helper.config ); data.gitCommit = gitCommit; - data.themes = fs.readdirSync("client/themes/").filter(function(file) { - return file.endsWith(".css"); + data.themes = fs.readdirSync("client/themes/").filter(function(themeFile) { + return themeFile.endsWith(".css"); }).map(function(css) { return css.slice(0, -4); }); @@ -317,7 +321,7 @@ function auth(data) { manager.loadUser(data.user); client = manager.findClient(data.user); } - if (Helper.config.webirc !== null && !client.config["ip"]) { + if (Helper.config.webirc !== null && !client.config.ip) { reverseDnsLookup(socket, client); } else { init(socket, client, token); diff --git a/src/userLog.js b/src/userLog.js index e35df410..d8ce8a8b 100644 --- a/src/userLog.js +++ b/src/userLog.js @@ -6,8 +6,9 @@ var moment = require("moment"); var Helper = require("./helper"); module.exports.write = function(user, network, chan, msg) { + const path = Helper.getUserLogsPath(user, network); + try { - var path = Helper.getUserLogsPath(user, network); fsextra.ensureDirSync(path); } catch (e) { log.error("Unabled to create logs directory", e); diff --git a/test/models/network.js b/test/models/network.js index 04314b3d..65ae67ac 100644 --- a/test/models/network.js +++ b/test/models/network.js @@ -29,8 +29,8 @@ describe("Network", function() { ip: null, hostname: null, channels: [ - {"name": "#thelounge"}, - {"name": "&foobar"}, + {name: "#thelounge"}, + {name: "&foobar"}, ] }); }); From 310ab8f43c119746dd9f96faa1aaa3f6593456b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Mon, 10 Oct 2016 13:08:23 -0400 Subject: [PATCH 0079/3926] Fix nick changes not being properly reported in the logs Before: ``` [2016-10-10 15:17:47] * nick ``` After: ``` [2016-10-10 16:32:47] * astorije nick astorije2 ``` --- client/views/actions/nick.tpl | 2 +- src/plugins/irc-events/nick.js | 2 +- src/userLog.js | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/client/views/actions/nick.tpl b/client/views/actions/nick.tpl index 73c790f4..1c93cb97 100644 --- a/client/views/actions/nick.tpl +++ b/client/views/actions/nick.tpl @@ -1,3 +1,3 @@ -{{mode}}{{nick}} +{{mode}}{{from}} is now known as {{mode}}{{new_nick}} diff --git a/src/plugins/irc-events/nick.js b/src/plugins/irc-events/nick.js index 51929f4c..a1bfc3b7 100644 --- a/src/plugins/irc-events/nick.js +++ b/src/plugins/irc-events/nick.js @@ -36,9 +36,9 @@ module.exports = function(irc, network) { }); msg = new Msg({ time: data.time, + from: data.nick, type: Msg.Type.NICK, mode: chan.getMode(data.new_nick), - nick: data.nick, new_nick: data.new_nick, self: self }); diff --git a/src/userLog.js b/src/userLog.js index d8ce8a8b..4b3020f8 100644 --- a/src/userLog.js +++ b/src/userLog.js @@ -37,7 +37,9 @@ module.exports.write = function(user, network, chan, msg) { line += msg.type; - if (msg.text) { + if (msg.new_nick) { // `/nick ` + line += ` ${msg.new_nick}`; + } else if (msg.text) { line += ` ${msg.text}`; } } From 0d058a5ef76ba3533029415b7966bba30961ce4b Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Wed, 5 Oct 2016 14:30:17 -0700 Subject: [PATCH 0080/3926] Begin work on overriding defaults in URL --- client/js/lounge.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/client/js/lounge.js b/client/js/lounge.js index cca024b9..5a10f9e0 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -993,7 +993,31 @@ $(function() { } }); }); + if ($("body").hasClass("public")) { + $("#connect").one("show", function() { + var params = window.URI(document.location.search); + params = params.search(true); + // Possible parameters: name, host, port, password, tls, nick, username, realname, join + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#Iterating_over_own_properties_only + for (var key in params) { + if (params.hasOwnProperty(key)) { + var value = params[key]; + // \W searches for non-word characters + key = key.replace(/\W/g, ""); + var element = $("#connect input[name='" + key + "']"); + // if the element exists, it isn't disabled, and it isn't hidden + if (element.length > 0 && !element.is(":disabled") && !element.is(":hidden")) { + if (element.is(":checkbox")) { + element.prop("checked", (value === "1" || value === "true") ? true : false); + } else { + element.val(value); + } + } + } + } + }); + } windows.on("show", "#settings", updateDesktopNotificationStatus); forms.on("submit", "form", function(e) { From 99218341ec2f7be28971d9ecca464ddf9d03fd72 Mon Sep 17 00:00:00 2001 From: William Boman Date: Mon, 5 Sep 2016 14:37:27 +0200 Subject: [PATCH 0081/3926] consolidate version numbers throughout all interfaces --- src/client.js | 2 +- src/command-line/index.js | 3 +-- src/helper.js | 26 ++++++++++++++++++++++++++ src/server.js | 19 +++++-------------- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/client.js b/src/client.js index e18ed33a..02d15f16 100644 --- a/src/client.js +++ b/src/client.js @@ -257,7 +257,7 @@ Client.prototype.connect = function(args) { }); network.irc.connect({ - version: pkg.name + " " + pkg.version + " -- " + pkg.homepage, + version: pkg.name + " " + Helper.getVersion() + " -- " + pkg.homepage, host: network.host, port: network.port, nick: nick, diff --git a/src/command-line/index.js b/src/command-line/index.js index 5b6ea728..6df07f09 100644 --- a/src/command-line/index.js +++ b/src/command-line/index.js @@ -3,13 +3,12 @@ global.log = require("../log.js"); var program = require("commander"); -var pkg = require("../../package.json"); var fs = require("fs"); var fsextra = require("fs-extra"); var path = require("path"); var Helper = require("../helper"); -program.version(pkg.version, "-v, --version"); +program.version(Helper.getVersion(), "-v, --version"); program.option(""); program.option(" --home " , "home path"); diff --git a/src/helper.js b/src/helper.js index 21e99fcb..3db79ca1 100644 --- a/src/helper.js +++ b/src/helper.js @@ -1,5 +1,6 @@ "use strict"; +const pkg = require("../package.json"); var _ = require("lodash"); var path = require("path"); var os = require("os"); @@ -11,6 +12,8 @@ var Helper = { getUserConfigPath: getUserConfigPath, getUserLogsPath: getUserLogsPath, setHome: setHome, + getVersion: getVersion, + getGitCommit: getGitCommit, }; module.exports = Helper; @@ -22,6 +25,29 @@ Helper.config = require(path.resolve(path.join( "config.js" ))); +function getVersion() { + const gitCommit = getGitCommit(); + return gitCommit ? `source (${gitCommit})` : `v${pkg.version}`; +} + +let _gitCommit; +function getGitCommit() { + if (_gitCommit !== undefined) { + return _gitCommit; + } + try { + _gitCommit = require("child_process") + .execSync("git rev-parse --short HEAD 2> /dev/null") // Returns hash of current commit + .toString() + .trim(); + return _gitCommit; + } catch (e) { + // Not a git repository or git is not installed + _gitCommit = null; + return null; + } +} + function setHome(homePath) { this.HOME = expandHome(homePath || "~/.lounge"); this.CONFIG_PATH = path.join(this.HOME, "config.js"); diff --git a/src/server.js b/src/server.js index c95102b2..2ce7be08 100644 --- a/src/server.js +++ b/src/server.js @@ -81,8 +81,10 @@ module.exports = function() { manager.sockets = sockets; - var protocol = config.https.enable ? "https" : "http"; - log.info("The Lounge v" + pkg.version + " is now running on", protocol + "://" + (config.host || "*") + ":" + config.port + "/", (config.public ? "in public mode" : "in private mode")); + let protocol = config.https.enable ? "https" : "http"; + let host = config.host || "*"; + log.info("The Lounge", Helper.getVersion(), "is now running"); + log.info(`Available on: ${protocol}://${host}:${config.port}/ in ${config.public ? "public" : "private"} mode`); log.info("Press ctrl-c to stop\n"); if (!config.public) { @@ -110,17 +112,6 @@ function allRequests(req, res, next) { return next(); } -// Information to populate the About section in UI, either from npm or from git -var gitCommit = null; -try { - gitCommit = require("child_process") - .execSync("git rev-parse --short HEAD 2> /dev/null") // Returns hash of current commit - .toString() - .trim(); -} catch (e) { - // Not a git repository or git is not installed: treat it as npm release -} - function index(req, res, next) { if (req.url.split("?")[0] !== "/") { return next(); @@ -135,7 +126,7 @@ function index(req, res, next) { pkg, Helper.config ); - data.gitCommit = gitCommit; + data.gitCommit = Helper.getGitCommit(); data.themes = fs.readdirSync("client/themes/").filter(function(themeFile) { return themeFile.endsWith(".css"); }).map(function(css) { From aabdf562a65d04814b7c2c184607200d77bef143 Mon Sep 17 00:00:00 2001 From: William Boman Date: Sun, 10 Jul 2016 12:36:08 +0200 Subject: [PATCH 0082/3926] client: re-focus input on chat form submit --- client/js/lounge.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/js/lounge.js b/client/js/lounge.js index 34e55817..a5f0879a 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -681,6 +681,7 @@ $(function() { $("#form").on("submit", function(e) { e.preventDefault(); + focus(); var text = input.val(); if (text.length === 0) { From 6dac7b1897f135ca0aeae3b54ca974f8a497b19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 11 Oct 2016 22:27:57 -0400 Subject: [PATCH 0083/3926] Use CI caches for downloaded files instead of installed ones This allows for a more meaningful build: if a newer version of a sub-package breaks, builds would still pass as it uses the cached version. This uses a cache for downloaded packages instead. I am expecting this to slow down a little bit the builds (but we are OK overall) but be more accurate in practice. See https://docs.npmjs.com/cli/cache#configuration and https://docs.npmjs.com/files/folders#node-modules. --- .travis.yml | 2 +- appveyor.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6c8bce0f..3cad9f94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ matrix: cache: directories: - - node_modules + - ~/.npm notifications: email: diff --git a/appveyor.yml b/appveyor.yml index 4e0b2fc8..e63ba59a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,7 +23,7 @@ test_script: # cache npm modules cache: - - node_modules + - '%AppData%/npm-cache' # Don't actually build build: off From b7814bc571ffee696f7816a83736ddba62006a20 Mon Sep 17 00:00:00 2001 From: Jeremy Apthorp Date: Tue, 27 Sep 2016 10:33:28 -0700 Subject: [PATCH 0084/3926] Fill in prefixLookup on network initialization Fixes #644. --- src/plugins/irc-events/connection.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/plugins/irc-events/connection.js b/src/plugins/irc-events/connection.js index 306872e6..0c5ab0f9 100644 --- a/src/plugins/irc-events/connection.js +++ b/src/plugins/irc-events/connection.js @@ -48,6 +48,11 @@ module.exports = function(irc, network) { }); irc.on("socket connected", function() { + network.prefixLookup = {}; + irc.network.options.PREFIX.forEach(function(mode) { + network.prefixLookup[mode.mode] = mode.symbol; + }); + network.channels[0].pushMessage(client, new Msg({ text: "Connected to the network." })); From 4f5bb559512b0430800d54e40746472c3747c5ee Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 14 Oct 2016 18:49:08 +0300 Subject: [PATCH 0085/3926] Revert "Do not trigger a DOM event on every message" --- client/js/lounge.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 34e55817..164e6134 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -355,14 +355,17 @@ $(function() { var target = "#chan-" + data.chan; var container = chat.find(target + " .messages"); - container.append(msg); + container + .append(msg) + .trigger("msg", [ + target, + data.msg + ]); if (data.msg.self) { container .find(".unread-marker") .appendTo(container); - } else { - chatMessageShown(target, data.msg); } }); @@ -951,7 +954,11 @@ $(function() { }); }); - function chatMessageShown(target, msg) { + chat.on("msg", ".messages", function(e, target, msg) { + if (msg.self) { + return; + } + var button = sidebar.find(".chan[data-target='" + target + "']"); if (msg.highlight || (options.notifyAllMessages && msg.type === "message")) { if (!document.hasFocus() || !$(target).hasClass("active")) { @@ -1015,7 +1022,7 @@ $(function() { badge.addClass("highlight"); } } - } + }); chat.on("click", ".show-more-button", function() { var self = $(this); From bfeaeee8735c926f9a8309d1fe980ca7c4b946bd Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 8 Oct 2016 18:11:18 +0300 Subject: [PATCH 0086/3926] Fix /mode command to correctly assume target --- src/plugins/inputs/mode.js | 68 +++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/src/plugins/inputs/mode.js b/src/plugins/inputs/mode.js index 37bc490c..b6ee5d80 100644 --- a/src/plugins/inputs/mode.js +++ b/src/plugins/inputs/mode.js @@ -2,30 +2,60 @@ exports.commands = ["mode", "op", "voice", "deop", "devoice"]; +var Chan = require("../../models/chan"); +var Msg = require("../../models/msg"); + +exports.commands = [ + "mode", + "op", + "deop", + "hop", + "dehop", + "voice", + "devoice", +]; + exports.input = function(network, chan, cmd, args) { - if (args.length === 0) { + if (cmd !== "mode") { + if (chan.type !== Chan.Type.CHANNEL) { + chan.pushMessage(this, new Msg({ + type: Msg.Type.ERROR, + text: `${cmd} command can only be used in channels.` + })); + + return; + } + + if (args.length === 0) { + chan.pushMessage(this, new Msg({ + type: Msg.Type.ERROR, + text: `Usage: /${cmd} [...nick]` + })); + + return; + } + + const mode = { + op: "+o", + hop: "+h", + voice: "+v", + deop: "-o", + dehop: "-h", + devoice: "-v" + }[cmd]; + + args.forEach(function(target) { + network.irc.raw("MODE", chan.name, mode, target); + }); + return; } - var mode; - var user; - if (cmd !== "mode") { - user = args[0]; - mode = { - op: "+o", - voice: "+v", - deop: "-o", - devoice: "-v" - }[cmd]; - } else if (args.length === 1) { - return true; - } else { - mode = args[0]; - user = args[1]; + if (args.length === 0 || args[0][0] === "+" || args[0][0] === "-") { + args.unshift(chan.type === Chan.Type.CHANNEL || chan.type === Chan.Type.QUERY ? chan.name : network.nick); } - var irc = network.irc; - irc.raw("MODE", chan.name, mode, user); + args.unshift("MODE"); - return true; + network.irc.raw.apply(network.irc, args); }; From 93053d497de38b563a289bb4dbe0ca4291ee2883 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 8 Oct 2016 18:56:41 +0300 Subject: [PATCH 0087/3926] Add mode command test suite --- test/commands/mode.js | 90 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 test/commands/mode.js diff --git a/test/commands/mode.js b/test/commands/mode.js new file mode 100644 index 00000000..b54301a3 --- /dev/null +++ b/test/commands/mode.js @@ -0,0 +1,90 @@ +"use strict"; + +var expect = require("chai").expect; + +var Chan = require("../../src/models/chan"); +var ModeCommand = require("../../src/plugins/inputs/mode"); + +describe("Commands", function() { + describe("/mode", function() { + const channel = new Chan({ + name: "#thelounge" + }); + + const lobby = new Chan({ + name: "Network Lobby", + type: Chan.Type.LOBBY + }); + + const testableNetwork = { + lastCommand: null, + nick: "xPaw", + irc: { + raw: function() { + testableNetwork.lastCommand = Array.prototype.join.call(arguments, " "); + } + } + }; + + it("should not mess with the given target", function() { + const test = function(expected, args) { + ModeCommand.input(testableNetwork, channel, "mode", Array.from(args)); + expect(testableNetwork.lastCommand).to.equal(expected); + + ModeCommand.input(testableNetwork, lobby, "mode", Array.from(args)); + expect(testableNetwork.lastCommand).to.equal(expected); + }; + + test("MODE xPaw +i", ["xPaw", "+i"]); + test("MODE xPaw -w", ["xPaw", "-w"]); + test("MODE #thelounge +o xPaw", ["#thelounge", "+o", "xPaw"]); + test("MODE #thelounge -v xPaw", ["#thelounge", "-v", "xPaw"]); + test("MODE #thelounge +o-o xPaw Max-P", ["#thelounge", "+o-o", "xPaw", "Max-P"]); + test("MODE #thelounge", ["#thelounge"]); + }); + + it("should assume target if none given", function() { + ModeCommand.input(testableNetwork, channel, "mode", []); + expect(testableNetwork.lastCommand).to.equal("MODE #thelounge"); + + ModeCommand.input(testableNetwork, lobby, "mode", []); + expect(testableNetwork.lastCommand).to.equal("MODE xPaw"); + + ModeCommand.input(testableNetwork, channel, "mode", ["+b"]); + expect(testableNetwork.lastCommand).to.equal("MODE #thelounge +b"); + + ModeCommand.input(testableNetwork, lobby, "mode", ["+b"]); + expect(testableNetwork.lastCommand).to.equal("MODE xPaw +b"); + + ModeCommand.input(testableNetwork, channel, "mode", ["-o", "hey"]); + expect(testableNetwork.lastCommand).to.equal("MODE #thelounge -o hey"); + + ModeCommand.input(testableNetwork, lobby, "mode", ["-i", "idk"]); + expect(testableNetwork.lastCommand).to.equal("MODE xPaw -i idk"); + }); + + it("should support shorthand commands", function() { + ModeCommand.input(testableNetwork, channel, "op", ["xPaw"]); + expect(testableNetwork.lastCommand).to.equal("MODE #thelounge +o xPaw"); + + ModeCommand.input(testableNetwork, channel, "deop", ["xPaw"]); + expect(testableNetwork.lastCommand).to.equal("MODE #thelounge -o xPaw"); + + ModeCommand.input(testableNetwork, channel, "hop", ["xPaw"]); + expect(testableNetwork.lastCommand).to.equal("MODE #thelounge +h xPaw"); + + ModeCommand.input(testableNetwork, channel, "dehop", ["xPaw"]); + expect(testableNetwork.lastCommand).to.equal("MODE #thelounge -h xPaw"); + + ModeCommand.input(testableNetwork, channel, "voice", ["xPaw"]); + expect(testableNetwork.lastCommand).to.equal("MODE #thelounge +v xPaw"); + + ModeCommand.input(testableNetwork, channel, "devoice", ["xPaw"]); + expect(testableNetwork.lastCommand).to.equal("MODE #thelounge -v xPaw"); + + // Multiple arguments are supported, sent as separate commands + ModeCommand.input(testableNetwork, channel, "devoice", ["xPaw", "Max-P"]); + expect(testableNetwork.lastCommand).to.equal("MODE #thelounge -v Max-P"); + }); + }); +}); From 089c315a8efac39d7ece52fe855bde0af450a140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 12 Oct 2016 03:55:40 -0400 Subject: [PATCH 0088/3926] Use forEach instead of lodash's each, and ES6 fat arrows like it's 2015 --- src/client.js | 18 ++++++++---------- src/clientManager.js | 9 ++++----- src/models/chan.js | 2 +- src/plugins/irc-events/connection.js | 7 +++---- src/plugins/irc-events/link.js | 12 ++++-------- src/plugins/irc-events/mode.js | 2 +- src/plugins/irc-events/motd.js | 2 +- src/plugins/irc-events/names.js | 9 +-------- src/plugins/irc-events/nick.js | 2 +- src/plugins/irc-events/quit.js | 2 +- test/models/chan.js | 3 +-- 11 files changed, 26 insertions(+), 42 deletions(-) diff --git a/src/client.js b/src/client.js index 02d15f16..70f8daa6 100644 --- a/src/client.js +++ b/src/client.js @@ -54,9 +54,7 @@ var inputs = [ ].reduce(function(plugins, name) { var path = "./plugins/inputs/" + name; var plugin = require(path); - plugin.commands.forEach(function(command) { - plugins[command] = plugin; - }); + plugin.commands.forEach(command => plugins[command] = plugin); return plugins; }, {}); @@ -83,7 +81,7 @@ function Client(manager, name, config) { } var delay = 0; - (client.config.networks || []).forEach(function(n) { + (client.config.networks || []).forEach(n => { setTimeout(function() { client.connect(n); }, delay); @@ -150,7 +148,7 @@ Client.prototype.connect = function(args) { if (args.channels) { var badName = false; - args.channels.forEach(function(chan) { + args.channels.forEach(chan => { if (!chan.name) { badName = true; return; @@ -248,7 +246,7 @@ Client.prototype.connect = function(args) { "znc.in/self-message", ]); - events.forEach(function(plugin) { + events.forEach(plugin => { var path = "./plugins/irc-events/" + plugin; require(path).apply(client, [ network.irc, @@ -311,7 +309,7 @@ Client.prototype.setPassword = function(hash, callback) { Client.prototype.input = function(data) { var client = this; - data.text.split("\n").forEach(function(line) { + data.text.split("\n").forEach(line => { data.text = line; client.inputLine(data); }); @@ -387,7 +385,7 @@ Client.prototype.sort = function(data) { switch (type) { case "networks": - _.each(order, function(i) { + order.forEach(i => { var find = _.find(self.networks, {id: i}); if (find) { sorted.push(find); @@ -402,7 +400,7 @@ Client.prototype.sort = function(data) { if (!network) { return; } - _.each(order, function(i) { + order.forEach(i => { var find = _.find(network.channels, {id: i}); if (find) { sorted.push(find); @@ -437,7 +435,7 @@ Client.prototype.quit = function() { socket.disconnect(); } } - this.networks.forEach(function(network) { + this.networks.forEach(network => { if (network.irc) { network.irc.quit("Page closed"); } diff --git a/src/clientManager.js b/src/clientManager.js index 56774b63..120320af 100644 --- a/src/clientManager.js +++ b/src/clientManager.js @@ -54,7 +54,7 @@ ClientManager.prototype.getUsers = function() { var users = []; try { var files = fs.readdirSync(Helper.USERS_PATH); - files.forEach(function(file) { + files.forEach(file => { if (file.indexOf(".json") !== -1) { users.push(file.replace(".json", "")); } @@ -148,11 +148,10 @@ ClientManager.prototype.autoload = function(/* sockets */) { "name" ); var added = _.difference(self.getUsers(), loaded); - _.each(added, function(name) { - self.loadUser(name); - }); + added.forEach(name => self.loadUser(name)); + var removed = _.difference(loaded, self.getUsers()); - _.each(removed, function(name) { + removed.forEach(name => { var client = _.find( self.clients, { name: name diff --git a/src/models/chan.js b/src/models/chan.js index 3aaba2f5..a8c10474 100644 --- a/src/models/chan.js +++ b/src/models/chan.js @@ -59,7 +59,7 @@ Chan.prototype.pushMessage = function(client, msg) { Chan.prototype.sortUsers = function(irc) { var userModeSortPriority = {}; - irc.network.options.PREFIX.forEach(function(prefix, index) { + irc.network.options.PREFIX.forEach((prefix, index) => { userModeSortPriority[prefix.symbol] = index; }); diff --git a/src/plugins/irc-events/connection.js b/src/plugins/irc-events/connection.js index 0c5ab0f9..a5507d93 100644 --- a/src/plugins/irc-events/connection.js +++ b/src/plugins/irc-events/connection.js @@ -1,6 +1,5 @@ "use strict"; -var _ = require("lodash"); var identd = require("../../identd"); var Msg = require("../../models/msg"); var Chan = require("../../models/chan"); @@ -24,7 +23,7 @@ module.exports = function(irc, network) { var delay = 1000; var commands = network.commands; if (Array.isArray(commands)) { - commands.forEach(function(cmd) { + commands.forEach(cmd => { setTimeout(function() { client.input({ target: network.channels[0].id, @@ -35,7 +34,7 @@ module.exports = function(irc, network) { }); } - network.channels.forEach(function(chan) { + network.channels.forEach(chan => { if (chan.type !== Chan.Type.CHANNEL) { return; } @@ -114,7 +113,7 @@ module.exports = function(irc, network) { network.prefixLookup = {}; - _.each(data.options.PREFIX, function(mode) { + data.options.PREFIX.forEach(mode => { network.prefixLookup[mode.mode] = mode.symbol; }); diff --git a/src/plugins/irc-events/link.js b/src/plugins/irc-events/link.js index 76cbfcba..8e7e900b 100644 --- a/src/plugins/irc-events/link.js +++ b/src/plugins/irc-events/link.js @@ -1,6 +1,5 @@ "use strict"; -var _ = require("lodash"); var cheerio = require("cheerio"); var Msg = require("../../models/msg"); var request = require("request"); @@ -16,13 +15,10 @@ module.exports = function(irc, network) { return; } - var links = []; - var split = data.message.replace(/\x02|\x1D|\x1F|\x16|\x0F|\x03(?:[0-9]{1,2}(?:,[0-9]{1,2})?)?/g, "").split(" "); - _.each(split, function(w) { - if (/^https?:\/\//.test(w)) { - links.push(w); - } - }); + const links = data.message + .replace(/\x02|\x1D|\x1F|\x16|\x0F|\x03(?:[0-9]{1,2}(?:,[0-9]{1,2})?)?/g, "") + .split(" ") + .filter(w => /^https?:\/\//.test(w)); if (links.length === 0) { return; diff --git a/src/plugins/irc-events/mode.js b/src/plugins/irc-events/mode.js index 6cadabee..b47985c9 100644 --- a/src/plugins/irc-events/mode.js +++ b/src/plugins/irc-events/mode.js @@ -22,7 +22,7 @@ module.exports = function(irc, network) { var supportsMultiPrefix = network.irc.network.cap.isEnabled("multi-prefix"); var userModeSortPriority = {}; - irc.network.options.PREFIX.forEach(function(prefix, index) { + irc.network.options.PREFIX.forEach((prefix, index) => { userModeSortPriority[prefix.symbol] = index; }); diff --git a/src/plugins/irc-events/motd.js b/src/plugins/irc-events/motd.js index 1075d229..0d777869 100644 --- a/src/plugins/irc-events/motd.js +++ b/src/plugins/irc-events/motd.js @@ -8,7 +8,7 @@ module.exports = function(irc, network) { var lobby = network.channels[0]; if (data.motd) { - data.motd.split("\n").forEach(function(text) { + data.motd.split("\n").forEach(text => { var msg = new Msg({ type: Msg.Type.MOTD, text: text diff --git a/src/plugins/irc-events/names.js b/src/plugins/irc-events/names.js index 3c16149c..daae14b3 100644 --- a/src/plugins/irc-events/names.js +++ b/src/plugins/irc-events/names.js @@ -1,6 +1,5 @@ "use strict"; -var _ = require("lodash"); var User = require("../../models/user"); module.exports = function(irc, network) { @@ -11,13 +10,7 @@ module.exports = function(irc, network) { return; } - chan.users = []; - - _.each(data.users, function(u) { - var user = new User(u, network.prefixLookup); - - chan.users.push(user); - }); + chan.users = data.users.map(u => new User(u, network.prefixLookup)); chan.sortUsers(irc); diff --git a/src/plugins/irc-events/nick.js b/src/plugins/irc-events/nick.js index a1bfc3b7..5096c7a2 100644 --- a/src/plugins/irc-events/nick.js +++ b/src/plugins/irc-events/nick.js @@ -24,7 +24,7 @@ module.exports = function(irc, network) { }); } - network.channels.forEach(function(chan) { + network.channels.forEach(chan => { var user = _.find(chan.users, {name: data.nick}); if (typeof user === "undefined") { return; diff --git a/src/plugins/irc-events/quit.js b/src/plugins/irc-events/quit.js index 24104852..1684f132 100644 --- a/src/plugins/irc-events/quit.js +++ b/src/plugins/irc-events/quit.js @@ -6,7 +6,7 @@ var Msg = require("../../models/msg"); module.exports = function(irc, network) { var client = this; irc.on("quit", function(data) { - network.channels.forEach(function(chan) { + network.channels.forEach(chan => { var from = data.nick; var user = _.find(chan.users, {name: from}); if (typeof user === "undefined") { diff --git a/test/models/chan.js b/test/models/chan.js index a12335c0..1a051182 100644 --- a/test/models/chan.js +++ b/test/models/chan.js @@ -1,6 +1,5 @@ "use strict"; -var _ = require("lodash"); var expect = require("chai").expect; var Chan = require("../../src/models/chan"); @@ -24,7 +23,7 @@ describe("Chan", function() { var prefixLookup = {}; - _.each(network.network.options.PREFIX, function(mode) { + network.network.options.PREFIX.forEach(mode => { prefixLookup[mode.mode] = mode.symbol; }); From e905c139d7f9c7bd8252e1b3792574ef916b4fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 12 Oct 2016 04:00:04 -0400 Subject: [PATCH 0089/3926] Use native ES5 map method instead of lodash's --- src/client.js | 9 +-------- src/clientManager.js | 5 +---- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/client.js b/src/client.js index 70f8daa6..6e08d814 100644 --- a/src/client.js +++ b/src/client.js @@ -458,14 +458,7 @@ Client.prototype.save = function(force) { return; } - var networks = _.map( - this.networks, - function(n) { - return n.export(); - } - ); - var json = {}; - json.networks = networks; + json.networks = this.networks.map(n => n.export()); client.manager.updateUser(client.name, json); }; diff --git a/src/clientManager.js b/src/clientManager.js index 120320af..f9b2d043 100644 --- a/src/clientManager.js +++ b/src/clientManager.js @@ -143,10 +143,7 @@ ClientManager.prototype.removeUser = function(name) { ClientManager.prototype.autoload = function(/* sockets */) { var self = this; setInterval(function() { - var loaded = _.map( - self.clients, - "name" - ); + var loaded = self.clients.map(c => c.name); var added = _.difference(self.getUsers(), loaded); added.forEach(name => self.loadUser(name)); From b503b8cb4f9153586dd37530c2d5eb7171c745ef Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Fri, 14 Oct 2016 22:34:01 -0700 Subject: [PATCH 0090/3926] Add image to readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 54ea3c3e..152a9cea 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ [![devDependency Status](https://david-dm.org/thelounge/lounge/dev-status.svg)](https://david-dm.org/thelounge/lounge?type=dev) # The Lounge +

+ +

__What is it?__ From 09f2d069de5ac4c74ed88e6578e8469f1426ef79 Mon Sep 17 00:00:00 2001 From: Girish Ramakrishnan Date: Sat, 15 Oct 2016 23:47:30 -0700 Subject: [PATCH 0091/3926] Fix crash when LDAP server is unreachable Fixes #667 --- src/server.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/server.js b/src/server.js index 2ce7be08..596f014c 100644 --- a/src/server.js +++ b/src/server.js @@ -13,7 +13,6 @@ var Helper = require("./helper"); var ldap = require("ldapjs"); var manager = null; -var ldapclient = null; var authFunction = localAuth; module.exports = function() { @@ -61,9 +60,6 @@ module.exports = function() { } if (!config.public && (config.ldap || {}).enable) { - ldapclient = ldap.createClient({ - url: config.ldap.url - }); authFunction = ldapAuth; } @@ -271,12 +267,22 @@ function ldapAuth(client, user, password, callback) { var userDN = user.replace(/([,\\\/#+<>;"= ])/g, "\\$1"); var bindDN = Helper.config.ldap.primaryKey + "=" + userDN + "," + Helper.config.ldap.baseDN; + var ldapclient = ldap.createClient({ + url: Helper.config.ldap.url + }); + + ldapclient.on("error", function(err) { + log.error("Unable to connect to LDAP server", err); + callback(!err); + }); + ldapclient.bind(bindDN, password, function(err) { if (!err && !client) { if (!manager.addUser(user, null)) { log.error("Unable to create new user", user); } } + ldapclient.unbind(); callback(!err); }); } From 38efe89f564f2c88e25bb869f32f3937a0e8ce95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Mon, 17 Oct 2016 00:49:48 -0400 Subject: [PATCH 0092/3926] Add change log entry for upcoming v2.1.0 --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dafdfeb..775cec47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,49 @@ All sections are explained on the link above, they are all optional, and each of Don't forget to thank the PR authors in a commit comment, and copy/paste the release content as-is in GitHub releases: https://github.com/thelounge/lounge/releases --> +## v2.1.0 - 2016-10-17 + +[See the full changelog](https://github.com/thelounge/lounge/compare/v2.0.1...v2.1.0) + +Here comes another release with some nice additions! + +While the administrators will notice some bug fixes, most of the changes are client-side: support for `/list`, a slideout menu on mobile, editing one's nick from the UI, wallops message handling. + +Enjoy! + +### Added + +- Implement `/list` ([#258](https://github.com/thelounge/lounge/pull/258) by [@maxpoulin64](https://github.com/maxpoulin64)) +- Add touch slideout menu for mobile ([#400](https://github.com/thelounge/lounge/pull/400) by [@maxpoulin64](https://github.com/maxpoulin64)) +- Display extra steps when loading the app ([#637](https://github.com/thelounge/lounge/pull/637) by [@xPaw](https://github.com/xPaw)) +- Display localized timestamp in title of message times ([#660](https://github.com/thelounge/lounge/pull/660) by [@astorije](https://github.com/astorije)) +- Changing nick in the UI ([#551](https://github.com/thelounge/lounge/pull/551) by [@astorije](https://github.com/astorije)) +- Add hostmasks in logs when possible ([#670](https://github.com/thelounge/lounge/pull/670) by [@astorije](https://github.com/astorije)) +- Display wallops in server window ([#658](https://github.com/thelounge/lounge/pull/658) by [@xPaw](https://github.com/xPaw)) + +### Changed + +- Make use of multi-prefix cap and remove NAMES spam on mode changes ([#632](https://github.com/thelounge/lounge/pull/632) by [@xPaw](https://github.com/xPaw)) +- Strict mode for all JS files ([#684](https://github.com/thelounge/lounge/pull/684) by [@astorije](https://github.com/astorije)) +- Enforce more ESLint rules ([#681](https://github.com/thelounge/lounge/pull/681) by [@xPaw](https://github.com/xPaw)) +- Use CI caches for downloaded files instead of installed ones ([#687](https://github.com/thelounge/lounge/pull/687) by [@astorije](https://github.com/astorije)) +- Consolidate version numbers throughout all interfaces ([#592](https://github.com/thelounge/lounge/pull/592) by [@williamboman](https://github.com/williamboman)) +- Replace lodash's each/map with ES5 native forEach/map ([#689](https://github.com/thelounge/lounge/pull/689) by [@astorije](https://github.com/astorije)) + +### Removed + +- Remove all font files except WOFF ([#682](https://github.com/thelounge/lounge/pull/682) by [@xPaw](https://github.com/xPaw)) + +### Fixed + +- Themes: Fixed CSS rule selectors for highlight messages ([#652](https://github.com/thelounge/lounge/pull/652) by [@DamonGant](https://github.com/DamonGant)) +- Fix unhandled message color in default and Crypto themes ([#653](https://github.com/thelounge/lounge/pull/653) by [@MaxLeiter](https://github.com/MaxLeiter)) +- Check if SSL key and certificate files exist ([#673](https://github.com/thelounge/lounge/pull/673) by [@toXel](https://github.com/toXel)) +- Fix loading fonts in Microsoft Edge ([#683](https://github.com/thelounge/lounge/pull/683) by [@xPaw](https://github.com/xPaw)) +- Fill in prefixLookup on network initialization ([#647](https://github.com/thelounge/lounge/pull/647) by [@nornagon](https://github.com/nornagon)) +- Fix nick changes not being properly reported in the logs ([#685](https://github.com/thelounge/lounge/pull/685) by [@astorije](https://github.com/astorije)) +- Fix memory and reference shuffling when creating models ([#664](https://github.com/thelounge/lounge/pull/664) by [@xPaw](https://github.com/xPaw)) + ## v2.0.1 - 2016-09-28 [See the full changelog](https://github.com/thelounge/lounge/compare/v2.0.0...v2.0.1) From b2a0cae626cbbcfc42dd4dc89410759fc3c81cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Mon, 17 Oct 2016 00:49:52 -0400 Subject: [PATCH 0093/3926] 2.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 177bd846..bafd97dc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "thelounge", "description": "The self-hosted Web IRC client", - "version": "2.0.1", + "version": "2.1.0", "preferGlobal": true, "bin": { "lounge": "index.js" From a1f56c73956a18e1ec07a1e4c097b114c501ca41 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sun, 25 Sep 2016 09:41:10 +0300 Subject: [PATCH 0094/3926] Improve support for opening multiple clients at once - Synchornize unread counter with the server - Fix unread marker on no attached clients - Increase unread counter for server messages --- client/js/lounge.js | 32 +++++++++++----------- client/views/chan.tpl | 2 +- src/client.js | 40 +++++++++++++++++++++------- src/models/chan.js | 17 +++++++++--- src/plugins/irc-events/connection.js | 14 +++++----- src/plugins/irc-events/error.js | 6 ++--- src/plugins/irc-events/message.js | 6 +---- src/plugins/irc-events/nick.js | 2 +- src/plugins/irc-events/unhandled.js | 2 +- src/server.js | 9 +++++-- 10 files changed, 81 insertions(+), 49 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 164e6134..21f64fa2 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -184,6 +184,13 @@ $(function() { } }); + socket.on("open", function(id) { + // Another client opened the channel, clear the unread counter + sidebar.find("[data-id='" + id + "'] .badge") + .removeClass("highlight") + .empty(); + }); + socket.on("join", function(data) { var id = data.network; var network = sidebar.find("#network-" + id); @@ -359,7 +366,7 @@ $(function() { .append(msg) .trigger("msg", [ target, - data.msg + data ]); if (data.msg.self) { @@ -846,7 +853,6 @@ $(function() { self.addClass("active") .find(".badge") .removeClass("highlight") - .data("count", 0) .empty(); if (sidebar.find(".highlight").length === 0) { @@ -955,6 +961,9 @@ $(function() { }); chat.on("msg", ".messages", function(e, target, msg) { + var unread = msg.unread; + msg = msg.msg; + if (msg.self) { return; } @@ -1004,23 +1013,14 @@ $(function() { return; } - var whitelistedActions = [ - "message", - "notice", - "action", - ]; - if (whitelistedActions.indexOf(msg.type) === -1) { + if (!unread) { return; } - var badge = button.find(".badge"); - if (badge.length !== 0) { - var i = (badge.data("count") || 0) + 1; - badge.data("count", i); - badge.html(Handlebars.helpers.roundBadgeNumber(i)); - if (msg.highlight) { - badge.addClass("highlight"); - } + var badge = button.find(".badge").html(Handlebars.helpers.roundBadgeNumber(unread)); + + if (msg.highlight) { + badge.addClass("highlight"); } }); diff --git a/client/views/chan.tpl b/client/views/chan.tpl index 22a6c3cc..626eb228 100644 --- a/client/views/chan.tpl +++ b/client/views/chan.tpl @@ -1,6 +1,6 @@ {{#each channels}}
- {{#if unread}}{{roundBadgeNumber unread}}{{/if}} + {{#if unread}}{{roundBadgeNumber unread}}{{/if}} {{name}}
diff --git a/src/client.js b/src/client.js index 6e08d814..77db906b 100644 --- a/src/client.js +++ b/src/client.js @@ -63,7 +63,8 @@ function Client(manager, name, config) { config = {}; } _.merge(this, { - activeChannel: -1, + lastActiveChannel: -1, + attachedClients: {}, config: config, id: id++, name: name, @@ -201,7 +202,7 @@ Client.prototype.connect = function(args) { network.channels[0].pushMessage(client, new Msg({ type: Msg.Type.ERROR, text: "Hostname you specified is not allowed." - })); + }), true); return; } @@ -214,7 +215,7 @@ Client.prototype.connect = function(args) { network.channels[0].pushMessage(client, new Msg({ type: Msg.Type.ERROR, text: "You must specify a hostname to connect." - })); + }), true); return; } @@ -319,6 +320,13 @@ Client.prototype.inputLine = function(data) { var client = this; var text = data.text; var target = client.find(data.target); + if (!target) { + return; + } + + // Sending a message to a channel is higher priority than merely opening one + // so that reloading the page will open this channel + this.lastActiveChannel = target.chan.id; // This is either a normal message or a command escaped with a leading '/' if (text.charAt(0) !== "/" || text.charAt(1) === "/") { @@ -366,14 +374,20 @@ Client.prototype.more = function(data) { }); }; -Client.prototype.open = function(data) { +Client.prototype.open = function(socketId, data) { var target = this.find(data); - if (target) { - target.chan.firstUnread = 0; - target.chan.unread = 0; - target.chan.highlight = false; - this.activeChannel = target.chan.id; + if (!target) { + return; } + + target.chan.firstUnread = 0; + target.chan.unread = 0; + target.chan.highlight = false; + + this.attachedClients[socketId] = target.chan.id; + this.lastActiveChannel = target.chan.id; + + this.emit("open", target.chan.id); }; Client.prototype.sort = function(data) { @@ -442,6 +456,14 @@ Client.prototype.quit = function() { }); }; +Client.prototype.clientAttach = function(socketId) { + this.attachedClients[socketId] = this.lastActiveChannel; +}; + +Client.prototype.clientDetach = function(socketId) { + delete this.attachedClients[socketId]; +}; + var timer; Client.prototype.save = function(force) { var client = this; diff --git a/src/models/chan.js b/src/models/chan.js index a8c10474..69e582ab 100644 --- a/src/models/chan.js +++ b/src/models/chan.js @@ -28,11 +28,20 @@ function Chan(attr) { }); } -Chan.prototype.pushMessage = function(client, msg) { - client.emit("msg", { +Chan.prototype.pushMessage = function(client, msg, increasesUnread) { + var obj = { chan: this.id, msg: msg - }); + }; + + // If this channel is open in any of the clients, do not increase unread counter + var isOpen = _.includes(client.attachedClients, this.id); + + if ((increasesUnread || msg.highlight) && !isOpen) { + obj.unread = ++this.unread; + } + + client.emit("msg", obj); // Never store messages in public mode as the session // is completely destroyed when the page gets closed @@ -46,7 +55,7 @@ Chan.prototype.pushMessage = function(client, msg) { this.messages.splice(0, this.messages.length - Helper.config.maxHistory); } - if (!msg.self && this.id !== client.activeChannel) { + if (!msg.self && !isOpen) { if (!this.firstUnread) { this.firstUnread = msg.id; } diff --git a/src/plugins/irc-events/connection.js b/src/plugins/irc-events/connection.js index a5507d93..2d843086 100644 --- a/src/plugins/irc-events/connection.js +++ b/src/plugins/irc-events/connection.js @@ -11,13 +11,13 @@ module.exports = function(irc, network) { network.channels[0].pushMessage(client, new Msg({ text: "Network created, connecting to " + network.host + ":" + network.port + "..." - })); + }), true); irc.on("registered", function() { if (network.irc.network.cap.enabled.length > 0) { network.channels[0].pushMessage(client, new Msg({ text: "Enabled capabilities: " + network.irc.network.cap.enabled.join(", ") - })); + }), true); } var delay = 1000; @@ -54,13 +54,13 @@ module.exports = function(irc, network) { network.channels[0].pushMessage(client, new Msg({ text: "Connected to the network." - })); + }), true); }); irc.on("close", function() { network.channels[0].pushMessage(client, new Msg({ text: "Disconnected from the network, and will not reconnect. Use /connect to reconnect again." - })); + }), true); }); if (identd.isEnabled()) { @@ -91,19 +91,19 @@ module.exports = function(irc, network) { network.channels[0].pushMessage(client, new Msg({ type: Msg.Type.ERROR, text: "Socket error: " + err - })); + }), true); }); irc.on("reconnecting", function(data) { network.channels[0].pushMessage(client, new Msg({ text: "Disconnected from the network. Reconnecting in " + Math.round(data.wait / 1000) + " seconds… (Attempt " + data.attempt + " of " + data.max_retries + ")" - })); + }), true); }); irc.on("ping timeout", function() { network.channels[0].pushMessage(client, new Msg({ text: "Ping timeout, disconnecting…" - })); + }), true); }); irc.on("server options", function(data) { diff --git a/src/plugins/irc-events/error.js b/src/plugins/irc-events/error.js index 477645fb..d08e033c 100644 --- a/src/plugins/irc-events/error.js +++ b/src/plugins/irc-events/error.js @@ -15,7 +15,7 @@ module.exports = function(irc, network) { type: Msg.Type.ERROR, text: text, }); - lobby.pushMessage(client, msg); + lobby.pushMessage(client, msg, true); }); irc.on("nick in use", function(data) { @@ -24,7 +24,7 @@ module.exports = function(irc, network) { type: Msg.Type.ERROR, text: data.nick + ": " + (data.reason || "Nickname is already in use."), }); - lobby.pushMessage(client, msg); + lobby.pushMessage(client, msg, true); if (irc.connection.registered === false) { var random = (data.nick || irc.user.nick) + Math.floor(10 + (Math.random() * 89)); @@ -43,7 +43,7 @@ module.exports = function(irc, network) { type: Msg.Type.ERROR, text: data.nick + ": " + (data.reason || "Nickname is invalid."), }); - lobby.pushMessage(client, msg); + lobby.pushMessage(client, msg, true); if (irc.connection.registered === false) { var random = "i" + Math.random().toString(36).substr(2, 10); // 'i' so it never begins with a number diff --git a/src/plugins/irc-events/message.js b/src/plugins/irc-events/message.js index 558964e6..5eb231fe 100644 --- a/src/plugins/irc-events/message.js +++ b/src/plugins/irc-events/message.js @@ -79,10 +79,6 @@ module.exports = function(irc, network) { highlight = network.highlightRegex.test(data.message); } - if (!self && chan.id !== client.activeChannel) { - chan.unread++; - } - var msg = new Msg({ type: data.type, time: data.time, @@ -92,6 +88,6 @@ module.exports = function(irc, network) { self: self, highlight: highlight }); - chan.pushMessage(client, msg); + chan.pushMessage(client, msg, !self); } }; diff --git a/src/plugins/irc-events/nick.js b/src/plugins/irc-events/nick.js index 5096c7a2..c8519530 100644 --- a/src/plugins/irc-events/nick.js +++ b/src/plugins/irc-events/nick.js @@ -15,7 +15,7 @@ module.exports = function(irc, network) { msg = new Msg({ text: "You're now known as " + data.new_nick, }); - lobby.pushMessage(client, msg); + lobby.pushMessage(client, msg, true); self = true; client.save(); client.emit("nick", { diff --git a/src/plugins/irc-events/unhandled.js b/src/plugins/irc-events/unhandled.js index 46b98f22..a07a48ee 100644 --- a/src/plugins/irc-events/unhandled.js +++ b/src/plugins/irc-events/unhandled.js @@ -15,6 +15,6 @@ module.exports = function(irc, network) { type: Msg.Type.UNHANDLED, command: command.command, params: command.params - })); + }), true); }); }; diff --git a/src/server.js b/src/server.js index 2ce7be08..bea7776b 100644 --- a/src/server.js +++ b/src/server.js @@ -147,6 +147,11 @@ function init(socket, client) { } else { socket.emit("authorized"); + socket.on("disconnect", function() { + client.clientDetach(socket.id); + }); + client.clientAttach(socket.id); + socket.on( "input", function(data) { @@ -215,7 +220,7 @@ function init(socket, client) { socket.on( "open", function(data) { - client.open(data); + client.open(socket.id, data); } ); socket.on( @@ -232,7 +237,7 @@ function init(socket, client) { ); socket.join(client.id); socket.emit("init", { - active: client.activeChannel, + active: client.lastActiveChannel, networks: client.networks, token: client.config.token || null }); From 79e20c83d5dcc6fc5bc2f4b93b00cf218059d6d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Mon, 17 Oct 2016 01:41:14 -0400 Subject: [PATCH 0095/3926] Fix AppVeyor cache never being successfully built Oh that wonderful Windows world... --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index e63ba59a..f259b74b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,7 +23,7 @@ test_script: # cache npm modules cache: - - '%AppData%/npm-cache' + - '%AppData%\npm-cache' # Don't actually build build: off From 1ff011dfaffffc52b7024da7dff85f28cb8233f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 18 Oct 2016 23:30:46 -0400 Subject: [PATCH 0096/3926] Add a simple (first) test for localetime Handlebars helper --- package.json | 4 +-- .../js/libs/handlebars/localetimeTest.js | 25 +++++++++++++++++++ test/mocha.opts | 2 ++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 test/client/js/libs/handlebars/localetimeTest.js create mode 100644 test/mocha.opts diff --git a/package.json b/package.json index bafd97dc..9b822647 100644 --- a/package.json +++ b/package.json @@ -12,14 +12,14 @@ }, "homepage": "https://thelounge.github.io/", "scripts": { - "coverage": "istanbul cover node_modules/mocha/bin/_mocha -r test/fixtures/env.js test/**/*.js", + "coverage": "istanbul cover node_modules/mocha/bin/_mocha", "start": "node index", "build": "npm-run-all build:*", "build:font-awesome": "node scripts/build-fontawesome.js", "build:libs": "uglifyjs client/js/libs/*.js client/js/libs/jquery/*.js client/js/libs/handlebars/*.js -o client/js/libs.min.js --source-map client/js/libs.min.js.map --source-map-url libs.min.js.map -p relative", "build:handlebars": "handlebars client/views/ -e tpl -f client/js/lounge.templates.js", "test": "npm-run-all -c test:mocha lint", - "test:mocha": "mocha -r test/fixtures/env.js test/**/*.js", + "test:mocha": "mocha", "lint": "npm-run-all -c lint:js lint:css", "lint:js": "npm-run-all -c lint:js:es5 lint:js:es6", "lint:js:es5": "eslint --parser-options=\"ecmaVersion:5\" client/", diff --git a/test/client/js/libs/handlebars/localetimeTest.js b/test/client/js/libs/handlebars/localetimeTest.js new file mode 100644 index 00000000..66cbc648 --- /dev/null +++ b/test/client/js/libs/handlebars/localetimeTest.js @@ -0,0 +1,25 @@ +"use strict"; + +const Handlebars = global.Handlebars = require("handlebars"); +const expect = require("chai").expect; + +require("../../../../../client/js/libs/handlebars/localetime"); + +describe("localetime Handlebars helper", () => { + + it("should render a human-readable date", () => { + const template = Handlebars.compile("{{localetime time}}"); + + // 12PM in UTC time + const date = new Date("2014-05-22T12:00:00"); + + // Offset between UTC and local timezone + const offset = date.getTimezoneOffset() * 60 * 1000; + + // Pretend local timezone is UTC by moving the clock of that offset + const time = date.getTime() + offset; + + expect(template({time: time})).to.equal("5/22/2014, 12:00:00 PM"); + }); + +}); diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 00000000..09f51914 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,2 @@ +--require test/fixtures/env +--recursive From dec9a173bf26d12ff1e4fd73c87dd1de0c91a0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 19 Oct 2016 20:33:48 -0400 Subject: [PATCH 0097/3926] Get rid of OSX CI builds until they get much faster OSX builds have been nothing but a pain on Travis CI: they fail with no good reason, they stay pending forever, etc. As far as I can tell, I can't remember one valid build they failed and we legitimately discovered a bug. Dev env on OSX is very close to Linux so it's good enough to have it here. --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3cad9f94..d155ef52 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,6 @@ node_js: matrix: fast_finish: true - include: - - os: osx - node_js: 4 cache: directories: From c5e0dee3a39562ecacc1f57cc475c054fb532476 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 21 Oct 2016 22:00:43 +0300 Subject: [PATCH 0098/3926] Change bcrypt rounds from 8 to 11 --- src/command-line/add.js | 4 +--- src/command-line/reset.js | 3 +-- src/helper.js | 19 +++++++++++++++++++ src/server.js | 39 +++++++++++++++++++++++++-------------- test/tests/passwords.js | 27 +++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 19 deletions(-) create mode 100644 test/tests/passwords.js diff --git a/src/command-line/add.js b/src/command-line/add.js index 2e3723e0..2a852663 100644 --- a/src/command-line/add.js +++ b/src/command-line/add.js @@ -1,7 +1,6 @@ "use strict"; var ClientManager = new require("../clientManager"); -var bcrypt = require("bcrypt-nodejs"); var program = require("commander"); var Helper = require("../helper"); @@ -26,8 +25,7 @@ program }); function add(manager, name, password) { - var salt = bcrypt.genSaltSync(8); - var hash = bcrypt.hashSync(password, salt); + var hash = Helper.password.hash(password); manager.addUser( name, hash diff --git a/src/command-line/reset.js b/src/command-line/reset.js index e5cf6609..511bd554 100644 --- a/src/command-line/reset.js +++ b/src/command-line/reset.js @@ -1,6 +1,5 @@ "use strict"; -var bcrypt = require("bcrypt-nodejs"); var ClientManager = new require("../clientManager"); var fs = require("fs"); var program = require("commander"); @@ -24,7 +23,7 @@ program if (err) { return; } - user.password = bcrypt.hashSync(password, bcrypt.genSaltSync(8)); + user.password = Helper.password.hash(password); user.token = null; // Will be regenerated when the user is loaded fs.writeFileSync( file, diff --git a/src/helper.js b/src/helper.js index 3db79ca1..d4af765f 100644 --- a/src/helper.js +++ b/src/helper.js @@ -5,6 +5,7 @@ var _ = require("lodash"); var path = require("path"); var os = require("os"); var fs = require("fs"); +var bcrypt = require("bcrypt-nodejs"); var Helper = { config: null, @@ -14,6 +15,12 @@ var Helper = { setHome: setHome, getVersion: getVersion, getGitCommit: getGitCommit, + + password: { + hash: passwordHash, + compare: passwordCompare, + requiresUpdate: passwordRequiresUpdate, + }, }; module.exports = Helper; @@ -83,3 +90,15 @@ function expandHome(shortenedPath) { return path.resolve(shortenedPath.replace(/^~($|\/|\\)/, home + "$1")); } + +function passwordRequiresUpdate(password) { + return bcrypt.getRounds(password) !== 11; +} + +function passwordHash(password) { + return bcrypt.hashSync(password, bcrypt.genSaltSync(11)); +} + +function passwordCompare(password, expected) { + return bcrypt.compareSync(password, expected); +} diff --git a/src/server.js b/src/server.js index bea7776b..d30a2449 100644 --- a/src/server.js +++ b/src/server.js @@ -2,7 +2,6 @@ var _ = require("lodash"); var pkg = require("../package.json"); -var bcrypt = require("bcrypt-nodejs"); var Client = require("./client"); var ClientManager = require("./clientManager"); var express = require("express"); @@ -192,15 +191,14 @@ function init(socket, client) { }); return; } - if (!bcrypt.compareSync(old || "", client.config.password)) { + if (!Helper.password.compare(old || "", client.config.password)) { socket.emit("change-password", { error: "The current password field does not match your account password" }); return; } - var salt = bcrypt.genSaltSync(8); - var hash = bcrypt.hashSync(p1, salt); + var hash = Helper.password.hash(p1); client.setPassword(hash, function(success) { var obj = {}; @@ -259,17 +257,30 @@ function reverseDnsLookup(socket, client) { } function localAuth(client, user, password, callback) { - var result = false; - try { - result = bcrypt.compareSync(password || "", client.config.password); - } catch (error) { - if (error === "Not a valid BCrypt hash.") { - log.error("User (" + user + ") with no local password set tried to sign in. (Probably a LDAP user)"); - } - result = false; - } finally { - callback(result); + if (!client || !password) { + return callback(false); } + + if (!client.config.password) { + log.error("User", user, "with no local password set tried to sign in. (Probably a LDAP user)"); + return callback(false); + } + + var result = Helper.password.compare(password, client.config.password); + + if (result && Helper.password.requiresUpdate(client.config.password)) { + var hash = Helper.password.hash(password); + + client.setPassword(hash, function(success) { + if (!success) { + log.error("Failed to update password of", client.name, "to match new security requirements"); + } else { + log.info("User", client.name, "logged in and their hashed password has been updated to match new security requirements"); + } + }); + } + + return callback(result); } function ldapAuth(client, user, password, callback) { diff --git a/test/tests/passwords.js b/test/tests/passwords.js new file mode 100644 index 00000000..d074477d --- /dev/null +++ b/test/tests/passwords.js @@ -0,0 +1,27 @@ +"use strict"; + +const expect = require("chai").expect; +const Helper = require("../../src/helper"); + +describe("Client passwords", function() { + const inputPassword = "my$Super@Cool Password"; + + it("hashed password should match", function() { + // Generated with third party tool to test implementation + let comparedPassword = Helper.password.compare(inputPassword, "$2a$11$zrPPcfZ091WNfs6QrRHtQeUitlgrJcecfZhxOFiQs0FWw7TN3Q1oS"); + + expect(comparedPassword).to.be.true; + }); + + it("freshly hashed password should match", function() { + let hashedPassword = Helper.password.hash(inputPassword); + let comparedPassword = Helper.password.compare(inputPassword, hashedPassword); + + expect(comparedPassword).to.be.true; + }); + + it("shout passwords should be marked as old", function() { + expect(Helper.password.requiresUpdate("$2a$08$K4l.hteJcCP9D1G5PANzYuBGvdqhUSUDOLQLU.xeRxTbvtp01KINm")).to.be.true; + expect(Helper.password.requiresUpdate("$2a$11$zrPPcfZ091WNfs6QrRHtQeUitlgrJcecfZhxOFiQs0FWw7TN3Q1oS")).to.be.false; + }); +}); From d82a894b7bae5ebbe0e15f84c9e13e699429f5b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Thu, 20 Oct 2016 00:41:30 -0400 Subject: [PATCH 0099/3926] Remove browser notification polyfill and inform user when unsupported --- client/index.html | 9 +- client/js/libs/notification.js | 218 --------------------------------- client/js/lounge.js | 61 +++++---- 3 files changed, 45 insertions(+), 243 deletions(-) delete mode 100644 client/js/libs/notification.js diff --git a/client/index.html b/client/index.html index 25be00ab..2a3c94e5 100644 --- a/client/index.html +++ b/client/index.html @@ -283,7 +283,14 @@
diff --git a/client/js/libs/notification.js b/client/js/libs/notification.js deleted file mode 100644 index 4d84b16a..00000000 --- a/client/js/libs/notification.js +++ /dev/null @@ -1,218 +0,0 @@ -/** - * Notification JS - * Shims up the Notification API - * - * @author Andrew Dodson - * @website http://adodson.com/notification.js/ - */ - -// -// Does the browser support the the Notification API? -// .. and does it have a permission property? -// - -(function(window, document){ - - var PERMISSION_GRANTED = 'granted', - PERMISSION_DENIED = 'denied', - PERMISSION_UNKNOWN = 'unknown'; - - var a = [], iv, i=0; - - // - // Swap the document.title with the notification - // - function swaptitle(title){ - - if(a.length===0){ - a = [document.title]; - } - - a.push(title); - - if(!iv){ - iv = setInterval(function(){ - - // has document.title changed externally? - if(a.indexOf(document.title) === -1 ){ - // update the default title - a[0] = document.title; - } - - document.title = a[++i%a.length]; - }, 1000); - } - } - - function swapTitleCancel(){ - - // dont do any more if we haven't got anything open - if(a.length===0){ - return; - } - - // if an IE overlay is present, kill it - if("external" in window && "msSiteModeClearIconOverlay" in window.external ){ - window.external.msSiteModeClearIconOverlay(); - } - - clearInterval(iv); - - iv = false; - document.title = a[0]; - a = []; - } - - // - // Add aevent handlers - function addEvent(el,name,func){ - if(name.match(" ")){ - var a = name.split(' '); - for(var i=0;i Date: Sun, 16 Oct 2016 10:11:52 +0300 Subject: [PATCH 0100/3926] Reduce badge size in readme --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 54ea3c3e..afeb15d6 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -[![#thelounge IRC channel on freenode](https://img.shields.io/badge/irc%20channel-%23thelounge%20on%20freenode-blue.svg)](https://avatar.playat.ch:1000/) -[![npm version](https://img.shields.io/npm/v/thelounge.svg)](https://www.npmjs.org/package/thelounge) -[![Travis CI Build Status](https://travis-ci.org/thelounge/lounge.svg?branch=master)](https://travis-ci.org/thelounge/lounge) -[![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/deymtp0lldq78s8t/branch/master?svg=true)](https://ci.appveyor.com/project/astorije/lounge/branch/master) -[![Dependency Status](https://david-dm.org/thelounge/lounge.svg)](https://david-dm.org/thelounge/lounge) -[![devDependency Status](https://david-dm.org/thelounge/lounge/dev-status.svg)](https://david-dm.org/thelounge/lounge?type=dev) - # The Lounge +[![#thelounge IRC channel on freenode](https://img.shields.io/badge/freenode-%23thelounge-BA68C8.svg)](https://avatar.playat.ch:1000/) +[![npm version](https://img.shields.io/npm/v/thelounge.svg)](https://www.npmjs.org/package/thelounge) +[![Travis CI Build Status](https://img.shields.io/travis/thelounge/lounge/master.svg?label=linux+build)](https://travis-ci.org/thelounge/lounge) +[![AppVeyor Build Status](https://img.shields.io/appveyor/ci/astorije/lounge/master.svg?label=windows+build)](https://ci.appveyor.com/project/astorije/lounge/branch/master) +[![Dependencies Status](https://img.shields.io/david/thelounge/lounge.svg)](https://david-dm.org/thelounge/lounge) +[![Developer Dependencies Status](https://img.shields.io/david/dev/thelounge/lounge.svg)](https://david-dm.org/thelounge/lounge?type=dev) + __What is it?__ The Lounge is a web IRC client that you host on your own server. From b93fa124948d59cf93f21fa34ce2c614190b95b4 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sun, 23 Oct 2016 11:11:04 +0300 Subject: [PATCH 0101/3926] Match window title border line to text color --- client/css/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/css/style.css b/client/css/style.css index 761b6f57..260e84ab 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -645,7 +645,7 @@ button { } #windows .window h2 { - border-bottom: 1px solid #eee; + border-bottom: 1px solid #7f8c8d; color: #7f8c8d; font-size: 22px; margin: 30px 0 10px; From 35af3b1710ec377160adb40a54058196f7908a2c Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sun, 23 Oct 2016 11:16:55 +0300 Subject: [PATCH 0102/3926] Add id to submit button --- client/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/index.html b/client/index.html index 2a3c94e5..1e6098e1 100644 --- a/client/index.html +++ b/client/index.html @@ -66,7 +66,7 @@ --> - +
From 1a4974b7df9be01816d52190ecd2dfbc18cbc9c9 Mon Sep 17 00:00:00 2001 From: stepie22 Date: Tue, 18 Oct 2016 23:53:39 +0200 Subject: [PATCH 0103/3926] Fix channels drag'n'droping --- client/css/style.css | 1 + client/js/lounge.js | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 761b6f57..8cc68849 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -409,6 +409,7 @@ button { text-align: left; transition: color .2s; width: 180px; + left: auto !important; /* Fix for drag'n'drop not recalculating left position */ } #sidebar .chan-placeholder { diff --git a/client/js/lounge.js b/client/js/lounge.js index df949240..9769efd2 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -1245,15 +1245,17 @@ $(function() { } function sortable() { - sidebar.sortable({ + sidebar.find(".networks").sortable({ axis: "y", containment: "parent", - cursor: "grabbing", + cursor: "move", distance: 12, items: ".network", handle: ".lobby", placeholder: "network-placeholder", forcePlaceholderSize: true, + tolerance: "pointer", // Use the pointer to figure out where the network is in the list + update: function() { var order = []; sidebar.find(".network").each(function() { @@ -1271,11 +1273,13 @@ $(function() { sidebar.find(".network").sortable({ axis: "y", containment: "parent", - cursor: "grabbing", + cursor: "move", distance: 12, items: ".chan:not(.lobby)", placeholder: "chan-placeholder", forcePlaceholderSize: true, + tolerance: "pointer", // Use the pointer to figure out where the channel is in the list + update: function(e, ui) { var order = []; var network = ui.item.parent(); From ddaf7ff300746e7e8aec4aa6a940294ddcd2a36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Thu, 6 Oct 2016 19:42:05 -0400 Subject: [PATCH 0104/3926] Add a way to cycle through nicks on mobile This was heavily inspired by https://github.com/maxpoulin64/lounge/commit/a877e46. Clearly not a definitive solution but a good start to have and to improve upon. --- client/css/style.css | 11 ++++++++++- client/index.html | 3 +++ client/js/lounge.js | 6 ++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/client/css/style.css b/client/css/style.css index 6a0b0408..ce41be48 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -141,6 +141,7 @@ button { #footer .icon, #chat .count:before, #settings #play:before, +#form #cycle-nicks:before, #form #submit:before, #chat .invite .from:before, #chat .join .from:before, @@ -186,6 +187,8 @@ button { #footer .settings:before { content: "\f013"; /* http://fontawesome.io/icon/cog/ */ } #footer .sign-out:before { content: "\f011"; /* http://fontawesome.io/icon/power-off/ */ } +#form #cycle-nicks:before { content: "\f007"; /* http://fontawesome.io/icon/user/ */ } + #form #submit:before { content: "\f1d8"; /* http://fontawesome.io/icon/paper-plane/ */ } #chat .invite .from:before { @@ -1362,16 +1365,22 @@ button { align-self: center; } +#form #cycle-nicks, #form #submit { color: #9ca5b4; font-size: 14px; height: 32px; transition: opacity .2s; - width: 32px; + width: 24px; -webkit-flex: 0 0 auto; flex: 0 0 auto; } +#form #submit { + margin-right: 4px; +} + +#form #cycle-nicks:hover, #form #submit:hover { opacity: .6; } diff --git a/client/index.html b/client/index.html index 1e6098e1..4226476f 100644 --- a/client/index.html +++ b/client/index.html @@ -66,6 +66,9 @@ -->
+ + + diff --git a/client/js/lounge.js b/client/js/lounge.js index 9769efd2..3722ed3c 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -721,6 +721,12 @@ $(function() { }) .tab(complete, {hint: false}); + // Cycle through nicks for the current word, just like hitting "Tab" + $("#cycle-nicks").on("click", function() { + input.triggerHandler($.Event("keydown.tabcomplete", {which: 9})); + focus(); + }); + $("#form").on("submit", function(e) { e.preventDefault(); var text = input.val(); From ee1a629be9a7704c1ee7c5d9a9d086987be2c325 Mon Sep 17 00:00:00 2001 From: stepie22 Date: Tue, 25 Oct 2016 13:58:28 +0200 Subject: [PATCH 0105/3926] Fix incorrect selector for undead badge on channels --- client/js/lounge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index df949240..1af88900 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -186,7 +186,7 @@ $(function() { socket.on("open", function(id) { // Another client opened the channel, clear the unread counter - sidebar.find("[data-id='" + id + "'] .badge") + sidebar.find(".chan[data-id='" + id + "'] .badge") .removeClass("highlight") .empty(); }); From e21ec8b447e2cc574bb0a9d7db9bc4c4db101a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Mon, 7 Nov 2016 00:16:10 -0500 Subject: [PATCH 0106/3926] Silence failures to trigger notifications when not available Recent Chrome versions are dropping out `new Notification` in favor of `ServiceWorkerRegistration.showNotification`. This makes sure nothing bad happens until we have proper support for Service Workers. See: - https://stackoverflow.com/questions/29774836/failed-to-construct-notification-illegal-constructor - https://stackoverflow.com/questions/31512504/html5-notification-not-working-in-mobile-chrome --- client/js/lounge.js | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 6eab611c..30186cd8 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -1031,19 +1031,24 @@ $(function() { body = msg.text.replace(/\x02|\x1D|\x1F|\x16|\x0F|\x03(?:[0-9]{1,2}(?:,[0-9]{1,2})?)?/g, "").trim(); } - var notify = new Notification(title, { - body: body, - icon: "img/logo-64.png", - tag: target - }); - notify.onclick = function() { - window.focus(); - button.click(); - this.close(); - }; - window.setTimeout(function() { - notify.close(); - }, 5 * 1000); + try { + var notify = new Notification(title, { + body: body, + icon: "img/logo-64.png", + tag: target + }); + notify.onclick = function() { + window.focus(); + button.click(); + this.close(); + }; + window.setTimeout(function() { + notify.close(); + }, 5 * 1000); + } catch (exception) { + // `new Notification(...)` is not supported and should be silenced. + } + } } } From dff1a48e05406eb12527506117fd0bb112505947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 8 Nov 2016 01:02:56 -0500 Subject: [PATCH 0107/3926] Prevent sound notification to throw an exception on mobile --- client/js/lounge.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 30186cd8..1eab796b 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -1011,7 +1011,11 @@ $(function() { if (msg.highlight || (options.notifyAllMessages && msg.type === "message")) { if (!document.hasFocus() || !$(target).hasClass("active")) { if (options.notification) { - pop.play(); + try { + pop.play(); + } catch (exception) { + // On mobile, sounds can not be played without user interaction. + } } toggleNotificationMarkers(true); From 9dd8a794e0448ccbfa72f69698b65ea767e450b1 Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Mon, 14 Nov 2016 19:27:11 -0800 Subject: [PATCH 0108/3926] Update link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 152a9cea..13dc6749 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ # The Lounge

- +

__What is it?__ From 2f77d6981b2f61aca907e43ccdbe546b5d3dfb6a Mon Sep 17 00:00:00 2001 From: William Boman Date: Tue, 15 Nov 2016 15:21:34 +0100 Subject: [PATCH 0109/3926] src/server: log config path on start-up --- src/server.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/server.js b/src/server.js index 8a8c9898..f4ed7c72 100644 --- a/src/server.js +++ b/src/server.js @@ -76,9 +76,10 @@ module.exports = function() { manager.sockets = sockets; - let protocol = config.https.enable ? "https" : "http"; - let host = config.host || "*"; + const protocol = config.https.enable ? "https" : "http"; + const host = config.host || "*"; log.info("The Lounge", Helper.getVersion(), "is now running"); + log.info(`Configuration file: ${Helper.CONFIG_PATH}`); log.info(`Available on: ${protocol}://${host}:${config.port}/ in ${config.public ? "public" : "private"} mode`); log.info("Press ctrl-c to stop\n"); From 6e1cdb370b43ac4bf6747e7201d7bf35f923fbaf Mon Sep 17 00:00:00 2001 From: William Boman Date: Wed, 16 Nov 2016 17:42:06 +0100 Subject: [PATCH 0110/3926] client: don't dismiss native web notifications programmatically after 5s --- client/js/lounge.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 1eab796b..f491462c 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -1041,14 +1041,11 @@ $(function() { icon: "img/logo-64.png", tag: target }); - notify.onclick = function() { + notify.addEventListener("click", function() { window.focus(); button.click(); this.close(); - }; - window.setTimeout(function() { - notify.close(); - }, 5 * 1000); + }); } catch (exception) { // `new Notification(...)` is not supported and should be silenced. } From b5db0abc18d60e117a03b08816419a55403a6dfd Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 18 Nov 2016 19:25:23 +0200 Subject: [PATCH 0111/3926] Print node version and platform --- src/server.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/server.js b/src/server.js index f4ed7c72..671402b4 100644 --- a/src/server.js +++ b/src/server.js @@ -10,6 +10,7 @@ var io = require("socket.io"); var dns = require("dns"); var Helper = require("./helper"); var ldap = require("ldapjs"); +var colors = require("colors/safe"); var manager = null; var authFunction = localAuth; @@ -78,10 +79,13 @@ module.exports = function() { const protocol = config.https.enable ? "https" : "http"; const host = config.host || "*"; - log.info("The Lounge", Helper.getVersion(), "is now running"); - log.info(`Configuration file: ${Helper.CONFIG_PATH}`); - log.info(`Available on: ${protocol}://${host}:${config.port}/ in ${config.public ? "public" : "private"} mode`); - log.info("Press ctrl-c to stop\n"); + + log.info(`The Lounge ${colors.green(Helper.getVersion())} is now running \ +using node ${colors.green(process.versions.node)} on ${colors.green(process.platform)} (${process.arch})`); + log.info(`Configuration file: ${colors.green(Helper.CONFIG_PATH)}`); + log.info(`Available on ${colors.green(protocol + "://" + host + ":" + config.port + "/")} \ +in ${config.public ? "public" : "private"} mode`); + log.info(`Press Ctrl-C to stop\n`); if (!config.public) { manager.loadUsers(); From f24f7071190a0e746e0b5d58695ab0802a1a1ea0 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 19 Nov 2016 10:24:39 +0200 Subject: [PATCH 0112/3926] Implement /away and /back commands --- client/js/lounge.js | 3 +++ src/client.js | 1 + src/plugins/inputs/away.js | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 src/plugins/inputs/away.js diff --git a/client/js/lounge.js b/client/js/lounge.js index 1eab796b..f27ff137 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -6,6 +6,8 @@ $(function() { var path = window.location.pathname + "socket.io/"; var socket = io({path: path}); var commands = [ + "/away", + "/back", "/close", "/connect", "/deop", @@ -15,6 +17,7 @@ $(function() { "/join", "/kick", "/leave", + "/me", "/mode", "/msg", "/nick", diff --git a/src/client.js b/src/client.js index 77db906b..427751af 100644 --- a/src/client.js +++ b/src/client.js @@ -39,6 +39,7 @@ var inputs = [ "msg", "part", "action", + "away", "connect", "disconnect", "invite", diff --git a/src/plugins/inputs/away.js b/src/plugins/inputs/away.js new file mode 100644 index 00000000..201559fe --- /dev/null +++ b/src/plugins/inputs/away.js @@ -0,0 +1,19 @@ +"use strict"; + +exports.commands = ["away", "back"]; + +exports.input = function(network, chan, cmd, args) { + if (cmd === "away") { + let reason = " "; + + if (args.length > 0) { + reason = args.join(" "); + } + + network.irc.raw("AWAY", reason); + + return; + } + + network.irc.raw("AWAY"); +}; From 6023035838dacb0c487246ab8aef6833a6df44d2 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 19 Nov 2016 10:49:16 +0200 Subject: [PATCH 0113/3926] Update depdencides to latest stable versions --- .eslintrc.yml | 1 + client/css/style.css | 4 ++-- client/js/lounge.js | 2 +- package.json | 34 +++++++++++++++++----------------- src/models/network.js | 2 +- src/server.js | 2 +- 6 files changed, 23 insertions(+), 22 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index de36b033..bb29017c 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -33,6 +33,7 @@ rules: no-trailing-spaces: 2 no-unsafe-negation: 2 no-useless-escape: 2 + no-useless-return: 2 object-curly-spacing: [2, never] quote-props: [2, as-needed] quotes: [2, double, avoid-escape] diff --git a/client/css/style.css b/client/css/style.css index ce41be48..52910002 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -25,8 +25,8 @@ font-weight: normal; font-style: normal; src: - url("../fonts/fontawesome-webfont.woff2?v=4.6.3") format("woff2"), - url("../fonts/fontawesome-webfont.woff?v=4.6.3") format("woff"); + url("../fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"), + url("../fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"); } html, diff --git a/client/js/lounge.js b/client/js/lounge.js index 1eab796b..1be5ea8b 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -512,7 +512,7 @@ $(function() { for (var i in options) { if (i === "userStyles") { - if (!/[\?&]nocss/.test(window.location.search)) { + if (!/[?&]nocss/.test(window.location.search)) { $(document.head).find("#user-specified-css").html(options[i]); } settings.find("#user-specified-css-input").val(options[i]); diff --git a/package.json b/package.json index 9b822647..f640b61e 100644 --- a/package.json +++ b/package.json @@ -41,31 +41,31 @@ }, "dependencies": { "bcrypt-nodejs": "0.0.3", - "cheerio": "0.20.0", + "cheerio": "0.22.0", "colors": "1.1.2", "commander": "2.9.0", - "event-stream": "3.3.2", - "express": "4.13.4", - "fs-extra": "0.30.0", + "event-stream": "3.3.4", + "express": "4.14.0", + "fs-extra": "1.0.0", "irc-framework": "2.5.0", - "lodash": "4.11.2", - "moment": "2.13.0", + "lodash": "4.17.2", + "moment": "2.16.0", "read": "1.0.7", - "request": "2.74.0", - "semver": "5.1.0", - "socket.io": "1.4.5", - "spdy": "3.3.2", - "ldapjs": "1.0.0" + "request": "2.79.0", + "semver": "5.3.0", + "socket.io": "1.5.1", + "spdy": "3.4.4", + "ldapjs": "1.0.1" }, "devDependencies": { "chai": "3.5.0", - "eslint": "3.6.0", - "font-awesome": "4.6.3", - "handlebars": "4.0.5", + "eslint": "3.10.2", + "font-awesome": "4.7.0", + "handlebars": "4.0.6", "istanbul": "0.4.5", - "mocha": "3.0.2", - "npm-run-all": "3.1.0", - "stylelint": "7.3.1", + "mocha": "3.1.2", + "npm-run-all": "3.1.1", + "stylelint": "7.5.0", "uglify-js": "2.7.3" } } diff --git a/src/models/network.js b/src/models/network.js index b180f523..0ddd3530 100644 --- a/src/models/network.js +++ b/src/models/network.js @@ -44,7 +44,7 @@ Network.prototype.setNick = function(nick) { "(?:^|[^a-z0-9]|\x03[0-9]{1,2})" + // Escape nickname, as it may contain regex stuff - nick.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&") + + _.escapeRegExp(nick) + // Do not match characters and numbers "(?:[^a-z0-9]|$)", diff --git a/src/server.js b/src/server.js index f4ed7c72..7d98ec9b 100644 --- a/src/server.js +++ b/src/server.js @@ -281,7 +281,7 @@ function localAuth(client, user, password, callback) { } function ldapAuth(client, user, password, callback) { - var userDN = user.replace(/([,\\\/#+<>;"= ])/g, "\\$1"); + var userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1"); var bindDN = Helper.config.ldap.primaryKey + "=" + userDN + "," + Helper.config.ldap.baseDN; var ldapclient = ldap.createClient({ From 2f363c98030b8e5280a5f24fd8d6539eaae0e70c Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 19 Nov 2016 12:05:46 +0200 Subject: [PATCH 0114/3926] Add lounge keyword --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 9b822647..7d03e036 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "prepublish": "npm run build" }, "keywords": [ + "lounge", "browser", "web", "chat", From f8e616ce2504df2f412943b12e482c29255041bb Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 19 Nov 2016 12:15:57 +0200 Subject: [PATCH 0115/3926] Remove errorneous classname from password field --- client/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/index.html b/client/index.html index 4226476f..a1a13c40 100644 --- a/client/index.html +++ b/client/index.html @@ -88,7 +88,7 @@
-
+
{{tz time}} diff --git a/client/views/msg_action.tpl b/client/views/msg_action.tpl index 9acc07d0..1735fa3f 100644 --- a/client/views/msg_action.tpl +++ b/client/views/msg_action.tpl @@ -1,4 +1,4 @@ -
+
{{tz time}} diff --git a/client/views/msg_unhandled.tpl b/client/views/msg_unhandled.tpl index 24838975..9c30f3d3 100644 --- a/client/views/msg_unhandled.tpl +++ b/client/views/msg_unhandled.tpl @@ -1,4 +1,4 @@ -
+
{{tz time}} From 06ecf625c6d27a25e4702dd93b2fa2f2e9f0b755 Mon Sep 17 00:00:00 2001 From: stepie22 Date: Fri, 25 Nov 2016 18:05:04 +0200 Subject: [PATCH 0121/3926] Fix date-marker not being removed when at the top of the page sometimes --- client/js/lounge.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index a665dca9..5940b2a9 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -420,9 +420,12 @@ $(function() { // Remove the date-change marker we put at the top, because it may // not actually be a date change now - var firstChild = $(chan).children().eq(0); - if (firstChild.attr("class") === "date-marker") { - firstChild.remove(); + var children = $(chan).children(); + if (children.eq(0).attr("class") === "date-marker") { // Check top most child + children.eq(0).remove(); + } else if (children.eq(0).attr("class") === "unread-marker" && children.eq(1).attr("class") === "date-marker") { + // Otherwise the date-marker would get 'stuck' because of the new-massages marker + children.eq(1).remove(); } // get the scrollable wrapper around messages From aa8e0ae2c2f7226d80cd8c6a676be1218c1a714f Mon Sep 17 00:00:00 2001 From: stepie22 Date: Fri, 25 Nov 2016 19:57:47 +0200 Subject: [PATCH 0122/3926] Set the (correct) time on link expands --- src/plugins/irc-events/link.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/irc-events/link.js b/src/plugins/irc-events/link.js index 8e7e900b..ff2ceac9 100644 --- a/src/plugins/irc-events/link.js +++ b/src/plugins/irc-events/link.js @@ -32,6 +32,7 @@ module.exports = function(irc, network) { var msg = new Msg({ self: data.nick === irc.user.nick, type: Msg.Type.TOGGLE, + time: data.time, // msg handles it if it isn't defined }); chan.pushMessage(client, msg); @@ -49,7 +50,8 @@ function parse(msg, url, res, client) { head: "", body: "", thumb: "", - link: url + link: url, + time: msg.time, }; switch (res.type) { From 8d31a0b312525335cc4f284ea303b84c9834b2d2 Mon Sep 17 00:00:00 2001 From: IlyaFinkelshteyn Date: Mon, 21 Nov 2016 19:02:56 -0800 Subject: [PATCH 0123/3926] Invalidate cache and use appveyor-retry with npm Return cache but invalidate it when needed Add appveyor-retry Remove spaces --- appveyor.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index cb2fec40..bc44f000 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,7 +12,7 @@ environment: install: - ps: Install-Product node $env:nodejs_version - - npm install + - appveyor-retry npm install - npm install mocha-appveyor-reporter - echo --reporter mocha-appveyor-reporter >> test/mocha.opts @@ -21,5 +21,9 @@ test_script: - npm --version - npm test +# cache npm modules +cache: + - '%AppData%\npm-cache -> package.json' + # Don't actually build build: off From 0d54879e45330c217793a595fc8e75b27bafcd8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Fri, 28 Oct 2016 20:13:54 -0400 Subject: [PATCH 0124/3926] Warn against running from source as root in README --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index afeb15d6..ff4ba2a8 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,7 @@ lounge --help ### Running from source -The following commands install the development version of The Lounge. A word of -caution: while it is the most recent codebase, this is not production-ready! +The following commands install the development version of The Lounge: ```sh git clone https://github.com/thelounge/lounge.git @@ -71,6 +70,12 @@ npm install npm start ``` +A word of caution: + +- While it is the most recent codebase, this is not production-ready! +- It is not recommended to run this as root. However, if you decide to do so, + you will have to run `npm run build`. + ## Development setup Simply follow the instructions to run The Lounge from source above, on your own From c1608520420b9261bf148d2fd33343fff61893b4 Mon Sep 17 00:00:00 2001 From: stepie22 Date: Mon, 28 Nov 2016 19:55:16 +0200 Subject: [PATCH 0125/3926] slight nit-pick --- client/js/lounge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 5940b2a9..dad513f4 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -424,7 +424,7 @@ $(function() { if (children.eq(0).attr("class") === "date-marker") { // Check top most child children.eq(0).remove(); } else if (children.eq(0).attr("class") === "unread-marker" && children.eq(1).attr("class") === "date-marker") { - // Otherwise the date-marker would get 'stuck' because of the new-massages marker + // Otherwise the date-marker would get 'stuck' because of the new-message marker children.eq(1).remove(); } From 10fefab2797d08957b420786601070b2ce2cbdda Mon Sep 17 00:00:00 2001 From: stepie22 Date: Thu, 1 Dec 2016 13:25:49 +0200 Subject: [PATCH 0126/3926] Switch to jQuery's hasClass instaid of checking direct class equality --- client/js/lounge.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index dad513f4..dea07450 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -421,9 +421,9 @@ $(function() { // Remove the date-change marker we put at the top, because it may // not actually be a date change now var children = $(chan).children(); - if (children.eq(0).attr("class") === "date-marker") { // Check top most child + if (children.eq(0).hasClass("date-marker")) { // Check top most child children.eq(0).remove(); - } else if (children.eq(0).attr("class") === "unread-marker" && children.eq(1).attr("class") === "date-marker") { + } else if (children.eq(0).hasClass("unread-marker") && children.eq(1).hasClass("date-marker")) { // Otherwise the date-marker would get 'stuck' because of the new-message marker children.eq(1).remove(); } From 69999f9190ad27c25ab209a115dbf34ad85efc33 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 19 Nov 2016 23:22:45 +0200 Subject: [PATCH 0127/3926] Change ghetto autoload to use fs.watch --- src/clientManager.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/clientManager.js b/src/clientManager.js index f9b2d043..f5afb9cf 100644 --- a/src/clientManager.js +++ b/src/clientManager.js @@ -140,9 +140,10 @@ ClientManager.prototype.removeUser = function(name) { return true; }; -ClientManager.prototype.autoload = function(/* sockets */) { +ClientManager.prototype.autoload = function() { var self = this; - setInterval(function() { + + fs.watch(Helper.USERS_PATH, _.debounce(() => { var loaded = self.clients.map(c => c.name); var added = _.difference(self.getUsers(), loaded); added.forEach(name => self.loadUser(name)); @@ -160,5 +161,5 @@ ClientManager.prototype.autoload = function(/* sockets */) { log.info("User '" + name + "' disconnected"); } }); - }, 1000); + }, 1000, {maxWait: 10000})); }; From a5ad573b2dde378dc32fb5172a51002ad1359bfe Mon Sep 17 00:00:00 2001 From: stepie22 Date: Tue, 22 Nov 2016 14:14:17 +0200 Subject: [PATCH 0128/3926] Sync reordering of channels/networks to other clients --- client/js/lounge.js | 51 +++++++++++++++++++++++++++++++++++++++++++++ src/client.js | 4 ++++ 2 files changed, 55 insertions(+) diff --git a/client/js/lounge.js b/client/js/lounge.js index dea07450..cdc84013 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -36,6 +36,8 @@ $(function() { var sidebar = $("#sidebar, #footer"); var chat = $("#chat"); + var ignoreSortSync = false; + var pop; try { pop = new Audio(); @@ -1370,6 +1372,8 @@ $(function() { order: order } ); + + ignoreSortSync = true; } }); sidebar.find(".network").sortable({ @@ -1396,10 +1400,57 @@ $(function() { order: order } ); + + ignoreSortSync = true; } }); } + socket.on("sync_sort", function(data) { + // Syncs the order of channels or networks when they are reordered + if (ignoreSortSync) { + ignoreSortSync = false; + return; // Ignore syncing because we 'caused' it + } + + var type = data.type; + var order = data.order; + + if (type === "networks") { + var container = $(".networks"); + + $.each(order, function(index, value) { + var position = $(container.children()[index]); + + if (position.data("id") === value) { // Network in correct place + return true; // No point in continuing + } + + var network = container.find("#network-" + value); + + $(network).insertBefore(position); + }); + } else if (type === "channels") { + var network = $("#network-" + data.target); + + $.each(order, function(index, value) { + if (index === 0) { // Shouldn't attempt to move lobby + return true; // same as `continue` -> skip to next item + } + + var position = $(network.children()[index]); // Target channel at position + + if (position.data("id") === value) { // Channel in correct place + return true; // No point in continuing + } + + var channel = network.find(".chan[data-id=" + value + "]"); // Channel at position + + $(channel).insertBefore(position); + }); + } + }); + function setNick(nick) { // Closes the nick editor when canceling, changing channel, or when a nick // is set in a different tab / browser / device. diff --git a/src/client.js b/src/client.js index 77db906b..e2027494 100644 --- a/src/client.js +++ b/src/client.js @@ -425,6 +425,10 @@ Client.prototype.sort = function(data) { } self.save(); + + // Sync order to connected clients + const syncOrder = sorted.map(obj => obj.id); + self.emit("sync_sort", {order: syncOrder, type: type, target: data.target}); }; Client.prototype.names = function(data) { From 62d4cd8fe824026f37c9564cd6b39f34c78a67de Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 9 Dec 2016 22:46:53 +0200 Subject: [PATCH 0129/3926] Use correct channel when pushing link prefetch messages Fixes #781 --- src/client.js | 1 - src/plugins/irc-events/link.js | 59 +++++++++++++------------------ src/plugins/irc-events/message.js | 7 ++-- test/plugins/link.js | 10 +++--- test/util.js | 4 +-- 5 files changed, 37 insertions(+), 44 deletions(-) diff --git a/src/client.js b/src/client.js index 77db906b..2e005624 100644 --- a/src/client.js +++ b/src/client.js @@ -24,7 +24,6 @@ var events = [ "mode", "motd", "message", - "link", "names", "nick", "part", diff --git a/src/plugins/irc-events/link.js b/src/plugins/irc-events/link.js index ff2ceac9..2502446d 100644 --- a/src/plugins/irc-events/link.js +++ b/src/plugins/irc-events/link.js @@ -1,45 +1,37 @@ "use strict"; -var cheerio = require("cheerio"); -var Msg = require("../../models/msg"); -var request = require("request"); -var Helper = require("../../helper"); -var es = require("event-stream"); +const cheerio = require("cheerio"); +const Msg = require("../../models/msg"); +const request = require("request"); +const Helper = require("../../helper"); +const es = require("event-stream"); process.setMaxListeners(0); -module.exports = function(irc, network) { - var client = this; - irc.on("privmsg", function(data) { - if (!Helper.config.prefetch) { - return; - } +module.exports = function(client, chan, originalMsg) { + if (!Helper.config.prefetch) { + return; + } - const links = data.message - .replace(/\x02|\x1D|\x1F|\x16|\x0F|\x03(?:[0-9]{1,2}(?:,[0-9]{1,2})?)?/g, "") - .split(" ") - .filter(w => /^https?:\/\//.test(w)); + const links = originalMsg.text + .replace(/\x02|\x1D|\x1F|\x16|\x0F|\x03(?:[0-9]{1,2}(?:,[0-9]{1,2})?)?/g, "") + .split(" ") + .filter(w => /^https?:\/\//.test(w)); - if (links.length === 0) { - return; - } + if (links.length === 0) { + return; + } - var chan = network.getChannel(data.target); - if (typeof chan === "undefined") { - return; - } + let msg = new Msg({ + type: Msg.Type.TOGGLE, + time: originalMsg.time, + self: originalMsg.self, + }); + chan.pushMessage(client, msg); - var msg = new Msg({ - self: data.nick === irc.user.nick, - type: Msg.Type.TOGGLE, - time: data.time, // msg handles it if it isn't defined - }); - chan.pushMessage(client, msg); - - var link = escapeHeader(links[0]); - fetch(link, function(res) { - parse(msg, link, res, client); - }); + const link = escapeHeader(links[0]); + fetch(link, function(res) { + parse(msg, link, res, client); }); }; @@ -51,7 +43,6 @@ function parse(msg, url, res, client) { body: "", thumb: "", link: url, - time: msg.time, }; switch (res.type) { diff --git a/src/plugins/irc-events/message.js b/src/plugins/irc-events/message.js index 5eb231fe..1107594a 100644 --- a/src/plugins/irc-events/message.js +++ b/src/plugins/irc-events/message.js @@ -1,7 +1,8 @@ "use strict"; -var Chan = require("../../models/chan"); -var Msg = require("../../models/msg"); +const Chan = require("../../models/chan"); +const Msg = require("../../models/msg"); +const LinkPrefetch = require("./link"); module.exports = function(irc, network) { var client = this; @@ -89,5 +90,7 @@ module.exports = function(irc, network) { highlight: highlight }); chan.pushMessage(client, msg, !self); + + LinkPrefetch(client, chan, msg); } }; diff --git a/test/plugins/link.js b/test/plugins/link.js index 5267dd58..4eab22f1 100644 --- a/test/plugins/link.js +++ b/test/plugins/link.js @@ -21,16 +21,16 @@ describe("Link plugin", function() { }); it("should be able to fetch basic information about URLs", function(done) { - link.call(this.irc, this.irc, this.network); + let message = this.irc.createMessage({ + text: "http://localhost:9002/basic" + }); + + link(this.irc, this.network.channels[0], message); this.app.get("/basic", function(req, res) { res.send("test"); }); - this.irc.createMessage({ - message: "http://localhost:9002/basic" - }); - this.irc.once("toggle", function(data) { assert.equal(data.head, "test"); done(); diff --git a/test/util.js b/test/util.js index 0b14f3f1..88e3b409 100644 --- a/test/util.js +++ b/test/util.js @@ -18,12 +18,12 @@ util.inherits(MockClient, EventEmitter); MockClient.prototype.createMessage = function(opts) { var message = _.extend({ - message: "dummy message", + text: "dummy message", nick: "test-user", target: "#test-channel" }, opts); - this.emit("privmsg", message); + return message; }; module.exports = { From 463a63aed3033459b9a4a635c07a131b783df580 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 19 Nov 2016 22:54:16 +0200 Subject: [PATCH 0130/3926] Avoid unnecessary disk writes if user object has not changed, make updateUser async --- src/client.js | 18 +++++++----------- src/clientManager.js | 34 ++++++++++++++++++++-------------- src/server.js | 4 +--- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/client.js b/src/client.js index 77db906b..d786bf88 100644 --- a/src/client.js +++ b/src/client.js @@ -292,19 +292,15 @@ Client.prototype.setPassword = function(hash, callback) { client.manager.updateUser(client.name, { token: token, password: hash - }); + }, function(err) { + if (err) { + log.error("Failed to update password of", client.name, err); + return callback(false); + } - // re-read the hash off disk to ensure we use whatever is saved. this will - // prevent situations where the password failed to save properly and so - // a restart of the server would forget the change and use the old - // password again. - var user = client.manager.readUserConfig(client.name); - if (user.password === hash) { client.config.password = hash; - callback(true); - } else { - callback(false); - } + return callback(true); + }); }); }; diff --git a/src/clientManager.js b/src/clientManager.js index f5afb9cf..8dd6af70 100644 --- a/src/clientManager.js +++ b/src/clientManager.js @@ -94,8 +94,8 @@ ClientManager.prototype.addUser = function(name, password) { return true; }; -ClientManager.prototype.updateUser = function(name, opts) { - var users = this.getUsers(); +ClientManager.prototype.updateUser = function(name, opts, callback) { + const users = this.getUsers(); if (users.indexOf(name) === -1) { return false; } @@ -103,19 +103,25 @@ ClientManager.prototype.updateUser = function(name, opts) { return false; } - var user = {}; - try { - user = this.readUserConfig(name); - _.assign(user, opts); - fs.writeFileSync( - Helper.getUserConfigPath(name), - JSON.stringify(user, null, "\t") - ); - } catch (e) { - log.error("Failed to update user", e); - return; + let user = this.readUserConfig(name); + const currentUser = JSON.stringify(user, null, "\t"); + _.assign(user, opts); + const newUser = JSON.stringify(user, null, "\t"); + + // Do not touch the disk if object has not changed + if (currentUser === newUser) { + return callback ? callback() : true; } - return true; + + fs.writeFile(Helper.getUserConfigPath(name), newUser, (err) => { + if (err) { + log.error("Failed to update user", err); + } + + if (callback) { + callback(err); + } + }); }; ClientManager.prototype.readUserConfig = function(name) { diff --git a/src/server.js b/src/server.js index d8cb0d9f..1e03e918 100644 --- a/src/server.js +++ b/src/server.js @@ -273,9 +273,7 @@ function localAuth(client, user, password, callback) { var hash = Helper.password.hash(password); client.setPassword(hash, function(success) { - if (!success) { - log.error("Failed to update password of", client.name, "to match new security requirements"); - } else { + if (success) { log.info("User", client.name, "logged in and their hashed password has been updated to match new security requirements"); } }); From 4fe3c5e96abadef1bb17350d9c38d690a474abc0 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 19 Nov 2016 23:00:54 +0200 Subject: [PATCH 0131/3926] Change ghetto timer to debounce --- src/client.js | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/client.js b/src/client.js index d786bf88..d217009c 100644 --- a/src/client.js +++ b/src/client.js @@ -460,23 +460,13 @@ Client.prototype.clientDetach = function(socketId) { delete this.attachedClients[socketId]; }; -var timer; -Client.prototype.save = function(force) { - var client = this; - +Client.prototype.save = _.debounce(function SaveClient() { if (Helper.config.public) { return; } - if (!force) { - clearTimeout(timer); - timer = setTimeout(function() { - client.save(true); - }, 1000); - return; - } - - var json = {}; + const client = this; + let json = {}; json.networks = this.networks.map(n => n.export()); client.manager.updateUser(client.name, json); -}; +}, 1000, {maxWait: 10000}); From 28056d678e1971cd69cf2e0d7119f80394ba0930 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sun, 20 Nov 2016 15:23:35 +0200 Subject: [PATCH 0132/3926] Correctly remove closed sockets from oident file, remove unused functions --- .eslintrc.yml | 4 +- src/oidentd.js | 56 ++++++++-------------------- src/plugins/irc-events/connection.js | 8 ++-- 3 files changed, 20 insertions(+), 48 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index bb29017c..1f4469f2 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -3,13 +3,11 @@ root: true env: + es6: true browser: true mocha: true node: true -parserOptions: - ecmaVersion: 6 - rules: block-scoped-var: 2 block-spacing: [2, always] diff --git a/src/oidentd.js b/src/oidentd.js index 80c42161..541b9772 100644 --- a/src/oidentd.js +++ b/src/oidentd.js @@ -1,68 +1,42 @@ "use strict"; -var fs = require("fs"); -var Helper = require("./helper"); +const fs = require("fs"); +const Helper = require("./helper"); function OidentdFile(file) { this.file = Helper.expandHome(file); this.connectionId = 0; - this.connections = {}; + this.connections = new Map(); this.refresh(); } OidentdFile.prototype = { - hookSocket: function(socket, user) { - var that = this; - var id = null; - - socket.on("connect", function() { - id = that.addSocket(socket, user); - that.refresh(); - }); - socket.on("close", function() { - that.removeConnection(id); - that.refresh(); - }); - }, - addSocket: function(socket, user) { - var id = this.connectionId++; - this.connections[id] = {socket: socket, user: user}; + const id = this.connectionId++; + + this.connections.set(id, {socket: socket, user: user}); + this.refresh(); + return id; }, - removeSocket: function(socket) { - for (var id in this.connections) { - if (this.connections[id] === socket) { - delete this.connections[id]; - break; - } - } - }, + removeSocket: function(id) { + this.connections.delete(id); - removeConnection: function(id) { - delete this.connections[id]; - }, - - getSockets: function() { - return this.connections; + this.refresh(); }, refresh: function() { - var file = "# Warning: file generated by The Lounge: changes will be overwritten!\n"; + let file = "# Warning: file generated by The Lounge: changes will be overwritten!\n"; - function makeRule(connection) { - return "to " + connection.socket.remoteAddress + this.connections.forEach((connection) => { + file += "to " + connection.socket.remoteAddress + " lport " + connection.socket.localPort + " from " + connection.socket.localAddress + " fport " + connection.socket.remotePort + " { reply \"" + connection.user + "\" }\n"; - } - - for (var id in this.connections) { - file += makeRule(this.connections[id]); - } + }); fs.writeFile(this.file, file, {flag: "w+"}, function(err) { if (err) { diff --git a/src/plugins/irc-events/connection.js b/src/plugins/irc-events/connection.js index 2d843086..d47c4cfe 100644 --- a/src/plugins/irc-events/connection.js +++ b/src/plugins/irc-events/connection.js @@ -70,14 +70,14 @@ module.exports = function(irc, network) { } if (identHandler) { + let identSocketId; + irc.on("socket connected", function() { - identHandler.addSocket(irc.connection.socket, client.name || network.username); - identHandler.refresh(); + identSocketId = identHandler.addSocket(irc.connection.socket, client.name || network.username); }); irc.on("socket close", function() { - identHandler.removeSocket(irc.connection.socket); - identHandler.refresh(); + identHandler.removeSocket(identSocketId); }); } From b01517861d5af76df93848f4a4916ed457c20165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 7 Dec 2016 00:50:11 -0500 Subject: [PATCH 0133/3926] Remove autoload option and always autoload users Since @xPaw provided a really nice way to watch user config files, there is now no need to be cheap about it (it used to be run every second, possibly why it could be disabled via settings?). This commit also improves the function a little bit by making use of ES6 syntax. A warning gets displayed on the server console when the `autoload` option is still present in the config file. --- defaults/config.js | 11 ---------- src/clientManager.js | 49 ++++++++++++++++++-------------------------- src/server.js | 7 ++++--- 3 files changed, 24 insertions(+), 43 deletions(-) diff --git a/defaults/config.js b/defaults/config.js index 22312601..b392b762 100644 --- a/defaults/config.js +++ b/defaults/config.js @@ -55,17 +55,6 @@ module.exports = { // theme: "themes/example.css", - // - // Autoload users - // - // When this setting is enabled, your 'users/' folder will be monitored. This is useful - // if you want to add/remove users while the server is running. - // - // @type boolean - // @default true - // - autoload: true, - // // Prefetch URLs // diff --git a/src/clientManager.js b/src/clientManager.js index 8dd6af70..e4350b87 100644 --- a/src/clientManager.js +++ b/src/clientManager.js @@ -26,11 +26,26 @@ ClientManager.prototype.findClient = function(name, token) { return false; }; -ClientManager.prototype.loadUsers = function() { - var users = this.getUsers(); - for (var i in users) { - this.loadUser(users[i]); - } +ClientManager.prototype.autoloadUsers = function() { + this.getUsers().forEach(name => this.loadUser(name)); + + fs.watch(Helper.USERS_PATH, _.debounce(() => { + const loaded = this.clients.map(c => c.name); + const updatedUsers = this.getUsers(); + + // New users created since last time users were loaded + _.difference(updatedUsers, loaded).forEach(name => this.loadUser(name)); + + // Existing users removed since last time users were loaded + _.difference(loaded, updatedUsers).forEach(name => { + const client = _.find(this.clients, {name: name}); + if (client) { + client.quit(); + this.clients = _.without(this.clients, client); + log.info("User '" + name + "' disconnected"); + } + }); + }, 1000, {maxWait: 10000})); }; ClientManager.prototype.loadUser = function(name) { @@ -145,27 +160,3 @@ ClientManager.prototype.removeUser = function(name) { } return true; }; - -ClientManager.prototype.autoload = function() { - var self = this; - - fs.watch(Helper.USERS_PATH, _.debounce(() => { - var loaded = self.clients.map(c => c.name); - var added = _.difference(self.getUsers(), loaded); - added.forEach(name => self.loadUser(name)); - - var removed = _.difference(loaded, self.getUsers()); - removed.forEach(name => { - var client = _.find( - self.clients, { - name: name - } - ); - if (client) { - client.quit(); - self.clients = _.without(self.clients, client); - log.info("User '" + name + "' disconnected"); - } - }); - }, 1000, {maxWait: 10000})); -}; diff --git a/src/server.js b/src/server.js index 62cea3a4..57aaae3c 100644 --- a/src/server.js +++ b/src/server.js @@ -88,10 +88,11 @@ in ${config.public ? "public" : "private"} mode`); log.info(`Press Ctrl-C to stop\n`); if (!config.public) { - manager.loadUsers(); - if (config.autoload) { - manager.autoload(); + if ("autoload" in config) { + log.warn(`Autoloading users is now always enabled. Please remove the ${colors.yellow("autoload")} option from your configuration file.`); } + + manager.autoloadUsers(); } }; From bc01d6ccd100b6c8df274dc365ee8869439bd55e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sun, 11 Dec 2016 03:29:09 -0500 Subject: [PATCH 0134/3926] Improve message and style of loading/unloading console logs, use ES6 template literals --- src/client.js | 3 ++- src/clientManager.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client.js b/src/client.js index 88efe83c..16df18e7 100644 --- a/src/client.js +++ b/src/client.js @@ -1,6 +1,7 @@ "use strict"; var _ = require("lodash"); +var colors = require("colors/safe"); var pkg = require("../package.json"); var Chan = require("./models/chan"); var crypto = require("crypto"); @@ -89,7 +90,7 @@ function Client(manager, name, config) { }); if (client.name) { - log.info("User '" + client.name + "' loaded"); + log.info(`User ${colors.bold(client.name)} loaded`); } } diff --git a/src/clientManager.js b/src/clientManager.js index e4350b87..fd5aefd0 100644 --- a/src/clientManager.js +++ b/src/clientManager.js @@ -1,6 +1,7 @@ "use strict"; var _ = require("lodash"); +var colors = require("colors/safe"); var fs = require("fs"); var Client = require("./client"); var Helper = require("./helper"); @@ -42,7 +43,7 @@ ClientManager.prototype.autoloadUsers = function() { if (client) { client.quit(); this.clients = _.without(this.clients, client); - log.info("User '" + name + "' disconnected"); + log.info(`User ${colors.bold(name)} disconnected and removed`); } }); }, 1000, {maxWait: 10000})); From bafe23b4b7766450f24c7432a9d5a2afa492c283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Fri, 9 Dec 2016 00:51:22 -0500 Subject: [PATCH 0135/3926] Fix wrong order between screenshot and badges on README Branch was not exactly up-to-date with `master` when merged, and badges were moved below the title in #713, so we missed that at review time. --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3041303a..f0b0d28d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,4 @@ # The Lounge -

- -

[![#thelounge IRC channel on freenode](https://img.shields.io/badge/freenode-%23thelounge-BA68C8.svg)](https://avatar.playat.ch:1000/) [![npm version](https://img.shields.io/npm/v/thelounge.svg)](https://www.npmjs.org/package/thelounge) @@ -37,6 +34,10 @@ This fork aims to be community managed, meaning that the decisions are taken in a collegial fashion, and that a bunch of maintainers should be able to make the review process quicker and more streamlined. +

+ +

+ ## Installation and usage The Lounge requires [Node.js](https://nodejs.org/) v4 or more recent. From 485fab6cd4ff1ce2e1d5f886901eb1ad2514e889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sun, 11 Dec 2016 04:14:17 -0500 Subject: [PATCH 0136/3926] Simplify introduction on README Features listed here are the same as on Shout repo, in same order, participating to the feeling of fork with nothing new. Instead of listing features here, we should refer to the website and improve it to make it as current as possible (there was some recent action there, and more coming, so it is reasonable to point there). Also, this "Why the fork?" section was useful right when we forked, but now it gives unnecessary and lengthy information (it is now the most verbose section of the README!). The Lounge has enough momentum as that point to be treated as its own project. Finally, shortening this section moves the screenshot back up on the page, and mobile view now has more context in the description. --- README.md | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index f0b0d28d..e0439fbc 100644 --- a/README.md +++ b/README.md @@ -7,37 +7,16 @@ [![Dependencies Status](https://img.shields.io/david/thelounge/lounge.svg)](https://david-dm.org/thelounge/lounge) [![Developer Dependencies Status](https://img.shields.io/david/dev/thelounge/lounge.svg)](https://david-dm.org/thelounge/lounge?type=dev) -__What is it?__ +The Lounge is a modern web IRC client designed for self-hosting. -The Lounge is a web IRC client that you host on your own server. - -*This is the official, community-managed fork of @erming's great initiative, the [Shout](https://github.com/erming/shout) project.* - -__What features does it have?__ - -- Multiple user support -- Stays connected even when you close the browser -- Connect from multiple devices at once -- Responsive layout — works well on your smartphone -- _.. and more!_ - -__Why the fork?__ - -We felt that the original [Shout](https://github.com/erming/shout) project -"stagnated" a little because its original author wanted it to remain his pet -project (which is a perfectly fine thing!). - -A bunch of people, excited about doing things a bit differently than the upstream -project forked it under a new name: “The Lounge”. - -This fork aims to be community managed, meaning that the decisions are taken -in a collegial fashion, and that a bunch of maintainers should be able to make -the review process quicker and more streamlined. +To learn more about configuration, usage and features of The Lounge, take a look at [the website](https://thelounge.github.io).

+The Lounge is the official and community-managed fork of [Shout](https://github.com/erming/shout), by [Mattias Erming](https://github.com/erming). + ## Installation and usage The Lounge requires [Node.js](https://nodejs.org/) v4 or more recent. From 2d1b33b9301d43813fc538e220f502cf67aa4ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sun, 11 Dec 2016 14:28:10 -0500 Subject: [PATCH 0137/3926] Bump irc-framework to bring a couple of fixes Diff at https://github.com/kiwiirc/irc-framework/compare/948a4b285f61d95dcb413f0a783f5c514f2672ae...90a69d300fa215d7c64f370e6f5168c5314164d7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7483c115..428d9a60 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "event-stream": "3.3.4", "express": "4.14.0", "fs-extra": "1.0.0", - "irc-framework": "2.5.0", + "irc-framework": "2.5.1", "lodash": "4.17.2", "moment": "2.16.0", "read": "1.0.7", From a8926e2ced625074adcb291b17394c8d40ff9f2d Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 10 Dec 2016 11:33:36 +0200 Subject: [PATCH 0138/3926] Stop refreshing the page on every socket.io error --- client/css/style.css | 22 ++++++++++++++++++++++ client/index.html | 1 + client/js/lounge.js | 43 +++++++++++++++++++++++++++++-------------- 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 43442956..e34f89f7 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1302,6 +1302,7 @@ button { -webkit-flex: 0 0 auto; flex: 0 0 auto; padding: 5px; + position: relative; } #windows #form .input { @@ -1317,6 +1318,27 @@ button { align-items: flex-end; } +#connection-error { + display: none; + align-items: center; + justify-content: center; + line-height: 1; + background: #f44336; + color: #fff; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + padding: 5px; + z-index: 30; + cursor: pointer; +} + +#connection-error.shown { + display: flex; +} + [contenteditable]:focus { outline: none; } diff --git a/client/index.html b/client/index.html index a1a13c40..01004283 100644 --- a/client/index.html +++ b/client/index.html @@ -58,6 +58,7 @@
+
Client connection lost. Click to reconnect.
+## v2.2.0 - 2017-01-XX + +For more details, [see the full changelog](https://github.com/thelounge/lounge/compare/v2.1.0...v2.2.0) and [milestone](https://github.com/thelounge/lounge/milestone/2). + +Another long-overdue release for The Lounge! + +On the client, it is now possible to generate URLs that pre-fill connection inputs in public mode, a date separator makes it into the chats, `/away` and `/back` commands are now supported, idle time gets displayed on `/whois`.
+Also, the client does not abruptly refresh when connection is lost anymore, and user search has been slightly improved. Note however that these last 2 items are still not optimal, but improvements are underway! + +On the server, more logging! The `debug` option is now an object instead of a boolean, so make sure to update your configuration file accordingly. More details [here](https://github.com/thelounge/lounge/blob/v2.2.0/defaults/config.js#L364-L383).
+There are changes revolving around user configuration autoloading: it has been greatly improved and therefore it is now enabled by default. Make sure to remove the `autoload` option from your configuration files. + +And of course, tons of fixes and less noticeable feature additions and changes, so make sure to check the full list below! + +### Added + +- Override network connection inputs with URL parameters ([#674](https://github.com/thelounge/lounge/pull/674) by [@MaxLeiter](https://github.com/MaxLeiter)) +- Add `id` to submit button ([#717](https://github.com/thelounge/lounge/pull/717) by [@xPaw](https://github.com/xPaw)) +- Add a UI element to cycle through nick completions on mobile ([#708](https://github.com/thelounge/lounge/pull/708) by [@astorije](https://github.com/astorije)) +- Report configuration file path, Node.js version and OS platform on server start-up ([#736](https://github.com/thelounge/lounge/pull/736) by [@williamboman](https://github.com/williamboman) and [#743](https://github.com/thelounge/lounge/pull/743) by [@xPaw](https://github.com/xPaw)) +- Add `lounge` keyword to npm registry ([#747](https://github.com/thelounge/lounge/pull/747) by [@xPaw](https://github.com/xPaw)) +- Add a date separator to channels/PMs ([#671](https://github.com/thelounge/lounge/pull/671) and [#765](https://github.com/thelounge/lounge/pull/765) by [@PolarizedIons](https://github.com/PolarizedIons)) +- Add support for hexip ilines and fix storing client IP address in configuration file ([#749](https://github.com/thelounge/lounge/pull/749) and [#822](https://github.com/thelounge/lounge/pull/822) by [@xPaw](https://github.com/xPaw)) +- Implement `/away` and `/back` commands ([#745](https://github.com/thelounge/lounge/pull/745) by [@xPaw](https://github.com/xPaw)) +- Remind channel name or nick in input placeholder ([#832](https://github.com/thelounge/lounge/pull/832) and [#889](https://github.com/thelounge/lounge/pull/889) by [@astorije](https://github.com/astorije)) +- Add human-readable idle time in whois info ([#721](https://github.com/thelounge/lounge/pull/721) by [@astorije](https://github.com/astorije)) +- Option to log raw IRC traffic ([#783](https://github.com/thelounge/lounge/pull/783) by [@astorije](https://github.com/astorije)) + +### Changed + +- Improve support for opening multiple clients at once ([#636](https://github.com/thelounge/lounge/pull/636) by [@xPaw](https://github.com/xPaw)) +- Match window title border line to text color ([#716](https://github.com/thelounge/lounge/pull/716) by [@xPaw](https://github.com/xPaw)) +- Focus input after chat form submit ([#483](https://github.com/thelounge/lounge/pull/483) by [@williamboman](https://github.com/williamboman)) +- Refactor user autoload to use `fs.watch` and make it more transparent in the app ([#751](https://github.com/thelounge/lounge/pull/751) by [@xPaw](https://github.com/xPaw) and [#779](https://github.com/thelounge/lounge/pull/779) by [@astorije](https://github.com/astorije)) +- Sync reordering of channels/networks to other clients in real-time ([#757](https://github.com/thelounge/lounge/pull/757) by [@PolarizedIons](https://github.com/PolarizedIons)) +- Do not accept empty password when adding new user ([#795](https://github.com/thelounge/lounge/pull/795) by [@MaxLeiter](https://github.com/MaxLeiter)) +- Stop refreshing the page on every socket.io error ([#784](https://github.com/thelounge/lounge/pull/784) by [@xPaw](https://github.com/xPaw)) +- Only append "says" to notifications if it is a message ([#805](https://github.com/thelounge/lounge/pull/805) by [@xPaw](https://github.com/xPaw)) +- Allow user search to find a pattern anywhere in the nicks ([#855](https://github.com/thelounge/lounge/pull/855) by [@MaxLeiter](https://github.com/MaxLeiter)) + +### Removed + +- Remove browser notification polyfill and inform user when unsupported ([#709](https://github.com/thelounge/lounge/pull/709) by [@astorije](https://github.com/astorije)) +- Remove erroneous classname from password field ([#748](https://github.com/thelounge/lounge/pull/748) by [@xPaw](https://github.com/xPaw)) +- Do not dismiss native web notifications programmatically after 5s ([#739](https://github.com/thelounge/lounge/pull/739) by [@williamboman](https://github.com/williamboman)) + +### Fixed + +- Fix `/mode` command to correctly assume target ([#679](https://github.com/thelounge/lounge/pull/679) by [@xPaw](https://github.com/xPaw)) +- Fix crash when LDAP server is unreachable ([#697](https://github.com/thelounge/lounge/pull/697) by [@gramakri](https://github.com/gramakri)) +- Fix channels behaving strangely while dragging ([#697](https://github.com/thelounge/lounge/pull/697) by [@PolarizedIons](https://github.com/PolarizedIons)) +- Fix unread counters resetting when they should not ([#720](https://github.com/thelounge/lounge/pull/720) by [@PolarizedIons](https://github.com/PolarizedIons)) +- Silence failures to trigger notifications when not available ([#732](https://github.com/thelounge/lounge/pull/732) by [@astorije](https://github.com/astorije)) +- Avoid unnecessary disk writes when saving user ([#750](https://github.com/thelounge/lounge/pull/750) by [@xPaw](https://github.com/xPaw)) +- Use correct channel when pushing link prefetch messages ([#782](https://github.com/thelounge/lounge/pull/782) by [@xPaw](https://github.com/xPaw)) +- Correctly remove closed sockets from oident file, remove unused functions ([#753](https://github.com/thelounge/lounge/pull/753) by [@xPaw](https://github.com/xPaw)) +- Do not automatically focus on touch devices ([#801](https://github.com/thelounge/lounge/pull/801) by [@xPaw](https://github.com/xPaw)) +- Strip control characters from notifications ([#818](https://github.com/thelounge/lounge/pull/818) by [@xPaw](https://github.com/xPaw)) +- Improve CLI a bit (output formatting and subcommand/option bug fix) ([#799](https://github.com/thelounge/lounge/pull/799) and [#868](https://github.com/thelounge/lounge/pull/868) by [@astorije](https://github.com/astorije)) +- Make HTML container take the entire screen estate ([#821](https://github.com/thelounge/lounge/pull/821) by [@xPaw](https://github.com/xPaw)) +- Fix unread marker being removed from DOM ([#820](https://github.com/thelounge/lounge/pull/820) by [@xPaw](https://github.com/xPaw)) +- Remove margin on date marker on smallest screen size ([#830](https://github.com/thelounge/lounge/pull/830) by [@xPaw](https://github.com/xPaw)) +- Do not ignore window opens when considering active channels ([#834](https://github.com/thelounge/lounge/pull/834) by [@xPaw](https://github.com/xPaw)) +- Calculate menu width on touch start ([#836](https://github.com/thelounge/lounge/pull/836) by [@xPaw](https://github.com/xPaw)) +- Increase IRC colors contrast ([#829](https://github.com/thelounge/lounge/pull/829) by [@xPaw](https://github.com/xPaw)) +- Do not prefetch URLs unless they are messages or `/me` actions ([#812](https://github.com/thelounge/lounge/pull/812) by [@birkof](https://github.com/birkof)) +- Bump `irc-framework` to bring a couple of fixes ([#790](https://github.com/thelounge/lounge/pull/790) by [@astorije](https://github.com/astorije), [#802](https://github.com/thelounge/lounge/pull/802) by [@xPaw](https://github.com/xPaw) and [#852](https://github.com/thelounge/lounge/pull/852) by [Greenkeeper](https://greenkeeper.io/)) + +### Security + +- Change bcrypt rounds from 8 to 11 ([#711](https://github.com/thelounge/lounge/pull/711) by [@xPaw](https://github.com/xPaw)) + +### Documentation + +In the main repository: + +- Warn against running from source as root in README ([#725](https://github.com/thelounge/lounge/pull/725) by [@astorije](https://github.com/astorije)) +- Add screenshot to README ([#694](https://github.com/thelounge/lounge/pull/694) by [@MaxLeiter](https://github.com/MaxLeiter)) +- Simplify introduction on README ([#789](https://github.com/thelounge/lounge/pull/789) by [@astorije](https://github.com/astorije)) + +On the website: + +- Remove distribution-specific install instructions of Node.js ([#49](https://github.com/thelounge/thelounge.github.io/pull/49) by [@astorije](https://github.com/astorije)) +- Remove wrong information about setting up password along with creating a user ([#50](https://github.com/thelounge/thelounge.github.io/pull/50) by [@astorije](https://github.com/astorije)) +- Update documentation of the configuration file ([#43](https://github.com/thelounge/thelounge.github.io/pull/43) by [@daftaupe](https://github.com/daftaupe)) +- Document the `/away` and `/back` commands ([#59](https://github.com/thelounge/thelounge.github.io/pull/59) by [@drkitty](https://github.com/drkitty)) + +### Internals + +- Fix AppVeyor cache never being successfully built and unblock AppVeyor ([#700](https://github.com/thelounge/lounge/pull/700) by [@astorije](https://github.com/astorije) and [#755](https://github.com/thelounge/lounge/pull/755) by [@IlyaFinkelshteyn](https://github.com/IlyaFinkelshteyn)) +- Add a simple (first) test for `localetime` Handlebars helper ([#703](https://github.com/thelounge/lounge/pull/703) by [@astorije](https://github.com/astorije)) +- Get rid of OSX CI builds until they get much faster ([#707](https://github.com/thelounge/lounge/pull/707) by [@astorije](https://github.com/astorije)) +- Update badges in README ([#713](https://github.com/thelounge/lounge/pull/713) by [@xPaw](https://github.com/xPaw) and [#780](https://github.com/thelounge/lounge/pull/780) by [@astorije](https://github.com/astorije)) +- Add Node.js v7, current stable, to Travis CI ([#800](https://github.com/thelounge/lounge/pull/800) by [@astorije](https://github.com/astorije)) +- Use Webpack to build our client code and dependencies ([#640](https://github.com/thelounge/lounge/pull/640) by [@nornagon](https://github.com/nornagon) and [#817](https://github.com/thelounge/lounge/pull/817) by [@xPaw](https://github.com/xPaw)) +- Switch `istanbul` code coverage CLI to more recent `nyc` one ([#850](https://github.com/thelounge/lounge/pull/850) by [@astorije](https://github.com/astorije)) +- Add web server tests ([#838](https://github.com/thelounge/lounge/pull/838) by [@xPaw](https://github.com/xPaw)) +- Fix stuff that breaks in jQuery 3 ([#854](https://github.com/thelounge/lounge/pull/854) by [@xPaw](https://github.com/xPaw)) +- Do not uglify builds when running start-dev ([#858](https://github.com/thelounge/lounge/pull/858) by [@xPaw](https://github.com/xPaw)) +- Update dependencies to latest stable versions ([#746](https://github.com/thelounge/lounge/pull/746) by [@xPaw](https://github.com/xPaw)) +- Update dependencies to enable Greenkeeper 🌴 ([#826](https://github.com/thelounge/lounge/pull/826) by [Greenkeeper](https://greenkeeper.io/)) +- Update `lodash` to the latest version 🚀 ([#840](https://github.com/thelounge/lounge/pull/840) and [#862](https://github.com/thelounge/lounge/pull/862) by [Greenkeeper](https://greenkeeper.io/)) +- Update `stylelint` to the latest version 🚀 ([#861](https://github.com/thelounge/lounge/pull/861) by [Greenkeeper](https://greenkeeper.io/)) +- Update `npm-run-all` to the latest version 🚀 ([#860](https://github.com/thelounge/lounge/pull/860) by [Greenkeeper](https://greenkeeper.io/)) +- Update `eslint` to the latest version 🚀 ([#875](https://github.com/thelounge/lounge/pull/875) by [Greenkeeper](https://greenkeeper.io/)) +- Update `babel-core` to the latest version 🚀 ([#883](https://github.com/thelounge/lounge/pull/883) by [Greenkeeper](https://greenkeeper.io/)) + ## v2.1.0 - 2016-10-17 [See the full changelog](https://github.com/thelounge/lounge/compare/v2.0.1...v2.1.0) From 4918fe699686d2be25e96a44f08abf69908fd665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 31 Jan 2017 21:08:41 -0500 Subject: [PATCH 0190/3926] 2.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3e8662f0..e597d9a0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "thelounge", "description": "The self-hosted Web IRC client", - "version": "2.1.0", + "version": "2.2.0", "preferGlobal": true, "bin": { "lounge": "index.js" From 0b9c8a3432c0be799d1ef07fcc25e0189ac5406e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 31 Jan 2017 21:20:35 -0500 Subject: [PATCH 0191/3926] Add nyc and Webpack config files to the files ignored when releasing Comparing https://unpkg.com/thelounge@2.1.0/ and https://unpkg.com/thelounge@2.2.0/, these have slipped in. --- .npmignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.npmignore b/.npmignore index 3ae7e661..e232345b 100644 --- a/.npmignore +++ b/.npmignore @@ -12,8 +12,9 @@ test/ .eslintrc.yml .gitattributes .gitignore -.istanbul.yml +.nycrc .npmignore .stylelintrc .travis.yml appveyor.yml +webpack.config.js From 7d910f305f42f01dc8e85ed3fd525d0896dcc2d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 31 Jan 2017 21:31:59 -0500 Subject: [PATCH 0192/3926] Fix date in v2.2.0 changelog entry, woops... --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8dc7388..184b3d08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ All sections are explained on the link above, they are all optional, and each of Don't forget to thank the PR authors in a commit comment, and copy/paste the release content as-is in GitHub releases: https://github.com/thelounge/lounge/releases --> -## v2.2.0 - 2017-01-XX +## v2.2.0 - 2017-01-31 For more details, [see the full changelog](https://github.com/thelounge/lounge/compare/v2.1.0...v2.2.0) and [milestone](https://github.com/thelounge/lounge/milestone/2). From 7fb3b0dfb06c8ce0d1f46729a2c2f9c58a4a3241 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 17 Jan 2017 11:13:24 +0000 Subject: [PATCH 0193/3926] chore(package): update npm-run-all to version 4.0.1 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 139c4331..fd83ddde 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "jquery-ui": "1.12.1", "mocha": "3.2.0", "mousetrap": "1.4.6", - "npm-run-all": "4.0.0", + "npm-run-all": "4.0.1", "nyc": "10.1.0", "socket.io-client": "1.7.2", "stylelint": "7.7.1", From 410eff428b3adffdb4152509995ad6252a92fdae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sun, 29 Jan 2017 14:33:57 -0500 Subject: [PATCH 0194/3926] Prompt admin for user log at user creation --- src/clientManager.js | 4 ++-- src/command-line/add.js | 21 +++++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/clientManager.js b/src/clientManager.js index fd5aefd0..fc681cd9 100644 --- a/src/clientManager.js +++ b/src/clientManager.js @@ -82,7 +82,7 @@ ClientManager.prototype.getUsers = function() { return users; }; -ClientManager.prototype.addUser = function(name, password) { +ClientManager.prototype.addUser = function(name, password, enableLog) { var users = this.getUsers(); if (users.indexOf(name) !== -1) { return false; @@ -96,7 +96,7 @@ ClientManager.prototype.addUser = function(name, password) { var user = { user: name, password: password || "", - log: false, + log: enableLog, networks: [] }; fs.writeFileSync( diff --git a/src/command-line/add.js b/src/command-line/add.js index 613a02bd..7c0214c5 100644 --- a/src/command-line/add.js +++ b/src/command-line/add.js @@ -24,17 +24,26 @@ program return; } if (!err) { - add(manager, name, password); + log.prompt({ + text: "Save logs to disk?", + default: "yes" + }, function(err2, enableLog) { + if (!err2) { + add( + manager, + name, + password, + enableLog.charAt(0).toLowerCase() === "y" + ); + } + }); } }); }); -function add(manager, name, password) { +function add(manager, name, password, enableLog) { var hash = Helper.password.hash(password); - manager.addUser( - name, - hash - ); + manager.addUser(name, hash, enableLog); log.info(`User ${colors.bold(name)} created.`); log.info(`User file located at ${colors.green(Helper.getUserConfigPath(name))}.`); From a74133ec6969d009d971d18a8b84deaf3e2fda45 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Thu, 2 Feb 2017 13:26:00 +0000 Subject: [PATCH 0195/3926] chore(package): update stylelint to version 7.8.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fd83ddde..aca15d22 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "npm-run-all": "4.0.1", "nyc": "10.1.0", "socket.io-client": "1.7.2", - "stylelint": "7.7.1", + "stylelint": "7.8.0", "urijs": "1.16.1", "webpack": "1.14.0" } From a8a1af4b627f0149f5b96c075c52bfc046406d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Thu, 2 Feb 2017 19:13:05 -0500 Subject: [PATCH 0196/3926] Update deprecated options and rules --- .stylelintrc | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.stylelintrc b/.stylelintrc index 5f32a858..cabb1fda 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -5,7 +5,7 @@ ], "rules": { "at-rule-empty-line-before": ["always", { - "except": ["blockless-group", "first-nested"], + "except": ["blockless-after-blockless", "first-nested"], "ignore": ["after-comment"] }], "block-closing-brace-newline-after": "always", @@ -20,7 +20,7 @@ "color-no-invalid-hex": true, "comment-empty-line-before": ["always", { "except": ["first-nested"], - "ignore": ["stylelint-commands"] + "ignore": ["stylelint-command"] }], "comment-whitespace-inside": "always", "declaration-bang-space-after": "never", @@ -59,8 +59,9 @@ "ignore": ["consecutive-duplicates"] }], "declaration-block-no-shorthand-property-overrides": true, - "rule-non-nested-empty-line-before": ["always-multi-line", { - "ignore": ["after-comment"] + "rule-empty-line-before": ["always-multi-line", { + "except": ["first-nested"], + "ignore": ["after-comment"], }], "declaration-block-trailing-semicolon": "always", "selector-combinator-space-after": "always", From 62c265337caa0d6777f84cb002afeb488bf07e06 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 3 Feb 2017 22:02:45 +0000 Subject: [PATCH 0197/3926] chore(package): update eslint to version 3.15.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fd83ddde..7d974444 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "babel-loader": "6.2.10", "babel-preset-es2015": "6.18.0", "chai": "3.5.0", - "eslint": "3.13.1", + "eslint": "3.15.0", "font-awesome": "4.7.0", "handlebars": "4.0.6", "handlebars-loader": "1.4.0", From 166e43ec3979002860a01c0e89995f5aec4dc86d Mon Sep 17 00:00:00 2001 From: Al McKinlay Date: Mon, 6 Feb 2017 12:41:17 +0000 Subject: [PATCH 0198/3926] Fix body height #821 fixed #793, but it appears since then, Chrome has changed the behaviour back to what it was befor ethe fix, so 2.2.0 on Chrome 56+ is even more broken. --- client/css/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/css/style.css b/client/css/style.css index 50b6d61f..11ccccd0 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -31,7 +31,7 @@ html, body { - height: 100vh; + height: 100%; } body { From e2abbff7b0f94bcddba519c1234473a7a86b691c Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Thu, 2 Feb 2017 20:52:37 +0000 Subject: [PATCH 0199/3926] fix(package): update irc-framework to version 2.5.5 https://greenkeeper.io/ --- package.json | 2 +- src/client.js | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index d1f3d1ef..5b225b68 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "event-stream": "3.3.4", "express": "4.14.0", "fs-extra": "1.0.0", - "irc-framework": "2.5.3", + "irc-framework": "2.5.5", "ldapjs": "1.0.1", "lodash": "4.17.4", "moment": "2.17.1", diff --git a/src/client.js b/src/client.js index ceee4088..111e353d 100644 --- a/src/client.js +++ b/src/client.js @@ -246,22 +246,7 @@ Client.prototype.connect = function(args) { } } - network.irc = new ircFramework.Client(); - - network.irc.requestCap([ - "echo-message", - "znc.in/self-message", - ]); - - events.forEach(plugin => { - var path = "./plugins/irc-events/" + plugin; - require(path).apply(client, [ - network.irc, - network - ]); - }); - - network.irc.connect({ + network.irc = new ircFramework.Client({ version: pkg.name + " " + Helper.getVersion() + " -- " + pkg.homepage, host: network.host, port: network.port, @@ -272,12 +257,27 @@ Client.prototype.connect = function(args) { tls: network.tls, localAddress: config.bind, rejectUnauthorized: false, + enable_echomessage: true, auto_reconnect: true, auto_reconnect_wait: 10000 + Math.floor(Math.random() * 1000), // If multiple users are connected to the same network, randomize their reconnections a little auto_reconnect_max_retries: 360, // At least one hour (plus timeouts) worth of reconnections webirc: webirc, }); + network.irc.requestCap([ + "znc.in/self-message", // Legacy echo-message for ZNc + ]); + + events.forEach(plugin => { + var path = "./plugins/irc-events/" + plugin; + require(path).apply(client, [ + network.irc, + network + ]); + }); + + network.irc.connect(); + client.save(); }; From ef0128fb005b98633f3f046a57381bae6a5f7d70 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 30 Jan 2017 10:05:50 +0000 Subject: [PATCH 0200/3926] chore(package): update urijs to version 1.18.6 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b225b68..e4269833 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "nyc": "10.1.0", "socket.io-client": "1.7.2", "stylelint": "7.8.0", - "urijs": "1.16.1", + "urijs": "1.18.6", "webpack": "1.14.0" } } From e9b4dd7c357c11e3c463f56c656edfe1746e2544 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sat, 28 Jan 2017 22:37:18 +0000 Subject: [PATCH 0201/3926] fix(package): update express to version 4.14.1 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b225b68..46928c96 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "colors": "1.1.2", "commander": "2.9.0", "event-stream": "3.3.4", - "express": "4.14.0", + "express": "4.14.1", "fs-extra": "1.0.0", "irc-framework": "2.5.5", "ldapjs": "1.0.1", From dfbe42b590d2d8a4a189f082bf985eaa40c43ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sun, 12 Feb 2017 16:54:01 -0500 Subject: [PATCH 0202/3926] Add change log entry for v2.2.1 --- CHANGELOG.md | 47 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 184b3d08..65688384 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,9 @@ Use the following template for each new release, built on recommendations from h ```md ## vX.Y.Z - YYYY-MM-DD -[See the full changelog](https://github.com/thelounge/lounge/compare/vPRE.VIO.US...vX.Y.Z) +For more details, [see the full changelog](https://github.com/thelounge/lounge/compare/vPRE.VIO.US...vX.Y.Z) and [milestone](https://github.com/thelounge/lounge/milestone/XXX). -OPTIONAL DESCRIPTION, ANNOUNCEMENT, ... +DESCRIPTION, ANNOUNCEMENT, ... ### Added ### Changed @@ -19,6 +19,10 @@ OPTIONAL DESCRIPTION, ANNOUNCEMENT, ... ### Removed ### Fixed ### Security +### Documentation +In the main repository: +On the website: +### Internals ``` All sections are explained on the link above, they are all optional, and each of them should contain a list of PRs formatted as such: @@ -26,10 +30,45 @@ All sections are explained on the link above, they are all optional, and each of ```md - Description ([#PR_NUMBER](https://github.com/thelounge/lounge/pull/PR_NUMBER) by [@GITHUB_USERNAME](https://github.com/GITHUB_USERNAME)) ``` - -Don't forget to thank the PR authors in a commit comment, and copy/paste the release content as-is in GitHub releases: https://github.com/thelounge/lounge/releases --> +## v2.2.1 - 2017-02-12 + +For more details, [see the full changelog](https://github.com/thelounge/lounge/compare/v2.2.0...v2.2.1) and [milestone](https://github.com/thelounge/lounge/milestone/10). + +This patch release packs up a change of the default value of `maxHistory`, an interactive prompt when creating a user to enable/disable user logging, a UI bug fix, and a few dependency upgrades. + +### Changed + +- Change default `maxHistory` to 10000 ([#899](https://github.com/thelounge/lounge/pull/899) by [@xPaw](https://github.com/xPaw)) +- Prompt admin for user log at user creation ([#903](https://github.com/thelounge/lounge/pull/903) by [@astorije](https://github.com/astorije)) +- Update `irc-framework` to the latest version 🚀 ([#902](https://github.com/thelounge/lounge/pull/902) by [Greenkeeper](https://greenkeeper.io/)) +- Update `urijs` to the latest version 🚀 ([#904](https://github.com/thelounge/lounge/pull/904) by [Greenkeeper](https://greenkeeper.io/)) +- Update `express` to the latest version 🚀 ([#898](https://github.com/thelounge/lounge/pull/898) by [Greenkeeper](https://greenkeeper.io/)) + +### Fixed + +- Fix body height, regression from v2.2.0 ([#913](https://github.com/thelounge/lounge/pull/913) by [@YaManicKill](https://github.com/YaManicKill)) + +### Documentation + +In the main repository: + +- Explain about `lounge` command in dev installations ([#887](https://github.com/thelounge/lounge/pull/887) by [@drkitty](https://github.com/drkitty)) + +On the website: + +- Port recent changes to `maxHistory` from default config file ([#60](https://github.com/thelounge/thelounge.github.io/pull/60) by [@astorije](https://github.com/astorije)) + +### Internals + +- Sort depedencies in `package.json` ([#896](https://github.com/thelounge/lounge/pull/896) by [@xPaw](https://github.com/xPaw)) +- Update `nyc` to the latest version 🚀 ([#882](https://github.com/thelounge/lounge/pull/882) by [Greenkeeper](https://greenkeeper.io/)) +- Update `npm-run-all` to the latest version 🚀 ([#880](https://github.com/thelounge/lounge/pull/880) by [Greenkeeper](https://greenkeeper.io/)) +- Add nyc and Webpack config files to the files ignored when releasing ([#906](https://github.com/thelounge/lounge/pull/906) by [@astorije](https://github.com/astorije)) +- Update `stylelint` to the latest version 🚀 ([#907](https://github.com/thelounge/lounge/pull/907) by [Greenkeeper](https://greenkeeper.io/)) +- Update `eslint` to the latest version 🚀 ([#910](https://github.com/thelounge/lounge/pull/910) by [Greenkeeper](https://greenkeeper.io/)) + ## v2.2.0 - 2017-01-31 For more details, [see the full changelog](https://github.com/thelounge/lounge/compare/v2.1.0...v2.2.0) and [milestone](https://github.com/thelounge/lounge/milestone/2). From 9e4708012d9c26b835a8f99573de731340f3463e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sun, 12 Feb 2017 16:54:09 -0500 Subject: [PATCH 0203/3926] 2.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 21485127..7777a7e3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "thelounge", "description": "The self-hosted Web IRC client", - "version": "2.2.0", + "version": "2.2.1", "preferGlobal": true, "bin": { "lounge": "index.js" From 15daca26858635e0cfc0c679ed94dfbcfadc6c44 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 17 Jan 2017 12:19:22 +0000 Subject: [PATCH 0204/3926] chore(package): update mousetrap to version 1.6.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7777a7e3..7710d871 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "jquery": "2.1.1", "jquery-ui": "1.12.1", "mocha": "3.2.0", - "mousetrap": "1.4.6", + "mousetrap": "1.6.0", "npm-run-all": "4.0.1", "nyc": "10.1.0", "socket.io-client": "1.7.2", From 2dbb56040c80aee9d550fe28fe6c84733675078c Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 16 Jan 2017 15:34:06 +0000 Subject: [PATCH 0205/3926] fix(package): update fs-extra to version 2.0.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7777a7e3..3b717b5e 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "commander": "2.9.0", "event-stream": "3.3.4", "express": "4.14.1", - "fs-extra": "1.0.0", + "fs-extra": "2.0.0", "irc-framework": "2.5.5", "ldapjs": "1.0.1", "lodash": "4.17.4", From 5e046a963bdddbaf53a1b3643cf5817e47b0d95e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sun, 12 Feb 2017 17:33:46 -0500 Subject: [PATCH 0206/3926] Fix `run_pr.sh` script `npm run build` is now mandatory. --- scripts/run-pr.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/run-pr.sh b/scripts/run-pr.sh index 09fae266..e8e65c5d 100755 --- a/scripts/run-pr.sh +++ b/scripts/run-pr.sh @@ -10,5 +10,6 @@ fi git fetch https://github.com/thelounge/lounge.git refs/pull/${1}/head git checkout FETCH_HEAD npm install +npm run build npm test || true npm start From dc3ebcf1bb8cd89288c2cabd01449b3fc7e6347f Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 13 Feb 2017 17:42:41 +0000 Subject: [PATCH 0207/3926] chore(package): update urijs to version 1.18.7 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fef01f2d..f927a486 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "nyc": "10.1.0", "socket.io-client": "1.7.2", "stylelint": "7.8.0", - "urijs": "1.18.6", + "urijs": "1.18.7", "webpack": "1.14.0" } } From 8d3c201a11ad44662b606a85cc7a61ecf9a73fdc Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sun, 12 Feb 2017 16:57:12 +0000 Subject: [PATCH 0208/3926] fix(package): update irc-framework to version 2.6.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fef01f2d..4af57185 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "event-stream": "3.3.4", "express": "4.14.1", "fs-extra": "2.0.0", - "irc-framework": "2.5.5", + "irc-framework": "2.6.0", "ldapjs": "1.0.1", "lodash": "4.17.4", "moment": "2.17.1", From e53c3ebe11250037cc5e63247268f81d5623240e Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 14 Feb 2017 01:19:52 +0000 Subject: [PATCH 0209/3926] chore(package): update babel-core to version 6.23.1 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 757dfc40..e03a5041 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "spdy": "3.4.4" }, "devDependencies": { - "babel-core": "6.22.1", + "babel-core": "6.23.1", "babel-loader": "6.2.10", "babel-preset-es2015": "6.18.0", "chai": "3.5.0", From 3127de7a81c84e43fa7234ef30b874dd2855cd1a Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 14 Feb 2017 09:20:06 +0000 Subject: [PATCH 0210/3926] chore(package): update babel-loader to version 6.3.1 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e03a5041..6d6c0013 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ }, "devDependencies": { "babel-core": "6.23.1", - "babel-loader": "6.2.10", + "babel-loader": "6.3.2", "babel-preset-es2015": "6.18.0", "chai": "3.5.0", "eslint": "3.15.0", From b7b12b9f750c045562a40fd1243623b7951f65a1 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 17 Feb 2017 16:34:26 +0200 Subject: [PATCH 0211/3926] update babel-preset-es2015 to version 6.22.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d6c0013..653f2ac7 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "devDependencies": { "babel-core": "6.23.1", "babel-loader": "6.3.2", - "babel-preset-es2015": "6.18.0", + "babel-preset-es2015": "6.22.0", "chai": "3.5.0", "eslint": "3.15.0", "font-awesome": "4.7.0", From 4786085ec4b1550d40ae49a822a8ace49037702e Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 17 Feb 2017 06:04:49 +0000 Subject: [PATCH 0212/3926] fix(package): update socket.io to version 1.7.3 https://greenkeeper.io/ --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 757dfc40..7e83c5bb 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "read": "1.0.7", "request": "2.79.0", "semver": "5.3.0", - "socket.io": "1.7.2", + "socket.io": "1.7.3", "spdy": "3.4.4" }, "devDependencies": { @@ -72,7 +72,7 @@ "mousetrap": "1.6.0", "npm-run-all": "4.0.1", "nyc": "10.1.0", - "socket.io-client": "1.7.2", + "socket.io-client": "1.7.3", "stylelint": "7.8.0", "urijs": "1.18.7", "webpack": "1.14.0" From e0111ed5876a64c62e6db1ddcaf28f6e5b3369e6 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 17 Feb 2017 17:01:20 +0200 Subject: [PATCH 0213/3926] Update to webpack 2 --- package.json | 2 +- webpack.config.js | 40 +++++++++++++++++++++------------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 757dfc40..54c39d00 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,6 @@ "socket.io-client": "1.7.2", "stylelint": "7.8.0", "urijs": "1.18.7", - "webpack": "1.14.0" + "webpack": "2.2.1" } } diff --git a/webpack.config.js b/webpack.config.js index e2f4b094..09c36561 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -26,17 +26,19 @@ let config = { publicPath: "/" }, module: { - loaders: [ + rules: [ { test: /\.js$/, include: [ path.resolve(__dirname, "client"), ], - loader: "babel", - query: { - presets: [ - "es2015" - ] + use: { + loader: "babel-loader", + options: { + presets: [ + "es2015" + ] + } } }, { @@ -44,14 +46,16 @@ let config = { include: [ path.resolve(__dirname, "client/views"), ], - loader: "handlebars-loader", - query: { - helperDirs: [ - path.resolve(__dirname, "client/js/libs/handlebars") - ], - extensions: [ - ".tpl" - ], + use: { + loader: "handlebars-loader", + options: { + helperDirs: [ + path.resolve(__dirname, "client/js/libs/handlebars") + ], + extensions: [ + ".tpl" + ], + } } }, ] @@ -66,13 +70,11 @@ let config = { // ********************************* if (process.env.NODE_ENV === "production") { - config.plugins.push(new webpack.optimize.DedupePlugin()); config.plugins.push(new webpack.optimize.UglifyJsPlugin({ - comments: false, - compress: { - warnings: false - } + comments: false })); +} else { + console.log("Building in development mode, bundles will not be minified."); } module.exports = config; From 78bb2edf1a90e576693dabe26a145fe5cd8c14c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sat, 18 Feb 2017 01:57:50 -0500 Subject: [PATCH 0214/3926] Make sure multiline chains of calls are correctly indented Without this rule, the following examples are passing ESLint: ```js foo() .bar(); // 0 tabs foo() .bar() // 2 tabs .baz(); // 1 tab foo() .bar() // 2 spaces .baz(); // 1 tab ``` --- .eslintrc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 016b18a4..d18619a9 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -17,7 +17,7 @@ rules: dot-notation: 2 eqeqeq: 2 handle-callback-err: 2 - indent: [2, tab] + indent: [2, tab, { "MemberExpression": 1 }] key-spacing: [2, {beforeColon: false, afterColon: true}] keyword-spacing: [2, {before: true, after: true}] linebreak-style: [2, unix] From db5afa0c1d605d6c5d82a76f65d14f8b137732b5 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 18 Feb 2017 20:41:27 +0200 Subject: [PATCH 0215/3926] Update to jQuery 3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1ba28a7d..14a6f65d 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "font-awesome": "4.7.0", "handlebars": "4.0.6", "handlebars-loader": "1.4.0", - "jquery": "2.1.1", + "jquery": "3.1.1", "jquery-ui": "1.12.1", "mocha": "3.2.0", "mousetrap": "1.6.0", From eec8dcd130f9163b93fa0150a229c42f11339af6 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sun, 19 Feb 2017 10:10:40 +0000 Subject: [PATCH 0216/3926] chore(package): update stylelint to version 7.9.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1ba28a7d..3a1bb688 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "npm-run-all": "4.0.1", "nyc": "10.1.0", "socket.io-client": "1.7.3", - "stylelint": "7.8.0", + "stylelint": "7.9.0", "urijs": "1.18.7", "webpack": "2.2.1" } From 54b26891a8e503a6bcf61369e0f38c5dd429e622 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Wed, 22 Feb 2017 22:48:30 +0000 Subject: [PATCH 0217/3926] chore(package): update npm-run-all to version 4.0.2 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3a1bb688..4bc23e39 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "jquery-ui": "1.12.1", "mocha": "3.2.0", "mousetrap": "1.6.0", - "npm-run-all": "4.0.1", + "npm-run-all": "4.0.2", "nyc": "10.1.0", "socket.io-client": "1.7.3", "stylelint": "7.9.0", From ffa0740b50977704cba026869f99cdc72af8e16c Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 24 Feb 2017 22:13:50 +0200 Subject: [PATCH 0218/3926] Disable (temporarily) client ping timeouts --- src/client.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client.js b/src/client.js index 111e353d..47023ed5 100644 --- a/src/client.js +++ b/src/client.js @@ -261,6 +261,7 @@ Client.prototype.connect = function(args) { auto_reconnect: true, auto_reconnect_wait: 10000 + Math.floor(Math.random() * 1000), // If multiple users are connected to the same network, randomize their reconnections a little auto_reconnect_max_retries: 360, // At least one hour (plus timeouts) worth of reconnections + ping_interval: 0, // Disable client ping timeouts due to buggy implementation webirc: webirc, }); From d3d374c12910d20acb5ba2a103599269e295b864 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 20 Feb 2017 16:35:09 +0000 Subject: [PATCH 0219/3926] chore(package): update eslint to version 3.16.1 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3a1bb688..98f45af0 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "babel-loader": "6.3.2", "babel-preset-es2015": "6.22.0", "chai": "3.5.0", - "eslint": "3.15.0", + "eslint": "3.16.1", "font-awesome": "4.7.0", "handlebars": "4.0.6", "handlebars-loader": "1.4.0", From 7fec8ebc72cb59ff66bd79207dde2e4a858f7231 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 27 Feb 2017 20:32:07 +0000 Subject: [PATCH 0220/3926] chore(package): update urijs to version 1.18.8 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b35216f7..ad145e06 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "nyc": "10.1.0", "socket.io-client": "1.7.3", "stylelint": "7.9.0", - "urijs": "1.18.7", + "urijs": "1.18.8", "webpack": "2.2.1" } } From 86ed0b6e59f1e53b9967f3344b4dd68de0cd6ca2 Mon Sep 17 00:00:00 2001 From: Matthew Saunders Date: Fri, 17 Feb 2017 22:23:01 -0500 Subject: [PATCH 0221/3926] Update arg parsing and default 'lounge' to 'lounge --help' --- package.json | 2 +- src/command-line/index.js | 12 +++++------- src/command-line/start.js | 10 +++++----- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 4bc23e39..e51320e5 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "homepage": "https://thelounge.github.io/", "scripts": { "coverage": "nyc mocha", - "start": "node index", + "start": "node index start", "start-dev": "npm-run-all --parallel watch start", "build": "npm-run-all build:*", "build:font-awesome": "node scripts/build-fontawesome.js", diff --git a/src/command-line/index.js b/src/command-line/index.js index 1149f939..46ba56cb 100644 --- a/src/command-line/index.js +++ b/src/command-line/index.js @@ -9,11 +9,9 @@ var fsextra = require("fs-extra"); var path = require("path"); var Helper = require("../helper"); -program.version(Helper.getVersion(), "-v, --version"); -program.option(""); -program.option(" --home " , "home path"); - -var argv = program.parseOptions(process.argv); +program.version(Helper.getVersion(), "-v, --version") + .option("-h, --home ", "path to configuration folder") + .parseOptions(process.argv); Helper.setHome(program.home || process.env.LOUNGE_HOME); @@ -40,8 +38,8 @@ require("./remove"); require("./reset"); require("./edit"); -program.parse(argv.args); +program.parse(process.argv); if (!program.args.length) { - program.parse(process.argv.concat("start")); + program.help(); } diff --git a/src/command-line/start.js b/src/command-line/start.js index 9a5fdbbd..f1c6ca10 100644 --- a/src/command-line/start.js +++ b/src/command-line/start.js @@ -8,11 +8,11 @@ var Helper = require("../helper"); program .command("start") - .option("-H, --host ", "host") - .option("-P, --port ", "port") - .option("-B, --bind ", "bind") - .option(" --public", "mode") - .option(" --private", "mode") + .option("-H, --host ", "set the IP address or hostname for the web server to listen on") + .option("-P, --port ", "set the port to listen on") + .option("-B, --bind ", "set the local IP to bind to for outgoing connections") + .option(" --public", "start in public mode") + .option(" --private", "start in private mode") .description("Start the server") .action(function(options) { var users = new ClientManager().getUsers(); From a3c05121430e82eb7f192c9337e4081a8a3481ff Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 6 Mar 2017 10:58:57 +0000 Subject: [PATCH 0222/3926] chore(package): update urijs to version 1.18.9 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ad145e06..193f74bb 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "nyc": "10.1.0", "socket.io-client": "1.7.3", "stylelint": "7.9.0", - "urijs": "1.18.8", + "urijs": "1.18.9", "webpack": "2.2.1" } } From a85b7a789e29ffa46f30cfedec1231cbf89f722b Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 6 Mar 2017 12:27:22 +0000 Subject: [PATCH 0223/3926] chore(package): update babel-loader to version 6.4.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ad145e06..ceff07d1 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ }, "devDependencies": { "babel-core": "6.23.1", - "babel-loader": "6.3.2", + "babel-loader": "6.4.0", "babel-preset-es2015": "6.22.0", "chai": "3.5.0", "eslint": "3.16.1", From 9f053dfaaea439506fc78e26fc608709f701b9bf Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 3 Mar 2017 22:44:15 +0000 Subject: [PATCH 0224/3926] chore(package): update eslint to version 3.17.1 https://greenkeeper.io/ --- .eslintrc.yml | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index d18619a9..d6b6a59e 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -21,6 +21,7 @@ rules: key-spacing: [2, {beforeColon: false, afterColon: true}] keyword-spacing: [2, {before: true, after: true}] linebreak-style: [2, unix] + no-compare-neg-zero: 2 no-console: 0 no-control-regex: 0 no-else-return: 2 diff --git a/package.json b/package.json index 193f74bb..882138ba 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "babel-loader": "6.3.2", "babel-preset-es2015": "6.22.0", "chai": "3.5.0", - "eslint": "3.16.1", + "eslint": "3.17.1", "font-awesome": "4.7.0", "handlebars": "4.0.6", "handlebars-loader": "1.4.0", From 30b86a8b4065578f16e2eedd0e679b17c148df92 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sat, 4 Mar 2017 04:54:00 +0000 Subject: [PATCH 0225/3926] fix(package): update request to version 2.81.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 193f74bb..0026b262 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "lodash": "4.17.4", "moment": "2.17.1", "read": "1.0.7", - "request": "2.79.0", + "request": "2.81.0", "semver": "5.3.0", "socket.io": "1.7.3", "spdy": "3.4.4" From 780657900924b0244cdc4dbddef5145f2729b99e Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sat, 11 Mar 2017 01:12:13 +0000 Subject: [PATCH 0226/3926] fix(package): update irc-framework to version 2.6.1 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 91dde911..50fc6db5 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "event-stream": "3.3.4", "express": "4.14.1", "fs-extra": "2.0.0", - "irc-framework": "2.6.0", + "irc-framework": "2.6.1", "ldapjs": "1.0.1", "lodash": "4.17.4", "moment": "2.17.1", From 0f9b12f2b8d547657f193881605ba88c81dab60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 28 Feb 2017 02:15:34 -0500 Subject: [PATCH 0227/3926] Add a help window within the app This brings commands and keyboard shortcuts from the website, after a massive overhaul. It comes as part of the big documentation rewrite that I am currently doing. `kbd` design inspiration from GitHub, `code` design inspiration from Bootstrap. This help page is accessible from an icon in the sidebar, near the Settings icon. --- client/css/style.css | 67 +++++++- client/index.html | 332 +++++++++++++++++++++++++++++++++++++++ client/themes/crypto.css | 13 +- 3 files changed, 408 insertions(+), 4 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 11ccccd0..b3293a87 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -57,7 +57,8 @@ a:hover { } h1, -h2 { +h2, +h3 { font: inherit; line-height: inherit; margin: 0; @@ -79,6 +80,33 @@ button { padding: 0; } +code, +kbd { + font-family: Consolas, Menlo, Monaco, "Lucida Console", "DejaVu Sans Mono", "Courier New", monospace; +} + +code { + font-size: 12px; + padding: 2px 4px; + color: #e74c3c; + background-color: #f9f2f4; + border-radius: 2px; +} + +kbd { + display: inline-block; + font-size: 11px; + line-height: 10px; + padding: 3px 5px; + color: #555; + vertical-align: middle; + background-color: #fcfcfc; + border: solid 1px #ccc; + border-bottom-color: #bbb; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #bbb; +} + .btn { border: 2px solid #84ce88; border-radius: 3px; @@ -185,6 +213,7 @@ button { #footer .sign-in:before { content: "\f023"; /* http://fontawesome.io/icon/lock/ */ } #footer .connect:before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ } #footer .settings:before { content: "\f013"; /* http://fontawesome.io/icon/cog/ */ } +#footer .help:before { content: "\f059"; /* http://fontawesome.io/icon/question/ */ } #footer .sign-out:before { content: "\f011"; /* http://fontawesome.io/icon/power-off/ */ } #form #cycle-nicks:before { content: "\f007"; /* http://fontawesome.io/icon/user/ */ } @@ -656,6 +685,12 @@ button { padding-bottom: 7px; } +#windows .window h3 { + color: #7f8c8d; + font-size: 18px; + margin: 20px 0 10px; +} + #windows .active { display: block; } @@ -1296,6 +1331,27 @@ button { margin-top: .2em; } +#help .help-item { + display: table-row; +} + +#help .help-item .subject, +#help .help-item .description { + display: table-cell; + padding-bottom: 15px; +} + +#help .help-item .subject { + white-space: nowrap; + padding-right: 10px; + min-width: 150px; +} + +#help .help-item .description p { + font-size: 14px; + margin-bottom: 0; +} + #form { background: #eee; border-top: 1px solid #ddd; @@ -1813,6 +1869,15 @@ button { #chat .unread-marker { margin: 0; } + + #help .help-item .subject { + display: inline-block; + padding-bottom: 4px; + } + + #help .help-item .description { + display: block; + } } ::-webkit-scrollbar { diff --git a/client/index.html b/client/index.html index de9311d7..82f9d0d6 100644 --- a/client/index.html +++ b/client/index.html @@ -37,6 +37,7 @@ +
@@ -377,6 +378,337 @@
+ +
+
+ +
+
+

Help

+ +

Keyboard Shortcuts

+ +

On Windows / Linux

+ +
+
+ Ctrl + / +
+
+

Switch to the previous/next window in the channel list

+
+
+ +
+
+ Ctrl + Shift + L +
+
+

Clear the current screen

+
+
+ +

On macOS

+ +
+
+ + / +
+
+

Switch to the previous/next window in the channel list

+
+
+ +
+
+ + K +
+
+

Clear the current screen

+
+
+ +

Commands

+ +
+
+ /away [message] +
+
+

Mark yourself as away with an optional message.

+
+
+ +
+
+ /back +
+
+

Remove your away status (set with /away).

+
+
+ +
+
+ /clear +
+
+

Clear the current screen.

+
+
+ +
+
+ /connect host [port] +
+
+

+ Connect to a new IRC network. If port starts with + a + sign, the connection will be made secure + using TLS. +

+

Alias: /server

+
+
+ +
+
+ /ctcp target cmd [args] +
+
+

+ Send a CTCP + request. Read more about this on + the dedicated Wikipedia article. +

+
+
+ +
+
+ /deop nick [...nick] +
+
+

+ Remove op (-o) from one or several users in the + current channel. +

+
+
+ +
+
+ /devoice nick [...nick] +
+
+

+ Remove voice (-v) from one or several users in + the current channel. +

+
+
+ +
+
+ /disconnect [message] +
+
+

+ Disconnect from the current network with an + optionally-provided message. +

+
+
+ +
+
+ /invite nick [channel] +
+
+

+ Invite a user to the specified channel. If + channel is ommitted, user will be invited to the + current channel. +

+
+
+ +
+
+ /join channel +
+
+

Join a channel.

+
+
+ +
+
+ /kick nick +
+
+

Kick a user from the current channel.

+
+
+ +
+
+ /list +
+
+

Retrieve a list of available channels on this network.

+
+
+ +
+
+ /me message +
+
+

+ Send an action message to the current channel. The Lounge will + display it inline, as if the message was posted in the third + person. +

+
+
+ +
+
+ /mode flags [args] +
+
+

+ Set the given flags to the current channel if the active + window is a channel, another user if the active window is a + private message window, or yourself if the current window is a + server window. +

+
+
+ +
+
+ /msg channel message +
+
+

Send a message to the specified channel.

+
+
+ +
+
+ /nick newnick +
+
+

Change your nickname on the current network.

+
+
+ +
+
+ /notice channel message +
+
+

Sends a notice message to the specified channel.

+
+
+ +
+
+ /op nick [...nick] +
+
+

+ Give op (+o) to one or several users in the + current channel. +

+
+
+ +
+
+ /part +
+
+

Close the current channel or private message window.

+

Aliases: /close, /leave

+
+
+ +
+
+ /query nick +
+
+

Send a private message to the specified user.

+
+
+ +
+
+ /quit [message] +
+
+

+ Disconnect from the current network with an optional message. +

+
+
+ +
+
+ /raw message +
+
+

Send a raw message to the current IRC network.

+

Aliases: /quote, /send

+
+
+ +
+
+ /slap nick +
+
+

Slap someone in the current channel with a trout!

+
+
+ +
+
+ /topic newtopic +
+
+

Set the topic in the current channel.

+
+
+ +
+
+ /voice nick [...nick] +
+
+

+ Give voice (+v) to one or several users in the + current channel. +

+
+
+ +
+
+ /whois nick +
+
+

+ Retrieve information about the given user on the current + network. +

+
+
+
+
diff --git a/client/themes/crypto.css b/client/themes/crypto.css index 07515ae6..96df390f 100644 --- a/client/themes/crypto.css +++ b/client/themes/crypto.css @@ -18,6 +18,11 @@ body { font: 16px Inconsolata-g, monospace; } +code, +kbd { + font-family: Inconsolata-g, monospace; +} + a, #chat a { color: #00ff0e; @@ -28,9 +33,12 @@ a:hover, color: #3eff48; } -#windows .window h2 { +#windows .window h2, +#windows .window h3 { color: #666; - font: regular 14px Lato, sans-serif; +} + +#windows .window h2 { border-bottom: none; } @@ -39,7 +47,6 @@ a:hover, } #sign-in label { - font: 14px Lato, sans-serif; color: #666; } From c1fc18564392d0694a3cbb4f36393db7a9d07f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 28 Feb 2017 02:21:26 -0500 Subject: [PATCH 0228/3926] Move "About The Lounge" section from Settings to Help window --- client/css/style.css | 16 +++++----------- client/index.html | 34 ++++++++++++++++------------------ 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index b3293a87..6c5ac484 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1289,15 +1289,10 @@ kbd { margin: 4px 10px 0 0; } -#settings .about, #settings #play { color: #7f8c8d; } -#settings .about small { - margin-left: 2px; -} - #settings #play { font-size: 14px; transition: opacity .2s; @@ -1307,12 +1302,6 @@ kbd { opacity: .8; } -#settings .about { - font-size: 14px; - padding-top: 2px; - line-height: 1.8; -} - #settings #change-password .error, #settings #change-password .success { margin-bottom: 1em; @@ -1352,6 +1341,11 @@ kbd { margin-bottom: 0; } +#help .about { + font-size: 14px; + line-height: 1.8; +} + #form { background: #eee; border-top: 1px solid #ddd; diff --git a/client/index.html b/client/index.html index 82f9d0d6..0ec349a6 100644 --- a/client/index.html +++ b/client/index.html @@ -357,24 +357,6 @@
-
-

About The Lounge

-
-
-

- <% if (gitCommit) { %> - The Lounge is running from source - (<%= gitCommit %>).
- <% } else { %> - The Lounge is in version <%= version %> - (See release notes).
- <% } %> - - Website
- Documentation
- Report a bug -

-
@@ -707,6 +689,22 @@

+ +

About The Lounge

+ +

+ <% if (gitCommit) { %> + The Lounge is running from source + (<%= gitCommit %>).
+ <% } else { %> + The Lounge is in version <%= version %> + (See release notes).
+ <% } %> + + Website
+ Documentation
+ Report a bug +

From 7c25a9e7f06395556ffcf8948ccf773776ac95ec Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 11 Mar 2017 11:20:40 +0200 Subject: [PATCH 0229/3926] Update express and nyc to latest versions --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 50fc6db5..a9134fbd 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "colors": "1.1.2", "commander": "2.9.0", "event-stream": "3.3.4", - "express": "4.14.1", + "express": "4.15.2", "fs-extra": "2.0.0", "irc-framework": "2.6.1", "ldapjs": "1.0.1", @@ -71,7 +71,7 @@ "mocha": "3.2.0", "mousetrap": "1.6.0", "npm-run-all": "4.0.2", - "nyc": "10.1.0", + "nyc": "10.1.2", "socket.io-client": "1.7.3", "stylelint": "7.9.0", "urijs": "1.18.9", From 23599fc39bbbc93be4c13de160535633a78cfd42 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 11 Mar 2017 20:09:37 +0200 Subject: [PATCH 0230/3926] Prevent message sending in lobbies Fixes #956 --- src/client.js | 8 ++++++++ src/plugins/inputs/action.js | 12 ++++++++++++ src/plugins/inputs/invite.js | 8 ++++++-- src/plugins/inputs/kick.js | 12 ++++++++++++ src/plugins/inputs/topic.js | 12 ++++++++++++ 5 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/client.js b/src/client.js index 47023ed5..b7e1b3ef 100644 --- a/src/client.js +++ b/src/client.js @@ -335,6 +335,14 @@ Client.prototype.inputLine = function(data) { // This is either a normal message or a command escaped with a leading '/' if (text.charAt(0) !== "/" || text.charAt(1) === "/") { + if (target.chan.type === Chan.Type.LOBBY) { + target.chan.pushMessage(this, new Msg({ + type: Msg.Type.ERROR, + text: "Messages can not be sent to lobbies." + })); + return; + } + text = "say " + text.replace(/^\//, ""); } else { text = text.substr(1); diff --git a/src/plugins/inputs/action.js b/src/plugins/inputs/action.js index 4b625854..ceb7a878 100644 --- a/src/plugins/inputs/action.js +++ b/src/plugins/inputs/action.js @@ -1,8 +1,20 @@ "use strict"; +var Chan = require("../../models/chan"); +var Msg = require("../../models/msg"); + exports.commands = ["slap", "me"]; exports.input = function(network, chan, cmd, args) { + if (chan.type !== Chan.Type.CHANNEL && chan.type !== Chan.Type.QUERY) { + chan.pushMessage(this, new Msg({ + type: Msg.Type.ERROR, + text: `${cmd} command can only be used in channels and queries.` + })); + + return; + } + var irc = network.irc; var text; diff --git a/src/plugins/inputs/invite.js b/src/plugins/inputs/invite.js index f7ac7a07..09ba427c 100644 --- a/src/plugins/inputs/invite.js +++ b/src/plugins/inputs/invite.js @@ -1,6 +1,7 @@ "use strict"; var Chan = require("../../models/chan"); +var Msg = require("../../models/msg"); exports.commands = ["invite"]; @@ -11,7 +12,10 @@ exports.input = function(network, chan, cmd, args) { irc.raw("INVITE", args[0], args[1]); // Channel provided in the command } else if (args.length === 1 && chan.type === Chan.Type.CHANNEL) { irc.raw("INVITE", args[0], chan.name); // Current channel + } else { + chan.pushMessage(this, new Msg({ + type: Msg.Type.ERROR, + text: `${cmd} command can only be used in channels or by specifying a target.` + })); } - - return true; }; diff --git a/src/plugins/inputs/kick.js b/src/plugins/inputs/kick.js index 2ce955a1..dcb55548 100644 --- a/src/plugins/inputs/kick.js +++ b/src/plugins/inputs/kick.js @@ -1,8 +1,20 @@ "use strict"; +var Chan = require("../../models/chan"); +var Msg = require("../../models/msg"); + exports.commands = ["kick"]; exports.input = function(network, chan, cmd, args) { + if (chan.type !== Chan.Type.CHANNEL) { + chan.pushMessage(this, new Msg({ + type: Msg.Type.ERROR, + text: `${cmd} command can only be used in channels.` + })); + + return; + } + if (args.length !== 0) { var irc = network.irc; irc.raw("KICK", chan.name, args[0], args.slice(1).join(" ")); diff --git a/src/plugins/inputs/topic.js b/src/plugins/inputs/topic.js index 1d403eb3..8e2327cc 100644 --- a/src/plugins/inputs/topic.js +++ b/src/plugins/inputs/topic.js @@ -1,8 +1,20 @@ "use strict"; +var Chan = require("../../models/chan"); +var Msg = require("../../models/msg"); + exports.commands = ["topic"]; exports.input = function(network, chan, cmd, args) { + if (chan.type !== Chan.Type.CHANNEL) { + chan.pushMessage(this, new Msg({ + type: Msg.Type.ERROR, + text: `${cmd} command can only be used in channels.` + })); + + return; + } + var irc = network.irc; irc.raw("TOPIC", chan.name, args.join(" ")); From f2e43b84bedf7542b68c9b87668ab1372b9ab233 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 17 Dec 2016 22:03:12 +0200 Subject: [PATCH 0231/3926] Implement color hotkeys --- client/js/lounge.js | 110 +++++++++++++++++++++++++++++--------------- 1 file changed, 73 insertions(+), 37 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 81275fbd..0d2a08ec 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -1284,45 +1284,81 @@ $(function() { forms.find(".username").val(nick); }); - Mousetrap.bind([ - "command+up", - "command+down", - "ctrl+up", - "ctrl+down" - ], function(e, keys) { - var channels = sidebar.find(".chan"); - var index = channels.index(channels.filter(".active")); - var direction = keys.split("+").pop(); - switch (direction) { - case "up": - // Loop - var upTarget = (channels.length + (index - 1 + channels.length)) % channels.length; - channels.eq(upTarget).click(); - break; + (function HotkeysScope() { + Mousetrap.bind([ + "command+up", + "command+down", + "ctrl+up", + "ctrl+down" + ], function(e, keys) { + var channels = sidebar.find(".chan"); + var index = channels.index(channels.filter(".active")); + var direction = keys.split("+").pop(); + switch (direction) { + case "up": + // Loop + var upTarget = (channels.length + (index - 1 + channels.length)) % channels.length; + channels.eq(upTarget).click(); + break; - case "down": - // Loop - var downTarget = (channels.length + (index + 1 + channels.length)) % channels.length; - channels.eq(downTarget).click(); - break; + case "down": + // Loop + var downTarget = (channels.length + (index + 1 + channels.length)) % channels.length; + channels.eq(downTarget).click(); + break; + } + }); + + Mousetrap.bind([ + "command+shift+l", + "ctrl+shift+l" + ], function(e) { + if (e.target === input[0]) { + clear(); + e.preventDefault(); + } + }); + + Mousetrap.bind([ + "escape" + ], function() { + contextMenuContainer.hide(); + }); + + var colorsHotkeys = { + k: "\x03", + b: "\x02", + u: "\x1F", + i: "\x1D", + o: "\x0F", + }; + + for (var hotkey in colorsHotkeys) { + Mousetrap.bind([ + "command+" + hotkey, + "ctrl+" + hotkey + ], function(e) { + e.preventDefault(); + + const cursorPosStart = input.prop("selectionStart"); + const cursorPosEnd = input.prop("selectionEnd"); + const value = input.val(); + let newValue = value.substring(0, cursorPosStart) + colorsHotkeys[e.key]; + + if (cursorPosStart === cursorPosEnd) { + // If no text is selected, insert at cursor + newValue += value.substring(cursorPosEnd, value.length); + } else { + // If text is selected, insert formatting character at start and the end + newValue += value.substring(cursorPosStart, cursorPosEnd) + colorsHotkeys[e.key] + value.substring(cursorPosEnd, value.length); + } + + input + .val(newValue) + .get(0).setSelectionRange(cursorPosStart + 1, cursorPosEnd + 1); + }); } - }); - - Mousetrap.bind([ - "command+k", - "ctrl+shift+l" - ], function(e) { - if (e.target === input[0]) { - clear(); - e.preventDefault(); - } - }); - - Mousetrap.bind([ - "escape" - ], function() { - contextMenuContainer.hide(); - }); + }()); setInterval(function() { chat.find(".chan:not(.active)").each(function() { From 9997aafec7f710e950463e2f6234217483142673 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 17 Dec 2016 11:51:33 +0200 Subject: [PATCH 0232/3926] Rewrite identd server, combine with oidentd --- src/clientManager.js | 18 +++-- src/identd.js | 55 -------------- src/identification.js | 108 +++++++++++++++++++++++++++ src/oidentd.js | 49 ------------ src/plugins/irc-events/connection.js | 24 +++--- src/server.js | 48 +++++------- 6 files changed, 150 insertions(+), 152 deletions(-) delete mode 100644 src/identd.js create mode 100644 src/identification.js delete mode 100644 src/oidentd.js diff --git a/src/clientManager.js b/src/clientManager.js index fc681cd9..578972c3 100644 --- a/src/clientManager.js +++ b/src/clientManager.js @@ -5,18 +5,26 @@ var colors = require("colors/safe"); var fs = require("fs"); var Client = require("./client"); var Helper = require("./helper"); -var Oidentd = require("./oidentd"); module.exports = ClientManager; function ClientManager() { this.clients = []; - - if (typeof Helper.config.oidentd === "string") { - this.identHandler = new Oidentd(Helper.config.oidentd); - } } +ClientManager.prototype.init = function(identHandler, sockets) { + this.sockets = sockets; + this.identHandler = identHandler; + + if (!Helper.config.public) { + if ("autoload" in Helper.config) { + log.warn(`Autoloading users is now always enabled. Please remove the ${colors.yellow("autoload")} option from your configuration file.`); + } + + this.autoloadUsers(); + } +}; + ClientManager.prototype.findClient = function(name, token) { for (var i in this.clients) { var client = this.clients[i]; diff --git a/src/identd.js b/src/identd.js deleted file mode 100644 index e8928074..00000000 --- a/src/identd.js +++ /dev/null @@ -1,55 +0,0 @@ -"use strict"; - -var _ = require("lodash"); -var net = require("net"); - -var users = {}; -var enabled = false; - -module.exports.start = function(port) { - port = port || 113; - log.info("Starting identd server on port", port); - net.createServer(init).listen(port); - enabled = true; -}; - -module.exports.hook = function(stream, user) { - var socket = stream.socket || stream; - var ports = _.pick(socket, "localPort", "remotePort"); - var id = _.values(ports).join(", "); - - users[id] = user; - - socket.on("close", function() { - delete users[id]; - }); -}; - -module.exports.isEnabled = function() { - return enabled; -}; - -function init(socket) { - socket.on("data", function(data) { - respond(socket, data); - }); -} - -function respond(socket, data) { - var id = parse(data); - var response = id + " : "; - if (users[id]) { - response += "USERID : UNIX : " + users[id]; - } else { - response += "ERROR : NO-USER"; - } - response += "\r\n"; - socket.write(response); - socket.end(); -} - -function parse(data) { - data = data.toString(); - data = data.split(","); - return parseInt(data[0]) + ", " + parseInt(data[1]); -} diff --git a/src/identification.js b/src/identification.js new file mode 100644 index 00000000..a3386cb1 --- /dev/null +++ b/src/identification.js @@ -0,0 +1,108 @@ +"use strict"; + +const fs = require("fs"); +const net = require("net"); +const colors = require("colors/safe"); +const Helper = require("./helper"); + +class Identification { + constructor(startedCallback) { + this.connectionId = 0; + this.connections = new Map(); + + if (typeof Helper.config.oidentd === "string") { + this.oidentdFile = Helper.expandHome(Helper.config.oidentd); + log.info(`Oidentd file: ${colors.green(this.oidentdFile)}`); + + this.refresh(); + } + + if (Helper.config.identd.enable) { + if (this.oidentdFile) { + log.warn("Using both identd and oidentd at the same time, this is most likely not intended."); + } + + var server = net.createServer(this.serverConnection.bind(this)); + server.listen({ + port: Helper.config.identd.port || 113, + host: Helper.config.bind || Helper.config.host, + }, () => { + var address = server.address(); + log.info(`Identd server available on ${colors.green(address.address + ":" + address.port)}`); + + startedCallback(); + }); + } else { + startedCallback(); + } + } + + serverConnection(socket) { + socket.on("data", data => { + this.respondToIdent(socket, data); + socket.end(); + }); + } + + respondToIdent(socket, data) { + data = data.toString().split(","); + + const lport = parseInt(data[0]); + const fport = parseInt(data[1]); + + if (lport < 1 || fport < 1 || lport > 65535 || fport > 65535) { + return; + } + + for (var connection of this.connections.values()) { + if (connection.socket.remoteAddress === socket.remoteAddress + && connection.socket.remotePort === fport + && connection.socket.localPort === lport + && connection.socket.localAddress === socket.localAddress) { + return socket.write(`${lport}, ${fport} : USERID : UNIX : ${connection.user}\r\n`); + } + } + + socket.write(`${lport}, ${fport} : ERROR : NO-USER\r\n`); + } + + addSocket(socket, user) { + const id = ++this.connectionId; + + this.connections.set(id, {socket: socket, user: user}); + + if (this.oidentdFile) { + this.refresh(); + } + + return id; + } + + removeSocket(id) { + this.connections.delete(id); + + if (this.oidentdFile) { + this.refresh(); + } + } + + refresh() { + let file = "# Warning: file generated by The Lounge: changes will be overwritten!\n"; + + this.connections.forEach((connection) => { + file += "to " + connection.socket.remoteAddress + + " lport " + connection.socket.localPort + + " from " + connection.socket.localAddress + + " fport " + connection.socket.remotePort + + " { reply \"" + connection.user + "\" }\n"; + }); + + fs.writeFile(this.oidentdFile, file, {flag: "w+"}, function(err) { + if (err) { + log.error("Failed to update oidentd file!", err); + } + }); + } +} + +module.exports = Identification; diff --git a/src/oidentd.js b/src/oidentd.js deleted file mode 100644 index 541b9772..00000000 --- a/src/oidentd.js +++ /dev/null @@ -1,49 +0,0 @@ -"use strict"; - -const fs = require("fs"); -const Helper = require("./helper"); - -function OidentdFile(file) { - this.file = Helper.expandHome(file); - this.connectionId = 0; - this.connections = new Map(); - - this.refresh(); -} - -OidentdFile.prototype = { - addSocket: function(socket, user) { - const id = this.connectionId++; - - this.connections.set(id, {socket: socket, user: user}); - this.refresh(); - - return id; - }, - - removeSocket: function(id) { - this.connections.delete(id); - - this.refresh(); - }, - - refresh: function() { - let file = "# Warning: file generated by The Lounge: changes will be overwritten!\n"; - - this.connections.forEach((connection) => { - file += "to " + connection.socket.remoteAddress - + " lport " + connection.socket.localPort - + " from " + connection.socket.localAddress - + " fport " + connection.socket.remotePort - + " { reply \"" + connection.user + "\" }\n"; - }); - - fs.writeFile(this.file, file, {flag: "w+"}, function(err) { - if (err) { - log.error("Failed to update oidentd file!", err); - } - }); - }, -}; - -module.exports = OidentdFile; diff --git a/src/plugins/irc-events/connection.js b/src/plugins/irc-events/connection.js index 9da7d6e0..fb2fdb74 100644 --- a/src/plugins/irc-events/connection.js +++ b/src/plugins/irc-events/connection.js @@ -1,6 +1,5 @@ "use strict"; -var identd = require("../../identd"); var Msg = require("../../models/msg"); var Chan = require("../../models/chan"); var Helper = require("../../helper"); @@ -63,23 +62,18 @@ module.exports = function(irc, network) { }), true); }); - if (identd.isEnabled()) { - irc.on("raw socket connected", function(socket) { - identd.hook(socket, client.name || network.username); - }); - } + let identSocketId; - if (identHandler) { - let identSocketId; + irc.on("raw socket connected", function(socket) { + identSocketId = identHandler.addSocket(socket, client.name || network.username); + }); - irc.on("raw socket connected", function(socket) { - identSocketId = identHandler.addSocket(socket, client.name || network.username); - }); - - irc.on("socket close", function() { + irc.on("socket close", function() { + if (identSocketId > 0) { identHandler.removeSocket(identSocketId); - }); - } + identSocketId = 0; + } + }); if (Helper.config.debug.ircFramework) { irc.on("debug", function(message) { diff --git a/src/server.js b/src/server.js index 34217944..347214c4 100644 --- a/src/server.js +++ b/src/server.js @@ -11,12 +11,16 @@ var dns = require("dns"); var Helper = require("./helper"); var ldap = require("ldapjs"); var colors = require("colors/safe"); +const Identification = require("./identification"); +let identHandler = null; var manager = null; var authFunction = localAuth; module.exports = function() { - manager = new ClientManager(); + log.info(`The Lounge ${colors.green(Helper.getVersion())} \ +(node ${colors.green(process.versions.node)} on ${colors.green(process.platform)} ${process.arch})`); + log.info(`Configuration file: ${colors.green(Helper.CONFIG_PATH)}`); if (!fs.existsSync("client/js/bundle.js")) { log.error(`The client application was not built. Run ${colors.bold("NODE_ENV=production npm run build")} to resolve this.`); @@ -37,7 +41,7 @@ module.exports = function() { if (!config.https.enable) { server = require("http"); - server = server.createServer(app).listen(config.port, config.host); + server = server.createServer(app); } else { server = require("spdy"); const keyPath = Helper.expandHome(config.https.key); @@ -53,16 +57,18 @@ module.exports = function() { server = server.createServer({ key: fs.readFileSync(keyPath), cert: fs.readFileSync(certPath) - }, app).listen(config.port, config.host); + }, app); } - if (config.identd.enable) { - if (manager.identHandler) { - log.warn("Using both identd and oidentd at the same time!"); - } - - require("./identd").start(config.identd.port); - } + server.listen({ + port: config.port, + host: config.host, + }, () => { + const protocol = config.https.enable ? "https" : "http"; + var address = server.address(); + log.info(`Available on ${colors.green(protocol + "://" + address.address + ":" + address.port + "/")} \ +in ${config.public ? "public" : "private"} mode`); + }); if (!config.public && (config.ldap || {}).enable) { authFunction = ldapAuth; @@ -81,25 +87,11 @@ module.exports = function() { } }); - manager.sockets = sockets; + manager = new ClientManager(); - const protocol = config.https.enable ? "https" : "http"; - const host = config.host || "*"; - - log.info(`The Lounge ${colors.green(Helper.getVersion())} is now running \ -using node ${colors.green(process.versions.node)} on ${colors.green(process.platform)} (${process.arch})`); - log.info(`Configuration file: ${colors.green(Helper.CONFIG_PATH)}`); - log.info(`Available on ${colors.green(protocol + "://" + host + ":" + config.port + "/")} \ -in ${config.public ? "public" : "private"} mode`); - log.info("Press Ctrl-C to stop\n"); - - if (!config.public) { - if ("autoload" in config) { - log.warn(`Autoloading users is now always enabled. Please remove the ${colors.yellow("autoload")} option from your configuration file.`); - } - - manager.autoloadUsers(); - } + identHandler = new Identification(() => { + manager.init(identHandler, sockets); + }); }; function getClientIp(req) { From 65a218d9de5624e48679828cc47d7ce5bbd055f0 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 13 Mar 2017 02:38:54 +0000 Subject: [PATCH 0233/3926] chore(package): update babel-core to version 6.24.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a9134fbd..513445ed 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "spdy": "3.4.4" }, "devDependencies": { - "babel-core": "6.23.1", + "babel-core": "6.24.0", "babel-loader": "6.4.0", "babel-preset-es2015": "6.22.0", "chai": "3.5.0", From 8cd8ba61015bf586e407dfead1729d12e1c1b1b4 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 13 Mar 2017 03:54:02 +0000 Subject: [PATCH 0234/3926] chore(package): update babel-preset-es2015 to version 6.24.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a9134fbd..d81f5e60 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "devDependencies": { "babel-core": "6.23.1", "babel-loader": "6.4.0", - "babel-preset-es2015": "6.22.0", + "babel-preset-es2015": "6.24.0", "chai": "3.5.0", "eslint": "3.17.1", "font-awesome": "4.7.0", From 1c732ffc5b6be519d94a9633ed1dbb2d2b8d2e52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Mon, 13 Mar 2017 01:06:11 -0400 Subject: [PATCH 0235/3926] Fix `-h` help option alias shadowed by a similar alias for `--home` In v2.2.1 (and in pretty much every software out there, really), `-h` was an alias of `--help`. By adding it as an alias of `--home`, it is now shown twice in the help. --- src/command-line/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command-line/index.js b/src/command-line/index.js index 46ba56cb..36633c28 100644 --- a/src/command-line/index.js +++ b/src/command-line/index.js @@ -10,7 +10,7 @@ var path = require("path"); var Helper = require("../helper"); program.version(Helper.getVersion(), "-v, --version") - .option("-h, --home ", "path to configuration folder") + .option("--home ", "path to configuration folder") .parseOptions(process.argv); Helper.setHome(program.home || process.env.LOUNGE_HOME); From 06f734753f7f56d51630c30c7471a1bc33ccc3fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Mon, 13 Mar 2017 01:27:57 -0400 Subject: [PATCH 0236/3926] Add changelog entry for v2.2.2 --- CHANGELOG.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65688384..9171a3e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,56 @@ All sections are explained on the link above, they are all optional, and each of ``` --> +## v2.2.2 - 2017-03-13 + +For more details, [see the full changelog](https://github.com/thelounge/lounge/compare/v2.2.1...v2.2.2) and [milestone](https://github.com/thelounge/lounge/milestone/11). + +This patch release brings a lot of dependency upgrades and a few fixes. Passing options to the `lounge` CLI (`lounge start --port 8080`, etc.) now works as expected without requiring `--`. We have also disabled ping timeouts for now to hopefully fix automatic reconnection. Finally, upgrading `irc-framework` allows us to fix an extra couple of bugs. + +You will now notice a new `(?)` icon at the bottom of the sidebar. It is home of a help center that currently details supported shortcuts and commands. It will be improved over time, but we encourage contributors to help us improve it. + +Note that as of this release, `lounge` without any arguments wil display the help information (mirroring `lounge --help`). Prior to this release, it used to start a server, which must now be done explicitly using `lounge start`. + +### Changed + +- Update to `jQuery` 3 ([#931](https://github.com/thelounge/lounge/pull/931) by [@xPaw](https://github.com/xPaw)) +- Update `express` and `nyc` to latest versions ([#954](https://github.com/thelounge/lounge/pull/954) by [@xPaw](https://github.com/xPaw)) +- Update production dependencies to their latest versions, by [Greenkeeper](https://greenkeeper.io/) 🚀: + - `mousetrap` ([#881](https://github.com/thelounge/lounge/pull/881)) + - `fs-extra` ([#878](https://github.com/thelounge/lounge/pull/878)) + - `irc-framework` ([#918](https://github.com/thelounge/lounge/pull/918) and [#952](https://github.com/thelounge/lounge/pull/952)) + - `urijs` ([#921](https://github.com/thelounge/lounge/pull/921), [#940](https://github.com/thelounge/lounge/pull/940) and [#946](https://github.com/thelounge/lounge/pull/946)) + - `socket.io` and `socket.io-client` ([#926](https://github.com/thelounge/lounge/pull/926)) + - `request` ([#944](https://github.com/thelounge/lounge/pull/944)) + +### Fixed + +- Disable (temporarily) client ping timeouts ([#939](https://github.com/thelounge/lounge/pull/939) by [@xPaw](https://github.com/xPaw)) +- Update arg parsing and default `lounge` to `lounge --help` ([#929](https://github.com/thelounge/lounge/pull/929) by [@msaun008](https://github.com/msaun008)) +- Prevent message sending in lobbies ([#957](https://github.com/thelounge/lounge/pull/957) by [@xPaw](https://github.com/xPaw)) + +### Documentation + +In the main repository: + +- Help window with supported commands and shortcuts ([#941](https://github.com/thelounge/lounge/pull/941) by [@astorije](https://github.com/astorije)) + +On the website: + +- Add notes about moving client docs to the app itself ([#63](https://github.com/thelounge/thelounge.github.io/pull/63) by [@astorije](https://github.com/astorije)) +- Deprecate (and attempt one last fixing) documentations of Heroku and Passenger ([#61](https://github.com/thelounge/thelounge.github.io/pull/61) by [@astorije](https://github.com/astorije)) + +### Internals + +- Fix `run_pr.sh` script ([#919](https://github.com/thelounge/lounge/pull/919) by [@astorije](https://github.com/astorije)) +- Make sure multiline chains of calls are correctly indented ([#930](https://github.com/thelounge/lounge/pull/930) by [@astorije](https://github.com/astorije)) +- Update development dependencies to their latest versions, by [Greenkeeper](https://greenkeeper.io/) 🚀: + - `babel-core`, `babel-loader` and `babel-preset-es2015` ([#922](https://github.com/thelounge/lounge/pull/922) and [#947](https://github.com/thelounge/lounge/pull/947)) + - `webpack` ([#905](https://github.com/thelounge/lounge/pull/905)) + - `stylelint` ([#934](https://github.com/thelounge/lounge/pull/934)) + - `npm-run-all` ([#938](https://github.com/thelounge/lounge/pull/938)) + - `eslint` ([#937](https://github.com/thelounge/lounge/pull/937) and [#943](https://github.com/thelounge/lounge/pull/943)) + ## v2.2.1 - 2017-02-12 For more details, [see the full changelog](https://github.com/thelounge/lounge/compare/v2.2.0...v2.2.1) and [milestone](https://github.com/thelounge/lounge/milestone/10). From 908f816c4379e7afc41d2672a8d45abe9b392957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Mon, 13 Mar 2017 01:29:56 -0400 Subject: [PATCH 0237/3926] 2.2.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a9134fbd..ce8a9b98 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "thelounge", "description": "The self-hosted Web IRC client", - "version": "2.2.1", + "version": "2.2.2", "preferGlobal": true, "bin": { "lounge": "index.js" From 8ef99d7ad831beedb8136edaf5f83626d76208b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Mon, 13 Mar 2017 01:58:39 -0400 Subject: [PATCH 0238/3926] Add shortcuts for new formatting in help window --- client/index.html | 116 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 2 deletions(-) diff --git a/client/index.html b/client/index.html index 0ec349a6..192a4085 100644 --- a/client/index.html +++ b/client/index.html @@ -390,6 +390,62 @@
+
+
+ Ctrl + K +
+
+

+ Mark any text typed after this shortcut to be colored. After + hitting this shortcut, enter an integer in the + 0—15 range to select the desired color. +

+

+ A color reference can be found + here. +

+
+
+ +
+
+ Ctrl + B +
+
+

Mark all text typed after this shortcut as bold.

+
+
+ +
+
+ Ctrl + U +
+
+

Mark all text typed after this shortcut as underlined.

+
+
+ +
+
+ Ctrl + I +
+
+

Mark all text typed after this shortcut as italics.

+
+
+ +
+
+ Ctrl + O +
+
+

+ Mark all text typed after this shortcut to be reset to its + original formatting. +

+
+
+

On macOS

@@ -403,13 +459,69 @@
- + K + + + L

Clear the current screen

+
+
+ + K +
+
+

+ Mark any text typed after this shortcut to be colored. After + hitting this shortcut, enter an integer in the + 0—15 range to select the desired color. +

+

+ A color reference can be found + here. +

+
+
+ +
+
+ + B +
+
+

Mark all text typed after this shortcut as bold.

+
+
+ +
+
+ + U +
+
+

Mark all text typed after this shortcut as underlined.

+
+
+ +
+
+ + I +
+
+

Mark all text typed after this shortcut as italics.

+
+
+ +
+
+ + O +
+
+

+ Mark all text typed after this shortcut to be reset to its + original formatting. +

+
+
+

Commands

@@ -461,7 +573,7 @@

Send a CTCP request. Read more about this on - the dedicated Wikipedia article. + the dedicated Wikipedia article.

From ae5f768dd1d3da76d170151bd0aa400decc73132 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Thu, 16 Mar 2017 21:35:11 +0000 Subject: [PATCH 0239/3926] chore(package): update jquery to version 3.2.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce8a9b98..72aa1a2e 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "font-awesome": "4.7.0", "handlebars": "4.0.6", "handlebars-loader": "1.4.0", - "jquery": "3.1.1", + "jquery": "3.2.0", "jquery-ui": "1.12.1", "mocha": "3.2.0", "mousetrap": "1.6.0", From c409328ddf38db3b421c61bf2f688317f8e07f9d Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 17 Mar 2017 22:19:08 +0200 Subject: [PATCH 0240/3926] Fix variable shuffling around ident handler Fixes #965 --- src/identification.js | 4 ++-- src/plugins/irc-events/connection.js | 5 ++--- src/server.js | 3 +-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/identification.js b/src/identification.js index a3386cb1..ea4cca28 100644 --- a/src/identification.js +++ b/src/identification.js @@ -30,10 +30,10 @@ class Identification { var address = server.address(); log.info(`Identd server available on ${colors.green(address.address + ":" + address.port)}`); - startedCallback(); + startedCallback(this); }); } else { - startedCallback(); + startedCallback(this); } } diff --git a/src/plugins/irc-events/connection.js b/src/plugins/irc-events/connection.js index fb2fdb74..a8ecef54 100644 --- a/src/plugins/irc-events/connection.js +++ b/src/plugins/irc-events/connection.js @@ -6,7 +6,6 @@ var Helper = require("../../helper"); module.exports = function(irc, network) { var client = this; - var identHandler = this.manager.identHandler; network.channels[0].pushMessage(client, new Msg({ text: "Network created, connecting to " + network.host + ":" + network.port + "..." @@ -65,12 +64,12 @@ module.exports = function(irc, network) { let identSocketId; irc.on("raw socket connected", function(socket) { - identSocketId = identHandler.addSocket(socket, client.name || network.username); + identSocketId = client.manager.identHandler.addSocket(socket, client.name || network.username); }); irc.on("socket close", function() { if (identSocketId > 0) { - identHandler.removeSocket(identSocketId); + client.manager.identHandler.removeSocket(identSocketId); identSocketId = 0; } }); diff --git a/src/server.js b/src/server.js index 347214c4..636b5ab6 100644 --- a/src/server.js +++ b/src/server.js @@ -13,7 +13,6 @@ var ldap = require("ldapjs"); var colors = require("colors/safe"); const Identification = require("./identification"); -let identHandler = null; var manager = null; var authFunction = localAuth; @@ -89,7 +88,7 @@ in ${config.public ? "public" : "private"} mode`); manager = new ClientManager(); - identHandler = new Identification(() => { + new Identification((identHandler) => { manager.init(identHandler, sockets); }); }; From aac6980eb50da833f72e3c97e3ba6e1f0c57f94c Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 17 Mar 2017 22:13:47 +0000 Subject: [PATCH 0241/3926] chore(package): update eslint to version 3.18.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce8a9b98..bc7ca844 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "babel-loader": "6.4.0", "babel-preset-es2015": "6.22.0", "chai": "3.5.0", - "eslint": "3.17.1", + "eslint": "3.18.0", "font-awesome": "4.7.0", "handlebars": "4.0.6", "handlebars-loader": "1.4.0", From dcefcd19cba4d30b21bdce88d811d24908dffee7 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 18 Mar 2017 11:21:18 +0200 Subject: [PATCH 0242/3926] Use require() instead of import in client code Closes #895 --- client/.eslintrc.yml | 3 --- client/js/libs/handlebars/colorClass.js | 2 ++ client/js/libs/handlebars/diff.js | 2 ++ client/js/libs/handlebars/equal.js | 2 ++ client/js/libs/handlebars/localedate.js | 2 ++ client/js/libs/handlebars/localetime.js | 2 ++ client/js/libs/handlebars/modes.js | 2 ++ client/js/libs/handlebars/parse.js | 6 +++-- client/js/libs/handlebars/roundBadgeNumber.js | 2 ++ client/js/libs/handlebars/tojson.js | 2 ++ client/js/libs/handlebars/tz.js | 2 ++ client/js/libs/handlebars/users.js | 2 ++ client/js/libs/slideout.js | 6 +++-- client/js/lounge.js | 26 ++++++++++--------- client/views/index.js | 2 ++ 15 files changed, 44 insertions(+), 19 deletions(-) delete mode 100644 client/.eslintrc.yml diff --git a/client/.eslintrc.yml b/client/.eslintrc.yml deleted file mode 100644 index cb4e55ff..00000000 --- a/client/.eslintrc.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -parserOptions: - sourceType: module diff --git a/client/js/libs/handlebars/colorClass.js b/client/js/libs/handlebars/colorClass.js index 53bfc0d4..e7b8d8e4 100644 --- a/client/js/libs/handlebars/colorClass.js +++ b/client/js/libs/handlebars/colorClass.js @@ -1,3 +1,5 @@ +"use strict"; + // Generates a string from "color-1" to "color-32" based on an input string module.exports = function(str) { var hash = 0; diff --git a/client/js/libs/handlebars/diff.js b/client/js/libs/handlebars/diff.js index 3b2116bd..198c8882 100644 --- a/client/js/libs/handlebars/diff.js +++ b/client/js/libs/handlebars/diff.js @@ -1,3 +1,5 @@ +"use strict"; + var diff; module.exports = function(a, opt) { diff --git a/client/js/libs/handlebars/equal.js b/client/js/libs/handlebars/equal.js index 1d7a4393..6a8033ca 100644 --- a/client/js/libs/handlebars/equal.js +++ b/client/js/libs/handlebars/equal.js @@ -1,3 +1,5 @@ +"use strict"; + module.exports = function(a, b, opt) { a = a.toString(); b = b.toString(); diff --git a/client/js/libs/handlebars/localedate.js b/client/js/libs/handlebars/localedate.js index 0f85a5f2..9c800ab0 100644 --- a/client/js/libs/handlebars/localedate.js +++ b/client/js/libs/handlebars/localedate.js @@ -1,3 +1,5 @@ +"use strict"; + module.exports = function(time) { return new Date(time).toLocaleDateString(); }; diff --git a/client/js/libs/handlebars/localetime.js b/client/js/libs/handlebars/localetime.js index 5318a022..59e1dbf8 100644 --- a/client/js/libs/handlebars/localetime.js +++ b/client/js/libs/handlebars/localetime.js @@ -1,3 +1,5 @@ +"use strict"; + module.exports = function(time) { return new Date(time).toLocaleString(); }; diff --git a/client/js/libs/handlebars/modes.js b/client/js/libs/handlebars/modes.js index 6416515f..ff3d555e 100644 --- a/client/js/libs/handlebars/modes.js +++ b/client/js/libs/handlebars/modes.js @@ -1,3 +1,5 @@ +"use strict"; + module.exports = function(mode) { var modes = { "~": "owner", diff --git a/client/js/libs/handlebars/parse.js b/client/js/libs/handlebars/parse.js index cd61ff7e..9d276f49 100644 --- a/client/js/libs/handlebars/parse.js +++ b/client/js/libs/handlebars/parse.js @@ -1,5 +1,7 @@ -import Handlebars from "handlebars/runtime"; -import URI from "urijs"; +"use strict"; + +const Handlebars = require("handlebars/runtime"); +const URI = require("urijs"); module.exports = function(text) { text = Handlebars.Utils.escapeExpression(text); diff --git a/client/js/libs/handlebars/roundBadgeNumber.js b/client/js/libs/handlebars/roundBadgeNumber.js index a7750de0..97c50afa 100644 --- a/client/js/libs/handlebars/roundBadgeNumber.js +++ b/client/js/libs/handlebars/roundBadgeNumber.js @@ -1,3 +1,5 @@ +"use strict"; + module.exports = function(count) { if (count < 1000) { return count; diff --git a/client/js/libs/handlebars/tojson.js b/client/js/libs/handlebars/tojson.js index fcd0fde1..418ac8c4 100644 --- a/client/js/libs/handlebars/tojson.js +++ b/client/js/libs/handlebars/tojson.js @@ -1,3 +1,5 @@ +"use strict"; + module.exports = function(context) { return window.JSON.stringify(context); }; diff --git a/client/js/libs/handlebars/tz.js b/client/js/libs/handlebars/tz.js index 14f05e5e..e62bf285 100644 --- a/client/js/libs/handlebars/tz.js +++ b/client/js/libs/handlebars/tz.js @@ -1,3 +1,5 @@ +"use strict"; + module.exports = function(time) { time = new Date(time); var h = time.getHours(); diff --git a/client/js/libs/handlebars/users.js b/client/js/libs/handlebars/users.js index e52e7526..d962423c 100644 --- a/client/js/libs/handlebars/users.js +++ b/client/js/libs/handlebars/users.js @@ -1,3 +1,5 @@ +"use strict"; + module.exports = function(count) { return count + " " + (count === 1 ? "user" : "users"); }; diff --git a/client/js/libs/slideout.js b/client/js/libs/slideout.js index 522cc448..34faa9a8 100644 --- a/client/js/libs/slideout.js +++ b/client/js/libs/slideout.js @@ -1,7 +1,9 @@ +"use strict"; + /** * Simple slideout menu implementation. */ -export default function slideoutMenu(viewport, menu) { +module.exports = function slideoutMenu(viewport, menu) { var touchStartPos = null; var touchCurPos = null; var touchStartTime = 0; @@ -98,4 +100,4 @@ export default function slideoutMenu(viewport, menu) { return menuIsOpen; } }; -} +}; diff --git a/client/js/lounge.js b/client/js/lounge.js index 81275fbd..c26f653b 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -1,18 +1,20 @@ +"use strict"; + // vendor libraries -import "jquery-ui/ui/widgets/sortable"; -import $ from "jquery"; -import io from "socket.io-client"; -import Mousetrap from "mousetrap"; -import URI from "urijs"; +require("jquery-ui/ui/widgets/sortable"); +const $ = require("jquery"); +const io = require("socket.io-client"); +const Mousetrap = require("mousetrap"); +const URI = require("urijs"); // our libraries -import "./libs/jquery/inputhistory"; -import "./libs/jquery/stickyscroll"; -import "./libs/jquery/tabcomplete"; -import helpers_parse from "./libs/handlebars/parse"; -import helpers_roundBadgeNumber from "./libs/handlebars/roundBadgeNumber"; -import slideoutMenu from "./libs/slideout"; -import templates from "../views"; +require("./libs/jquery/inputhistory"); +require("./libs/jquery/stickyscroll"); +require("./libs/jquery/tabcomplete"); +const helpers_parse = require("./libs/handlebars/parse"); +const helpers_roundBadgeNumber = require("./libs/handlebars/roundBadgeNumber"); +const slideoutMenu = require("./libs/slideout"); +const templates = require("../views"); $(function() { var path = window.location.pathname + "socket.io/"; diff --git a/client/views/index.js b/client/views/index.js index 2890dea9..0b92ab8e 100644 --- a/client/views/index.js +++ b/client/views/index.js @@ -1,3 +1,5 @@ +"use strict"; + module.exports = { actions: { action: require("./actions/action.tpl"), From 777f4771351864fa52db83b02de687379a08ea90 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Wed, 15 Mar 2017 12:39:35 +0000 Subject: [PATCH 0243/3926] fix(package): update fs-extra to version 2.1.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce8a9b98..d0821c2a 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "commander": "2.9.0", "event-stream": "3.3.4", "express": "4.15.2", - "fs-extra": "2.0.0", + "fs-extra": "2.1.2", "irc-framework": "2.6.1", "ldapjs": "1.0.1", "lodash": "4.17.4", From d9358d555fb7bf9c4640ec49e0ac42348dc25670 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 18 Mar 2017 16:09:43 +0200 Subject: [PATCH 0244/3926] Generate sourcemap in production build Fixes #974 --- webpack.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/webpack.config.js b/webpack.config.js index 09c36561..8d66b1a9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -71,6 +71,7 @@ let config = { if (process.env.NODE_ENV === "production") { config.plugins.push(new webpack.optimize.UglifyJsPlugin({ + sourceMap: true, comments: false })); } else { From 3318acd16beb5a5b1f82fda5291318623dfda30d Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 9 Jan 2017 16:24:04 +0000 Subject: [PATCH 0245/3926] fix filling in nickname overriding username (in add network tab) --- client/js/lounge.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 774aeddd..754ab1a4 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -1283,9 +1283,27 @@ $(function() { ); }); + forms.on("focusin", ".nick", function() { + // Need to set the first "lastvalue", so it can be used in the below function + var nick = $(this); + nick.data("lastvalue", nick.val()); + }); + forms.on("input", ".nick", function() { var nick = $(this).val(); - forms.find(".username").val(nick); + var usernameInput = forms.find(".username"); + + // Because this gets called /after/ it has already changed, we need use the previous value + var lastValue = $(this).data("lastvalue"); + + // They were the same before the change, so update the username field + if (usernameInput.val() === lastValue) { + usernameInput.val(nick); + } + + // Store the "previous" value, for next time + $(this).data("lastvalue", nick); + }); Mousetrap.bind([ From 3b2e3fc08c8155256f817396a24ce99ca6b97591 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 18 Mar 2017 21:40:39 +0200 Subject: [PATCH 0246/3926] Enforce more space and new line rules --- .eslintrc.yml | 3 +++ client/js/libs/handlebars/parse.js | 1 - client/js/lounge.js | 3 --- src/clientManager.js | 1 - test/client/js/libs/handlebars/localetimeTest.js | 2 -- test/models/network.js | 1 - 6 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index d6b6a59e..353706cc 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -27,6 +27,7 @@ rules: no-else-return: 2 no-implicit-globals: 2 no-multi-spaces: 2 + no-multiple-empty-lines: [2, { "max": 1 }] no-shadow: 2 no-template-curly-in-string: 2 no-trailing-spaces: 2 @@ -34,11 +35,13 @@ rules: no-useless-escape: 2 no-useless-return: 2 object-curly-spacing: [2, never] + padded-blocks: [2, never] quote-props: [2, as-needed] quotes: [2, double, avoid-escape] semi: [2, always] space-before-blocks: 2 space-before-function-paren: [2, never] + space-in-parens: [2, never] space-infix-ops: 2 spaced-comment: [2, always] strict: 2 diff --git a/client/js/libs/handlebars/parse.js b/client/js/libs/handlebars/parse.js index cd61ff7e..325b049f 100644 --- a/client/js/libs/handlebars/parse.js +++ b/client/js/libs/handlebars/parse.js @@ -49,7 +49,6 @@ var styleCheck_Re = /[\x00-\x1F]/, // breaks all open styles ^O (\x0F) styleBreak = "\x0F"; - function styleTemplate(settings) { return "" + settings.text + ""; } diff --git a/client/js/lounge.js b/client/js/lounge.js index 81275fbd..a51b3fec 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -364,7 +364,6 @@ $(function() { lastDate = msgDate; }); } - } function renderChannelUsers(data) { @@ -498,7 +497,6 @@ $(function() { lastDate = msgDate; }); - }); socket.on("network", function(data) { @@ -1168,7 +1166,6 @@ $(function() { } catch (exception) { // `new Notification(...)` is not supported and should be silenced. } - } } } diff --git a/src/clientManager.js b/src/clientManager.js index 578972c3..a0f94f0f 100644 --- a/src/clientManager.js +++ b/src/clientManager.js @@ -96,7 +96,6 @@ ClientManager.prototype.addUser = function(name, password, enableLog) { return false; } try { - if (require("path").basename(name) !== name) { throw new Error(name + " is an invalid username."); } diff --git a/test/client/js/libs/handlebars/localetimeTest.js b/test/client/js/libs/handlebars/localetimeTest.js index 75debd8e..bccf3649 100644 --- a/test/client/js/libs/handlebars/localetimeTest.js +++ b/test/client/js/libs/handlebars/localetimeTest.js @@ -4,7 +4,6 @@ const expect = require("chai").expect; const localetime = require("../../../../../client/js/libs/handlebars/localetime"); describe("localetime Handlebars helper", () => { - it("should render a human-readable date", () => { // 12PM in UTC time const date = new Date("2014-05-22T12:00:00"); @@ -17,5 +16,4 @@ describe("localetime Handlebars helper", () => { expect(localetime(time)).to.equal("5/22/2014, 12:00:00 PM"); }); - }); diff --git a/test/models/network.js b/test/models/network.js index fe56157d..438c6251 100644 --- a/test/models/network.js +++ b/test/models/network.js @@ -8,7 +8,6 @@ var Network = require("../../src/models/network"); describe("Network", function() { describe("#export()", function() { - it("should produce an valid object", function() { var network = new Network({ name: "networkName", From 7175dc1706c84362d3c56e8f86ad663c3a988418 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sat, 18 Mar 2017 21:27:01 +0000 Subject: [PATCH 0247/3926] fix(package): update moment to version 2.18.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce8a9b98..04805315 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "irc-framework": "2.6.1", "ldapjs": "1.0.1", "lodash": "4.17.4", - "moment": "2.17.1", + "moment": "2.18.0", "read": "1.0.7", "request": "2.81.0", "semver": "5.3.0", From c6ed95e55575a4850ee394f211538809f86123b8 Mon Sep 17 00:00:00 2001 From: William Boman Date: Mon, 20 Mar 2017 23:08:28 +0100 Subject: [PATCH 0248/3926] views/msg: set data-from attribute to allow styling messages from specific user(s) --- client/views/msg.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/views/msg.tpl b/client/views/msg.tpl index 72811106..36dc4c27 100644 --- a/client/views/msg.tpl +++ b/client/views/msg.tpl @@ -1,4 +1,4 @@ -
+
{{tz time}} From 642442c0417fe7a4525acdd8d179760058f5c9bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89lie=20Michel?= Date: Tue, 21 Mar 2017 15:15:33 +0100 Subject: [PATCH 0249/3926] Implement a proper LDAP authentication process The Lounge first log as a special user in order to search (as in LDAP's '"search" verb) for the user's full DN. It then attempts to bind using the found user DN and the user provided password. --- defaults/config.js | 70 ++++++++++++++++++++++++++++++++++++++++++++-- src/server.js | 65 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 123 insertions(+), 12 deletions(-) diff --git a/defaults/config.js b/defaults/config.js index 98f4876c..742449f7 100644 --- a/defaults/config.js +++ b/defaults/config.js @@ -329,6 +329,23 @@ module.exports = { // @type object // @default {} // + // The authentication process works as follows: + // + // 1. Lounge connects to the LDAP server with its system credentials + // 2. It performs a LDAP search query to find the full DN associated to the + // user requesting to log in. + // 3. Lounge tries to connect a second time, but this time using the user's + // DN and password. Auth is validated iff this connection is successful. + // + // The search query takes a couple of parameters: + // - A base DN. Only children nodes of this DN will be likely to be returned + // - A search scope (see LDAP documentation) + // - The query itself, build as (&(=) ) + // where is the user name provided in the log in request, + // is provided by the config and is a filtering complement + // also given in the config, to filter for instance only for nodes of type + // inetOrgPerson, or whatever LDAP search allows. + // ldap: { // // Enable LDAP user authentication @@ -346,11 +363,35 @@ module.exports = { url: "ldaps://example.com", // - // LDAP base dn + // LDAP connection tls options (only used if scheme is ldaps://) + // + // @type object (see nodejs' tls.connect() options) + // @default {} + // + // Example: + // You can use this option in order to force the use of IPv6: + // { + // host: 'my::ip::v6' + // servername: 'ldaps://example.com' + // } + tlsOptions: {}, + + // + // LDAP searching bind DN + // This bind DN is used to query the server for the DN of the user. + // This is supposed to be a system user that has access in read only to + // the DNs of the people that are allowed to log in. // // @type string // - baseDN: "ou=accounts,dc=example,dc=com", + rootDN: "cn=thelounge,ou=system-users,dc=example,dc=com", + + // + // Password of the lounge LDAP system user + // + // @type string + // + rootPassword: "1234", // // LDAP primary key @@ -358,7 +399,30 @@ module.exports = { // @type string // @default "uid" // - primaryKey: "uid" + primaryKey: "uid", + + // + // LDAP filter + // + // @type string + // @default "uid" + // + filter: "(objectClass=inetOrgPerson)(memberOf=ou=accounts,dc=example,dc=com)", + + // + // LDAP search base (search only within this node) + // + // @type string + // + base: "dc=example,dc=com", + + // + // LDAP search scope + // + // @type string + // @default "sub" + // + scope: "sub" }, // Extra debugging diff --git a/src/server.js b/src/server.js index 636b5ab6..9fcd759a 100644 --- a/src/server.js +++ b/src/server.js @@ -283,26 +283,73 @@ function localAuth(client, user, password, callback) { } function ldapAuth(client, user, password, callback) { + if (!user) { + return callback(false); + } var userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1"); - var bindDN = Helper.config.ldap.primaryKey + "=" + userDN + "," + Helper.config.ldap.baseDN; var ldapclient = ldap.createClient({ - url: Helper.config.ldap.url + url: Helper.config.ldap.url, + tlsOptions: Helper.config.ldap.tlsOptions }); + var base = Helper.config.ldap.base; + var searchOptions = { + scope: Helper.config.ldap.scope, + filter: '(&(' + Helper.config.ldap.primaryKey + '=' + userDN + ')' + Helper.config.ldap.filter + ')', + attributes: ['dn'] + }; + ldapclient.on("error", function(err) { log.error("Unable to connect to LDAP server", err); callback(!err); }); - ldapclient.bind(bindDN, password, function(err) { - if (!err && !client) { - if (!manager.addUser(user, null)) { - log.error("Unable to create new user", user); - } + ldapclient.bind(Helper.config.ldap.rootDN, + Helper.config.ldap.rootPassword, + function(err) { + if (err) { + log.error("Invalid LDAP root credentials"); + ldapclient.unbind(); + callback(false); + } else { + ldapclient.search(base, searchOptions, function(err, res) { + if (err) { + log.warning("User not found: ", userDN); + ldapclient.unbind(); + callback(false); + } else { + var found = false; + res.on('searchEntry', function(entry) { + found = true; + var bindDN = entry.objectName; + log.info("Auth against LDAP ", Helper.config.ldap.url, " with bindDN ", bindDN); + ldapclient.unbind() + var ldapclient2 = ldap.createClient({ + url: Helper.config.ldap.url, + tlsOptions: Helper.config.ldap.tlsOptions + }); + ldapclient2.bind(bindDN, password, function(err) { + if (!err && !client) { + if (!manager.addUser(user, null)) { + log.error("Unable to create new user", user); + } + } + ldapclient2.unbind(); + callback(!err); + }); + }); + res.on('error', function(resErr) { + callback(false); + }); + res.on('end', function(result) { + if (!found) { + callback(false); + } + }); + } + }); } - ldapclient.unbind(); - callback(!err); }); } From ed3b4faa62a459a818c321d0850b58312b91476f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89lie=20Michel?= Date: Tue, 21 Mar 2017 15:49:54 +0100 Subject: [PATCH 0250/3926] Fix eslint styling issues --- src/server.js | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/server.js b/src/server.js index 9fcd759a..6af0fe47 100644 --- a/src/server.js +++ b/src/server.js @@ -286,18 +286,19 @@ function ldapAuth(client, user, password, callback) { if (!user) { return callback(false); } + var config = Helper.config; var userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1"); var ldapclient = ldap.createClient({ - url: Helper.config.ldap.url, - tlsOptions: Helper.config.ldap.tlsOptions + url: config.ldap.url, + tlsOptions: config.ldap.tlsOptions }); - var base = Helper.config.ldap.base; + var base = config.ldap.base; var searchOptions = { - scope: Helper.config.ldap.scope, - filter: '(&(' + Helper.config.ldap.primaryKey + '=' + userDN + ')' + Helper.config.ldap.filter + ')', - attributes: ['dn'] + scope: config.ldap.scope, + filter: "(&(" + config.ldap.primaryKey + "=" + userDN + ")" + config.ldap.filter + ")", + attributes: ["dn"] }; ldapclient.on("error", function(err) { @@ -305,44 +306,43 @@ function ldapAuth(client, user, password, callback) { callback(!err); }); - ldapclient.bind(Helper.config.ldap.rootDN, - Helper.config.ldap.rootPassword, - function(err) { + ldapclient.bind(config.ldap.rootDN, config.ldap.rootPassword, function(err) { if (err) { log.error("Invalid LDAP root credentials"); ldapclient.unbind(); callback(false); } else { - ldapclient.search(base, searchOptions, function(err, res) { - if (err) { + ldapclient.search(base, searchOptions, function(err2, res) { + if (err2) { log.warning("User not found: ", userDN); ldapclient.unbind(); callback(false); } else { var found = false; - res.on('searchEntry', function(entry) { + res.on("searchEntry", function(entry) { found = true; var bindDN = entry.objectName; - log.info("Auth against LDAP ", Helper.config.ldap.url, " with bindDN ", bindDN); - ldapclient.unbind() + log.info("Auth against LDAP ", config.ldap.url, " with bindDN ", bindDN); + ldapclient.unbind(); var ldapclient2 = ldap.createClient({ - url: Helper.config.ldap.url, - tlsOptions: Helper.config.ldap.tlsOptions + url: config.ldap.url, + tlsOptions: config.ldap.tlsOptions }); - ldapclient2.bind(bindDN, password, function(err) { - if (!err && !client) { + ldapclient2.bind(bindDN, password, function(err3) { + if (!err3 && !client) { if (!manager.addUser(user, null)) { log.error("Unable to create new user", user); } } ldapclient2.unbind(); - callback(!err); + callback(!err3); }); }); - res.on('error', function(resErr) { + res.on("error", function(err3) { + log.error("LDAP error: ", err3); callback(false); }); - res.on('end', function(result) { + res.on("end", function() { if (!found) { callback(false); } From 03fe53e87f3ca9bb681ada5e9db4e9eacdde7a26 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Thu, 16 Mar 2017 10:54:48 +0000 Subject: [PATCH 0251/3926] chore(package): update babel-loader to version 6.4.1 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0fdc2e77..e0f0f00f 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ }, "devDependencies": { "babel-core": "6.24.0", - "babel-loader": "6.4.0", + "babel-loader": "6.4.1", "babel-preset-es2015": "6.24.0", "chai": "3.5.0", "eslint": "3.18.0", From 1f01da21ff6a05fcaed063283056dbdc89b4c4fe Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 28 Mar 2017 05:16:34 +0000 Subject: [PATCH 0252/3926] chore(package): update nyc to version 10.2.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e0f0f00f..6f750e11 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "mocha": "3.2.0", "mousetrap": "1.6.0", "npm-run-all": "4.0.2", - "nyc": "10.1.2", + "nyc": "10.2.0", "socket.io-client": "1.7.3", "stylelint": "7.9.0", "urijs": "1.18.9", From 2bcbb62af0af93db08c32ecfdd7f8db70dd1584c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 28 Mar 2017 19:09:40 -0400 Subject: [PATCH 0253/3926] Remove unnecessary coverage dir from excluded nyc files This is possible since https://github.com/istanbuljs/nyc/commit/50adde41969eb067aefc787b5ddb5cb463fd0bb3. --- .nycrc | 1 - 1 file changed, 1 deletion(-) diff --git a/.nycrc b/.nycrc index 998e5bac..ae33aa14 100644 --- a/.nycrc +++ b/.nycrc @@ -3,7 +3,6 @@ "exclude": [ "client/js/bundle.js", "client/js/bundle.vendor.js", - "coverage/", "test/" ], "reporter": [ From d287dede499acebffd924c66479813e35d165598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 29 Mar 2017 00:05:28 -0400 Subject: [PATCH 0254/3926] Setup ESLint to make sure an EOF feed is always present --- .eslintrc.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.yml b/.eslintrc.yml index 353706cc..937aef1f 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -15,6 +15,7 @@ rules: comma-dangle: 0 curly: [2, all] dot-notation: 2 + eol-last: 2 eqeqeq: 2 handle-callback-err: 2 indent: [2, tab, { "MemberExpression": 1 }] From 4a68b78fd512f60a0c76275e81b7e8f2a90bac4c Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sun, 18 Dec 2016 11:24:50 +0200 Subject: [PATCH 0255/3926] Implement away message restoration on reconnections and auto away --- src/client.js | 24 ++++++++++++++++++++++++ src/models/network.js | 3 +++ src/plugins/inputs/away.js | 14 ++++++-------- src/plugins/irc-events/connection.js | 9 +++++++++ test/models/network.js | 2 ++ 5 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/client.js b/src/client.js index b7e1b3ef..00f3eb35 100644 --- a/src/client.js +++ b/src/client.js @@ -65,6 +65,7 @@ function Client(manager, name, config) { config = {}; } _.merge(this, { + awayMessage: "", lastActiveChannel: -1, attachedClients: {}, config: config, @@ -485,6 +486,16 @@ Client.prototype.clientAttach = function(socketId) { client.attachedClients[socketId] = client.lastActiveChannel; + if (client.awayMessage && _.size(client.attachedClients) === 0) { + client.networks.forEach(function(network) { + // Only remove away on client attachment if + // there is no away message on this network + if (!network.awayMessage) { + network.irc.raw("AWAY"); + } + }); + } + // Update old networks to store ip and hostmask client.networks.forEach(network => { if (!network.ip) { @@ -508,7 +519,19 @@ Client.prototype.clientAttach = function(socketId) { }; Client.prototype.clientDetach = function(socketId) { + const client = this; + delete this.attachedClients[socketId]; + + if (client.awayMessage && _.size(client.attachedClients) === 0) { + client.networks.forEach(function(network) { + // Only set away on client deattachment if + // there is no away message on this network + if (!network.awayMessage) { + network.irc.raw("AWAY", client.awayMessage); + } + }); + } }; Client.prototype.save = _.debounce(function SaveClient() { @@ -518,6 +541,7 @@ Client.prototype.save = _.debounce(function SaveClient() { const client = this; let json = {}; + json.awayMessage = client.awayMessage; json.networks = this.networks.map(n => n.export()); client.manager.updateUser(client.name, json); }, 1000, {maxWait: 10000}); diff --git a/src/models/network.js b/src/models/network.js index 0ddd3530..4484ed96 100644 --- a/src/models/network.js +++ b/src/models/network.js @@ -14,6 +14,7 @@ function Network(attr) { port: 6667, tls: false, password: "", + awayMessage: "", commands: [], username: "", realname: "", @@ -56,6 +57,7 @@ Network.prototype.setNick = function(nick) { Network.prototype.toJSON = function() { return _.omit(this, [ + "awayMessage", "chanCache", "highlightRegex", "irc", @@ -65,6 +67,7 @@ Network.prototype.toJSON = function() { Network.prototype.export = function() { var network = _.pick(this, [ + "awayMessage", "nick", "name", "host", diff --git a/src/plugins/inputs/away.js b/src/plugins/inputs/away.js index 201559fe..34c58596 100644 --- a/src/plugins/inputs/away.js +++ b/src/plugins/inputs/away.js @@ -3,17 +3,15 @@ exports.commands = ["away", "back"]; exports.input = function(network, chan, cmd, args) { - if (cmd === "away") { - let reason = " "; + let reason = ""; - if (args.length > 0) { - reason = args.join(" "); - } + if (cmd === "away") { + reason = args.length > 0 ? args.join(" ") : " "; network.irc.raw("AWAY", reason); - - return; + } else { // back command + network.irc.raw("AWAY"); } - network.irc.raw("AWAY"); + network.awayMessage = reason; }; diff --git a/src/plugins/irc-events/connection.js b/src/plugins/irc-events/connection.js index a8ecef54..4e34a154 100644 --- a/src/plugins/irc-events/connection.js +++ b/src/plugins/irc-events/connection.js @@ -1,5 +1,6 @@ "use strict"; +var _ = require("lodash"); var Msg = require("../../models/msg"); var Chan = require("../../models/chan"); var Helper = require("../../helper"); @@ -18,6 +19,14 @@ module.exports = function(irc, network) { }), true); } + // Always restore away message for this network + if (network.awayMessage) { + irc.raw("AWAY", network.awayMessage); + // Only set generic away message if there are no clients attached + } else if (client.awayMessage && _.size(client.attachedClients) === 0) { + irc.raw("AWAY", client.awayMessage); + } + var delay = 1000; var commands = network.commands; if (Array.isArray(commands)) { diff --git a/test/models/network.js b/test/models/network.js index 438c6251..a5ebb5e4 100644 --- a/test/models/network.js +++ b/test/models/network.js @@ -10,6 +10,7 @@ describe("Network", function() { describe("#export()", function() { it("should produce an valid object", function() { var network = new Network({ + awayMessage: "I am away", name: "networkName", channels: [ new Chan({name: "#thelounge"}), @@ -21,6 +22,7 @@ describe("Network", function() { network.setNick("chillin`"); expect(network.export()).to.deep.equal({ + awayMessage: "I am away", name: "networkName", host: "", port: 6667, From 3f031ba6ffe6f1e9d8ff5b6e651ec16787243965 Mon Sep 17 00:00:00 2001 From: Michael van Tricht Date: Wed, 29 Mar 2017 10:11:12 +0200 Subject: [PATCH 0256/3926] Help page: commands can be autocompleted. --- client/index.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/index.html b/client/index.html index 0ec349a6..f34280e2 100644 --- a/client/index.html +++ b/client/index.html @@ -411,6 +411,8 @@

Commands

+ +

All commands can be autocompleted with tab.

From 9bf1e6e0d52e9e4ffa52c5d0b8e32a9209e60154 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sun, 19 Mar 2017 10:02:39 +0200 Subject: [PATCH 0257/3926] Do not build json3 module of Webpack --- package.json | 2 +- webpack.config.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ce8a9b98..53086a42 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "start-dev": "npm-run-all --parallel watch start", "build": "npm-run-all build:*", "build:font-awesome": "node scripts/build-fontawesome.js", - "build:webpack": "webpack", + "build:webpack": "webpack --progress", "watch": "webpack --watch", "test": "npm-run-all -c test:* lint", "test:mocha": "mocha", diff --git a/webpack.config.js b/webpack.config.js index 09c36561..edd58dd5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -60,6 +60,9 @@ let config = { }, ] }, + externals: { + json3: "JSON", // socket.io uses json3.js, but we do not target any browsers that need it + }, plugins: [ new webpack.optimize.CommonsChunkPlugin("js/bundle.vendor.js") ] From da0a52e3cb5c9ab00764d8e53722f8063d778cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Thu, 30 Mar 2017 02:19:26 -0400 Subject: [PATCH 0258/3926] Fix wrong font size in help center labels This only concerns plain texts, not `` or ``. --- client/css/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/css/style.css b/client/css/style.css index 6c5ac484..3287eba4 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1327,6 +1327,7 @@ kbd { #help .help-item .subject, #help .help-item .description { display: table-cell; + font-size: 14px; padding-bottom: 15px; } @@ -1337,7 +1338,6 @@ kbd { } #help .help-item .description p { - font-size: 14px; margin-bottom: 0; } From 92349976cb06b3d1bae7386a9d25ec1db5a124b0 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Thu, 30 Mar 2017 09:10:14 +0000 Subject: [PATCH 0259/3926] chore(package): update urijs to version 1.18.10 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2d01131a..14086ee4 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "nyc": "10.2.0", "socket.io-client": "1.7.3", "stylelint": "7.9.0", - "urijs": "1.18.9", + "urijs": "1.18.10", "webpack": "2.2.1" } } From 35b6b47de33700946261604d7aeba5bad1a67fcc Mon Sep 17 00:00:00 2001 From: Maxime Poulin Date: Sat, 3 Sep 2016 21:29:48 -0400 Subject: [PATCH 0260/3926] Remove table layout for chat messages (and fix layout issues yet again) --- client/css/style.css | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 3287eba4..14603d2b 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -832,15 +832,15 @@ kbd { } #chat .messages { - display: table; - table-layout: fixed; - width: 100%; padding: 10px 0; } #chat .msg { word-wrap: break-word; word-break: break-word; /* Webkit-specific */ + display: flex; + overflow: hidden; + position: relative; } #chat .unread-marker { @@ -912,16 +912,15 @@ kbd { #chat .time, #chat .from, #chat .text { - display: table-cell; + display: block; padding: 2px 0; - vertical-align: top; + flex: 0 0 auto; } #chat .time { color: #ddd; text-align: right; - max-width: 46px; - min-width: 46px; + width: 46px; } #chat .from { @@ -929,8 +928,14 @@ kbd { color: #b1c3ce; padding-right: 10px; text-align: right; - max-width: 134px; - min-width: 134px; + width: 134px; + align-self: stretch; +} + +#chat .text { + margin: auto; + overflow: hidden; + flex: 1 1 auto; } #loading a, From fb672ab57ff2a6d5ae8a54b2acef64d4e4354513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Thu, 30 Mar 2017 19:50:36 -0400 Subject: [PATCH 0261/3926] Improvements to the new flex layout for messages --- client/css/style.css | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 14603d2b..fc111802 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -929,12 +929,9 @@ kbd { padding-right: 10px; text-align: right; width: 134px; - align-self: stretch; } #chat .text { - margin: auto; - overflow: hidden; flex: 1 1 auto; } @@ -1001,14 +998,6 @@ kbd { color: #999; } -#chat .msg.motd .text, -#chat .msg.message .text, -#chat .msg.action .action-text, -#chat .msg.notice .text { - white-space: pre-wrap; - overflow: hidden; -} - #chat .msg.channel_list_loading .text { color: #999; font-style: italic; From e62da5b1ea4c5b1d96bd7b0739674f52f53fa9fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Fri, 31 Mar 2017 01:26:37 -0400 Subject: [PATCH 0262/3926] Remove extra newline to please ESLint See https://travis-ci.org/thelounge/lounge/jobs/217041734#L1200 --- client/js/lounge.js | 1 - 1 file changed, 1 deletion(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index a706b6d9..52873305 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -1296,7 +1296,6 @@ $(function() { // Store the "previous" value, for next time $(this).data("lastvalue", nick); - }); Mousetrap.bind([ From 6b641059c10c0ef1504a5ba23929973a289850ae Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 21 Mar 2017 22:57:55 +0000 Subject: [PATCH 0263/3926] chore(package): update webpack to version 2.3.2 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 14086ee4..313ecaf1 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,6 @@ "socket.io-client": "1.7.3", "stylelint": "7.9.0", "urijs": "1.18.10", - "webpack": "2.2.1" + "webpack": "2.3.2" } } From 0a06874a97ebc5c94b4571ba0c133ee5600e01f6 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 20 Mar 2017 19:07:33 +0000 Subject: [PATCH 0264/3926] chore(package): update jquery to version 3.2.1 Closes #969 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 313ecaf1..8c652fa5 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "font-awesome": "4.7.0", "handlebars": "4.0.6", "handlebars-loader": "1.4.0", - "jquery": "3.2.0", + "jquery": "3.2.1", "jquery-ui": "1.12.1", "mocha": "3.2.0", "mousetrap": "1.6.0", From 5ce8d934100d97bfa3f7b9ddc3d0f7548edf4d5d Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Wed, 22 Mar 2017 00:11:25 +0000 Subject: [PATCH 0265/3926] fix(package): update moment to version 2.18.1 Closes #976 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 313ecaf1..89b521b5 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "irc-framework": "2.6.1", "ldapjs": "1.0.1", "lodash": "4.17.4", - "moment": "2.18.0", + "moment": "2.18.1", "read": "1.0.7", "request": "2.81.0", "semver": "5.3.0", From 5923e48dda7538ee94f94a033b362baffc6f4f89 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 31 Mar 2017 20:50:18 +0000 Subject: [PATCH 0266/3926] chore(package): update eslint to version 3.19.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 313ecaf1..ba6e6340 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "babel-loader": "6.4.1", "babel-preset-es2015": "6.24.0", "chai": "3.5.0", - "eslint": "3.18.0", + "eslint": "3.19.0", "font-awesome": "4.7.0", "handlebars": "4.0.6", "handlebars-loader": "1.4.0", From 001f96035b37bf0369eb047e034e79e6e49ff41e Mon Sep 17 00:00:00 2001 From: S Date: Thu, 23 Mar 2017 08:47:51 +0100 Subject: [PATCH 0267/3926] Switch to bcryptjs and make password comparison async - PasswordCompareAsync prevents timeouts on resource constraint devices - All password.compare calls are now async - Updated tests to accept async functions --- package.json | 2 +- src/helper.js | 4 +-- src/server.js | 69 +++++++++++++++++++++++------------------ test/tests/passwords.js | 17 ++++++++-- 4 files changed, 57 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index 5615941c..63f68dce 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "node": ">=4.2.0" }, "dependencies": { - "bcrypt-nodejs": "0.0.3", + "bcryptjs": "2.4.3", "cheerio": "0.22.0", "colors": "1.1.2", "commander": "2.9.0", diff --git a/src/helper.js b/src/helper.js index c6e971c1..830b91c9 100644 --- a/src/helper.js +++ b/src/helper.js @@ -6,7 +6,7 @@ var path = require("path"); var os = require("os"); var fs = require("fs"); var net = require("net"); -var bcrypt = require("bcrypt-nodejs"); +var bcrypt = require("bcryptjs"); var Helper = { config: null, @@ -125,5 +125,5 @@ function passwordHash(password) { } function passwordCompare(password, expected) { - return bcrypt.compareSync(password, expected); + return bcrypt.compare(password, expected); } diff --git a/src/server.js b/src/server.js index 636b5ab6..28b35436 100644 --- a/src/server.js +++ b/src/server.js @@ -192,27 +192,33 @@ function init(socket, client) { }); return; } - if (!Helper.password.compare(old || "", client.config.password)) { - socket.emit("change-password", { - error: "The current password field does not match your account password" + + Helper.password + .compare(old || "", client.config.password) + .then(matching => { + if (!matching) { + socket.emit("change-password", { + error: "The current password field does not match your account password" + }); + return; + } + const hash = Helper.password.hash(p1); + + client.setPassword(hash, success => { + const obj = {}; + + if (success) { + obj.success = "Successfully updated your password, all your other sessions were logged out"; + obj.token = client.config.token; + } else { + obj.error = "Failed to update your password"; + } + + socket.emit("change-password", obj); + }); + }).catch(error => { + log.error(`Error while checking users password. Error: ${error}`); }); - return; - } - - var hash = Helper.password.hash(p1); - - client.setPassword(hash, function(success) { - var obj = {}; - - if (success) { - obj.success = "Successfully updated your password, all your other sessions were logged out"; - obj.token = client.config.token; - } else { - obj.error = "Failed to update your password"; - } - - socket.emit("change-password", obj); - }); } ); } @@ -267,19 +273,22 @@ function localAuth(client, user, password, callback) { return callback(false); } - var result = Helper.password.compare(password, client.config.password); + Helper.password + .compare(password, client.config.password) + .then(matching => { + if (Helper.password.requiresUpdate(client.config.password)) { + const hash = Helper.password.hash(password); - if (result && Helper.password.requiresUpdate(client.config.password)) { - var hash = Helper.password.hash(password); - - client.setPassword(hash, function(success) { - if (success) { - log.info(`User ${colors.bold(client.name)} logged in and their hashed password has been updated to match new security requirements`); + client.setPassword(hash, success => { + if (success) { + log.info(`User ${colors.bold(client.name)} logged in and their hashed password has been updated to match new security requirements`); + } + }); } + callback(matching); + }).catch(error => { + log.error(`Error while checking users password. Error: ${error}`); }); - } - - return callback(result); } function ldapAuth(client, user, password, callback) { diff --git a/test/tests/passwords.js b/test/tests/passwords.js index d074477d..48d45c9d 100644 --- a/test/tests/passwords.js +++ b/test/tests/passwords.js @@ -10,14 +10,27 @@ describe("Client passwords", function() { // Generated with third party tool to test implementation let comparedPassword = Helper.password.compare(inputPassword, "$2a$11$zrPPcfZ091WNfs6QrRHtQeUitlgrJcecfZhxOFiQs0FWw7TN3Q1oS"); - expect(comparedPassword).to.be.true; + return comparedPassword.then(result => { + expect(result).to.be.true; + }); + }); + + it("wrong hashed password should not match", function() { + // Compare against a fake hash + let comparedPassword = Helper.password.compare(inputPassword, "$2a$11$zrPPcfZ091WRONGPASSWORDitlgrJcecfZhxOFiQs0FWw7TN3Q1oS"); + + return comparedPassword.then(result => { + expect(result).to.be.false; + }); }); it("freshly hashed password should match", function() { let hashedPassword = Helper.password.hash(inputPassword); let comparedPassword = Helper.password.compare(inputPassword, hashedPassword); - expect(comparedPassword).to.be.true; + return comparedPassword.then((result) => { + expect(result).to.be.true; + }); }); it("shout passwords should be marked as old", function() { From 110c0f0c87cba3e4d44a62dc54ce1d55e69fab01 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 1 Apr 2017 11:06:01 +0300 Subject: [PATCH 0268/3926] Correctly append date marker when receiving a message --- client/js/lounge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 43775dc2..fa3ec21d 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -432,7 +432,7 @@ $(function() { } if (prevMsgTime.toDateString() !== msgTime.toDateString()) { - prevMsg.append(templates.date_marker({msgDate: msgTime})); + prevMsg.after(templates.date_marker({msgDate: msgTime})); } // Add message to the container From 4d592a6a4094f1df82c67980f0490b43b93ac364 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sat, 1 Apr 2017 16:14:00 +0000 Subject: [PATCH 0269/3926] chore(package): update stylelint to version 7.10.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 63f68dce..5b5b9d20 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "npm-run-all": "4.0.2", "nyc": "10.2.0", "socket.io-client": "1.7.3", - "stylelint": "7.9.0", + "stylelint": "7.10.0", "urijs": "1.18.10", "webpack": "2.3.2" } From c0e364e1c2213b668381f06c67ad474d9d7ed1e1 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 1 Apr 2017 11:33:17 +0300 Subject: [PATCH 0270/3926] Store channel keys --- src/client.js | 3 +- src/models/chan.js | 1 + src/models/network.js | 3 +- src/plugins/irc-events/connection.js | 2 +- src/plugins/irc-events/join.js | 3 ++ src/plugins/irc-events/mode.js | 70 ++++++++++++++++++++-------- test/models/network.js | 12 +++-- 7 files changed, 68 insertions(+), 26 deletions(-) diff --git a/src/client.js b/src/client.js index 00f3eb35..27387778 100644 --- a/src/client.js +++ b/src/client.js @@ -159,7 +159,8 @@ Client.prototype.connect = function(args) { } channels.push(new Chan({ - name: chan.name + name: chan.name, + key: chan.key || "", })); }); diff --git a/src/models/chan.js b/src/models/chan.js index 69e582ab..1448be06 100644 --- a/src/models/chan.js +++ b/src/models/chan.js @@ -19,6 +19,7 @@ function Chan(attr) { id: id++, messages: [], name: "", + key: "", topic: "", type: Chan.Type.CHANNEL, firstUnread: 0, diff --git a/src/models/network.js b/src/models/network.js index 4484ed96..f5576133 100644 --- a/src/models/network.js +++ b/src/models/network.js @@ -87,7 +87,8 @@ Network.prototype.export = function() { }) .map(function(chan) { return _.pick(chan, [ - "name" + "name", + "key", ]); }); diff --git a/src/plugins/irc-events/connection.js b/src/plugins/irc-events/connection.js index 4e34a154..47cb331c 100644 --- a/src/plugins/irc-events/connection.js +++ b/src/plugins/irc-events/connection.js @@ -47,7 +47,7 @@ module.exports = function(irc, network) { } setTimeout(function() { - network.irc.join(chan.name); + network.irc.join(chan.name, chan.key); }, delay); delay += 1000; }); diff --git a/src/plugins/irc-events/join.js b/src/plugins/irc-events/join.js index c7f91ec9..1ad88e6f 100644 --- a/src/plugins/irc-events/join.js +++ b/src/plugins/irc-events/join.js @@ -18,6 +18,9 @@ module.exports = function(irc, network) { network: network.id, chan: chan }); + + // Request channels' modes + network.irc.raw("MODE", chan.name); } chan.users.push(new User({nick: data.nick})); chan.sortUsers(irc); diff --git a/src/plugins/irc-events/mode.js b/src/plugins/irc-events/mode.js index b47985c9..44db8a73 100644 --- a/src/plugins/irc-events/mode.js +++ b/src/plugins/irc-events/mode.js @@ -1,13 +1,40 @@ "use strict"; -var _ = require("lodash"); -var Chan = require("../../models/chan"); -var Msg = require("../../models/msg"); +const _ = require("lodash"); +const Chan = require("../../models/chan"); +const Msg = require("../../models/msg"); module.exports = function(irc, network) { - var client = this; + const client = this; + + // The following saves the channel key based on channel mode instead of + // extracting it from `/join #channel key`. This lets us not have to + // temporarily store the key until successful join, but also saves the key + // if a key is set or changed while being on the channel. + irc.on("channel info", function(data) { + if (!data.modes) { + return; + } + + const targetChan = network.getChannel(data.channel); + if (typeof targetChan === "undefined") { + return; + } + + data.modes.forEach(mode => { + const text = mode.mode; + const add = text[0] === "+"; + const char = text[1]; + + if (char === "k") { + targetChan.key = add ? mode.param : ""; + client.save(); + } + }); + }); + irc.on("mode", function(data) { - var targetChan; + let targetChan; if (data.target === irc.user.nick) { targetChan = network.channels[0]; @@ -18,23 +45,29 @@ module.exports = function(irc, network) { } } - var usersUpdated; - var supportsMultiPrefix = network.irc.network.cap.isEnabled("multi-prefix"); - var userModeSortPriority = {}; + let usersUpdated; + let userModeSortPriority = {}; + const supportsMultiPrefix = network.irc.network.cap.isEnabled("multi-prefix"); irc.network.options.PREFIX.forEach((prefix, index) => { userModeSortPriority[prefix.symbol] = index; }); - for (var i = 0; i < data.modes.length; i++) { - var mode = data.modes[i]; - var text = mode.mode; + data.modes.forEach(mode => { + let text = mode.mode; + const add = text[0] === "+"; + const char = text[1]; + + if (char === "k") { + targetChan.key = add ? mode.param : ""; + client.save(); + } if (mode.param) { text += " " + mode.param; } - var msg = new Msg({ + const msg = new Msg({ time: data.time, type: Msg.Type.MODE, mode: (targetChan.type !== Chan.Type.LOBBY && targetChan.getMode(data.nick)) || "", @@ -45,22 +78,21 @@ module.exports = function(irc, network) { targetChan.pushMessage(client, msg); if (!mode.param) { - continue; + return; } - var user = _.find(targetChan.users, {name: mode.param}); + const user = _.find(targetChan.users, {name: mode.param}); if (!user) { - continue; + return; } usersUpdated = true; if (!supportsMultiPrefix) { - continue; + return; } - var add = mode.mode[0] === "+"; - var changedMode = network.prefixLookup[mode.mode[1]]; + const changedMode = network.prefixLookup[char]; if (!add) { _.pull(user.modes, changedMode); @@ -73,7 +105,7 @@ module.exports = function(irc, network) { // TODO: remove in future user.mode = (user.modes && user.modes[0]) || ""; - } + }); if (!usersUpdated) { return; diff --git a/test/models/network.js b/test/models/network.js index a5ebb5e4..1727580c 100644 --- a/test/models/network.js +++ b/test/models/network.js @@ -13,8 +13,10 @@ describe("Network", function() { awayMessage: "I am away", name: "networkName", channels: [ - new Chan({name: "#thelounge"}), - new Chan({name: "&foobar"}), + new Chan({name: "#thelounge", key: ""}), + new Chan({name: "&foobar", key: ""}), + new Chan({name: "#secret", key: "foo"}), + new Chan({name: "&secure", key: "bar"}), new Chan({name: "Channel List", type: Chan.Type.SPECIAL}), new Chan({name: "PrivateChat", type: Chan.Type.QUERY}), ] @@ -35,8 +37,10 @@ describe("Network", function() { ip: null, hostname: null, channels: [ - {name: "#thelounge"}, - {name: "&foobar"}, + {name: "#thelounge", key: ""}, + {name: "&foobar", key: ""}, + {name: "#secret", key: "foo"}, + {name: "&secure", key: "bar"}, ] }); }); From 2d9aa35c063e6c5a4e8241eee3630b82f86f026a Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 11 Mar 2017 12:41:46 +0200 Subject: [PATCH 0271/3926] Implement pgup/pgdown keys --- client/js/lounge.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/client/js/lounge.js b/client/js/lounge.js index fa3ec21d..a7a67796 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -1301,6 +1301,33 @@ $(function() { }); (function HotkeysScope() { + Mousetrap.bind([ + "pageup", + "pagedown" + ], function(e, key) { + let container = windows.find(".window.active"); + + // Chat windows scroll message container + if (container.attr("id") === "chat-container") { + container = container.find(".chan.active .chat"); + } + + const offset = container.get(0).clientHeight * 0.94; + let scrollTop = container.scrollTop(); + + if (key === "pageup") { + scrollTop -= offset; + } else { + scrollTop += offset; + } + + container.stop().animate({ + scrollTop: scrollTop + }, 200); + + return false; + }); + Mousetrap.bind([ "command+up", "command+down", From 332047c0dc4cdcb26532b23c43f17421a3f2eeb9 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sun, 2 Apr 2017 10:54:48 +0000 Subject: [PATCH 0272/3926] chore(package): update stylelint to version 7.10.1 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b5b9d20..251c4aa4 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "npm-run-all": "4.0.2", "nyc": "10.2.0", "socket.io-client": "1.7.3", - "stylelint": "7.10.0", + "stylelint": "7.10.1", "urijs": "1.18.10", "webpack": "2.3.2" } From 855092aa44c46c3a9306971003d538032c797150 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sun, 2 Apr 2017 19:46:29 +0000 Subject: [PATCH 0273/3926] chore(package): update mousetrap to version 1.6.1 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 251c4aa4..6990a606 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "jquery": "3.2.1", "jquery-ui": "1.12.1", "mocha": "3.2.0", - "mousetrap": "1.6.0", + "mousetrap": "1.6.1", "npm-run-all": "4.0.2", "nyc": "10.2.0", "socket.io-client": "1.7.3", From 09eaf80f8c824f16999afd3a88f10cf0baa37189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sun, 2 Apr 2017 21:03:01 -0400 Subject: [PATCH 0274/3926] Fix page scroll glitch --- client/js/lounge.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index a7a67796..00ab4f02 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -808,7 +808,7 @@ $(function() { var input = $("#input") .history() - .on("input keyup", function() { + .on("input", function() { var style = window.getComputedStyle(this); // Start by resetting height before computing as scrollHeight does not @@ -1312,13 +1312,13 @@ $(function() { container = container.find(".chan.active .chat"); } - const offset = container.get(0).clientHeight * 0.94; + const offset = container.get(0).clientHeight * 0.9; let scrollTop = container.scrollTop(); if (key === "pageup") { - scrollTop -= offset; + scrollTop = Math.floor(scrollTop - offset); } else { - scrollTop += offset; + scrollTop = Math.ceil(scrollTop + offset); } container.stop().animate({ From e1ff04174f7ec129118e858583ab2ae27d6a1df5 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 3 Apr 2017 08:03:32 +0000 Subject: [PATCH 0275/3926] chore(package): update webpack to version 2.3.3 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6990a606..e16a0b8a 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,6 @@ "socket.io-client": "1.7.3", "stylelint": "7.10.1", "urijs": "1.18.10", - "webpack": "2.3.2" + "webpack": "2.3.3" } } From 6a273d825aed3b4fdc7dbb9461c92ed7479ad672 Mon Sep 17 00:00:00 2001 From: Maxime Poulin Date: Fri, 22 Jul 2016 23:01:55 -0400 Subject: [PATCH 0276/3926] Improve inline previews for links and images --- client/css/style.css | 21 +++++++++++++++++---- client/views/toggle.tpl | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index fc111802..7f46637c 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1105,6 +1105,7 @@ kbd { max-width: 100%; padding: 6px 8px; margin-top: 2px; + overflow: hidden; } #chat .toggle-content a { @@ -1113,18 +1114,23 @@ kbd { #chat .toggle-content img { max-width: 100%; - max-height: 250px; + max-height: 128px; display: block; margin: 2px 0; } #chat .toggle-content .thumb { - max-height: 110px; - max-width: 210px; + float: left; + margin-right: 10px; + max-width: 48px; + max-height: 36px; } #chat .toggle-content .head { font-weight: bold; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; } #chat .toggle-content .body { @@ -1132,10 +1138,17 @@ kbd { max-width: 460px; word-break: normal; word-wrap: break-word; + overflow: hidden; + max-height: 30px; + text-overflow: ellipsis; } #chat .toggle-content.show { - display: inline-block !important; + display: block; +} + +#chat .toggle-content.toggle-type-image.show { + display: inline; } #chat .count { diff --git a/client/views/toggle.tpl b/client/views/toggle.tpl index 1c51c5f5..2c8f84eb 100644 --- a/client/views/toggle.tpl +++ b/client/views/toggle.tpl @@ -1,5 +1,5 @@ {{#toggle}} -
+
{{#equal type "image"}} From d842517c4ec2f11e157feb77c3feb855ebda695d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 4 Apr 2017 02:09:53 -0400 Subject: [PATCH 0277/3926] Fix image preview talking full width Bootstrap was taking over these declarations because they use `!important`. --- client/css/style.css | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 7f46637c..acbcd8df 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1144,11 +1144,7 @@ kbd { } #chat .toggle-content.show { - display: block; -} - -#chat .toggle-content.toggle-type-image.show { - display: inline; + display: inline-block !important; } #chat .count { From dce42df05031185f0b7d13fac2cb4f9dc520c08f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 4 Apr 2017 02:30:16 -0400 Subject: [PATCH 0278/3926] Fix link preview title going underneath the user list Also fix the preview description not respecting the ellipsis, and update the image size and margin to nicely align with text. --- client/css/style.css | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index acbcd8df..f32261e4 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -933,6 +933,7 @@ kbd { #chat .text { flex: 1 1 auto; + overflow: hidden; } #loading a, @@ -1103,7 +1104,7 @@ kbd { color: #222; font-size: 12px; max-width: 100%; - padding: 6px 8px; + padding: 6px; margin-top: 2px; overflow: hidden; } @@ -1116,31 +1117,28 @@ kbd { max-width: 100%; max-height: 128px; display: block; - margin: 2px 0; } #chat .toggle-content .thumb { float: left; - margin-right: 10px; + margin-right: 6px; max-width: 48px; - max-height: 36px; + max-height: 32px; } -#chat .toggle-content .head { - font-weight: bold; +#chat .toggle-content .head, +#chat .toggle-content .body { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } +#chat .toggle-content .head { + font-weight: bold; +} + #chat .toggle-content .body { color: #999; - max-width: 460px; - word-break: normal; - word-wrap: break-word; - overflow: hidden; - max-height: 30px; - text-overflow: ellipsis; } #chat .toggle-content.show { From c066f25b171eb7fe2e6a02b045c968e3095f70af Mon Sep 17 00:00:00 2001 From: Awal Garg Date: Thu, 6 Apr 2017 00:45:28 +0530 Subject: [PATCH 0279/3926] fix: count only message items for show-more the `messages` div contains a `date-marker` div and an `unread-marker` div. this causes the `count` variable to be 2 more than the expected value, which makes the show-more button skip two messages when loading history. this change filters the counted elements to fix this issue. --- client/js/lounge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index fa3ec21d..3edd49dc 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -1189,7 +1189,7 @@ $(function() { chat.on("click", ".show-more-button", function() { var self = $(this); - var count = self.parent().next(".messages").children().length; + var count = self.parent().next(".messages").children(".msg").length; socket.emit("more", { target: self.data("id"), count: count From fe7c570cc9f8ac283618e8f5743c0d72dad6325b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Thu, 6 Apr 2017 02:25:43 -0400 Subject: [PATCH 0280/3926] Use Referrer-Policy header instead of CSP referrer According to MDN: > referrer > Used to specify information in the referer (sic) header for links away from a page. > Use the Referrer-Policy header instead. See: - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/referrer - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy --- src/server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server.js b/src/server.js index 28b35436..846ff78b 100644 --- a/src/server.js +++ b/src/server.js @@ -131,7 +131,8 @@ function index(req, res, next) { return css.slice(0, -4); }); var template = _.template(file); - res.setHeader("Content-Security-Policy", "default-src *; connect-src 'self' ws: wss:; style-src * 'unsafe-inline'; script-src 'self'; child-src 'self'; object-src 'none'; form-action 'none'; referrer no-referrer;"); + res.setHeader("Content-Security-Policy", "default-src *; connect-src 'self' ws: wss:; style-src * 'unsafe-inline'; script-src 'self'; child-src 'self'; object-src 'none'; form-action 'none';"); + res.setHeader("Referrer-Policy", "no-referrer"); res.setHeader("Content-Type", "text/html"); res.writeHead(200); res.end(template(data)); From 8744d754ff17db520e61d37915cd8bfb57d6ec59 Mon Sep 17 00:00:00 2001 From: Michael van Tricht Date: Thu, 6 Apr 2017 16:45:01 +0200 Subject: [PATCH 0281/3926] Fix Zenburn and Morning channel list font color. --- client/themes/morning.css | 4 ++++ client/themes/zenburn.css | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/client/themes/morning.css b/client/themes/morning.css index 49750ecd..9698330c 100644 --- a/client/themes/morning.css +++ b/client/themes/morning.css @@ -212,6 +212,10 @@ body { color: #84ce88 !important; } +#chat table.channel-list td { + color: #ccc; +} + /* Embeds */ #chat .toggle-content, #chat .toggle-button { diff --git a/client/themes/zenburn.css b/client/themes/zenburn.css index a5969370..2c075412 100644 --- a/client/themes/zenburn.css +++ b/client/themes/zenburn.css @@ -238,6 +238,10 @@ body { color: #8cd0d3 !important; } +#chat table.channel-list td { + color: #ccc; +} + /* Embeds */ #chat .toggle-content, #chat .toggle-button { From ba2aa7a852d696b8ca4a86c636ff3f1111156e01 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 7 Apr 2017 15:55:35 +0000 Subject: [PATCH 0282/3926] chore(package): update babel-preset-es2015 to version 6.24.1 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e16a0b8a..57e10cca 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "devDependencies": { "babel-core": "6.24.0", "babel-loader": "6.4.1", - "babel-preset-es2015": "6.24.0", + "babel-preset-es2015": "6.24.1", "chai": "3.5.0", "eslint": "3.19.0", "font-awesome": "4.7.0", From 7c5f63131944f8c633f402fff75fea34cd52bd25 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 7 Apr 2017 17:11:52 +0000 Subject: [PATCH 0283/3926] chore(package): update babel-core to version 6.24.1 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e16a0b8a..c0ed6045 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "spdy": "3.4.4" }, "devDependencies": { - "babel-core": "6.24.0", + "babel-core": "6.24.1", "babel-loader": "6.4.1", "babel-preset-es2015": "6.24.0", "chai": "3.5.0", From bcbd29cd22efa98d7b31bea8040ba39afafc12dd Mon Sep 17 00:00:00 2001 From: Michael van Tricht Date: Thu, 6 Apr 2017 13:09:55 +0200 Subject: [PATCH 0284/3926] Unread marker takes hidden messages into account. --- client/js/lounge.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index fcc06179..3bd2e50f 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -443,7 +443,8 @@ $(function() { data ]); - if (data.msg.self) { + if (data.msg.self + || container.find("div:visible").last().hasClass("unread-marker")) { container .find(".unread-marker") .appendTo(container); From c0a7ae9d92050d80f8a8575bc015af62c426372e Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Mon, 19 Dec 2016 16:59:06 +0200 Subject: [PATCH 0285/3926] Use css tooltips on time elements --- client/css/style.css | 1 - client/views/msg.tpl | 2 +- client/views/msg_action.tpl | 2 +- client/views/msg_unhandled.tpl | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index f32261e4..f96771ed 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -839,7 +839,6 @@ kbd { word-wrap: break-word; word-break: break-word; /* Webkit-specific */ display: flex; - overflow: hidden; position: relative; } diff --git a/client/views/msg.tpl b/client/views/msg.tpl index 36dc4c27..2fa5f930 100644 --- a/client/views/msg.tpl +++ b/client/views/msg.tpl @@ -1,5 +1,5 @@
- + {{tz time}} diff --git a/client/views/msg_action.tpl b/client/views/msg_action.tpl index c2e9657c..02187a3f 100644 --- a/client/views/msg_action.tpl +++ b/client/views/msg_action.tpl @@ -1,5 +1,5 @@
- + {{tz time}} diff --git a/client/views/msg_unhandled.tpl b/client/views/msg_unhandled.tpl index 9c30f3d3..1a6fcfd7 100644 --- a/client/views/msg_unhandled.tpl +++ b/client/views/msg_unhandled.tpl @@ -1,5 +1,5 @@
- + {{tz time}} [{{command}}] From 5d36b29aa8949b632ceacdac86a6eb1f06bfd277 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Mon, 19 Dec 2016 16:59:19 +0200 Subject: [PATCH 0286/3926] Only disable touch tooltips on certain buttons --- client/css/style.css | 8 ++------ client/index.html | 16 ++++++++-------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index f96771ed..a7894265 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1763,12 +1763,8 @@ kbd { * - https://www.w3.org/TR/mediaqueries-4/ * - https://developer.mozilla.org/en-US/docs/Web/CSS/@media/hover */ - .tooltipped:hover:before, - .tooltipped:hover:after, - .tooltipped:active:before, - .tooltipped:active:after, - .tooltipped:focus:before, - .tooltipped:focus:after { + .tooltipped-no-touch:hover:before, + .tooltipped-no-touch:hover:after { visibility: hidden; opacity: 0; } diff --git a/client/index.html b/client/index.html index f1e3641a..9f6d64d6 100644 --- a/client/index.html +++ b/client/index.html @@ -34,11 +34,11 @@
- - - - - + + + + +
@@ -68,10 +68,10 @@ --> - + - +
@@ -523,7 +523,7 @@

Commands

- +

All commands can be autocompleted with tab.

From 5b721c1b9997e4db87de8999bd781316a8317e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 4 Apr 2017 01:27:13 -0400 Subject: [PATCH 0287/3926] Update Primer tooltips to latest v0.5.3 This: - Makes tooltips appear after timer instead of instantly, necessary for timestamp tooltips (see https://github.com/thelounge/lounge/pull/824#pullrequestreview-13676231) - Uses Primer default animation (not sure if .2s transition was ours or theirs but here it is) - Goes closer to default tooltips which will help to bump future versions and/or to streamline this in build process --- client/css/style.css | 61 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index a7894265..89a90bfc 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1519,9 +1519,10 @@ kbd { } /** - * Tooltips + * Tooltips v0.5.3 * See http://primercss.io/tooltips/ */ + .tooltipped { position: relative; } @@ -1529,12 +1530,11 @@ kbd { .tooltipped:after { position: absolute; z-index: 1000000; - display: inline-block; - visibility: hidden; - opacity: 0; + display: none; padding: 5px 8px; font: 12px Lato; line-height: 1.2; + -webkit-font-smoothing: subpixel-antialiased; color: #fff; text-align: center; text-decoration: none; @@ -1547,23 +1547,40 @@ kbd { content: attr(aria-label); background: #222; border-radius: 3px; - -webkit-font-smoothing: subpixel-antialiased; - transition: .2s; + opacity: 0; } .tooltipped:before { position: absolute; z-index: 1000001; - display: inline-block; - visibility: hidden; - opacity: 0; + display: none; width: 0; height: 0; color: #fff; pointer-events: none; content: ""; border: 5px solid transparent; - transition: .2s; + opacity: 0; +} + +@-webkit-keyframes tooltip-appear { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes tooltip-appear { + from { + opacity: 0; + } + + to { + opacity: 1; + } } .tooltipped:hover:before, @@ -1572,9 +1589,18 @@ kbd { .tooltipped:active:after, .tooltipped:focus:before, .tooltipped:focus:after { - visibility: visible; - opacity: 1; + display: inline-block; text-decoration: none; + -webkit-animation-name: tooltip-appear; + animation-name: tooltip-appear; + -webkit-animation-duration: .1s; + animation-duration: .1s; + -webkit-animation-fill-mode: forwards; + animation-fill-mode: forwards; + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + -webkit-animation-delay: .4s; + animation-delay: .4s; } .tooltipped-s:after, @@ -1671,6 +1697,17 @@ kbd { border-right-color: #222; } +@media only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and (min--moz-device-pixel-ratio: 2), + only screen and (-moz-min-device-pixel-ratio: 2), + only screen and (min-device-pixel-ratio: 2), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 2dppx) { + .tooltipped-w:after { + margin-right: 4.5px; + } +} + /* End tooltips */ /** From b7d353b6200f7f9841779da5ba4b1bd20c3e2cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Thu, 13 Apr 2017 02:30:36 -0400 Subject: [PATCH 0288/3926] Remove invalid CSS perspective properties These are not valid without units per the CSS validator, which is confirmed in the Chrome dev tools. I could not trigger any consequences by removing these. --- client/css/style.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 89a90bfc..64da0489 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -310,8 +310,6 @@ kbd { transition: transform 160ms, -webkit-transform 160ms; -webkit-transform: translateZ(0); transform: translateZ(0); - -webkit-perspective: 1000; - perspective: 1000; } #viewport.menu-open { @@ -791,8 +789,6 @@ kbd { transition: all .4s; -webkit-transform: translateZ(0); transform: translateZ(0); - -webkit-perspective: 1000; - perspective: 1000; } #chat .lobby .chat, From f645c32cb944fb30c22e9e69fc9000ee625139b9 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 14 Apr 2017 00:05:28 +0300 Subject: [PATCH 0289/3926] Use local variables to check length --- src/server.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/server.js b/src/server.js index 846ff78b..42c3461a 100644 --- a/src/server.js +++ b/src/server.js @@ -42,17 +42,20 @@ module.exports = function() { server = require("http"); server = server.createServer(app); } else { - server = require("spdy"); const keyPath = Helper.expandHome(config.https.key); const certPath = Helper.expandHome(config.https.certificate); - if (!config.https.key.length || !fs.existsSync(keyPath)) { + + if (!keyPath.length || !fs.existsSync(keyPath)) { log.error("Path to SSL key is invalid. Stopping server..."); process.exit(); } - if (!config.https.certificate.length || !fs.existsSync(certPath)) { + + if (!certPath.length || !fs.existsSync(certPath)) { log.error("Path to SSL certificate is invalid. Stopping server..."); process.exit(); } + + server = require("spdy"); server = server.createServer({ key: fs.readFileSync(keyPath), cert: fs.readFileSync(certPath) From fce3d11e7497a39cf9ce63879ed1b3ad0e16c34e Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 14 Apr 2017 21:29:04 +0300 Subject: [PATCH 0290/3926] Stick to bottom when opening user list Fixes #1031 --- client/js/lounge.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index fcc06179..83d7441b 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -729,6 +729,7 @@ $(function() { var self = $(this); viewport.toggleClass(self.attr("class")); e.stopPropagation(); + chat.find(".chan.active .chat").trigger("msg.sticky"); }); function positionContextMenu(that, e) { @@ -822,7 +823,7 @@ $(function() { + Math.round(parseFloat(style.borderBottomWidth) || 0) ) + "px"; - $("#chat .chan.active .chat").trigger("msg.sticky"); // fix growing + chat.find(".chan.active .chat").trigger("msg.sticky"); // fix growing }) .tab(complete, {hint: false}); From 507bf05d2410802acdedcc1834e278ef1cfb41da Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 14 Apr 2017 21:10:05 +0300 Subject: [PATCH 0291/3926] Remove referrer meta tag, we send Referrer-Policy header --- client/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/client/index.html b/client/index.html index 9f6d64d6..967bef53 100644 --- a/client/index.html +++ b/client/index.html @@ -8,7 +8,6 @@ - The Lounge From 8020c3c81713e93a8d16fb4b24c772de9024e383 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 14 Apr 2017 21:10:17 +0300 Subject: [PATCH 0292/3926] Preload scripts as soon as possible --- client/index.html | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/client/index.html b/client/index.html index 967bef53..9d4e58a2 100644 --- a/client/index.html +++ b/client/index.html @@ -4,23 +4,26 @@ - - - - - - - The Lounge + + + The Lounge + + + + + + + "> From 1e2e8a82dbaff79c26865728a286dda451d4941b Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 14 Apr 2017 21:36:41 +0300 Subject: [PATCH 0293/3926] Add rel noopener to URLs in index.html --- client/index.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/client/index.html b/client/index.html index 9f6d64d6..5502e712 100644 --- a/client/index.html +++ b/client/index.html @@ -49,7 +49,7 @@

The Lounge is loading…

-

Loading the app… Make sure to have JavaScript enabled.

+

Loading the app… Make sure to have JavaScript enabled.

This is taking longer than it should, there might be connectivity issues.

@@ -402,7 +402,7 @@

A color reference can be found - here. + here.

@@ -478,7 +478,7 @@

A color reference can be found - here. + here.

@@ -575,7 +575,7 @@

Send a CTCP request. Read more about this on - the dedicated Wikipedia article. + the dedicated Wikipedia article.

@@ -809,15 +809,15 @@

<% if (gitCommit) { %> The Lounge is running from source - (<%= gitCommit %>).
+ (<%= gitCommit %>).
<% } else { %> The Lounge is in version <%= version %> - (See release notes).
+ (See release notes).
<% } %> - Website
- Documentation
- Report a bug + Website
+ Documentation
+ Report a bug

From d093a7f4c2955eb47bf02aeaab8f701753e34984 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 15 Apr 2017 18:40:19 +0300 Subject: [PATCH 0294/3926] Reset notification markers on document focus Fixes #837 --- client/js/lounge.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 83d7441b..ecbe493d 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -1595,14 +1595,11 @@ $(function() { $("#viewport .lt").toggleClass("notified", newState); } - document.addEventListener( - "visibilitychange", - function() { - if (sidebar.find(".highlight").length === 0) { - toggleNotificationMarkers(false); - } + $(document).on("visibilitychange focus", () => { + if (sidebar.find(".highlight").length === 0) { + toggleNotificationMarkers(false); } - ); + }); // Only start opening socket.io connection after all events have been registered socket.open(); From fa51a2c281d60464f1642c1f6e05ee921f069e08 Mon Sep 17 00:00:00 2001 From: Metsjeesus Date: Mon, 10 Apr 2017 18:49:58 +0000 Subject: [PATCH 0295/3926] Add CA bundle option in SSL --- defaults/config.js | 11 ++++++++++- src/server.js | 9 ++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/defaults/config.js b/defaults/config.js index 98f4876c..30a66bd9 100644 --- a/defaults/config.js +++ b/defaults/config.js @@ -287,7 +287,16 @@ module.exports = { // @example "sslcert/key-cert.pem" // @default "" // - certificate: "" + certificate: "", + + // + // Path to the CA bundle. + // + // @type string + // @example "sslcert/bundle.pem" + // @default "" + // + ca: "" }, // diff --git a/src/server.js b/src/server.js index 42c3461a..0ff97b76 100644 --- a/src/server.js +++ b/src/server.js @@ -44,6 +44,7 @@ module.exports = function() { } else { const keyPath = Helper.expandHome(config.https.key); const certPath = Helper.expandHome(config.https.certificate); + const caPath = Helper.expandHome(config.https.ca); if (!keyPath.length || !fs.existsSync(keyPath)) { log.error("Path to SSL key is invalid. Stopping server..."); @@ -55,10 +56,16 @@ module.exports = function() { process.exit(); } + if (caPath.length && !fs.existsSync(caPath)) { + log.error("Path to SSL ca bundle is invalid. Stopping server..."); + process.exit(); + } + server = require("spdy"); server = server.createServer({ key: fs.readFileSync(keyPath), - cert: fs.readFileSync(certPath) + cert: fs.readFileSync(certPath), + ca: caPath ? fs.readFileSync(caPath) : undefined }, app); } From cc0962ba12ece0454dc8a768f74143f2136003e1 Mon Sep 17 00:00:00 2001 From: William Boman Date: Sat, 28 Jan 2017 18:48:34 +0100 Subject: [PATCH 0296/3926] client: implement states using the History Web API --- client/js/lounge.js | 55 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index ecbe493d..e10c9d73 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -155,7 +155,9 @@ $(function() { return; } sidebar.find(".sign-in") - .click() + .trigger("click", { + pushState: false, + }) .end() .find(".networks") .html("") @@ -197,7 +199,9 @@ $(function() { $("#loading-page-message").text("Rendering…"); if (data.networks.length === 0) { - $("#footer").find(".connect").trigger("click"); + $("#footer").find(".connect").trigger("click", { + pushState: false, + }); } else { renderNetworks(data); } @@ -219,7 +223,9 @@ $(function() { .eq(0) .trigger("click"); if (first.length === 0) { - $("#footer").find(".connect").trigger("click"); + $("#footer").find(".connect").trigger("click", { + pushState: false, + }); } } }); @@ -986,6 +992,33 @@ $(function() { }); }); + sidebar.on("click", ".chan, button", function(e, data) { + // Pushes states to history web API when clicking elements with a data-target attribute. + // States are very trivial and only contain a single `clickTarget` property which + // contains a CSS selector that targets elements which takes the user to a different view + // when clicked. The `popstate` event listener will trigger synthetic click events using that + // selector and thus take the user to a different view/state. + if (data && data.pushState === false) { + return; + } + const self = $(this); + const target = self.data("target"); + if (!target) { + return; + } + const state = {}; + + if (self.hasClass("chan")) { + state.clickTarget = `.chan[data-id="${self.data("id")}"]`; + } else { + state.clickTarget = `#footer button[data-target="${target}"]`; + } + + if (history && history.pushState) { + history.pushState(state, null, null); + } + }); + sidebar.on("click", ".chan, button", function() { var self = $(this); var target = self.data("target"); @@ -1603,4 +1636,20 @@ $(function() { // Only start opening socket.io connection after all events have been registered socket.open(); + + window.addEventListener( + "popstate", + (e) => { + const {state} = e; + if (!state) { + return; + } + const {clickTarget} = state; + if (clickTarget) { + $(clickTarget).trigger("click", { + pushState: false + }); + } + } + ); }); From 5c336d3789d9228758fde9ce1936d2263de3d4ca Mon Sep 17 00:00:00 2001 From: Daniel Oaks Date: Sun, 16 Apr 2017 12:31:32 +0300 Subject: [PATCH 0297/3926] Add slug with command to unhandled messages --- client/js/libs/handlebars/slugify.js | 5 +++++ client/views/chan.tpl | 2 +- client/views/msg_unhandled.tpl | 2 +- client/views/network.tpl | 2 +- src/models/network.js | 1 + src/plugins/irc-events/connection.js | 3 ++- 6 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 client/js/libs/handlebars/slugify.js diff --git a/client/js/libs/handlebars/slugify.js b/client/js/libs/handlebars/slugify.js new file mode 100644 index 00000000..a8b385e8 --- /dev/null +++ b/client/js/libs/handlebars/slugify.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = function(orig) { + return orig.toLowerCase().replace(/[^a-z0-9]/, "-"); +}; diff --git a/client/views/chan.tpl b/client/views/chan.tpl index 626eb228..d9e9d005 100644 --- a/client/views/chan.tpl +++ b/client/views/chan.tpl @@ -1,5 +1,5 @@ {{#each channels}} -
+
{{#if unread}}{{roundBadgeNumber unread}}{{/if}} {{name}} diff --git a/client/views/msg_unhandled.tpl b/client/views/msg_unhandled.tpl index 1a6fcfd7..69ad3fe5 100644 --- a/client/views/msg_unhandled.tpl +++ b/client/views/msg_unhandled.tpl @@ -1,4 +1,4 @@ -
+
{{tz time}} diff --git a/client/views/network.tpl b/client/views/network.tpl index 9da98019..d525dff2 100644 --- a/client/views/network.tpl +++ b/client/views/network.tpl @@ -1,5 +1,5 @@ {{#each networks}} -
+
{{> chan}}
{{/each}} diff --git a/src/models/network.js b/src/models/network.js index f5576133..4a63d355 100644 --- a/src/models/network.js +++ b/src/models/network.js @@ -25,6 +25,7 @@ function Network(attr) { irc: null, serverOptions: { PREFIX: [], + NETWORK: "", }, chanCache: [], }); diff --git a/src/plugins/irc-events/connection.js b/src/plugins/irc-events/connection.js index 47cb331c..e44b7676 100644 --- a/src/plugins/irc-events/connection.js +++ b/src/plugins/irc-events/connection.js @@ -120,7 +120,7 @@ module.exports = function(irc, network) { }); irc.on("server options", function(data) { - if (network.serverOptions.PREFIX === data.options.PREFIX) { + if (network.serverOptions.PREFIX === data.options.PREFIX && network.serverOptions.NETWORK === data.options.NETWORK) { return; } @@ -131,6 +131,7 @@ module.exports = function(irc, network) { }); network.serverOptions.PREFIX = data.options.PREFIX; + network.serverOptions.NETWORK = data.options.NETWORK; client.emit("network_changed", { network: network.id, From 955aada1cff89cc0cef6fe68501fb6fc722db644 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 14 Apr 2017 12:00:08 +0000 Subject: [PATCH 0298/3926] chore(package): update webpack to version 2.4.1 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 977d56b9..30154e29 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,6 @@ "socket.io-client": "1.7.3", "stylelint": "7.10.1", "urijs": "1.18.10", - "webpack": "2.3.3" + "webpack": "2.4.1" } } From 4938878d10041c4105793dfd484a0b83a7d8bd98 Mon Sep 17 00:00:00 2001 From: Alistair McKinlay Date: Mon, 17 Apr 2017 10:35:27 +0100 Subject: [PATCH 0299/3926] Disable show more button when loading messages --- client/js/lounge.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/js/lounge.js b/client/js/lounge.js index e10c9d73..7fcf5d25 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -505,6 +505,8 @@ $(function() { lastDate = msgDate; }); + + scrollable.find(".show-more").prop("disabled", false); }); socket.on("network", function(data) { @@ -1224,6 +1226,7 @@ $(function() { chat.on("click", ".show-more-button", function() { var self = $(this); var count = self.parent().next(".messages").children(".msg").length; + self.prop("disabled", true); socket.emit("more", { target: self.data("id"), count: count From b750da3f9da42922936b5775acc79be532c06b6d Mon Sep 17 00:00:00 2001 From: Metsjeesus Date: Mon, 17 Apr 2017 22:48:28 +0300 Subject: [PATCH 0300/3926] Fix to helper.expandhome to correctly resolve "" and undefined --- src/helper.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/helper.js b/src/helper.js index 830b91c9..552b11ad 100644 --- a/src/helper.js +++ b/src/helper.js @@ -101,6 +101,9 @@ function ip2hex(address) { } function expandHome(shortenedPath) { + if (!shortenedPath) { + return ""; + } var home; if (os.homedir) { @@ -112,7 +115,6 @@ function expandHome(shortenedPath) { } home = home.replace("$", "$$$$"); - return path.resolve(shortenedPath.replace(/^~($|\/|\\)/, home + "$1")); } From a900abc2a4ead565300bbdfa57c0f29bb1bf3a33 Mon Sep 17 00:00:00 2001 From: Kyle Terrien Date: Tue, 18 Apr 2017 19:48:14 -0700 Subject: [PATCH 0301/3926] Issue 1019: Show MOTD by default --- client/js/lounge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index e10c9d73..ddc3ad3a 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -605,7 +605,7 @@ $(function() { join: true, links: true, mode: true, - motd: false, + motd: true, nick: true, notification: true, notifyAllMessages: false, From 8aa6f9c500b6e5f1b6fd08fd3f4c5bb08c5aecfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 19 Apr 2017 01:19:11 -0400 Subject: [PATCH 0302/3926] Exclude Webpack config from coverage report --- .nycrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.nycrc b/.nycrc index ae33aa14..99b69780 100644 --- a/.nycrc +++ b/.nycrc @@ -3,7 +3,8 @@ "exclude": [ "client/js/bundle.js", "client/js/bundle.vendor.js", - "test/" + "test/", + "webpack.config.js" ], "reporter": [ "lcov", From 764ac831d4cac52bd2cc636f4fd72a5a65f3a9e1 Mon Sep 17 00:00:00 2001 From: Michael van Tricht Date: Thu, 6 Apr 2017 18:22:56 +0200 Subject: [PATCH 0303/3926] Improve channels list. - Set fixed width to channel and users column. - Sort by number of users in channel. - Executing /list multiple times wont show multiple tables. - Channel list is not stickied to the bottom. - Limit channels to 500. Scrolling through 1k is very slow on my system. --- client/css/style.css | 5 +++++ client/js/lounge.js | 6 +++++- src/plugins/irc-events/list.js | 6 ++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index f32261e4..ee76a268 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1022,6 +1022,10 @@ kbd { border-bottom: #eee 1px solid; } +#chat table.channel-list .channel { + width: 80px; +} + #chat table.channel-list .channel, #chat table.channel-list .topic { text-align: left; @@ -1029,6 +1033,7 @@ kbd { #chat table.channel-list .users { text-align: center; + width: 50px; } #chat table.channel-list td.channel .inline-channel { diff --git a/client/js/lounge.js b/client/js/lounge.js index fcc06179..5439d277 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -421,6 +421,10 @@ $(function() { var target = "#chan-" + data.chan; var container = chat.find(target + " .messages"); + if (data.msg.type === "channel_list") { + $(container).empty(); + } + // Check if date changed var prevMsg = $(container.find(".msg")).last(); var prevMsgTime = new Date(prevMsg.attr("data-time")); @@ -1050,7 +1054,7 @@ $(function() { } var chanChat = chan.find(".chat"); - if (chanChat.length > 0) { + if (chanChat.length > 0 && chan.data("type") !== "special") { chanChat.sticky(); } diff --git a/src/plugins/irc-events/list.js b/src/plugins/irc-events/list.js index 0dbf2881..848f912d 100644 --- a/src/plugins/irc-events/list.js +++ b/src/plugins/irc-events/list.js @@ -5,7 +5,7 @@ var Msg = require("../../models/msg"); module.exports = function(irc, network) { var client = this; - var MAX_CHANS = 1000; + var MAX_CHANS = 500; irc.on("channel list start", function() { network.chanCache = []; @@ -23,7 +23,9 @@ module.exports = function(irc, network) { irc.on("channel list end", function() { updateListStatus(new Msg({ type: "channel_list", - channels: network.chanCache.slice(0, MAX_CHANS) + channels: network.chanCache.sort(function(a, b) { + return b.num_users - a.num_users; + }).slice(0, MAX_CHANS) })); if (network.chanCache.length > MAX_CHANS) { From 5bab511c42be718e2683f69d4bcac8b4c96a18fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Mon, 17 Apr 2017 18:29:52 -0400 Subject: [PATCH 0304/3926] Use HTTPS version to the IRC color guide --- client/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/index.html b/client/index.html index 348c5f11..83ac96ad 100644 --- a/client/index.html +++ b/client/index.html @@ -404,7 +404,7 @@

A color reference can be found - here. + here.

@@ -480,7 +480,7 @@

A color reference can be found - here. + here.

From f9de811df169d71342816e5f932b2df5de0af321 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 21 Apr 2017 03:48:20 +0000 Subject: [PATCH 0305/3926] chore(package): update handlebars-loader to version 1.5.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 30154e29..15ed8db8 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "eslint": "3.19.0", "font-awesome": "4.7.0", "handlebars": "4.0.6", - "handlebars-loader": "1.4.0", + "handlebars-loader": "1.5.0", "jquery": "3.2.1", "jquery-ui": "1.12.1", "mocha": "3.2.0", From cc85b2143c197ea033d4c5ff9ac409c5584cadc5 Mon Sep 17 00:00:00 2001 From: Alistair McKinlay Date: Thu, 20 Apr 2017 11:17:25 +0100 Subject: [PATCH 0306/3926] Change index.html to be rendered using handlebars --- client/index.html | 68 +++++++++++-------- .../libs/handlebars/firstLetterUppercase.js | 5 ++ client/js/libs/handlebars/index.js | 18 +++++ client/js/libs/handlebars/ternary.js | 5 ++ package.json | 1 + src/server.js | 40 +++++------ 6 files changed, 85 insertions(+), 52 deletions(-) create mode 100644 client/js/libs/handlebars/firstLetterUppercase.js create mode 100644 client/js/libs/handlebars/index.js create mode 100644 client/js/libs/handlebars/ternary.js diff --git a/client/index.html b/client/index.html index 83ac96ad..0bd055f5 100644 --- a/client/index.html +++ b/client/index.html @@ -9,7 +9,7 @@ - + The Lounge @@ -25,7 +25,7 @@ - "> +
@@ -122,12 +122,17 @@

- <%= public ? "The Lounge - " : "" %> + {{ternary public "The Lounge " " " }} Connect - <%= !displayNetwork && lockNetwork ? "to " + defaults.name : "" %> + {{#unless displayNetwork}} + {{#if lockNetwork}} + to {{defaults.name}} + {{/if}} + {{/unless}}

-
> + {{#unless displayNetwork}} +

Network settings

@@ -135,17 +140,17 @@
- +
- > +
- > +
@@ -153,16 +158,17 @@
- +
+ {{/unless}}

User preferences

@@ -170,27 +176,27 @@
- +
- <% if (!useHexIp) { %> + {{#unless useHexIp}}
- +
- <% } %> + {{/unless}}
- +
- +
@@ -261,14 +267,14 @@
- <% if (typeof prefetch === "undefined" || prefetch !== false) { %> + {{#unless prefetch}}

Links and URLs

@@ -284,7 +290,7 @@ Auto-expand links
- <% } %> + {{/unless}}

Notifications

@@ -328,7 +334,8 @@
- <% if (!public && !ldap.enable) { %> + {{#unless public}} + {{#unless ldap.enable}}
@@ -352,7 +359,8 @@
- <% } %> + {{/unless}} + {{/unless}}

Custom Stylesheet

@@ -809,13 +817,13 @@

About The Lounge

- <% if (gitCommit) { %> + {{#if gitCommit}} The Lounge is running from source - (<%= gitCommit %>).
- <% } else { %> - The Lounge is in version <%= version %> - (See release notes).
- <% } %> + ({{ gitCommit }}).
+ {{else}} + The Lounge is in version {{version}} + (See release notes).
+ {{/if}} Website
Documentation
diff --git a/client/js/libs/handlebars/firstLetterUppercase.js b/client/js/libs/handlebars/firstLetterUppercase.js new file mode 100644 index 00000000..d36bff54 --- /dev/null +++ b/client/js/libs/handlebars/firstLetterUppercase.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = function(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +}; diff --git a/client/js/libs/handlebars/index.js b/client/js/libs/handlebars/index.js new file mode 100644 index 00000000..fe485b47 --- /dev/null +++ b/client/js/libs/handlebars/index.js @@ -0,0 +1,18 @@ +"use strict"; + +module.exports = { + colorClass: require("./colorClass"), + diff: require("./diff"), + equal: require("./equal"), + firstLetterUppercase: require("./firstLetterUppercase"), + localedate: require("./localedate"), + localetime: require("./localetime"), + modes: require("./modes"), + parse: require("./parse"), + roundBadgeNumber: require("./roundBadgeNumber"), + slugify: require("./slugify"), + ternary: require("./ternary"), + tojson: require("./tojson"), + tz: require("./tz"), + users: require("./users"), +}; diff --git a/client/js/libs/handlebars/ternary.js b/client/js/libs/handlebars/ternary.js new file mode 100644 index 00000000..4dfa2995 --- /dev/null +++ b/client/js/libs/handlebars/ternary.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = function(test, yes, no) { + return test ? yes : no; +}; diff --git a/package.json b/package.json index 30154e29..3628e2b3 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "commander": "2.9.0", "event-stream": "3.3.4", "express": "4.15.2", + "express-handlebars": "^3.0.0", "fs-extra": "2.1.2", "irc-framework": "2.6.1", "ldapjs": "1.0.1", diff --git a/src/server.js b/src/server.js index 0ff97b76..082ef98f 100644 --- a/src/server.js +++ b/src/server.js @@ -5,7 +5,9 @@ var pkg = require("../package.json"); var Client = require("./client"); var ClientManager = require("./clientManager"); var express = require("express"); +var expressHandlebars = require("express-handlebars"); var fs = require("fs"); +var path = require("path"); var io = require("socket.io"); var dns = require("dns"); var Helper = require("./helper"); @@ -29,7 +31,10 @@ module.exports = function() { var app = express() .use(allRequests) .use(index) - .use(express.static("client")); + .use(express.static("client")) + .engine("html", expressHandlebars({extname: ".html", helpers: require("../client/js/libs/handlebars")})) + .set("view engine", "html") + .set("views", path.join(__dirname, "..", "client")); var config = Helper.config; var server = null; @@ -125,28 +130,19 @@ function index(req, res, next) { return next(); } - return fs.readFile("client/index.html", "utf-8", function(err, file) { - if (err) { - throw err; - } - - var data = _.merge( - pkg, - Helper.config - ); - data.gitCommit = Helper.getGitCommit(); - data.themes = fs.readdirSync("client/themes/").filter(function(themeFile) { - return themeFile.endsWith(".css"); - }).map(function(css) { - return css.slice(0, -4); - }); - var template = _.template(file); - res.setHeader("Content-Security-Policy", "default-src *; connect-src 'self' ws: wss:; style-src * 'unsafe-inline'; script-src 'self'; child-src 'self'; object-src 'none'; form-action 'none';"); - res.setHeader("Referrer-Policy", "no-referrer"); - res.setHeader("Content-Type", "text/html"); - res.writeHead(200); - res.end(template(data)); + var data = _.merge( + pkg, + Helper.config + ); + data.gitCommit = Helper.getGitCommit(); + data.themes = fs.readdirSync("client/themes/").filter(function(themeFile) { + return themeFile.endsWith(".css"); + }).map(function(css) { + return css.slice(0, -4); }); + res.setHeader("Content-Security-Policy", "default-src *; connect-src 'self' ws: wss:; style-src * 'unsafe-inline'; script-src 'self'; child-src 'self'; object-src 'none'; form-action 'none';"); + res.setHeader("Referrer-Policy", "no-referrer"); + res.render("index", data); } function init(socket, client) { From b4310dbc03734fd23f3d6ae72adee33d85949939 Mon Sep 17 00:00:00 2001 From: Alistair McKinlay Date: Fri, 21 Apr 2017 09:26:02 +0100 Subject: [PATCH 0307/3926] Review changes (Should be squashed before merge) --- client/index.html | 14 +++++++------- .../js/libs/handlebars/firstLetterUppercase.js | 5 ----- client/js/libs/handlebars/index.js | 18 ------------------ client/js/libs/handlebars/ternary.js | 5 ----- package.json | 2 +- src/server.js | 8 ++++++-- 6 files changed, 14 insertions(+), 38 deletions(-) delete mode 100644 client/js/libs/handlebars/firstLetterUppercase.js delete mode 100644 client/js/libs/handlebars/index.js delete mode 100644 client/js/libs/handlebars/ternary.js diff --git a/client/index.html b/client/index.html index 0bd055f5..7d751664 100644 --- a/client/index.html +++ b/client/index.html @@ -25,7 +25,7 @@ - +

@@ -122,7 +122,7 @@

- {{ternary public "The Lounge " " " }} + {{#if public}}The Lounge - {{/if}} Connect {{#unless displayNetwork}} {{#if lockNetwork}} @@ -146,11 +146,11 @@

- +
- +
@@ -162,7 +162,7 @@
@@ -268,8 +268,8 @@ diff --git a/client/js/libs/handlebars/firstLetterUppercase.js b/client/js/libs/handlebars/firstLetterUppercase.js deleted file mode 100644 index d36bff54..00000000 --- a/client/js/libs/handlebars/firstLetterUppercase.js +++ /dev/null @@ -1,5 +0,0 @@ -"use strict"; - -module.exports = function(string) { - return string.charAt(0).toUpperCase() + string.slice(1); -}; diff --git a/client/js/libs/handlebars/index.js b/client/js/libs/handlebars/index.js deleted file mode 100644 index fe485b47..00000000 --- a/client/js/libs/handlebars/index.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; - -module.exports = { - colorClass: require("./colorClass"), - diff: require("./diff"), - equal: require("./equal"), - firstLetterUppercase: require("./firstLetterUppercase"), - localedate: require("./localedate"), - localetime: require("./localetime"), - modes: require("./modes"), - parse: require("./parse"), - roundBadgeNumber: require("./roundBadgeNumber"), - slugify: require("./slugify"), - ternary: require("./ternary"), - tojson: require("./tojson"), - tz: require("./tz"), - users: require("./users"), -}; diff --git a/client/js/libs/handlebars/ternary.js b/client/js/libs/handlebars/ternary.js deleted file mode 100644 index 4dfa2995..00000000 --- a/client/js/libs/handlebars/ternary.js +++ /dev/null @@ -1,5 +0,0 @@ -"use strict"; - -module.exports = function(test, yes, no) { - return test ? yes : no; -}; diff --git a/package.json b/package.json index 3628e2b3..98754da7 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "commander": "2.9.0", "event-stream": "3.3.4", "express": "4.15.2", - "express-handlebars": "^3.0.0", + "express-handlebars": "3.0.0", "fs-extra": "2.1.2", "irc-framework": "2.6.1", "ldapjs": "1.0.1", diff --git a/src/server.js b/src/server.js index 082ef98f..db92efe7 100644 --- a/src/server.js +++ b/src/server.js @@ -32,7 +32,7 @@ module.exports = function() { .use(allRequests) .use(index) .use(express.static("client")) - .engine("html", expressHandlebars({extname: ".html", helpers: require("../client/js/libs/handlebars")})) + .engine("html", expressHandlebars({extname: ".html"})) .set("view engine", "html") .set("views", path.join(__dirname, "..", "client")); @@ -138,7 +138,11 @@ function index(req, res, next) { data.themes = fs.readdirSync("client/themes/").filter(function(themeFile) { return themeFile.endsWith(".css"); }).map(function(css) { - return css.slice(0, -4); + const filename = css.slice(0, -4); + return { + name: filename.charAt(0).toUpperCase() + filename.slice(1), + filename: filename + }; }); res.setHeader("Content-Security-Policy", "default-src *; connect-src 'self' ws: wss:; style-src * 'unsafe-inline'; script-src 'self'; child-src 'self'; object-src 'none'; form-action 'none';"); res.setHeader("Referrer-Policy", "no-referrer"); From adfd99c92c93aa387a3578d960707cfc118e8573 Mon Sep 17 00:00:00 2001 From: Alistair McKinlay Date: Mon, 17 Apr 2017 08:25:21 +0100 Subject: [PATCH 0308/3926] Add fix for undefined name being slugified --- client/views/network.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/views/network.tpl b/client/views/network.tpl index d525dff2..05885d47 100644 --- a/client/views/network.tpl +++ b/client/views/network.tpl @@ -1,5 +1,5 @@ {{#each networks}} -
+
{{> chan}}
{{/each}} From 05d363d9a5b556a86a025a789b865dfd9419c60a Mon Sep 17 00:00:00 2001 From: Alistair McKinlay Date: Tue, 18 Apr 2017 08:31:46 +0100 Subject: [PATCH 0309/3926] Create socket module --- client/js/lounge.js | 50 +---------------------------------------- client/js/socket.js | 54 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 49 deletions(-) create mode 100644 client/js/socket.js diff --git a/client/js/lounge.js b/client/js/lounge.js index 0e65b5d3..41505797 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -3,7 +3,6 @@ // vendor libraries require("jquery-ui/ui/widgets/sortable"); const $ = require("jquery"); -const io = require("socket.io-client"); const Mousetrap = require("mousetrap"); const URI = require("urijs"); @@ -15,14 +14,9 @@ const helpers_parse = require("./libs/handlebars/parse"); const helpers_roundBadgeNumber = require("./libs/handlebars/roundBadgeNumber"); const slideoutMenu = require("./libs/slideout"); const templates = require("../views"); +const socket = require("./socket"); $(function() { - var path = window.location.pathname + "socket.io/"; - var socket = io({ - path: path, - autoConnect: false, - reconnection: false - }); var commands = [ "/away", "/back", @@ -84,48 +78,6 @@ $(function() { // available. See http://stackoverflow.com/q/14555347/1935861. } } - - [ - "connect_error", - "connect_failed", - "disconnect", - "error", - ].forEach(function(e) { - socket.on(e, function(data) { - $("#loading-page-message").text("Connection failed: " + data); - $("#connection-error").addClass("shown").one("click", function() { - window.onbeforeunload = null; - window.location.reload(); - }); - - // Disables sending a message by pressing Enter. `off` is necessary to - // cancel `inputhistory`, which overrides hitting Enter. `on` is then - // necessary to avoid creating new lines when hitting Enter without Shift. - // This is fairly hacky but this solution is not permanent. - $("#input").off("keydown").on("keydown", function(event) { - if (event.which === 13 && !event.shiftKey) { - event.preventDefault(); - } - }); - // Hides the "Send Message" button - $("#submit").remove(); - - console.error(data); - }); - }); - - socket.on("connecting", function() { - $("#loading-page-message").text("Connecting…"); - }); - - socket.on("connect", function() { - $("#loading-page-message").text("Finalizing connection…"); - }); - - socket.on("authorized", function() { - $("#loading-page-message").text("Authorized, loading messages…"); - }); - socket.on("auth", function(data) { var login = $("#sign-in"); var token; diff --git a/client/js/socket.js b/client/js/socket.js new file mode 100644 index 00000000..278802d7 --- /dev/null +++ b/client/js/socket.js @@ -0,0 +1,54 @@ +"use strict"; + +const $ = require("jquery"); +const io = require("socket.io-client"); +const path = window.location.pathname + "socket.io/"; + +const socket = io({ + path: path, + autoConnect: false, + reconnection: false +}); + +[ + "connect_error", + "connect_failed", + "disconnect", + "error", +].forEach(function(e) { + socket.on(e, function(data) { + $("#loading-page-message").text("Connection failed: " + data); + $("#connection-error").addClass("shown").one("click", function() { + window.onbeforeunload = null; + window.location.reload(); + }); + + // Disables sending a message by pressing Enter. `off` is necessary to + // cancel `inputhistory`, which overrides hitting Enter. `on` is then + // necessary to avoid creating new lines when hitting Enter without Shift. + // This is fairly hacky but this solution is not permanent. + $("#input").off("keydown").on("keydown", function(event) { + if (event.which === 13 && !event.shiftKey) { + event.preventDefault(); + } + }); + // Hides the "Send Message" button + $("#submit").remove(); + + console.error(data); + }); +}); + +socket.on("connecting", function() { + $("#loading-page-message").text("Connecting…"); +}); + +socket.on("connect", function() { + $("#loading-page-message").text("Finalizing connection…"); +}); + +socket.on("authorized", function() { + $("#loading-page-message").text("Authorized, loading messages…"); +}); + +module.exports = socket; From 0b85ded53fa66a1ac1474194052416662d9c9533 Mon Sep 17 00:00:00 2001 From: Bonuspunkt Date: Sat, 18 Mar 2017 10:35:17 +0200 Subject: [PATCH 0310/3926] Add bonuspunkt's parser Fixes #15. Fixes #199. Fixes #583. Fixes #654. Fixes #928. Fixes #1001. --- .../ircmessageparser/anyIntersection.js | 10 + .../libs/handlebars/ircmessageparser/fill.js | 29 +++ .../ircmessageparser/findChannels.js | 32 +++ .../libs/handlebars/ircmessageparser/merge.js | 47 ++++ .../handlebars/ircmessageparser/parseStyle.js | 131 +++++++++++ client/js/libs/handlebars/parse.js | 210 +++++++++--------- 6 files changed, 348 insertions(+), 111 deletions(-) create mode 100644 client/js/libs/handlebars/ircmessageparser/anyIntersection.js create mode 100644 client/js/libs/handlebars/ircmessageparser/fill.js create mode 100644 client/js/libs/handlebars/ircmessageparser/findChannels.js create mode 100644 client/js/libs/handlebars/ircmessageparser/merge.js create mode 100644 client/js/libs/handlebars/ircmessageparser/parseStyle.js diff --git a/client/js/libs/handlebars/ircmessageparser/anyIntersection.js b/client/js/libs/handlebars/ircmessageparser/anyIntersection.js new file mode 100644 index 00000000..4fd0d239 --- /dev/null +++ b/client/js/libs/handlebars/ircmessageparser/anyIntersection.js @@ -0,0 +1,10 @@ +"use strict"; + +function anyIntersection(a, b) { + return a.start <= b.start && b.start < a.end || + a.start < b.end && b.end <= a.end || + b.start <= a.start && a.start < b.end || + b.start < a.end && a.end <= b.end; +} + +module.exports = anyIntersection; diff --git a/client/js/libs/handlebars/ircmessageparser/fill.js b/client/js/libs/handlebars/ircmessageparser/fill.js new file mode 100644 index 00000000..2cc9f705 --- /dev/null +++ b/client/js/libs/handlebars/ircmessageparser/fill.js @@ -0,0 +1,29 @@ +"use strict"; + +function fill(existingEntries, text) { + let position = 0; + const result = []; + + for (let i = 0; i < existingEntries.length; i++) { + const textSegment = existingEntries[i]; + + if (textSegment.start > position) { + result.push({ + start: position, + end: textSegment.start + }); + } + position = textSegment.end; + } + + if (position < text.length) { + result.push({ + start: position, + end: text.length + }); + } + + return result; +} + +module.exports = fill; diff --git a/client/js/libs/handlebars/ircmessageparser/findChannels.js b/client/js/libs/handlebars/ircmessageparser/findChannels.js new file mode 100644 index 00000000..b613415c --- /dev/null +++ b/client/js/libs/handlebars/ircmessageparser/findChannels.js @@ -0,0 +1,32 @@ +"use strict"; + +const escapeRegExp = require("lodash/escapeRegExp"); + +// NOTE: channel prefixes should be RPL_ISUPPORT.CHANTYPES +// NOTE: userModes should be RPL_ISUPPORT.PREFIX +function findChannels(text, channelPrefixes, userModes) { + const userModePattern = userModes.map(escapeRegExp).join(""); + const channelPrefixPattern = channelPrefixes.map(escapeRegExp).join(""); + + const channelPattern = `(?:^|\\s)[${ userModePattern }]*([${ channelPrefixPattern }][^ \u0007]+)`; + const channelRegExp = new RegExp(channelPattern, "g"); + + const result = []; + let match; + + do { + match = channelRegExp.exec(text); + + if (match) { + result.push({ + start: match.index + match[0].length - match[1].length, + end: match.index + match[0].length, + channel: match[1] + }); + } + } while (match); + + return result; +} + +module.exports = findChannels; diff --git a/client/js/libs/handlebars/ircmessageparser/merge.js b/client/js/libs/handlebars/ircmessageparser/merge.js new file mode 100644 index 00000000..3da520e8 --- /dev/null +++ b/client/js/libs/handlebars/ircmessageparser/merge.js @@ -0,0 +1,47 @@ +"use strict"; + +const anyIntersection = require("./anyIntersection"); +const fill = require("./fill"); + +let Object_assign = Object.assign; + +if (typeof Object_assign !== "function") { + Object_assign = function(target) { + Array.prototype.slice.call(arguments, 1).forEach(function(obj) { + Object.keys(obj).forEach(function(key) { + target[key] = obj[key]; + }); + }); + return target; + }; +} + +function assign(textPart, fragment) { + const fragStart = fragment.start; + const start = Math.max(fragment.start, textPart.start); + const end = Math.min(fragment.end, textPart.end); + + return Object_assign({}, fragment, { + start: start, + end: end, + text: fragment.text.slice(start - fragStart, end - fragStart) + }); +} + +function merge(textParts, styleFragments) { + const cleanText = styleFragments.map(fragment => fragment.text).join(""); + + const allParts = textParts + .concat(fill(textParts, cleanText)) + .sort((a, b) => a.start - b.start); + + return allParts.map(textPart => { + textPart.fragments = styleFragments + .filter(fragment => anyIntersection(textPart, fragment)) + .map(fragment => assign(textPart, fragment)); + + return textPart; + }); +} + +module.exports = merge; diff --git a/client/js/libs/handlebars/ircmessageparser/parseStyle.js b/client/js/libs/handlebars/ircmessageparser/parseStyle.js new file mode 100644 index 00000000..54e1c191 --- /dev/null +++ b/client/js/libs/handlebars/ircmessageparser/parseStyle.js @@ -0,0 +1,131 @@ +"use strict"; + +const BOLD = "\x02"; +const COLOR = "\x03"; +const RESET = "\x0f"; +const REVERSE = "\x16"; +const ITALIC = "\x1d"; +const UNDERLINE = "\x1f"; + +const colorRx = /^(\d{1,2})(?:,(\d{1,2}))?/; +const controlCodesRx = /[\u0000-\u001F]/g; + +function parseStyle(text) { + const result = []; + let start = 0; + let position = 0; + + let colorCodes, bold, textColor, bgColor, reverse, italic, underline; + + const resetStyle = () => { + bold = false; + textColor = undefined; + bgColor = undefined; + reverse = false; + italic = false; + underline = false; + }; + resetStyle(); + + const emitFragment = () => { + const textPart = text.slice(start, position); + start = position + 1; + + const processedText = textPart.replace(controlCodesRx, ""); + + if (!processedText.length) { + return; + } + + result.push({ + bold, + textColor, + bgColor, + reverse, + italic, + underline, + text: processedText + }); + }; + + while (position < text.length) { + switch (text[position]) { + + case RESET: + emitFragment(); + resetStyle(); + break; + + case BOLD: + emitFragment(); + bold = !bold; + break; + + case COLOR: + emitFragment(); + + colorCodes = text.slice(position + 1).match(colorRx); + + if (colorCodes) { + textColor = Number(colorCodes[1]); + bgColor = Number(colorCodes[2]); + if (Number.isNaN(bgColor)) { + bgColor = undefined; + } + position += colorCodes[0].length; + } else { + textColor = undefined; + bgColor = undefined; + } + start = position + 1; + break; + + case REVERSE: + emitFragment(); + reverse = !reverse; + break; + + case ITALIC: + emitFragment(); + italic = !italic; + break; + + case UNDERLINE: + emitFragment(); + underline = !underline; + break; + } + position += 1; + } + + emitFragment(); + + return result; +} + +const properties = ["bold", "textColor", "bgColor", "italic", "underline", "reverse"]; + +function prepare(text) { + return parseStyle(text) + .filter(fragment => fragment.text.length) + .reduce((prev, curr, i) => { + if (i === 0) { + return prev.concat([curr]); + } + + const lastEntry = prev[prev.length - 1]; + if (properties.some(key => curr[key] !== lastEntry[key])) { + return prev.concat([curr]); + } + + lastEntry.text += curr.text; + return prev; + }, []) + .map((fragment, i, array) => { + fragment.start = i === 0 ? 0 : array[i - 1].end; + fragment.end = fragment.start + fragment.text.length; + return fragment; + }); +} + +module.exports = prepare; diff --git a/client/js/libs/handlebars/parse.js b/client/js/libs/handlebars/parse.js index 45d5c8d2..8c6ae432 100644 --- a/client/js/libs/handlebars/parse.js +++ b/client/js/libs/handlebars/parse.js @@ -2,125 +2,113 @@ const Handlebars = require("handlebars/runtime"); const URI = require("urijs"); +const parseStyle = require("./ircmessageparser/parseStyle"); +const findChannels = require("./ircmessageparser/findChannels"); +const merge = require("./ircmessageparser/merge"); -module.exports = function(text) { - text = Handlebars.Utils.escapeExpression(text); - text = colors(text); - text = channels(text); - text = uri(text); - return text; -}; +const commonSchemes = [ + "http", "https", + "ftp", "sftp", + "smb", "file", + "irc", "ircs", + "svn", "git", + "steam", "mumble", "ts3server", + "svn+ssh", "ssh", +]; -function uri(text) { - return URI.withinString(text, function(url) { - if (url.indexOf("javascript:") === 0) { - return url; - } - var split = url.split("<"); - url = "" + split[0] + ""; - if (split.length > 1) { - url += "<" + split.slice(1).join("<"); - } - return url; - }); -} +function findLinks(text) { + let result = []; + let lastPosition = 0; -/** - * Channels names are strings of length up to fifty (50) characters. - * The only restriction on a channel name is that it SHALL NOT contain - * any spaces (' '), a control G (^G or ASCII 7), a comma (','). - * Channel prefix '&' is handled as '&' because this parser is executed - * after entities in the message have been escaped. This prevents a couple of bugs. - */ -function channels(text) { - return text.replace( - /(^|\s|\x07|,)((?:#|&)[^\x07\s,]{1,49})/g, - '$1$2' - ); -} - -/** - * MIRC compliant colour and style parser - * Unfortuanately this is a non trivial operation - * See this branch for source and tests - * https://github.com/megawac/irc-style-parser/tree/shout - */ -var styleCheck_Re = /[\x00-\x1F]/, - back_re = /^([0-9]{1,2})(,([0-9]{1,2}))?/, - colourKey = "\x03", - // breaks all open styles ^O (\x0F) - styleBreak = "\x0F"; - -function styleTemplate(settings) { - return "" + settings.text + ""; -} - -var styles = [ - ["normal", "\x00", ""], ["underline", "\x1F"], - ["bold", "\x02"], ["italic", "\x1D"] -].map(function(style) { - var escaped = encodeURI(style[1]).replace("%", "\\x"); - return { - name: style[0], - style: style[2] ? style[2] : "irc-" + style[0], - key: style[1], - keyregex: new RegExp(escaped + "(.*?)(" + escaped + "|$)") - }; -}); - -function colors(line) { - // http://www.mirc.com/colors.html - // http://www.aviran.org/stripremove-irc-client-control-characters/ - // https://github.com/perl6/mu/blob/master/examples/rules/Grammar-IRC.pm - // regexs are cruel to parse this thing - - // already done? - if (!styleCheck_Re.test(line)) { - return line; - } - - // split up by the irc style break character ^O - if (line.indexOf(styleBreak) >= 0) { - return line.split(styleBreak).map(colors).join(""); - } - - var result = line; - var parseArr = result.split(colourKey); - var text, match, colour, background = ""; - for (var i = 0; i < parseArr.length; i++) { - text = parseArr[i]; - match = text.match(back_re); - if (!match) { - // ^C (no colour) ending. Escape current colour and carry on - background = ""; - continue; - } - colour = "irc-fg" + +match[1]; - // set the background colour - if (match[3]) { - background = " irc-bg" + +match[3]; - } - // update the parsed text result - result = result.replace(colourKey + text, styleTemplate({ - style: colour + background, - text: text.slice(match[0].length) - })); - } - - // Matching styles (italics/bold/underline) - // if only colours were this easy... - styles.forEach(function(style) { - if (result.indexOf(style.key) < 0) { + URI.withinString(text, function(url, start, end) { + // v-- fix: url was modified and does not match input string -> cant be mapped + if (text.indexOf(url, lastPosition) < 0) { return; } + // ^-- /fix: url was modified and does not match input string -> cant be mapped - result = result.replace(style.keyregex, function(matchedTrash, matchedText) { - return styleTemplate({ - style: style.style, - text: matchedText - }); + // v-- fix: use prefered scheme + const parsed = URI(url); + const parsedScheme = parsed.scheme().toLowerCase(); + const matchedScheme = commonSchemes.find(scheme => parsedScheme.endsWith(scheme)); + + if (matchedScheme) { + const prefix = parsedScheme.length - matchedScheme.length; + start += prefix; + url = url.slice(prefix); + } + // ^-- /fix: use prefered scheme + + // URL matched, but does not start with a protocol, add it + if (!parsedScheme.length) { + url = "http://" + url; + } + + result.push({ + start: start, + end: end, + link: url }); }); return result; } + +function createFragment(fragment) { + let className = ""; + if (fragment.bold) { + className += " irc-bold"; + } + if (fragment.textColor !== undefined) { + className += " irc-fg" + fragment.textColor; + } + if (fragment.bgColor !== undefined) { + className += " irc-bg" + fragment.bgColor; + } + if (fragment.italic) { + className += " irc-italic"; + } + if (fragment.underline) { + className += " irc-underline"; + } + const escapedText = Handlebars.Utils.escapeExpression(fragment.text); + if (className) { + return "" + escapedText + ""; + } + return escapedText; +} + +module.exports = function parse(text) { + const styleFragments = parseStyle(text); + const cleanText = styleFragments.map(fragment => fragment.text).join(""); + + const channelPrefixes = ["#", "&"]; // RPL_ISUPPORT.CHANTYPES + const userModes = ["!", "@", "%", "+"]; // RPL_ISUPPORT.PREFIX + const channelParts = findChannels(cleanText, channelPrefixes, userModes); + + const linkParts = findLinks(cleanText); + + const parts = channelParts + .concat(linkParts) + .sort((a, b) => a.start - b.start); + + return merge(parts, styleFragments).map(textPart => { + const fragments = textPart.fragments.map(createFragment).join(""); + + if (textPart.link) { + const escapedLink = Handlebars.Utils.escapeExpression(textPart.link); + return ( + "" + + fragments + + ""); + } else if (textPart.channel) { + const escapedChannel = Handlebars.Utils.escapeExpression(textPart.channel); + return ( + "" + + fragments + + ""); + } + + return fragments; + }).join(""); +}; From eb1360c3af3c2f02cc3facbd843e7e34af3091e9 Mon Sep 17 00:00:00 2001 From: Bonuspunkt Date: Sat, 18 Mar 2017 10:18:47 +0200 Subject: [PATCH 0311/3926] Add message parser tests --- .../ircmessageparser/anyIntersection.js | 30 ++ .../ircmessageparser/findChannels.js | 123 +++++++ .../libs/handlebars/ircmessageparser/merge.js | 63 ++++ .../handlebars/ircmessageparser/parseStyle.js | 274 ++++++++++++++ test/client/js/libs/handlebars/parse.js | 336 ++++++++++++++++++ 5 files changed, 826 insertions(+) create mode 100644 test/client/js/libs/handlebars/ircmessageparser/anyIntersection.js create mode 100644 test/client/js/libs/handlebars/ircmessageparser/findChannels.js create mode 100644 test/client/js/libs/handlebars/ircmessageparser/merge.js create mode 100644 test/client/js/libs/handlebars/ircmessageparser/parseStyle.js create mode 100644 test/client/js/libs/handlebars/parse.js diff --git a/test/client/js/libs/handlebars/ircmessageparser/anyIntersection.js b/test/client/js/libs/handlebars/ircmessageparser/anyIntersection.js new file mode 100644 index 00000000..b80a44ed --- /dev/null +++ b/test/client/js/libs/handlebars/ircmessageparser/anyIntersection.js @@ -0,0 +1,30 @@ +"use strict"; + +const expect = require("chai").expect; +const anyIntersection = require("../../../../../../client/js/libs/handlebars/ircmessageparser/anyIntersection"); + +describe("anyIntersection", () => { + it("should not intersect on edges", () => { + const a = {start: 1, end: 2}; + const b = {start: 2, end: 3}; + + expect(anyIntersection(a, b)).to.equal(false); + expect(anyIntersection(b, a)).to.equal(false); + }); + + it("should intersect on overlapping", () => { + const a = {start: 0, end: 3}; + const b = {start: 1, end: 2}; + + expect(anyIntersection(a, b)).to.equal(true); + expect(anyIntersection(b, a)).to.equal(true); + }); + + it("should not intersect", () => { + const a = {start: 0, end: 1}; + const b = {start: 2, end: 3}; + + expect(anyIntersection(a, b)).to.equal(false); + expect(anyIntersection(b, a)).to.equal(false); + }); +}); diff --git a/test/client/js/libs/handlebars/ircmessageparser/findChannels.js b/test/client/js/libs/handlebars/ircmessageparser/findChannels.js new file mode 100644 index 00000000..93c119ee --- /dev/null +++ b/test/client/js/libs/handlebars/ircmessageparser/findChannels.js @@ -0,0 +1,123 @@ +"use strict"; + +const expect = require("chai").expect; +const analyseText = require("../../../../../../client/js/libs/handlebars/ircmessageparser/findChannels"); + +describe("findChannels", () => { + it("should find single letter channel", () => { + const input = "#a"; + const expected = [{ + channel: "#a", + start: 0, + end: 2 + }]; + + const actual = analyseText(input, ["#"], ["@", "+"]); + + expect(actual).to.deep.equal(expected); + }); + + it("should find utf8 channels", () => { + const input = "#äöü"; + const expected = [{ + channel: "#äöü", + start: 0, + end: 4 + }]; + + const actual = analyseText(input, ["#"], ["@", "+"]); + + expect(actual).to.deep.equal(expected); + }); + + it("should find inline channel", () => { + const input = "inline #channel text"; + const expected = [{ + channel: "#channel", + start: 7, + end: 15 + }]; + + const actual = analyseText(input, ["#"], ["@", "+"]); + + expect(actual).to.deep.equal(expected); + }); + + it("should stop at \\0x07", () => { + const input = "#chan\x07nel"; + const expected = [{ + channel: "#chan", + start: 0, + end: 5 + }]; + + const actual = analyseText(input, ["#"], ["@", "+"]); + + expect(actual).to.deep.equal(expected); + }); + + it("should allow classics pranks", () => { + const input = "#1,000"; + const expected = [{ + channel: "#1,000", + start: 0, + end: 6 + }]; + + const actual = analyseText(input, ["#"], ["@", "+"]); + + expect(actual).to.deep.equal(expected); + }); + + it("should work with whois reponses", () => { + const input = "@#a"; + const expected = [{ + channel: "#a", + start: 1, + end: 3 + }]; + + const actual = analyseText(input, ["#"], ["@", "+"]); + + expect(actual).to.deep.equal(expected); + }); + + it("should work with IRCv3.1 multi-prefix", () => { + const input = "!@%+#a"; + const expected = [{ + channel: "#a", + start: 4, + end: 6 + }]; + + const actual = analyseText(input, ["#"], ["!", "@", "%", "+"]); + + expect(actual).to.deep.equal(expected); + }); + + it("should work with custom channelPrefixes", () => { + const input = "@a"; + const expected = [{ + channel: "@a", + start: 0, + end: 2 + }]; + + const actual = analyseText(input, ["@"], ["#", "+"]); + + expect(actual).to.deep.equal(expected); + }); + + it("should handle multiple channelPrefix correctly", () => { + const input = "##test"; + const expected = [{ + channel: "##test", + start: 0, + end: 6 + }]; + + const actual = analyseText(input, ["#"], ["@", "+"]); + + expect(actual).to.deep.equal(expected); + }); +}); diff --git a/test/client/js/libs/handlebars/ircmessageparser/merge.js b/test/client/js/libs/handlebars/ircmessageparser/merge.js new file mode 100644 index 00000000..d55ac1a2 --- /dev/null +++ b/test/client/js/libs/handlebars/ircmessageparser/merge.js @@ -0,0 +1,63 @@ +"use strict"; + +const expect = require("chai").expect; +const merge = require("../../../../../../client/js/libs/handlebars/ircmessageparser/merge"); + +describe("merge", () => { + it("should split style information", () => { + const textParts = [{ + start: 0, + end: 10, + flag1: true + }, { + start: 10, + end: 20, + flag2: true + }]; + const styleFragments = [{ + start: 0, + end: 5, + text: "01234" + }, { + start: 5, + end: 15, + text: "5678901234" + }, { + start: 15, + end: 20, + text: "56789" + }]; + + const expected = [{ + start: 0, + end: 10, + flag1: true, + fragments: [{ + start: 0, + end: 5, + text: "01234" + }, { + start: 5, + end: 10, + text: "56789" + }] + }, { + start: 10, + end: 20, + flag2: true, + fragments: [{ + start: 10, + end: 15, + text: "01234" + }, { + start: 15, + end: 20, + text: "56789" + }] + }]; + + const actual = merge(textParts, styleFragments); + + expect(actual).to.deep.equal(expected); + }); +}); diff --git a/test/client/js/libs/handlebars/ircmessageparser/parseStyle.js b/test/client/js/libs/handlebars/ircmessageparser/parseStyle.js new file mode 100644 index 00000000..6af289c4 --- /dev/null +++ b/test/client/js/libs/handlebars/ircmessageparser/parseStyle.js @@ -0,0 +1,274 @@ +"use strict"; + +const expect = require("chai").expect; +const parseStyle = require("../../../../../../client/js/libs/handlebars/ircmessageparser/parseStyle"); + +describe("parseStyle", () => { + it("should skip control codes", () => { + const input = "text\x01with\x04control\x05codes"; + const expected = [{ + bold: false, + textColor: undefined, + bgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: "textwithcontrolcodes", + + start: 0, + end: 20 + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should parse bold", () => { + const input = "\x02bold"; + const expected = [{ + bold: true, + textColor: undefined, + bgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: "bold", + + start: 0, + end: 4 + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should parse textColor", () => { + const input = "\x038yellowText"; + const expected = [{ + bold: false, + textColor: 8, + bgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: "yellowText", + + start: 0, + end: 10 + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should parse textColor and background", () => { + const input = "\x034,8yellowBG redText"; + const expected = [{ + textColor: 4, + bgColor: 8, + bold: false, + reverse: false, + italic: false, + underline: false, + text: "yellowBG redText", + + start: 0, + end: 16 + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should parse italic", () => { + const input = "\x1ditalic"; + const expected = [{ + bold: false, + textColor: undefined, + bgColor: undefined, + reverse: false, + italic: true, + underline: false, + text: "italic", + + start: 0, + end: 6 + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should carry state corretly forward", () => { + const input = "\x02bold\x038yellow\x02nonBold\x03default"; + const expected = [{ + bold: true, + textColor: undefined, + bgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: "bold", + + start: 0, + end: 4 + }, { + bold: true, + textColor: 8, + bgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: "yellow", + + start: 4, + end: 10 + }, { + bold: false, + textColor: 8, + bgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: "nonBold", + + start: 10, + end: 17 + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: "default", + + start: 17, + end: 24 + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should toggle bold correctly", () => { + const input = "\x02bold\x02 \x02bold\x02"; + const expected = [{ + bold: true, + textColor: undefined, + bgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: "bold", + + start: 0, + end: 4 + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: " ", + + start: 4, + end: 5 + }, { + bold: true, + textColor: undefined, + bgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: "bold", + + start: 5, + end: 9 + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should reset all styles", () => { + const input = "\x02\x034\x16\x1d\x1ffull\x0fnone"; + const expected = [{ + bold: true, + textColor: 4, + bgColor: undefined, + reverse: true, + italic: true, + underline: true, + text: "full", + + start: 0, + end: 4 + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: "none", + + start: 4, + end: 8 + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should not emit empty fragments", () => { + const input = "\x031\x031,2\x031\x031,2\x031\x031,2\x03a"; + const expected = [{ + bold: false, + textColor: undefined, + bgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: "a", + + start: 0, + end: 1 + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should optimize fragments", () => { + const rawString = "oh hi test text"; + const colorCode = "\x0312"; + const input = colorCode + rawString.split("").join(colorCode); + const expected = [{ + bold: false, + textColor: 12, + bgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: rawString, + + start: 0, + end: rawString.length + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); +}); diff --git a/test/client/js/libs/handlebars/parse.js b/test/client/js/libs/handlebars/parse.js new file mode 100644 index 00000000..7d1e025a --- /dev/null +++ b/test/client/js/libs/handlebars/parse.js @@ -0,0 +1,336 @@ +"use strict"; + +const expect = require("chai").expect; +const parse = require("../../../../../client/js/libs/handlebars/parse"); + +describe("parse Handlebars helper", () => { + it("should not introduce xss", () => { + const testCases = [{ + input: "", + expected: "<img onerror='location.href="//youtube.com"'>" + }, { + input: "#&\">bug", + expected: "#&">bug" + }]; + + const actual = testCases.map(testCase => parse(testCase.input)); + const expected = testCases.map(testCase => testCase.expected); + + expect(actual).to.deep.equal(expected); + }); + + it("should skip control codes", () => { + const testCases = [{ + input: "text\x01with\x04control\x05codes", + expected: "textwithcontrolcodes" + }]; + + const actual = testCases.map(testCase => parse(testCase.input)); + const expected = testCases.map(testCase => testCase.expected); + + expect(actual).to.deep.equal(expected); + }); + + it("should find urls", () => { + const testCases = [{ + input: "irc://freenode.net/thelounge", + expected: + "" + + "irc://freenode.net/thelounge" + + "" + }, { + input: "www.nooooooooooooooo.com", + expected: + "" + + "www.nooooooooooooooo.com" + + "" + }, { + input: "look at https://thelounge.github.io/ for more information", + expected: + "look at " + + "" + + "https://thelounge.github.io/" + + "" + + " for more information", + }, { + input: "use www.duckduckgo.com for privacy reasons", + expected: + "use " + + "" + + "www.duckduckgo.com" + + "" + + " for privacy reasons" + }, { + input: "svn+ssh://example.org", + expected: + "" + + "svn+ssh://example.org" + + "" + }]; + + const actual = testCases.map(testCase => parse(testCase.input)); + const expected = testCases.map(testCase => testCase.expected); + + expect(actual).to.deep.equal(expected); + }); + + it("url with a dot parsed correctly", () => { + const input = + "bonuspunkt: your URL parser misparses this URL: https://msdn.microsoft.com/en-us/library/windows/desktop/ms644989(v=vs.85).aspx"; + const correctResult = + "bonuspunkt: your URL parser misparses this URL: " + + "" + + "https://msdn.microsoft.com/en-us/library/windows/desktop/ms644989(v=vs.85).aspx" + + ""; + + const actual = parse(input); + + expect(actual).to.deep.equal(correctResult); + }); + + it("should balance brackets", () => { + const testCases = [{ + input: "", + expected: + "<" + + "" + + "https://theos.kyriasis.com/~kyrias/stats/archlinux.html" + + "" + + ">" + }, { + input: "abc (www.example.com)", + expected: + "abc (" + + "" + + "www.example.com" + + "" + + ")" + }, { + input: "http://example.com/Test_(Page)", + expected: + "" + + "http://example.com/Test_(Page)" + + "" + }, { + input: "www.example.com/Test_(Page)", + expected: + "" + + "www.example.com/Test_(Page)" + + "" + }]; + + const actual = testCases.map(testCase => parse(testCase.input)); + const expected = testCases.map(testCase => testCase.expected); + + expect(actual).to.deep.equal(expected); + }); + + it("should not find urls", () => { + const testCases = [{ + input: "text www. text", + expected: "text www. text" + }, { + input: "http://.", + expected: "http://." + }]; + + const actual = testCases.map(testCase => parse(testCase.input)); + const expected = testCases.map(testCase => testCase.expected); + + expect(actual).to.deep.equal(expected); + }); + + it("should find channels", () => { + const testCases = [{ + input: "#a", + expected: + "" + + "#a" + + "" + }, { + input: "#test", + expected: + "" + + "#test" + + "" + }, { + input: "#äöü", + expected: + "" + + "#äöü" + + "" + }, { + input: "inline #channel text", + expected: + "inline " + + "" + + "#channel" + + "" + + " text" + }, { + input: "#1,000", + expected: + "" + + "#1,000" + + "" + }, { + input: "@#a", + expected: + "@" + + "" + + "#a" + + "" + }]; + + const actual = testCases.map(testCase => parse(testCase.input)); + const expected = testCases.map(testCase => testCase.expected); + + expect(actual).to.deep.equal(expected); + }); + + it("should not find channels", () => { + const testCases = [{ + input: "hi#test", + expected: "hi#test" + }, { + input: "#", + expected: "#" + }]; + + const actual = testCases.map(testCase => parse(testCase.input)); + const expected = testCases.map(testCase => testCase.expected); + + expect(actual).to.deep.equal(expected); + }); + + it("should style like mirc", () => { + const testCases = [{ + input: "\x02bold", + expected: "bold" + }, { + input: "\x038yellowText", + expected: "yellowText" + }, { + input: "\x030,0white,white", + expected: "white,white" + }, { + input: "\x034,8yellowBGredText", + expected: "yellowBGredText" + }, { + input: "\x1ditalic", + expected: "italic" + }, { + input: "\x1funderline", + expected: "underline" + }, { + input: "\x02bold\x038yellow\x02nonBold\x03default", + expected: + "bold" + + "yellow" + + "nonBold" + + "default" + }, { + input: "\x02bold\x02 \x02bold\x02", + expected: + "bold" + + " " + + "bold" + }]; + + const actual = testCases.map(testCase => parse(testCase.input)); + const expected = testCases.map(testCase => testCase.expected); + + expect(actual).to.deep.equal(expected); + }); + + it("should go bonkers like mirc", () => { + const testCases = [{ + input: "\x02irc\x0f://\x1dfreenode.net\x0f/\x034,8thelounge", + expected: + "" + + "irc" + + "://" + + "freenode.net" + + "/" + + "thelounge" + + "" + }, { + input: "\x02#\x038,9thelounge", + expected: + "" + + "#" + + "thelounge" + + "" + }]; + + const actual = testCases.map(testCase => parse(testCase.input)); + const expected = testCases.map(testCase => testCase.expected); + + expect(actual).to.deep.equal(expected); + }); + + it("should optimize generated html", () => { + const testCases = [{ + input: "test \x0312#\x0312\x0312\"te\x0312st\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312a", + expected: + "test " + + "" + + "#"testa" + + "" + }]; + + const actual = testCases.map(testCase => parse(testCase.input)); + const expected = testCases.map(testCase => testCase.expected); + + expect(actual).to.deep.equal(expected); + }); + + it("should trim commom protocols", () => { + const testCases = [{ + input: "like..http://example.com", + expected: + "like.." + + "" + + "http://example.com" + + "" + }, { + input: "like..HTTP://example.com", + expected: + "like.." + + "" + + "HTTP://example.com" + + "" + }]; + + const actual = testCases.map(testCase => parse(testCase.input)); + const expected = testCases.map(testCase => testCase.expected); + + expect(actual).to.deep.equal(expected); + }); + + it("should not find channel in fragment", () => { + const testCases = [{ + input: "http://example.com/#hash", + expected: + "" + + "" + + "http://example.com/#hash" + + "" + }]; + + const actual = testCases.map(testCase => parse(testCase.input)); + const expected = testCases.map(testCase => testCase.expected); + + expect(actual).to.deep.equal(expected); + }); + + it("should not overlap parts", () => { + const input = "Url: http://example.com/path Channel: ##channel"; + const actual = parse(input); + + expect(actual).to.equal( + "Url: http://example.com/path " + + "Channel: ##channel" + ); + }); +}); From 5b4c00d8ca595a1c392f70893d0ca9830ce4028c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Mon, 17 Apr 2017 23:28:35 -0400 Subject: [PATCH 0312/3926] Extract findLinks into its own file and add tests Tests were taken from https://github.com/Bonuspunkt/ircmessageparser/blob/5a249c30b1e0379e5df62f10734a3dc7ce02306b/test/findLinks.js. The underlying code is different but the tests are the same. --- .../handlebars/ircmessageparser/findLinks.js | 53 +++++++++ client/js/libs/handlebars/parse.js | 50 +-------- .../handlebars/ircmessageparser/findLinks.js | 106 ++++++++++++++++++ 3 files changed, 160 insertions(+), 49 deletions(-) create mode 100644 client/js/libs/handlebars/ircmessageparser/findLinks.js create mode 100644 test/client/js/libs/handlebars/ircmessageparser/findLinks.js diff --git a/client/js/libs/handlebars/ircmessageparser/findLinks.js b/client/js/libs/handlebars/ircmessageparser/findLinks.js new file mode 100644 index 00000000..031afa17 --- /dev/null +++ b/client/js/libs/handlebars/ircmessageparser/findLinks.js @@ -0,0 +1,53 @@ +"use strict"; + +const URI = require("urijs"); + +const commonSchemes = [ + "http", "https", + "ftp", "sftp", + "smb", "file", + "irc", "ircs", + "svn", "git", + "steam", "mumble", "ts3server", + "svn+ssh", "ssh", +]; + +function findLinks(text) { + let result = []; + let lastPosition = 0; + + URI.withinString(text, function(url, start, end) { + // v-- fix: url was modified and does not match input string -> cant be mapped + if (text.indexOf(url, lastPosition) < 0) { + return; + } + // ^-- /fix: url was modified and does not match input string -> cant be mapped + + // v-- fix: use prefered scheme + const parsed = URI(url); + const parsedScheme = parsed.scheme().toLowerCase(); + const matchedScheme = commonSchemes.find(scheme => parsedScheme.endsWith(scheme)); + + if (matchedScheme) { + const prefix = parsedScheme.length - matchedScheme.length; + start += prefix; + url = url.slice(prefix); + } + // ^-- /fix: use prefered scheme + + // URL matched, but does not start with a protocol, add it + if (!parsedScheme.length) { + url = "http://" + url; + } + + result.push({ + start: start, + end: end, + link: url + }); + }); + + return result; +} + +module.exports = findLinks; diff --git a/client/js/libs/handlebars/parse.js b/client/js/libs/handlebars/parse.js index 8c6ae432..fa21b898 100644 --- a/client/js/libs/handlebars/parse.js +++ b/client/js/libs/handlebars/parse.js @@ -1,59 +1,11 @@ "use strict"; const Handlebars = require("handlebars/runtime"); -const URI = require("urijs"); const parseStyle = require("./ircmessageparser/parseStyle"); const findChannels = require("./ircmessageparser/findChannels"); +const findLinks = require("./ircmessageparser/findLinks"); const merge = require("./ircmessageparser/merge"); -const commonSchemes = [ - "http", "https", - "ftp", "sftp", - "smb", "file", - "irc", "ircs", - "svn", "git", - "steam", "mumble", "ts3server", - "svn+ssh", "ssh", -]; - -function findLinks(text) { - let result = []; - let lastPosition = 0; - - URI.withinString(text, function(url, start, end) { - // v-- fix: url was modified and does not match input string -> cant be mapped - if (text.indexOf(url, lastPosition) < 0) { - return; - } - // ^-- /fix: url was modified and does not match input string -> cant be mapped - - // v-- fix: use prefered scheme - const parsed = URI(url); - const parsedScheme = parsed.scheme().toLowerCase(); - const matchedScheme = commonSchemes.find(scheme => parsedScheme.endsWith(scheme)); - - if (matchedScheme) { - const prefix = parsedScheme.length - matchedScheme.length; - start += prefix; - url = url.slice(prefix); - } - // ^-- /fix: use prefered scheme - - // URL matched, but does not start with a protocol, add it - if (!parsedScheme.length) { - url = "http://" + url; - } - - result.push({ - start: start, - end: end, - link: url - }); - }); - - return result; -} - function createFragment(fragment) { let className = ""; if (fragment.bold) { diff --git a/test/client/js/libs/handlebars/ircmessageparser/findLinks.js b/test/client/js/libs/handlebars/ircmessageparser/findLinks.js new file mode 100644 index 00000000..f3f228f2 --- /dev/null +++ b/test/client/js/libs/handlebars/ircmessageparser/findLinks.js @@ -0,0 +1,106 @@ +"use strict"; + +const expect = require("chai").expect; +const findLinks = require("../../../../../../client/js/libs/handlebars/ircmessageparser/findLinks"); + +describe("findLinks", () => { + it("should find url", () => { + const input = "irc://freenode.net/thelounge"; + const expected = [{ + start: 0, + end: 28, + link: "irc://freenode.net/thelounge", + }]; + + const actual = findLinks(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should find urls with www", () => { + const input = "www.nooooooooooooooo.com"; + const expected = [{ + start: 0, + end: 24, + link: "http://www.nooooooooooooooo.com" + }]; + + const actual = findLinks(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should find urls in strings", () => { + const input = "look at https://thelounge.github.io/ for more information"; + const expected = [{ + link: "https://thelounge.github.io/", + start: 8, + end: 36 + }]; + + const actual = findLinks(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should find urls in strings starting with www", () => { + const input = "use www.duckduckgo.com for privacy reasons"; + const expected = [{ + link: "http://www.duckduckgo.com", + start: 4, + end: 22 + }]; + + const actual = findLinks(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should find urls with odd surroundings", () => { + const input = ""; + const expected = [{ + link: "https://theos.kyriasis.com/~kyrias/stats/archlinux.html", + start: 1, + end: 56 + }]; + + const actual = findLinks(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should find urls with starting with www. and odd surroundings", () => { + const input = ".:www.github.com:."; + const expected = [{ + link: "http://www.github.com", + start: 2, + end: 16 + }]; + + const actual = findLinks(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should not find urls", () => { + const input = "text www. text"; + const expected = []; + + const actual = findLinks(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should handle multiple www. correctly", () => { + const input = "www.www.test.com"; + const expected = [{ + link: "http://www.www.test.com", + start: 0, + end: 16 + }]; + + const actual = findLinks(input); + + expect(actual).to.deep.equal(expected); + }); +}); From 90f4a94bb246bd91a0c2d2e2e54f717d16f8ba32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 18 Apr 2017 00:53:12 -0400 Subject: [PATCH 0313/3926] Use template literals in parse Also make it output double quotes for consistency with web stuff. --- client/js/libs/handlebars/parse.js | 26 +++----- test/client/js/libs/handlebars/parse.js | 84 ++++++++++++------------- 2 files changed, 52 insertions(+), 58 deletions(-) diff --git a/client/js/libs/handlebars/parse.js b/client/js/libs/handlebars/parse.js index fa21b898..7e9aebb8 100644 --- a/client/js/libs/handlebars/parse.js +++ b/client/js/libs/handlebars/parse.js @@ -7,25 +7,25 @@ const findLinks = require("./ircmessageparser/findLinks"); const merge = require("./ircmessageparser/merge"); function createFragment(fragment) { - let className = ""; + let classes = []; if (fragment.bold) { - className += " irc-bold"; + classes.push("irc-bold"); } if (fragment.textColor !== undefined) { - className += " irc-fg" + fragment.textColor; + classes.push("irc-fg" + fragment.textColor); } if (fragment.bgColor !== undefined) { - className += " irc-bg" + fragment.bgColor; + classes.push("irc-bg" + fragment.bgColor); } if (fragment.italic) { - className += " irc-italic"; + classes.push("irc-italic"); } if (fragment.underline) { - className += " irc-underline"; + classes.push("irc-underline"); } const escapedText = Handlebars.Utils.escapeExpression(fragment.text); - if (className) { - return "" + escapedText + ""; + if (classes.length) { + return `${escapedText}`; } return escapedText; } @@ -49,16 +49,10 @@ module.exports = function parse(text) { if (textPart.link) { const escapedLink = Handlebars.Utils.escapeExpression(textPart.link); - return ( - "" + - fragments + - ""); + return `${fragments}`; } else if (textPart.channel) { const escapedChannel = Handlebars.Utils.escapeExpression(textPart.channel); - return ( - "" + - fragments + - ""); + return `${fragments}`; } return fragments; diff --git a/test/client/js/libs/handlebars/parse.js b/test/client/js/libs/handlebars/parse.js index 7d1e025a..d3737e98 100644 --- a/test/client/js/libs/handlebars/parse.js +++ b/test/client/js/libs/handlebars/parse.js @@ -10,7 +10,7 @@ describe("parse Handlebars helper", () => { expected: "<img onerror='location.href="//youtube.com"'>" }, { input: "#&\">bug", - expected: "#&">bug" + expected: "#&">bug" }]; const actual = testCases.map(testCase => parse(testCase.input)); @@ -35,20 +35,20 @@ describe("parse Handlebars helper", () => { const testCases = [{ input: "irc://freenode.net/thelounge", expected: - "" + + "" + "irc://freenode.net/thelounge" + "" }, { input: "www.nooooooooooooooo.com", expected: - "" + + "" + "www.nooooooooooooooo.com" + "" }, { input: "look at https://thelounge.github.io/ for more information", expected: "look at " + - "" + + "" + "https://thelounge.github.io/" + "" + " for more information", @@ -56,14 +56,14 @@ describe("parse Handlebars helper", () => { input: "use www.duckduckgo.com for privacy reasons", expected: "use " + - "" + + "" + "www.duckduckgo.com" + "" + " for privacy reasons" }, { input: "svn+ssh://example.org", expected: - "" + + "" + "svn+ssh://example.org" + "" }]; @@ -79,7 +79,7 @@ describe("parse Handlebars helper", () => { "bonuspunkt: your URL parser misparses this URL: https://msdn.microsoft.com/en-us/library/windows/desktop/ms644989(v=vs.85).aspx"; const correctResult = "bonuspunkt: your URL parser misparses this URL: " + - "" + + "" + "https://msdn.microsoft.com/en-us/library/windows/desktop/ms644989(v=vs.85).aspx" + ""; @@ -93,7 +93,7 @@ describe("parse Handlebars helper", () => { input: "", expected: "<" + - "" + + "" + "https://theos.kyriasis.com/~kyrias/stats/archlinux.html" + "" + ">" @@ -101,20 +101,20 @@ describe("parse Handlebars helper", () => { input: "abc (www.example.com)", expected: "abc (" + - "" + + "" + "www.example.com" + "" + ")" }, { input: "http://example.com/Test_(Page)", expected: - "" + + "" + "http://example.com/Test_(Page)" + "" }, { input: "www.example.com/Test_(Page)", expected: - "" + + "" + "www.example.com/Test_(Page)" + "" }]; @@ -144,40 +144,40 @@ describe("parse Handlebars helper", () => { const testCases = [{ input: "#a", expected: - "" + + "" + "#a" + "" }, { input: "#test", expected: - "" + + "" + "#test" + "" }, { input: "#äöü", expected: - "" + + "" + "#äöü" + "" }, { input: "inline #channel text", expected: "inline " + - "" + + "" + "#channel" + "" + " text" }, { input: "#1,000", expected: - "" + + "" + "#1,000" + "" }, { input: "@#a", expected: "@" + - "" + + "" + "#a" + "" }]; @@ -206,35 +206,35 @@ describe("parse Handlebars helper", () => { it("should style like mirc", () => { const testCases = [{ input: "\x02bold", - expected: "bold" + expected: "bold" }, { input: "\x038yellowText", - expected: "yellowText" + expected: "yellowText" }, { input: "\x030,0white,white", - expected: "white,white" + expected: "white,white" }, { input: "\x034,8yellowBGredText", - expected: "yellowBGredText" + expected: "yellowBGredText" }, { input: "\x1ditalic", - expected: "italic" + expected: "italic" }, { input: "\x1funderline", - expected: "underline" + expected: "underline" }, { input: "\x02bold\x038yellow\x02nonBold\x03default", expected: - "bold" + - "yellow" + - "nonBold" + + "bold" + + "yellow" + + "nonBold" + "default" }, { input: "\x02bold\x02 \x02bold\x02", expected: - "bold" + + "bold" + " " + - "bold" + "bold" }]; const actual = testCases.map(testCase => parse(testCase.input)); @@ -247,19 +247,19 @@ describe("parse Handlebars helper", () => { const testCases = [{ input: "\x02irc\x0f://\x1dfreenode.net\x0f/\x034,8thelounge", expected: - "" + - "irc" + + "" + + "irc" + "://" + - "freenode.net" + + "freenode.net" + "/" + - "thelounge" + + "thelounge" + "" }, { input: "\x02#\x038,9thelounge", expected: - "" + - "#" + - "thelounge" + + "" + + "#" + + "thelounge" + "" }]; @@ -274,8 +274,8 @@ describe("parse Handlebars helper", () => { input: "test \x0312#\x0312\x0312\"te\x0312st\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312a", expected: "test " + - "" + - "#"testa" + + "" + + "#"testa" + "" }]; @@ -290,14 +290,14 @@ describe("parse Handlebars helper", () => { input: "like..http://example.com", expected: "like.." + - "" + + "" + "http://example.com" + "" }, { input: "like..HTTP://example.com", expected: "like.." + - "" + + "" + "HTTP://example.com" + "" }]; @@ -313,7 +313,7 @@ describe("parse Handlebars helper", () => { input: "http://example.com/#hash", expected: "" + - "" + + "" + "http://example.com/#hash" + "" }]; @@ -329,8 +329,8 @@ describe("parse Handlebars helper", () => { const actual = parse(input); expect(actual).to.equal( - "Url: http://example.com/path " + - "Channel: ##channel" + "Url: http://example.com/path " + + "Channel: ##channel" ); }); }); From 03e3444a352bc5045ff8f4669bc3ed565b369bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 4 Apr 2017 00:36:03 -0400 Subject: [PATCH 0314/3926] Explain the modules of the message parser and add tests - Add comments and descriptions to: - `findChannels.js` - `parseStyle` - `findLinks` - `fill` - `anyIntersection` - `merge` - `parse` - Minor optimizations to `parseStyle` - Add tests for `fill` --- .../ircmessageparser/anyIntersection.js | 2 + .../libs/handlebars/ircmessageparser/fill.js | 17 ++-- .../ircmessageparser/findChannels.js | 19 +++- .../handlebars/ircmessageparser/findLinks.js | 20 +++- .../libs/handlebars/ircmessageparser/merge.js | 15 ++- .../handlebars/ircmessageparser/parseStyle.js | 98 ++++++++++++------- client/js/libs/handlebars/parse.js | 17 +++- .../libs/handlebars/ircmessageparser/fill.js | 50 ++++++++++ .../ircmessageparser/findChannels.js | 20 ++-- 9 files changed, 195 insertions(+), 63 deletions(-) create mode 100644 test/client/js/libs/handlebars/ircmessageparser/fill.js diff --git a/client/js/libs/handlebars/ircmessageparser/anyIntersection.js b/client/js/libs/handlebars/ircmessageparser/anyIntersection.js index 4fd0d239..a77e031d 100644 --- a/client/js/libs/handlebars/ircmessageparser/anyIntersection.js +++ b/client/js/libs/handlebars/ircmessageparser/anyIntersection.js @@ -1,5 +1,7 @@ "use strict"; +// Return true if any section of "a" or "b" parts (defined by their start/end +// markers) intersect each other, false otherwise. function anyIntersection(a, b) { return a.start <= b.start && b.start < a.end || a.start < b.end && b.end <= a.end || diff --git a/client/js/libs/handlebars/ircmessageparser/fill.js b/client/js/libs/handlebars/ircmessageparser/fill.js index 2cc9f705..7d90a96c 100644 --- a/client/js/libs/handlebars/ircmessageparser/fill.js +++ b/client/js/libs/handlebars/ircmessageparser/fill.js @@ -1,21 +1,26 @@ "use strict"; +// Create plain text entries corresponding to areas of the text that match no +// existing entries. Returns an empty array if all parts of the text have been +// parsed into recognizable entries already. function fill(existingEntries, text) { let position = 0; - const result = []; - - for (let i = 0; i < existingEntries.length; i++) { - const textSegment = existingEntries[i]; + // Fill inner parts of the text. For example, if text is `foobarbaz` and both + // `foo` and `baz` have matched into an entry, this will return a dummy entry + // corresponding to `bar`. + const result = existingEntries.reduce((acc, textSegment) => { if (textSegment.start > position) { - result.push({ + acc.push({ start: position, end: textSegment.start }); } position = textSegment.end; - } + return acc; + }, []); + // Complete the unmatched end of the text with a dummy entry if (position < text.length) { result.push({ start: position, diff --git a/client/js/libs/handlebars/ircmessageparser/findChannels.js b/client/js/libs/handlebars/ircmessageparser/findChannels.js index b613415c..6edd5dad 100644 --- a/client/js/libs/handlebars/ircmessageparser/findChannels.js +++ b/client/js/libs/handlebars/ircmessageparser/findChannels.js @@ -1,20 +1,31 @@ "use strict"; +// Escapes the RegExp special characters "^", "$", "", ".", "*", "+", "?", "(", +// ")", "[", "]", "{", "}", and "|" in string. +// See https://lodash.com/docs/#escapeRegExp const escapeRegExp = require("lodash/escapeRegExp"); -// NOTE: channel prefixes should be RPL_ISUPPORT.CHANTYPES -// NOTE: userModes should be RPL_ISUPPORT.PREFIX +// Given an array of channel prefixes (such as "#" and "&") and an array of user +// modes (such as "@" and "+"), this function extracts channels and nicks from a +// text. +// It returns an array of objects for each channel found with their start index, +// end index and channel name. function findChannels(text, channelPrefixes, userModes) { + // `userModePattern` is necessary to ignore user modes in /whois responses. + // For example, a voiced user in #thelounge will have a /whois response of: + // > foo is on the following channels: +#thelounge + // We need to explicitly ignore user modes to parse such channels correctly. const userModePattern = userModes.map(escapeRegExp).join(""); const channelPrefixPattern = channelPrefixes.map(escapeRegExp).join(""); - - const channelPattern = `(?:^|\\s)[${ userModePattern }]*([${ channelPrefixPattern }][^ \u0007]+)`; + const channelPattern = `(?:^|\\s)[${userModePattern}]*([${channelPrefixPattern}][^ \u0007]+)`; const channelRegExp = new RegExp(channelPattern, "g"); const result = []; let match; do { + // With global ("g") regexes, calling `exec` multiple times will find + // successive matches in the same string. match = channelRegExp.exec(text); if (match) { diff --git a/client/js/libs/handlebars/ircmessageparser/findLinks.js b/client/js/libs/handlebars/ircmessageparser/findLinks.js index 031afa17..9596a5a0 100644 --- a/client/js/libs/handlebars/ircmessageparser/findLinks.js +++ b/client/js/libs/handlebars/ircmessageparser/findLinks.js @@ -2,6 +2,9 @@ const URI = require("urijs"); +// Known schemes to detect in a text. If a text contains `foo...bar://foo.com`, +// the parsed scheme should be `foo...bar` but if it contains +// `foo...http://foo.com`, we assume the scheme to extract will be `http`. const commonSchemes = [ "http", "https", "ftp", "sftp", @@ -16,6 +19,10 @@ function findLinks(text) { let result = []; let lastPosition = 0; + // URI.withinString() identifies URIs within text, e.g. to translate them to + // -Tags. + // See https://medialize.github.io/URI.js/docs.html#static-withinString + // In our case, we store each URI encountered in a result array. URI.withinString(text, function(url, start, end) { // v-- fix: url was modified and does not match input string -> cant be mapped if (text.indexOf(url, lastPosition) < 0) { @@ -23,19 +30,22 @@ function findLinks(text) { } // ^-- /fix: url was modified and does not match input string -> cant be mapped - // v-- fix: use prefered scheme - const parsed = URI(url); - const parsedScheme = parsed.scheme().toLowerCase(); + // Extract the scheme of the URL detected, if there is one + const parsedScheme = URI(url).scheme().toLowerCase(); + + // Check if the scheme of the detected URL matches a common one above. + // In a URL like `foo..http://example.com`, the scheme would be `foo..http`, + // so we need to clean up the end of the scheme and filter out the rest. const matchedScheme = commonSchemes.find(scheme => parsedScheme.endsWith(scheme)); + // A known scheme was found, extract the unknown part from the URL if (matchedScheme) { const prefix = parsedScheme.length - matchedScheme.length; start += prefix; url = url.slice(prefix); } - // ^-- /fix: use prefered scheme - // URL matched, but does not start with a protocol, add it + // The URL matched but does not start with a scheme (`www.foo.com`), add it if (!parsedScheme.length) { url = "http://" + url; } diff --git a/client/js/libs/handlebars/ircmessageparser/merge.js b/client/js/libs/handlebars/ircmessageparser/merge.js index 3da520e8..893997cc 100644 --- a/client/js/libs/handlebars/ircmessageparser/merge.js +++ b/client/js/libs/handlebars/ircmessageparser/merge.js @@ -16,6 +16,7 @@ if (typeof Object_assign !== "function") { }; } +// Merge text part information within a styling fragment function assign(textPart, fragment) { const fragStart = fragment.start; const start = Math.max(fragment.start, textPart.start); @@ -28,13 +29,25 @@ function assign(textPart, fragment) { }); } +// Merge the style fragments withing the text parts, taking into account +// boundaries and text sections that have not matched to links or channels. +// For example, given a string "foobar" where "foo" and "bar" have been +// identified as parts (channels, links, etc.) and "fo", "ob" and "ar" have 3 +// different styles, the first resulting part will contain fragments "fo" and +// "o", and the second resulting part will contain "b" and "ar". "o" and "b" +// fragments will contain duplicate styling attributes. function merge(textParts, styleFragments) { - const cleanText = styleFragments.map(fragment => fragment.text).join(""); + // Re-build the overall text (without control codes) from the style fragments + const cleanText = styleFragments.reduce((acc, frag) => acc + frag.text, ""); + // Every section of the original text that has not been captured in a "part" + // is filled with "text" parts, dummy objects with start/end but no extra + // metadata. const allParts = textParts .concat(fill(textParts, cleanText)) .sort((a, b) => a.start - b.start); + // Distribute the style fragments within the text parts return allParts.map(textPart => { textPart.fragments = styleFragments .filter(fragment => anyIntersection(textPart, fragment)) diff --git a/client/js/libs/handlebars/ircmessageparser/parseStyle.js b/client/js/libs/handlebars/ircmessageparser/parseStyle.js index 54e1c191..d23d5bd6 100644 --- a/client/js/libs/handlebars/ircmessageparser/parseStyle.js +++ b/client/js/libs/handlebars/ircmessageparser/parseStyle.js @@ -1,5 +1,6 @@ "use strict"; +// Styling control codes const BOLD = "\x02"; const COLOR = "\x03"; const RESET = "\x0f"; @@ -7,14 +8,24 @@ const REVERSE = "\x16"; const ITALIC = "\x1d"; const UNDERLINE = "\x1f"; +// Color code matcher, with format `XX,YY` where both `XX` and `YY` are +// integers, `XX` is the text color and `YY` is an optional background color. const colorRx = /^(\d{1,2})(?:,(\d{1,2}))?/; + +// Represents all other control codes that to be ignored/filtered from the text const controlCodesRx = /[\u0000-\u001F]/g; +// Converts a given text into an array of objects, each of them representing a +// similarly styled section of the text. Each object carries the `text`, style +// information (`bold`, `textColor`, `bgcolor`, `reverse`, `italic`, +// `underline`), and `start`/`end` cursors. function parseStyle(text) { const result = []; let start = 0; let position = 0; + // At any given time, these carry style information since last time a styling + // control code was met. let colorCodes, bold, textColor, bgColor, reverse, italic, underline; const resetStyle = () => { @@ -27,27 +38,42 @@ function parseStyle(text) { }; resetStyle(); + // When called, this "closes" the current fragment by adding an entry to the + // `result` array using the styling information set last time a control code + // was met. const emitFragment = () => { + // Uses the text fragment starting from the last control code position up to + // the current position const textPart = text.slice(start, position); - start = position + 1; + // Filters out all non-style related control codes present in this text const processedText = textPart.replace(controlCodesRx, ""); - if (!processedText.length) { - return; + if (processedText.length) { + // Current fragment starts where the previous one ends, or at 0 if none + const fragmentStart = result.length ? result[result.length - 1].end : 0; + + result.push({ + bold, + textColor, + bgColor, + reverse, + italic, + underline, + text: processedText, + start: fragmentStart, + end: fragmentStart + processedText.length + }); } - result.push({ - bold, - textColor, - bgColor, - reverse, - italic, - underline, - text: processedText - }); + // Now that a fragment has been "closed", the next one will start after that + start = position + 1; }; + // This loop goes through each character of the given text one by one by + // bumping the `position` cursor. Every time a new special "styling" character + // is met, an object gets created (with `emitFragment()`)information on text + // encountered since the previous styling character. while (position < text.length) { switch (text[position]) { @@ -56,6 +82,10 @@ function parseStyle(text) { resetStyle(); break; + // Meeting a BOLD character means that the ongoing text is either going to + // be in bold or that the previous one was in bold and the following one + // must be reset. + // This same behavior applies to COLOR, REVERSE, ITALIC, and UNDERLINE. case BOLD: emitFragment(); bold = !bold; @@ -64,20 +94,23 @@ function parseStyle(text) { case COLOR: emitFragment(); + // Go one step further to find the corresponding color colorCodes = text.slice(position + 1).match(colorRx); if (colorCodes) { textColor = Number(colorCodes[1]); - bgColor = Number(colorCodes[2]); - if (Number.isNaN(bgColor)) { - bgColor = undefined; + if (colorCodes[2]) { + bgColor = Number(colorCodes[2]); } + // Color code length is > 1, so bump the current position cursor by as + // much (and reset the start cursor for the current text block as well) position += colorCodes[0].length; + start = position + 1; } else { + // If no color codes were found, toggles back to no colors (like BOLD). textColor = undefined; bgColor = undefined; } - start = position + 1; break; case REVERSE: @@ -95,9 +128,12 @@ function parseStyle(text) { underline = !underline; break; } + + // Evaluate the next character at the next iteration position += 1; } + // The entire text has been parsed, so we finalize the current text fragment. emitFragment(); return result; @@ -107,25 +143,19 @@ const properties = ["bold", "textColor", "bgColor", "italic", "underline", "reve function prepare(text) { return parseStyle(text) - .filter(fragment => fragment.text.length) - .reduce((prev, curr, i) => { - if (i === 0) { - return prev.concat([curr]); + // This optimizes fragments by combining them together when all their values + // for the properties defined above are equal. + .reduce((prev, curr) => { + if (prev.length) { + const lastEntry = prev[prev.length - 1]; + if (properties.every(key => curr[key] === lastEntry[key])) { + lastEntry.text += curr.text; + lastEntry.end += curr.text.length; + return prev; + } } - - const lastEntry = prev[prev.length - 1]; - if (properties.some(key => curr[key] !== lastEntry[key])) { - return prev.concat([curr]); - } - - lastEntry.text += curr.text; - return prev; - }, []) - .map((fragment, i, array) => { - fragment.start = i === 0 ? 0 : array[i - 1].end; - fragment.end = fragment.start + fragment.text.length; - return fragment; - }); + return prev.concat([curr]); + }, []); } module.exports = prepare; diff --git a/client/js/libs/handlebars/parse.js b/client/js/libs/handlebars/parse.js index 7e9aebb8..915a432c 100644 --- a/client/js/libs/handlebars/parse.js +++ b/client/js/libs/handlebars/parse.js @@ -6,6 +6,7 @@ const findChannels = require("./ircmessageparser/findChannels"); const findLinks = require("./ircmessageparser/findLinks"); const merge = require("./ircmessageparser/merge"); +// Create an HTML `span` with styling information for a given fragment function createFragment(fragment) { let classes = []; if (fragment.bold) { @@ -30,23 +31,33 @@ function createFragment(fragment) { return escapedText; } +// Transform an IRC message potentially filled with styling control codes, URLs +// and channels into a string of HTML elements to display on the client. module.exports = function parse(text) { + // Extract the styling information and get the plain text version from it const styleFragments = parseStyle(text); const cleanText = styleFragments.map(fragment => fragment.text).join(""); - const channelPrefixes = ["#", "&"]; // RPL_ISUPPORT.CHANTYPES - const userModes = ["!", "@", "%", "+"]; // RPL_ISUPPORT.PREFIX + // On the plain text, find channels and URLs, returned as "parts". Parts are + // arrays of objects containing start and end markers, as well as metadata + // depending on what was found (channel or link). + const channelPrefixes = ["#", "&"]; // TODO Channel prefixes should be RPL_ISUPPORT.CHANTYPES + const userModes = ["!", "@", "%", "+"]; // TODO User modes should be RPL_ISUPPORT.PREFIX const channelParts = findChannels(cleanText, channelPrefixes, userModes); - const linkParts = findLinks(cleanText); + // Sort all parts identified based on their position in the original text const parts = channelParts .concat(linkParts) .sort((a, b) => a.start - b.start); + // Merge the styling information with the channels / URLs / text objects and + // generate HTML strings with the resulting fragments return merge(parts, styleFragments).map(textPart => { + // Create HTML strings with styling information const fragments = textPart.fragments.map(createFragment).join(""); + // Wrap these potentially styled fragments with links and channel buttons if (textPart.link) { const escapedLink = Handlebars.Utils.escapeExpression(textPart.link); return `${fragments}`; diff --git a/test/client/js/libs/handlebars/ircmessageparser/fill.js b/test/client/js/libs/handlebars/ircmessageparser/fill.js new file mode 100644 index 00000000..8723ad52 --- /dev/null +++ b/test/client/js/libs/handlebars/ircmessageparser/fill.js @@ -0,0 +1,50 @@ +"use strict"; + +const expect = require("chai").expect; +const fill = require("../../../../../../client/js/libs/handlebars/ircmessageparser/fill"); + +describe("fill", () => { + const text = "01234567890123456789"; + + it("should return an entry for the unmatched end of string", () => { + const existingEntries = [ + {start: 0, end: 10}, + {start: 5, end: 15}, + ]; + + const expected = [ + {start: 15, end: 20}, + ]; + + const actual = fill(existingEntries, text); + + expect(actual).to.deep.equal(expected); + }); + + it("should return an entry per unmatched areas of the text", () => { + const existingEntries = [ + {start: 0, end: 5}, + {start: 10, end: 15}, + ]; + + const expected = [ + {start: 5, end: 10}, + {start: 15, end: 20}, + ]; + + const actual = fill(existingEntries, text); + + expect(actual).to.deep.equal(expected); + }); + + it("should not return anything when entries match all text", () => { + const existingEntries = [ + {start: 0, end: 10}, + {start: 10, end: 20}, + ]; + + const actual = fill(existingEntries, text); + + expect(actual).to.be.empty; + }); +}); diff --git a/test/client/js/libs/handlebars/ircmessageparser/findChannels.js b/test/client/js/libs/handlebars/ircmessageparser/findChannels.js index 93c119ee..4c676e57 100644 --- a/test/client/js/libs/handlebars/ircmessageparser/findChannels.js +++ b/test/client/js/libs/handlebars/ircmessageparser/findChannels.js @@ -1,7 +1,7 @@ "use strict"; const expect = require("chai").expect; -const analyseText = require("../../../../../../client/js/libs/handlebars/ircmessageparser/findChannels"); +const findChannels = require("../../../../../../client/js/libs/handlebars/ircmessageparser/findChannels"); describe("findChannels", () => { it("should find single letter channel", () => { @@ -12,7 +12,7 @@ describe("findChannels", () => { end: 2 }]; - const actual = analyseText(input, ["#"], ["@", "+"]); + const actual = findChannels(input, ["#"], ["@", "+"]); expect(actual).to.deep.equal(expected); }); @@ -25,7 +25,7 @@ describe("findChannels", () => { end: 4 }]; - const actual = analyseText(input, ["#"], ["@", "+"]); + const actual = findChannels(input, ["#"], ["@", "+"]); expect(actual).to.deep.equal(expected); }); @@ -38,7 +38,7 @@ describe("findChannels", () => { end: 15 }]; - const actual = analyseText(input, ["#"], ["@", "+"]); + const actual = findChannels(input, ["#"], ["@", "+"]); expect(actual).to.deep.equal(expected); }); @@ -51,7 +51,7 @@ describe("findChannels", () => { end: 5 }]; - const actual = analyseText(input, ["#"], ["@", "+"]); + const actual = findChannels(input, ["#"], ["@", "+"]); expect(actual).to.deep.equal(expected); }); @@ -64,7 +64,7 @@ describe("findChannels", () => { end: 6 }]; - const actual = analyseText(input, ["#"], ["@", "+"]); + const actual = findChannels(input, ["#"], ["@", "+"]); expect(actual).to.deep.equal(expected); }); @@ -77,7 +77,7 @@ describe("findChannels", () => { end: 3 }]; - const actual = analyseText(input, ["#"], ["@", "+"]); + const actual = findChannels(input, ["#"], ["@", "+"]); expect(actual).to.deep.equal(expected); }); @@ -90,7 +90,7 @@ describe("findChannels", () => { end: 6 }]; - const actual = analyseText(input, ["#"], ["!", "@", "%", "+"]); + const actual = findChannels(input, ["#"], ["!", "@", "%", "+"]); expect(actual).to.deep.equal(expected); }); @@ -103,7 +103,7 @@ describe("findChannels", () => { end: 2 }]; - const actual = analyseText(input, ["@"], ["#", "+"]); + const actual = findChannels(input, ["@"], ["#", "+"]); expect(actual).to.deep.equal(expected); }); @@ -116,7 +116,7 @@ describe("findChannels", () => { end: 6 }]; - const actual = analyseText(input, ["#"], ["@", "+"]); + const actual = findChannels(input, ["#"], ["@", "+"]); expect(actual).to.deep.equal(expected); }); From fa1aecdd9e9e0cab27e2f5f92f485e99fe69a376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Thu, 20 Apr 2017 01:37:10 -0400 Subject: [PATCH 0315/3926] Remove URI.js monkey-patch as fix landed in v1.18.5 See https://github.com/medialize/URI.js/issues/325 --- client/js/libs/handlebars/ircmessageparser/findLinks.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/client/js/libs/handlebars/ircmessageparser/findLinks.js b/client/js/libs/handlebars/ircmessageparser/findLinks.js index 9596a5a0..1bd989b2 100644 --- a/client/js/libs/handlebars/ircmessageparser/findLinks.js +++ b/client/js/libs/handlebars/ircmessageparser/findLinks.js @@ -17,19 +17,12 @@ const commonSchemes = [ function findLinks(text) { let result = []; - let lastPosition = 0; // URI.withinString() identifies URIs within text, e.g. to translate them to // -Tags. // See https://medialize.github.io/URI.js/docs.html#static-withinString // In our case, we store each URI encountered in a result array. URI.withinString(text, function(url, start, end) { - // v-- fix: url was modified and does not match input string -> cant be mapped - if (text.indexOf(url, lastPosition) < 0) { - return; - } - // ^-- /fix: url was modified and does not match input string -> cant be mapped - // Extract the scheme of the URL detected, if there is one const parsedScheme = URI(url).scheme().toLowerCase(); From 999e419636caf37b8b46e4ffdbc5c4aee0ef1081 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 21 Apr 2017 21:00:57 +0300 Subject: [PATCH 0316/3926] Remove cycle nicks button Reverts #708. Fixes #869. Fixes #1023. --- client/css/style.css | 11 +---------- client/index.html | 3 --- client/js/lounge.js | 6 ------ 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index d74baf61..c34216e7 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -169,7 +169,6 @@ kbd { #footer .icon, #chat .count:before, #settings #play:before, -#form #cycle-nicks:before, #form #submit:before, #chat .invite .from:before, #chat .join .from:before, @@ -216,8 +215,6 @@ kbd { #footer .help:before { content: "\f059"; /* http://fontawesome.io/icon/question/ */ } #footer .sign-out:before { content: "\f011"; /* http://fontawesome.io/icon/power-off/ */ } -#form #cycle-nicks:before { content: "\f007"; /* http://fontawesome.io/icon/user/ */ } - #form #submit:before { content: "\f1d8"; /* http://fontawesome.io/icon/paper-plane/ */ } #chat .invite .from:before { @@ -1451,22 +1448,16 @@ kbd { align-self: center; } -#form #cycle-nicks, #form #submit { color: #9ca5b4; font-size: 14px; height: 32px; transition: opacity .2s; - width: 24px; + width: 32px; -webkit-flex: 0 0 auto; flex: 0 0 auto; } -#form #submit { - margin-right: 4px; -} - -#form #cycle-nicks:hover, #form #submit:hover { opacity: .6; } diff --git a/client/index.html b/client/index.html index 83ac96ad..f3073fa3 100644 --- a/client/index.html +++ b/client/index.html @@ -70,9 +70,6 @@ --> - - - diff --git a/client/js/lounge.js b/client/js/lounge.js index 0e65b5d3..6b664e56 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -870,12 +870,6 @@ $(function() { input.trigger("click").focus(); }; - // Cycle through nicks for the current word, just like hitting "Tab" - $("#cycle-nicks").on("click", function() { - input.triggerHandler($.Event("keydown.tabcomplete", {which: 9})); - forceFocus(); - }); - $("#form").on("submit", function(e) { e.preventDefault(); forceFocus(); From 751d6690ffd3867737b71dfb849d38af9773b76a Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 21 Apr 2017 20:25:48 +0000 Subject: [PATCH 0317/3926] chore(package): update babel-loader to version 7.0.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 30154e29..2ab0302c 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ }, "devDependencies": { "babel-core": "6.24.1", - "babel-loader": "6.4.1", + "babel-loader": "7.0.0", "babel-preset-es2015": "6.24.1", "chai": "3.5.0", "eslint": "3.19.0", From 648cfd12db53ab7d4d0a31815df3147b84679bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 19 Apr 2017 02:33:05 -0400 Subject: [PATCH 0318/3926] Use moment on the client to display friendly dates Also, unread and date markers are now half-transparent based on their colors and not parent opacity. This is necessary to display a non-translucide tooltip. --- client/css/style.css | 12 ++++------ client/js/libs/handlebars/friendlydate.js | 13 ++++++++++ client/themes/morning.css | 5 ---- client/themes/zenburn.css | 5 ---- client/views/date-marker.tpl | 6 +++-- .../js/libs/handlebars/friendlydateTest.js | 24 +++++++++++++++++++ webpack.config.js | 1 + 7 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 client/js/libs/handlebars/friendlydate.js create mode 100644 test/client/js/libs/handlebars/friendlydateTest.js diff --git a/client/css/style.css b/client/css/style.css index d74baf61..23e2e5a5 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -841,7 +841,6 @@ kbd { #chat .unread-marker { position: relative; text-align: center; - opacity: .5; margin: 0 10px; z-index: 0; font-weight: bold; @@ -855,13 +854,13 @@ kbd { left: 0; right: 0; top: 50%; - border-top: 1px solid #e74c3c; + border-top: 1px solid rgba(231, 76, 60, .5); } #chat .unread-marker-text:before { content: "New messages"; background-color: white; - color: #e74c3c; + color: rgba(231, 76, 60, .5); padding: 0 10px; } @@ -872,7 +871,6 @@ kbd { #chat .date-marker { position: relative; text-align: center; - opacity: .5; margin: 0 10px; z-index: 0; font-weight: bold; @@ -886,13 +884,13 @@ kbd { left: 0; right: 0; top: 50%; - border-top: 1px solid #006b3b; + border-top: 1px solid rgba(0, 107, 59, .5); } #chat .date-marker-text:before { - content: attr(data-date); + content: attr(data-label); background-color: white; - color: #006b3b; + color: rgba(0, 107, 59, .5); padding: 0 10px; } diff --git a/client/js/libs/handlebars/friendlydate.js b/client/js/libs/handlebars/friendlydate.js new file mode 100644 index 00000000..92e81080 --- /dev/null +++ b/client/js/libs/handlebars/friendlydate.js @@ -0,0 +1,13 @@ +"use strict"; + +const moment = require("moment"); + +module.exports = function(time) { + // See http://momentjs.com/docs/#/displaying/calendar-time/ + return moment(new Date(time)).calendar(null, { + sameDay: "[Today]", + lastDay: "[Yesterday]", + lastWeek: "L", // Locale + sameElse: "L" + }); +}; diff --git a/client/themes/morning.css b/client/themes/morning.css index 9698330c..d5ca62e9 100644 --- a/client/themes/morning.css +++ b/client/themes/morning.css @@ -155,11 +155,6 @@ body { border-color: #333c4a; } -#chat .unread-marker, -#chat .date-marker { - opacity: 1; -} - #chat .unread-marker-text:before { background-color: #333c4a; } diff --git a/client/themes/zenburn.css b/client/themes/zenburn.css index 2c075412..b4abd4ed 100644 --- a/client/themes/zenburn.css +++ b/client/themes/zenburn.css @@ -181,11 +181,6 @@ body { border-color: #3f3f3f; } -#chat .unread-marker, -.date-marker { - opacity: 1; -} - #chat .unread-marker-text:before { background-color: #3f3f3f; } diff --git a/client/views/date-marker.tpl b/client/views/date-marker.tpl index bf9d89c7..3e5ec60d 100644 --- a/client/views/date-marker.tpl +++ b/client/views/date-marker.tpl @@ -1,3 +1,5 @@ -
- +
+
+ +
diff --git a/test/client/js/libs/handlebars/friendlydateTest.js b/test/client/js/libs/handlebars/friendlydateTest.js new file mode 100644 index 00000000..16f335c3 --- /dev/null +++ b/test/client/js/libs/handlebars/friendlydateTest.js @@ -0,0 +1,24 @@ +"use strict"; + +const expect = require("chai").expect; +const moment = require("moment"); +const friendlydate = require("../../../../../client/js/libs/handlebars/friendlydate"); + +describe("friendlydate Handlebars helper", () => { + it("should render 'Today' as a human-friendly date", () => { + const time = new Date().getTime(); + expect(friendlydate(time)).to.equal("Today"); + }); + + it("should render 'Yesterday' as a human-friendly date", () => { + const time = new Date().getTime() - 24 * 3600 * 1000; + expect(friendlydate(time)).to.equal("Yesterday"); + }); + + it("should not render any friendly dates prior to the day before", () => { + [2, 7, 30, 365, 1000].forEach(day => { + const time = new Date().getTime() - 24 * 3600 * 1000 * day; + expect(friendlydate(time)).to.equal(moment(time).format("L")); + }); + }); +}); diff --git a/webpack.config.js b/webpack.config.js index c71979f4..93514dca 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -14,6 +14,7 @@ let config = { "handlebars/runtime", "jquery", "jquery-ui/ui/widgets/sortable", + "moment", "mousetrap", "socket.io-client", "urijs", From 5fabf2f61ac1a2c6fc4c56d03bce0a4a167ed4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sat, 22 Apr 2017 00:06:07 -0400 Subject: [PATCH 0319/3926] Make sure friendly date markers are reset at midnight --- client/js/lounge.js | 20 ++++++++++++++++++++ client/views/date-marker.tpl | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 41505797..ec18b3f1 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -3,6 +3,7 @@ // vendor libraries require("jquery-ui/ui/widgets/sortable"); const $ = require("jquery"); +const moment = require("moment"); const Mousetrap = require("mousetrap"); const URI = require("urijs"); @@ -1593,6 +1594,25 @@ $(function() { } }); + // Compute how many milliseconds are remaining until the next day starts + function msUntilNextDay() { + return moment().add(1, "day").startOf("day") - moment(); + } + + // Go through all Today/Yesterday date markers in the DOM and recompute their + // labels. When done, restart the timer for the next day. + function updateDateMarkers() { + $(".date-marker-text[data-label='Today'], .date-marker-text[data-label='Yesterday']") + .closest(".date-marker-container") + .each(function() { + $(this).replaceWith(templates.date_marker({msgDate: $(this).data("timestamp")})); + }); + + // This should always be 24h later but re-computing exact value just in case + setTimeout(updateDateMarkers, msUntilNextDay()); + } + setTimeout(updateDateMarkers, msUntilNextDay()); + // Only start opening socket.io connection after all events have been registered socket.open(); diff --git a/client/views/date-marker.tpl b/client/views/date-marker.tpl index 3e5ec60d..9e67f09f 100644 --- a/client/views/date-marker.tpl +++ b/client/views/date-marker.tpl @@ -1,4 +1,4 @@ -
+
From a1bdd6f740aa339f8ada5264c5eb989a8925461b Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 22 Apr 2017 13:25:36 +0300 Subject: [PATCH 0320/3926] Rewrite server code of channel sorting Fixes server crash, fixes losing channels --- src/client.js | 44 ++++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/src/client.js b/src/client.js index 27387778..d7145acd 100644 --- a/src/client.js +++ b/src/client.js @@ -412,44 +412,40 @@ Client.prototype.open = function(socketId, target) { }; Client.prototype.sort = function(data) { - var self = this; + const order = data.order; - var type = data.type; - var order = data.order || []; - var sorted = []; + if (!_.isArray(order)) { + return; + } - switch (type) { + switch (data.type) { case "networks": - order.forEach(i => { - var find = _.find(self.networks, {id: i}); - if (find) { - sorted.push(find); - } + this.networks.sort((a, b) => { + return order.indexOf(a.id) > order.indexOf(b.id); }); - self.networks = sorted; + + // Sync order to connected clients + this.emit("sync_sort", {order: this.networks.map(obj => obj.id), type: data.type, target: data.target}); + break; case "channels": - var target = data.target; - var network = _.find(self.networks, {id: target}); + var network = _.find(this.networks, {id: data.target}); if (!network) { return; } - order.forEach(i => { - var find = _.find(network.channels, {id: i}); - if (find) { - sorted.push(find); - } + + network.channels.sort((a, b) => { + return order.indexOf(a.id) > order.indexOf(b.id); }); - network.channels = sorted; + + // Sync order to connected clients + this.emit("sync_sort", {order: network.channels.map(obj => obj.id), type: data.type, target: data.target}); + break; } - self.save(); - - // Sync order to connected clients - const syncOrder = sorted.map(obj => obj.id); - self.emit("sync_sort", {order: syncOrder, type: type, target: data.target}); + this.save(); }; Client.prototype.names = function(data) { From 1e504f438333eea92a02f4862ba0d0d90bb4d7ea Mon Sep 17 00:00:00 2001 From: Alistair McKinlay Date: Sat, 22 Apr 2017 13:51:21 +0100 Subject: [PATCH 0321/3926] Add support for banlist messages --- client/css/style.css | 12 ++++++-- client/index.html | 9 ++++++ client/js/lounge.js | 4 ++- client/views/actions/ban_list.tpl | 18 ++++++++++++ client/views/index.js | 1 + src/client.js | 1 + src/models/msg.js | 3 +- src/plugins/inputs/mode.js | 13 +++++++-- src/plugins/irc-events/banlist.js | 48 +++++++++++++++++++++++++++++++ 9 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 client/views/actions/ban_list.tpl create mode 100644 src/plugins/irc-events/banlist.js diff --git a/client/css/style.css b/client/css/style.css index d74baf61..b483f0a1 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1005,13 +1005,16 @@ kbd { padding-left: 20px; } -#chat table.channel-list { +#chat table.channel-list, +#chat table.ban-list { margin: 5px 10px; width: calc(100% - 30px); } #chat table.channel-list th, -#chat table.channel-list td { +#chat table.ban-list th, +#chat table.channel-list td, +#chat table.ban-list td { padding: 5px; vertical-align: top; border-bottom: #eee 1px solid; @@ -1022,7 +1025,10 @@ kbd { } #chat table.channel-list .channel, -#chat table.channel-list .topic { +#chat table.channel-list .topic, +#chat table.ban-list .hostmask, +#chat table.ban-list .banned_by, +#chat table.ban-list .banned_at { text-align: left; } diff --git a/client/index.html b/client/index.html index 7d751664..58884e6f 100644 --- a/client/index.html +++ b/client/index.html @@ -554,6 +554,15 @@
+
+
+ /banlist +
+
+

Load the banlist for the current channel.

+
+
+
/clear diff --git a/client/js/lounge.js b/client/js/lounge.js index 41505797..ab5713dc 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -20,6 +20,7 @@ $(function() { var commands = [ "/away", "/back", + "/banlist", "/close", "/connect", "/deop", @@ -247,6 +248,7 @@ $(function() { "whois", "ctcp", "channel_list", + "ban_list", ].indexOf(type) !== -1) { template = "msg_action"; } else if (type === "unhandled") { @@ -379,7 +381,7 @@ $(function() { var target = "#chan-" + data.chan; var container = chat.find(target + " .messages"); - if (data.msg.type === "channel_list") { + if (data.msg.type === "channel_list" || data.msg.type === "ban_list") { $(container).empty(); } diff --git a/client/views/actions/ban_list.tpl b/client/views/actions/ban_list.tpl new file mode 100644 index 00000000..c73ba7dc --- /dev/null +++ b/client/views/actions/ban_list.tpl @@ -0,0 +1,18 @@ + + + + + + + + + + {{#each bans}} + + + + + + {{/each}} + +
BannedBanned ByBanned At
{{hostmask}}{{{parse banned_by}}}{{{localetime banned_at}}}
diff --git a/client/views/index.js b/client/views/index.js index 0b92ab8e..fa1916ee 100644 --- a/client/views/index.js +++ b/client/views/index.js @@ -3,6 +3,7 @@ module.exports = { actions: { action: require("./actions/action.tpl"), + ban_list: require("./actions/ban_list.tpl"), channel_list: require("./actions/channel_list.tpl"), ctcp: require("./actions/ctcp.tpl"), invite: require("./actions/invite.tpl"), diff --git a/src/client.js b/src/client.js index 27387778..c7c57efb 100644 --- a/src/client.js +++ b/src/client.js @@ -17,6 +17,7 @@ var id = 0; var events = [ "connection", "unhandled", + "banlist", "ctcp", "error", "invite", diff --git a/src/models/msg.js b/src/models/msg.js index a3ce4bc8..3b36256e 100644 --- a/src/models/msg.js +++ b/src/models/msg.js @@ -20,7 +20,8 @@ Msg.Type = { CTCP: "ctcp", TOPIC: "topic", TOPIC_SET_BY: "topic_set_by", - WHOIS: "whois" + WHOIS: "whois", + BANLIST: "ban_list" }; module.exports = Msg; diff --git a/src/plugins/inputs/mode.js b/src/plugins/inputs/mode.js index b6ee5d80..8395da15 100644 --- a/src/plugins/inputs/mode.js +++ b/src/plugins/inputs/mode.js @@ -1,11 +1,10 @@ "use strict"; -exports.commands = ["mode", "op", "voice", "deop", "devoice"]; - var Chan = require("../../models/chan"); var Msg = require("../../models/msg"); exports.commands = [ + "banlist", "mode", "op", "deop", @@ -15,6 +14,10 @@ exports.commands = [ "devoice", ]; +const chanCommands = [ + "banlist" +]; + exports.input = function(network, chan, cmd, args) { if (cmd !== "mode") { if (chan.type !== Chan.Type.CHANNEL) { @@ -26,7 +29,7 @@ exports.input = function(network, chan, cmd, args) { return; } - if (args.length === 0) { + if (args.length === 0 && chanCommands.indexOf(cmd) === -1) { chan.pushMessage(this, new Msg({ type: Msg.Type.ERROR, text: `Usage: /${cmd} [...nick]` @@ -36,6 +39,7 @@ exports.input = function(network, chan, cmd, args) { } const mode = { + banlist: "+b", op: "+o", hop: "+h", voice: "+v", @@ -44,6 +48,9 @@ exports.input = function(network, chan, cmd, args) { devoice: "-v" }[cmd]; + if (chanCommands.indexOf(cmd) > -1 && args.length === 0) { + network.irc.raw("MODE", chan.name, mode); + } args.forEach(function(target) { network.irc.raw("MODE", chan.name, mode, target); }); diff --git a/src/plugins/irc-events/banlist.js b/src/plugins/irc-events/banlist.js new file mode 100644 index 00000000..d45d81cc --- /dev/null +++ b/src/plugins/irc-events/banlist.js @@ -0,0 +1,48 @@ +"use strict"; + +const Chan = require("../../models/chan"); +const Msg = require("../../models/msg"); + +module.exports = function(irc, network) { + const client = this; + + irc.on("banlist", function(banlist) { + const channel = banlist.channel; + const bans = banlist.bans; + if (!bans) { + const msg = new Msg({ + time: Date.now(), + type: Msg.Type.ERROR, + text: "Banlist empty" + }); + network.getChannel(channel).pushMessage(client, msg, true); + return; + } + + const chanName = `Banlist for ${channel}`; + let chan = network.getChannel(chanName); + if (typeof chan === "undefined") { + chan = new Chan({ + type: Chan.Type.SPECIAL, + name: chanName + }); + network.channels.push(chan); + client.emit("join", { + network: network.id, + chan: chan + }); + } + + chan.pushMessage(client, + new Msg({ + type: Msg.Type.BANLIST, + bans: bans.map((data) => ({ + hostmask: data.banned, + banned_by: data.banned_by, + banned_at: data.banned_at * 1000 + })) + }), + true + ); + }); +}; From 934400f5ee094e61c62dd0304cb55ea9f9666078 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 22 Apr 2017 16:04:18 +0300 Subject: [PATCH 0322/3926] Do not build feature branch with open pull requests on AppVeyor Ref: https://github.com/appveyor/ci/issues/882 --- appveyor.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index e36f70f2..25eba312 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,6 +7,9 @@ version: "{build}" # Do not build on tags (GitHub only) skip_tags: true +# Do not build feature branch with open pull requests +skip_branch_with_pr: true + environment: nodejs_version: '4' From 7522847eccdfd70a6baeed0e12af08b367e90eef Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 22 Apr 2017 19:04:46 +0300 Subject: [PATCH 0323/3926] Enable show more button correctly --- client/js/lounge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index ab5713dc..0de7a10e 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -464,7 +464,7 @@ $(function() { lastDate = msgDate; }); - scrollable.find(".show-more").prop("disabled", false); + scrollable.find(".show-more-button").prop("disabled", false); }); socket.on("network", function(data) { From d1ecdb6b523ae12c8dab6540b4480f94d5b95044 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 22 Apr 2017 19:05:58 +0300 Subject: [PATCH 0324/3926] Fix displayNetwork to work correctly --- client/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/index.html b/client/index.html index 58884e6f..81f63d04 100644 --- a/client/index.html +++ b/client/index.html @@ -131,7 +131,7 @@ {{/unless}}
- {{#unless displayNetwork}} + {{#if displayNetwork}}

Network settings

@@ -168,7 +168,7 @@
- {{/unless}} + {{/if}}

User preferences

From 2e286849fc890164c687ab9dd54eec0217709031 Mon Sep 17 00:00:00 2001 From: Alistair McKinlay Date: Tue, 18 Apr 2017 08:42:26 +0100 Subject: [PATCH 0325/3926] Move commands into constants module --- client/js/constants.js | 37 +++++++++++++++++++++++++++++++++++++ client/js/lounge.js | 35 ++--------------------------------- 2 files changed, 39 insertions(+), 33 deletions(-) create mode 100644 client/js/constants.js diff --git a/client/js/constants.js b/client/js/constants.js new file mode 100644 index 00000000..546637fd --- /dev/null +++ b/client/js/constants.js @@ -0,0 +1,37 @@ +"use strict"; + +const commands = [ + "/away", + "/back", + "/banlist", + "/close", + "/connect", + "/deop", + "/devoice", + "/disconnect", + "/invite", + "/join", + "/kick", + "/leave", + "/me", + "/mode", + "/msg", + "/nick", + "/notice", + "/op", + "/part", + "/query", + "/quit", + "/raw", + "/say", + "/send", + "/server", + "/slap", + "/topic", + "/voice", + "/whois" +]; + +module.exports = { + commands: commands +}; diff --git a/client/js/lounge.js b/client/js/lounge.js index 0de7a10e..9c8c7b1e 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -15,40 +15,9 @@ const helpers_roundBadgeNumber = require("./libs/handlebars/roundBadgeNumber"); const slideoutMenu = require("./libs/slideout"); const templates = require("../views"); const socket = require("./socket"); +const constants = require("./constants"); $(function() { - var commands = [ - "/away", - "/back", - "/banlist", - "/close", - "/connect", - "/deop", - "/devoice", - "/disconnect", - "/invite", - "/join", - "/kick", - "/leave", - "/me", - "/mode", - "/msg", - "/nick", - "/notice", - "/op", - "/part", - "/query", - "/quit", - "/raw", - "/say", - "/send", - "/server", - "/slap", - "/topic", - "/voice", - "/whois" - ]; - var sidebar = $("#sidebar, #footer"); var chat = $("#chat"); @@ -1422,7 +1391,7 @@ $(function() { } function complete(word) { - var words = commands.slice(); + var words = constants.commands.slice(); var users = chat.find(".active").find(".users"); var nicks = users.data("nicks"); From 184dd177a6b49edc121db46f04cc997807e4f84c Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sun, 23 Apr 2017 02:13:17 +0000 Subject: [PATCH 0326/3926] fix(package): update irc-framework to version 2.7.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9c2edd45..c98022dd 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "express": "4.15.2", "express-handlebars": "3.0.0", "fs-extra": "2.1.2", - "irc-framework": "2.6.1", + "irc-framework": "2.7.0", "ldapjs": "1.0.1", "lodash": "4.17.4", "moment": "2.18.1", From 57acf0f5ce197c9a1150dedfce7d55d285bbf0f1 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sun, 23 Apr 2017 09:42:16 +0300 Subject: [PATCH 0327/3926] Revert "Disable (temporarily) client ping timeouts" This reverts commit ffa0740b50977704cba026869f99cdc72af8e16c. --- src/client.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client.js b/src/client.js index c7c57efb..f9eaa3cd 100644 --- a/src/client.js +++ b/src/client.js @@ -264,7 +264,6 @@ Client.prototype.connect = function(args) { auto_reconnect: true, auto_reconnect_wait: 10000 + Math.floor(Math.random() * 1000), // If multiple users are connected to the same network, randomize their reconnections a little auto_reconnect_max_retries: 360, // At least one hour (plus timeouts) worth of reconnections - ping_interval: 0, // Disable client ping timeouts due to buggy implementation webirc: webirc, }); From 2cfc9119cb8749ec92046b1c5c61e49062f2f3c3 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 22 Apr 2017 00:27:18 +0300 Subject: [PATCH 0328/3926] Use babel-preset-env --- package.json | 2 +- webpack.config.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4783cf1e..254aae0a 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "devDependencies": { "babel-core": "6.24.1", "babel-loader": "7.0.0", - "babel-preset-es2015": "6.24.1", + "babel-preset-env": "1.4.0", "chai": "3.5.0", "eslint": "3.19.0", "font-awesome": "4.7.0", diff --git a/webpack.config.js b/webpack.config.js index 93514dca..b54015cb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -37,7 +37,11 @@ let config = { loader: "babel-loader", options: { presets: [ - "es2015" + ["env", { + targets: { + browsers: "last 2 versions" + } + }] ] } } From 36a7cf4007e4c317538ac92fe68f9ec478bae81a Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sun, 23 Apr 2017 18:13:49 +0000 Subject: [PATCH 0329/3926] fix(package): update irc-framework to version 2.8.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a23c0366..f7739fd8 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "express": "4.15.2", "express-handlebars": "3.0.0", "fs-extra": "2.1.2", - "irc-framework": "2.7.0", + "irc-framework": "2.8.0", "ldapjs": "1.0.1", "lodash": "4.17.4", "moment": "2.18.1", From 6a26014b81004416f32a23ead7d770e96f6622f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Fri, 30 Dec 2016 00:32:27 -0500 Subject: [PATCH 0330/3926] Implement fuzzy-matching for the user list --- client/js/lounge.js | 31 +++++++++++++++++++++---------- package.json | 1 + webpack.config.js | 1 + 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 1857339e..4090b898 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -6,6 +6,7 @@ const $ = require("jquery"); const moment = require("moment"); const Mousetrap = require("mousetrap"); const URI = require("urijs"); +const fuzzy = require("fuzzy"); // our libraries require("./libs/jquery/inputhistory"); @@ -1067,16 +1068,26 @@ $(function() { }); chat.on("input", ".search", function() { - var value = $(this).val().toLowerCase(); - var names = $(this).closest(".users").find(".names"); - names.find(".user").each(function() { - var btn = $(this); - var name = btn.text().toLowerCase().replace(/[+%@~]/, ""); - if (name.indexOf(value) > -1) { - btn.show(); - } else { - btn.hide(); - } + const value = $(this).val().toLowerCase(); + const names = $(this).closest(".users").find(".names"); + + names.find(".user").each((i, el) => { + $(el).text($(el).text().replace(/<\/?b>;/, "")).hide(); + }); + + const fuzzyOptions = { + pre: "", + post: "", + extract: el => $(el).text().toLowerCase().replace(/[+%@~]/, "") + }; + + fuzzy.filter( + value, + names.find(".user").toArray(), + fuzzyOptions + ).forEach(el => { + const firstChar = $(el.original).text()[0].replace(/[^+%@~]/, ""); + $(el.original).html(firstChar + el.string).show(); }); }); diff --git a/package.json b/package.json index a23c0366..d2d88b0d 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "chai": "3.5.0", "eslint": "3.19.0", "font-awesome": "4.7.0", + "fuzzy": "0.1.3", "handlebars": "4.0.6", "handlebars-loader": "1.5.0", "jquery": "3.2.1", diff --git a/webpack.config.js b/webpack.config.js index b54015cb..cf2e9990 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -18,6 +18,7 @@ let config = { "mousetrap", "socket.io-client", "urijs", + "fuzzy", ], }, devtool: "source-map", From cfa9da17a728ea0e2b8799c2cf6df7caca018b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sun, 1 Jan 2017 21:20:48 -0500 Subject: [PATCH 0331/3926] Rely on fuzzy's case insensitivity, do not trim mode --- client/js/lounge.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 4090b898..5fd07abe 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -1068,7 +1068,7 @@ $(function() { }); chat.on("input", ".search", function() { - const value = $(this).val().toLowerCase(); + const value = $(this).val(); const names = $(this).closest(".users").find(".names"); names.find(".user").each((i, el) => { @@ -1078,7 +1078,7 @@ $(function() { const fuzzyOptions = { pre: "", post: "", - extract: el => $(el).text().toLowerCase().replace(/[+%@~]/, "") + extract: el => $(el).text() }; fuzzy.filter( @@ -1086,8 +1086,7 @@ $(function() { names.find(".user").toArray(), fuzzyOptions ).forEach(el => { - const firstChar = $(el.original).text()[0].replace(/[^+%@~]/, ""); - $(el.original).html(firstChar + el.string).show(); + $(el.original).html(el.string).show(); }); }); From b1e9a7ffda5bdca68b29c9229d8f954b2d9b0cb0 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 28 Jan 2017 19:37:26 +0200 Subject: [PATCH 0332/3926] Use separate container for search results --- client/css/style.css | 4 ++++ client/js/lounge.js | 21 +++++++++++++-------- client/views/index.js | 1 + client/views/user.tpl | 3 ++- client/views/user_filtered.tpl | 5 +++++ 5 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 client/views/user_filtered.tpl diff --git a/client/css/style.css b/client/css/style.css index 7fbd6492..1327f271 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1219,6 +1219,10 @@ kbd { content: "Users"; } +#chat .user-mode-search:before { + content: "Search Results"; +} + #loading { font-size: 14px; z-index: 1; diff --git a/client/js/lounge.js b/client/js/lounge.js index 5fd07abe..3d27ec68 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -1069,11 +1069,15 @@ $(function() { chat.on("input", ".search", function() { const value = $(this).val(); - const names = $(this).closest(".users").find(".names"); + const parent = $(this).closest(".users"); + const names = parent.find(".names-original"); + const container = parent.find(".names-filtered"); - names.find(".user").each((i, el) => { - $(el).text($(el).text().replace(/<\/?b>;/, "")).hide(); - }); + if (!value.length) { + container.hide(); + names.show(); + return; + } const fuzzyOptions = { pre: "", @@ -1081,13 +1085,14 @@ $(function() { extract: el => $(el).text() }; - fuzzy.filter( + const result = fuzzy.filter( value, names.find(".user").toArray(), fuzzyOptions - ).forEach(el => { - $(el.original).html(el.string).show(); - }); + ); + + names.hide(); + container.html(templates.user_filtered({matches: result})).show(); }); chat.on("msg", ".messages", function(e, target, msg) { diff --git a/client/views/index.js b/client/views/index.js index fa1916ee..201690ee 100644 --- a/client/views/index.js +++ b/client/views/index.js @@ -30,4 +30,5 @@ module.exports = { toggle: require("./toggle.tpl"), unread_marker: require("./unread_marker.tpl"), user: require("./user.tpl"), + user_filtered: require("./user_filtered.tpl"), }; diff --git a/client/views/user.tpl b/client/views/user.tpl index 23a3a9f0..29a6872c 100644 --- a/client/views/user.tpl +++ b/client/views/user.tpl @@ -2,8 +2,9 @@
+
{{/if}} -
+
{{#diff "reset"}}{{/diff}} {{#each users}} {{#diff mode}} diff --git a/client/views/user_filtered.tpl b/client/views/user_filtered.tpl new file mode 100644 index 00000000..1b86f99d --- /dev/null +++ b/client/views/user_filtered.tpl @@ -0,0 +1,5 @@ + From c583d6edf9d01276e6be03e372146bd44dae452c Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 22 Apr 2017 15:49:01 +0300 Subject: [PATCH 0333/3926] Correctly update user list and search filtering on user updates --- client/css/style.css | 19 +++++++------------ client/js/libs/handlebars/users.js | 5 ----- client/js/lounge.js | 19 +++++++++++++++++-- client/views/chat.tpl | 10 +++++++++- client/views/user.tpl | 28 ++++++++++------------------ 5 files changed, 43 insertions(+), 38 deletions(-) delete mode 100644 client/js/libs/handlebars/users.js diff --git a/client/css/style.css b/client/css/style.css index 1327f271..89b01c94 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -768,6 +768,9 @@ kbd { overflow: auto; -webkit-overflow-scrolling: touch; position: absolute; +} + +#chat .channel .chat { right: 180px; } @@ -791,18 +794,6 @@ kbd { transform: translateZ(0); } -#chat .lobby .chat, -#chat .special .chat, -#chat .query .chat { - right: 0; -} - -#chat .lobby .sidebar, -#chat .special .sidebar, -#chat .query .sidebar { - display: none; -} - #chat .show-more { display: none; padding: 10px; @@ -1180,6 +1171,10 @@ kbd { width: 100%; } +#chat .names-filtered { + display: none; +} + #chat .names .user { display: block; line-height: 1.6; diff --git a/client/js/libs/handlebars/users.js b/client/js/libs/handlebars/users.js deleted file mode 100644 index d962423c..00000000 --- a/client/js/libs/handlebars/users.js +++ /dev/null @@ -1,5 +0,0 @@ -"use strict"; - -module.exports = function(count) { - return count + " " + (count === 1 ? "user" : "users"); -}; diff --git a/client/js/lounge.js b/client/js/lounge.js index 3d27ec68..a0e52e60 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -258,7 +258,10 @@ $(function() { function renderChannel(data) { renderChannelMessages(data); - renderChannelUsers(data); + + if (data.type === "channel") { + renderChannelUsers(data); + } } function renderChannelMessages(data) { @@ -318,7 +321,19 @@ $(function() { return (oldSortOrder[a] || Number.MAX_VALUE) - (oldSortOrder[b] || Number.MAX_VALUE); }); - users.html(templates.user(data)).data("nicks", nicks); + const search = users + .find(".search") + .attr("placeholder", nicks.length + " " + (nicks.length === 1 ? "user" : "users")); + + users + .find(".names-original") + .html(templates.user(data)) + .data("nicks", nicks); + + // Refresh user search + if (search.val().length) { + search.trigger("input"); + } } function renderNetworks(data) { diff --git a/client/views/chat.tpl b/client/views/chat.tpl index 9ccb6391..898dd75b 100644 --- a/client/views/chat.tpl +++ b/client/views/chat.tpl @@ -19,8 +19,16 @@
+ {{#equal type "channel"}} + {{/equal}}
{{/each}} diff --git a/client/views/user.tpl b/client/views/user.tpl index 29a6872c..b0af2a0c 100644 --- a/client/views/user.tpl +++ b/client/views/user.tpl @@ -1,19 +1,11 @@ -{{#if users.length}} -
- -
-
-{{/if}} -
- {{#diff "reset"}}{{/diff}} - {{#each users}} - {{#diff mode}} - {{#unless @first}} -
- {{/unless}} -
- {{/diff}} - {{mode}}{{name}} - {{/each}} -
+{{#diff "reset"}}{{/diff}} +{{#each users}} + {{#diff mode}} + {{#unless @first}} +
+ {{/unless}} +
+ {{/diff}} + {{mode}}{{name}} +{{/each}}
From 326f1ac47633c71452752f53850a18630411eadb Mon Sep 17 00:00:00 2001 From: Alistair McKinlay Date: Sat, 22 Apr 2017 14:03:00 +0100 Subject: [PATCH 0334/3926] Create options module --- client/js/localStorage.js | 19 +++++ client/js/lounge.js | 149 +++----------------------------------- client/js/options.js | 124 +++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 137 deletions(-) create mode 100644 client/js/localStorage.js create mode 100644 client/js/options.js diff --git a/client/js/localStorage.js b/client/js/localStorage.js new file mode 100644 index 00000000..3b08b3bd --- /dev/null +++ b/client/js/localStorage.js @@ -0,0 +1,19 @@ +"use strict"; + +module.exports = { + set: function(key, value) { + try { + window.localStorage.setItem(key, value); + } catch (e) { + // Do nothing. If we end up here, web storage quota exceeded, or user is + // in Safari's private browsing where localStorage's setItem is not + // available. See http://stackoverflow.com/q/14555347/1935861. + } + }, + get: function(key) { + return window.localStorage.getItem(key); + }, + remove: function(key, value) { + window.localStorage.removeItem(key, value); + } +}; diff --git a/client/js/lounge.js b/client/js/lounge.js index 1857339e..40a52b34 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -17,6 +17,7 @@ const slideoutMenu = require("./libs/slideout"); const templates = require("../views"); const socket = require("./socket"); const constants = require("./constants"); +const storage = require("./localStorage"); $(function() { var sidebar = $("#sidebar, #footer"); @@ -40,15 +41,6 @@ $(function() { var favicon = $("#favicon"); - function setLocalStorageItem(key, value) { - try { - window.localStorage.setItem(key, value); - } catch (e) { - // Do nothing. If we end up here, web storage quota exceeded, or user is - // in Safari's private browsing where localStorage's setItem is not - // available. See http://stackoverflow.com/q/14555347/1935861. - } - } socket.on("auth", function(data) { var login = $("#sign-in"); var token; @@ -56,14 +48,14 @@ $(function() { login.find(".btn").prop("disabled", false); if (!data.success) { - window.localStorage.removeItem("token"); + storage.remove("token"); var error = login.find(".error"); error.show().closest("form").one("submit", function() { error.hide(); }); } else { - token = window.localStorage.getItem("token"); + token = storage.get("token"); if (token) { $("#loading-page-message").text("Authorizing…"); socket.emit("auth", {token: token}); @@ -72,7 +64,7 @@ $(function() { var input = login.find("input[name='user']"); if (input.val() === "") { - input.val(window.localStorage.getItem("user") || ""); + input.val(storage.get("user") || ""); } if (token) { return; @@ -106,8 +98,8 @@ $(function() { }); } - if (data.token && window.localStorage.getItem("token") !== null) { - setLocalStorageItem("token", data.token); + if (data.token && storage.get("token") !== null) { + storage.set("token", data.token); } passwordForm @@ -130,9 +122,9 @@ $(function() { } if (data.token && $("#sign-in-remember").is(":checked")) { - setLocalStorageItem("token", data.token); + storage.set("token", data.token); } else { - window.localStorage.removeItem("token"); + storage.remove("token"); } $("body").removeClass("signed-out"); @@ -198,7 +190,7 @@ $(function() { var chan = chat.find(target); var template = "msg"; - if (!data.msg.highlight && !data.msg.self && (type === "message" || type === "notice") && highlights.some(function(h) { + if (!data.msg.highlight && !data.msg.self && (type === "message" || type === "notice") && options.highlights.some(function(h) { return data.msg.text.toLocaleLowerCase().indexOf(h.toLocaleLowerCase()) > -1; })) { data.msg.highlight = true; @@ -527,127 +519,10 @@ $(function() { socket.on("names", renderChannelUsers); - var userStyles = $("#user-specified-css"); - var highlights = []; - var options = $.extend({ - coloredNicks: true, - desktopNotifications: false, - join: true, - links: true, - mode: true, - motd: true, - nick: true, - notification: true, - notifyAllMessages: false, - part: true, - quit: true, - theme: $("#theme").attr("href").replace(/^themes\/(.*).css$/, "$1"), // Extracts default theme name, set on the server configuration - thumbnails: true, - userStyles: userStyles.text(), - }, JSON.parse(window.localStorage.getItem("settings"))); + var options = require("./options"); var windows = $("#windows"); - (function SettingsScope() { - var settings = $("#settings"); - - for (var i in options) { - if (i === "userStyles") { - if (!/[?&]nocss/.test(window.location.search)) { - $(document.head).find("#user-specified-css").html(options[i]); - } - settings.find("#user-specified-css-input").val(options[i]); - } else if (i === "highlights") { - settings.find("input[name=" + i + "]").val(options[i]); - } else if (i === "theme") { - $("#theme").attr("href", "themes/" + options[i] + ".css"); - settings.find("select[name=" + i + "]").val(options[i]); - } else if (options[i]) { - settings.find("input[name=" + i + "]").prop("checked", true); - } - } - - settings.on("change", "input, select, textarea", function() { - var self = $(this); - var name = self.attr("name"); - - if (self.attr("type") === "checkbox") { - options[name] = self.prop("checked"); - } else { - options[name] = self.val(); - } - - setLocalStorageItem("settings", JSON.stringify(options)); - - if ([ - "join", - "mode", - "motd", - "nick", - "part", - "quit", - "notifyAllMessages", - ].indexOf(name) !== -1) { - chat.toggleClass("hide-" + name, !self.prop("checked")); - } else if (name === "coloredNicks") { - chat.toggleClass("colored-nicks", self.prop("checked")); - } else if (name === "theme") { - $("#theme").attr("href", "themes/" + options[name] + ".css"); - } else if (name === "userStyles") { - userStyles.html(options[name]); - } else if (name === "highlights") { - var highlightString = options[name]; - highlights = highlightString.split(",").map(function(h) { - return h.trim(); - }).filter(function(h) { - // Ensure we don't have empty string in the list of highlights - // otherwise, users get notifications for everything - return h !== ""; - }); - } - }).find("input") - .trigger("change"); - - $("#desktopNotifications").on("change", function() { - if ($(this).prop("checked") && Notification.permission !== "granted") { - Notification.requestPermission(updateDesktopNotificationStatus); - } - }); - - // Updates the checkbox and warning in settings when the Settings page is - // opened or when the checkbox state is changed. - // When notifications are not supported, this is never called (because - // checkbox state can not be changed). - var updateDesktopNotificationStatus = function() { - if (Notification.permission === "denied") { - desktopNotificationsCheckbox.attr("disabled", true); - desktopNotificationsCheckbox.attr("checked", false); - warningBlocked.show(); - } else { - if (Notification.permission === "default" && desktopNotificationsCheckbox.prop("checked")) { - desktopNotificationsCheckbox.attr("checked", false); - } - desktopNotificationsCheckbox.attr("disabled", false); - warningBlocked.hide(); - } - }; - - // If browser does not support notifications, override existing settings and - // display proper message in settings. - var desktopNotificationsCheckbox = $("#desktopNotifications"); - var warningUnsupported = $("#warnUnsupportedDesktopNotifications"); - var warningBlocked = $("#warnBlockedDesktopNotifications"); - warningBlocked.hide(); - if (("Notification" in window)) { - warningUnsupported.hide(); - windows.on("show", "#settings", updateDesktopNotificationStatus); - } else { - options.desktopNotifications = false; - desktopNotificationsCheckbox.attr("disabled", true); - desktopNotificationsCheckbox.attr("checked", false); - } - }()); - var viewport = $("#viewport"); var sidebarSlide = slideoutMenu(viewport[0], sidebar[0]); var contextMenuContainer = $("#context-menu-container"); @@ -1027,7 +902,7 @@ $(function() { }); sidebar.on("click", "#sign-out", function() { - window.localStorage.removeItem("token"); + storage.remove("token"); location.reload(); }); @@ -1236,7 +1111,7 @@ $(function() { } }); if (values.user) { - setLocalStorageItem("user", values.user); + storage.set("user", values.user); } socket.emit( event, values diff --git a/client/js/options.js b/client/js/options.js new file mode 100644 index 00000000..24265a56 --- /dev/null +++ b/client/js/options.js @@ -0,0 +1,124 @@ +"use strict"; +const $ = require("jquery"); +const settings = $("#settings"); +const userStyles = $("#user-specified-css"); +const storage = require("./localStorage"); + +const windows = $("#windows"); +const chat = $("#chat"); + +const options = $.extend({ + coloredNicks: true, + desktopNotifications: false, + join: true, + links: true, + mode: true, + motd: false, + nick: true, + notification: true, + notifyAllMessages: false, + part: true, + quit: true, + theme: $("#theme").attr("href").replace(/^themes\/(.*).css$/, "$1"), // Extracts default theme name, set on the server configuration + thumbnails: true, + userStyles: userStyles.text(), + highlights: [] +}, JSON.parse(storage.get("settings"))); + +module.exports = options; + +for (var i in options) { + if (i === "userStyles") { + if (!/[?&]nocss/.test(window.location.search)) { + $(document.head).find("#user-specified-css").html(options[i]); + } + settings.find("#user-specified-css-input").val(options[i]); + } else if (i === "highlights") { + settings.find("input[name=" + i + "]").val(options[i]); + } else if (i === "theme") { + $("#theme").attr("href", "themes/" + options[i] + ".css"); + settings.find("select[name=" + i + "]").val(options[i]); + } else if (options[i]) { + settings.find("input[name=" + i + "]").prop("checked", true); + } +} + +settings.on("change", "input, select, textarea", function() { + var self = $(this); + var name = self.attr("name"); + + if (self.attr("type") === "checkbox") { + options[name] = self.prop("checked"); + } else { + options[name] = self.val(); + } + + storage.set("settings", JSON.stringify(options)); + + if ([ + "join", + "mode", + "motd", + "nick", + "part", + "quit", + "notifyAllMessages", + ].indexOf(name) !== -1) { + chat.toggleClass("hide-" + name, !self.prop("checked")); + } else if (name === "coloredNicks") { + chat.toggleClass("colored-nicks", self.prop("checked")); + } else if (name === "theme") { + $("#theme").attr("href", "themes/" + options[name] + ".css"); + } else if (name === "userStyles") { + userStyles.html(options[name]); + } else if (name === "highlights") { + var highlightString = options[name]; + options.highlights = highlightString.split(",").map(function(h) { + return h.trim(); + }).filter(function(h) { + // Ensure we don't have empty string in the list of highlights + // otherwise, users get notifications for everything + return h !== ""; + }); + } +}).find("input") + .trigger("change"); + +$("#desktopNotifications").on("change", function() { + if ($(this).prop("checked") && Notification.permission !== "granted") { + Notification.requestPermission(updateDesktopNotificationStatus); + } +}); + +// Updates the checkbox and warning in settings when the Settings page is +// opened or when the checkbox state is changed. +// When notifications are not supported, this is never called (because +// checkbox state can not be changed). +var updateDesktopNotificationStatus = function() { + if (Notification.permission === "denied") { + desktopNotificationsCheckbox.attr("disabled", true); + desktopNotificationsCheckbox.attr("checked", false); + warningBlocked.show(); + } else { + if (Notification.permission === "default" && desktopNotificationsCheckbox.prop("checked")) { + desktopNotificationsCheckbox.attr("checked", false); + } + desktopNotificationsCheckbox.attr("disabled", false); + warningBlocked.hide(); + } +}; + +// If browser does not support notifications, override existing settings and +// display proper message in settings. +var desktopNotificationsCheckbox = $("#desktopNotifications"); +var warningUnsupported = $("#warnUnsupportedDesktopNotifications"); +var warningBlocked = $("#warnBlockedDesktopNotifications"); +warningBlocked.hide(); +if (("Notification" in window)) { + warningUnsupported.hide(); + windows.on("show", "#settings", updateDesktopNotificationStatus); +} else { + options.desktopNotifications = false; + desktopNotificationsCheckbox.attr("disabled", true); + desktopNotificationsCheckbox.attr("checked", false); +} From 413ab234d698c28e3ae8704bb3f9e7049152a1db Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 24 Apr 2017 09:45:02 +0000 Subject: [PATCH 0335/3926] chore(package): update mocha to version 3.3.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a23c0366..4dcc5edd 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "handlebars-loader": "1.5.0", "jquery": "3.2.1", "jquery-ui": "1.12.1", - "mocha": "3.2.0", + "mocha": "3.3.0", "mousetrap": "1.6.1", "npm-run-all": "4.0.2", "nyc": "10.2.0", From fe07bf66373d39ef5f3a4f616a04fa33ffc78ce6 Mon Sep 17 00:00:00 2001 From: Alistair McKinlay Date: Mon, 24 Apr 2017 12:01:24 +0100 Subject: [PATCH 0336/3926] Add fix for slow scrolling when holding pg-up/pg-dn Fixes #1022 --- client/js/lounge.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/js/lounge.js b/client/js/lounge.js index 1857339e..1043465f 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -1271,6 +1271,9 @@ $(function() { "pagedown" ], function(e, key) { let container = windows.find(".window.active"); + if (container.is(":animated")) { + return; + } // Chat windows scroll message container if (container.attr("id") === "chat-container") { From e45cfbf02ca6f30750e5b268eb241593ffec6fbd Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Wed, 19 Apr 2017 12:27:26 -0700 Subject: [PATCH 0337/3926] Use irc-framework setTopic() for topic command --- src/plugins/inputs/topic.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/plugins/inputs/topic.js b/src/plugins/inputs/topic.js index 8e2327cc..ef7a7cb4 100644 --- a/src/plugins/inputs/topic.js +++ b/src/plugins/inputs/topic.js @@ -14,9 +14,6 @@ exports.input = function(network, chan, cmd, args) { return; } - - var irc = network.irc; - irc.raw("TOPIC", chan.name, args.join(" ")); - + network.irc.setTopic(chan.name, args.join(" ")); return true; }; From b69ba5e4b11233977a8e48c1f21060c9a8e5a92b Mon Sep 17 00:00:00 2001 From: Alistair McKinlay Date: Tue, 25 Apr 2017 10:26:52 +0100 Subject: [PATCH 0338/3926] Fix showing prefetch options --- client/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/index.html b/client/index.html index 81f63d04..362688ba 100644 --- a/client/index.html +++ b/client/index.html @@ -274,7 +274,7 @@ {{/each}}
- {{#unless prefetch}} + {{#if prefetch}}

Links and URLs

@@ -290,7 +290,7 @@ Auto-expand links
- {{/unless}} + {{/if}}

Notifications

From d0987719ce76ac8d8174d81cc93463aee7a6f394 Mon Sep 17 00:00:00 2001 From: Alistair McKinlay Date: Mon, 24 Apr 2017 12:20:48 +0100 Subject: [PATCH 0339/3926] Replace the state on init rather than adding a new entry Fixes #1042 --- client/js/lounge.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 40a52b34..e9b78f2f 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -132,7 +132,9 @@ $(function() { $("#sign-in").remove(); var id = data.active; - var target = sidebar.find("[data-id='" + id + "']").trigger("click"); + var target = sidebar.find("[data-id='" + id + "']").trigger("click", { + replaceHistory: true + }); if (target.length === 0) { var first = sidebar.find(".chan") .eq(0) @@ -820,7 +822,11 @@ $(function() { } if (history && history.pushState) { - history.pushState(state, null, null); + if (data && data.replaceHistory && history.replaceState) { + history.replaceState(state, null, null); + } else { + history.pushState(state, null, null); + } } }); From b03d01b6eb83fe7f5519706f0ad4c428a39672bb Mon Sep 17 00:00:00 2001 From: Alistair McKinlay Date: Mon, 24 Apr 2017 11:40:53 +0100 Subject: [PATCH 0340/3926] Add ban/unban command Fixes #1073 --- client/index.html | 20 ++++++++++++++ client/js/constants.js | 2 ++ src/client.js | 1 + src/plugins/inputs/ban.js | 44 +++++++++++++++++++++++++++++++ src/plugins/inputs/mode.js | 11 +------- src/plugins/irc-events/banlist.js | 2 +- 6 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 src/plugins/inputs/ban.js diff --git a/client/index.html b/client/index.html index 362688ba..1b40bfce 100644 --- a/client/index.html +++ b/client/index.html @@ -554,6 +554,16 @@ +
+
+ /ban nick +
+
+

Ban (+b) a user from the current channel. + This can be a nickname or a hostmask.

+
+
+
/banlist @@ -799,6 +809,16 @@
+
+
+ /unban nick +
+
+

Unban (-b) a user from the current channel.

+ This can be a nickname or a hostmask.

+
+
+
/voice nick [...nick] diff --git a/client/js/constants.js b/client/js/constants.js index 546637fd..c9587709 100644 --- a/client/js/constants.js +++ b/client/js/constants.js @@ -3,6 +3,7 @@ const commands = [ "/away", "/back", + "/ban", "/banlist", "/close", "/connect", @@ -28,6 +29,7 @@ const commands = [ "/server", "/slap", "/topic", + "/unban", "/voice", "/whois" ]; diff --git a/src/client.js b/src/client.js index 0d3caffa..aa3b7045 100644 --- a/src/client.js +++ b/src/client.js @@ -36,6 +36,7 @@ var events = [ "whois" ]; var inputs = [ + "ban", "ctcp", "msg", "part", diff --git a/src/plugins/inputs/ban.js b/src/plugins/inputs/ban.js new file mode 100644 index 00000000..b6745652 --- /dev/null +++ b/src/plugins/inputs/ban.js @@ -0,0 +1,44 @@ +"use strict"; + +var Chan = require("../../models/chan"); +var Msg = require("../../models/msg"); + +exports.commands = [ + "ban", + "unban", + "banlist" +]; + +exports.input = function(network, chan, cmd, args) { + if (chan.type !== Chan.Type.CHANNEL) { + chan.pushMessage(this, new Msg({ + type: Msg.Type.ERROR, + text: `${cmd} command can only be used in channels.` + })); + + return; + } + + if (cmd !== "banlist" && args.length === 0) { + if (args.length === 0) { + chan.pushMessage(this, new Msg({ + type: Msg.Type.ERROR, + text: `Usage: /${cmd} ` + })); + + return; + } + } + + switch (cmd) { + case "ban": + network.irc.ban(chan.name, args[0]); + break; + case "unban": + network.irc.unban(chan.name, args[0]); + break; + case "banlist": + network.irc.banlist(chan.name); + break; + } +}; diff --git a/src/plugins/inputs/mode.js b/src/plugins/inputs/mode.js index 8395da15..4e1227ad 100644 --- a/src/plugins/inputs/mode.js +++ b/src/plugins/inputs/mode.js @@ -4,7 +4,6 @@ var Chan = require("../../models/chan"); var Msg = require("../../models/msg"); exports.commands = [ - "banlist", "mode", "op", "deop", @@ -14,10 +13,6 @@ exports.commands = [ "devoice", ]; -const chanCommands = [ - "banlist" -]; - exports.input = function(network, chan, cmd, args) { if (cmd !== "mode") { if (chan.type !== Chan.Type.CHANNEL) { @@ -29,7 +24,7 @@ exports.input = function(network, chan, cmd, args) { return; } - if (args.length === 0 && chanCommands.indexOf(cmd) === -1) { + if (args.length === 0) { chan.pushMessage(this, new Msg({ type: Msg.Type.ERROR, text: `Usage: /${cmd} [...nick]` @@ -39,7 +34,6 @@ exports.input = function(network, chan, cmd, args) { } const mode = { - banlist: "+b", op: "+o", hop: "+h", voice: "+v", @@ -48,9 +42,6 @@ exports.input = function(network, chan, cmd, args) { devoice: "-v" }[cmd]; - if (chanCommands.indexOf(cmd) > -1 && args.length === 0) { - network.irc.raw("MODE", chan.name, mode); - } args.forEach(function(target) { network.irc.raw("MODE", chan.name, mode, target); }); diff --git a/src/plugins/irc-events/banlist.js b/src/plugins/irc-events/banlist.js index d45d81cc..fa5993ea 100644 --- a/src/plugins/irc-events/banlist.js +++ b/src/plugins/irc-events/banlist.js @@ -9,7 +9,7 @@ module.exports = function(irc, network) { irc.on("banlist", function(banlist) { const channel = banlist.channel; const bans = banlist.bans; - if (!bans) { + if (!bans || bans.length === 0) { const msg = new Msg({ time: Date.now(), type: Msg.Type.ERROR, From 785842cde558db47a36aefd2753dbff2ceb21634 Mon Sep 17 00:00:00 2001 From: Yash Srivastav Date: Sun, 11 Dec 2016 05:43:26 +0530 Subject: [PATCH 0341/3926] Add emoji/nick/commands/chan autocomplete --- client/css/style.css | 33 +- client/js/libs/jquery/inputhistory.js | 2 +- client/js/libs/simplemap.json | 1434 +++++++++++++++++++++++++ client/js/lounge.js | 118 +- package.json | 2 + webpack.config.js | 5 + 6 files changed, 1582 insertions(+), 12 deletions(-) create mode 100644 client/js/libs/simplemap.json diff --git a/client/css/style.css b/client/css/style.css index 7fbd6492..522b319d 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1486,7 +1486,8 @@ kbd { background: transparent; } -#context-menu { +#context-menu, +.textcomplete-menu { position: absolute; list-style: none; margin: 0; @@ -1505,7 +1506,8 @@ kbd { background-color: rgba(0, 0, 0, .1); } -.context-menu-item { +.context-menu-item, +.textcomplete-item { cursor: pointer; display: block; padding: 4px 8px; @@ -1514,15 +1516,38 @@ kbd { margin-bottom: 6px; } -.context-menu-item:hover { +.context-menu-item:hover, +.textcomplete-item:hover, +.textcomplete-menu .active { background-color: #f6f6f6; } -.context-menu-item:before { +.context-menu-item:before, +.textcomplete-item:before { width: 20px; display: inline-block; } +.textcomplete-item { + border-bottom-color: rgba(0, 0, 0, .1); + border-bottom-style: solid; + border-bottom-width: 1px; + margin-top: 0; + margin-bottom: 0; + padding: 10px 8px; +} + +.textcomplete-item a { + color: #333; +} + +.textcomplete-item .emoji { + margin-right: 8px; + width: 16px; + text-align: center; + display: inline-block; +} + /** * Tooltips v0.5.3 * See http://primercss.io/tooltips/ diff --git a/client/js/libs/jquery/inputhistory.js b/client/js/libs/jquery/inputhistory.js index 1f5b7bf8..778fa696 100644 --- a/client/js/libs/jquery/inputhistory.js +++ b/client/js/libs/jquery/inputhistory.js @@ -34,7 +34,7 @@ import jQuery from "jquery"; var key = e.which; switch (key) { case 13: // Enter - if (e.shiftKey) { + if (e.shiftKey || self.data("autocompleting")) { return; // multiline input } diff --git a/client/js/libs/simplemap.json b/client/js/libs/simplemap.json new file mode 100644 index 00000000..2f3f0567 --- /dev/null +++ b/client/js/libs/simplemap.json @@ -0,0 +1,1434 @@ +{ + "100": "💯", + "1234": "🔢", + "grinning": "😀", + "grimacing": "😬", + "grin": "😁", + "joy": "😂", + "rofl": "🤣", + "smiley": "😃", + "smile": "😄", + "sweat_smile": "😅", + "laughing": "😆", + "innocent": "😇", + "wink": "😉", + "blush": "😊", + "slightly_smiling_face": "🙂", + "upside_down_face": "🙃", + "relaxed": "☺️", + "yum": "😋", + "relieved": "😌", + "heart_eyes": "😍", + "kissing_heart": "😘", + "kissing": "😗", + "kissing_smiling_eyes": "😙", + "kissing_closed_eyes": "😚", + "stuck_out_tongue_winking_eye": "😜", + "stuck_out_tongue_closed_eyes": "😝", + "stuck_out_tongue": "😛", + "money_mouth_face": "🤑", + "nerd_face": "🤓", + "sunglasses": "😎", + "clown_face": "🤡", + "cowboy_hat_face": "🤠", + "hugs": "🤗", + "smirk": "😏", + "no_mouth": "😶", + "neutral_face": "😐", + "expressionless": "😑", + "unamused": "😒", + "roll_eyes": "🙄", + "thinking": "🤔", + "lying_face": "🤥", + "flushed": "😳", + "disappointed": "😞", + "worried": "😟", + "angry": "😠", + "rage": "😡", + "pensive": "😔", + "confused": "😕", + "slightly_frowning_face": "🙁", + "frowning_face": "☹", + "persevere": "😣", + "confounded": "😖", + "tired_face": "😫", + "weary": "😩", + "triumph": "😤", + "open_mouth": "😮", + "scream": "😱", + "fearful": "😨", + "cold_sweat": "😰", + "hushed": "😯", + "frowning": "😦", + "anguished": "😧", + "cry": "😢", + "disappointed_relieved": "😥", + "drooling_face": "🤤", + "sleepy": "😪", + "sweat": "😓", + "sob": "😭", + "dizzy_face": "😵", + "astonished": "😲", + "zipper_mouth_face": "🤐", + "nauseated_face": "🤢", + "sneezing_face": "🤧", + "mask": "😷", + "face_with_thermometer": "🤒", + "face_with_head_bandage": "🤕", + "sleeping": "😴", + "zzz": "💤", + "poop": "💩", + "smiling_imp": "😈", + "imp": "👿", + "japanese_ogre": "👹", + "japanese_goblin": "👺", + "skull": "💀", + "ghost": "👻", + "alien": "👽", + "robot": "🤖", + "smiley_cat": "😺", + "smile_cat": "😸", + "joy_cat": "😹", + "heart_eyes_cat": "😻", + "smirk_cat": "😼", + "kissing_cat": "😽", + "scream_cat": "🙀", + "crying_cat_face": "😿", + "pouting_cat": "😾", + "raised_hands": "🙌", + "clap": "👏", + "wave": "👋", + "call_me_hand": "🤙", + "+1": "👍", + "-1": "👎", + "facepunch": "👊", + "fist": "✊", + "fist_left": "🤛", + "fist_right": "🤜", + "v": "✌", + "ok_hand": "👌", + "raised_hand": "✋", + "raised_back_of_hand": "🤚", + "open_hands": "👐", + "muscle": "💪", + "pray": "🙏", + "handshake": "🤝", + "point_up": "☝", + "point_up_2": "👆", + "point_down": "👇", + "point_left": "👈", + "point_right": "👉", + "fu": "🖕", + "raised_hand_with_fingers_splayed": "🖐", + "metal": "🤘", + "crossed_fingers": "🤞", + "vulcan_salute": "🖖", + "writing_hand": "✍", + "selfie": "🤳", + "nail_care": "💅", + "lips": "👄", + "tongue": "👅", + "ear": "👂", + "nose": "👃", + "eye": "👁", + "eyes": "👀", + "bust_in_silhouette": "👤", + "busts_in_silhouette": "👥", + "speaking_head": "🗣", + "baby": "👶", + "boy": "👦", + "girl": "👧", + "man": "👨", + "woman": "👩", + "blonde_woman": "👱‍♀️", + "blonde_man": "👱", + "older_man": "👴", + "older_woman": "👵", + "man_with_gua_pi_mao": "👲", + "woman_with_turban": "👳‍♀️", + "man_with_turban": "👳", + "policewoman": "👮‍♀️", + "policeman": "👮", + "construction_worker_woman": "👷‍♀️", + "construction_worker_man": "👷", + "guardswoman": "💂‍♀️", + "guardsman": "💂", + "female_detective": "🕵️‍♀️", + "male_detective": "🕵", + "woman_health_worker": "👩‍⚕️", + "man_health_worker": "👨‍⚕️", + "woman_farmer": "👩‍🌾", + "man_farmer": "👨‍🌾", + "woman_cook": "👩‍🍳", + "man_cook": "👨‍🍳", + "woman_student": "👩‍🎓", + "man_student": "👨‍🎓", + "woman_singer": "👩‍🎤", + "man_singer": "👨‍🎤", + "woman_teacher": "👩‍🏫", + "man_teacher": "👨‍🏫", + "woman_factory_worker": "👩‍🏭", + "man_factory_worker": "👨‍🏭", + "woman_technologist": "👩‍💻", + "man_technologist": "👨‍💻", + "woman_office_worker": "👩‍💼", + "man_office_worker": "👨‍💼", + "woman_mechanic": "👩‍🔧", + "man_mechanic": "👨‍🔧", + "woman_scientist": "👩‍🔬", + "man_scientist": "👨‍🔬", + "woman_artist": "👩‍🎨", + "man_artist": "👨‍🎨", + "woman_firefighter": "👩‍🚒", + "man_firefighter": "👨‍🚒", + "woman_pilot": "👩‍✈️", + "man_pilot": "👨‍✈️", + "woman_astronaut": "👩‍🚀", + "man_astronaut": "👨‍🚀", + "woman_judge": "👩‍⚖️", + "man_judge": "👨‍⚖️", + "mrs_claus": "🤶", + "santa": "🎅", + "angel": "👼", + "pregnant_woman": "🤰", + "princess": "👸", + "prince": "🤴", + "bride_with_veil": "👰", + "man_in_tuxedo": "🤵", + "running_woman": "🏃‍♀️", + "running_man": "🏃", + "walking_woman": "🚶‍♀️", + "walking_man": "🚶", + "dancer": "💃", + "man_dancing": "🕺", + "dancing_women": "👯", + "dancing_men": "👯‍♂️", + "couple": "👫", + "two_men_holding_hands": "👬", + "two_women_holding_hands": "👭", + "bowing_woman": "🙇‍♀️", + "bowing_man": "🙇", + "man_facepalming": "🤦", + "woman_facepalming": "🤦‍♀️", + "woman_shrugging": "🤷", + "man_shrugging": "🤷‍♂️", + "tipping_hand_woman": "💁", + "tipping_hand_man": "💁‍♂️", + "no_good_woman": "🙅", + "no_good_man": "🙅‍♂️", + "ok_woman": "🙆", + "ok_man": "🙆‍♂️", + "raising_hand_woman": "🙋", + "raising_hand_man": "🙋‍♂️", + "pouting_woman": "🙎", + "pouting_man": "🙎‍♂️", + "frowning_woman": "🙍", + "frowning_man": "🙍‍♂️", + "haircut_woman": "💇", + "haircut_man": "💇‍♂️", + "massage_woman": "💆", + "massage_man": "💆‍♂️", + "couple_with_heart_woman_man": "💑", + "couple_with_heart_woman_woman": "👩‍❤️‍👩", + "couple_with_heart_man_man": "👨‍❤️‍👨", + "couplekiss_man_woman": "💏", + "couplekiss_woman_woman": "👩‍❤️‍💋‍👩", + "couplekiss_man_man": "👨‍❤️‍💋‍👨", + "family_man_woman_boy": "👪", + "family_man_woman_girl": "👨‍👩‍👧", + "family_man_woman_girl_boy": "👨‍👩‍👧‍👦", + "family_man_woman_boy_boy": "👨‍👩‍👦‍👦", + "family_man_woman_girl_girl": "👨‍👩‍👧‍👧", + "family_woman_woman_boy": "👩‍👩‍👦", + "family_woman_woman_girl": "👩‍👩‍👧", + "family_woman_woman_girl_boy": "👩‍👩‍👧‍👦", + "family_woman_woman_boy_boy": "👩‍👩‍👦‍👦", + "family_woman_woman_girl_girl": "👩‍👩‍👧‍👧", + "family_man_man_boy": "👨‍👨‍👦", + "family_man_man_girl": "👨‍👨‍👧", + "family_man_man_girl_boy": "👨‍👨‍👧‍👦", + "family_man_man_boy_boy": "👨‍👨‍👦‍👦", + "family_man_man_girl_girl": "👨‍👨‍👧‍👧", + "family_woman_boy": "👩‍👦", + "family_woman_girl": "👩‍👧", + "family_woman_girl_boy": "👩‍👧‍👦", + "family_woman_boy_boy": "👩‍👦‍👦", + "family_woman_girl_girl": "👩‍👧‍👧", + "family_man_boy": "👨‍👦", + "family_man_girl": "👨‍👧", + "family_man_girl_boy": "👨‍👧‍👦", + "family_man_boy_boy": "👨‍👦‍👦", + "family_man_girl_girl": "👨‍👧‍👧", + "womans_clothes": "👚", + "tshirt": "👕", + "jeans": "👖", + "necktie": "👔", + "dress": "👗", + "bikini": "👙", + "kimono": "👘", + "lipstick": "💄", + "kiss": "💋", + "footprints": "👣", + "high_heel": "👠", + "sandal": "👡", + "boot": "👢", + "mans_shoe": "👞", + "athletic_shoe": "👟", + "womans_hat": "👒", + "tophat": "🎩", + "rescue_worker_helmet": "⛑", + "mortar_board": "🎓", + "crown": "👑", + "school_satchel": "🎒", + "pouch": "👝", + "purse": "👛", + "handbag": "👜", + "briefcase": "💼", + "eyeglasses": "👓", + "dark_sunglasses": "🕶", + "ring": "💍", + "closed_umbrella": "🌂", + "dog": "🐶", + "cat": "🐱", + "mouse": "🐭", + "hamster": "🐹", + "rabbit": "🐰", + "fox_face": "🦊", + "bear": "🐻", + "panda_face": "🐼", + "koala": "🐨", + "tiger": "🐯", + "lion": "🦁", + "cow": "🐮", + "pig": "🐷", + "pig_nose": "🐽", + "frog": "🐸", + "squid": "🦑", + "octopus": "🐙", + "shrimp": "🦐", + "monkey_face": "🐵", + "gorilla": "🦍", + "see_no_evil": "🙈", + "hear_no_evil": "🙉", + "speak_no_evil": "🙊", + "monkey": "🐒", + "chicken": "🐔", + "penguin": "🐧", + "bird": "🐦", + "baby_chick": "🐤", + "hatching_chick": "🐣", + "hatched_chick": "🐥", + "duck": "🦆", + "eagle": "🦅", + "owl": "🦉", + "bat": "🦇", + "wolf": "🐺", + "boar": "🐗", + "horse": "🐴", + "unicorn": "🦄", + "honeybee": "🐝", + "bug": "🐛", + "butterfly": "🦋", + "snail": "🐌", + "beetle": "🐞", + "ant": "🐜", + "spider": "🕷", + "scorpion": "🦂", + "crab": "🦀", + "snake": "🐍", + "lizard": "🦎", + "turtle": "🐢", + "tropical_fish": "🐠", + "fish": "🐟", + "blowfish": "🐡", + "dolphin": "🐬", + "shark": "🦈", + "whale": "🐳", + "whale2": "🐋", + "crocodile": "🐊", + "leopard": "🐆", + "tiger2": "🐅", + "water_buffalo": "🐃", + "ox": "🐂", + "cow2": "🐄", + "deer": "🦌", + "dromedary_camel": "🐪", + "camel": "🐫", + "elephant": "🐘", + "rhinoceros": "🦏", + "goat": "🐐", + "ram": "🐏", + "sheep": "🐑", + "racehorse": "🐎", + "pig2": "🐖", + "rat": "🐀", + "mouse2": "🐁", + "rooster": "🐓", + "turkey": "🦃", + "dove": "🕊", + "dog2": "🐕", + "poodle": "🐩", + "cat2": "🐈", + "rabbit2": "🐇", + "chipmunk": "🐿", + "paw_prints": "🐾", + "dragon": "🐉", + "dragon_face": "🐲", + "cactus": "🌵", + "christmas_tree": "🎄", + "evergreen_tree": "🌲", + "deciduous_tree": "🌳", + "palm_tree": "🌴", + "seedling": "🌱", + "herb": "🌿", + "shamrock": "☘", + "four_leaf_clover": "🍀", + "bamboo": "🎍", + "tanabata_tree": "🎋", + "leaves": "🍃", + "fallen_leaf": "🍂", + "maple_leaf": "🍁", + "ear_of_rice": "🌾", + "hibiscus": "🌺", + "sunflower": "🌻", + "rose": "🌹", + "wilted_flower": "🥀", + "tulip": "🌷", + "blossom": "🌼", + "cherry_blossom": "🌸", + "bouquet": "💐", + "mushroom": "🍄", + "chestnut": "🌰", + "jack_o_lantern": "🎃", + "shell": "🐚", + "spider_web": "🕸", + "earth_americas": "🌎", + "earth_africa": "🌍", + "earth_asia": "🌏", + "full_moon": "🌕", + "waning_gibbous_moon": "🌖", + "last_quarter_moon": "🌗", + "waning_crescent_moon": "🌘", + "new_moon": "🌑", + "waxing_crescent_moon": "🌒", + "first_quarter_moon": "🌓", + "waxing_gibbous_moon": "🌔", + "new_moon_with_face": "🌚", + "full_moon_with_face": "🌝", + "first_quarter_moon_with_face": "🌛", + "last_quarter_moon_with_face": "🌜", + "sun_with_face": "🌞", + "crescent_moon": "🌙", + "star": "⭐", + "star2": "🌟", + "dizzy": "💫", + "sparkles": "✨", + "comet": "☄", + "sunny": "☀️", + "sun_behind_small_cloud": "🌤", + "partly_sunny": "⛅", + "sun_behind_large_cloud": "🌥", + "sun_behind_rain_cloud": "🌦", + "cloud": "☁️", + "cloud_with_rain": "🌧", + "cloud_with_lightning_and_rain": "⛈", + "cloud_with_lightning": "🌩", + "zap": "⚡", + "fire": "🔥", + "boom": "💥", + "snowflake": "❄️", + "cloud_with_snow": "🌨", + "snowman": "⛄", + "snowman_with_snow": "☃", + "wind_face": "🌬", + "dash": "💨", + "tornado": "🌪", + "fog": "🌫", + "open_umbrella": "☂", + "umbrella": "☔", + "droplet": "💧", + "sweat_drops": "💦", + "ocean": "🌊", + "green_apple": "🍏", + "apple": "🍎", + "pear": "🍐", + "tangerine": "🍊", + "lemon": "🍋", + "banana": "🍌", + "watermelon": "🍉", + "grapes": "🍇", + "strawberry": "🍓", + "melon": "🍈", + "cherries": "🍒", + "peach": "🍑", + "pineapple": "🍍", + "kiwi_fruit": "🥝", + "avocado": "🥑", + "tomato": "🍅", + "eggplant": "🍆", + "cucumber": "🥒", + "carrot": "🥕", + "hot_pepper": "🌶", + "potato": "🥔", + "corn": "🌽", + "sweet_potato": "🍠", + "peanuts": "🥜", + "honey_pot": "🍯", + "croissant": "🥐", + "bread": "🍞", + "baguette_bread": "🥖", + "cheese": "🧀", + "egg": "🥚", + "bacon": "🥓", + "pancakes": "🥞", + "poultry_leg": "🍗", + "meat_on_bone": "🍖", + "fried_shrimp": "🍤", + "fried_egg": "🍳", + "hamburger": "🍔", + "fries": "🍟", + "stuffed_flatbread": "🥙", + "hotdog": "🌭", + "pizza": "🍕", + "spaghetti": "🍝", + "taco": "🌮", + "burrito": "🌯", + "green_salad": "🥗", + "shallow_pan_of_food": "🥘", + "ramen": "🍜", + "stew": "🍲", + "fish_cake": "🍥", + "sushi": "🍣", + "bento": "🍱", + "curry": "🍛", + "rice_ball": "🍙", + "rice": "🍚", + "rice_cracker": "🍘", + "oden": "🍢", + "dango": "🍡", + "shaved_ice": "🍧", + "ice_cream": "🍨", + "icecream": "🍦", + "cake": "🍰", + "birthday": "🎂", + "custard": "🍮", + "candy": "🍬", + "lollipop": "🍭", + "chocolate_bar": "🍫", + "popcorn": "🍿", + "doughnut": "🍩", + "cookie": "🍪", + "milk_glass": "🥛", + "beer": "🍺", + "beers": "🍻", + "clinking_glasses": "🥂", + "wine_glass": "🍷", + "tumbler_glass": "🥃", + "cocktail": "🍸", + "tropical_drink": "🍹", + "champagne": "🍾", + "sake": "🍶", + "tea": "🍵", + "coffee": "☕", + "baby_bottle": "🍼", + "spoon": "🥄", + "fork_and_knife": "🍴", + "plate_with_cutlery": "🍽", + "soccer": "⚽", + "basketball": "🏀", + "football": "🏈", + "baseball": "⚾", + "tennis": "🎾", + "volleyball": "🏐", + "rugby_football": "🏉", + "8ball": "🎱", + "golf": "⛳", + "golfing_woman": "🏌️‍♀️", + "golfing_man": "🏌", + "ping_pong": "🏓", + "badminton": "🏸", + "goal_net": "🥅", + "ice_hockey": "🏒", + "field_hockey": "🏑", + "cricket": "🏏", + "ski": "🎿", + "skier": "⛷", + "snowboarder": "🏂", + "person_fencing": "🤺", + "women_wrestling": "🤼‍♀️", + "men_wrestling": "🤼‍♂️", + "woman_cartwheeling": "🤸‍♀️", + "man_cartwheeling": "🤸‍♂️", + "woman_playing_handball": "🤾‍♀️", + "man_playing_handball": "🤾‍♂️", + "ice_skate": "⛸", + "bow_and_arrow": "🏹", + "fishing_pole_and_fish": "🎣", + "boxing_glove": "🥊", + "martial_arts_uniform": "🥋", + "rowing_woman": "🚣‍♀️", + "rowing_man": "🚣", + "swimming_woman": "🏊‍♀️", + "swimming_man": "🏊", + "woman_playing_water_polo": "🤽‍♀️", + "man_playing_water_polo": "🤽‍♂️", + "surfing_woman": "🏄‍♀️", + "surfing_man": "🏄", + "bath": "🛀", + "basketball_woman": "⛹️‍♀️", + "basketball_man": "⛹", + "weight_lifting_woman": "🏋️‍♀️", + "weight_lifting_man": "🏋", + "biking_woman": "🚴‍♀️", + "biking_man": "🚴", + "mountain_biking_woman": "🚵‍♀️", + "mountain_biking_man": "🚵", + "horse_racing": "🏇", + "business_suit_levitating": "🕴", + "trophy": "🏆", + "running_shirt_with_sash": "🎽", + "medal_sports": "🏅", + "medal_military": "🎖", + "1st_place_medal": "🥇", + "2nd_place_medal": "🥈", + "3rd_place_medal": "🥉", + "reminder_ribbon": "🎗", + "rosette": "🏵", + "ticket": "🎫", + "tickets": "🎟", + "performing_arts": "🎭", + "art": "🎨", + "circus_tent": "🎪", + "woman_juggling": "🤹‍♀️", + "man_juggling": "🤹‍♂️", + "microphone": "🎤", + "headphones": "🎧", + "musical_score": "🎼", + "musical_keyboard": "🎹", + "drum": "🥁", + "saxophone": "🎷", + "trumpet": "🎺", + "guitar": "🎸", + "violin": "🎻", + "clapper": "🎬", + "video_game": "🎮", + "space_invader": "👾", + "dart": "🎯", + "game_die": "🎲", + "slot_machine": "🎰", + "bowling": "🎳", + "red_car": "🚗", + "taxi": "🚕", + "blue_car": "🚙", + "bus": "🚌", + "trolleybus": "🚎", + "racing_car": "🏎", + "police_car": "🚓", + "ambulance": "🚑", + "fire_engine": "🚒", + "minibus": "🚐", + "truck": "🚚", + "articulated_lorry": "🚛", + "tractor": "🚜", + "kick_scooter": "🛴", + "motorcycle": "🏍", + "bike": "🚲", + "motor_scooter": "🛵", + "rotating_light": "🚨", + "oncoming_police_car": "🚔", + "oncoming_bus": "🚍", + "oncoming_automobile": "🚘", + "oncoming_taxi": "🚖", + "aerial_tramway": "🚡", + "mountain_cableway": "🚠", + "suspension_railway": "🚟", + "railway_car": "🚃", + "train": "🚋", + "monorail": "🚝", + "bullettrain_side": "🚄", + "bullettrain_front": "🚅", + "light_rail": "🚈", + "mountain_railway": "🚞", + "steam_locomotive": "🚂", + "train2": "🚆", + "metro": "🚇", + "tram": "🚊", + "station": "🚉", + "helicopter": "🚁", + "small_airplane": "🛩", + "airplane": "✈️", + "flight_departure": "🛫", + "flight_arrival": "🛬", + "sailboat": "⛵", + "motor_boat": "🛥", + "speedboat": "🚤", + "ferry": "⛴", + "passenger_ship": "🛳", + "rocket": "🚀", + "artificial_satellite": "🛰", + "seat": "💺", + "canoe": "🛶", + "anchor": "⚓", + "construction": "🚧", + "fuelpump": "⛽", + "busstop": "🚏", + "vertical_traffic_light": "🚦", + "traffic_light": "🚥", + "checkered_flag": "🏁", + "ship": "🚢", + "ferris_wheel": "🎡", + "roller_coaster": "🎢", + "carousel_horse": "🎠", + "building_construction": "🏗", + "foggy": "🌁", + "tokyo_tower": "🗼", + "factory": "🏭", + "fountain": "⛲", + "rice_scene": "🎑", + "mountain": "⛰", + "mountain_snow": "🏔", + "mount_fuji": "🗻", + "volcano": "🌋", + "japan": "🗾", + "camping": "🏕", + "tent": "⛺", + "national_park": "🏞", + "motorway": "🛣", + "railway_track": "🛤", + "sunrise": "🌅", + "sunrise_over_mountains": "🌄", + "desert": "🏜", + "beach_umbrella": "🏖", + "desert_island": "🏝", + "city_sunrise": "🌇", + "city_sunset": "🌆", + "cityscape": "🏙", + "night_with_stars": "🌃", + "bridge_at_night": "🌉", + "milky_way": "🌌", + "stars": "🌠", + "sparkler": "🎇", + "fireworks": "🎆", + "rainbow": "🌈", + "houses": "🏘", + "european_castle": "🏰", + "japanese_castle": "🏯", + "stadium": "🏟", + "statue_of_liberty": "🗽", + "house": "🏠", + "house_with_garden": "🏡", + "derelict_house": "🏚", + "office": "🏢", + "department_store": "🏬", + "post_office": "🏣", + "european_post_office": "🏤", + "hospital": "🏥", + "bank": "🏦", + "hotel": "🏨", + "convenience_store": "🏪", + "school": "🏫", + "love_hotel": "🏩", + "wedding": "💒", + "classical_building": "🏛", + "church": "⛪", + "mosque": "🕌", + "synagogue": "🕍", + "kaaba": "🕋", + "shinto_shrine": "⛩", + "watch": "⌚", + "iphone": "📱", + "calling": "📲", + "computer": "💻", + "keyboard": "⌨", + "desktop_computer": "🖥", + "printer": "🖨", + "computer_mouse": "🖱", + "trackball": "🖲", + "joystick": "🕹", + "clamp": "🗜", + "minidisc": "💽", + "floppy_disk": "💾", + "cd": "💿", + "dvd": "📀", + "vhs": "📼", + "camera": "📷", + "camera_flash": "📸", + "video_camera": "📹", + "movie_camera": "🎥", + "film_projector": "📽", + "film_strip": "🎞", + "telephone_receiver": "📞", + "phone": "☎️", + "pager": "📟", + "fax": "📠", + "tv": "📺", + "radio": "📻", + "studio_microphone": "🎙", + "level_slider": "🎚", + "control_knobs": "🎛", + "stopwatch": "⏱", + "timer_clock": "⏲", + "alarm_clock": "⏰", + "mantelpiece_clock": "🕰", + "hourglass_flowing_sand": "⏳", + "hourglass": "⌛", + "satellite": "📡", + "battery": "🔋", + "electric_plug": "🔌", + "bulb": "💡", + "flashlight": "🔦", + "candle": "🕯", + "wastebasket": "🗑", + "oil_drum": "🛢", + "money_with_wings": "💸", + "dollar": "💵", + "yen": "💴", + "euro": "💶", + "pound": "💷", + "moneybag": "💰", + "credit_card": "💳", + "gem": "💎", + "balance_scale": "⚖", + "wrench": "🔧", + "hammer": "🔨", + "hammer_and_pick": "⚒", + "hammer_and_wrench": "🛠", + "pick": "⛏", + "nut_and_bolt": "🔩", + "gear": "⚙", + "chains": "⛓", + "gun": "🔫", + "bomb": "💣", + "hocho": "🔪", + "dagger": "🗡", + "crossed_swords": "⚔", + "shield": "🛡", + "smoking": "🚬", + "skull_and_crossbones": "☠", + "coffin": "⚰", + "funeral_urn": "⚱", + "amphora": "🏺", + "crystal_ball": "🔮", + "prayer_beads": "📿", + "barber": "💈", + "alembic": "⚗", + "telescope": "🔭", + "microscope": "🔬", + "hole": "🕳", + "pill": "💊", + "syringe": "💉", + "thermometer": "🌡", + "label": "🏷", + "bookmark": "🔖", + "toilet": "🚽", + "shower": "🚿", + "bathtub": "🛁", + "key": "🔑", + "old_key": "🗝", + "couch_and_lamp": "🛋", + "sleeping_bed": "🛌", + "bed": "🛏", + "door": "🚪", + "bellhop_bell": "🛎", + "framed_picture": "🖼", + "world_map": "🗺", + "parasol_on_ground": "⛱", + "moyai": "🗿", + "shopping": "🛍", + "shopping_cart": "🛒", + "balloon": "🎈", + "flags": "🎏", + "ribbon": "🎀", + "gift": "🎁", + "confetti_ball": "🎊", + "tada": "🎉", + "dolls": "🎎", + "wind_chime": "🎐", + "crossed_flags": "🎌", + "izakaya_lantern": "🏮", + "email": "✉️", + "envelope_with_arrow": "📩", + "incoming_envelope": "📨", + "e-mail": "📧", + "love_letter": "💌", + "postbox": "📮", + "mailbox_closed": "📪", + "mailbox": "📫", + "mailbox_with_mail": "📬", + "mailbox_with_no_mail": "📭", + "package": "📦", + "postal_horn": "📯", + "inbox_tray": "📥", + "outbox_tray": "📤", + "scroll": "📜", + "page_with_curl": "📃", + "bookmark_tabs": "📑", + "bar_chart": "📊", + "chart_with_upwards_trend": "📈", + "chart_with_downwards_trend": "📉", + "page_facing_up": "📄", + "date": "📅", + "calendar": "📆", + "spiral_calendar": "🗓", + "card_index": "📇", + "card_file_box": "🗃", + "ballot_box": "🗳", + "file_cabinet": "🗄", + "clipboard": "📋", + "spiral_notepad": "🗒", + "file_folder": "📁", + "open_file_folder": "📂", + "card_index_dividers": "🗂", + "newspaper_roll": "🗞", + "newspaper": "📰", + "notebook": "📓", + "closed_book": "📕", + "green_book": "📗", + "blue_book": "📘", + "orange_book": "📙", + "notebook_with_decorative_cover": "📔", + "ledger": "📒", + "books": "📚", + "open_book": "📖", + "link": "🔗", + "paperclip": "📎", + "paperclips": "🖇", + "scissors": "✂️", + "triangular_ruler": "📐", + "straight_ruler": "📏", + "pushpin": "📌", + "round_pushpin": "📍", + "triangular_flag_on_post": "🚩", + "white_flag": "🏳", + "black_flag": "🏴", + "rainbow_flag": "🏳️‍🌈", + "closed_lock_with_key": "🔐", + "lock": "🔒", + "unlock": "🔓", + "lock_with_ink_pen": "🔏", + "pen": "🖊", + "fountain_pen": "🖋", + "black_nib": "✒️", + "memo": "📝", + "pencil2": "✏️", + "crayon": "🖍", + "paintbrush": "🖌", + "mag": "🔍", + "mag_right": "🔎", + "heart": "❤️", + "yellow_heart": "💛", + "green_heart": "💚", + "blue_heart": "💙", + "purple_heart": "💜", + "black_heart": "🖤", + "broken_heart": "💔", + "heavy_heart_exclamation": "❣", + "two_hearts": "💕", + "revolving_hearts": "💞", + "heartbeat": "💓", + "heartpulse": "💗", + "sparkling_heart": "💖", + "cupid": "💘", + "gift_heart": "💝", + "heart_decoration": "💟", + "peace_symbol": "☮", + "latin_cross": "✝", + "star_and_crescent": "☪", + "om": "🕉", + "wheel_of_dharma": "☸", + "star_of_david": "✡", + "six_pointed_star": "🔯", + "menorah": "🕎", + "yin_yang": "☯", + "orthodox_cross": "☦", + "place_of_worship": "🛐", + "ophiuchus": "⛎", + "aries": "♈", + "taurus": "♉", + "gemini": "♊", + "cancer": "♋", + "leo": "♌", + "virgo": "♍", + "libra": "♎", + "scorpius": "♏", + "sagittarius": "♐", + "capricorn": "♑", + "aquarius": "♒", + "pisces": "♓", + "id": "🆔", + "atom_symbol": "⚛", + "u7a7a": "🈳", + "u5272": "🈹", + "radioactive": "☢", + "biohazard": "☣", + "mobile_phone_off": "📴", + "vibration_mode": "📳", + "u6709": "🈶", + "u7121": "🈚", + "u7533": "🈸", + "u55b6": "🈺", + "u6708": "🈷️", + "eight_pointed_black_star": "✴️", + "vs": "🆚", + "accept": "🉑", + "white_flower": "💮", + "ideograph_advantage": "🉐", + "secret": "㊙️", + "congratulations": "㊗️", + "u5408": "🈴", + "u6e80": "🈵", + "u7981": "🈲", + "a": "🅰️", + "b": "🅱️", + "ab": "🆎", + "cl": "🆑", + "o2": "🅾️", + "sos": "🆘", + "no_entry": "⛔", + "name_badge": "📛", + "no_entry_sign": "🚫", + "x": "❌", + "o": "⭕", + "stop_sign": "🛑", + "anger": "💢", + "hotsprings": "♨️", + "no_pedestrians": "🚷", + "do_not_litter": "🚯", + "no_bicycles": "🚳", + "non-potable_water": "🚱", + "underage": "🔞", + "no_mobile_phones": "📵", + "exclamation": "❗", + "grey_exclamation": "❕", + "question": "❓", + "grey_question": "❔", + "bangbang": "‼️", + "interrobang": "⁉️", + "low_brightness": "🔅", + "high_brightness": "🔆", + "trident": "🔱", + "fleur_de_lis": "⚜", + "part_alternation_mark": "〽️", + "warning": "⚠️", + "children_crossing": "🚸", + "beginner": "🔰", + "recycle": "♻️", + "u6307": "🈯", + "chart": "💹", + "sparkle": "❇️", + "eight_spoked_asterisk": "✳️", + "negative_squared_cross_mark": "❎", + "white_check_mark": "✅", + "diamond_shape_with_a_dot_inside": "💠", + "cyclone": "🌀", + "loop": "➿", + "globe_with_meridians": "🌐", + "m": "Ⓜ️", + "atm": "🏧", + "sa": "🈂️", + "passport_control": "🛂", + "customs": "🛃", + "baggage_claim": "🛄", + "left_luggage": "🛅", + "wheelchair": "♿", + "no_smoking": "🚭", + "wc": "🚾", + "parking": "🅿️", + "potable_water": "🚰", + "mens": "🚹", + "womens": "🚺", + "baby_symbol": "🚼", + "restroom": "🚻", + "put_litter_in_its_place": "🚮", + "cinema": "🎦", + "signal_strength": "📶", + "koko": "🈁", + "ng": "🆖", + "ok": "🆗", + "up": "🆙", + "cool": "🆒", + "new": "🆕", + "free": "🆓", + "zero": "0️⃣", + "one": "1️⃣", + "two": "2️⃣", + "three": "3️⃣", + "four": "4️⃣", + "five": "5️⃣", + "six": "6️⃣", + "seven": "7️⃣", + "eight": "8️⃣", + "nine": "9️⃣", + "keycap_ten": "🔟", + "asterisk": "*⃣", + "arrow_forward": "▶️", + "pause_button": "⏸", + "next_track_button": "⏭", + "stop_button": "⏹", + "record_button": "⏺", + "play_or_pause_button": "⏯", + "previous_track_button": "⏮", + "fast_forward": "⏩", + "rewind": "⏪", + "twisted_rightwards_arrows": "🔀", + "repeat": "🔁", + "repeat_one": "🔂", + "arrow_backward": "◀️", + "arrow_up_small": "🔼", + "arrow_down_small": "🔽", + "arrow_double_up": "⏫", + "arrow_double_down": "⏬", + "arrow_right": "➡️", + "arrow_left": "⬅️", + "arrow_up": "⬆️", + "arrow_down": "⬇️", + "arrow_upper_right": "↗️", + "arrow_lower_right": "↘️", + "arrow_lower_left": "↙️", + "arrow_upper_left": "↖️", + "arrow_up_down": "↕️", + "left_right_arrow": "↔️", + "arrows_counterclockwise": "🔄", + "arrow_right_hook": "↪️", + "leftwards_arrow_with_hook": "↩️", + "arrow_heading_up": "⤴️", + "arrow_heading_down": "⤵️", + "hash": "#️⃣", + "information_source": "ℹ️", + "abc": "🔤", + "abcd": "🔡", + "capital_abcd": "🔠", + "symbols": "🔣", + "musical_note": "🎵", + "notes": "🎶", + "wavy_dash": "〰️", + "curly_loop": "➰", + "heavy_check_mark": "✔️", + "arrows_clockwise": "🔃", + "heavy_plus_sign": "➕", + "heavy_minus_sign": "➖", + "heavy_division_sign": "➗", + "heavy_multiplication_x": "✖️", + "heavy_dollar_sign": "💲", + "currency_exchange": "💱", + "copyright": "©️", + "registered": "®️", + "tm": "™️", + "end": "🔚", + "back": "🔙", + "on": "🔛", + "top": "🔝", + "soon": "🔜", + "ballot_box_with_check": "☑️", + "radio_button": "🔘", + "white_circle": "⚪", + "black_circle": "⚫", + "red_circle": "🔴", + "large_blue_circle": "🔵", + "small_orange_diamond": "🔸", + "small_blue_diamond": "🔹", + "large_orange_diamond": "🔶", + "large_blue_diamond": "🔷", + "small_red_triangle": "🔺", + "black_small_square": "▪️", + "white_small_square": "▫️", + "black_large_square": "⬛", + "white_large_square": "⬜", + "small_red_triangle_down": "🔻", + "black_medium_square": "◼️", + "white_medium_square": "◻️", + "black_medium_small_square": "◾", + "white_medium_small_square": "◽", + "black_square_button": "🔲", + "white_square_button": "🔳", + "speaker": "🔈", + "sound": "🔉", + "loud_sound": "🔊", + "mute": "🔇", + "mega": "📣", + "loudspeaker": "📢", + "bell": "🔔", + "no_bell": "🔕", + "black_joker": "🃏", + "mahjong": "🀄", + "spades": "♠️", + "clubs": "♣️", + "hearts": "♥️", + "diamonds": "♦️", + "flower_playing_cards": "🎴", + "thought_balloon": "💭", + "right_anger_bubble": "🗯", + "speech_balloon": "💬", + "left_speech_bubble": "🗨", + "clock1": "🕐", + "clock2": "🕑", + "clock3": "🕒", + "clock4": "🕓", + "clock5": "🕔", + "clock6": "🕕", + "clock7": "🕖", + "clock8": "🕗", + "clock9": "🕘", + "clock10": "🕙", + "clock11": "🕚", + "clock12": "🕛", + "clock130": "🕜", + "clock230": "🕝", + "clock330": "🕞", + "clock430": "🕟", + "clock530": "🕠", + "clock630": "🕡", + "clock730": "🕢", + "clock830": "🕣", + "clock930": "🕤", + "clock1030": "🕥", + "clock1130": "🕦", + "clock1230": "🕧", + "afghanistan": "🇦🇫", + "aland_islands": "🇦🇽", + "albania": "🇦🇱", + "algeria": "🇩🇿", + "american_samoa": "🇦🇸", + "andorra": "🇦🇩", + "angola": "🇦🇴", + "anguilla": "🇦🇮", + "antarctica": "🇦🇶", + "antigua_barbuda": "🇦🇬", + "argentina": "🇦🇷", + "armenia": "🇦🇲", + "aruba": "🇦🇼", + "australia": "🇦🇺", + "austria": "🇦🇹", + "azerbaijan": "🇦🇿", + "bahamas": "🇧🇸", + "bahrain": "🇧🇭", + "bangladesh": "🇧🇩", + "barbados": "🇧🇧", + "belarus": "🇧🇾", + "belgium": "🇧🇪", + "belize": "🇧🇿", + "benin": "🇧🇯", + "bermuda": "🇧🇲", + "bhutan": "🇧🇹", + "bolivia": "🇧🇴", + "caribbean_netherlands": "🇧🇶", + "bosnia_herzegovina": "🇧🇦", + "botswana": "🇧🇼", + "brazil": "🇧🇷", + "british_indian_ocean_territory": "🇮🇴", + "british_virgin_islands": "🇻🇬", + "brunei": "🇧🇳", + "bulgaria": "🇧🇬", + "burkina_faso": "🇧🇫", + "burundi": "🇧🇮", + "cape_verde": "🇨🇻", + "cambodia": "🇰🇭", + "cameroon": "🇨🇲", + "canada": "🇨🇦", + "canary_islands": "🇮🇨", + "cayman_islands": "🇰🇾", + "central_african_republic": "🇨🇫", + "chad": "🇹🇩", + "chile": "🇨🇱", + "cn": "🇨🇳", + "christmas_island": "🇨🇽", + "cocos_islands": "🇨🇨", + "colombia": "🇨🇴", + "comoros": "🇰🇲", + "congo_brazzaville": "🇨🇬", + "congo_kinshasa": "🇨🇩", + "cook_islands": "🇨🇰", + "costa_rica": "🇨🇷", + "croatia": "🇭🇷", + "cuba": "🇨🇺", + "curacao": "🇨🇼", + "cyprus": "🇨🇾", + "czech_republic": "🇨🇿", + "denmark": "🇩🇰", + "djibouti": "🇩🇯", + "dominica": "🇩🇲", + "dominican_republic": "🇩🇴", + "ecuador": "🇪🇨", + "egypt": "🇪🇬", + "el_salvador": "🇸🇻", + "equatorial_guinea": "🇬🇶", + "eritrea": "🇪🇷", + "estonia": "🇪🇪", + "ethiopia": "🇪🇹", + "eu": "🇪🇺", + "falkland_islands": "🇫🇰", + "faroe_islands": "🇫🇴", + "fiji": "🇫🇯", + "finland": "🇫🇮", + "fr": "🇫🇷", + "french_guiana": "🇬🇫", + "french_polynesia": "🇵🇫", + "french_southern_territories": "🇹🇫", + "gabon": "🇬🇦", + "gambia": "🇬🇲", + "georgia": "🇬🇪", + "de": "🇩🇪", + "ghana": "🇬🇭", + "gibraltar": "🇬🇮", + "greece": "🇬🇷", + "greenland": "🇬🇱", + "grenada": "🇬🇩", + "guadeloupe": "🇬🇵", + "guam": "🇬🇺", + "guatemala": "🇬🇹", + "guernsey": "🇬🇬", + "guinea": "🇬🇳", + "guinea_bissau": "🇬🇼", + "guyana": "🇬🇾", + "haiti": "🇭🇹", + "honduras": "🇭🇳", + "hong_kong": "🇭🇰", + "hungary": "🇭🇺", + "iceland": "🇮🇸", + "india": "🇮🇳", + "indonesia": "🇮🇩", + "iran": "🇮🇷", + "iraq": "🇮🇶", + "ireland": "🇮🇪", + "isle_of_man": "🇮🇲", + "israel": "🇮🇱", + "it": "🇮🇹", + "cote_divoire": "🇨🇮", + "jamaica": "🇯🇲", + "jp": "🇯🇵", + "jersey": "🇯🇪", + "jordan": "🇯🇴", + "kazakhstan": "🇰🇿", + "kenya": "🇰🇪", + "kiribati": "🇰🇮", + "kosovo": "🇽🇰", + "kuwait": "🇰🇼", + "kyrgyzstan": "🇰🇬", + "laos": "🇱🇦", + "latvia": "🇱🇻", + "lebanon": "🇱🇧", + "lesotho": "🇱🇸", + "liberia": "🇱🇷", + "libya": "🇱🇾", + "liechtenstein": "🇱🇮", + "lithuania": "🇱🇹", + "luxembourg": "🇱🇺", + "macau": "🇲🇴", + "macedonia": "🇲🇰", + "madagascar": "🇲🇬", + "malawi": "🇲🇼", + "malaysia": "🇲🇾", + "maldives": "🇲🇻", + "mali": "🇲🇱", + "malta": "🇲🇹", + "marshall_islands": "🇲🇭", + "martinique": "🇲🇶", + "mauritania": "🇲🇷", + "mauritius": "🇲🇺", + "mayotte": "🇾🇹", + "mexico": "🇲🇽", + "micronesia": "🇫🇲", + "moldova": "🇲🇩", + "monaco": "🇲🇨", + "mongolia": "🇲🇳", + "montenegro": "🇲🇪", + "montserrat": "🇲🇸", + "morocco": "🇲🇦", + "mozambique": "🇲🇿", + "myanmar": "🇲🇲", + "namibia": "🇳🇦", + "nauru": "🇳🇷", + "nepal": "🇳🇵", + "netherlands": "🇳🇱", + "new_caledonia": "🇳🇨", + "new_zealand": "🇳🇿", + "nicaragua": "🇳🇮", + "niger": "🇳🇪", + "nigeria": "🇳🇬", + "niue": "🇳🇺", + "norfolk_island": "🇳🇫", + "northern_mariana_islands": "🇲🇵", + "north_korea": "🇰🇵", + "norway": "🇳🇴", + "oman": "🇴🇲", + "pakistan": "🇵🇰", + "palau": "🇵🇼", + "palestinian_territories": "🇵🇸", + "panama": "🇵🇦", + "papua_new_guinea": "🇵🇬", + "paraguay": "🇵🇾", + "peru": "🇵🇪", + "philippines": "🇵🇭", + "pitcairn_islands": "🇵🇳", + "poland": "🇵🇱", + "portugal": "🇵🇹", + "puerto_rico": "🇵🇷", + "qatar": "🇶🇦", + "reunion": "🇷🇪", + "romania": "🇷🇴", + "ru": "🇷🇺", + "rwanda": "🇷🇼", + "st_barthelemy": "🇧🇱", + "st_helena": "🇸🇭", + "st_kitts_nevis": "🇰🇳", + "st_lucia": "🇱🇨", + "st_pierre_miquelon": "🇵🇲", + "st_vincent_grenadines": "🇻🇨", + "samoa": "🇼🇸", + "san_marino": "🇸🇲", + "sao_tome_principe": "🇸🇹", + "saudi_arabia": "🇸🇦", + "senegal": "🇸🇳", + "serbia": "🇷🇸", + "seychelles": "🇸🇨", + "sierra_leone": "🇸🇱", + "singapore": "🇸🇬", + "sint_maarten": "🇸🇽", + "slovakia": "🇸🇰", + "slovenia": "🇸🇮", + "solomon_islands": "🇸🇧", + "somalia": "🇸🇴", + "south_africa": "🇿🇦", + "south_georgia_south_sandwich_islands": "🇬🇸", + "kr": "🇰🇷", + "south_sudan": "🇸🇸", + "es": "🇪🇸", + "sri_lanka": "🇱🇰", + "sudan": "🇸🇩", + "suriname": "🇸🇷", + "swaziland": "🇸🇿", + "sweden": "🇸🇪", + "switzerland": "🇨🇭", + "syria": "🇸🇾", + "taiwan": "🇹🇼", + "tajikistan": "🇹🇯", + "tanzania": "🇹🇿", + "thailand": "🇹🇭", + "timor_leste": "🇹🇱", + "togo": "🇹🇬", + "tokelau": "🇹🇰", + "tonga": "🇹🇴", + "trinidad_tobago": "🇹🇹", + "tunisia": "🇹🇳", + "tr": "🇹🇷", + "turkmenistan": "🇹🇲", + "turks_caicos_islands": "🇹🇨", + "tuvalu": "🇹🇻", + "uganda": "🇺🇬", + "ukraine": "🇺🇦", + "united_arab_emirates": "🇦🇪", + "uk": "🇬🇧", + "us": "🇺🇸", + "us_virgin_islands": "🇻🇮", + "uruguay": "🇺🇾", + "uzbekistan": "🇺🇿", + "vanuatu": "🇻🇺", + "vatican_city": "🇻🇦", + "venezuela": "🇻🇪", + "vietnam": "🇻🇳", + "wallis_futuna": "🇼🇫", + "western_sahara": "🇪🇭", + "yemen": "🇾🇪", + "zambia": "🇿🇲", + "zimbabwe": "🇿🇼" +} \ No newline at end of file diff --git a/client/js/lounge.js b/client/js/lounge.js index e9b78f2f..5c44ba3a 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -2,12 +2,14 @@ // vendor libraries require("jquery-ui/ui/widgets/sortable"); +require("jquery-textcomplete"); const $ = require("jquery"); const moment = require("moment"); const Mousetrap = require("mousetrap"); const URI = require("urijs"); // our libraries +const emojiMap = require("./libs/simplemap.json"); require("./libs/jquery/inputhistory"); require("./libs/jquery/stickyscroll"); require("./libs/jquery/tabcomplete"); @@ -41,6 +43,80 @@ $(function() { var favicon = $("#favicon"); + // Autocompletion Strategies + + var emojiStrategy = { + id: "emoji", + match: /\B:([-+\w]*)$/, + search: function(term, callback) { + callback($.map(Object.keys(emojiMap), function(e) { + return e.indexOf(term) === 0 ? e : null; + })); + }, + template: function(value) { + return `${emojiMap[value]} ${value}`; + }, + replace: function(value) { + return emojiMap[value]; + }, + index: 1 + }; + + var nicksStrategy = { + id: "nicks", + match: /\B(@([a-zA-Z_[\]\\^{}|`@][a-zA-Z0-9_[\]\\^{}|`-]*)?)$/, + search: function(term, callback) { + term = term.slice(1); + if (term[0] === "@") { + callback(completeNicks(term.slice(1)).map(function(val) { + return "@" + val; + })); + } else { + callback(completeNicks(term)); + } + }, + template: function(value) { + if (value[0] === "@") { + return value; + } + return "@" + value; + }, + replace: function(value) { + return value; + }, + index: 1 + }; + + var chanStrategy = { + id: "chans", + match: /\B((#|\+|&|![A-Z0-9]{5})([^\x00\x0A\x0D\x20\x2C\x3A]+(:[^\x00\x0A\x0D\x20\x2C\x3A]*)?)?)$/, + search: function(term, callback, match) { + callback(completeChans(match[0])); + }, + template: function(value) { + return value; + }, + replace: function(value) { + return value; + }, + index: 1 + }; + + var commandStrategy = { + id: "commands", + match: /^\/(\w*)$/, + search: function(term, callback) { + callback(completeCommands("/" + term)); + }, + template: function(value) { + return value; + }, + replace: function(value) { + return value; + }, + index: 1 + }; + socket.on("auth", function(data) { var login = $("#sign-in"); var token; @@ -638,7 +714,18 @@ $(function() { chat.find(".chan.active .chat").trigger("msg.sticky"); // fix growing }) - .tab(complete, {hint: false}); + .tab(completeNicks, {hint: false}) + .textcomplete([emojiStrategy, nicksStrategy, chanStrategy, commandStrategy], { + dropdownClassName: "textcomplete-menu", + placement: "top" + }).on({ + "textComplete:show": function() { + $(this).data("autocompleting", true); + }, + "textComplete:hide": function() { + $(this).data("autocompleting", false); + } + }); var focus = $.noop; if (!("ontouchstart" in window || navigator.maxTouchPoints > 0)) { @@ -1272,14 +1359,31 @@ $(function() { .find(".messages .msg, .date-marker").remove(); } - function complete(word) { - var words = constants.commands.slice(); + function completeNicks(word) { var users = chat.find(".active").find(".users"); - var nicks = users.data("nicks"); + var words = users.data("nicks"); - for (var i in nicks) { - words.push(nicks[i]); - } + return $.grep( + words, + function(w) { + return !w.toLowerCase().indexOf(word.toLowerCase()); + } + ); + } + + function completeCommands(word) { + var words = constants.commands.slice(); + + return $.grep( + words, + function(w) { + return !w.toLowerCase().indexOf(word.toLowerCase()); + } + ); + } + + function completeChans(word) { + var words = []; sidebar.find(".chan") .each(function() { diff --git a/package.json b/package.json index 447d0e43..6f31a1c6 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "express-handlebars": "3.0.0", "fs-extra": "2.1.2", "irc-framework": "2.8.0", + "json-loader": "0.5.4", "ldapjs": "1.0.1", "lodash": "4.17.4", "moment": "2.18.1", @@ -68,6 +69,7 @@ "handlebars": "4.0.6", "handlebars-loader": "1.5.0", "jquery": "3.2.1", + "jquery-textcomplete": "1.8.0", "jquery-ui": "1.12.1", "mocha": "3.3.0", "mousetrap": "1.6.1", diff --git a/webpack.config.js b/webpack.config.js index b54015cb..fb9e2fbf 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,6 +13,7 @@ let config = { "js/bundle.vendor.js": [ "handlebars/runtime", "jquery", + "jquery-textcomplete", "jquery-ui/ui/widgets/sortable", "moment", "mousetrap", @@ -46,6 +47,10 @@ let config = { } } }, + { + test: /\.json$/, + loader: "json-loader" + }, { test: /\.tpl$/, include: [ From 7229e0dda4d3565e78dd55eef4e9aa4de293d270 Mon Sep 17 00:00:00 2001 From: Yash Srivastav Date: Mon, 12 Dec 2016 05:12:33 +0530 Subject: [PATCH 0342/3926] Disable history completion during emoji completion --- client/js/libs/jquery/inputhistory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/js/libs/jquery/inputhistory.js b/client/js/libs/jquery/inputhistory.js index 778fa696..d6f98360 100644 --- a/client/js/libs/jquery/inputhistory.js +++ b/client/js/libs/jquery/inputhistory.js @@ -56,7 +56,7 @@ import jQuery from "jquery"; case 38: // Up case 40: // Down // NOTICE: This is specific to The Lounge. - if (e.ctrlKey || e.metaKey) { + if (e.ctrlKey || e.metaKey || self.data("autocompleting")) { break; } From 29d8bc9d3da29bac49f1d978eb8768744a1ab43e Mon Sep 17 00:00:00 2001 From: Yash Srivastav Date: Wed, 29 Mar 2017 11:05:40 +0530 Subject: [PATCH 0343/3926] Add Help for autocompletion --- client/index.html | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/client/index.html b/client/index.html index 1b40bfce..af4d3bcd 100644 --- a/client/index.html +++ b/client/index.html @@ -532,6 +532,48 @@
+

Autocompletion

+ +

Start typing the following characters followed by any letter to + trigger the autocompletion dropdown:

+ +
+
+ @ +
+
+

Nickname

+
+
+ +
+
+ # +
+
+

Chan

+
+
+ +
+
+ / +
+
+

Commands (see list of commands below)

+
+
+ +
+
+ : +
+
+

Emoji

+
+
+ +

Commands

All commands can be autocompleted with tab.

From e000ba45dfb594dfdf8dada5155ab6af30be21b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 25 Apr 2017 01:01:24 +0200 Subject: [PATCH 0344/3926] Improve details of emoji/chan/nick/command autocompletion - Make dropdown items match context menu items - Disable transparency on dropdown item links - Clean up help page additions - Better align help page autocompletion characters - Use ES6 features (`const`, arrow functions, method definition shorthands) - Use `Array#filter` instead of `$.map` - Do not display `@` in nick completion *when* only one `@` is used (to be less confusing and more consistent) --- client/css/style.css | 16 ++++------- client/index.html | 15 +++++----- client/js/lounge.js | 67 ++++++++++++++++++-------------------------- 3 files changed, 40 insertions(+), 58 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 522b319d..29323197 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1338,8 +1338,7 @@ kbd { #help .help-item .subject { white-space: nowrap; - padding-right: 10px; - min-width: 150px; + padding-right: 15px; } #help .help-item .description p { @@ -1528,19 +1527,14 @@ kbd { display: inline-block; } -.textcomplete-item { - border-bottom-color: rgba(0, 0, 0, .1); - border-bottom-style: solid; - border-bottom-width: 1px; - margin-top: 0; - margin-bottom: 0; - padding: 10px 8px; -} - .textcomplete-item a { color: #333; } +.textcomplete-item a:hover { + opacity: 1; +} + .textcomplete-item .emoji { margin-right: 8px; width: 16px; diff --git a/client/index.html b/client/index.html index af4d3bcd..5103e0ba 100644 --- a/client/index.html +++ b/client/index.html @@ -534,15 +534,17 @@

Autocompletion

-

Start typing the following characters followed by any letter to - trigger the autocompletion dropdown:

+

+ Start typing the following characters followed by any letter to + trigger the autocompletion dropdown: +

@
-

Nickname

+

Nickname

@@ -551,7 +553,7 @@ #
-

Chan

+

Channel

@@ -560,7 +562,7 @@ /
-

Commands (see list of commands below)

+

Commands (see list of commands below)

@@ -569,11 +571,10 @@ :
-

Emoji

+

Emoji

-

Commands

All commands can be autocompleted with tab.

diff --git a/client/js/lounge.js b/client/js/lounge.js index 5c44ba3a..327808dd 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -45,73 +45,66 @@ $(function() { // Autocompletion Strategies - var emojiStrategy = { + const emojiStrategy = { id: "emoji", match: /\B:([-+\w]*)$/, - search: function(term, callback) { - callback($.map(Object.keys(emojiMap), function(e) { - return e.indexOf(term) === 0 ? e : null; - })); + search(term, callback) { + callback(Object.keys(emojiMap).filter(name => name.indexOf(term) === 0)); }, - template: function(value) { + template(value) { return `${emojiMap[value]} ${value}`; }, - replace: function(value) { + replace(value) { return emojiMap[value]; }, index: 1 }; - var nicksStrategy = { + const nicksStrategy = { id: "nicks", match: /\B(@([a-zA-Z_[\]\\^{}|`@][a-zA-Z0-9_[\]\\^{}|`-]*)?)$/, - search: function(term, callback) { + search(term, callback) { term = term.slice(1); if (term[0] === "@") { - callback(completeNicks(term.slice(1)).map(function(val) { - return "@" + val; - })); + callback(completeNicks(term.slice(1)).map(val => "@" + val)); } else { callback(completeNicks(term)); } }, - template: function(value) { - if (value[0] === "@") { - return value; - } - return "@" + value; + template(value) { + return value; }, - replace: function(value) { + replace(value) { return value; }, index: 1 }; - var chanStrategy = { + const chanStrategy = { id: "chans", match: /\B((#|\+|&|![A-Z0-9]{5})([^\x00\x0A\x0D\x20\x2C\x3A]+(:[^\x00\x0A\x0D\x20\x2C\x3A]*)?)?)$/, - search: function(term, callback, match) { + search(term, callback, match) { callback(completeChans(match[0])); }, - template: function(value) { + template(value) { return value; }, - replace: function(value) { + replace(value) { return value; }, index: 1 }; - var commandStrategy = { + const commandStrategy = { id: "commands", match: /^\/(\w*)$/, - search: function(term, callback) { + search(term, callback) { callback(completeCommands("/" + term)); }, - template: function(value) { + template(value) { return value; }, - replace: function(value) { + replace(value) { return value; }, index: 1 @@ -1360,34 +1353,30 @@ $(function() { } function completeNicks(word) { - var users = chat.find(".active").find(".users"); - var words = users.data("nicks"); + const users = chat.find(".active").find(".users"); + const words = users.data("nicks"); return $.grep( words, - function(w) { - return !w.toLowerCase().indexOf(word.toLowerCase()); - } + w => !w.toLowerCase().indexOf(word.toLowerCase()) ); } function completeCommands(word) { - var words = constants.commands.slice(); + const words = constants.commands.slice(); return $.grep( words, - function(w) { - return !w.toLowerCase().indexOf(word.toLowerCase()); - } + w => !w.toLowerCase().indexOf(word.toLowerCase()) ); } function completeChans(word) { - var words = []; + const words = []; sidebar.find(".chan") .each(function() { - var self = $(this); + const self = $(this); if (!self.hasClass("lobby")) { words.push(self.data("title")); } @@ -1395,9 +1384,7 @@ $(function() { return $.grep( words, - function(w) { - return !w.toLowerCase().indexOf(word.toLowerCase()); - } + w => !w.toLowerCase().indexOf(word.toLowerCase()) ); } From 5c3e15e17c0dc4a608f1a7f98bb8ff6dcbef7c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Tue, 25 Apr 2017 22:31:37 +0200 Subject: [PATCH 0345/3926] Remove json-loader, unnecessary with Webpack v2 See these notes: - https://webpack.js.org/guides/migrating/#json-loader-is-not-required-anymore - https://github.com/webpack-contrib/json-loader#json-loader --- package.json | 1 - webpack.config.js | 4 ---- 2 files changed, 5 deletions(-) diff --git a/package.json b/package.json index 6f31a1c6..fc8693d7 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "express-handlebars": "3.0.0", "fs-extra": "2.1.2", "irc-framework": "2.8.0", - "json-loader": "0.5.4", "ldapjs": "1.0.1", "lodash": "4.17.4", "moment": "2.18.1", diff --git a/webpack.config.js b/webpack.config.js index fb9e2fbf..0cdd6833 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -47,10 +47,6 @@ let config = { } } }, - { - test: /\.json$/, - loader: "json-loader" - }, { test: /\.tpl$/, include: [ From 81a5615c9a58f2d91962f053f5eb0b38320e553b Mon Sep 17 00:00:00 2001 From: PolarizedIons Date: Wed, 26 Apr 2017 19:34:31 +0200 Subject: [PATCH 0346/3926] Fix nick autocomplete --- client/js/lounge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 539eb975..2d9eac34 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -1380,7 +1380,7 @@ $(function() { } function completeNicks(word) { - const users = chat.find(".active").find(".users"); + const users = chat.find(".active").find(".names-original"); const words = users.data("nicks"); return $.grep( From 927c40739e7bb82aa2066b2f008abb287e93afc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Wed, 26 Apr 2017 23:30:51 +0200 Subject: [PATCH 0347/3926] Fix network layout displaying the scrollbar incorrectly This was introduced by https://github.com/thelounge/lounge/pull/856/files#diff-97db1f70168fb5f12457b238ff6052b5R773 (and L794-798): a right position got introduced for all channel containers, but default position for other types of containers was absent before this script. --- client/css/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/client/css/style.css b/client/css/style.css index 490439ce..8f079476 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -762,6 +762,7 @@ kbd { #chat .chat { bottom: 0; left: 0; + right: 0; overflow: auto; -webkit-overflow-scrolling: touch; position: absolute; From b9ead20fc7bef1149370e8f15e318bda65a48ba2 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Thu, 27 Apr 2017 19:39:32 +0000 Subject: [PATCH 0348/3926] fix(package): update fs-extra to version 3.0.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e7c54e1e..00ab14c3 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "event-stream": "3.3.4", "express": "4.15.2", "express-handlebars": "3.0.0", - "fs-extra": "2.1.2", + "fs-extra": "3.0.0", "irc-framework": "2.8.0", "ldapjs": "1.0.1", "lodash": "4.17.4", From d6d7df62fe02f0933ea187767f2c8284ce4f4b40 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 28 Apr 2017 18:58:14 +0300 Subject: [PATCH 0349/3926] Fix away message disappearing Closes #1102 --- src/client.js | 8 ++++---- src/clientManager.js | 1 + src/plugins/inputs/away.js | 2 ++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/client.js b/src/client.js index aa3b7045..554f2b6c 100644 --- a/src/client.js +++ b/src/client.js @@ -66,8 +66,9 @@ function Client(manager, name, config) { if (typeof config !== "object") { config = {}; } + _.merge(this, { - awayMessage: "", + awayMessage: config.awayMessage || "", lastActiveChannel: -1, attachedClients: {}, config: config, @@ -482,8 +483,6 @@ Client.prototype.clientAttach = function(socketId) { var client = this; var save = false; - client.attachedClients[socketId] = client.lastActiveChannel; - if (client.awayMessage && _.size(client.attachedClients) === 0) { client.networks.forEach(function(network) { // Only remove away on client attachment if @@ -494,6 +493,8 @@ Client.prototype.clientAttach = function(socketId) { }); } + client.attachedClients[socketId] = client.lastActiveChannel; + // Update old networks to store ip and hostmask client.networks.forEach(network => { if (!network.ip) { @@ -539,7 +540,6 @@ Client.prototype.save = _.debounce(function SaveClient() { const client = this; let json = {}; - json.awayMessage = client.awayMessage; json.networks = this.networks.map(n => n.export()); client.manager.updateUser(client.name, json); }, 1000, {maxWait: 10000}); diff --git a/src/clientManager.js b/src/clientManager.js index a0f94f0f..6e73af19 100644 --- a/src/clientManager.js +++ b/src/clientManager.js @@ -104,6 +104,7 @@ ClientManager.prototype.addUser = function(name, password, enableLog) { user: name, password: password || "", log: enableLog, + awayMessage: "", networks: [] }; fs.writeFileSync( diff --git a/src/plugins/inputs/away.js b/src/plugins/inputs/away.js index 34c58596..aa4d604b 100644 --- a/src/plugins/inputs/away.js +++ b/src/plugins/inputs/away.js @@ -14,4 +14,6 @@ exports.input = function(network, chan, cmd, args) { } network.awayMessage = reason; + + this.save(); }; From a3810dea06f1559aca2f155dc95e3f08b9afcae5 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 28 Apr 2017 21:58:00 +0300 Subject: [PATCH 0350/3926] Fix chat layout on small devices when users list is hidden Fixes #1092 --- client/css/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/css/style.css b/client/css/style.css index 8f079476..af9b5605 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1856,6 +1856,10 @@ kbd { display: block; } + #chat .channel .chat { + right: 0; + } + #chat .sidebar { right: -180px; } From 70655120cb2a51756c240c46ac0206aa28c81631 Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Fri, 28 Apr 2017 14:45:18 -0700 Subject: [PATCH 0351/3926] Add ctcp to constants, adds to auto-complete --- client/js/constants.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/js/constants.js b/client/js/constants.js index c9587709..e1990158 100644 --- a/client/js/constants.js +++ b/client/js/constants.js @@ -7,6 +7,7 @@ const commands = [ "/banlist", "/close", "/connect", + "/ctcp", "/deop", "/devoice", "/disconnect", From 0b645d54c68d719a0ca67133a70653da099a4ead Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 28 Apr 2017 08:28:45 +0300 Subject: [PATCH 0352/3926] Add support for 0x04 hex colors Ref: https://modern.ircdocs.horse/formatting.html#hex-color --- .../handlebars/ircmessageparser/parseStyle.js | 34 ++++- client/js/libs/handlebars/parse.js | 18 ++- .../handlebars/ircmessageparser/parseStyle.js | 119 ++++++++++++++++++ 3 files changed, 167 insertions(+), 4 deletions(-) diff --git a/client/js/libs/handlebars/ircmessageparser/parseStyle.js b/client/js/libs/handlebars/ircmessageparser/parseStyle.js index d23d5bd6..a0f3fd08 100644 --- a/client/js/libs/handlebars/ircmessageparser/parseStyle.js +++ b/client/js/libs/handlebars/ircmessageparser/parseStyle.js @@ -3,6 +3,7 @@ // Styling control codes const BOLD = "\x02"; const COLOR = "\x03"; +const HEX_COLOR = "\x04"; const RESET = "\x0f"; const REVERSE = "\x16"; const ITALIC = "\x1d"; @@ -12,6 +13,9 @@ const UNDERLINE = "\x1f"; // integers, `XX` is the text color and `YY` is an optional background color. const colorRx = /^(\d{1,2})(?:,(\d{1,2}))?/; +// 6-char Hex color code matcher +const hexColorRx = /^([0-9a-f]{6})(?:,([0-9a-f]{6}))?/i; + // Represents all other control codes that to be ignored/filtered from the text const controlCodesRx = /[\u0000-\u001F]/g; @@ -26,12 +30,14 @@ function parseStyle(text) { // At any given time, these carry style information since last time a styling // control code was met. - let colorCodes, bold, textColor, bgColor, reverse, italic, underline; + let colorCodes, bold, textColor, bgColor, hexColor, hexBgColor, reverse, italic, underline; const resetStyle = () => { bold = false; textColor = undefined; bgColor = undefined; + hexColor = undefined; + hexBgColor = undefined; reverse = false; italic = false; underline = false; @@ -57,6 +63,8 @@ function parseStyle(text) { bold, textColor, bgColor, + hexColor, + hexBgColor, reverse, italic, underline, @@ -113,6 +121,28 @@ function parseStyle(text) { } break; + case HEX_COLOR: + emitFragment(); + + colorCodes = text.slice(position + 1).match(hexColorRx); + + if (colorCodes) { + hexColor = colorCodes[1].toUpperCase(); + if (colorCodes[2]) { + hexBgColor = colorCodes[2].toUpperCase(); + } + // Color code length is > 1, so bump the current position cursor by as + // much (and reset the start cursor for the current text block as well) + position += colorCodes[0].length; + start = position + 1; + } else { + // If no color codes were found, toggles back to no colors (like BOLD). + hexColor = undefined; + hexBgColor = undefined; + } + + break; + case REVERSE: emitFragment(); reverse = !reverse; @@ -139,7 +169,7 @@ function parseStyle(text) { return result; } -const properties = ["bold", "textColor", "bgColor", "italic", "underline", "reverse"]; +const properties = ["bold", "textColor", "bgColor", "hexColor", "hexBgColor", "italic", "underline", "reverse"]; function prepare(text) { return parseStyle(text) diff --git a/client/js/libs/handlebars/parse.js b/client/js/libs/handlebars/parse.js index 915a432c..426cbe4f 100644 --- a/client/js/libs/handlebars/parse.js +++ b/client/js/libs/handlebars/parse.js @@ -24,10 +24,24 @@ function createFragment(fragment) { if (fragment.underline) { classes.push("irc-underline"); } + + let attributes = classes.length ? ` class="${classes.join(" ")}"` : ""; const escapedText = Handlebars.Utils.escapeExpression(fragment.text); - if (classes.length) { - return `${escapedText}`; + + if (fragment.hexColor) { + attributes += ` style="color:#${fragment.hexColor}`; + + if (fragment.hexBgColor) { + attributes += `;background-color:#${fragment.hexBgColor}`; + } + + attributes += "\""; } + + if (attributes.length) { + return `${escapedText}`; + } + return escapedText; } diff --git a/test/client/js/libs/handlebars/ircmessageparser/parseStyle.js b/test/client/js/libs/handlebars/ircmessageparser/parseStyle.js index 6af289c4..e978cbcc 100644 --- a/test/client/js/libs/handlebars/ircmessageparser/parseStyle.js +++ b/test/client/js/libs/handlebars/ircmessageparser/parseStyle.js @@ -10,6 +10,8 @@ describe("parseStyle", () => { bold: false, textColor: undefined, bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, reverse: false, italic: false, underline: false, @@ -30,6 +32,8 @@ describe("parseStyle", () => { bold: true, textColor: undefined, bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, reverse: false, italic: false, underline: false, @@ -50,6 +54,8 @@ describe("parseStyle", () => { bold: false, textColor: 8, bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, reverse: false, italic: false, underline: false, @@ -69,6 +75,8 @@ describe("parseStyle", () => { const expected = [{ textColor: 4, bgColor: 8, + hexColor: undefined, + hexBgColor: undefined, bold: false, reverse: false, italic: false, @@ -90,6 +98,8 @@ describe("parseStyle", () => { bold: false, textColor: undefined, bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, reverse: false, italic: true, underline: false, @@ -104,12 +114,101 @@ describe("parseStyle", () => { expect(actual).to.deep.equal(expected); }); + it("should parse hex colors", () => { + const input = "test \x04FFFFFFnice \x02\x04RES006 \x0303,04\x04ff00FFcolored\x04eeeaFF,001122 background\x04\x03\x02?"; + const expected = [{ + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: "test ", + + start: 0, + end: 5 + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: "FFFFFF", + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: "nice ", + + start: 5, + end: 10 + }, { + bold: true, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: "RES006 ", + + start: 10, + end: 17 + }, { + bold: true, + textColor: 3, + bgColor: 4, + hexColor: "FF00FF", + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: "colored", + + start: 17, + end: 24 + }, { + bold: true, + textColor: 3, + bgColor: 4, + hexColor: "EEEAFF", + hexBgColor: "001122", + reverse: false, + italic: false, + underline: false, + text: " background", + + start: 24, + end: 35 + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + text: "?", + + start: 35, + end: 36 + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + it("should carry state corretly forward", () => { const input = "\x02bold\x038yellow\x02nonBold\x03default"; const expected = [{ bold: true, textColor: undefined, bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, reverse: false, italic: false, underline: false, @@ -121,6 +220,8 @@ describe("parseStyle", () => { bold: true, textColor: 8, bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, reverse: false, italic: false, underline: false, @@ -132,6 +233,8 @@ describe("parseStyle", () => { bold: false, textColor: 8, bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, reverse: false, italic: false, underline: false, @@ -143,6 +246,8 @@ describe("parseStyle", () => { bold: false, textColor: undefined, bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, reverse: false, italic: false, underline: false, @@ -163,6 +268,8 @@ describe("parseStyle", () => { bold: true, textColor: undefined, bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, reverse: false, italic: false, underline: false, @@ -174,6 +281,8 @@ describe("parseStyle", () => { bold: false, textColor: undefined, bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, reverse: false, italic: false, underline: false, @@ -185,6 +294,8 @@ describe("parseStyle", () => { bold: true, textColor: undefined, bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, reverse: false, italic: false, underline: false, @@ -205,6 +316,8 @@ describe("parseStyle", () => { bold: true, textColor: 4, bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, reverse: true, italic: true, underline: true, @@ -216,6 +329,8 @@ describe("parseStyle", () => { bold: false, textColor: undefined, bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, reverse: false, italic: false, underline: false, @@ -236,6 +351,8 @@ describe("parseStyle", () => { bold: false, textColor: undefined, bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, reverse: false, italic: false, underline: false, @@ -258,6 +375,8 @@ describe("parseStyle", () => { bold: false, textColor: 12, bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, reverse: false, italic: false, underline: false, From 14ea84988f84dc66c1f01ba42586ca21d5c35e35 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sat, 29 Apr 2017 07:07:10 +0000 Subject: [PATCH 0353/3926] chore(package): update nyc to version 10.3.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e7c54e1e..ca655e98 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "mocha": "3.3.0", "mousetrap": "1.6.1", "npm-run-all": "4.0.2", - "nyc": "10.2.0", + "nyc": "10.3.0", "socket.io-client": "1.7.3", "stylelint": "7.10.1", "urijs": "1.18.10", From dd48ba4e87dbcf3d5bbc28dc29b95fd3fe367dae Mon Sep 17 00:00:00 2001 From: PolarizedIons Date: Sat, 29 Apr 2017 13:17:21 +0200 Subject: [PATCH 0354/3926] Fix channel sorting messing up the order --- src/client.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.js b/src/client.js index aa3b7045..8f0d06ba 100644 --- a/src/client.js +++ b/src/client.js @@ -422,7 +422,7 @@ Client.prototype.sort = function(data) { switch (data.type) { case "networks": this.networks.sort((a, b) => { - return order.indexOf(a.id) > order.indexOf(b.id); + return order.indexOf(a.id) - order.indexOf(b.id); }); // Sync order to connected clients @@ -437,7 +437,7 @@ Client.prototype.sort = function(data) { } network.channels.sort((a, b) => { - return order.indexOf(a.id) > order.indexOf(b.id); + return order.indexOf(a.id) - order.indexOf(b.id); }); // Sync order to connected clients From 29cd9b80ef2173e980dbe1ad624c7394e4cc5490 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sat, 29 Apr 2017 20:56:52 +0000 Subject: [PATCH 0355/3926] chore(package): update handlebars to version 4.0.7 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0560e6d7..1c9d5b87 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "eslint": "3.19.0", "font-awesome": "4.7.0", "fuzzy": "0.1.3", - "handlebars": "4.0.6", + "handlebars": "4.0.7", "handlebars-loader": "1.5.0", "jquery": "3.2.1", "jquery-textcomplete": "1.8.0", From f7b7248ff7b0f606f409106bc09b4b086387a401 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Thu, 27 Apr 2017 21:32:58 +0300 Subject: [PATCH 0356/3926] Fix nick autocomplete Fixes #1119. --- client/js/lounge.js | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 2d9eac34..b11b2fc7 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -300,8 +300,9 @@ $(function() { var nicks = chan.find(".users").data("nicks"); if (nicks) { var find = nicks.indexOf(data.msg.from); - if (find !== -1 && typeof move === "function") { - move(nicks, find, 0); + if (find !== -1) { + nicks.splice(find, 1); + nicks.unshift(data.msg.from); } } } @@ -389,9 +390,9 @@ $(function() { .attr("placeholder", nicks.length + " " + (nicks.length === 1 ? "user" : "users")); users + .data("nicks", nicks) .find(".names-original") - .html(templates.user(data)) - .data("nicks", nicks); + .html(templates.user(data)); // Refresh user search if (search.val().length) { @@ -1380,7 +1381,13 @@ $(function() { } function completeNicks(word) { - const users = chat.find(".active").find(".names-original"); + const users = chat.find(".active .users"); + + // Lobbies and private chats do not have an user list + if (!users.length) { + return []; + } + const words = users.data("nicks"); return $.grep( @@ -1534,17 +1541,6 @@ $(function() { $("#nick-value").text(nick); } - function move(array, old_index, new_index) { - if (new_index >= array.length) { - var k = new_index - array.length; - while ((k--) + 1) { - this.push(undefined); - } - } - array.splice(new_index, 0, array.splice(old_index, 1)[0]); - return array; - } - function toggleNotificationMarkers(newState) { // Toggles the favicon to red when there are unread notifications if (favicon.data("toggled") !== newState) { From 381ea326f4a2d3588854485f17025258e1600ee7 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sun, 30 Apr 2017 15:07:09 +0300 Subject: [PATCH 0357/3926] Disable tabindex on userlist search input Fixes #1036. --- client/views/chat.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/views/chat.tpl b/client/views/chat.tpl index 898dd75b..55924b6f 100644 --- a/client/views/chat.tpl +++ b/client/views/chat.tpl @@ -23,7 +23,7 @@
+ {{/equal}} + {{/each}} From 97ed29e1ddacc8fb68b4f38887dcb5c991b3e541 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 6 Mar 2018 05:06:03 +0000 Subject: [PATCH 1213/3926] chore(package): update mocha to version 5.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c1b76e36..79648c82 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "istanbul-instrumenter-loader": "3.0.0", "jquery": "3.3.1", "jquery-ui": "1.12.1", - "mocha": "5.0.1", + "mocha": "5.0.2", "mocha-loader": "1.1.3", "mocha-webpack": "1.0.1", "mousetrap": "1.6.1", From 9f503b6de904e7d0a6b8d1e7b04a0c2ed20db120 Mon Sep 17 00:00:00 2001 From: greenkeeperio-bot Date: Tue, 6 Mar 2018 05:08:51 +0000 Subject: [PATCH 1214/3926] chore(package): update lockfile https://npm.im/greenkeeper-lockfile --- yarn.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index 39698452..4329e7d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -995,9 +995,9 @@ brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" -browser-stdout@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" browserify-aes@^1.0.0, browserify-aes@^1.0.4: version "1.1.1" @@ -4381,11 +4381,11 @@ mocha-webpack@1.0.1: toposort "^1.0.0" yargs "^4.8.0" -mocha@5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.0.1.tgz#759b62c836b0732382a62b6b1fb245ec1bc943ac" +mocha@5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.0.2.tgz#caab0b813575141c5690c37d53c894b605ae60b5" dependencies: - browser-stdout "1.3.0" + browser-stdout "1.3.1" commander "2.11.0" debug "3.1.0" diff "3.3.1" From 11eedc3ea14a832b6885b1916b63926b77a12ae8 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Tue, 6 Mar 2018 10:46:52 +0200 Subject: [PATCH 1215/3926] Update chalk and lock file to latest version --- package.json | 2 +- yarn.lock | 238 ++++++++++++++++++++++++++------------------------- 2 files changed, 121 insertions(+), 119 deletions(-) diff --git a/package.json b/package.json index 79648c82..8ad49e35 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "bcryptjs": "2.4.3", - "chalk": "2.3.1", + "chalk": "2.3.2", "cheerio": "0.22.0", "commander": "2.14.1", "express": "4.16.2", diff --git a/yarn.lock b/yarn.lock index 4329e7d3..c9d925d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11,10 +11,10 @@ abstract-logging@^1.0.0: resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-1.0.0.tgz#8b7deafd310559bc28f77724dd1bb30177278c1b" accepts@~1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" + version "1.3.5" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" dependencies: - mime-types "~2.1.16" + mime-types "~2.1.18" negotiator "0.6.1" acorn-dynamic-import@^2.0.0: @@ -37,9 +37,9 @@ acorn@^4.0.3: version "4.0.13" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" -acorn@^5.0.0, acorn@^5.4.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102" +acorn@^5.0.0, acorn@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.0.tgz#1abb587fbf051f94e3de20e6b26ef910b1828298" after@0.8.2: version "0.8.2" @@ -49,7 +49,7 @@ ajv-keywords@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" -ajv-keywords@^3.1.0: +ajv-keywords@^3.0.0, ajv-keywords@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be" @@ -69,9 +69,9 @@ ajv@^5.0.0, ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" -ajv@^6.1.0: - version "6.1.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.1.1.tgz#978d597fbc2b7d0e5a5c3ddeb149a682f2abfa0e" +ajv@^6.0.1, ajv@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.2.1.tgz#28a6abc493a2abe0fb4c8507acaedb43fa550671" dependencies: fast-deep-equal "^1.0.0" fast-json-stable-stringify "^2.0.0" @@ -113,9 +113,9 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -ansi-styles@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" dependencies: color-convert "^1.9.0" @@ -327,14 +327,14 @@ autoprefixer@^6.3.1: postcss-value-parser "^3.2.3" autoprefixer@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-8.0.0.tgz#c19e480f061013127c373df0b01cf46919943f74" + version "8.1.0" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-8.1.0.tgz#374cf35be1c0e8fce97408d876f95f66f5cb4641" dependencies: - browserslist "^3.0.0" - caniuse-lite "^1.0.30000808" + browserslist "^3.1.1" + caniuse-lite "^1.0.30000810" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^6.0.17" + postcss "^6.0.19" postcss-value-parser "^3.2.3" aws-sign2@~0.6.0: @@ -1065,7 +1065,7 @@ browserslist@^2.1.2: caniuse-lite "^1.0.30000792" electron-to-chromium "^1.3.30" -browserslist@^3.0.0: +browserslist@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.1.1.tgz#d380fc048bc3a33e60fb87dc135110ebaaa6320a" dependencies: @@ -1199,12 +1199,12 @@ caniuse-api@^1.5.2: lodash.uniq "^4.5.0" caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: - version "1.0.30000810" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000810.tgz#bd25830c41efab64339a2e381f49677343c84509" + version "1.0.30000813" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000813.tgz#e0a1c603f8880ad787b2a35652b2733f32a5e29a" -caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000808, caniuse-lite@^1.0.30000809: - version "1.0.30000810" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000810.tgz#47585fffce0e9f3593a6feea4673b945424351d9" +caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000809, caniuse-lite@^1.0.30000810: + version "1.0.30000813" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000813.tgz#7b25e27fdfb8d133f3c932b01f77452140fcc6c9" capture-stack-trace@^1.0.0: version "1.0.0" @@ -1236,13 +1236,13 @@ chai@4.1.2: pathval "^1.0.0" type-detect "^4.0.0" -chalk@2.3.1, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796" +chalk@2.3.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65" dependencies: - ansi-styles "^3.2.0" + ansi-styles "^3.2.1" escape-string-regexp "^1.0.5" - supports-color "^5.2.0" + supports-color "^5.3.0" chalk@^1.1.3: version "1.1.3" @@ -1299,7 +1299,7 @@ cheerio@0.22.0: lodash.reject "^4.4.0" lodash.some "^4.4.0" -chokidar@^1.6.1, chokidar@^1.7.0: +chokidar@^1.6.1: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" dependencies: @@ -1314,7 +1314,7 @@ chokidar@^1.6.1, chokidar@^1.7.0: optionalDependencies: fsevents "^1.0.0" -chokidar@^2.0.0: +chokidar@^2.0.0, chokidar@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.2.tgz#4dc65139eeb2714977735b6a35d06e97b494dfd7" dependencies: @@ -1521,8 +1521,8 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" concat-stream@^1.5.0, concat-stream@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + version "1.6.1" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.1.tgz#261b8f518301f1d834e36342b9fea095d2620a26" dependencies: inherits "^2.0.3" readable-stream "^2.2.2" @@ -1699,8 +1699,8 @@ css-color-names@0.0.4: resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" css-loader@^0.28.7: - version "0.28.9" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.9.tgz#68064b85f4e271d7ce4c48a58300928e535d1c95" + version "0.28.10" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.10.tgz#40282e79230f7bcb4e483efa631d670b735ebf42" dependencies: babel-code-frame "^6.26.0" css-selector-tokenizer "^0.7.0" @@ -2061,8 +2061,8 @@ duplexer@~0.1.1: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" duplexify@^3.4.2, duplexify@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.3.tgz#8b5818800df92fd0125b27ab896491912858243e" + version "3.5.4" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.4.tgz#4bb46c1796eabebeec4ca9a2e66b808cb7a3d8b4" dependencies: end-of-stream "^1.0.0" inherits "^2.0.1" @@ -2087,8 +2087,8 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.33: - version "1.3.33" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.33.tgz#bf00703d62a7c65238136578c352d6c5c042a545" + version "1.3.36" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.36.tgz#0eabf71a9ebea9013fb1cc35a390e068624f27e8" elliptic@^6.0.0: version "6.4.0" @@ -2325,10 +2325,10 @@ eslint@4.18.2: text-table "~0.2.0" espree@^3.5.2: - version "3.5.3" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.3.tgz#931e0af64e7fbbed26b050a29daad1fc64799fa6" + version "3.5.4" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" dependencies: - acorn "^5.4.0" + acorn "^5.5.0" acorn-jsx "^3.0.0" esprima@^2.6.0: @@ -2346,11 +2346,10 @@ esquery@^1.0.0: estraverse "^4.0.0" esrecurse@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" dependencies: estraverse "^4.1.0" - object-assign "^4.0.1" estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1: version "4.2.0" @@ -2542,8 +2541,8 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" fast-deep-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" fast-json-stable-stringify@^2.0.0: version "2.0.0" @@ -3744,8 +3743,8 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" js-yaml@^3.9.0, js-yaml@^3.9.1: - version "3.10.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" + version "3.11.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -3975,10 +3974,6 @@ lodash.defaults@^4.0.1: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" -lodash.endswith@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/lodash.endswith/-/lodash.endswith-4.2.1.tgz#fed59ac1738ed3e236edd7064ec456448b37bc09" - lodash.filter@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" @@ -3991,14 +3986,6 @@ lodash.foreach@^4.3.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" -lodash.isfunction@^3.0.8: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - lodash.map@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" @@ -4027,10 +4014,6 @@ lodash.some@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" -lodash.startswith@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/lodash.startswith/-/lodash.startswith-4.2.1.tgz#c598c4adce188a27e53145731cdc6c0e7177600c" - lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -4236,8 +4219,8 @@ micromatch@^2.1.5, micromatch@^2.3.11: regex-cache "^0.4.2" micromatch@^3.1.4: - version "3.1.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.8.tgz#5c8caa008de588eebb395e8c0ad12c128f25fff1" + version "3.1.9" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.9.tgz#15dc93175ae39e52e93087847096effc73efcf89" dependencies: arr-diff "^4.0.0" array-unique "^0.3.2" @@ -4268,7 +4251,7 @@ miller-rabin@^4.0.0: version "1.33.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" -mime-types@2.1.18, mime-types@^2.1.12, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7: +mime-types@2.1.18, mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7: version "2.1.18" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" dependencies: @@ -4435,8 +4418,8 @@ mute-stream@0.0.7, mute-stream@~0.0.4: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" nan@^2.3.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" + version "2.9.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.9.2.tgz#f564d75f5f8f36a6d9456cca7a6c4fe488ab7866" nanomatch@^1.2.9: version "1.2.9" @@ -4463,6 +4446,10 @@ negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" +neo-async@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.0.tgz#76b1c823130cca26acfbaccc8fbaf0a2fa33b18f" + node-fetch@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.0.0.tgz#982bba43ecd4f2922a29cc186a6bbb0bb73fcba6" @@ -4516,8 +4503,8 @@ node-pre-gyp@^0.6.39: tar-pack "^3.4.0" nodent-runtime@^3.0.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/nodent-runtime/-/nodent-runtime-3.2.0.tgz#8b79500a1274176d732b60284c7a7d10d9e44180" + version "3.2.1" + resolved "https://registry.yarnpkg.com/nodent-runtime/-/nodent-runtime-3.2.1.tgz#9e2755d85e39f764288f0d4752ebcfe3e541e00e" nopt@^4.0.1: version "4.0.1" @@ -5242,10 +5229,10 @@ postcss-sass@^0.3.0: postcss "^6.0.16" postcss-scss@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-1.0.3.tgz#4c00ab440fc1c994134e3d4e600c23341af6cd27" + version "1.0.4" + resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-1.0.4.tgz#6310fe1a15be418707a2cfd77f21dd4a06d1e09d" dependencies: - postcss "^6.0.15" + postcss "^6.0.19" postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2: version "2.2.3" @@ -5301,7 +5288,7 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0 source-map "^0.5.6" supports-color "^3.2.3" -postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.15, postcss@^6.0.16, postcss@^6.0.17, postcss@^6.0.6, postcss@^6.0.8: +postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.16, postcss@^6.0.19, postcss@^6.0.6, postcss@^6.0.8: version "6.0.19" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.19.tgz#76a78386f670b9d9494a655bf23ac012effd1555" dependencies: @@ -5550,8 +5537,8 @@ read@1.0.7: mute-stream "~0.0.4" "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3: - version "2.3.4" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071" + version "2.3.5" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d" dependencies: core-util-is "~1.0.0" inherits "~2.0.3" @@ -5620,7 +5607,7 @@ regex-cache@^0.4.2: dependencies: is-equal-shallow "^0.1.3" -regex-not@^1.0.0: +regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" dependencies: @@ -6123,11 +6110,11 @@ socket.io-client@2.0.4: to-array "0.1.4" socket.io-parser@~3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.1.2.tgz#dbc2282151fc4faebbe40aeedc0772eba619f7f2" + version "3.1.3" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.1.3.tgz#ed2da5ee79f10955036e3da413bfd7f1e4d86c8e" dependencies: component-emitter "1.2.1" - debug "~2.6.4" + debug "~3.1.0" has-binary2 "~1.0.2" isarray "2.0.1" @@ -6220,19 +6207,27 @@ spawn-wrap@^1.4.2: signal-exit "^3.0.2" which "^1.3.0" -spdx-correct@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" +spdx-correct@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" dependencies: - spdx-license-ids "^1.0.2" + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" -spdx-expression-parse@~1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" +spdx-exceptions@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9" -spdx-license-ids@^1.0.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87" spdy-transport@^2.0.18: version "2.0.20" @@ -6530,9 +6525,9 @@ supports-color@^4.2.1: dependencies: has-flag "^2.0.0" -supports-color@^5.1.0, supports-color@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a" +supports-color@^5.1.0, supports-color@^5.2.0, supports-color@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.3.0.tgz#5b24ac15db80fa927cf5227a4a33fd3c4c7676c0" dependencies: has-flag "^3.0.0" @@ -6552,7 +6547,7 @@ svgo@^0.7.0: sax "~1.2.1" whet.extend "~0.9.9" -table@4.0.2, table@^4.0.1: +table@4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" dependencies: @@ -6563,6 +6558,17 @@ table@4.0.2, table@^4.0.1: slice-ansi "1.0.0" string-width "^2.1.1" +table@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc" + dependencies: + ajv "^6.0.1" + ajv-keywords "^3.0.0" + chalk "^2.1.0" + lodash "^4.17.4" + slice-ansi "1.0.0" + string-width "^2.1.1" + tapable@^0.2.7: version "0.2.8" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22" @@ -6689,20 +6695,21 @@ to-regex-range@^2.1.0: repeat-string "^1.6.1" to-regex@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.1.tgz#15358bee4a2c83bd76377ba1dc049d0f18837aae" + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" dependencies: - define-property "^0.2.5" - extend-shallow "^2.0.1" - regex-not "^1.0.0" + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" toposort@^1.0.0: version "1.0.6" resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.6.tgz#c31748e55d210effc00fdcdc7d6e68d7d7bb9cec" tough-cookie@~2.3.0, tough-cookie@~2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" + version "2.3.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" dependencies: punycode "^1.4.1" @@ -6908,13 +6915,8 @@ unzip-response@^2.0.1: resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" upath@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.2.tgz#80aaae5395abc5fd402933ae2f58694f0860204c" - dependencies: - lodash.endswith "^4.2.1" - lodash.isfunction "^3.0.8" - lodash.isstring "^4.0.1" - lodash.startswith "^4.2.1" + version "1.0.4" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.4.tgz#ee2321ba0a786c50973db043a50b7bcba822361d" urijs@1.19.1: version "1.19.1" @@ -6986,11 +6988,11 @@ uws@~9.14.0: resolved "https://registry.yarnpkg.com/uws/-/uws-9.14.0.tgz#fac8386befc33a7a3705cbd58dc47b430ca4dd95" validate-npm-package-license@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + version "3.0.3" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338" dependencies: - spdx-correct "~1.0.0" - spdx-expression-parse "~1.0.0" + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" vary@~1.1.2: version "1.1.2" @@ -7046,12 +7048,12 @@ vm-browserify@0.0.4: indexof "0.0.1" watchpack@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac" + version "1.5.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.5.0.tgz#231e783af830a22f8966f65c4c4bacc814072eed" dependencies: - async "^2.1.2" - chokidar "^1.7.0" + chokidar "^2.0.2" graceful-fs "^4.1.2" + neo-async "^2.5.0" wbuf@^1.1.0, wbuf@^1.7.2: version "1.7.2" From 91889432618cfd4b18021d4b8e67e3eae5df57c7 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Mon, 5 Mar 2018 20:11:41 +0200 Subject: [PATCH 1216/3926] Fix rejectUnauthorized --- client/views/windows/connect.tpl | 4 ++-- src/client.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/views/windows/connect.tpl b/client/views/windows/connect.tpl index eb1f0302..56ab219d 100644 --- a/client/views/windows/connect.tpl +++ b/client/views/windows/connect.tpl @@ -45,8 +45,8 @@
diff --git a/src/client.js b/src/client.js index 58f77c9d..f69f6991 100644 --- a/src/client.js +++ b/src/client.js @@ -178,7 +178,7 @@ Client.prototype.connect = function(args) { host: args.host || "", port: parseInt(args.port, 10) || (args.tls ? 6697 : 6667), tls: !!args.tls, - rejectUnauthorized: !args.allowUnauthorized, + rejectUnauthorized: !!args.rejectUnauthorized, password: args.password, username: args.username || nick.replace(/[^a-zA-Z0-9]/g, ""), realname: args.realname || "The Lounge User", From 501730f2cad1a3f0ca57d75833cc88e5a18e066b Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Tue, 6 Mar 2018 15:11:42 +0200 Subject: [PATCH 1217/3926] Fix default ecdh curve for better compatibility --- src/plugins/irc-events/link.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/plugins/irc-events/link.js b/src/plugins/irc-events/link.js index 02aaf55c..50cfeedf 100644 --- a/src/plugins/irc-events/link.js +++ b/src/plugins/irc-events/link.js @@ -11,6 +11,16 @@ const storage = require("../storage"); process.setMaxListeners(0); +// Fix ECDH curve client compatibility in Node v8/v9 +// This is fixed in Node 10, but The Lounge supports LTS versions +// https://github.com/nodejs/node/issues/16196 +// https://github.com/nodejs/node/pull/16853 +const tls = require("tls"); + +if (tls.DEFAULT_ECDH_CURVE === "prime256v1") { + tls.DEFAULT_ECDH_CURVE = "auto"; +} + module.exports = function(client, chan, msg) { if (!Helper.config.prefetch) { return; From 8b417fe97a348bd720c1d127ebddacde9e9f31c3 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Tue, 6 Mar 2018 20:01:39 +0200 Subject: [PATCH 1218/3926] Fix chat and userlist not scrolling --- client/css/style.css | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 5c49ce01..c3f0216a 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -257,8 +257,16 @@ kbd { } #viewport .lt::before { content: "\f0c9"; /* http://fontawesome.io/icon/bars/ */ } -#viewport .rt::before { content: "\f0c0"; /* http://fontawesome.io/icon/users/ */ } -#chat button.menu::before { content: "\f142"; /* http://fontawesome.io/icon/ellipsis-v/ */ } + +#viewport .rt::before { + content: "\f0c0"; /* http://fontawesome.io/icon/users/ */ + line-height: 36px; /* fix alignment in Microsoft Edge */ +} + +#chat button.menu::before { + content: "\f142"; /* http://fontawesome.io/icon/ellipsis-v/ */ + line-height: 36px; /* fix alignment in Microsoft Edge */ +} .context-menu-join::before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ } .context-menu-user::before { content: "\f007"; /* http://fontawesome.io/icon/user/ */ } @@ -494,10 +502,6 @@ kbd { opacity: 1; } -#viewport .rt-tooltip { - float: right; -} - #viewport.rt #chat .userlist { display: none; } @@ -1020,6 +1024,8 @@ button.collapse-network:first-child:nth-last-child(3) { #chat .chat-content { display: flex; flex-grow: 1; + overflow: hidden; + contain: strict; } #chat .chat { From 827310a645568fbd3a3f7d3a4cd7c328e57a2f83 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Wed, 7 Mar 2018 08:44:22 +0200 Subject: [PATCH 1219/3926] Apply ECDH curve fix only on affected version Fixes #2162 --- src/plugins/irc-events/link.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/irc-events/link.js b/src/plugins/irc-events/link.js index 50cfeedf..01599c69 100644 --- a/src/plugins/irc-events/link.js +++ b/src/plugins/irc-events/link.js @@ -15,9 +15,11 @@ process.setMaxListeners(0); // This is fixed in Node 10, but The Lounge supports LTS versions // https://github.com/nodejs/node/issues/16196 // https://github.com/nodejs/node/pull/16853 +// https://github.com/nodejs/node/pull/15206 const tls = require("tls"); +const semver = require("semver"); -if (tls.DEFAULT_ECDH_CURVE === "prime256v1") { +if (semver.gte(process.version, "8.6.0") && tls.DEFAULT_ECDH_CURVE === "prime256v1") { tls.DEFAULT_ECDH_CURVE = "auto"; } From 6fa48d3acfd3081c03a825f51ffdf266dbcc5c54 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Wed, 7 Mar 2018 09:13:06 +0200 Subject: [PATCH 1220/3926] Remove setMaxListeners Reverts 2cee0ea6ef5ee51de0190332f976934b55bbc8e4 as this no longer causes the EventEmitter warning due to `maxRedirects` being set to 5 on our end. Ref: https://github.com/request/request/issues/311#issuecomment-153507416 --- src/plugins/irc-events/link.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/plugins/irc-events/link.js b/src/plugins/irc-events/link.js index 50cfeedf..f46ea1f3 100644 --- a/src/plugins/irc-events/link.js +++ b/src/plugins/irc-events/link.js @@ -9,8 +9,6 @@ const cleanIrcMessage = require("../../../client/js/libs/handlebars/ircmessagepa const findLinks = require("../../../client/js/libs/handlebars/ircmessageparser/findLinks"); const storage = require("../storage"); -process.setMaxListeners(0); - // Fix ECDH curve client compatibility in Node v8/v9 // This is fixed in Node 10, but The Lounge supports LTS versions // https://github.com/nodejs/node/issues/16196 From bb066ecb027da53462fb69143a4aadb4bb3ea42b Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Tue, 6 Mar 2018 13:29:28 +0200 Subject: [PATCH 1221/3926] Do not listen to touch events until client is initialized Fixes #2148 --- client/js/libs/slideout.js | 103 -------------------------------- client/js/lounge.js | 13 ++-- client/js/slideout.js | 99 ++++++++++++++++++++++++++++++ client/js/socket-events/init.js | 3 + 4 files changed, 108 insertions(+), 110 deletions(-) delete mode 100644 client/js/libs/slideout.js create mode 100644 client/js/slideout.js diff --git a/client/js/libs/slideout.js b/client/js/libs/slideout.js deleted file mode 100644 index 5d833b5d..00000000 --- a/client/js/libs/slideout.js +++ /dev/null @@ -1,103 +0,0 @@ -"use strict"; - -/** - * Simple slideout menu implementation. - */ -module.exports = function slideoutMenu(viewport, menu) { - let touchStartPos = null; - let touchCurPos = null; - let touchStartTime = 0; - let menuWidth = 0; - let menuIsOpen = false; - let menuIsMoving = false; - - function toggleMenu(state) { - menuIsOpen = state; - viewport.classList.toggle("menu-open", state); - } - - function disableSlideout() { - viewport.removeEventListener("ontouchstart", onTouchStart); - } - - function onTouchStart(e) { - if (e.touches.length !== 1) { - onTouchEnd(); - return; - } - - const touch = e.touches.item(0); - viewport.classList.toggle("menu-dragging", true); - - menuWidth = parseFloat(window.getComputedStyle(menu).width); - - if ((!menuIsOpen && touch.screenX < 50) || (menuIsOpen && touch.screenX > menuWidth)) { - touchStartPos = touch; - touchCurPos = touch; - touchStartTime = Date.now(); - - viewport.addEventListener("touchmove", onTouchMove); - viewport.addEventListener("touchend", onTouchEnd, {passive: true}); - } - } - - function onTouchMove(e) { - const touch = touchCurPos = e.touches.item(0); - let setX = touch.screenX - touchStartPos.screenX; - - if (Math.abs(setX > 30)) { - menuIsMoving = true; - } - - if (!menuIsMoving && Math.abs(touch.screenY - touchStartPos.screenY) > 30) { - onTouchEnd(); - return; - } - - if (menuIsOpen) { - setX += menuWidth; - } - - if (setX > menuWidth) { - setX = menuWidth; - } else if (setX < 0) { - setX = 0; - } - - viewport.style.transform = "translate3d(" + setX + "px, 0, 0)"; - - if (menuIsMoving) { - e.preventDefault(); - e.stopPropagation(); - } - } - - function onTouchEnd() { - const diff = touchCurPos.screenX - touchStartPos.screenX; - const absDiff = Math.abs(diff); - - if (absDiff > menuWidth / 2 || Date.now() - touchStartTime < 180 && absDiff > 50) { - toggleMenu(diff > 0); - } - - viewport.removeEventListener("touchmove", onTouchMove); - viewport.removeEventListener("touchend", onTouchEnd); - viewport.classList.toggle("menu-dragging", false); - viewport.style.transform = null; - - touchStartPos = null; - touchCurPos = null; - touchStartTime = 0; - menuIsMoving = false; - } - - viewport.addEventListener("touchstart", onTouchStart, {passive: true}); - - return { - disable: disableSlideout, - toggle: toggleMenu, - isOpen: function() { - return menuIsOpen; - }, - }; -}; diff --git a/client/js/lounge.js b/client/js/lounge.js index 0cc67053..22560ece 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -9,7 +9,7 @@ const URI = require("urijs"); // our libraries require("./libs/jquery/inputhistory"); require("./libs/jquery/stickyscroll"); -const slideoutMenu = require("./libs/slideout"); +const slideoutMenu = require("./slideout"); const templates = require("../views"); const socket = require("./socket"); const render = require("./render"); @@ -29,15 +29,14 @@ $(function() { $(document.body).data("app-name", document.title); const viewport = $("#viewport"); - const sidebarSlide = slideoutMenu(viewport[0], sidebar[0]); const contextMenuContainer = $("#context-menu-container"); const contextMenu = $("#context-menu"); $("#main").on("click", function(e) { - if ($(e.target).is(".lt")) { - sidebarSlide.toggle(!sidebarSlide.isOpen()); - } else if (sidebarSlide.isOpen()) { - sidebarSlide.toggle(false); + const isOpen = slideoutMenu.isOpen(); + + if (isOpen || $(e.target).is(".lt")) { + slideoutMenu.toggle(!isOpen); } }); @@ -376,7 +375,7 @@ $(function() { utils.toggleNotificationMarkers(false); } - sidebarSlide.toggle(false); + slideoutMenu.toggle(false); } const lastActive = $("#windows > .active"); diff --git a/client/js/slideout.js b/client/js/slideout.js new file mode 100644 index 00000000..d9244e8f --- /dev/null +++ b/client/js/slideout.js @@ -0,0 +1,99 @@ +"use strict"; + +const viewport = document.getElementById("viewport"); +const menu = document.getElementById("sidebar"); + +let touchStartPos = null; +let touchCurPos = null; +let touchStartTime = 0; +let menuWidth = 0; +let menuIsOpen = false; +let menuIsMoving = false; + +class SlideoutMenu { + static enable() { + viewport.addEventListener("touchstart", onTouchStart, {passive: true}); + } + + static toggle(state) { + menuIsOpen = state; + viewport.classList.toggle("menu-open", state); + } + + static isOpen() { + return menuIsOpen; + } +} + +function onTouchStart(e) { + if (e.touches.length !== 1) { + onTouchEnd(); + return; + } + + const touch = e.touches.item(0); + + menuWidth = parseFloat(window.getComputedStyle(menu).width); + + if ((!menuIsOpen && touch.screenX < 50) || (menuIsOpen && touch.screenX > menuWidth)) { + touchStartPos = touch; + touchCurPos = touch; + touchStartTime = Date.now(); + + viewport.classList.toggle("menu-dragging", true); + viewport.addEventListener("touchmove", onTouchMove); + viewport.addEventListener("touchend", onTouchEnd, {passive: true}); + } +} + +function onTouchMove(e) { + const touch = touchCurPos = e.touches.item(0); + let setX = touch.screenX - touchStartPos.screenX; + + if (Math.abs(setX > 30)) { + menuIsMoving = true; + } + + if (!menuIsMoving && Math.abs(touch.screenY - touchStartPos.screenY) > 30) { + onTouchEnd(); + return; + } + + if (menuIsOpen) { + setX += menuWidth; + } + + if (setX > menuWidth) { + setX = menuWidth; + } else if (setX < 0) { + setX = 0; + } + + viewport.style.transform = "translate3d(" + setX + "px, 0, 0)"; + + if (menuIsMoving) { + e.preventDefault(); + e.stopPropagation(); + } +} + +function onTouchEnd() { + const diff = touchCurPos.screenX - touchStartPos.screenX; + const absDiff = Math.abs(diff); + + if (absDiff > menuWidth / 2 || Date.now() - touchStartTime < 180 && absDiff > 50) { + SlideoutMenu.toggle(diff > 0); + } + + viewport.removeEventListener("touchmove", onTouchMove); + viewport.removeEventListener("touchend", onTouchEnd); + viewport.classList.toggle("menu-dragging", false); + viewport.style.transform = null; + + touchStartPos = null; + touchCurPos = null; + touchStartTime = 0; + menuIsMoving = false; +} + +module.exports = SlideoutMenu; diff --git a/client/js/socket-events/init.js b/client/js/socket-events/init.js index b9b284d1..009f7a1a 100644 --- a/client/js/socket-events/init.js +++ b/client/js/socket-events/init.js @@ -5,6 +5,7 @@ const escape = require("css.escape"); const socket = require("../socket"); const render = require("../render"); const webpush = require("../webpush"); +const slideoutMenu = require("../slideout"); const sidebar = $("#sidebar"); const storage = require("../localStorage"); const utils = require("../utils"); @@ -45,6 +46,8 @@ socket.on("init", function(data) { $("#loading").remove(); $("#sign-in").remove(); + slideoutMenu.enable(); + if (window.g_LoungeErrorHandler) { window.removeEventListener("error", window.g_LoungeErrorHandler); window.g_LoungeErrorHandler = null; From 5c7f34bd48795f6f09bd0b9149eba2e8516ddcfe Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Wed, 7 Mar 2018 09:19:02 +0000 Subject: [PATCH 1222/3926] chore(package): update mocha to version 5.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8ad49e35..6b66f0fb 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "istanbul-instrumenter-loader": "3.0.0", "jquery": "3.3.1", "jquery-ui": "1.12.1", - "mocha": "5.0.2", + "mocha": "5.0.3", "mocha-loader": "1.1.3", "mocha-webpack": "1.0.1", "mousetrap": "1.6.1", From 2506feb1ea9328431633788dbcd950651cb33a2a Mon Sep 17 00:00:00 2001 From: greenkeeperio-bot Date: Wed, 7 Mar 2018 09:23:17 +0000 Subject: [PATCH 1223/3926] chore(package): update lockfile https://npm.im/greenkeeper-lockfile --- yarn.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index c9d925d8..2e93d001 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1965,9 +1965,9 @@ detect-node@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127" -diff@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" +diff@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" diffie-hellman@^5.0.0: version "5.0.2" @@ -4364,14 +4364,14 @@ mocha-webpack@1.0.1: toposort "^1.0.0" yargs "^4.8.0" -mocha@5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.0.2.tgz#caab0b813575141c5690c37d53c894b605ae60b5" +mocha@5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.0.3.tgz#0a95a6ddea9073239851a3a26ffc9c8d679c757a" dependencies: browser-stdout "1.3.1" commander "2.11.0" debug "3.1.0" - diff "3.3.1" + diff "3.5.0" escape-string-regexp "1.0.5" glob "7.1.2" growl "1.10.3" From fe51c6d7e7cc736a0c45e35244d1dd438a6b25f4 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Thu, 14 Dec 2017 13:12:22 +0200 Subject: [PATCH 1224/3926] Move video size to css --- client/css/style.css | 5 +++++ client/views/msg_preview.tpl | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/client/css/style.css b/client/css/style.css index c3f0216a..556dda79 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1449,6 +1449,11 @@ button.collapse-network:first-child:nth-last-child(3) { max-width: 100%; } +#chat video { + max-width: 640px; + max-height: 240px; +} + /* Do not display an empty div when there are no previews. Useful for example in part/quit messages where we don't load previews (adds a blank line otherwise) */ #chat .preview:empty { diff --git a/client/views/msg_preview.tpl b/client/views/msg_preview.tpl index 451b99f0..05cf7680 100644 --- a/client/views/msg_preview.tpl +++ b/client/views/msg_preview.tpl @@ -12,7 +12,7 @@ {{/equal}} {{#equal type "video"}} -