From 0c0df1efc981e4ed7af552f92043ec0dc17991f4 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Mon, 28 Aug 2017 12:18:31 +0300 Subject: [PATCH 1/5] Force reload the page if socket reconnects and server restarted --- client/js/lounge.js | 6 +++--- client/js/socket-events/auth.js | 26 +++++++++++++++++++++++++- client/js/socket.js | 15 +++++++++++---- client/js/utils.js | 5 +++++ src/server.js | 8 +++++++- 5 files changed, 51 insertions(+), 9 deletions(-) diff --git a/client/js/lounge.js b/client/js/lounge.js index 1f5a5251..ef4e20a1 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -588,9 +588,6 @@ $(function() { } setTimeout(updateDateMarkers, msUntilNextDay()); - // Only start opening socket.io connection after all events have been registered - socket.open(); - window.addEventListener("popstate", (e) => { const {state} = e; if (!state) { @@ -604,4 +601,7 @@ $(function() { }); } }); + + // Only start opening socket.io connection after all events have been registered + socket.open(); }); diff --git a/client/js/socket-events/auth.js b/client/js/socket-events/auth.js index 08ce2ebd..6cdf923f 100644 --- a/client/js/socket-events/auth.js +++ b/client/js/socket-events/auth.js @@ -3,8 +3,19 @@ const $ = require("jquery"); const socket = require("../socket"); const storage = require("../localStorage"); +const utils = require("../utils"); socket.on("auth", function(data) { + // If we reconnected and serverHash differents, that means the server restarted + // And we will reload the page to grab the latest version + if (utils.serverHash > -1 && data.serverHash > -1 && data.serverHash !== utils.serverHash) { + socket.disconnect(); + location.reload(true); + return; + } + + utils.serverHash = data.serverHash; + const login = $("#sign-in"); let token; const user = storage.get("user"); @@ -12,6 +23,13 @@ socket.on("auth", function(data) { login.find(".btn").prop("disabled", false); if (!data.success) { + if (login.length === 0) { + socket.disconnect(); + $("#connection-error").text("Authentication failed, reloading…"); + location.reload(); + return; + } + storage.remove("token"); const error = login.find(".error"); @@ -20,9 +38,15 @@ socket.on("auth", function(data) { }); } else if (user) { token = storage.get("token"); + if (token) { $("#loading-page-message").text("Authorizing…"); - socket.emit("auth", {user: user, token: token}); + + socket.emit("auth", { + user: user, + token: token, + lastMessage: utils.lastMessageId, + }); } } diff --git a/client/js/socket.js b/client/js/socket.js index b7ba0e70..a646ad47 100644 --- a/client/js/socket.js +++ b/client/js/socket.js @@ -8,9 +8,11 @@ const socket = io({ transports: $(document.body).data("transports"), path: path, autoConnect: false, - reconnection: false + reconnection: !$(document.body).hasClass("public") }); +window.lounge_socket = socket; // TODO: Remove later, this is for debugging + [ "connect_error", "connect_failed", @@ -34,7 +36,7 @@ const socket = io({ } }); // Hides the "Send Message" button - $("#submit").remove(); + $("#submit").hide(); }); }); @@ -43,11 +45,16 @@ socket.on("connecting", function() { }); socket.on("connect", function() { - $("#loading-page-message").text("Finalizing connection…"); + // Clear send buffer when reconnecting, socket.io would emit these + // immediately upon connection and it will have no effect, so we ensure + // nothing is sent to the server that might have happened. + socket.sendBuffer = []; + + status.text("Finalizing connection…"); }); socket.on("authorized", function() { - $("#loading-page-message").text("Authorized, loading messages…"); + $("#loading-page-message").text("Loading messages…"); }); module.exports = socket; diff --git a/client/js/utils.js b/client/js/utils.js index 086a796e..07b1d328 100644 --- a/client/js/utils.js +++ b/client/js/utils.js @@ -3,7 +3,12 @@ const $ = require("jquery"); const input = $("#input"); +var serverHash = -1; +var lastMessageId = -1; + module.exports = { + serverHash, + lastMessageId, confirmExit, forceFocus, move, diff --git a/src/server.js b/src/server.js index aebc6bfb..7069a905 100644 --- a/src/server.js +++ b/src/server.js @@ -23,6 +23,9 @@ const authPlugins = [ require("./plugins/auth/local"), ]; +// A random number that will force clients to reload the page if it differs +const serverHash = Math.floor(Date.now() * Math.random()); + var manager = null; module.exports = function() { @@ -135,7 +138,10 @@ module.exports = function() { if (config.public) { performAuthentication.call(socket, {}); } else { - socket.emit("auth", {success: true}); + socket.emit("auth", { + serverHash: serverHash, + success: true, + }); socket.on("auth", performAuthentication); } }); From 05fc00d9be2de82780f8dc78282bb1413df8dde1 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Mon, 28 Aug 2017 18:03:27 +0300 Subject: [PATCH 2/5] Display all the status changes in UI --- client/css/style.css | 9 +++++++ client/index.html | 2 +- client/js/render.js | 4 +++ client/js/socket-events/auth.js | 3 ++- client/js/socket-events/init.js | 35 +++++++++++++++++++------- client/js/socket.js | 44 ++++++++++++--------------------- client/themes/crypto.css | 6 +---- client/themes/example.css | 8 ------ client/themes/morning.css | 8 ------ client/themes/zenburn.css | 8 ------ 10 files changed, 59 insertions(+), 68 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 08f6006a..d776988a 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1483,6 +1483,15 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */ } #connection-error { + font-size: 12px; + line-height: 36px; + font-weight: bold; + letter-spacing: 1px; + word-spacing: 3px; + text-transform: uppercase; + background: #e74c3c; + color: #fff; + text-align: center; display: none; } diff --git a/client/index.html b/client/index.html index cdce541f..61abcde2 100644 --- a/client/index.html +++ b/client/index.html @@ -63,7 +63,7 @@
- +
diff --git a/client/js/render.js b/client/js/render.js index cb0adc36..9e18ffe7 100644 --- a/client/js/render.js +++ b/client/js/render.js @@ -35,6 +35,10 @@ function buildChannelMessages(chanId, chanType, messages) { } function appendMessage(container, chanId, chanType, msg) { + if (utils.lastMessageId < msg.id) { + utils.lastMessageId = msg.id; + } + let lastChild = container.children(".msg, .date-marker-container").last(); const renderedMessage = buildChatMessage(msg); diff --git a/client/js/socket-events/auth.js b/client/js/socket-events/auth.js index 6cdf923f..cb0ef4ee 100644 --- a/client/js/socket-events/auth.js +++ b/client/js/socket-events/auth.js @@ -10,6 +10,7 @@ socket.on("auth", function(data) { // And we will reload the page to grab the latest version if (utils.serverHash > -1 && data.serverHash > -1 && data.serverHash !== utils.serverHash) { socket.disconnect(); + $("#connection-error").text("Server restarted, reloading…"); location.reload(true); return; } @@ -40,7 +41,7 @@ socket.on("auth", function(data) { token = storage.get("token"); if (token) { - $("#loading-page-message").text("Authorizing…"); + $("#loading-page-message, #connection-error").text("Authorizing…"); socket.emit("auth", { user: user, diff --git a/client/js/socket-events/init.js b/client/js/socket-events/init.js index cd66f391..f200a834 100644 --- a/client/js/socket-events/init.js +++ b/client/js/socket-events/init.js @@ -6,11 +6,22 @@ const render = require("../render"); const webpush = require("../webpush"); const sidebar = $("#sidebar"); const storage = require("../localStorage"); +const utils = require("../utils"); socket.on("init", function(data) { - $("#loading-page-message").text("Rendering…"); + $("#loading-page-message, #connection-error").text("Rendering…"); + + const lastMessageId = utils.lastMessageId; + + // TODO: this is hacky + if (lastMessageId > -1) { + sidebar.find(".networks").empty(); + $("#chat").empty(); + } if (data.networks.length === 0) { + sidebar.find(".empty").show(); + $("#footer").find(".connect").trigger("click", { pushState: false, }); @@ -18,16 +29,22 @@ socket.on("init", function(data) { render.renderNetworks(data); } - if (data.token) { - storage.set("token", data.token); + if (lastMessageId > -1) { + $("#connection-error").removeClass("shown"); + $(".show-more-button, #input").prop("disabled", false); + $("#submit").show(); + } else { + if (data.token) { + storage.set("token", data.token); + } + + webpush.configurePushNotifications(data.pushSubscription, data.applicationServerKey); + + $("body").removeClass("signed-out"); + $("#loading").remove(); + $("#sign-in").remove(); } - webpush.configurePushNotifications(data.pushSubscription, data.applicationServerKey); - - $("body").removeClass("signed-out"); - $("#loading").remove(); - $("#sign-in").remove(); - const id = data.active; const target = sidebar.find("[data-id='" + id + "']").trigger("click", { replaceHistory: true diff --git a/client/js/socket.js b/client/js/socket.js index a646ad47..c5593f90 100644 --- a/client/js/socket.js +++ b/client/js/socket.js @@ -3,6 +3,7 @@ const $ = require("jquery"); const io = require("socket.io-client"); const path = window.location.pathname + "socket.io/"; +const status = $("#loading-page-message, #connection-error"); const socket = io({ transports: $(document.body).data("transports"), @@ -11,37 +12,16 @@ const socket = io({ reconnection: !$(document.body).hasClass("public") }); -window.lounge_socket = socket; // TODO: Remove later, this is for debugging +socket.on("disconnect", handleDisconnect); +socket.on("connect_error", handleDisconnect); +socket.on("error", handleDisconnect); -[ - "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").hide(); - }); +socket.on("reconnecting", function(attempt) { + status.text(`Reconnecting… (attempt ${attempt})`); }); socket.on("connecting", function() { - $("#loading-page-message").text("Connecting…"); + status.text("Connecting…"); }); socket.on("connect", function() { @@ -54,7 +34,15 @@ socket.on("connect", function() { }); socket.on("authorized", function() { - $("#loading-page-message").text("Loading messages…"); + status.text("Loading messages…"); }); +function handleDisconnect(data) { + const message = data.message || data; + + status.text(`Waiting to reconnect… (${message})`).addClass("shown"); + $(".show-more-button, #input").prop("disabled", true); + $("#submit").hide(); +} + module.exports = socket; diff --git a/client/themes/crypto.css b/client/themes/crypto.css index 9b7bf72b..2f9f5424 100644 --- a/client/themes/crypto.css +++ b/client/themes/crypto.css @@ -65,12 +65,8 @@ a:hover, background: #00ff0e; } -.btn-reconnect { +#connection-error { background: #f00; - color: #fff; - border: 0; - border-radius: 0; - margin: 0; } #settings .opt { diff --git a/client/themes/example.css b/client/themes/example.css index a8efcbf9..d9764ac1 100644 --- a/client/themes/example.css +++ b/client/themes/example.css @@ -46,14 +46,6 @@ body { border-radius: 2px; } -.btn-reconnect { - background: #e74c3c; - color: #fff; - border: 0; - border-radius: 0; - margin: 0; -} - @media (max-width: 768px) { #sidebar { left: -220px; diff --git a/client/themes/morning.css b/client/themes/morning.css index 856c530c..0b576e8d 100644 --- a/client/themes/morning.css +++ b/client/themes/morning.css @@ -205,14 +205,6 @@ body { color: #99a2b4; } -.btn-reconnect { - background: #e74c3c; - color: #fff; - border: 0; - border-radius: 0; - margin: 0; -} - /* Form elements */ #chat-container ::-moz-placeholder { diff --git a/client/themes/zenburn.css b/client/themes/zenburn.css index 21aabd23..e4823cdf 100644 --- a/client/themes/zenburn.css +++ b/client/themes/zenburn.css @@ -232,14 +232,6 @@ body { color: #d2d39b; } -.btn-reconnect { - background: #e74c3c; - color: #fff; - border: 0; - border-radius: 0; - margin: 0; -} - /* Form elements */ #chat-container ::-moz-placeholder { From cffa957e34ce2e2cc09d605f6ea9d929c80e9088 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Mon, 28 Aug 2017 23:14:01 +0300 Subject: [PATCH 3/5] Only send messages newer than last seen id --- src/server.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/server.js b/src/server.js index 7069a905..f222f6da 100644 --- a/src/server.js +++ b/src/server.js @@ -236,7 +236,7 @@ function index(req, res, next) { res.render("index", data); } -function initializeClient(socket, client, token) { +function initializeClient(socket, client, token, lastMessage) { socket.emit("authorized"); socket.on("disconnect", function() { @@ -400,11 +400,24 @@ function initializeClient(socket, client, token) { socket.join(client.id); const sendInitEvent = (tokenToSend) => { + let networks = client.networks; + + if (lastMessage > -1) { + // We need a deep cloned object because we are going to remove unneeded messages + networks = _.cloneDeep(networks); + + networks.forEach((network) => { + network.channels.forEach((channel) => { + channel.messages = channel.messages.filter((m) => m.id > lastMessage); + }); + }); + } + socket.emit("init", { applicationServerKey: manager.webPush.vapidKeys.publicKey, pushSubscription: client.config.sessions[token], active: client.lastActiveChannel, - networks: client.networks, + networks: networks, token: tokenToSend }); }; @@ -434,7 +447,7 @@ function performAuthentication(data) { const socket = this; let client; - const finalInit = () => initializeClient(socket, client, data.token || null); + const finalInit = () => initializeClient(socket, client, data.token || null, data.lastMessage || -1); const initClient = () => { client.ip = getClientIp(socket.request); From 532f55cb862202dc3ca393756212186a2b4b9714 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Mon, 28 Aug 2017 23:06:28 +0300 Subject: [PATCH 4/5] Redraw channels --- client/js/render.js | 47 ++++++++++++++++++++++--- client/js/socket-events/auth.js | 2 +- client/js/socket-events/init.js | 56 ++++++++++++++++++++---------- client/js/socket-events/more.js | 2 +- client/js/socket-events/network.js | 3 +- 5 files changed, 82 insertions(+), 28 deletions(-) diff --git a/client/js/render.js b/client/js/render.js index 9e18ffe7..bb8be296 100644 --- a/client/js/render.js +++ b/client/js/render.js @@ -8,6 +8,7 @@ const utils = require("./utils"); const sorting = require("./sorting"); const constants = require("./constants"); const condensed = require("./condensed"); +const helpers_parse = require("./libs/handlebars/parse"); const chat = $("#chat"); const sidebar = $("#sidebar"); @@ -27,11 +28,11 @@ module.exports = { renderNetworks, }; -function buildChannelMessages(chanId, chanType, messages) { +function buildChannelMessages(container, chanId, chanType, messages) { return messages.reduce((docFragment, message) => { appendMessage(docFragment, chanId, chanType, message); return docFragment; - }, $(document.createDocumentFragment())); + }, container); } function appendMessage(container, chanId, chanType, msg) { @@ -121,7 +122,7 @@ function renderChannel(data) { } function renderChannelMessages(data) { - const documentFragment = buildChannelMessages(data.id, data.type, data.messages); + const documentFragment = buildChannelMessages($(document.createDocumentFragment()), data.id, data.type, data.messages); const channel = chat.find("#chan-" + data.id + " .messages").append(documentFragment); const template = $(templates.unread_marker()); @@ -168,7 +169,7 @@ function renderChannelUsers(data) { } } -function renderNetworks(data) { +function renderNetworks(data, singleNetwork) { sidebar.find(".empty").hide(); sidebar.find(".networks").append( templates.network({ @@ -176,15 +177,51 @@ function renderNetworks(data) { }) ); + let newChannels; const channels = $.map(data.networks, function(n) { return n.channels; }); + + if (!singleNetwork && utils.lastMessageId > -1) { + newChannels = []; + + channels.forEach((channel) => { + const chan = $("#chan-" + channel.id); + + if (chan.length > 0) { + if (chan.data("type") === "channel") { + chan + .data("needsNamesRefresh", true) + .find(".header .topic") + .html(helpers_parse(channel.topic)) + .attr("title", channel.topic); + } + + if (channel.messages.length > 0) { + const container = chan.find(".messages"); + buildChannelMessages(container, channel.id, channel.type, channel.messages); + + if (container.find(".msg").length >= 100) { + container.find(".show-more").addClass("show"); + } + + container.trigger("keepToBottom"); + } + } else { + newChannels.push(channel); + } + }); + } else { + newChannels = channels; + } + chat.append( templates.chat({ channels: channels }) ); - channels.forEach((channel) => { + + newChannels.forEach((channel) => { renderChannel(channel); if (channel.type === "channel") { diff --git a/client/js/socket-events/auth.js b/client/js/socket-events/auth.js index cb0ef4ee..e544948f 100644 --- a/client/js/socket-events/auth.js +++ b/client/js/socket-events/auth.js @@ -6,7 +6,7 @@ const storage = require("../localStorage"); const utils = require("../utils"); socket.on("auth", function(data) { - // If we reconnected and serverHash differents, that means the server restarted + // If we reconnected and serverHash differs, that means the server restarted // And we will reload the page to grab the latest version if (utils.serverHash > -1 && data.serverHash > -1 && data.serverHash !== utils.serverHash) { socket.disconnect(); diff --git a/client/js/socket-events/init.js b/client/js/socket-events/init.js index f200a834..6debd9d9 100644 --- a/client/js/socket-events/init.js +++ b/client/js/socket-events/init.js @@ -1,6 +1,7 @@ "use strict"; const $ = require("jquery"); +const escape = require("css.escape"); const socket = require("../socket"); const render = require("../render"); const webpush = require("../webpush"); @@ -12,11 +13,11 @@ socket.on("init", function(data) { $("#loading-page-message, #connection-error").text("Rendering…"); const lastMessageId = utils.lastMessageId; + let previousActive = 0; - // TODO: this is hacky if (lastMessageId > -1) { + previousActive = sidebar.find(".active").data("id"); sidebar.find(".networks").empty(); - $("#chat").empty(); } if (data.networks.length === 0) { @@ -45,21 +46,38 @@ socket.on("init", function(data) { $("#sign-in").remove(); } - const id = data.active; - const target = sidebar.find("[data-id='" + id + "']").trigger("click", { - replaceHistory: true - }); - const dataTarget = document.querySelector("[data-target='" + window.location.hash + "']"); - if (window.location.hash && dataTarget) { - dataTarget.click(); - } else if (target.length === 0) { - const first = sidebar.find(".chan") - .eq(0) - .trigger("click"); - if (first.length === 0) { - $("#footer").find(".connect").trigger("click", { - pushState: false, - }); - } - } + openCorrectChannel(previousActive, data.active); }); + +function openCorrectChannel(clientActive, serverActive) { + let target; + + // Open last active channel + if (clientActive > 0) { + target = sidebar.find("[data-id='" + clientActive + "']"); + } + + // Open window provided in location.hash + if (!target && window.location.hash) { + target = $("#footer, #sidebar").find("[data-target='" + escape(window.location.hash) + "']"); + } + + // Open last active channel according to the server + if (!target) { + target = sidebar.find("[data-id='" + serverActive + "']"); + } + + // If target channel is found, open it + if (target) { + target.trigger("click", { + replaceHistory: true + }); + + return; + } + + // Open the connect window + $("#footer .connect").trigger("click", { + pushState: false + }); +} diff --git a/client/js/socket-events/more.js b/client/js/socket-events/more.js index d762b542..b5f0fab6 100644 --- a/client/js/socket-events/more.js +++ b/client/js/socket-events/more.js @@ -33,7 +33,7 @@ socket.on("more", function(data) { } // Add the older messages - const documentFragment = render.buildChannelMessages(data.chan, type, data.messages); + const documentFragment = render.buildChannelMessages($(document.createDocumentFragment()), data.chan, type, data.messages); chan.prepend(documentFragment); // Move unread marker to correct spot if needed diff --git a/client/js/socket-events/network.js b/client/js/socket-events/network.js index 846a6a34..a55b0433 100644 --- a/client/js/socket-events/network.js +++ b/client/js/socket-events/network.js @@ -6,7 +6,7 @@ const render = require("../render"); const sidebar = $("#sidebar"); socket.on("network", function(data) { - render.renderNetworks(data); + render.renderNetworks(data, true); sidebar.find(".chan") .last() @@ -20,4 +20,3 @@ socket.on("network", function(data) { socket.on("network_changed", function(data) { sidebar.find("#network-" + data.network).data("options", data.serverOptions); }); - From 935c5b309ab380fbfe90127ad52c2f64efee35aa Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sun, 10 Sep 2017 18:28:28 +0300 Subject: [PATCH 5/5] Force reconnect on server shutdown --- client/js/socket.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/js/socket.js b/client/js/socket.js index c5593f90..a9916b0a 100644 --- a/client/js/socket.js +++ b/client/js/socket.js @@ -2,6 +2,7 @@ const $ = require("jquery"); const io = require("socket.io-client"); +const utils = require("./utils"); const path = window.location.pathname + "socket.io/"; const status = $("#loading-page-message, #connection-error"); @@ -43,6 +44,12 @@ function handleDisconnect(data) { status.text(`Waiting to reconnect… (${message})`).addClass("shown"); $(".show-more-button, #input").prop("disabled", true); $("#submit").hide(); + + // If the server shuts down, socket.io skips reconnection + // and we have to manually call connect to start the process + if (socket.io.skipReconnect) { + utils.requestIdleCallback(() => socket.connect(), 2000); + } } module.exports = socket;