Simplify initial commit and remove unnecessary refactor
This commit is contained in:
commit
f1c40aa8de
@ -9,54 +9,54 @@ env:
|
||||
node: true
|
||||
|
||||
rules:
|
||||
arrow-body-style: 2
|
||||
arrow-parens: [2, always]
|
||||
arrow-spacing: 2
|
||||
block-scoped-var: 2
|
||||
block-spacing: [2, always]
|
||||
brace-style: [2, 1tbs]
|
||||
comma-dangle: 0
|
||||
curly: [2, all]
|
||||
dot-location: [2, property]
|
||||
dot-notation: 2
|
||||
eol-last: 2
|
||||
eqeqeq: 2
|
||||
handle-callback-err: 2
|
||||
indent: [2, tab]
|
||||
key-spacing: [2, {beforeColon: false, afterColon: true}]
|
||||
keyword-spacing: [2, {before: true, after: true}]
|
||||
linebreak-style: [2, unix]
|
||||
no-catch-shadow: 2
|
||||
no-confusing-arrow: 2
|
||||
no-console: 0
|
||||
no-control-regex: 0
|
||||
no-duplicate-imports: 2
|
||||
no-else-return: 2
|
||||
no-implicit-globals: 2
|
||||
no-multi-spaces: 2
|
||||
no-multiple-empty-lines: [2, { "max": 1 }]
|
||||
no-shadow: 2
|
||||
no-template-curly-in-string: 2
|
||||
no-trailing-spaces: 2
|
||||
no-unsafe-negation: 2
|
||||
no-useless-computed-key: 2
|
||||
no-useless-return: 2
|
||||
object-curly-spacing: [2, never]
|
||||
padded-blocks: [2, never]
|
||||
prefer-const: 2
|
||||
quote-props: [2, as-needed]
|
||||
quotes: [2, double, avoid-escape]
|
||||
semi-spacing: 2
|
||||
semi-style: [2, last]
|
||||
semi: [2, always]
|
||||
space-before-blocks: 2
|
||||
space-before-function-paren: [2, never]
|
||||
space-in-parens: [2, never]
|
||||
space-infix-ops: 2
|
||||
spaced-comment: [2, always]
|
||||
strict: 2
|
||||
template-curly-spacing: 2
|
||||
yoda: 2
|
||||
arrow-body-style: error
|
||||
arrow-parens: [error, always]
|
||||
arrow-spacing: error
|
||||
block-scoped-var: error
|
||||
block-spacing: [error, always]
|
||||
brace-style: [error, 1tbs]
|
||||
comma-dangle: off
|
||||
curly: [error, all]
|
||||
dot-location: [error, property]
|
||||
dot-notation: error
|
||||
eol-last: error
|
||||
eqeqeq: error
|
||||
handle-callback-err: error
|
||||
indent: [error, tab]
|
||||
key-spacing: [error, {beforeColon: false, afterColon: true}]
|
||||
keyword-spacing: [error, {before: true, after: true}]
|
||||
linebreak-style: [error, unix]
|
||||
no-alert: error
|
||||
no-catch-shadow: error
|
||||
no-confusing-arrow: error
|
||||
no-control-regex: off
|
||||
no-duplicate-imports: error
|
||||
no-else-return: error
|
||||
no-implicit-globals: error
|
||||
no-multi-spaces: error
|
||||
no-multiple-empty-lines: [error, { "max": 1 }]
|
||||
no-shadow: error
|
||||
no-template-curly-in-string: error
|
||||
no-trailing-spaces: error
|
||||
no-unsafe-negation: error
|
||||
no-useless-computed-key: error
|
||||
no-useless-return: error
|
||||
object-curly-spacing: [error, never]
|
||||
padded-blocks: [error, never]
|
||||
prefer-const: error
|
||||
quote-props: [error, as-needed]
|
||||
quotes: [error, double, avoid-escape]
|
||||
semi-spacing: error
|
||||
semi-style: [error, last]
|
||||
semi: [error, always]
|
||||
space-before-blocks: error
|
||||
space-before-function-paren: [error, never]
|
||||
space-in-parens: [error, never]
|
||||
space-infix-ops: error
|
||||
spaced-comment: [error, always]
|
||||
strict: error
|
||||
template-curly-spacing: error
|
||||
yoda: error
|
||||
|
||||
globals:
|
||||
log: false
|
||||
|
@ -1,93 +0,0 @@
|
||||
Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
Binary file not shown.
Binary file not shown.
@ -1,93 +0,0 @@
|
||||
Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
Binary file not shown.
Binary file not shown.
@ -1,25 +1,3 @@
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
src:
|
||||
local("Lato Regular"),
|
||||
local("Lato-regular"),
|
||||
url("fonts/Lato-regular/Lato-regular.woff2") format("woff2"),
|
||||
url("fonts/Lato-regular/Lato-regular.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
src:
|
||||
local("Lato Bold"),
|
||||
local("Lato-700"),
|
||||
url("fonts/Lato-700/Lato-700.woff2") format("woff2"),
|
||||
url("fonts/Lato-700/Lato-700.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "FontAwesome";
|
||||
font-weight: normal;
|
||||
@ -37,13 +15,14 @@ body {
|
||||
body {
|
||||
background: #455164;
|
||||
color: #222;
|
||||
font: 16px Lato, sans-serif;
|
||||
font: 16px -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
margin: 0;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
touch-action: none;
|
||||
|
||||
/**
|
||||
* Disable pull-to-refresh on mobile that conflicts with scrolling the message list.
|
||||
@ -159,6 +138,7 @@ kbd {
|
||||
.container {
|
||||
margin: 80px auto;
|
||||
max-width: 480px;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
::-moz-placeholder {
|
||||
@ -174,6 +154,7 @@ kbd {
|
||||
color: rgba(0, 0, 0, 0.35) !important;
|
||||
}
|
||||
|
||||
#js-copy-hack,
|
||||
#help,
|
||||
#windows .header .title,
|
||||
#windows .header .topic,
|
||||
@ -185,6 +166,16 @@ kbd {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
#js-copy-hack {
|
||||
position: absolute;
|
||||
left: -999999px;
|
||||
}
|
||||
|
||||
#chat #js-copy-hack .condensed:not(.closed) .msg,
|
||||
#chat #js-copy-hack > .msg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
|
||||
#viewport .lt::before,
|
||||
@ -197,6 +188,8 @@ kbd {
|
||||
#settings .extra-help,
|
||||
#settings #play::before,
|
||||
#form #submit::before,
|
||||
#chat .away .from::before,
|
||||
#chat .back .from::before,
|
||||
#chat .invite .from::before,
|
||||
#chat .join .from::before,
|
||||
#chat .kick .from::before,
|
||||
@ -247,6 +240,12 @@ kbd {
|
||||
|
||||
#form #submit::before { content: "\f1d8"; /* http://fontawesome.io/icon/paper-plane/ */ }
|
||||
|
||||
#chat .away .from::before,
|
||||
#chat .back .from::before {
|
||||
content: "\f017"; /* http://fontawesome.io/icon/clock-o/ */
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
#chat .invite .from::before {
|
||||
content: "\f003"; /* http://fontawesome.io/icon/envelope-o/ */
|
||||
color: #2ecc40;
|
||||
@ -473,6 +472,7 @@ kbd {
|
||||
|
||||
#sidebar .networks {
|
||||
padding: 20px 30px 0;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
#sidebar .networks:empty {
|
||||
@ -482,6 +482,7 @@ kbd {
|
||||
#sidebar .network,
|
||||
#sidebar .network-placeholder {
|
||||
margin-bottom: 30px;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
#sidebar .empty {
|
||||
@ -834,7 +835,7 @@ kbd {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#chat .condensed-summary:hover {
|
||||
#chat .condensed-summary .content:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@ -850,13 +851,18 @@ kbd {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#windows #form .input,
|
||||
#windows .header .topic,
|
||||
.messages .msg,
|
||||
.sidebar {
|
||||
font: 13px Consolas, Menlo, Monaco, "Lucida Console", "DejaVu Sans Mono", "Courier New", monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
#windows #form .input {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#windows #chat .header {
|
||||
display: block;
|
||||
}
|
||||
@ -896,6 +902,7 @@ kbd {
|
||||
right: 0;
|
||||
width: 180px;
|
||||
transition: right 0.4s;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
#chat .show-more {
|
||||
@ -924,6 +931,7 @@ kbd {
|
||||
|
||||
#chat .messages {
|
||||
padding: 10px 0;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
#chat .msg {
|
||||
@ -1001,7 +1009,6 @@ kbd {
|
||||
#chat .time,
|
||||
#chat .from,
|
||||
#chat .content {
|
||||
display: block;
|
||||
padding: 2px 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
@ -1147,6 +1154,8 @@ kbd {
|
||||
}
|
||||
|
||||
#chat .condensed .content,
|
||||
#chat .away .content,
|
||||
#chat .back .content,
|
||||
#chat .join .content,
|
||||
#chat .kick .content,
|
||||
#chat .mode .content,
|
||||
@ -1286,6 +1295,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
width: 100%;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
#chat .names-filtered {
|
||||
@ -1463,6 +1473,15 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.is-apple #help .key-all,
|
||||
#help .key-apple {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.is-apple #help .key-apple {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#form {
|
||||
background: #eee;
|
||||
border-top: 1px solid #ddd;
|
||||
@ -1472,7 +1491,6 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
}
|
||||
|
||||
#windows #form .input {
|
||||
font: 13px Consolas, Menlo, Monaco, "Lucida Console", "DejaVu Sans Mono", "Courier New", monospace;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 2px;
|
||||
margin: 0;
|
||||
@ -1485,6 +1503,15 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
}
|
||||
|
||||
#connection-error {
|
||||
font-size: 12px;
|
||||
line-height: 36px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
word-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -1502,7 +1529,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
background: #f6f6f6;
|
||||
color: #666;
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
margin: 4px;
|
||||
line-height: 22px;
|
||||
height: 24px;
|
||||
@ -1561,6 +1588,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
-webkit-flex: 1 0 auto;
|
||||
flex: 1 0 auto;
|
||||
align-self: center;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
#form #submit {
|
||||
@ -1671,7 +1699,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
z-index: 1000000;
|
||||
display: none;
|
||||
padding: 5px 8px;
|
||||
font: 12px Lato;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
color: #fff;
|
||||
@ -1976,6 +2004,10 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
margin-top: 60px !important;
|
||||
}
|
||||
|
||||
.messages .msg {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#sidebar,
|
||||
#footer {
|
||||
left: -220px;
|
||||
@ -2019,14 +2051,6 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1610px) {
|
||||
#windows .header .topic,
|
||||
.messages .msg,
|
||||
.sidebar {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 479px) {
|
||||
.container {
|
||||
margin: 40px 0 !important;
|
||||
@ -2180,6 +2204,13 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
|
||||
/* Top/Bottom margins + button height + image/button margin */
|
||||
max-height: calc(100vh - 2 * 10px - 37px - 10px);
|
||||
|
||||
/* Checkered background for transparent images */
|
||||
background-position: 0 0, 10px 10px;
|
||||
background-size: 20px 20px;
|
||||
background-image:
|
||||
linear-gradient(45deg, #eee 25%, rgba(0, 0, 0, 0) 25%, rgba(0, 0, 0, 0) 75%, #eee 75%, #eee 100%),
|
||||
linear-gradient(45deg, #eee 25%, #fff 25%, #fff 75%, #eee 75%, #eee 100%);
|
||||
}
|
||||
|
||||
#image-viewer .open-btn {
|
||||
|
@ -63,7 +63,7 @@
|
||||
</div>
|
||||
<div id="chat-container" class="window">
|
||||
<div id="chat"></div>
|
||||
<button id="connection-error" class="btn btn-reconnect">Client connection lost — Click here to reconnect</button>
|
||||
<div id="connection-error"></div>
|
||||
<form id="form" method="post" action="">
|
||||
<div class="input">
|
||||
<span id="nick">
|
||||
@ -225,8 +225,8 @@
|
||||
<div class="col-sm-12">
|
||||
<h2>
|
||||
Status messages
|
||||
<span class="tooltipped tooltipped-n tooltipped-no-delay" aria-label="Joins, parts, kicks, nick changes, and mode changes">
|
||||
<button class="extra-help" aria-label="Joins, parts, kicks, nick changes, and mode changes"></button>
|
||||
<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>
|
||||
@ -264,8 +264,8 @@
|
||||
<label for="theme-select" class="sr-only">Theme</label>
|
||||
<select id="theme-select" name="theme" class="input">
|
||||
{{#each themes}}
|
||||
<option value="{{filename}}">
|
||||
{{name}}
|
||||
<option value="{{name}}">
|
||||
{{displayName}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
@ -392,11 +392,9 @@
|
||||
|
||||
<h2>Keyboard Shortcuts</h2>
|
||||
|
||||
<h3>On Windows / Linux</h3>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<kbd>Ctrl</kbd> + <kbd>↑</kbd> / <kbd>↓</kbd>
|
||||
<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>
|
||||
@ -405,16 +403,7 @@
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>L</kbd>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Clear the current screen</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<kbd>Ctrl</kbd> + <kbd>K</kbd>
|
||||
<kbd class="key-all">Ctrl</kbd><kbd class="key-apple">⌘</kbd> + <kbd>K</kbd>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
@ -431,7 +420,7 @@
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<kbd>Ctrl</kbd> + <kbd>B</kbd>
|
||||
<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>
|
||||
@ -440,7 +429,7 @@
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<kbd>Ctrl</kbd> + <kbd>U</kbd>
|
||||
<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>
|
||||
@ -449,7 +438,7 @@
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<kbd>Ctrl</kbd> + <kbd>I</kbd>
|
||||
<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>
|
||||
@ -458,83 +447,7 @@
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<kbd>Ctrl</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>
|
||||
|
||||
<h3>On macOS</h3>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<kbd>⌘</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>⇧</kbd> + <kbd>⌘</kbd> + <kbd>L</kbd>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Clear the current screen</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<kbd>⌘</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
|
||||
<code>0—15</code> range to select the desired color.
|
||||
</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>⌘</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>⌘</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>⌘</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>⌘</kbd> + <kbd>O</kbd>
|
||||
<kbd class="key-all">Ctrl</kbd><kbd class="key-apple">⌘</kbd> + <kbd>O</kbd>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
@ -583,15 +496,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/clear</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Clear the current screen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/collapse</code>
|
||||
@ -787,10 +691,13 @@
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/part</code>
|
||||
<code>/part [channel]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Close the current channel or private message window.</p>
|
||||
<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>
|
||||
|
38
client/js/clipboard.js
Normal file
38
client/js/clipboard.js
Normal file
@ -0,0 +1,38 @@
|
||||
"use strict";
|
||||
|
||||
const $ = require("jquery");
|
||||
const chat = document.getElementById("chat");
|
||||
|
||||
function copyMessages() {
|
||||
const selection = window.getSelection();
|
||||
|
||||
// If selection does not span multiple elements, do nothing
|
||||
if (selection.anchorNode === selection.focusNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
const documentFragment = range.cloneContents();
|
||||
const div = document.createElement("div");
|
||||
|
||||
$(documentFragment)
|
||||
.find(".from .user")
|
||||
.each((_, el) => {
|
||||
el = $(el);
|
||||
el.text(`<${el.text()}>`);
|
||||
});
|
||||
|
||||
div.id = "js-copy-hack";
|
||||
div.appendChild(documentFragment);
|
||||
chat.appendChild(div);
|
||||
|
||||
selection.selectAllChildren(div);
|
||||
|
||||
window.setTimeout(() => {
|
||||
chat.removeChild(div);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
$(chat).on("copy", ".messages", copyMessages);
|
@ -23,6 +23,12 @@ function updateText(condensed, addedTypes) {
|
||||
constants.condensedTypes.forEach((type) => {
|
||||
if (obj[type]) {
|
||||
switch (type) {
|
||||
case "away":
|
||||
strings.push(obj[type] + (obj[type] > 1 ? " users have gone away" : " user has gone away"));
|
||||
break;
|
||||
case "back":
|
||||
strings.push(obj[type] + (obj[type] > 1 ? " users have come back" : " user has come back"));
|
||||
break;
|
||||
case "join":
|
||||
strings.push(obj[type] + (obj[type] > 1 ? " users have joined the channel" : " user has joined the channel"));
|
||||
break;
|
||||
|
@ -37,6 +37,7 @@ const commands = [
|
||||
"/join",
|
||||
"/kick",
|
||||
"/leave",
|
||||
"/list",
|
||||
"/me",
|
||||
"/mode",
|
||||
"/msg",
|
||||
@ -59,6 +60,8 @@ const commands = [
|
||||
];
|
||||
|
||||
const actionTypes = [
|
||||
"away",
|
||||
"back",
|
||||
"ban_list",
|
||||
"invite",
|
||||
"join",
|
||||
@ -76,6 +79,8 @@ const actionTypes = [
|
||||
];
|
||||
|
||||
const condensedTypes = [
|
||||
"away",
|
||||
"back",
|
||||
"join",
|
||||
"part",
|
||||
"quit",
|
||||
|
101
client/js/keybinds.js
Normal file
101
client/js/keybinds.js
Normal file
@ -0,0 +1,101 @@
|
||||
"use strict";
|
||||
|
||||
const $ = require("jquery");
|
||||
const Mousetrap = require("mousetrap");
|
||||
const input = $("#input");
|
||||
const sidebar = $("#sidebar");
|
||||
const windows = $("#windows");
|
||||
const contextMenuContainer = $("#context-menu-container");
|
||||
|
||||
Mousetrap.bind([
|
||||
"pageup",
|
||||
"pagedown"
|
||||
], function(e, key) {
|
||||
let container = windows.find(".window.active");
|
||||
|
||||
// Chat windows scroll message container
|
||||
if (container.attr("id") === "chat-container") {
|
||||
container = container.find(".chan.active .chat");
|
||||
}
|
||||
|
||||
container.finish();
|
||||
|
||||
const offset = container.get(0).clientHeight * 0.9;
|
||||
let scrollTop = container.scrollTop();
|
||||
|
||||
if (key === "pageup") {
|
||||
scrollTop = Math.floor(scrollTop - offset);
|
||||
} else {
|
||||
scrollTop = Math.ceil(scrollTop + offset);
|
||||
}
|
||||
|
||||
container.animate({
|
||||
scrollTop: scrollTop
|
||||
}, 200);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
Mousetrap.bind([
|
||||
"command+up",
|
||||
"command+down",
|
||||
"ctrl+up",
|
||||
"ctrl+down"
|
||||
], function(e, keys) {
|
||||
const channels = sidebar.find(".chan");
|
||||
const index = channels.index(channels.filter(".active"));
|
||||
const direction = keys.split("+").pop();
|
||||
let target;
|
||||
|
||||
switch (direction) {
|
||||
case "up":
|
||||
target = (channels.length + (index - 1 + channels.length)) % channels.length;
|
||||
break;
|
||||
|
||||
case "down":
|
||||
target = (channels.length + (index + 1 + channels.length)) % channels.length;
|
||||
break;
|
||||
}
|
||||
|
||||
channels.eq(target).click();
|
||||
});
|
||||
|
||||
Mousetrap.bind([
|
||||
"escape"
|
||||
], function() {
|
||||
contextMenuContainer.hide();
|
||||
});
|
||||
|
||||
const colorsHotkeys = {
|
||||
k: "\x03",
|
||||
b: "\x02",
|
||||
u: "\x1F",
|
||||
i: "\x1D",
|
||||
o: "\x0F",
|
||||
};
|
||||
|
||||
for (const hotkey in colorsHotkeys) {
|
||||
Mousetrap.bind([
|
||||
"command+" + hotkey,
|
||||
"ctrl+" + hotkey
|
||||
], function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const cursorPosStart = input.prop("selectionStart");
|
||||
const cursorPosEnd = input.prop("selectionEnd");
|
||||
const value = input.val();
|
||||
let newValue = value.substring(0, cursorPosStart) + colorsHotkeys[e.key];
|
||||
|
||||
if (cursorPosStart === cursorPosEnd) {
|
||||
// If no text is selected, insert at cursor
|
||||
newValue += value.substring(cursorPosEnd, value.length);
|
||||
} else {
|
||||
// If text is selected, insert formatting character at start and the end
|
||||
newValue += value.substring(cursorPosStart, cursorPosEnd) + colorsHotkeys[e.key] + value.substring(cursorPosEnd, value.length);
|
||||
}
|
||||
|
||||
input
|
||||
.val(newValue)
|
||||
.get(0).setSelectionRange(cursorPosStart + 1, cursorPosEnd + 1);
|
||||
});
|
||||
}
|
@ -4,24 +4,23 @@
|
||||
require("jquery-ui/ui/widgets/sortable");
|
||||
const $ = require("jquery");
|
||||
const moment = require("moment");
|
||||
const Mousetrap = require("mousetrap");
|
||||
const URI = require("urijs");
|
||||
const fuzzy = require("fuzzy");
|
||||
|
||||
// our libraries
|
||||
require("./libs/jquery/inputhistory");
|
||||
require("./libs/jquery/stickyscroll");
|
||||
const helpers_roundBadgeNumber = require("./libs/handlebars/roundBadgeNumber");
|
||||
const slideoutMenu = require("./libs/slideout");
|
||||
const templates = require("../views");
|
||||
const socket = require("./socket");
|
||||
require("./socket-events");
|
||||
const storage = require("./localStorage");
|
||||
const options = require("./options");
|
||||
require("./options");
|
||||
const utils = require("./utils");
|
||||
const modules = require("./modules");
|
||||
require("./autocompletion");
|
||||
require("./webpush");
|
||||
require("./keybinds");
|
||||
require("./clipboard");
|
||||
|
||||
$(function() {
|
||||
var sidebar = $("#sidebar, #footer");
|
||||
@ -29,20 +28,6 @@ $(function() {
|
||||
|
||||
$(document.body).data("app-name", document.title);
|
||||
|
||||
var pop;
|
||||
try {
|
||||
pop = new Audio();
|
||||
pop.src = "audio/pop.ogg";
|
||||
} catch (e) {
|
||||
pop = {
|
||||
play: $.noop
|
||||
};
|
||||
}
|
||||
|
||||
$("#play").on("click", function() {
|
||||
pop.play();
|
||||
});
|
||||
|
||||
var windows = $("#windows");
|
||||
var viewport = $("#viewport");
|
||||
var sidebarSlide = slideoutMenu(viewport[0], sidebar[0]);
|
||||
@ -183,6 +168,10 @@ $(function() {
|
||||
});
|
||||
}
|
||||
|
||||
if (navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i)) {
|
||||
$(document.body).addClass("is-apple");
|
||||
}
|
||||
|
||||
$("#form").on("submit", function(e) {
|
||||
e.preventDefault();
|
||||
utils.forceFocus();
|
||||
@ -199,22 +188,19 @@ $(function() {
|
||||
const separatorPos = text.indexOf(" ");
|
||||
const cmd = text.substring(1, separatorPos > 1 ? separatorPos : text.length);
|
||||
const parameters = separatorPos > text.indexOf(cmd) ? text.substring(text.indexOf(cmd) + cmd.length + 1, text.length) : "";
|
||||
switch (cmd) {
|
||||
case "clear":
|
||||
if (modules.clear()) return;
|
||||
break;
|
||||
case "collapse":
|
||||
if (modules.collapse()) return;
|
||||
break;
|
||||
case "expand":
|
||||
if (modules.expand()) return;
|
||||
break;
|
||||
case "join":
|
||||
if (typeof utils[cmd] === "function") {
|
||||
if (cmd === "join") {
|
||||
const channel = parameters.split(" ")[0];
|
||||
if (channel != "") {
|
||||
if (modules.join(channel)) return;
|
||||
if (channel !== "") {
|
||||
if (utils[cmd](channel)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
if (utils[cmd]()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -327,16 +313,16 @@ $(function() {
|
||||
const state = {};
|
||||
|
||||
if (self.hasClass("chan")) {
|
||||
state.clickTarget = `.chan[data-id="${self.data("id")}"]`;
|
||||
state.clickTarget = `#sidebar .chan[data-id="${self.data("id")}"]`;
|
||||
} else {
|
||||
state.clickTarget = `#footer button[data-target="${target}"]`;
|
||||
}
|
||||
|
||||
if (history && history.pushState) {
|
||||
if (data && data.replaceHistory && history.replaceState) {
|
||||
history.replaceState(state, null, null);
|
||||
history.replaceState(state, null, target);
|
||||
} else {
|
||||
history.pushState(state, null, null);
|
||||
history.pushState(state, null, target);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -382,6 +368,7 @@ $(function() {
|
||||
|
||||
lastActiveChan
|
||||
.find(".unread-marker")
|
||||
.data("unread-id", 0)
|
||||
.appendTo(lastActiveChan.find(".messages"));
|
||||
|
||||
var chan = $(target)
|
||||
@ -433,7 +420,7 @@ $(function() {
|
||||
if (chan.hasClass("lobby")) {
|
||||
cmd = "/quit";
|
||||
var server = chan.find(".name").html();
|
||||
if (!confirm("Disconnect from " + server + "?")) {
|
||||
if (!confirm("Disconnect from " + server + "?")) { // eslint-disable-line no-alert
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -490,95 +477,6 @@ $(function() {
|
||||
container.html(templates.user_filtered({matches: result})).show();
|
||||
});
|
||||
|
||||
chat.on("msg", ".messages", function(e, target, msg) {
|
||||
var unread = msg.unread;
|
||||
msg = msg.msg;
|
||||
|
||||
if (msg.self) {
|
||||
return;
|
||||
}
|
||||
|
||||
var button = sidebar.find(".chan[data-target='" + target + "']");
|
||||
if (msg.highlight || (options.notifyAllMessages && msg.type === "message")) {
|
||||
if (!document.hasFocus() || !$(target).hasClass("active")) {
|
||||
if (options.notification) {
|
||||
try {
|
||||
pop.play();
|
||||
} catch (exception) {
|
||||
// On mobile, sounds can not be played without user interaction.
|
||||
}
|
||||
}
|
||||
utils.toggleNotificationMarkers(true);
|
||||
|
||||
if (options.desktopNotifications && Notification.permission === "granted") {
|
||||
var title;
|
||||
var body;
|
||||
|
||||
if (msg.type === "invite") {
|
||||
title = "New channel invite:";
|
||||
body = msg.from + " invited you to " + msg.channel;
|
||||
} else {
|
||||
title = msg.from;
|
||||
if (!button.hasClass("query")) {
|
||||
title += " (" + button.data("title").trim() + ")";
|
||||
}
|
||||
if (msg.type === "message") {
|
||||
title += " says:";
|
||||
}
|
||||
body = msg.text.replace(/\x03(?:[0-9]{1,2}(?:,[0-9]{1,2})?)?|[\x00-\x1F]|\x7F/g, "").trim();
|
||||
}
|
||||
|
||||
try {
|
||||
var notify = new Notification(title, {
|
||||
body: body,
|
||||
icon: "img/logo-64.png",
|
||||
tag: target
|
||||
});
|
||||
notify.addEventListener("click", function() {
|
||||
window.focus();
|
||||
button.click();
|
||||
this.close();
|
||||
});
|
||||
} catch (exception) {
|
||||
// `new Notification(...)` is not supported and should be silenced.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (button.hasClass("active")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!unread) {
|
||||
return;
|
||||
}
|
||||
|
||||
var badge = button.find(".badge").html(helpers_roundBadgeNumber(unread));
|
||||
|
||||
if (msg.highlight) {
|
||||
badge.addClass("highlight");
|
||||
}
|
||||
});
|
||||
|
||||
chat.on("click", ".show-more-button", function() {
|
||||
var self = $(this);
|
||||
var lastMessage = self.parent().next(".messages").children(".msg").first();
|
||||
if (lastMessage.is(".condensed")) {
|
||||
lastMessage = lastMessage.children(".msg").first();
|
||||
}
|
||||
var lastMessageId = parseInt(lastMessage[0].id.replace("msg-", ""), 10);
|
||||
|
||||
self
|
||||
.text("Loading older messages…")
|
||||
.prop("disabled", true);
|
||||
|
||||
socket.emit("more", {
|
||||
target: self.data("id"),
|
||||
lastId: lastMessageId
|
||||
});
|
||||
});
|
||||
|
||||
var forms = $("#sign-in, #connect, #change-password");
|
||||
|
||||
windows.on("show", "#sign-in", function() {
|
||||
@ -590,7 +488,8 @@ $(function() {
|
||||
}
|
||||
});
|
||||
});
|
||||
if ($("body").hasClass("public")) {
|
||||
|
||||
if ($("body").hasClass("public") && window.location.hash === "#connect") {
|
||||
$("#connect").one("show", function() {
|
||||
var params = URI(document.location.search);
|
||||
params = params.search(true);
|
||||
@ -620,23 +519,25 @@ $(function() {
|
||||
e.preventDefault();
|
||||
var event = "auth";
|
||||
var form = $(this);
|
||||
form.find(".btn")
|
||||
.attr("disabled", true)
|
||||
.end();
|
||||
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
|
||||
);
|
||||
@ -664,111 +565,6 @@ $(function() {
|
||||
$(this).data("lastvalue", nick);
|
||||
});
|
||||
|
||||
(function HotkeysScope() {
|
||||
Mousetrap.bind([
|
||||
"pageup",
|
||||
"pagedown"
|
||||
], function(e, key) {
|
||||
let container = windows.find(".window.active");
|
||||
|
||||
// Chat windows scroll message container
|
||||
if (container.attr("id") === "chat-container") {
|
||||
container = container.find(".chan.active .chat");
|
||||
}
|
||||
|
||||
container.finish();
|
||||
|
||||
const offset = container.get(0).clientHeight * 0.9;
|
||||
let scrollTop = container.scrollTop();
|
||||
|
||||
if (key === "pageup") {
|
||||
scrollTop = Math.floor(scrollTop - offset);
|
||||
} else {
|
||||
scrollTop = Math.ceil(scrollTop + offset);
|
||||
}
|
||||
|
||||
container.animate({
|
||||
scrollTop: scrollTop
|
||||
}, 200);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
Mousetrap.bind([
|
||||
"command+up",
|
||||
"command+down",
|
||||
"ctrl+up",
|
||||
"ctrl+down"
|
||||
], function(e, keys) {
|
||||
var channels = sidebar.find(".chan");
|
||||
var index = channels.index(channels.filter(".active"));
|
||||
var direction = keys.split("+").pop();
|
||||
switch (direction) {
|
||||
case "up":
|
||||
// Loop
|
||||
var upTarget = (channels.length + (index - 1 + channels.length)) % channels.length;
|
||||
channels.eq(upTarget).click();
|
||||
break;
|
||||
|
||||
case "down":
|
||||
// Loop
|
||||
var downTarget = (channels.length + (index + 1 + channels.length)) % channels.length;
|
||||
channels.eq(downTarget).click();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
Mousetrap.bind([
|
||||
"command+shift+l",
|
||||
"ctrl+shift+l"
|
||||
], function(e) {
|
||||
if (e.target === input[0]) {
|
||||
utils.clear();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
Mousetrap.bind([
|
||||
"escape"
|
||||
], function() {
|
||||
contextMenuContainer.hide();
|
||||
});
|
||||
|
||||
var colorsHotkeys = {
|
||||
k: "\x03",
|
||||
b: "\x02",
|
||||
u: "\x1F",
|
||||
i: "\x1D",
|
||||
o: "\x0F",
|
||||
};
|
||||
|
||||
for (var hotkey in colorsHotkeys) {
|
||||
Mousetrap.bind([
|
||||
"command+" + hotkey,
|
||||
"ctrl+" + hotkey
|
||||
], function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const cursorPosStart = input.prop("selectionStart");
|
||||
const cursorPosEnd = input.prop("selectionEnd");
|
||||
const value = input.val();
|
||||
let newValue = value.substring(0, cursorPosStart) + colorsHotkeys[e.key];
|
||||
|
||||
if (cursorPosStart === cursorPosEnd) {
|
||||
// If no text is selected, insert at cursor
|
||||
newValue += value.substring(cursorPosEnd, value.length);
|
||||
} else {
|
||||
// If text is selected, insert formatting character at start and the end
|
||||
newValue += value.substring(cursorPosStart, cursorPosEnd) + colorsHotkeys[e.key] + value.substring(cursorPosEnd, value.length);
|
||||
}
|
||||
|
||||
input
|
||||
.val(newValue)
|
||||
.get(0).setSelectionRange(cursorPosStart + 1, cursorPosEnd + 1);
|
||||
});
|
||||
}
|
||||
}());
|
||||
|
||||
$(document).on("visibilitychange focus click", () => {
|
||||
if (sidebar.find(".highlight").length === 0) {
|
||||
utils.toggleNotificationMarkers(false);
|
||||
@ -786,7 +582,7 @@ $(function() {
|
||||
$(".date-marker-text[data-label='Today'], .date-marker-text[data-label='Yesterday']")
|
||||
.closest(".date-marker-container")
|
||||
.each(function() {
|
||||
$(this).replaceWith(templates.date_marker({msgDate: $(this).data("timestamp")}));
|
||||
$(this).replaceWith(templates.date_marker({time: $(this).data("time")}));
|
||||
});
|
||||
|
||||
// This should always be 24h later but re-computing exact value just in case
|
||||
@ -794,20 +590,33 @@ $(function() {
|
||||
}
|
||||
setTimeout(updateDateMarkers, msUntilNextDay());
|
||||
|
||||
// Only start opening socket.io connection after all events have been registered
|
||||
socket.open();
|
||||
|
||||
window.addEventListener("popstate", (e) => {
|
||||
const {state} = e;
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {clickTarget} = state;
|
||||
let {clickTarget} = state;
|
||||
|
||||
if (clickTarget) {
|
||||
// This will be true when click target corresponds to opening a thumbnail,
|
||||
// browsing to the previous/next thumbnail, or closing the image viewer.
|
||||
const imageViewerRelated = clickTarget.includes(".toggle-thumbnail");
|
||||
|
||||
// If the click target is not related to the image viewer but the viewer
|
||||
// is currently opened, we need to close it.
|
||||
if (!imageViewerRelated && $("#image-viewer").hasClass("opened")) {
|
||||
clickTarget += ", #image-viewer";
|
||||
}
|
||||
|
||||
// Emit the click to the target, while making sure it is not going to be
|
||||
// added to the state again.
|
||||
$(clickTarget).trigger("click", {
|
||||
pushState: false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Only start opening socket.io connection after all events have been registered
|
||||
socket.open();
|
||||
});
|
||||
|
@ -1,35 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
// vendor libraries
|
||||
const $ = require("jquery");
|
||||
|
||||
// our libraries
|
||||
const utils = require("./utils");
|
||||
|
||||
module.exports = {
|
||||
clear,
|
||||
collapse,
|
||||
expand,
|
||||
join
|
||||
};
|
||||
|
||||
function clear() {
|
||||
utils.clear();
|
||||
}
|
||||
|
||||
function collapse() {
|
||||
$(".chan.active .toggle-button.opened").click();
|
||||
}
|
||||
|
||||
function expand() {
|
||||
$(".chan.active .toggle-button:not(.opened)").click();
|
||||
}
|
||||
|
||||
function join(channel) {
|
||||
var chan = utils.findCurrentNetworkChan(channel);
|
||||
|
||||
if (chan.length) {
|
||||
chan.click();
|
||||
return true;
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ const utils = require("./utils");
|
||||
const sorting = require("./sorting");
|
||||
const constants = require("./constants");
|
||||
const condensed = require("./condensed");
|
||||
const helpers_parse = require("./libs/handlebars/parse");
|
||||
|
||||
const chat = $("#chat");
|
||||
const sidebar = $("#sidebar");
|
||||
@ -27,45 +28,29 @@ module.exports = {
|
||||
renderNetworks,
|
||||
};
|
||||
|
||||
function buildChannelMessages(chanId, chanType, messages) {
|
||||
function buildChannelMessages(container, chanId, chanType, messages) {
|
||||
return messages.reduce((docFragment, message) => {
|
||||
appendMessage(docFragment, chanId, chanType, message);
|
||||
return docFragment;
|
||||
}, $(document.createDocumentFragment()));
|
||||
}, container);
|
||||
}
|
||||
|
||||
function appendMessage(container, chanId, chanType, msg) {
|
||||
const renderedMessage = buildChatMessage(chanId, msg);
|
||||
if (utils.lastMessageId < msg.id) {
|
||||
utils.lastMessageId = msg.id;
|
||||
}
|
||||
|
||||
let lastChild = container.children(".msg, .date-marker-container").last();
|
||||
const renderedMessage = buildChatMessage(msg);
|
||||
|
||||
// Check if date changed
|
||||
let lastChild = container.find(".msg").last();
|
||||
const msgTime = new Date(msg.time);
|
||||
|
||||
// It's the first message in a window,
|
||||
// then just append the message and do nothing else
|
||||
if (lastChild.length === 0) {
|
||||
container
|
||||
.append(templates.date_marker({msgDate: msgTime}))
|
||||
.append(renderedMessage);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const prevMsgTime = new Date(lastChild.attr("data-time"));
|
||||
const parent = lastChild.parent();
|
||||
|
||||
// If this message is condensed, we have to work on the wrapper
|
||||
if (parent.hasClass("condensed")) {
|
||||
lastChild = parent;
|
||||
}
|
||||
const prevMsgTime = new Date(lastChild.data("time"));
|
||||
|
||||
// Insert date marker if date changed compared to previous message
|
||||
if (prevMsgTime.toDateString() !== msgTime.toDateString()) {
|
||||
lastChild.after(templates.date_marker({msgDate: msgTime}));
|
||||
|
||||
// If date changed, we don't need to do condensed logic
|
||||
container.append(renderedMessage);
|
||||
return;
|
||||
lastChild = $(templates.date_marker({time: msg.time}));
|
||||
container.append(lastChild);
|
||||
}
|
||||
|
||||
// If current window is not a channel or this message is not condensable,
|
||||
@ -83,25 +68,16 @@ function appendMessage(container, chanId, chanType, msg) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newCondensed = buildChatMessage(chanId, {
|
||||
type: "condensed",
|
||||
time: msg.time,
|
||||
previews: []
|
||||
});
|
||||
// Always create a condensed container
|
||||
const newCondensed = $(templates.msg_condensed({time: msg.time}));
|
||||
|
||||
condensed.updateText(newCondensed, [msg.type]);
|
||||
newCondensed.append(renderedMessage);
|
||||
container.append(newCondensed);
|
||||
}
|
||||
|
||||
function buildChatMessage(chanId, msg) {
|
||||
function buildChatMessage(msg) {
|
||||
const type = msg.type;
|
||||
let target = "#chan-" + chanId;
|
||||
if (type === "error") {
|
||||
target = "#chan-" + chat.find(".active").data("id");
|
||||
}
|
||||
|
||||
const chan = chat.find(target);
|
||||
let template = "msg";
|
||||
|
||||
// See if any of the custom highlight regexes match
|
||||
@ -117,8 +93,6 @@ function buildChatMessage(chanId, msg) {
|
||||
template = "msg_action";
|
||||
} else if (type === "unhandled") {
|
||||
template = "msg_unhandled";
|
||||
} else if (type === "condensed") {
|
||||
template = "msg_condensed";
|
||||
}
|
||||
|
||||
const renderedMessage = $(templates[template](msg));
|
||||
@ -132,17 +106,6 @@ function buildChatMessage(chanId, msg) {
|
||||
renderPreview(preview, renderedMessage);
|
||||
});
|
||||
|
||||
if ((type === "message" || type === "action" || type === "notice") && chan.hasClass("channel")) {
|
||||
const nicks = chan.find(".users").data("nicks");
|
||||
if (nicks) {
|
||||
const find = nicks.indexOf(msg.from);
|
||||
if (find !== -1) {
|
||||
nicks.splice(find, 1);
|
||||
nicks.unshift(msg.from);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return renderedMessage;
|
||||
}
|
||||
|
||||
@ -159,22 +122,28 @@ function renderChannel(data) {
|
||||
}
|
||||
|
||||
function renderChannelMessages(data) {
|
||||
const documentFragment = buildChannelMessages(data.id, data.type, data.messages);
|
||||
const documentFragment = buildChannelMessages($(document.createDocumentFragment()), data.id, data.type, data.messages);
|
||||
const channel = chat.find("#chan-" + data.id + " .messages").append(documentFragment);
|
||||
|
||||
if (data.firstUnread > 0) {
|
||||
const first = channel.find("#msg-" + data.firstUnread);
|
||||
const template = $(templates.unread_marker());
|
||||
|
||||
if (data.firstUnread > 0) {
|
||||
let first = channel.find("#msg-" + data.firstUnread);
|
||||
|
||||
// TODO: If the message is far off in the history, we still need to append the marker into DOM
|
||||
if (!first.length) {
|
||||
channel.prepend(templates.unread_marker());
|
||||
} else if (first.parent().hasClass("condensed")) {
|
||||
first.parent().before(templates.unread_marker());
|
||||
template.data("unread-id", data.firstUnread);
|
||||
channel.prepend(template);
|
||||
} else {
|
||||
first.before(templates.unread_marker());
|
||||
const parent = first.parent();
|
||||
|
||||
if (parent.hasClass("condensed")) {
|
||||
first = parent;
|
||||
}
|
||||
|
||||
first.before(template);
|
||||
}
|
||||
} else {
|
||||
channel.append(templates.unread_marker());
|
||||
channel.append(template);
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,7 +169,7 @@ function renderChannelUsers(data) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderNetworks(data) {
|
||||
function renderNetworks(data, singleNetwork) {
|
||||
sidebar.find(".empty").hide();
|
||||
sidebar.find(".networks").append(
|
||||
templates.network({
|
||||
@ -208,15 +177,51 @@ function renderNetworks(data) {
|
||||
})
|
||||
);
|
||||
|
||||
let newChannels;
|
||||
const channels = $.map(data.networks, function(n) {
|
||||
return n.channels;
|
||||
});
|
||||
|
||||
if (!singleNetwork && utils.lastMessageId > -1) {
|
||||
newChannels = [];
|
||||
|
||||
channels.forEach((channel) => {
|
||||
const chan = $("#chan-" + channel.id);
|
||||
|
||||
if (chan.length > 0) {
|
||||
if (chan.data("type") === "channel") {
|
||||
chan
|
||||
.data("needsNamesRefresh", true)
|
||||
.find(".header .topic")
|
||||
.html(helpers_parse(channel.topic))
|
||||
.attr("title", channel.topic);
|
||||
}
|
||||
|
||||
if (channel.messages.length > 0) {
|
||||
const container = chan.find(".messages");
|
||||
buildChannelMessages(container, channel.id, channel.type, channel.messages);
|
||||
|
||||
if (container.find(".msg").length >= 100) {
|
||||
container.find(".show-more").addClass("show");
|
||||
}
|
||||
|
||||
container.trigger("keepToBottom");
|
||||
}
|
||||
} else {
|
||||
newChannels.push(channel);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
newChannels = channels;
|
||||
}
|
||||
|
||||
chat.append(
|
||||
templates.chat({
|
||||
channels: channels
|
||||
})
|
||||
);
|
||||
channels.forEach((channel) => {
|
||||
|
||||
newChannels.forEach((channel) => {
|
||||
renderChannel(channel);
|
||||
|
||||
if (channel.type === "channel") {
|
||||
|
@ -92,10 +92,12 @@ function handleImageInPreview(content, container) {
|
||||
|
||||
const imageViewer = $("#image-viewer");
|
||||
|
||||
$("#chat").on("click", ".toggle-thumbnail", function() {
|
||||
$("#chat").on("click", ".toggle-thumbnail", function(event, data = {}) {
|
||||
const link = $(this);
|
||||
|
||||
openImageViewer(link);
|
||||
// Passing `data`, specifically `data.pushState`, to not add the action to the
|
||||
// history state if back or forward buttons were pressed.
|
||||
openImageViewer(link, data);
|
||||
|
||||
// Prevent the link to open a new page since we're opening the image viewer,
|
||||
// but keep it a link to allow for Ctrl/Cmd+click.
|
||||
@ -103,8 +105,10 @@ $("#chat").on("click", ".toggle-thumbnail", function() {
|
||||
return false;
|
||||
});
|
||||
|
||||
imageViewer.on("click", function() {
|
||||
closeImageViewer();
|
||||
imageViewer.on("click", function(event, data = {}) {
|
||||
// Passing `data`, specifically `data.pushState`, to not add the action to the
|
||||
// history state if back or forward buttons were pressed.
|
||||
closeImageViewer(data);
|
||||
});
|
||||
|
||||
$(document).keydown(function(e) {
|
||||
@ -125,7 +129,7 @@ $(document).keydown(function(e) {
|
||||
}
|
||||
});
|
||||
|
||||
function openImageViewer(link) {
|
||||
function openImageViewer(link, {pushState = true} = {}) {
|
||||
$(".previous-image").removeClass("previous-image");
|
||||
$(".next-image").removeClass("next-image");
|
||||
|
||||
@ -161,7 +165,20 @@ function openImageViewer(link) {
|
||||
hasNextImage: nextImage.length > 0,
|
||||
}));
|
||||
|
||||
imageViewer.addClass("opened");
|
||||
// Turn off transitionend listener before opening the viewer,
|
||||
// which caused image viewer to become empty in rare cases
|
||||
imageViewer
|
||||
.off("transitionend")
|
||||
.addClass("opened");
|
||||
|
||||
// History management
|
||||
if (pushState) {
|
||||
const clickTarget =
|
||||
`#${link.closest(".msg").attr("id")} ` +
|
||||
`a.toggle-thumbnail[href="${link.attr("href")}"] ` +
|
||||
"img";
|
||||
history.pushState({clickTarget}, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
imageViewer.on("click", ".previous-image-btn", function() {
|
||||
@ -174,7 +191,7 @@ imageViewer.on("click", ".next-image-btn", function() {
|
||||
return false;
|
||||
});
|
||||
|
||||
function closeImageViewer() {
|
||||
function closeImageViewer({pushState = true} = {}) {
|
||||
imageViewer
|
||||
.removeClass("opened")
|
||||
.one("transitionend", function() {
|
||||
@ -182,4 +199,12 @@ function closeImageViewer() {
|
||||
});
|
||||
|
||||
input.focus();
|
||||
|
||||
// History management
|
||||
if (pushState) {
|
||||
const clickTarget =
|
||||
"#sidebar " +
|
||||
`.chan[data-id="${$("#sidebar .chan.active").data("id")}"]`;
|
||||
history.pushState({clickTarget}, null, null);
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,20 @@
|
||||
const $ = require("jquery");
|
||||
const socket = require("../socket");
|
||||
const storage = require("../localStorage");
|
||||
const utils = require("../utils");
|
||||
|
||||
socket.on("auth", function(data) {
|
||||
// If we reconnected and serverHash differs, that means the server restarted
|
||||
// And we will reload the page to grab the latest version
|
||||
if (utils.serverHash > -1 && data.serverHash > -1 && data.serverHash !== utils.serverHash) {
|
||||
socket.disconnect();
|
||||
$("#connection-error").text("Server restarted, reloading…");
|
||||
location.reload(true);
|
||||
return;
|
||||
}
|
||||
|
||||
utils.serverHash = data.serverHash;
|
||||
|
||||
const login = $("#sign-in");
|
||||
let token;
|
||||
const user = storage.get("user");
|
||||
@ -12,6 +24,13 @@ socket.on("auth", function(data) {
|
||||
login.find(".btn").prop("disabled", false);
|
||||
|
||||
if (!data.success) {
|
||||
if (login.length === 0) {
|
||||
socket.disconnect();
|
||||
$("#connection-error").text("Authentication failed, reloading…");
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
storage.remove("token");
|
||||
|
||||
const error = login.find(".error");
|
||||
@ -20,9 +39,15 @@ socket.on("auth", function(data) {
|
||||
});
|
||||
} else if (user) {
|
||||
token = storage.get("token");
|
||||
|
||||
if (token) {
|
||||
$("#loading-page-message").text("Authorizing…");
|
||||
socket.emit("auth", {user: user, token: token});
|
||||
$("#loading-page-message, #connection-error").text("Authorizing…");
|
||||
|
||||
socket.emit("auth", {
|
||||
user: user,
|
||||
token: token,
|
||||
lastMessage: utils.lastMessageId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,13 +59,9 @@ socket.on("auth", function(data) {
|
||||
return;
|
||||
}
|
||||
|
||||
$("#footer").find(".sign-in")
|
||||
$("#footer")
|
||||
.find(".sign-in")
|
||||
.trigger("click", {
|
||||
pushState: false,
|
||||
})
|
||||
.end()
|
||||
.find(".networks")
|
||||
.html("")
|
||||
.next()
|
||||
.show();
|
||||
});
|
||||
});
|
||||
|
@ -1,16 +1,28 @@
|
||||
"use strict";
|
||||
|
||||
const $ = require("jquery");
|
||||
const escape = require("css.escape");
|
||||
const socket = require("../socket");
|
||||
const render = require("../render");
|
||||
const webpush = require("../webpush");
|
||||
const sidebar = $("#sidebar");
|
||||
const storage = require("../localStorage");
|
||||
const utils = require("../utils");
|
||||
|
||||
socket.on("init", function(data) {
|
||||
$("#loading-page-message").text("Rendering…");
|
||||
$("#loading-page-message, #connection-error").text("Rendering…");
|
||||
|
||||
const lastMessageId = utils.lastMessageId;
|
||||
let previousActive = 0;
|
||||
|
||||
if (lastMessageId > -1) {
|
||||
previousActive = sidebar.find(".active").data("id");
|
||||
sidebar.find(".networks").empty();
|
||||
}
|
||||
|
||||
if (data.networks.length === 0) {
|
||||
sidebar.find(".empty").show();
|
||||
|
||||
$("#footer").find(".connect").trigger("click", {
|
||||
pushState: false,
|
||||
});
|
||||
@ -18,28 +30,59 @@ socket.on("init", function(data) {
|
||||
render.renderNetworks(data);
|
||||
}
|
||||
|
||||
if (data.token) {
|
||||
storage.set("token", data.token);
|
||||
}
|
||||
|
||||
webpush.configurePushNotifications(data.pushSubscription, data.applicationServerKey);
|
||||
|
||||
$("body").removeClass("signed-out");
|
||||
$("#loading").remove();
|
||||
$("#sign-in").remove();
|
||||
|
||||
const id = data.active;
|
||||
const target = sidebar.find("[data-id='" + id + "']").trigger("click", {
|
||||
replaceHistory: true
|
||||
});
|
||||
if (target.length === 0) {
|
||||
const first = sidebar.find(".chan")
|
||||
.eq(0)
|
||||
.trigger("click");
|
||||
if (first.length === 0) {
|
||||
$("#footer").find(".connect").trigger("click", {
|
||||
pushState: false,
|
||||
});
|
||||
if (lastMessageId > -1) {
|
||||
$("#connection-error").removeClass("shown");
|
||||
$(".show-more-button, #input").prop("disabled", false);
|
||||
$("#submit").show();
|
||||
} else {
|
||||
if (data.token) {
|
||||
storage.set("token", data.token);
|
||||
}
|
||||
|
||||
webpush.configurePushNotifications(data.pushSubscription, data.applicationServerKey);
|
||||
|
||||
$("body").removeClass("signed-out");
|
||||
$("#loading").remove();
|
||||
$("#sign-in").remove();
|
||||
}
|
||||
|
||||
openCorrectChannel(previousActive, data.active);
|
||||
});
|
||||
|
||||
function openCorrectChannel(clientActive, serverActive) {
|
||||
let target = $();
|
||||
|
||||
// Open last active channel
|
||||
if (clientActive > 0) {
|
||||
target = sidebar.find("[data-id='" + clientActive + "']");
|
||||
}
|
||||
|
||||
// Open window provided in location.hash
|
||||
if (target.length === 0 && window.location.hash) {
|
||||
target = $("#footer, #sidebar").find("[data-target='" + escape(window.location.hash) + "']");
|
||||
}
|
||||
|
||||
// Open last active channel according to the server
|
||||
if (serverActive > 0 && target.length === 0) {
|
||||
target = sidebar.find("[data-id='" + serverActive + "']");
|
||||
}
|
||||
|
||||
// Open first available channel
|
||||
if (target.length === 0) {
|
||||
target = sidebar.find(".chan").first();
|
||||
}
|
||||
|
||||
// If target channel is found, open it
|
||||
if (target.length > 0) {
|
||||
target.trigger("click", {
|
||||
replaceHistory: true
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Open the connect window
|
||||
$("#footer .connect").trigger("click", {
|
||||
pushState: false
|
||||
});
|
||||
}
|
||||
|
@ -33,8 +33,30 @@ socket.on("more", function(data) {
|
||||
}
|
||||
|
||||
// Add the older messages
|
||||
const documentFragment = render.buildChannelMessages(data.chan, type, data.messages);
|
||||
chan.prepend(documentFragment).end();
|
||||
const documentFragment = render.buildChannelMessages($(document.createDocumentFragment()), data.chan, type, data.messages);
|
||||
chan.prepend(documentFragment);
|
||||
|
||||
// Move unread marker to correct spot if needed
|
||||
const unreadMarker = chan.find(".unread-marker");
|
||||
const firstUnread = unreadMarker.data("unread-id");
|
||||
|
||||
if (firstUnread > 0) {
|
||||
let first = chan.find("#msg-" + firstUnread);
|
||||
|
||||
if (!first.length) {
|
||||
chan.prepend(unreadMarker);
|
||||
} else {
|
||||
const parent = first.parent();
|
||||
|
||||
if (parent.hasClass("condensed")) {
|
||||
first = parent;
|
||||
}
|
||||
|
||||
unreadMarker.data("unread-id", 0);
|
||||
|
||||
first.before(unreadMarker);
|
||||
}
|
||||
}
|
||||
|
||||
// restore scroll position
|
||||
const position = chan.height() - heightOld;
|
||||
@ -54,3 +76,22 @@ socket.on("more", function(data) {
|
||||
.text("Show older messages")
|
||||
.prop("disabled", false);
|
||||
});
|
||||
|
||||
chat.on("click", ".show-more-button", function() {
|
||||
const self = $(this);
|
||||
const lastMessage = self.closest(".chat").find(".msg:not(.condensed)").first();
|
||||
let lastMessageId = -1;
|
||||
|
||||
if (lastMessage.length > 0) {
|
||||
lastMessageId = parseInt(lastMessage.attr("id").replace("msg-", ""), 10);
|
||||
}
|
||||
|
||||
self
|
||||
.text("Loading older messages…")
|
||||
.prop("disabled", true);
|
||||
|
||||
socket.emit("more", {
|
||||
target: self.data("id"),
|
||||
lastId: lastMessageId
|
||||
});
|
||||
});
|
||||
|
@ -3,17 +3,27 @@
|
||||
const $ = require("jquery");
|
||||
const socket = require("../socket");
|
||||
const render = require("../render");
|
||||
const utils = require("../utils");
|
||||
const options = require("../options");
|
||||
const helpers_roundBadgeNumber = require("../libs/handlebars/roundBadgeNumber");
|
||||
const chat = $("#chat");
|
||||
const sidebar = $("#sidebar");
|
||||
|
||||
let pop;
|
||||
try {
|
||||
pop = new Audio();
|
||||
pop.src = "audio/pop.ogg";
|
||||
} catch (e) {
|
||||
pop = {
|
||||
play: $.noop
|
||||
};
|
||||
}
|
||||
|
||||
$("#play").on("click", () => pop.play());
|
||||
|
||||
socket.on("msg", function(data) {
|
||||
if (window.requestIdleCallback) {
|
||||
// During an idle period the user agent will run idle callbacks in FIFO order
|
||||
// until either the idle period ends or there are no more idle callbacks eligible to be run.
|
||||
// We set a maximum timeout of 2 seconds so that messages don't take too long to appear.
|
||||
window.requestIdleCallback(() => processReceivedMessage(data), {timeout: 2000});
|
||||
} else {
|
||||
processReceivedMessage(data);
|
||||
}
|
||||
// We set a maximum timeout of 2 seconds so that messages don't take too long to appear.
|
||||
utils.requestIdleCallback(() => processReceivedMessage(data), 2000);
|
||||
});
|
||||
|
||||
function processReceivedMessage(data) {
|
||||
@ -32,14 +42,13 @@ function processReceivedMessage(data) {
|
||||
render.appendMessage(
|
||||
container,
|
||||
targetId,
|
||||
$(target).attr("data-type"),
|
||||
channel.attr("data-type"),
|
||||
data.msg
|
||||
);
|
||||
|
||||
container.trigger("msg", [
|
||||
target,
|
||||
data
|
||||
]).trigger("keepToBottom");
|
||||
container.trigger("keepToBottom");
|
||||
|
||||
notifyMessage(targetId, channel, data);
|
||||
|
||||
var lastVisible = container.find("div:visible").last();
|
||||
if (data.msg.self
|
||||
@ -48,6 +57,7 @@ function processReceivedMessage(data) {
|
||||
&& lastVisible.prev().hasClass("unread-marker"))) {
|
||||
container
|
||||
.find(".unread-marker")
|
||||
.data("unread-id", 0)
|
||||
.appendTo(container);
|
||||
}
|
||||
|
||||
@ -63,4 +73,83 @@ function processReceivedMessage(data) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ((data.msg.type === "message" || data.msg.type === "action" || data.msg.type === "notice") && channel.hasClass("channel")) {
|
||||
const nicks = channel.find(".users").data("nicks");
|
||||
if (nicks) {
|
||||
const find = nicks.indexOf(data.msg.from);
|
||||
if (find !== -1) {
|
||||
nicks.splice(find, 1);
|
||||
nicks.unshift(data.msg.from);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function notifyMessage(targetId, channel, msg) {
|
||||
const unread = msg.unread;
|
||||
msg = msg.msg;
|
||||
|
||||
if (msg.self) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = sidebar.find(".chan[data-id='" + targetId + "']");
|
||||
if (msg.highlight || (options.notifyAllMessages && msg.type === "message")) {
|
||||
if (!document.hasFocus() || !channel.hasClass("active")) {
|
||||
if (options.notification) {
|
||||
try {
|
||||
pop.play();
|
||||
} catch (exception) {
|
||||
// On mobile, sounds can not be played without user interaction.
|
||||
}
|
||||
}
|
||||
|
||||
utils.toggleNotificationMarkers(true);
|
||||
|
||||
if (options.desktopNotifications && Notification.permission === "granted") {
|
||||
let title;
|
||||
let body;
|
||||
|
||||
if (msg.type === "invite") {
|
||||
title = "New channel invite:";
|
||||
body = msg.from + " invited you to " + msg.channel;
|
||||
} else {
|
||||
title = msg.from;
|
||||
if (!button.hasClass("query")) {
|
||||
title += " (" + button.data("title").trim() + ")";
|
||||
}
|
||||
if (msg.type === "message") {
|
||||
title += " says:";
|
||||
}
|
||||
body = msg.text.replace(/\x03(?:[0-9]{1,2}(?:,[0-9]{1,2})?)?|[\x00-\x1F]|\x7F/g, "").trim();
|
||||
}
|
||||
|
||||
try {
|
||||
const notify = new Notification(title, {
|
||||
body: body,
|
||||
icon: "img/logo-64.png",
|
||||
tag: `lounge-${targetId}`
|
||||
});
|
||||
notify.addEventListener("click", function() {
|
||||
window.focus();
|
||||
button.click();
|
||||
this.close();
|
||||
});
|
||||
} catch (exception) {
|
||||
// `new Notification(...)` is not supported and should be silenced.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!unread || button.hasClass("active")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const badge = button.find(".badge").html(helpers_roundBadgeNumber(unread));
|
||||
|
||||
if (msg.highlight) {
|
||||
badge.addClass("highlight");
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,9 @@
|
||||
const $ = require("jquery");
|
||||
const renderPreview = require("../renderPreview");
|
||||
const socket = require("../socket");
|
||||
const utils = require("../utils");
|
||||
|
||||
socket.on("msg:preview", function(data) {
|
||||
const msg = $("#msg-" + data.id);
|
||||
|
||||
renderPreview(data.preview, msg);
|
||||
// Previews are not as important, we can wait longer for them to appear
|
||||
utils.requestIdleCallback(() => renderPreview(data.preview, $("#msg-" + data.id)), 6000);
|
||||
});
|
||||
|
@ -6,7 +6,7 @@ const render = require("../render");
|
||||
const sidebar = $("#sidebar");
|
||||
|
||||
socket.on("network", function(data) {
|
||||
render.renderNetworks(data);
|
||||
render.renderNetworks(data, true);
|
||||
|
||||
sidebar.find(".chan")
|
||||
.last()
|
||||
@ -14,11 +14,9 @@ socket.on("network", function(data) {
|
||||
|
||||
$("#connect")
|
||||
.find(".btn")
|
||||
.prop("disabled", false)
|
||||
.end();
|
||||
.prop("disabled", false);
|
||||
});
|
||||
|
||||
socket.on("network_changed", function(data) {
|
||||
sidebar.find("#network-" + data.network).data("options", data.serverOptions);
|
||||
});
|
||||
|
||||
|
@ -6,12 +6,12 @@ const sidebar = $("#sidebar");
|
||||
|
||||
socket.on("quit", function(data) {
|
||||
const id = data.network;
|
||||
sidebar.find("#network-" + id)
|
||||
.remove()
|
||||
.end();
|
||||
sidebar.find("#network-" + id).remove();
|
||||
|
||||
const chan = sidebar.find(".chan")
|
||||
.eq(0)
|
||||
.trigger("click");
|
||||
|
||||
if (chan.length === 0) {
|
||||
sidebar.find(".empty").show();
|
||||
}
|
||||
|
@ -2,54 +2,54 @@
|
||||
|
||||
const $ = require("jquery");
|
||||
const io = require("socket.io-client");
|
||||
const utils = require("./utils");
|
||||
const path = window.location.pathname + "socket.io/";
|
||||
const status = $("#loading-page-message, #connection-error");
|
||||
|
||||
const socket = io({
|
||||
transports: $(document.body).data("transports"),
|
||||
path: path,
|
||||
autoConnect: false,
|
||||
reconnection: false
|
||||
reconnection: !$(document.body).hasClass("public")
|
||||
});
|
||||
|
||||
[
|
||||
"connect_error",
|
||||
"connect_failed",
|
||||
"disconnect",
|
||||
"error",
|
||||
].forEach(function(e) {
|
||||
socket.on(e, function(data) {
|
||||
$("#loading-page-message").text("Connection failed: " + data);
|
||||
$("#connection-error").addClass("shown").one("click", function() {
|
||||
window.onbeforeunload = null;
|
||||
window.location.reload();
|
||||
});
|
||||
socket.on("disconnect", handleDisconnect);
|
||||
socket.on("connect_error", handleDisconnect);
|
||||
socket.on("error", handleDisconnect);
|
||||
|
||||
// Disables sending a message by pressing Enter. `off` is necessary to
|
||||
// cancel `inputhistory`, which overrides hitting Enter. `on` is then
|
||||
// necessary to avoid creating new lines when hitting Enter without Shift.
|
||||
// This is fairly hacky but this solution is not permanent.
|
||||
$("#input").off("keydown").on("keydown", function(event) {
|
||||
if (event.which === 13 && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
// Hides the "Send Message" button
|
||||
$("#submit").remove();
|
||||
|
||||
console.error(data);
|
||||
});
|
||||
socket.on("reconnecting", function(attempt) {
|
||||
status.text(`Reconnecting… (attempt ${attempt})`);
|
||||
});
|
||||
|
||||
socket.on("connecting", function() {
|
||||
$("#loading-page-message").text("Connecting…");
|
||||
status.text("Connecting…");
|
||||
});
|
||||
|
||||
socket.on("connect", function() {
|
||||
$("#loading-page-message").text("Finalizing connection…");
|
||||
// Clear send buffer when reconnecting, socket.io would emit these
|
||||
// immediately upon connection and it will have no effect, so we ensure
|
||||
// nothing is sent to the server that might have happened.
|
||||
socket.sendBuffer = [];
|
||||
|
||||
status.text("Finalizing connection…");
|
||||
});
|
||||
|
||||
socket.on("authorized", function() {
|
||||
$("#loading-page-message").text("Authorized, loading messages…");
|
||||
status.text("Loading messages…");
|
||||
});
|
||||
|
||||
function handleDisconnect(data) {
|
||||
const message = data.message || data;
|
||||
|
||||
status.text(`Waiting to reconnect… (${message})`).addClass("shown");
|
||||
$(".show-more-button, #input").prop("disabled", true);
|
||||
$("#submit").hide();
|
||||
|
||||
// If the server shuts down, socket.io skips reconnection
|
||||
// and we have to manually call connect to start the process
|
||||
if (socket.io.skipReconnect) {
|
||||
utils.requestIdleCallback(() => socket.connect(), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = socket;
|
||||
|
@ -4,16 +4,25 @@ const $ = require("jquery");
|
||||
const chat = $("#chat");
|
||||
const input = $("#input");
|
||||
|
||||
var serverHash = -1;
|
||||
var lastMessageId = -1;
|
||||
|
||||
module.exports = {
|
||||
findCurrentNetworkChan,
|
||||
clear,
|
||||
collapse,
|
||||
expand,
|
||||
join,
|
||||
serverHash,
|
||||
lastMessageId,
|
||||
confirmExit,
|
||||
forceFocus,
|
||||
move,
|
||||
resetHeight,
|
||||
setNick,
|
||||
toggleNickEditor,
|
||||
toggleNotificationMarkers
|
||||
toggleNotificationMarkers,
|
||||
requestIdleCallback,
|
||||
};
|
||||
|
||||
function findCurrentNetworkChan(name) {
|
||||
@ -42,6 +51,26 @@ function clear() {
|
||||
chat.find(".active")
|
||||
.find(".show-more").addClass("show").end()
|
||||
.find(".messages .msg, .date-marker-container").remove();
|
||||
return true;
|
||||
}
|
||||
|
||||
function collapse() {
|
||||
$(".chan.active .toggle-button.opened").click();
|
||||
return true;
|
||||
}
|
||||
|
||||
function expand() {
|
||||
$(".chan.active .toggle-button:not(.opened)").click();
|
||||
return true;
|
||||
}
|
||||
|
||||
function join(channel) {
|
||||
var chan = findCurrentNetworkChan(channel);
|
||||
|
||||
if (chan.length) {
|
||||
chan.click();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleNickEditor(toggle) {
|
||||
@ -90,3 +119,13 @@ function move(array, old_index, new_index) {
|
||||
array.splice(new_index, 0, array.splice(old_index, 1)[0]);
|
||||
return array;
|
||||
}
|
||||
|
||||
function requestIdleCallback(callback, timeout) {
|
||||
if (window.requestIdleCallback) {
|
||||
// During an idle period the user agent will run idle callbacks in FIFO order
|
||||
// until either the idle period ends or there are no more idle callbacks eligible to be run.
|
||||
window.requestIdleCallback(callback, {timeout: timeout});
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
@ -9,16 +9,30 @@ self.addEventListener("push", function(event) {
|
||||
|
||||
const payload = event.data.json();
|
||||
|
||||
if (payload.type === "notification") {
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(payload.title, {
|
||||
badge: "img/logo-64.png",
|
||||
icon: "img/touch-icon-192x192.png",
|
||||
body: payload.body,
|
||||
timestamp: payload.timestamp,
|
||||
})
|
||||
);
|
||||
if (payload.type !== "notification") {
|
||||
return;
|
||||
}
|
||||
|
||||
// get current notification, close it, and draw new
|
||||
event.waitUntil(
|
||||
self.registration
|
||||
.getNotifications({
|
||||
tag: `chan-${payload.chanId}`
|
||||
})
|
||||
.then((notifications) => {
|
||||
for (const notification of notifications) {
|
||||
notification.close();
|
||||
}
|
||||
|
||||
return self.registration.showNotification(payload.title, {
|
||||
tag: `chan-${payload.chanId}`,
|
||||
badge: "img/logo-64.png",
|
||||
icon: "img/touch-icon-192x192.png",
|
||||
body: payload.body,
|
||||
timestamp: payload.timestamp,
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("notificationclick", function(event) {
|
||||
|
@ -65,12 +65,8 @@ a:hover,
|
||||
background: #00ff0e;
|
||||
}
|
||||
|
||||
.btn-reconnect {
|
||||
#connection-error {
|
||||
background: #f00;
|
||||
color: #fff;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#settings .opt {
|
||||
|
@ -46,14 +46,6 @@ body {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.btn-reconnect {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#sidebar {
|
||||
left: -220px;
|
||||
|
@ -29,14 +29,6 @@ body {
|
||||
background: #333c4a;
|
||||
}
|
||||
|
||||
#windows .header .topic,
|
||||
#windows #form .input,
|
||||
.messages .msg,
|
||||
.sidebar {
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#chat .count {
|
||||
background-color: #2e3642;
|
||||
}
|
||||
@ -213,14 +205,6 @@ body {
|
||||
color: #99a2b4;
|
||||
}
|
||||
|
||||
.btn-reconnect {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Form elements */
|
||||
|
||||
#chat-container ::-moz-placeholder {
|
||||
|
@ -30,14 +30,6 @@ body {
|
||||
background: #3f3f3f;
|
||||
}
|
||||
|
||||
#windows .header .topic,
|
||||
#windows #form .input,
|
||||
.messages .msg,
|
||||
.sidebar {
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#settings,
|
||||
#sign-in,
|
||||
#connect .title {
|
||||
@ -240,14 +232,6 @@ body {
|
||||
color: #d2d39b;
|
||||
}
|
||||
|
||||
.btn-reconnect {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Form elements */
|
||||
|
||||
#chat-container ::-moz-placeholder {
|
||||
|
3
client/views/actions/away.tpl
Normal file
3
client/views/actions/away.tpl
Normal file
@ -0,0 +1,3 @@
|
||||
{{> ../user_name nick=from}}
|
||||
is away
|
||||
<i class="away-message">({{{parse text}}})</i>
|
2
client/views/actions/back.tpl
Normal file
2
client/views/actions/back.tpl
Normal file
@ -0,0 +1,2 @@
|
||||
{{> ../user_name nick=from}}
|
||||
is back
|
@ -1,6 +1,6 @@
|
||||
{{> ../user_name nick=from}}
|
||||
{{> ../user_name nick=from.nick mode=from.mode}}
|
||||
has kicked
|
||||
{{> ../user_name nick=target mode=""}}
|
||||
{{> ../user_name nick=target.nick mode=target.mode}}
|
||||
{{#if text}}
|
||||
<i class="part-reason">({{{parse text}}})</i>
|
||||
{{/if}}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="date-marker-container tooltipped tooltipped-s" data-timestamp="{{msgDate}}" aria-label="{{localedate msgDate}}">
|
||||
<div class="date-marker-container tooltipped tooltipped-s" data-time="{{time}}" aria-label="{{localedate time}}">
|
||||
<div class="date-marker">
|
||||
<span class="date-marker-text" data-label="{{friendlydate msgDate}}"></span>
|
||||
<span class="date-marker-text" data-label="{{friendlydate time}}"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,6 +3,8 @@
|
||||
module.exports = {
|
||||
actions: {
|
||||
action: require("./actions/action.tpl"),
|
||||
away: require("./actions/away.tpl"),
|
||||
back: require("./actions/back.tpl"),
|
||||
ban_list: require("./actions/ban_list.tpl"),
|
||||
channel_list: require("./actions/channel_list.tpl"),
|
||||
ctcp: require("./actions/ctcp.tpl"),
|
||||
|
@ -1,5 +1,5 @@
|
||||
{{#preview}}
|
||||
<button class="toggle-button {{#if shown}} opened{{/if}}"
|
||||
<button class="toggle-button toggle-preview {{#if shown}} opened{{/if}}"
|
||||
data-url="{{link}}"
|
||||
{{#equal type "image"}}
|
||||
aria-label="Toggle image preview"
|
||||
|
@ -51,11 +51,12 @@ module.exports = {
|
||||
|
||||
//
|
||||
// Set the default theme.
|
||||
// Find out how to add new themes at https://thelounge.github.io/docs/packages/themes
|
||||
//
|
||||
// @type string
|
||||
// @default "themes/example.css"
|
||||
// @default "example"
|
||||
//
|
||||
theme: "themes/example.css",
|
||||
theme: "example",
|
||||
|
||||
//
|
||||
// Prefetch URLs
|
||||
@ -89,12 +90,12 @@ module.exports = {
|
||||
// Prefetch URLs Image Preview size limit
|
||||
//
|
||||
// If prefetch is enabled, The Lounge will only display content under the maximum size.
|
||||
// Specified value is in kilobytes. Default value is 512 kilobytes.
|
||||
// Specified value is in kilobytes. Default value is 2048 kilobytes.
|
||||
//
|
||||
// @type int
|
||||
// @default 512
|
||||
// @default 2048
|
||||
//
|
||||
prefetchMaxImageSize: 512,
|
||||
prefetchMaxImageSize: 2048,
|
||||
|
||||
//
|
||||
// Display network
|
||||
@ -365,6 +366,30 @@ module.exports = {
|
||||
// @type object
|
||||
// @default {}
|
||||
//
|
||||
// The authentication process works as follows:
|
||||
//
|
||||
// 1. Lounge connects to the LDAP server with its system credentials
|
||||
// 2. It performs a LDAP search query to find the full DN associated to the
|
||||
// user requesting to log in.
|
||||
// 3. Lounge tries to connect a second time, but this time using the user's
|
||||
// DN and password. Auth is validated iff this connection is successful.
|
||||
//
|
||||
// The search query takes a couple of parameters in `searchDN`:
|
||||
// - a base DN `searchDN/base`. Only children nodes of this DN will be likely
|
||||
// to be returned;
|
||||
// - a search scope `searchDN/scope` (see LDAP documentation);
|
||||
// - the query itself, build as (&(<primaryKey>=<username>) <filter>)
|
||||
// where <username> is the user name provided in the log in request,
|
||||
// <primaryKey> is provided by the config and <fitler> is a filtering complement
|
||||
// also given in the config, to filter for instance only for nodes of type
|
||||
// inetOrgPerson, or whatever LDAP search allows.
|
||||
//
|
||||
// Alternatively, you can specify the `bindDN` parameter. This will make the lounge
|
||||
// ignore searchDN options and assume that the user DN is always:
|
||||
// <bindDN>,<primaryKey>=<username>
|
||||
// where <username> is the user name provided in the log in request, and <bindDN>
|
||||
// and <primaryKey> are provided by the config.
|
||||
//
|
||||
ldap: {
|
||||
//
|
||||
// Enable LDAP user authentication
|
||||
@ -382,11 +407,25 @@ module.exports = {
|
||||
url: "ldaps://example.com",
|
||||
|
||||
//
|
||||
// LDAP base dn
|
||||
// LDAP connection tls options (only used if scheme is ldaps://)
|
||||
//
|
||||
// @type object (see nodejs' tls.connect() options)
|
||||
// @default {}
|
||||
//
|
||||
// Example:
|
||||
// You can use this option in order to force the use of IPv6:
|
||||
// {
|
||||
// host: 'my::ip::v6',
|
||||
// servername: 'example.com'
|
||||
// }
|
||||
tlsOptions: {},
|
||||
|
||||
//
|
||||
// LDAP base dn, alternative to searchDN
|
||||
//
|
||||
// @type string
|
||||
//
|
||||
baseDN: "ou=accounts,dc=example,dc=com",
|
||||
// baseDN: "ou=accounts,dc=example,dc=com",
|
||||
|
||||
//
|
||||
// LDAP primary key
|
||||
@ -394,7 +433,58 @@ module.exports = {
|
||||
// @type string
|
||||
// @default "uid"
|
||||
//
|
||||
primaryKey: "uid"
|
||||
primaryKey: "uid",
|
||||
|
||||
//
|
||||
// LDAP search dn settings. This defines the procedure by which the
|
||||
// lounge first look for user DN before authenticating her.
|
||||
// Ignored if baseDN is specified
|
||||
//
|
||||
// @type object
|
||||
//
|
||||
searchDN: {
|
||||
|
||||
//
|
||||
// LDAP searching bind DN
|
||||
// This bind DN is used to query the server for the DN of the user.
|
||||
// This is supposed to be a system user that has access in read only to
|
||||
// the DNs of the people that are allowed to log in.
|
||||
//
|
||||
// @type string
|
||||
//
|
||||
rootDN: "cn=thelounge,ou=system-users,dc=example,dc=com",
|
||||
|
||||
//
|
||||
// Password of the lounge LDAP system user
|
||||
//
|
||||
// @type string
|
||||
//
|
||||
rootPassword: "1234",
|
||||
|
||||
//
|
||||
// LDAP filter
|
||||
//
|
||||
// @type string
|
||||
// @default "uid"
|
||||
//
|
||||
filter: "(objectClass=person)(memberOf=ou=accounts,dc=example,dc=com)",
|
||||
|
||||
//
|
||||
// LDAP search base (search only within this node)
|
||||
//
|
||||
// @type string
|
||||
//
|
||||
base: "dc=example,dc=com",
|
||||
|
||||
//
|
||||
// LDAP search scope
|
||||
//
|
||||
// @type string
|
||||
// @default "sub"
|
||||
//
|
||||
scope: "sub"
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
// Extra debugging
|
||||
|
2
index.js
2
index.js
@ -8,9 +8,11 @@ process.chdir(__dirname);
|
||||
// Doing this check as soon as possible allows us to avoid ES6 parser errors or other issues
|
||||
var pkg = require("./package.json");
|
||||
if (!require("semver").satisfies(process.version, pkg.engines.node)) {
|
||||
/* eslint-disable no-console */
|
||||
console.error("=== WARNING!");
|
||||
console.error("=== The oldest supported Node.js version is", pkg.engines.node);
|
||||
console.error("=== We strongly encourage you to upgrade, see https://nodejs.org/en/download/package-manager/ for more details\n");
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
|
||||
require("./src/command-line");
|
||||
|
17
package.json
17
package.json
@ -47,19 +47,20 @@
|
||||
"event-stream": "3.3.4",
|
||||
"express": "4.15.4",
|
||||
"express-handlebars": "3.0.0",
|
||||
"fs-extra": "4.0.1",
|
||||
"fs-extra": "4.0.2",
|
||||
"irc-framework": "2.9.1",
|
||||
"ldapjs": "1.0.1",
|
||||
"lodash": "4.17.4",
|
||||
"moment": "2.18.1",
|
||||
"package-json": "4.0.1",
|
||||
"read": "1.0.7",
|
||||
"request": "2.81.0",
|
||||
"request": "2.82.0",
|
||||
"semver": "5.4.1",
|
||||
"socket.io": "1.7.4",
|
||||
"spdy": "3.4.7",
|
||||
"ua-parser-js": "0.7.14",
|
||||
"urijs": "1.18.12",
|
||||
"web-push": "3.2.2"
|
||||
"web-push": "3.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "6.26.0",
|
||||
@ -68,7 +69,7 @@
|
||||
"chai": "4.1.2",
|
||||
"css.escape": "1.5.1",
|
||||
"emoji-regex": "6.5.1",
|
||||
"eslint": "4.5.0",
|
||||
"eslint": "4.7.2",
|
||||
"font-awesome": "4.7.0",
|
||||
"fuzzy": "0.1.3",
|
||||
"handlebars": "4.0.10",
|
||||
@ -77,13 +78,13 @@
|
||||
"jquery": "3.2.1",
|
||||
"jquery-textcomplete": "1.8.4",
|
||||
"jquery-ui": "1.12.1",
|
||||
"mocha": "3.5.0",
|
||||
"mocha": "3.5.3",
|
||||
"mousetrap": "1.6.1",
|
||||
"npm-run-all": "4.1.1",
|
||||
"nyc": "11.1.0",
|
||||
"nyc": "11.2.1",
|
||||
"socket.io-client": "1.7.4",
|
||||
"stylelint": "8.0.0",
|
||||
"stylelint": "8.1.1",
|
||||
"stylelint-config-standard": "17.0.0",
|
||||
"webpack": "3.5.5"
|
||||
"webpack": "3.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,27 @@
|
||||
"use strict";
|
||||
|
||||
var fs = require("fs-extra");
|
||||
const colors = require("colors/safe");
|
||||
const fs = require("fs-extra");
|
||||
const log = require("../src/log");
|
||||
|
||||
var srcDir = "./node_modules/font-awesome/fonts/";
|
||||
var destDir = "./client/fonts/";
|
||||
var fonts = [
|
||||
const srcDir = "./node_modules/font-awesome/fonts/";
|
||||
const destDir = "./client/fonts/";
|
||||
const fonts = [
|
||||
"fontawesome-webfont.woff",
|
||||
"fontawesome-webfont.woff2"
|
||||
];
|
||||
|
||||
fs.ensureDir(destDir, function(dirErr) {
|
||||
fs.ensureDir(destDir, (dirErr) => {
|
||||
if (dirErr) {
|
||||
console.error(dirErr);
|
||||
log.error(dirErr);
|
||||
}
|
||||
|
||||
fonts.forEach(function(font) {
|
||||
fs.copy(srcDir + font, destDir + font, function(err) {
|
||||
fonts.forEach((font) => {
|
||||
fs.copy(srcDir + font, destDir + font, (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
log.error(err);
|
||||
} else {
|
||||
console.log(font + " successfully installed.");
|
||||
log.info(colors.bold(font) + " successfully installed.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -5,7 +5,6 @@ var colors = require("colors/safe");
|
||||
var pkg = require("../package.json");
|
||||
var Chan = require("./models/chan");
|
||||
var crypto = require("crypto");
|
||||
var userLog = require("./userLog");
|
||||
var Msg = require("./models/msg");
|
||||
var Network = require("./models/network");
|
||||
var ircFramework = require("irc-framework");
|
||||
@ -16,6 +15,7 @@ module.exports = Client;
|
||||
|
||||
var id = 0;
|
||||
var events = [
|
||||
"away",
|
||||
"connection",
|
||||
"unhandled",
|
||||
"banlist",
|
||||
@ -114,23 +114,6 @@ Client.prototype.emit = function(event, data) {
|
||||
if (this.sockets !== null) {
|
||||
this.sockets.in(this.id).emit(event, data);
|
||||
}
|
||||
if (this.config.log === true) {
|
||||
if (event === "msg") {
|
||||
var target = this.find(data.chan);
|
||||
if (target) {
|
||||
var chan = target.chan.name;
|
||||
if (target.chan.type === Chan.Type.LOBBY) {
|
||||
chan = target.network.host;
|
||||
}
|
||||
userLog.write(
|
||||
this.name,
|
||||
target.network.host,
|
||||
chan,
|
||||
data.msg
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Client.prototype.find = function(channelId) {
|
||||
@ -412,10 +395,20 @@ Client.prototype.more = function(data) {
|
||||
}
|
||||
|
||||
const chan = target.chan;
|
||||
const index = chan.messages.findIndex((val) => val.id === data.lastId);
|
||||
let messages = [];
|
||||
let index = 0;
|
||||
|
||||
// If we don't find the requested message, send an empty array
|
||||
const messages = index > 0 ? chan.messages.slice(Math.max(0, index - 100), index) : [];
|
||||
// If client requests -1, send last 100 messages
|
||||
if (data.lastId < 0) {
|
||||
index = chan.messages.length;
|
||||
} else {
|
||||
index = chan.messages.findIndex((val) => val.id === data.lastId);
|
||||
}
|
||||
|
||||
// If requested id is not found, an empty array will be sent
|
||||
if (index > 0) {
|
||||
messages = chan.messages.slice(Math.max(0, index - 100), index);
|
||||
}
|
||||
|
||||
client.emit("more", {
|
||||
chan: chan.id,
|
||||
@ -437,7 +430,7 @@ Client.prototype.open = function(socketId, target) {
|
||||
|
||||
target.chan.firstUnread = 0;
|
||||
target.chan.unread = 0;
|
||||
target.chan.highlight = false;
|
||||
target.chan.highlight = 0;
|
||||
|
||||
this.attachedClients[socketId].openChannel = target.chan.id;
|
||||
this.lastActiveChannel = target.chan.id;
|
||||
|
@ -32,6 +32,7 @@ require("./add");
|
||||
require("./remove");
|
||||
require("./reset");
|
||||
require("./edit");
|
||||
require("./install");
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
|
82
src/command-line/install.js
Normal file
82
src/command-line/install.js
Normal file
@ -0,0 +1,82 @@
|
||||
"use strict";
|
||||
|
||||
const colors = require("colors/safe");
|
||||
const program = require("commander");
|
||||
const Helper = require("../helper");
|
||||
const Utils = require("./utils");
|
||||
|
||||
program
|
||||
.command("install <package>")
|
||||
.description("Install a theme or a package")
|
||||
.on("--help", Utils.extraHelp)
|
||||
.action(function(packageName) {
|
||||
const fs = require("fs");
|
||||
const fsextra = require("fs-extra");
|
||||
const path = require("path");
|
||||
const child = require("child_process");
|
||||
const packageJson = require("package-json");
|
||||
|
||||
if (!fs.existsSync(Helper.CONFIG_PATH)) {
|
||||
log.error(`${Helper.CONFIG_PATH} does not exist.`);
|
||||
return;
|
||||
}
|
||||
|
||||
packageJson(packageName, {
|
||||
fullMetadata: true
|
||||
}).then((json) => {
|
||||
if (!("lounge" in json)) {
|
||||
log.error(`${colors.red(packageName)} does not have The Lounge metadata.`);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log.info(`Installing ${colors.green(packageName)}...`);
|
||||
|
||||
const packagesPath = Helper.getPackagesPath();
|
||||
const packagesParent = path.dirname(packagesPath);
|
||||
const packagesConfig = path.join(packagesParent, "package.json");
|
||||
|
||||
// Create node_modules folder, otherwise npm will start walking upwards to find one
|
||||
fsextra.ensureDirSync(packagesPath);
|
||||
|
||||
// Create package.json with private set to true to avoid npm warnings
|
||||
fs.writeFileSync(packagesConfig, JSON.stringify({
|
||||
private: true,
|
||||
description: "Packages for The Lounge. All packages in node_modules directory will be automatically loaded.",
|
||||
}, null, "\t"));
|
||||
|
||||
const npm = child.spawn(
|
||||
process.platform === "win32" ? "npm.cmd" : "npm",
|
||||
[
|
||||
"install",
|
||||
"--production",
|
||||
"--no-save",
|
||||
"--no-bin-links",
|
||||
"--no-package-lock",
|
||||
"--prefix",
|
||||
packagesParent,
|
||||
packageName
|
||||
],
|
||||
{
|
||||
stdio: "inherit"
|
||||
}
|
||||
);
|
||||
|
||||
npm.on("error", (e) => {
|
||||
log.error(`${e}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
npm.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
log.error(`Failed to install ${colors.green(packageName)}. Exit code: ${code}`);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`${colors.green(packageName)} has been successfully installed.`);
|
||||
});
|
||||
}).catch((e) => {
|
||||
log.error(`${e}`);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
@ -15,7 +15,7 @@ class Utils {
|
||||
"",
|
||||
` LOUNGE_HOME Path for all configuration files and folders. Defaults to ${colors.green(Utils.defaultLoungeHome())}.`,
|
||||
"",
|
||||
].forEach((e) => console.log(e));
|
||||
].forEach((e) => console.log(e)); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
static defaultLoungeHome() {
|
||||
|
@ -12,6 +12,8 @@ const colors = require("colors/safe");
|
||||
var Helper = {
|
||||
config: null,
|
||||
expandHome: expandHome,
|
||||
getPackagesPath: getPackagesPath,
|
||||
getPackageModulePath: getPackageModulePath,
|
||||
getStoragePath: getStoragePath,
|
||||
getUserConfigPath: getUserConfigPath,
|
||||
getUserLogsPath: getUserLogsPath,
|
||||
@ -82,6 +84,14 @@ function setHome(homePath) {
|
||||
log.warn("debug option is now an object, see defaults file for more information.");
|
||||
this.config.debug = {ircFramework: true};
|
||||
}
|
||||
|
||||
// TODO: Remove in future release
|
||||
// Backwards compatibility for old way of specifying themes in settings
|
||||
if (this.config.theme.includes(".css")) {
|
||||
log.warn(`Referring to CSS files in the ${colors.green("theme")} setting of ${colors.green(Helper.CONFIG_PATH)} is ${colors.bold("deprecated")} and will be removed in a future version.`);
|
||||
} else {
|
||||
this.config.theme = `themes/${this.config.theme}.css`;
|
||||
}
|
||||
}
|
||||
|
||||
function getUserConfigPath(name) {
|
||||
@ -96,6 +106,14 @@ function getStoragePath() {
|
||||
return path.join(this.HOME, "storage");
|
||||
}
|
||||
|
||||
function getPackagesPath() {
|
||||
return path.join(this.HOME, "packages", "node_modules");
|
||||
}
|
||||
|
||||
function getPackageModulePath(packageName) {
|
||||
return path.join(Helper.getPackagesPath(), packageName);
|
||||
}
|
||||
|
||||
function ip2hex(address) {
|
||||
// no ipv6 support
|
||||
if (!net.isIPv4(address)) {
|
||||
|
@ -16,6 +16,7 @@ function timestamp(type, messageArgs) {
|
||||
return messageArgs;
|
||||
}
|
||||
|
||||
/* eslint-disable no-console */
|
||||
exports.error = function() {
|
||||
console.error.apply(console, timestamp(colors.red("[ERROR]"), arguments));
|
||||
};
|
||||
@ -31,6 +32,7 @@ exports.info = function() {
|
||||
exports.debug = function() {
|
||||
console.log.apply(console, timestamp(colors.green("[DEBUG]"), arguments));
|
||||
};
|
||||
/* eslint-enable no-console */
|
||||
|
||||
exports.prompt = (options, callback) => {
|
||||
options.prompt = timestamp(colors.cyan("[PROMPT]"), [options.text]).join(" ");
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
var _ = require("lodash");
|
||||
var Helper = require("../helper");
|
||||
const userLog = require("../userLog");
|
||||
const storage = require("../plugins/storage");
|
||||
|
||||
module.exports = Chan;
|
||||
@ -13,7 +14,7 @@ Chan.Type = {
|
||||
SPECIAL: "special",
|
||||
};
|
||||
|
||||
var id = 0;
|
||||
let id = 1;
|
||||
|
||||
function Chan(attr) {
|
||||
_.defaults(this, attr, {
|
||||
@ -25,7 +26,7 @@ function Chan(attr) {
|
||||
type: Chan.Type.CHANNEL,
|
||||
firstUnread: 0,
|
||||
unread: 0,
|
||||
highlight: false,
|
||||
highlight: 0,
|
||||
users: []
|
||||
});
|
||||
}
|
||||
@ -57,6 +58,10 @@ Chan.prototype.pushMessage = function(client, msg, increasesUnread) {
|
||||
|
||||
this.messages.push(msg);
|
||||
|
||||
if (client.config.log === true) {
|
||||
writeUserLog.call(this, client, msg);
|
||||
}
|
||||
|
||||
if (Helper.config.maxHistory >= 0 && this.messages.length > Helper.config.maxHistory) {
|
||||
const deleted = this.messages.splice(0, this.messages.length - Helper.config.maxHistory);
|
||||
|
||||
@ -73,7 +78,7 @@ Chan.prototype.pushMessage = function(client, msg, increasesUnread) {
|
||||
}
|
||||
|
||||
if (msg.highlight) {
|
||||
this.highlight = true;
|
||||
this.highlight++;
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -127,3 +132,14 @@ Chan.prototype.toJSON = function() {
|
||||
clone.messages = clone.messages.slice(-100);
|
||||
return clone;
|
||||
};
|
||||
|
||||
function writeUserLog(client, msg) {
|
||||
const target = client.find(this.id);
|
||||
|
||||
userLog.write(
|
||||
client.name,
|
||||
target.network.host, // TODO: Fix #1392, multiple connections to same server results in duplicate logs
|
||||
this.type === Chan.Type.LOBBY ? target.network.host : this.name,
|
||||
msg
|
||||
);
|
||||
}
|
||||
|
@ -29,7 +29,9 @@ class Msg {
|
||||
|
||||
Msg.Type = {
|
||||
UNHANDLED: "unhandled",
|
||||
AWAY: "away",
|
||||
ACTION: "action",
|
||||
BACK: "back",
|
||||
ERROR: "error",
|
||||
INVITE: "invite",
|
||||
JOIN: "join",
|
||||
|
@ -5,7 +5,7 @@ var Chan = require("./chan");
|
||||
|
||||
module.exports = Network;
|
||||
|
||||
var id = 0;
|
||||
let id = 1;
|
||||
|
||||
function Network(attr) {
|
||||
_.defaults(this, attr, {
|
||||
|
@ -7,6 +7,7 @@ module.exports = User;
|
||||
function User(attr, prefixLookup) {
|
||||
_.defaults(this, attr, {
|
||||
modes: [],
|
||||
away: "",
|
||||
mode: "",
|
||||
nick: "",
|
||||
lastMessage: 0,
|
||||
|
132
src/plugins/auth/ldap.js
Normal file
132
src/plugins/auth/ldap.js
Normal file
@ -0,0 +1,132 @@
|
||||
"use strict";
|
||||
|
||||
const Helper = require("../../helper");
|
||||
const ldap = require("ldapjs");
|
||||
|
||||
function ldapAuthCommon(user, bindDN, password, callback) {
|
||||
const config = Helper.config;
|
||||
|
||||
const ldapclient = ldap.createClient({
|
||||
url: config.ldap.url,
|
||||
tlsOptions: config.ldap.tlsOptions
|
||||
});
|
||||
|
||||
ldapclient.on("error", function(err) {
|
||||
log.error(`Unable to connect to LDAP server: ${err}`);
|
||||
callback(!err);
|
||||
});
|
||||
|
||||
ldapclient.bind(bindDN, password, function(err) {
|
||||
ldapclient.unbind();
|
||||
callback(!err);
|
||||
});
|
||||
}
|
||||
|
||||
function simpleLdapAuth(user, password, callback) {
|
||||
if (!user) {
|
||||
return callback(false);
|
||||
}
|
||||
|
||||
const config = Helper.config;
|
||||
|
||||
const userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1");
|
||||
const bindDN = `${config.ldap.primaryKey}=${userDN},${config.ldap.baseDN}`;
|
||||
|
||||
log.info(`Auth against LDAP ${config.ldap.url} with provided bindDN ${bindDN}`);
|
||||
|
||||
ldapAuthCommon(user, bindDN, password, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* LDAP auth using initial DN search (see config comment for ldap.searchDN)
|
||||
*/
|
||||
function advancedLdapAuth(user, password, callback) {
|
||||
if (!user) {
|
||||
return callback(false);
|
||||
}
|
||||
|
||||
const config = Helper.config;
|
||||
const userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1");
|
||||
|
||||
const ldapclient = ldap.createClient({
|
||||
url: config.ldap.url,
|
||||
tlsOptions: config.ldap.tlsOptions
|
||||
});
|
||||
|
||||
const base = config.ldap.searchDN.base;
|
||||
const searchOptions = {
|
||||
scope: config.ldap.searchDN.scope,
|
||||
filter: `(&(${config.ldap.primaryKey}=${userDN})${config.ldap.searchDN.filter})`,
|
||||
attributes: ["dn"]
|
||||
};
|
||||
|
||||
ldapclient.on("error", function(err) {
|
||||
log.error(`Unable to connect to LDAP server: ${err}`);
|
||||
callback(!err);
|
||||
});
|
||||
|
||||
ldapclient.bind(config.ldap.searchDN.rootDN, config.ldap.searchDN.rootPassword, function(err) {
|
||||
if (err) {
|
||||
log.error("Invalid LDAP root credentials");
|
||||
ldapclient.unbind();
|
||||
callback(false);
|
||||
} else {
|
||||
ldapclient.search(base, searchOptions, function(err2, res) {
|
||||
if (err2) {
|
||||
log.warning(`User not found: ${userDN}`);
|
||||
ldapclient.unbind();
|
||||
callback(false);
|
||||
} else {
|
||||
let found = false;
|
||||
res.on("searchEntry", function(entry) {
|
||||
found = true;
|
||||
const bindDN = entry.objectName;
|
||||
log.info(`Auth against LDAP ${config.ldap.url} with found bindDN ${bindDN}`);
|
||||
ldapclient.unbind();
|
||||
|
||||
ldapAuthCommon(user, bindDN, password, callback);
|
||||
});
|
||||
res.on("error", function(err3) {
|
||||
log.error(`LDAP error: ${err3}`);
|
||||
callback(false);
|
||||
});
|
||||
res.on("end", function() {
|
||||
if (!found) {
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ldapAuth(manager, client, user, password, callback) {
|
||||
// TODO: Enable the use of starttls() as an alternative to ldaps
|
||||
|
||||
// TODO: move this out of here and get rid of `manager` and `client` in
|
||||
// auth plugin API
|
||||
function callbackWrapper(valid) {
|
||||
if (valid && !client) {
|
||||
manager.addUser(user, null);
|
||||
}
|
||||
callback(valid);
|
||||
}
|
||||
|
||||
let auth;
|
||||
if ("baseDN" in Helper.config.ldap) {
|
||||
auth = simpleLdapAuth;
|
||||
} else {
|
||||
auth = advancedLdapAuth;
|
||||
}
|
||||
return auth(user, password, callbackWrapper);
|
||||
}
|
||||
|
||||
function isLdapEnabled() {
|
||||
return !Helper.config.public && Helper.config.ldap.enable;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
auth: ldapAuth,
|
||||
isEnabled: isLdapEnabled
|
||||
};
|
42
src/plugins/auth/local.js
Normal file
42
src/plugins/auth/local.js
Normal file
@ -0,0 +1,42 @@
|
||||
"use strict";
|
||||
|
||||
const Helper = require("../../helper");
|
||||
const colors = require("colors/safe");
|
||||
|
||||
function localAuth(manager, client, user, password, callback) {
|
||||
// If no user is found, or if the client has not provided a password,
|
||||
// fail the authentication straight away
|
||||
if (!client || !password) {
|
||||
return callback(false);
|
||||
}
|
||||
|
||||
// If this user has no password set, fail the authentication
|
||||
if (!client.config.password) {
|
||||
log.error(`User ${colors.bold(user)} with no local password set tried to sign in. (Probably a LDAP user)`);
|
||||
return callback(false);
|
||||
}
|
||||
|
||||
Helper.password
|
||||
.compare(password, client.config.password)
|
||||
.then((matching) => {
|
||||
if (matching && Helper.password.requiresUpdate(client.config.password)) {
|
||||
const hash = Helper.password.hash(password);
|
||||
|
||||
client.setPassword(hash, (success) => {
|
||||
if (success) {
|
||||
log.info(`User ${colors.bold(client.name)} logged in and their hashed password has been updated to match new security requirements`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
callback(matching);
|
||||
}).catch((error) => {
|
||||
log.error(`Error while checking users password. Error: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
auth: localAuth,
|
||||
isEnabled: () => true
|
||||
};
|
||||
|
@ -9,7 +9,17 @@ exports.commands = ["close", "leave", "part"];
|
||||
exports.allowDisconnected = true;
|
||||
|
||||
exports.input = function(network, chan, cmd, args) {
|
||||
if (chan.type === Chan.Type.LOBBY) {
|
||||
let target = args.length === 0 ? chan : _.find(network.channels, {name: args[0]});
|
||||
let partMessage = args.length <= 1 ? Helper.config.leaveMessage : args.slice(1).join(" ");
|
||||
|
||||
if (typeof target === "undefined") {
|
||||
// In this case, we assume that the word args[0] is part of the leave
|
||||
// message and we part the current chan.
|
||||
target = chan;
|
||||
partMessage = args.join(" ");
|
||||
}
|
||||
|
||||
if (target.type === Chan.Type.LOBBY) {
|
||||
chan.pushMessage(this, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "You can not part from networks, use /quit instead."
|
||||
@ -17,18 +27,17 @@ exports.input = function(network, chan, cmd, args) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels = _.without(network.channels, chan);
|
||||
chan.destroy();
|
||||
network.channels = _.without(network.channels, target);
|
||||
target.destroy();
|
||||
this.emit("part", {
|
||||
chan: chan.id
|
||||
chan: target.id
|
||||
});
|
||||
|
||||
if (chan.type === Chan.Type.CHANNEL) {
|
||||
if (target.type === Chan.Type.CHANNEL) {
|
||||
this.save();
|
||||
|
||||
if (network.irc) {
|
||||
const partMessage = args[0] ? args.join(" ") : Helper.config.leaveMessage;
|
||||
network.irc.part(chan.name, partMessage);
|
||||
network.irc.part(target.name, partMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
30
src/plugins/irc-events/away.js
Normal file
30
src/plugins/irc-events/away.js
Normal file
@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
|
||||
const _ = require("lodash");
|
||||
const Msg = require("../../models/msg");
|
||||
|
||||
module.exports = function(irc, network) {
|
||||
const client = this;
|
||||
irc.on("away", (data) => {
|
||||
const away = data.message;
|
||||
|
||||
network.channels.forEach((chan) => {
|
||||
const user = _.find(chan.users, {nick: data.nick});
|
||||
|
||||
if (!user || user.away === away) {
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = new Msg({
|
||||
type: away ? Msg.Type.AWAY : Msg.Type.BACK,
|
||||
text: away || "",
|
||||
time: data.time,
|
||||
from: data.nick,
|
||||
mode: user.mode
|
||||
});
|
||||
|
||||
chan.pushMessage(client, msg);
|
||||
user.away = away;
|
||||
});
|
||||
});
|
||||
};
|
@ -11,12 +11,13 @@ module.exports = function(irc, network) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = chan.findUser(data.kicked);
|
||||
const kicker = chan.findUser(data.nick);
|
||||
const target = chan.findUser(data.kicked);
|
||||
|
||||
if (data.kicked === irc.user.nick) {
|
||||
chan.users = [];
|
||||
} else {
|
||||
chan.users = _.without(chan.users, user);
|
||||
chan.users = _.without(chan.users, target);
|
||||
}
|
||||
|
||||
client.emit("users", {
|
||||
@ -26,9 +27,8 @@ module.exports = function(irc, network) {
|
||||
var msg = new Msg({
|
||||
type: Msg.Type.KICK,
|
||||
time: data.time,
|
||||
mode: user.mode,
|
||||
from: data.nick,
|
||||
target: data.kicked,
|
||||
from: kicker,
|
||||
target: target,
|
||||
text: data.message || "",
|
||||
highlight: data.kicked === irc.user.nick,
|
||||
self: data.nick === irc.user.nick
|
||||
|
@ -106,20 +106,29 @@ module.exports = function(irc, network) {
|
||||
|
||||
// Do not send notifications for messages older than 15 minutes (znc buffer for example)
|
||||
if (highlight && (!data.time || data.time > Date.now() - 900000)) {
|
||||
let title = data.nick;
|
||||
let title = chan.name;
|
||||
let body = Helper.cleanIrcMessage(data.message);
|
||||
|
||||
// In channels, prepend sender nickname to the message
|
||||
if (chan.type !== Chan.Type.QUERY) {
|
||||
title += ` (${chan.name}) mentioned you`;
|
||||
} else {
|
||||
title += " sent you a message";
|
||||
body = `${data.nick}: ${body}`;
|
||||
}
|
||||
|
||||
// If a channel is active on any client, highlight won't increment and notification will say (0 mention)
|
||||
if (chan.highlight > 0) {
|
||||
title += ` (${chan.highlight} ${chan.type === Chan.Type.QUERY ? "new message" : "mention"}${chan.highlight > 1 ? "s" : ""})`;
|
||||
}
|
||||
|
||||
if (chan.highlight > 1) {
|
||||
body += `\n\n… and ${chan.highlight - 1} other message${chan.highlight > 2 ? "s" : ""}`;
|
||||
}
|
||||
|
||||
client.manager.webPush.push(client, {
|
||||
type: "notification",
|
||||
chanId: chan.id,
|
||||
timestamp: data.time || Date.now(),
|
||||
title: `The Lounge: ${title}`,
|
||||
body: Helper.cleanIrcMessage(data.message)
|
||||
title: title,
|
||||
body: body
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
85
src/plugins/themes.js
Normal file
85
src/plugins/themes.js
Normal file
@ -0,0 +1,85 @@
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const Helper = require("../helper");
|
||||
const colors = require("colors/safe");
|
||||
const path = require("path");
|
||||
const _ = require("lodash");
|
||||
const themes = new Map();
|
||||
|
||||
module.exports = {
|
||||
getAll: getAll,
|
||||
getFilename: getFilename
|
||||
};
|
||||
|
||||
fs.readdir("client/themes/", (err, builtInThemes) => {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
builtInThemes
|
||||
.filter((theme) => theme.endsWith(".css"))
|
||||
.map(makeLocalThemeObject)
|
||||
.forEach((theme) => themes.set(theme.name, theme));
|
||||
});
|
||||
|
||||
fs.readdir(Helper.getPackagesPath(), (err, packages) => {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
packages
|
||||
.map(makePackageThemeObject)
|
||||
.forEach((theme) => {
|
||||
if (theme) {
|
||||
themes.set(theme.name, theme);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getAll() {
|
||||
return _.sortBy(Array.from(themes.values()), "displayName");
|
||||
}
|
||||
|
||||
function getFilename(module) {
|
||||
if (themes.has(module)) {
|
||||
return themes.get(module).filename;
|
||||
}
|
||||
}
|
||||
|
||||
function makeLocalThemeObject(css) {
|
||||
const themeName = css.slice(0, -4);
|
||||
return {
|
||||
displayName: themeName.charAt(0).toUpperCase() + themeName.slice(1),
|
||||
filename: path.join(__dirname, "..", "client", "themes", `${themeName}.css`),
|
||||
name: themeName
|
||||
};
|
||||
}
|
||||
|
||||
function getModuleInfo(packageName) {
|
||||
let module;
|
||||
try {
|
||||
module = require(Helper.getPackageModulePath(packageName));
|
||||
} catch (e) {
|
||||
log.warn(`Specified theme ${colors.yellow(packageName)} is not installed in packages directory`);
|
||||
return;
|
||||
}
|
||||
if (!module.lounge) {
|
||||
log.warn(`Specified theme ${colors.yellow(packageName)} doesn't have required information.`);
|
||||
return;
|
||||
}
|
||||
return module.lounge;
|
||||
}
|
||||
|
||||
function makePackageThemeObject(moduleName) {
|
||||
const module = getModuleInfo(moduleName);
|
||||
if (!module || module.type !== "theme") {
|
||||
return;
|
||||
}
|
||||
const modulePath = Helper.getPackageModulePath(moduleName);
|
||||
const displayName = module.name || moduleName;
|
||||
const filename = path.join(modulePath, module.css);
|
||||
return {
|
||||
displayName: displayName,
|
||||
filename: filename,
|
||||
name: moduleName
|
||||
};
|
||||
}
|
126
src/server.js
126
src/server.js
@ -11,10 +11,20 @@ var path = require("path");
|
||||
var io = require("socket.io");
|
||||
var dns = require("dns");
|
||||
var Helper = require("./helper");
|
||||
var ldap = require("ldapjs");
|
||||
var colors = require("colors/safe");
|
||||
const net = require("net");
|
||||
const Identification = require("./identification");
|
||||
const themes = require("./plugins/themes");
|
||||
|
||||
// The order defined the priority: the first available plugin is used
|
||||
// ALways keep local auth in the end, which should always be enabled.
|
||||
const authPlugins = [
|
||||
require("./plugins/auth/ldap"),
|
||||
require("./plugins/auth/local"),
|
||||
];
|
||||
|
||||
// A random number that will force clients to reload the page if it differs
|
||||
const serverHash = Math.floor(Date.now() * Math.random());
|
||||
|
||||
var manager = null;
|
||||
|
||||
@ -45,6 +55,15 @@ module.exports = function() {
|
||||
.set("view engine", "html")
|
||||
.set("views", path.join(__dirname, "..", "client"));
|
||||
|
||||
app.get("/themes/:theme.css", (req, res) => {
|
||||
const themeName = req.params.theme;
|
||||
const theme = themes.getFilename(themeName);
|
||||
if (theme === undefined) {
|
||||
return res.status(404).send("Not found");
|
||||
}
|
||||
return res.sendFile(theme);
|
||||
});
|
||||
|
||||
var config = Helper.config;
|
||||
var server = null;
|
||||
|
||||
@ -94,6 +113,8 @@ module.exports = function() {
|
||||
};
|
||||
}
|
||||
|
||||
server.on("error", (err) => log.error(`${err}`));
|
||||
|
||||
server.listen(listenParams, () => {
|
||||
if (typeof listenParams === "string") {
|
||||
log.info("Available on socket " + colors.green(listenParams));
|
||||
@ -117,7 +138,10 @@ module.exports = function() {
|
||||
if (config.public) {
|
||||
performAuthentication.call(socket, {});
|
||||
} else {
|
||||
socket.emit("auth", {success: true});
|
||||
socket.emit("auth", {
|
||||
serverHash: serverHash,
|
||||
success: true,
|
||||
});
|
||||
socket.on("auth", performAuthentication);
|
||||
}
|
||||
});
|
||||
@ -184,15 +208,7 @@ function index(req, res, next) {
|
||||
Helper.config
|
||||
);
|
||||
data.gitCommit = Helper.getGitCommit();
|
||||
data.themes = fs.readdirSync("client/themes/").filter(function(themeFile) {
|
||||
return themeFile.endsWith(".css");
|
||||
}).map(function(css) {
|
||||
const filename = css.slice(0, -4);
|
||||
return {
|
||||
name: filename.charAt(0).toUpperCase() + filename.slice(1),
|
||||
filename: filename
|
||||
};
|
||||
});
|
||||
data.themes = themes.getAll();
|
||||
|
||||
const policies = [
|
||||
"default-src *",
|
||||
@ -215,7 +231,7 @@ function index(req, res, next) {
|
||||
res.render("index", data);
|
||||
}
|
||||
|
||||
function initializeClient(socket, client, token) {
|
||||
function initializeClient(socket, client, token, lastMessage) {
|
||||
socket.emit("authorized");
|
||||
|
||||
socket.on("disconnect", function() {
|
||||
@ -379,11 +395,24 @@ function initializeClient(socket, client, token) {
|
||||
socket.join(client.id);
|
||||
|
||||
const sendInitEvent = (tokenToSend) => {
|
||||
let networks = client.networks;
|
||||
|
||||
if (lastMessage > -1) {
|
||||
// We need a deep cloned object because we are going to remove unneeded messages
|
||||
networks = _.cloneDeep(networks);
|
||||
|
||||
networks.forEach((network) => {
|
||||
network.channels.forEach((channel) => {
|
||||
channel.messages = channel.messages.filter((m) => m.id > lastMessage);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
socket.emit("init", {
|
||||
applicationServerKey: manager.webPush.vapidKeys.publicKey,
|
||||
pushSubscription: client.config.sessions[token],
|
||||
active: client.lastActiveChannel,
|
||||
networks: client.networks,
|
||||
networks: networks,
|
||||
token: tokenToSend
|
||||
});
|
||||
};
|
||||
@ -409,67 +438,11 @@ function initializeClient(socket, client, token) {
|
||||
}
|
||||
}
|
||||
|
||||
function localAuth(client, user, password, callback) {
|
||||
// If no user is found, or if the client has not provided a password,
|
||||
// fail the authentication straight away
|
||||
if (!client || !password) {
|
||||
return callback(false);
|
||||
}
|
||||
|
||||
// If this user has no password set, fail the authentication
|
||||
if (!client.config.password) {
|
||||
log.error(`User ${colors.bold(user)} with no local password set tried to sign in. (Probably a LDAP user)`);
|
||||
return callback(false);
|
||||
}
|
||||
|
||||
Helper.password
|
||||
.compare(password, client.config.password)
|
||||
.then((matching) => {
|
||||
if (matching && Helper.password.requiresUpdate(client.config.password)) {
|
||||
const hash = Helper.password.hash(password);
|
||||
|
||||
client.setPassword(hash, (success) => {
|
||||
if (success) {
|
||||
log.info(`User ${colors.bold(client.name)} logged in and their hashed password has been updated to match new security requirements`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
callback(matching);
|
||||
}).catch((error) => {
|
||||
log.error(`Error while checking users password. Error: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
function ldapAuth(client, user, password, callback) {
|
||||
var userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1");
|
||||
var bindDN = Helper.config.ldap.primaryKey + "=" + userDN + "," + Helper.config.ldap.baseDN;
|
||||
|
||||
var ldapclient = ldap.createClient({
|
||||
url: Helper.config.ldap.url
|
||||
});
|
||||
|
||||
ldapclient.on("error", function(err) {
|
||||
log.error("Unable to connect to LDAP server", err);
|
||||
callback(!err);
|
||||
});
|
||||
|
||||
ldapclient.bind(bindDN, password, function(err) {
|
||||
if (!err && !client) {
|
||||
if (!manager.addUser(user, null)) {
|
||||
log.error("Unable to create new user", user);
|
||||
}
|
||||
}
|
||||
ldapclient.unbind();
|
||||
callback(!err);
|
||||
});
|
||||
}
|
||||
|
||||
function performAuthentication(data) {
|
||||
const socket = this;
|
||||
let client;
|
||||
|
||||
const finalInit = () => initializeClient(socket, client, data.token || null);
|
||||
const finalInit = () => initializeClient(socket, client, data.token || null, data.lastMessage || -1);
|
||||
|
||||
const initClient = () => {
|
||||
client.ip = getClientIp(socket.request);
|
||||
@ -528,11 +501,16 @@ function performAuthentication(data) {
|
||||
}
|
||||
|
||||
// Perform password checking
|
||||
if (!Helper.config.public && Helper.config.ldap.enable) {
|
||||
ldapAuth(client, data.user, data.password, authCallback);
|
||||
} else {
|
||||
localAuth(client, data.user, data.password, authCallback);
|
||||
let auth = () => {
|
||||
log.error("None of the auth plugins is enabled");
|
||||
};
|
||||
for (let i = 0; i < authPlugins.length; ++i) {
|
||||
if (authPlugins[i].isEnabled()) {
|
||||
auth = authPlugins[i].auth;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auth(manager, client, data.user, data.password, authCallback);
|
||||
}
|
||||
|
||||
function reverseDnsLookup(ip, callback) {
|
||||
|
@ -18,7 +18,7 @@ module.exports.write = function(user, network, chan, msg) {
|
||||
var format = Helper.config.logs.format || "YYYY-MM-DD HH:mm:ss";
|
||||
var tz = Helper.config.logs.timezone || "UTC+00:00";
|
||||
|
||||
var time = moment().utcOffset(tz).format(format);
|
||||
var time = moment(msg.time).utcOffset(tz).format(format);
|
||||
var line = `[${time}] `;
|
||||
|
||||
var type = msg.type.trim();
|
||||
|
2
test/fixtures/env.js
vendored
2
test/fixtures/env.js
vendored
@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
global.log = {
|
||||
error: () => console.error.apply(console, arguments),
|
||||
error: () => console.error.apply(console, arguments), // eslint-disable-line no-console
|
||||
warn: () => {},
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
|
148
test/plugins/auth/ldap.js
Normal file
148
test/plugins/auth/ldap.js
Normal file
@ -0,0 +1,148 @@
|
||||
"use strict";
|
||||
|
||||
const ldapAuth = require("../../../src/plugins/auth/ldap");
|
||||
const Helper = require("../../../src/helper");
|
||||
const ldap = require("ldapjs");
|
||||
const expect = require("chai").expect;
|
||||
|
||||
const user = "johndoe";
|
||||
const wrongUser = "eve";
|
||||
const correctPassword = "loremipsum";
|
||||
const wrongPassword = "dolorsitamet";
|
||||
const baseDN = "ou=accounts,dc=example,dc=com";
|
||||
const primaryKey = "uid";
|
||||
const serverPort = 1389;
|
||||
|
||||
function normalizeDN(dn) {
|
||||
return ldap.parseDN(dn).toString();
|
||||
}
|
||||
|
||||
function startLdapServer(callback) {
|
||||
const server = ldap.createServer();
|
||||
|
||||
const searchConf = Helper.config.ldap.searchDN;
|
||||
const userDN = primaryKey + "=" + user + "," + baseDN;
|
||||
|
||||
// Two users are authorized: john doe and the root user in case of
|
||||
// advanced auth (the user that does the search for john's actual
|
||||
// bindDN)
|
||||
const authorizedUsers = {};
|
||||
authorizedUsers[normalizeDN(searchConf.rootDN)] = searchConf.rootPassword;
|
||||
authorizedUsers[normalizeDN(userDN)] = correctPassword;
|
||||
|
||||
function authorize(req, res, next) {
|
||||
const bindDN = req.connection.ldap.bindDN;
|
||||
|
||||
if (bindDN in authorizedUsers) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return next(new ldap.InsufficientAccessRightsError());
|
||||
}
|
||||
|
||||
Object.keys(authorizedUsers).forEach(function(dn) {
|
||||
server.bind(dn, function(req, res, next) {
|
||||
const bindDN = req.dn.toString();
|
||||
const password = req.credentials;
|
||||
|
||||
if (bindDN in authorizedUsers && authorizedUsers[bindDN] === password) {
|
||||
req.connection.ldap.bindDN = req.dn;
|
||||
res.end();
|
||||
return next();
|
||||
}
|
||||
|
||||
return next(new ldap.InsufficientAccessRightsError());
|
||||
});
|
||||
});
|
||||
|
||||
server.search(searchConf.base, authorize, function(req, res) {
|
||||
const obj = {
|
||||
dn: userDN,
|
||||
attributes: {
|
||||
objectclass: ["person", "top"],
|
||||
cn: ["john doe"],
|
||||
sn: ["johnny"],
|
||||
uid: ["johndoe"],
|
||||
memberof: [baseDN]
|
||||
}
|
||||
};
|
||||
|
||||
if (req.filter.matches(obj.attributes)) {
|
||||
// TODO: check req.scope if ldapjs does not
|
||||
res.send(obj);
|
||||
}
|
||||
|
||||
res.end();
|
||||
});
|
||||
|
||||
server.listen(serverPort, callback);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
function testLdapAuth() {
|
||||
// Create mock manager and client. When client is true, manager should not
|
||||
// be used. But ideally the auth plugin should not use any of those.
|
||||
const manager = {};
|
||||
const client = true;
|
||||
|
||||
it("should successfully authenticate with correct password", function(done) {
|
||||
ldapAuth.auth(manager, client, user, correctPassword, function(valid) {
|
||||
expect(valid).to.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail to authenticate with incorrect password", function(done) {
|
||||
ldapAuth.auth(manager, client, user, wrongPassword, function(valid) {
|
||||
expect(valid).to.equal(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail to authenticate with incorrect username", function(done) {
|
||||
ldapAuth.auth(manager, client, wrongUser, correctPassword, function(valid) {
|
||||
expect(valid).to.equal(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("LDAP authentication plugin", function() {
|
||||
before(function(done) {
|
||||
this.server = startLdapServer(done);
|
||||
});
|
||||
after(function(done) {
|
||||
this.server.close();
|
||||
done();
|
||||
});
|
||||
|
||||
beforeEach(function(done) {
|
||||
Helper.config.public = false;
|
||||
Helper.config.ldap.enable = true;
|
||||
Helper.config.ldap.url = "ldap://localhost:" + String(serverPort);
|
||||
Helper.config.ldap.primaryKey = primaryKey;
|
||||
done();
|
||||
});
|
||||
|
||||
describe("LDAP authentication availability", function() {
|
||||
it("checks that the configuration is correctly tied to isEnabled()", function(done) {
|
||||
Helper.config.ldap.enable = true;
|
||||
expect(ldapAuth.isEnabled()).to.equal(true);
|
||||
Helper.config.ldap.enable = false;
|
||||
expect(ldapAuth.isEnabled()).to.equal(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Simple LDAP authentication (predefined DN pattern)", function() {
|
||||
Helper.config.ldap.baseDN = baseDN;
|
||||
testLdapAuth();
|
||||
});
|
||||
|
||||
describe("Advanced LDAP authentication (DN found by a prior search query)", function() {
|
||||
delete Helper.config.ldap.baseDN;
|
||||
testLdapAuth();
|
||||
});
|
||||
});
|
||||
|
@ -37,6 +37,11 @@ describe("Server", () => {
|
||||
describe("WebSockets", () => {
|
||||
let client;
|
||||
|
||||
before((done) => {
|
||||
Helper.config.public = true;
|
||||
done();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
client = io(webURL, {
|
||||
path: "/socket.io/",
|
||||
|
@ -79,8 +79,6 @@ if (process.env.NODE_ENV === "production") {
|
||||
sourceMap: true,
|
||||
comments: false
|
||||
}));
|
||||
} else {
|
||||
console.log("Building in development mode, bundles will not be minified.");
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
|
Loading…
Reference in New Issue
Block a user