diff --git a/client/index.html b/client/index.html index 4e606379..78051d25 100644 --- a/client/index.html +++ b/client/index.html @@ -9,23 +9,23 @@ - + The Lounge - + - + - + " data-transports="<%- JSON.stringify(transports) %>">
@@ -79,805 +79,10 @@
-
-
-
-
-

Sign in to The Lounge

-
-
- -
-
- -
- -
- -
-
-
-
-
-
- -
-
-
-
-

- {{#if public}}The Lounge - {{/if}} - Connect - {{#unless displayNetwork}} - {{#if lockNetwork}} - to {{defaults.name}} - {{/if}} - {{/unless}} -

-
- {{#if displayNetwork}} -
-
-

Network settings

-
-
- -
-
- -
-
- -
-
- -
-
-
- -
-
-
-
- -
-
- -
-
- -
-
-
- {{/if}} -
-

User preferences

-
-
- -
-
- -
- {{#unless useHexIp}} -
- -
-
- -
- {{/unless}} -
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
- -
-
-

Settings

- -
-
-

Messages

-
-
- -
-
- -
-
-

- Status messages - - - -

-
-
- - - -
-
-

Visual Aids

-
-
- - -
-
-

Theme

-
-
- - -
- {{#if prefetch}} -
-

Link previews

-
-
- -
-
- -
- {{/if}} - {{#unless public}} -
-

Push Notifications

-
-
- -
- Warning: - Push notifications are only supported over HTTPS connections. -
-
- Warning: - Push notifications are not supported by your browser. -
-
- {{/unless}} -
-

Browser Notifications

-
-
- -
-
- -
-
-
- -
-
- -
- -
- -
- -
- - {{#unless public}} - {{#unless ldap.enable}} -
-
-
-

Change password

-
-
- - -
-
- - -
-
- - -
- -
- -
-
-
- {{/unless}} - {{/unless}} -
-

Custom Stylesheet

-
-
- -
-
- - {{#unless public}} -
-

Sessions

- -

Current session

-
- -

Other sessions

-
-
- {{/unless}} -
-
- -
-
- -
-
-

Help

- -

Keyboard Shortcuts

- -
-
- Ctrl + / -
-
-

Switch to the previous/next window in the channel list

-
-
- -
-
- Ctrl + K -
-
-

- Mark any text typed after this shortcut to be colored. After - hitting this shortcut, enter an integer in the range - 0—15 to select the desired color, or use the - autocompletion menu to choose a color name (see below). -

-

- Background color can be specified by putting a comma and - another integer in the range 0—15 after the - foreground color number (autocompletion works too). -

-

- 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. -

-
-
- -

Autocompletion

- -

- To auto-complete nicknames, channels, commands, and emoji, type one of the characters below to open - a suggestion list. Use the and keys to highlight an item, and insert it by - pressing Tab or Enter (or by clicking the desired item). -

-

- Autocompletion can be disabled in settings. -

- -
-
- @ -
-
-

Nickname

-
-
- -
-
- # -
-
-

Channel

-
-
- -
-
- / -
-
-

Commands (see list of commands below)

-
-
- -
-
- : -
-
-

Emoji (note: requires two search characters, to avoid conflicting with common emoticons like :))

-
-
- -

Commands

- -
-
- /away [message] -
-
-

Mark yourself as away with an optional message.

-
-
- -
-
- /back -
-
-

Remove your away status (set with /away).

-
-
- -
-
- /ban nick -
-
-

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

-
-
- -
-
- /banlist -
-
-

Load the banlist for the current channel.

-
-
- -
-
- /collapse -
-
-

- Collapse all previews in the current channel (opposite of - /expand) -

-
-
- -
-
- /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. -

-
-
- -
-
- /expand -
-
-

- Expand all previews in the current channel (opposite of - /collapse) -

-
-
- -
-
- /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 [channel] -
-
-

- Close the specified channel or private message window, or the - current channel if channel is ommitted. -

-

Aliases: /close, /leave

-
-
- -
-
- /rejoin -
-
-

- Leave and immediately rejoin the current channel. Useful to - quickly get op from ChanServ in an empty channel, for example. -

-

Alias: /cycle

-
-
- -
-
- /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.

-
-
- -
-
- /unban nick -
-
-

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

-
-
- -
-
- /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. -

-
-
- -

About The Lounge

- -

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

-
-
+
+
+
+
diff --git a/client/js/lounge.js b/client/js/lounge.js index 307de2ac..440920f2 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -15,7 +15,6 @@ const templates = require("../views"); const socket = require("./socket"); require("./socket-events"); const storage = require("./localStorage"); -require("./options"); const utils = require("./utils"); require("./autocompletion"); require("./webpush"); @@ -28,7 +27,6 @@ $(function() { $(document.body).data("app-name", document.title); - var windows = $("#windows"); var viewport = $("#viewport"); var sidebarSlide = slideoutMenu(viewport[0], sidebar[0]); var contextMenuContainer = $("#context-menu-container"); @@ -485,18 +483,6 @@ $(function() { container.html(templates.user_filtered({matches: result})).show(); }); - var forms = $("#sign-in, #connect, #change-password"); - - windows.on("show", "#sign-in", function() { - $(this).find("input").each(function() { - var self = $(this); - if (self.val() === "") { - self.focus(); - return false; - } - }); - }); - if ($("body").hasClass("public") && (window.location.hash === "#connect" || window.location.hash === "")) { $("#connect").one("show", function() { var params = URI(document.location.search); @@ -523,56 +509,6 @@ $(function() { }); } - forms.on("submit", "form", function(e) { - e.preventDefault(); - var event = "auth"; - var form = $(this); - form.find(".btn").attr("disabled", true); - - if (form.closest(".window").attr("id") === "connect") { - event = "conn"; - } else if (form.closest("div").attr("id") === "change-password") { - event = "change-password"; - } - - var values = {}; - $.each(form.serializeArray(), function(i, obj) { - if (obj.value !== "") { - values[obj.name] = obj.value; - } - }); - - if (values.user) { - storage.set("user", values.user); - } - - socket.emit( - event, values - ); - }); - - 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(); - 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); - }); - $(document).on("visibilitychange focus click", () => { if (sidebar.find(".highlight").length === 0) { utils.toggleNotificationMarkers(false); diff --git a/client/js/options.js b/client/js/options.js index f9621430..4faca65e 100644 --- a/client/js/options.js +++ b/client/js/options.js @@ -3,7 +3,6 @@ const $ = require("jquery"); require("jquery-textcomplete"); const escapeRegExp = require("lodash/escapeRegExp"); -const settings = $("#settings"); const userStyles = $("#user-specified-css"); const storage = require("./localStorage"); const tz = require("./libs/handlebars/tz"); @@ -35,6 +34,11 @@ for (const key in options) { } } +// Apply custom CSS on page load +if (typeof userOptions.userStyles === "string" && !/[?&]nocss/.test(window.location.search)) { + userStyles.html(userOptions.userStyles); +} + userOptions = null; module.exports = options; @@ -43,123 +47,126 @@ module.exports.shouldOpenMessagePreview = function(type) { return (options.links && type === "link") || (options.thumbnails && type === "image"); }; -for (var i in options) { - if (i === "userStyles") { - if (!/[?&]nocss/.test(window.location.search)) { - $(document.head).find("#user-specified-css").html(options[i]); +module.exports.initialize = () => { + module.exports.initialize = null; + + const settings = $("#settings"); + + for (var i in options) { + if (i === "userStyles") { + settings.find("#user-specified-css-input").val(options[i]); + } else if (i === "highlights") { + settings.find("input[name=" + i + "]").val(options[i]); + } else if (i === "statusMessages") { + settings.find(`input[name=${i}][value=${options[i]}]`) + .prop("checked", true); + } 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.find("#user-specified-css-input").val(options[i]); - } else if (i === "highlights") { - settings.find("input[name=" + i + "]").val(options[i]); - } else if (i === "statusMessages") { - settings.find(`input[name=${i}][value=${options[i]}]`) - .prop("checked", true); - } 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() { - const self = $(this); - const type = self.attr("type"); - const name = self.attr("name"); + settings.on("change", "input, select, textarea", function() { + const self = $(this); + const type = self.attr("type"); + const name = self.attr("name"); - if (type === "password") { - return; - } else if (type === "radio") { - if (self.prop("checked")) { + if (type === "password") { + return; + } else if (type === "radio") { + if (self.prop("checked")) { + options[name] = self.val(); + } + } else if (type === "checkbox") { + options[name] = self.prop("checked"); + } else { options[name] = self.val(); } - } else if (type === "checkbox") { - options[name] = self.prop("checked"); + + storage.set("settings", JSON.stringify(options)); + + if (name === "motd") { + chat.toggleClass("hide-" + name, !self.prop("checked")); + } else if (name === "statusMessages") { + chat.toggleClass("hide-status-messages", options[name] === "hidden"); + chat.toggleClass("condensed-status-messages", options[name] === "condensed"); + } 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 !== ""; + }); + // Construct regex with wordboundary for every highlight item + const highlightsTokens = options.highlights.map(function(h) { + return escapeRegExp(h); + }); + if (highlightsTokens && highlightsTokens.length) { + module.exports.highlightsRE = new RegExp("\\b(?:" + highlightsTokens.join("|") + ")\\b", "i"); + } else { + module.exports.highlightsRE = null; + } + } else if (name === "showSeconds") { + chat.find(".msg > .time").each(function() { + $(this).text(tz($(this).parent().data("time"))); + }); + chat.toggleClass("show-seconds", self.prop("checked")); + } else if (name === "autocomplete") { + if (self.prop("checked")) { + $("#input").trigger("autocomplete:on"); + } else { + $("#input").textcomplete("destroy"); + } + } + }).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[name] = self.val(); - } - - storage.set("settings", JSON.stringify(options)); - - if (name === "motd") { - chat.toggleClass("hide-" + name, !self.prop("checked")); - } else if (name === "statusMessages") { - chat.toggleClass("hide-status-messages", options[name] === "hidden"); - chat.toggleClass("condensed-status-messages", options[name] === "condensed"); - } 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 !== ""; - }); - // Construct regex with wordboundary for every highlight item - const highlightsTokens = options.highlights.map(function(h) { - return escapeRegExp(h); - }); - if (highlightsTokens && highlightsTokens.length) { - module.exports.highlightsRE = new RegExp("\\b(?:" + highlightsTokens.join("|") + ")\\b", "i"); - } else { - module.exports.highlightsRE = null; - } - } else if (name === "showSeconds") { - chat.find(".msg > .time").each(function() { - $(this).text(tz($(this).parent().data("time"))); - }); - chat.toggleClass("show-seconds", self.prop("checked")); - } else if (name === "autocomplete") { - if (self.prop("checked")) { - $("#input").trigger("autocomplete:on"); - } else { - $("#input").textcomplete("destroy"); - } - } -}).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") { + options.desktopNotifications = false; 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); -} diff --git a/client/js/socket-events/auth.js b/client/js/socket-events/auth.js index e544948f..ef9f4d5c 100644 --- a/client/js/socket-events/auth.js +++ b/client/js/socket-events/auth.js @@ -4,6 +4,9 @@ const $ = require("jquery"); const socket = require("../socket"); const storage = require("../localStorage"); const utils = require("../utils"); +const templates = require("../../views"); + +const login = $("#sign-in").html(templates.windows.sign_in()); socket.on("auth", function(data) { // If we reconnected and serverHash differs, that means the server restarted @@ -17,12 +20,28 @@ socket.on("auth", function(data) { utils.serverHash = data.serverHash; - const login = $("#sign-in"); let token; const user = storage.get("user"); login.find(".btn").prop("disabled", false); + login.find("form").on("submit", function() { + const form = $(this); + + form.find(".btn").attr("disabled", true); + + const values = {}; + $.each(form.serializeArray(), function(i, obj) { + values[obj.name] = obj.value; + }); + + storage.set("user", values.user); + + socket.emit("auth", values); + + return false; + }); + if (!data.success) { if (login.length === 0) { socket.disconnect(); diff --git a/client/js/socket-events/configuration.js b/client/js/socket-events/configuration.js new file mode 100644 index 00000000..fd687a7b --- /dev/null +++ b/client/js/socket-events/configuration.js @@ -0,0 +1,68 @@ +"use strict"; + +const $ = require("jquery"); +const socket = require("../socket"); +const templates = require("../../views"); +const options = require("../options"); +const webpush = require("../webpush"); + +socket.on("configuration", function(data) { + if (!options.initialize) { + return; + } + + $("#settings").html(templates.windows.settings(data)); + $("#connect").html(templates.windows.connect(data)); + $("#help").html(templates.windows.help(data)); + + $("#play").on("click", () => { + const pop = new Audio(); + pop.src = "audio/pop.ogg"; + pop.play(); + }); + + options.initialize(); + webpush.initialize(); + + const forms = $("#connect form, #change-password form"); + + forms.on("submit", function() { + const form = $(this); + const event = form.data("event"); + + form.find(".btn").attr("disabled", true); + + const values = {}; + $.each(form.serializeArray(), function(i, obj) { + if (obj.value !== "") { + values[obj.name] = obj.value; + } + }); + + socket.emit(event, values); + + return false; + }); + + $(".nick") + .on("focusin", function() { + // Need to set the first "lastvalue", so it can be used in the below function + const nick = $(this); + nick.data("lastvalue", nick.val()); + }) + .on("input", function() { + const nick = $(this).val(); + const usernameInput = forms.find(".username"); + + // Because this gets called /after/ it has already changed, we need use the previous value + const 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); + }); +}); diff --git a/client/js/socket-events/index.js b/client/js/socket-events/index.js index b0064665..f14c670a 100644 --- a/client/js/socket-events/index.js +++ b/client/js/socket-events/index.js @@ -18,3 +18,4 @@ require("./topic"); require("./users"); require("./sign_out"); require("./sessions_list"); +require("./configuration"); diff --git a/client/js/socket-events/msg.js b/client/js/socket-events/msg.js index 585dc7ef..d8f50958 100644 --- a/client/js/socket-events/msg.js +++ b/client/js/socket-events/msg.js @@ -21,8 +21,6 @@ try { }; } -$("#play").on("click", () => pop.play()); - socket.on("msg", function(data) { // We set a maximum timeout of 2 seconds so that messages don't take too long to appear. utils.requestIdleCallback(() => processReceivedMessage(data), 2000); diff --git a/client/js/webpush.js b/client/js/webpush.js index 16eb9dd8..bffe65c7 100644 --- a/client/js/webpush.js +++ b/client/js/webpush.js @@ -4,7 +4,7 @@ const $ = require("jquery"); const storage = require("./localStorage"); const socket = require("./socket"); -const pushNotificationsButton = $("#pushNotifications"); +let pushNotificationsButton; let clientSubscribed = null; let applicationServerKey; @@ -29,7 +29,13 @@ module.exports.configurePushNotifications = (subscribedOnServer, key) => { } }; -if (isAllowedServiceWorkersHost()) { +module.exports.initialize = () => { + pushNotificationsButton = $("#pushNotifications"); + + if (!isAllowedServiceWorkersHost()) { + return; + } + $("#pushNotificationsHttps").hide(); if ("serviceWorker" in navigator) { @@ -57,7 +63,7 @@ if (isAllowedServiceWorkersHost()) { $("#pushNotificationsUnsupported span").text(err); }); } -} +}; function onPushButton() { pushNotificationsButton.attr("disabled", true); diff --git a/client/views/index.js b/client/views/index.js index 118fa60c..267f0691 100644 --- a/client/views/index.js +++ b/client/views/index.js @@ -20,6 +20,13 @@ module.exports = { whois: require("./actions/whois.tpl"), }, + windows: { + sign_in: require("./windows/sign_in.tpl"), + settings: require("./windows/settings.tpl"), + connect: require("./windows/connect.tpl"), + help: require("./windows/help.tpl"), + }, + chan: require("./chan.tpl"), chat: require("./chat.tpl"), contextmenu_divider: require("./contextmenu_divider.tpl"), diff --git a/client/views/windows/connect.tpl b/client/views/windows/connect.tpl new file mode 100644 index 00000000..9ef5d09e --- /dev/null +++ b/client/views/windows/connect.tpl @@ -0,0 +1,88 @@ +
+ +
+
+
+
+

+ {{#if public}}The Lounge - {{/if}} + Connect + {{#unless displayNetwork}} + {{#if lockNetwork}} + to {{defaults.name}} + {{/if}} + {{/unless}} +

+
+ {{#if displayNetwork}} +
+
+

Network settings

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ {{/if}} +
+

User preferences

+
+
+ +
+
+ +
+ {{#unless useHexIp}} +
+ +
+
+ +
+ {{/unless}} +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
diff --git a/client/views/windows/help.tpl b/client/views/windows/help.tpl new file mode 100644 index 00000000..6db8d46d --- /dev/null +++ b/client/views/windows/help.tpl @@ -0,0 +1,482 @@ +
+ +
+
+

Help

+ +

Keyboard Shortcuts

+ +
+
+ Ctrl + / +
+
+

Switch to the previous/next window in the channel list

+
+
+ +
+
+ Ctrl + K +
+
+

+ Mark any text typed after this shortcut to be colored. After + hitting this shortcut, enter an integer in the range + 0—15 to select the desired color, or use the + autocompletion menu to choose a color name (see below). +

+

+ Background color can be specified by putting a comma and + another integer in the range 0—15 after the + foreground color number (autocompletion works too). +

+

+ 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. +

+
+
+ +

Autocompletion

+ +

+ To auto-complete nicknames, channels, commands, and emoji, type one of the characters below to open + a suggestion list. Use the and keys to highlight an item, and insert it by + pressing Tab or Enter (or by clicking the desired item). +

+

+ Autocompletion can be disabled in settings. +

+ +
+
+ @ +
+
+

Nickname

+
+
+ +
+
+ # +
+
+

Channel

+
+
+ +
+
+ / +
+
+

Commands (see list of commands below)

+
+
+ +
+
+ : +
+
+

Emoji (note: requires two search characters, to avoid conflicting with common emoticons like :))

+
+
+ +

Commands

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

Mark yourself as away with an optional message.

+
+
+ +
+
+ /back +
+
+

Remove your away status (set with /away).

+
+
+ +
+
+ /ban nick +
+
+

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

+
+
+ +
+
+ /banlist +
+
+

Load the banlist for the current channel.

+
+
+ +
+
+ /collapse +
+
+

+ Collapse all previews in the current channel (opposite of + /expand) +

+
+
+ +
+
+ /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. +

+
+
+ +
+
+ /expand +
+
+

+ Expand all previews in the current channel (opposite of + /collapse) +

+
+
+ +
+
+ /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 [channel] +
+
+

+ Close the specified channel or private message window, or the + current channel if channel is ommitted. +

+

Aliases: /close, /leave

+
+
+ +
+
+ /rejoin +
+
+

+ Leave and immediately rejoin the current channel. Useful to + quickly get op from ChanServ in an empty channel, for example. +

+

Alias: /cycle

+
+
+ +
+
+ /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.

+
+
+ +
+
+ /unban nick +
+
+

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

+
+
+ +
+
+ /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. +

+
+
+ +

About The Lounge

+ +

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

+
diff --git a/client/views/windows/settings.tpl b/client/views/windows/settings.tpl new file mode 100644 index 00000000..f22a3bdf --- /dev/null +++ b/client/views/windows/settings.tpl @@ -0,0 +1,193 @@ +
+ +
+
+

Settings

+ +
+
+

Messages

+
+
+ +
+
+ +
+
+

+ Status messages + + + +

+
+
+ + + +
+
+

Visual Aids

+
+
+ + +
+
+

Theme

+
+
+ + +
+ {{#if prefetch}} +
+

Link previews

+
+
+ +
+
+ +
+ {{/if}} + {{#unless public}} +
+

Push Notifications

+
+
+ +
+ Warning: + Push notifications are only supported over HTTPS connections. +
+
+ Warning: + Push notifications are not supported by your browser. +
+
+ {{/unless}} +
+

Browser Notifications

+
+
+ +
+
+ +
+
+
+ +
+
+ +
+ +
+ +
+ +
+ + {{#unless public}} + {{#unless ldapEnabled}} +
+
+
+

Change password

+
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+
+ {{/unless}} + {{/unless}} +
+

Custom Stylesheet

+
+
+ +
+
+ + {{#unless public}} +
+

Sessions

+ +

Current session

+
+ +

Other sessions

+
+
+ {{/unless}} +
diff --git a/client/views/windows/sign_in.tpl b/client/views/windows/sign_in.tpl new file mode 100644 index 00000000..2f0e2e5a --- /dev/null +++ b/client/views/windows/sign_in.tpl @@ -0,0 +1,23 @@ +
+
+
+

Sign in to The Lounge

+
+
+ +
+
+ +
+ +
+ +
+
+
diff --git a/package.json b/package.json index 4a708b30..10b557a1 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "colors": "1.1.2", "commander": "2.11.0", "express": "4.16.2", - "express-handlebars": "3.0.0", "fs-extra": "4.0.2", "irc-framework": "2.9.1", "ldapjs": "1.0.1", diff --git a/src/command-line/index.js b/src/command-line/index.js index 44170481..a1c5451c 100644 --- a/src/command-line/index.js +++ b/src/command-line/index.js @@ -2,6 +2,8 @@ global.log = require("../log.js"); +const fs = require("fs"); +const path = require("path"); const program = require("commander"); const colors = require("colors/safe"); const Helper = require("../helper"); @@ -22,6 +24,18 @@ if (program.home) { log.warn(`Use the ${colors.green("LOUNGE_HOME")} environment variable instead.`); } +// Check if the app was built before calling setHome as it wants to load manifest.json from the public folder +if (!fs.existsSync(path.join( + __dirname, + "..", + "..", + "public", + "manifest.json" +))) { + log.error(`The client application was not built. Run ${colors.bold("NODE_ENV=production npm run build")} to resolve this.`); + process.exit(1); +} + let home = program.home || process.env.LOUNGE_HOME; if (!home) { diff --git a/src/helper.js b/src/helper.js index 1f44d81e..0f65d085 100644 --- a/src/helper.js +++ b/src/helper.js @@ -78,6 +78,10 @@ function setHome(homePath) { log.warn(`${colors.bold("displayNetwork")} and ${colors.bold("lockNetwork")} are false, setting ${colors.bold("lockNetwork")} to true.`); } + // Load theme color from manifest.json + const manifest = require("../public/manifest.json"); + this.config.themeColor = manifest.theme_color; + // TODO: Remove in future release if (this.config.debug === true) { log.warn("debug option is now an object, see defaults file for more information."); diff --git a/src/server.js b/src/server.js index ca29b286..418fbc92 100644 --- a/src/server.js +++ b/src/server.js @@ -5,7 +5,6 @@ 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"); @@ -33,27 +32,15 @@ module.exports = function() { (Node.js ${colors.green(process.versions.node)} on ${colors.green(process.platform)} ${process.arch})`); log.info(`Configuration file: ${colors.green(Helper.CONFIG_PATH)}`); - if (!fs.existsSync("public/js/bundle.js")) { - log.error(`The client application was not built. Run ${colors.bold("NODE_ENV=production npm run build")} to resolve this.`); - process.exit(); - } - var app = express() + .disable("x-powered-by") .use(allRequests) .use(index) .use(express.static("public")) .use("/storage/", express.static(Helper.getStoragePath(), { redirect: false, maxAge: 86400 * 1000, - })) - .engine("html", expressHandlebars({ - extname: ".html", - helpers: { - tojson: (c) => JSON.stringify(c), - }, - })) - .set("view engine", "html") - .set("views", path.join(__dirname, "..", "public")); + })); app.get("/themes/:theme.css", (req, res) => { const themeName = req.params.theme; @@ -205,13 +192,6 @@ function index(req, res, next) { return next(); } - var data = _.merge( - pkg, - Helper.config - ); - data.gitCommit = Helper.getGitCommit(); - data.themes = themes.getAll(); - const policies = [ "default-src *", "connect-src 'self' ws: wss:", @@ -228,9 +208,17 @@ function index(req, res, next) { policies.unshift("block-all-mixed-content"); } + res.setHeader("Content-Type", "text/html"); res.setHeader("Content-Security-Policy", policies.join("; ")); res.setHeader("Referrer-Policy", "no-referrer"); - res.render("index", data); + + return fs.readFile(path.join(__dirname, "..", "public", "index.html"), "utf-8", (err, file) => { + if (err) { + throw err; + } + + res.send(_.template(file)(Helper.config)); + }); } function initializeClient(socket, client, token, lastMessage) { @@ -467,6 +455,28 @@ function initializeClient(socket, client, token, lastMessage) { } } +function getClientConfiguration() { + const config = _.pick(Helper.config, [ + "public", + "lockNetwork", + "displayNetwork", + "useHexIp", + "themes", + "prefetch", + ]); + + config.ldapEnabled = Helper.config.ldap.enable; + config.version = pkg.version; + config.gitCommit = Helper.getGitCommit(); + config.themes = themes.getAll(); + + if (config.displayNetwork) { + config.defaults = Helper.config.defaults; + } + + return config; +} + function performAuthentication(data) { const socket = this; let client; @@ -474,6 +484,8 @@ function performAuthentication(data) { const finalInit = () => initializeClient(socket, client, data.token || null, data.lastMessage || -1); const initClient = () => { + socket.emit("configuration", getClientConfiguration()); + client.ip = getClientIp(socket); // If webirc is enabled perform reverse dns lookup diff --git a/test/server.js b/test/server.js index 4c0e9005..d417bc68 100644 --- a/test/server.js +++ b/test/server.js @@ -23,7 +23,7 @@ describe("Server", () => { request(webURL, (error, response, body) => { expect(error).to.be.null; expect(body).to.include("The Lounge"); - expect(body).to.include("https://thelounge.github.io/"); + expect(body).to.include("js/bundle.js"); done(); });