Merge pull request #1683 from thelounge/astorije/split-index

Split index.html into components
This commit is contained in:
Pavel Djundik 2017-11-20 12:10:52 +02:00 committed by GitHub
commit 489bb8e395
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1070 additions and 1008 deletions

View file

@ -9,23 +9,23 @@
<link rel="preload" as="script" href="js/bundle.js">
<link rel="stylesheet" href="css/bootstrap.css">
<link rel="stylesheet" href="css/style.css">
<link id="theme" rel="stylesheet" href="{{ theme }}">
<link id="theme" rel="stylesheet" href="<%- theme %>">
<style id="user-specified-css"></style>
<title>The Lounge</title>
<link rel="shortcut icon" href="img/favicon.png" data-other="img/favicon-notification.png" data-toggled="false" id="favicon">
<link rel="apple-touch-icon" sizes="120x120" href="img/apple-touch-icon-120x120.png">
<link rel="mask-icon" href="img/logo.svg" color="#455164">
<link rel="mask-icon" href="img/logo.svg" color="<%- themeColor %>">
<link rel="manifest" href="manifest.json">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#455164">
<meta name="theme-color" content="<%- themeColor %>">
</head>
<body class="signed-out {{#if public}}public{{/if}}" data-transports="{{tojson transports}}">
<body class="signed-out<%- public ? " public" : "" %>" data-transports="<%- JSON.stringify(transports) %>">
<div id="wrap">
<div id="viewport">
@ -79,805 +79,10 @@
</div>
</form>
</div>
<div id="sign-in" class="window">
<form class="container" method="post" action="">
<div class="row">
<div class="col-xs-12">
<h1 class="title">Sign in to The Lounge</h1>
</div>
<div class="col-xs-12">
<label>
Username
<input class="input" name="user">
</label>
</div>
<div class="col-xs-12">
<label>
Password
<input class="input" type="password" name="password">
</label>
</div>
<div class="col-xs-12 error" style="display: none;">
Authentication failed.
</div>
<div class="col-xs-12">
<button type="submit" class="btn">
Sign in
</button>
</div>
</div>
</form>
</div>
<div id="connect" class="window">
<div class="header">
<button class="lt" aria-label="Toggle channel list"></button>
</div>
<form class="container" method="post" action="">
<div class="row">
<div class="col-sm-12">
<h1 class="title">
{{#if public}}The Lounge - {{/if}}
Connect
{{#unless displayNetwork}}
{{#if lockNetwork}}
to {{defaults.name}}
{{/if}}
{{/unless}}
</h1>
</div>
{{#if displayNetwork}}
<div>
<div class="col-sm-12">
<h2>Network settings</h2>
</div>
<div class="col-sm-3">
<label for="connect:name">Name</label>
</div>
<div class="col-sm-9">
<input class="input" id="connect:name" name="name" value="{{defaults.name}}">
</div>
<div class="col-sm-3">
<label for="connect:host">Server</label>
</div>
<div class="col-sm-6 col-xs-8">
<input class="input" id="connect:host" name="host" value="{{defaults.host}}" aria-label="Server address" {{#if lockNetwork}}disabled{{/if}}>
</div>
<div class="col-sm-3 col-xs-4">
<div class="port">
<input class="input" type="number" min="1" max="65535" name="port" value="{{defaults.port}}" aria-label="Server port" {{#if lockNetwork}}disabled{{/if}}>
</div>
</div>
<div class="clearfix"></div>
<div class="col-sm-3">
<label for="connect:password">Password</label>
</div>
<div class="col-sm-9">
<input class="input" id="connect:password" type="password" name="password" value="{{defaults.password}}">
</div>
<div class="col-sm-9 col-sm-offset-3">
<label class="tls">
<input type="checkbox" name="tls" {{#if defaults.tls}}checked{{/if}} {{#if lockNetwork}}disabled{{/if}}>
Enable TLS/SSL
</label>
</div>
<div class="clearfix"></div>
</div>
{{/if}}
<div class="col-sm-12">
<h2>User preferences</h2>
</div>
<div class="col-sm-3">
<label for="connect:nick">Nick</label>
</div>
<div class="col-sm-9">
<input class="input nick" id="connect:nick" name="nick" value="{{defaults.nick}}">
</div>
{{#unless useHexIp}}
<div class="col-sm-3">
<label for="connect:username">Username</label>
</div>
<div class="col-sm-9">
<input class="input username" id="connect:username" name="username" value="{{defaults.username}}">
</div>
{{/unless}}
<div class="col-sm-3">
<label for="connect:realname">Real name</label>
</div>
<div class="col-sm-9">
<input class="input" id="connect:realname" name="realname" value="{{defaults.realname}}">
</div>
<div class="col-sm-3">
<label for="connect:channels">Channels</label>
</div>
<div class="col-sm-9">
<input class="input" id="connect:channels" name="join" value="{{defaults.join}}">
</div>
<div class="col-sm-9 col-sm-offset-3">
<button type="submit" class="btn">Connect</button>
</div>
</div>
</form>
</div>
<div id="settings" class="window" data-type="settings">
<div class="header">
<button class="lt" aria-label="Toggle channel list"></button>
</div>
<div class="container">
<h1 class="title">Settings</h1>
<div class="row">
<div class="col-sm-12">
<h2>Messages</h2>
</div>
<div class="col-sm-6">
<label class="opt">
<input type="checkbox" name="motd">
Show <abbr title="Message Of The Day">MOTD</abbr>
</label>
</div>
<div class="col-sm-6">
<label class="opt">
<input type="checkbox" name="showSeconds">
Show seconds in timestamp
</label>
</div>
<div class="col-sm-12">
<h2>
Status messages
<span class="tooltipped tooltipped-n tooltipped-no-delay" aria-label="Joins, parts, kicks, nick changes, away changes, and mode changes">
<button class="extra-help" aria-label="Joins, parts, kicks, nick changes, away changes, and mode changes"></button>
</span>
</h2>
</div>
<div class="col-sm-12">
<label class="opt">
<input type="radio" name="statusMessages" value="shown">
Show all status messages individually
</label>
<label class="opt">
<input type="radio" name="statusMessages" value="condensed">
Condense status messages together
</label>
<label class="opt">
<input type="radio" name="statusMessages" value="hidden">
Hide all status messages
</label>
</div>
<div class="col-sm-12">
<h2>Visual Aids</h2>
</div>
<div class="col-sm-12">
<label class="opt">
<input type="checkbox" name="coloredNicks">
Enable colored nicknames
</label>
<label class="opt">
<input type="checkbox" name="autocomplete">
Enable autocomplete
</label>
</div>
<div class="col-sm-12">
<h2>Theme</h2>
</div>
<div class="col-sm-12">
<label for="theme-select" class="sr-only">Theme</label>
<select id="theme-select" name="theme" class="input">
{{#each themes}}
<option value="{{name}}">
{{displayName}}
</option>
{{/each}}
</select>
</div>
{{#if prefetch}}
<div class="col-sm-12">
<h2>Link previews</h2>
</div>
<div class="col-sm-6">
<label class="opt">
<input type="checkbox" name="thumbnails">
Auto-expand images
</label>
</div>
<div class="col-sm-6">
<label class="opt">
<input type="checkbox" name="links">
Auto-expand websites
</label>
</div>
{{/if}}
{{#unless public}}
<div class="col-sm-12">
<h2>Push Notifications</h2>
</div>
<div class="col-sm-12">
<button type="button" class="btn" id="pushNotifications" disabled data-text-alternate="Unsubscribe from push notifications">Subscribe to push notifications</button>
<div class="error" id="pushNotificationsHttps">
<strong>Warning</strong>:
Push notifications are only supported over HTTPS connections.
</div>
<div class="error" id="pushNotificationsUnsupported">
<strong>Warning</strong>:
<span>Push notifications are not supported by your browser.</span>
</div>
</div>
{{/unless}}
<div class="col-sm-12">
<h2>Browser Notifications</h2>
</div>
<div class="col-sm-12">
<label class="opt">
<input id="desktopNotifications" type="checkbox" name="desktopNotifications">
Enable browser notifications<br>
<div class="error" id="warnUnsupportedDesktopNotifications">
<strong>Warning</strong>:
Notifications are not supported by your browser.
</div>
<div class="error" id="warnBlockedDesktopNotifications">
<strong>Warning</strong>:
Notifications are blocked by your browser.
</div>
</label>
</div>
<div class="col-sm-12">
<label class="opt">
<input type="checkbox" name="notification">
Enable notification sound
</label>
</div>
<div class="col-sm-12">
<div class="opt">
<button id="play">Play sound</button>
</div>
</div>
<div class="col-sm-12">
<label class="opt">
<input type="checkbox" name="notifyAllMessages">
Enable notification for all messages
</label>
</div>
<div class="col-sm-12">
<label class="opt">
<label for="highlights" class="sr-only">Custom highlights (comma-separated keywords)</label>
<input type="text" id="highlights" name="highlights" class="input" placeholder="Custom highlights (comma-separated keywords)">
</label>
</div>
{{#unless public}}
{{#unless ldap.enable}}
<div id="change-password">
<form action="" method="post">
<div class="col-sm-12">
<h2>Change password</h2>
</div>
<div class="col-sm-12">
<label for="old_password_input" class="sr-only">Enter current password</label>
<input type="password" id="old_password_input" name="old_password" class="input" placeholder="Enter current password">
</div>
<div class="col-sm-12">
<label for="new_password_input" class="sr-only">Enter desired new password</label>
<input type="password" id="new_password_input" name="new_password" class="input" placeholder="Enter desired new password">
</div>
<div class="col-sm-12">
<label for="verify_password_input" class="sr-only">Repeat new password</label>
<input type="password" id="verify_password_input" name="verify_password" class="input" placeholder="Repeat new password">
</div>
<div class="col-sm-12 feedback"></div>
<div class="col-sm-12">
<button type="submit" class="btn">Change password</button>
</div>
</form>
</div>
{{/unless}}
{{/unless}}
<div class="col-sm-12">
<h2>Custom Stylesheet</h2>
</div>
<div class="col-sm-12">
<textarea class="input" name="userStyles" id="user-specified-css-input" placeholder="/* You can override any style with CSS here */"></textarea>
</div>
</div>
{{#unless public}}
<div class="session-list">
<h2>Sessions</h2>
<h3>Current session</h3>
<div id="session-current"></div>
<h3>Other sessions</h3>
<div id="session-list"></div>
</div>
{{/unless}}
</div>
</div>
<div id="help" class="window">
<div class="header">
<button class="lt" aria-label="Toggle channel list"></button>
</div>
<div class="container">
<h1 class="title">Help</h1>
<h2>Keyboard Shortcuts</h2>
<div class="help-item">
<div class="subject">
<kbd class="key-all">Ctrl</kbd><kbd class="key-apple"></kbd> + <kbd></kbd> / <kbd></kbd>
</div>
<div class="description">
<p>Switch to the previous/next window in the channel list</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<kbd class="key-all">Ctrl</kbd><kbd class="key-apple"></kbd> + <kbd>K</kbd>
</div>
<div class="description">
<p>
Mark any text typed after this shortcut to be colored. After
hitting this shortcut, enter an integer in the range
<code>0—15</code> to select the desired color, or use the
autocompletion menu to choose a color name (see below).
</p>
<p>
Background color can be specified by putting a comma and
another integer in the range <code>0—15</code> after the
foreground color number (autocompletion works too).
</p>
<p>
A color reference can be found
<a href="https://modern.ircdocs.horse/formatting.html#colors" target="_blank" rel="noopener">here</a>.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<kbd class="key-all">Ctrl</kbd><kbd class="key-apple"></kbd> + <kbd>B</kbd>
</div>
<div class="description">
<p>Mark all text typed after this shortcut as bold.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<kbd class="key-all">Ctrl</kbd><kbd class="key-apple"></kbd> + <kbd>U</kbd>
</div>
<div class="description">
<p>Mark all text typed after this shortcut as underlined.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<kbd class="key-all">Ctrl</kbd><kbd class="key-apple"></kbd> + <kbd>I</kbd>
</div>
<div class="description">
<p>Mark all text typed after this shortcut as italics.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<kbd class="key-all">Ctrl</kbd><kbd class="key-apple"></kbd> + <kbd>O</kbd>
</div>
<div class="description">
<p>
Mark all text typed after this shortcut to be reset to its
original formatting.
</p>
</div>
</div>
<h2>Autocompletion</h2>
<p>
To auto-complete nicknames, channels, commands, and emoji, type one of the characters below to open
a suggestion list. Use the <kbd></kbd> and <kbd></kbd> keys to highlight an item, and insert it by
pressing <kbd>Tab</kbd> or <kbd>Enter</kbd> (or by clicking the desired item).
</p>
<p>
Autocompletion can be disabled in settings.
</p>
<div class="help-item">
<div class="subject">
<code>@</code>
</div>
<div class="description">
<p>Nickname</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>#</code>
</div>
<div class="description">
<p>Channel</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/</code>
</div>
<div class="description">
<p>Commands (see list of commands below)</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>:</code>
</div>
<div class="description">
<p>Emoji (note: requires two search characters, to avoid conflicting with common emoticons like <code>:)</code>)</p>
</div>
</div>
<h2>Commands</h2>
<div class="help-item">
<div class="subject">
<code>/away [message]</code>
</div>
<div class="description">
<p>Mark yourself as away with an optional message.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/back</code>
</div>
<div class="description">
<p>Remove your away status (set with <code>/away</code>).</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/ban nick</code>
</div>
<div class="description">
<p>Ban (<code>+b</code>) a user from the current channel.
This can be a nickname or a hostmask.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/banlist</code>
</div>
<div class="description">
<p>Load the banlist for the current channel.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/collapse</code>
</div>
<div class="description">
<p>
Collapse all previews in the current channel (opposite of
<code>/expand</code>)
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/connect host [port]</code>
</div>
<div class="description">
<p>
Connect to a new IRC network. If <code>port</code> starts with
a <code>+</code> sign, the connection will be made secure
using TLS.
</p>
<p>Alias: <code>/server</code></p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/ctcp target cmd [args]</code>
</div>
<div class="description">
<p>
Send a <abbr title="Client-to-client protocol">CTCP</abbr>
request. Read more about this on
<a href="https://en.wikipedia.org/wiki/Client-to-client_protocol" target="_blank" rel="noopener">the dedicated Wikipedia article</a>.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/deop nick [...nick]</code>
</div>
<div class="description">
<p>
Remove op (<code>-o</code>) from one or several users in the
current channel.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/devoice nick [...nick]</code>
</div>
<div class="description">
<p>
Remove voice (<code>-v</code>) from one or several users in
the current channel.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/disconnect [message]</code>
</div>
<div class="description">
<p>
Disconnect from the current network with an
optionally-provided message.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/expand</code>
</div>
<div class="description">
<p>
Expand all previews in the current channel (opposite of
<code>/collapse</code>)
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/invite nick [channel]</code>
</div>
<div class="description">
<p>
Invite a user to the specified channel. If
<code>channel</code> is ommitted, user will be invited to the
current channel.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/join channel</code>
</div>
<div class="description">
<p>Join a channel.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/kick nick</code>
</div>
<div class="description">
<p>Kick a user from the current channel.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/list</code>
</div>
<div class="description">
<p>Retrieve a list of available channels on this network.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/me message</code>
</div>
<div class="description">
<p>
Send an action message to the current channel. The Lounge will
display it inline, as if the message was posted in the third
person.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/mode flags [args]</code>
</div>
<div class="description">
<p>
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.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/msg channel message</code>
</div>
<div class="description">
<p>Send a message to the specified channel.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/nick newnick</code>
</div>
<div class="description">
<p>Change your nickname on the current network.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/notice channel message</code>
</div>
<div class="description">
<p>Sends a notice message to the specified channel.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/op nick [...nick]</code>
</div>
<div class="description">
<p>
Give op (<code>+o</code>) to one or several users in the
current channel.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/part [channel]</code>
</div>
<div class="description">
<p>
Close the specified channel or private message window, or the
current channel if <code>channel</code> is ommitted.
</p>
<p>Aliases: <code>/close</code>, <code>/leave</code></p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/rejoin</code>
</div>
<div class="description">
<p>
Leave and immediately rejoin the current channel. Useful to
quickly get op from ChanServ in an empty channel, for example.
</p>
<p>Alias: <code>/cycle</code></p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/query nick</code>
</div>
<div class="description">
<p>Send a private message to the specified user.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/quit [message]</code>
</div>
<div class="description">
<p>
Disconnect from the current network with an optional message.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/raw message</code>
</div>
<div class="description">
<p>Send a raw message to the current IRC network.</p>
<p>Aliases: <code>/quote</code>, <code>/send</code></p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/slap nick</code>
</div>
<div class="description">
<p>Slap someone in the current channel with a trout!</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/topic newtopic</code>
</div>
<div class="description">
<p>Set the topic in the current channel.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/unban nick</code>
</div>
<div class="description">
<p>Unban (<code>-b</code>) a user from the current channel.
This can be a nickname or a hostmask.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/voice nick [...nick]</code>
</div>
<div class="description">
<p>
Give voice (<code>+v</code>) to one or several users in the
current channel.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/whois nick</code>
</div>
<div class="description">
<p>
Retrieve information about the given user on the current
network.
</p>
</div>
</div>
<h2>About The Lounge</h2>
<p class="about">
{{#if gitCommit}}
The Lounge is running from source
(<a href="https://github.com/thelounge/lounge/tree/{{ gitCommit }}" target="_blank" rel="noopener"><code>{{ gitCommit }}</code></a>).<br>
{{else}}
The Lounge is in version <strong>{{version}}</strong>
(<a href="https://github.com/thelounge/lounge/releases/tag/v{{ version }}" target="_blank" rel="noopener">See release notes</a>).<br>
{{/if}}
<a href="https://thelounge.github.io/" target="_blank" rel="noopener">Website</a><br>
<a href="https://thelounge.github.io/docs/" target="_blank" rel="noopener">Documentation</a><br>
<a href="https://github.com/thelounge/lounge/issues/new" target="_blank" rel="noopener">Report a bug</a>
</p>
</div>
</div>
<div id="sign-in" class="window"></div>
<div id="connect" class="window"></div>
<div id="settings" class="window" data-type="settings"></div>
<div id="help" class="window"></div>
</div>
</div>
</div>

View file

@ -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);

View file

@ -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);
}

View file

@ -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();

View file

@ -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);
});
});

View file

@ -18,3 +18,4 @@ require("./topic");
require("./users");
require("./sign_out");
require("./sessions_list");
require("./configuration");

View file

@ -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);

View file

@ -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);

View file

@ -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"),

View file

@ -0,0 +1,88 @@
<div class="header">
<button class="lt" aria-label="Toggle channel list"></button>
</div>
<form class="container" method="post" action="" data-event="conn">
<div class="row">
<div class="col-sm-12">
<h1 class="title">
{{#if public}}The Lounge - {{/if}}
Connect
{{#unless displayNetwork}}
{{#if lockNetwork}}
to {{defaults.name}}
{{/if}}
{{/unless}}
</h1>
</div>
{{#if displayNetwork}}
<div>
<div class="col-sm-12">
<h2>Network settings</h2>
</div>
<div class="col-sm-3">
<label for="connect:name">Name</label>
</div>
<div class="col-sm-9">
<input class="input" id="connect:name" name="name" value="{{defaults.name}}">
</div>
<div class="col-sm-3">
<label for="connect:host">Server</label>
</div>
<div class="col-sm-6 col-xs-8">
<input class="input" id="connect:host" name="host" value="{{defaults.host}}" aria-label="Server address" {{#if lockNetwork}}disabled{{/if}}>
</div>
<div class="col-sm-3 col-xs-4">
<div class="port">
<input class="input" type="number" min="1" max="65535" name="port" value="{{defaults.port}}" aria-label="Server port" {{#if lockNetwork}}disabled{{/if}}>
</div>
</div>
<div class="clearfix"></div>
<div class="col-sm-3">
<label for="connect:password">Password</label>
</div>
<div class="col-sm-9">
<input class="input" id="connect:password" type="password" name="password" value="{{defaults.password}}">
</div>
<div class="col-sm-9 col-sm-offset-3">
<label class="tls">
<input type="checkbox" name="tls" {{#if defaults.tls}}checked{{/if}} {{#if lockNetwork}}disabled{{/if}}>
Enable TLS/SSL
</label>
</div>
<div class="clearfix"></div>
</div>
{{/if}}
<div class="col-sm-12">
<h2>User preferences</h2>
</div>
<div class="col-sm-3">
<label for="connect:nick">Nick</label>
</div>
<div class="col-sm-9">
<input class="input nick" id="connect:nick" name="nick" value="{{defaults.nick}}">
</div>
{{#unless useHexIp}}
<div class="col-sm-3">
<label for="connect:username">Username</label>
</div>
<div class="col-sm-9">
<input class="input username" id="connect:username" name="username" value="{{defaults.username}}">
</div>
{{/unless}}
<div class="col-sm-3">
<label for="connect:realname">Real name</label>
</div>
<div class="col-sm-9">
<input class="input" id="connect:realname" name="realname" value="{{defaults.realname}}">
</div>
<div class="col-sm-3">
<label for="connect:channels">Channels</label>
</div>
<div class="col-sm-9">
<input class="input" id="connect:channels" name="join" value="{{defaults.join}}">
</div>
<div class="col-sm-9 col-sm-offset-3">
<button type="submit" class="btn">Connect</button>
</div>
</div>
</form>

View file

@ -0,0 +1,482 @@
<div class="header">
<button class="lt" aria-label="Toggle channel list"></button>
</div>
<div class="container">
<h1 class="title">Help</h1>
<h2>Keyboard Shortcuts</h2>
<div class="help-item">
<div class="subject">
<kbd class="key-all">Ctrl</kbd><kbd class="key-apple">⌘</kbd> + <kbd>↑</kbd> / <kbd>↓</kbd>
</div>
<div class="description">
<p>Switch to the previous/next window in the channel list</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<kbd class="key-all">Ctrl</kbd><kbd class="key-apple">⌘</kbd> + <kbd>K</kbd>
</div>
<div class="description">
<p>
Mark any text typed after this shortcut to be colored. After
hitting this shortcut, enter an integer in the range
<code>0—15</code> to select the desired color, or use the
autocompletion menu to choose a color name (see below).
</p>
<p>
Background color can be specified by putting a comma and
another integer in the range <code>0—15</code> after the
foreground color number (autocompletion works too).
</p>
<p>
A color reference can be found
<a href="https://modern.ircdocs.horse/formatting.html#colors" target="_blank" rel="noopener">here</a>.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<kbd class="key-all">Ctrl</kbd><kbd class="key-apple">⌘</kbd> + <kbd>B</kbd>
</div>
<div class="description">
<p>Mark all text typed after this shortcut as bold.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<kbd class="key-all">Ctrl</kbd><kbd class="key-apple">⌘</kbd> + <kbd>U</kbd>
</div>
<div class="description">
<p>Mark all text typed after this shortcut as underlined.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<kbd class="key-all">Ctrl</kbd><kbd class="key-apple">⌘</kbd> + <kbd>I</kbd>
</div>
<div class="description">
<p>Mark all text typed after this shortcut as italics.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<kbd class="key-all">Ctrl</kbd><kbd class="key-apple">⌘</kbd> + <kbd>O</kbd>
</div>
<div class="description">
<p>
Mark all text typed after this shortcut to be reset to its
original formatting.
</p>
</div>
</div>
<h2>Autocompletion</h2>
<p>
To auto-complete nicknames, channels, commands, and emoji, type one of the characters below to open
a suggestion list. Use the <kbd>↑</kbd> and <kbd>↓</kbd> keys to highlight an item, and insert it by
pressing <kbd>Tab</kbd> or <kbd>Enter</kbd> (or by clicking the desired item).
</p>
<p>
Autocompletion can be disabled in settings.
</p>
<div class="help-item">
<div class="subject">
<code>@</code>
</div>
<div class="description">
<p>Nickname</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>#</code>
</div>
<div class="description">
<p>Channel</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/</code>
</div>
<div class="description">
<p>Commands (see list of commands below)</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>:</code>
</div>
<div class="description">
<p>Emoji (note: requires two search characters, to avoid conflicting with common emoticons like <code>:)</code>)</p>
</div>
</div>
<h2>Commands</h2>
<div class="help-item">
<div class="subject">
<code>/away [message]</code>
</div>
<div class="description">
<p>Mark yourself as away with an optional message.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/back</code>
</div>
<div class="description">
<p>Remove your away status (set with <code>/away</code>).</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/ban nick</code>
</div>
<div class="description">
<p>Ban (<code>+b</code>) a user from the current channel.
This can be a nickname or a hostmask.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/banlist</code>
</div>
<div class="description">
<p>Load the banlist for the current channel.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/collapse</code>
</div>
<div class="description">
<p>
Collapse all previews in the current channel (opposite of
<code>/expand</code>)
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/connect host [port]</code>
</div>
<div class="description">
<p>
Connect to a new IRC network. If <code>port</code> starts with
a <code>+</code> sign, the connection will be made secure
using TLS.
</p>
<p>Alias: <code>/server</code></p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/ctcp target cmd [args]</code>
</div>
<div class="description">
<p>
Send a <abbr title="Client-to-client protocol">CTCP</abbr>
request. Read more about this on
<a href="https://en.wikipedia.org/wiki/Client-to-client_protocol" target="_blank" rel="noopener">the dedicated Wikipedia article</a>.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/deop nick [...nick]</code>
</div>
<div class="description">
<p>
Remove op (<code>-o</code>) from one or several users in the
current channel.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/devoice nick [...nick]</code>
</div>
<div class="description">
<p>
Remove voice (<code>-v</code>) from one or several users in
the current channel.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/disconnect [message]</code>
</div>
<div class="description">
<p>
Disconnect from the current network with an
optionally-provided message.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/expand</code>
</div>
<div class="description">
<p>
Expand all previews in the current channel (opposite of
<code>/collapse</code>)
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/invite nick [channel]</code>
</div>
<div class="description">
<p>
Invite a user to the specified channel. If
<code>channel</code> is ommitted, user will be invited to the
current channel.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/join channel</code>
</div>
<div class="description">
<p>Join a channel.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/kick nick</code>
</div>
<div class="description">
<p>Kick a user from the current channel.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/list</code>
</div>
<div class="description">
<p>Retrieve a list of available channels on this network.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/me message</code>
</div>
<div class="description">
<p>
Send an action message to the current channel. The Lounge will
display it inline, as if the message was posted in the third
person.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/mode flags [args]</code>
</div>
<div class="description">
<p>
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.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/msg channel message</code>
</div>
<div class="description">
<p>Send a message to the specified channel.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/nick newnick</code>
</div>
<div class="description">
<p>Change your nickname on the current network.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/notice channel message</code>
</div>
<div class="description">
<p>Sends a notice message to the specified channel.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/op nick [...nick]</code>
</div>
<div class="description">
<p>
Give op (<code>+o</code>) to one or several users in the
current channel.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/part [channel]</code>
</div>
<div class="description">
<p>
Close the specified channel or private message window, or the
current channel if <code>channel</code> is ommitted.
</p>
<p>Aliases: <code>/close</code>, <code>/leave</code></p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/rejoin</code>
</div>
<div class="description">
<p>
Leave and immediately rejoin the current channel. Useful to
quickly get op from ChanServ in an empty channel, for example.
</p>
<p>Alias: <code>/cycle</code></p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/query nick</code>
</div>
<div class="description">
<p>Send a private message to the specified user.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/quit [message]</code>
</div>
<div class="description">
<p>
Disconnect from the current network with an optional message.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/raw message</code>
</div>
<div class="description">
<p>Send a raw message to the current IRC network.</p>
<p>Aliases: <code>/quote</code>, <code>/send</code></p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/slap nick</code>
</div>
<div class="description">
<p>Slap someone in the current channel with a trout!</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/topic newtopic</code>
</div>
<div class="description">
<p>Set the topic in the current channel.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/unban nick</code>
</div>
<div class="description">
<p>Unban (<code>-b</code>) a user from the current channel.
This can be a nickname or a hostmask.</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/voice nick [...nick]</code>
</div>
<div class="description">
<p>
Give voice (<code>+v</code>) to one or several users in the
current channel.
</p>
</div>
</div>
<div class="help-item">
<div class="subject">
<code>/whois nick</code>
</div>
<div class="description">
<p>
Retrieve information about the given user on the current
network.
</p>
</div>
</div>
<h2>About The Lounge</h2>
<p class="about">
{{#if gitCommit}}
The Lounge is running from source
(<a href="https://github.com/thelounge/lounge/tree/{{ gitCommit }}" target="_blank" rel="noopener"><code>{{ gitCommit }}</code></a>).<br>
{{else}}
The Lounge is in version <strong>{{version}}</strong>
(<a href="https://github.com/thelounge/lounge/releases/tag/v{{ version }}" target="_blank" rel="noopener">See release notes</a>).<br>
{{/if}}
<a href="https://thelounge.github.io/" target="_blank" rel="noopener">Website</a><br>
<a href="https://thelounge.github.io/docs/" target="_blank" rel="noopener">Documentation</a><br>
<a href="https://github.com/thelounge/lounge/issues/new" target="_blank" rel="noopener">Report a bug</a>
</p>
</div>

View file

@ -0,0 +1,193 @@
<div class="header">
<button class="lt" aria-label="Toggle channel list"></button>
</div>
<div class="container">
<h1 class="title">Settings</h1>
<div class="row">
<div class="col-sm-12">
<h2>Messages</h2>
</div>
<div class="col-sm-6">
<label class="opt">
<input type="checkbox" name="motd">
Show <abbr title="Message Of The Day">MOTD</abbr>
</label>
</div>
<div class="col-sm-6">
<label class="opt">
<input type="checkbox" name="showSeconds">
Show seconds in timestamp
</label>
</div>
<div class="col-sm-12">
<h2>
Status messages
<span class="tooltipped tooltipped-n tooltipped-no-delay" aria-label="Joins, parts, kicks, nick changes, away changes, and mode changes">
<button class="extra-help" aria-label="Joins, parts, kicks, nick changes, away changes, and mode changes"></button>
</span>
</h2>
</div>
<div class="col-sm-12">
<label class="opt">
<input type="radio" name="statusMessages" value="shown">
Show all status messages individually
</label>
<label class="opt">
<input type="radio" name="statusMessages" value="condensed">
Condense status messages together
</label>
<label class="opt">
<input type="radio" name="statusMessages" value="hidden">
Hide all status messages
</label>
</div>
<div class="col-sm-12">
<h2>Visual Aids</h2>
</div>
<div class="col-sm-12">
<label class="opt">
<input type="checkbox" name="coloredNicks">
Enable colored nicknames
</label>
<label class="opt">
<input type="checkbox" name="autocomplete">
Enable autocomplete
</label>
</div>
<div class="col-sm-12">
<h2>Theme</h2>
</div>
<div class="col-sm-12">
<label for="theme-select" class="sr-only">Theme</label>
<select id="theme-select" name="theme" class="input">
{{#each themes}}
<option value="{{name}}">
{{displayName}}
</option>
{{/each}}
</select>
</div>
{{#if prefetch}}
<div class="col-sm-12">
<h2>Link previews</h2>
</div>
<div class="col-sm-6">
<label class="opt">
<input type="checkbox" name="thumbnails">
Auto-expand images
</label>
</div>
<div class="col-sm-6">
<label class="opt">
<input type="checkbox" name="links">
Auto-expand websites
</label>
</div>
{{/if}}
{{#unless public}}
<div class="col-sm-12">
<h2>Push Notifications</h2>
</div>
<div class="col-sm-12">
<button type="button" class="btn" id="pushNotifications" disabled data-text-alternate="Unsubscribe from push notifications">Subscribe to push notifications</button>
<div class="error" id="pushNotificationsHttps">
<strong>Warning</strong>:
Push notifications are only supported over HTTPS connections.
</div>
<div class="error" id="pushNotificationsUnsupported">
<strong>Warning</strong>:
<span>Push notifications are not supported by your browser.</span>
</div>
</div>
{{/unless}}
<div class="col-sm-12">
<h2>Browser Notifications</h2>
</div>
<div class="col-sm-12">
<label class="opt">
<input id="desktopNotifications" type="checkbox" name="desktopNotifications">
Enable browser notifications<br>
<div class="error" id="warnUnsupportedDesktopNotifications">
<strong>Warning</strong>:
Notifications are not supported by your browser.
</div>
<div class="error" id="warnBlockedDesktopNotifications">
<strong>Warning</strong>:
Notifications are blocked by your browser.
</div>
</label>
</div>
<div class="col-sm-12">
<label class="opt">
<input type="checkbox" name="notification">
Enable notification sound
</label>
</div>
<div class="col-sm-12">
<div class="opt">
<button id="play">Play sound</button>
</div>
</div>
<div class="col-sm-12">
<label class="opt">
<input type="checkbox" name="notifyAllMessages">
Enable notification for all messages
</label>
</div>
<div class="col-sm-12">
<label class="opt">
<label for="highlights" class="sr-only">Custom highlights (comma-separated keywords)</label>
<input type="text" id="highlights" name="highlights" class="input" placeholder="Custom highlights (comma-separated keywords)">
</label>
</div>
{{#unless public}}
{{#unless ldapEnabled}}
<div id="change-password">
<form action="" method="post" data-event="change-password">
<div class="col-sm-12">
<h2>Change password</h2>
</div>
<div class="col-sm-12">
<label for="old_password_input" class="sr-only">Enter current password</label>
<input type="password" id="old_password_input" name="old_password" class="input" placeholder="Enter current password">
</div>
<div class="col-sm-12">
<label for="new_password_input" class="sr-only">Enter desired new password</label>
<input type="password" id="new_password_input" name="new_password" class="input" placeholder="Enter desired new password">
</div>
<div class="col-sm-12">
<label for="verify_password_input" class="sr-only">Repeat new password</label>
<input type="password" id="verify_password_input" name="verify_password" class="input" placeholder="Repeat new password">
</div>
<div class="col-sm-12 feedback"></div>
<div class="col-sm-12">
<button type="submit" class="btn">Change password</button>
</div>
</form>
</div>
{{/unless}}
{{/unless}}
<div class="col-sm-12">
<h2>Custom Stylesheet</h2>
</div>
<div class="col-sm-12">
<textarea class="input" name="userStyles" id="user-specified-css-input" placeholder="/* You can override any style with CSS here */"></textarea>
</div>
</div>
{{#unless public}}
<div class="session-list">
<h2>Sessions</h2>
<h3>Current session</h3>
<div id="session-current"></div>
<h3>Other sessions</h3>
<div id="session-list"></div>
</div>
{{/unless}}
</div>

View file

@ -0,0 +1,23 @@
<form class="container" method="post" action="">
<div class="row">
<div class="col-xs-12">
<h1 class="title">Sign in to The Lounge</h1>
</div>
<div class="col-xs-12">
<label>
Username
<input class="input" name="user" autofocus>
</label>
</div>
<div class="col-xs-12">
<label>
Password
<input class="input" type="password" name="password">
</label>
</div>
<div class="col-xs-12 error" style="display: none;">Authentication failed.</div>
<div class="col-xs-12">
<button type="submit" class="btn">Sign in</button>
</div>
</div>
</form>

View file

@ -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",

View file

@ -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) {

View file

@ -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.");

View file

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

View file

@ -23,7 +23,7 @@ describe("Server", () => {
request(webURL, (error, response, body) => {
expect(error).to.be.null;
expect(body).to.include("<title>The Lounge</title>");
expect(body).to.include("https://thelounge.github.io/");
expect(body).to.include("js/bundle.js");
done();
});