Set up prettier for JS/Vue files (#3312)

Set up prettier for JS/Vue files
This commit is contained in:
Al McKinlay 2019-07-19 11:55:55 +01:00 committed by GitHub
commit 9ef5c6c67e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
154 changed files with 4800 additions and 3910 deletions

View file

@ -11,48 +11,21 @@ env:
node: true
rules:
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:
- error
- always-multiline
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
- allowParens: true
no-control-regex: off
no-console: error
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-constructor: error
@ -61,12 +34,10 @@ rules:
- error
- functions: false
no-var: error
object-curly-spacing: [error, never]
object-shorthand:
- error
- methods
- avoidExplicitReturnArrows: true
padded-blocks: [error, never]
padding-line-between-statements:
- error
- blankLine: always
@ -82,31 +53,12 @@ rules:
prefer-const: error
prefer-rest-params: error
prefer-spread: error
quote-props: [error, consistent-as-needed]
quotes: [error, double, avoid-escape]
rest-spread-spacing: error
semi-spacing: error
semi-style: [error, last]
semi: [error, always]
space-before-blocks: error
space-before-function-paren:
- error
- anonymous: never
named: never
asyncArrow: always # Otherwise requires `async()`
space-in-parens: [error, never]
space-infix-ops: error
spaced-comment: [error, always]
strict: off
template-curly-spacing: error
yoda: error
vue/html-indent: [error, tab]
vue/require-default-prop: off
vue/no-v-html: off
vue/no-use-v-if-with-v-for: off
vue/html-closing-bracket-newline: error
vue/multiline-html-element-content-newline: off
vue/singleline-html-element-content-newline: off
plugins:
- vue
@ -114,3 +66,5 @@ plugins:
extends:
- eslint:recommended
- plugin:vue/recommended
- prettier
- prettier/vue

View file

@ -31,3 +31,7 @@ your contributions.
corner](https://github.com/thelounge/thelounge/wiki/Maintainers'-corner).
- Please document any relevant changes in the documentation that can be found
[in its own repository](https://github.com/thelounge/thelounge.chat).
- Note that we use prettier on the project. You can set up IDE plugins to format
on save ([see VS Code one here](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)).
- We have a git hook to automatically run prettier before commit, in case you don't install the plugin.
- If for any reason, prettier does not work for you, you can run `yarn format:prettier` and that should format everything.

View file

@ -23,5 +23,3 @@ yarn.lock
.gitattributes
*.css
*.js
*.vue

View file

@ -1,5 +1,6 @@
arrowParens: always
bracketSpacing: false
printWidth: 100
trailingComma: "es5"
overrides:
- files: "*.webmanifest"

View file

@ -1,8 +1,5 @@
<template>
<div
id="viewport"
role="tablist"
>
<div id="viewport" role="tablist">
<aside id="sidebar">
<div class="scrollable-area">
<div class="logo-container">
@ -10,62 +7,55 @@
:src="`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg.svg`"
class="logo"
alt="The Lounge"
>
/>
<img
:src="`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg-inverted.svg`"
:src="
`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg-inverted.svg`
"
class="logo-inverted"
alt="The Lounge"
>
/>
</div>
<NetworkList
:networks="networks"
:active-channel="activeChannel"
/>
<NetworkList :networks="networks" :active-channel="activeChannel" />
</div>
<footer id="footer">
<span
class="tooltipped tooltipped-n tooltipped-no-touch"
aria-label="Sign in"
><button
class="icon sign-in"
data-target="#sign-in"
aria-label="Sign in"
role="tab"
aria-controls="sign-in"
aria-selected="false"
<span class="tooltipped tooltipped-n tooltipped-no-touch" aria-label="Sign in"
><button
class="icon sign-in"
data-target="#sign-in"
aria-label="Sign in"
role="tab"
aria-controls="sign-in"
aria-selected="false"
/></span>
<span
class="tooltipped tooltipped-n tooltipped-no-touch"
aria-label="Connect to network"
><button
class="icon connect"
data-target="#connect"
aria-label="Connect to network"
role="tab"
aria-controls="connect"
aria-selected="false"
><button
class="icon connect"
data-target="#connect"
aria-label="Connect to network"
role="tab"
aria-controls="connect"
aria-selected="false"
/></span>
<span
class="tooltipped tooltipped-n tooltipped-no-touch"
aria-label="Settings"
><button
class="icon settings"
data-target="#settings"
aria-label="Settings"
role="tab"
aria-controls="settings"
aria-selected="false"
<span class="tooltipped tooltipped-n tooltipped-no-touch" aria-label="Settings"
><button
class="icon settings"
data-target="#settings"
aria-label="Settings"
role="tab"
aria-controls="settings"
aria-selected="false"
/></span>
<span
class="tooltipped tooltipped-n tooltipped-no-touch"
aria-label="Help"
><button
class="icon help"
data-target="#help"
aria-label="Help"
role="tab"
aria-controls="help"
aria-selected="false"
<span class="tooltipped tooltipped-n tooltipped-no-touch" aria-label="Help"
><button
class="icon help"
data-target="#help"
aria-label="Help"
role="tab"
aria-controls="help"
aria-selected="false"
/></span>
</footer>
</aside>
@ -76,35 +66,11 @@
:network="activeChannel.network"
:channel="activeChannel.channel"
/>
<div
id="sign-in"
class="window"
role="tabpanel"
aria-label="Sign-in"
/>
<div
id="connect"
class="window"
role="tabpanel"
aria-label="Connect"
/>
<div
id="settings"
class="window"
role="tabpanel"
aria-label="Settings"
/>
<div
id="help"
class="window"
role="tabpanel"
aria-label="Help"
/>
<div
id="changelog"
class="window"
aria-label="Changelog"
/>
<div id="sign-in" class="window" role="tabpanel" aria-label="Sign-in" />
<div id="connect" class="window" role="tabpanel" aria-label="Connect" />
<div id="settings" class="window" role="tabpanel" aria-label="Settings" />
<div id="help" class="window" role="tabpanel" aria-label="Help" />
<div id="changelog" class="window" aria-label="Changelog" />
</article>
</div>
</template>

View file

@ -1,15 +1,9 @@
<template>
<ChannelWrapper
:network="network"
:channel="channel"
:active-channel="activeChannel"
>
<ChannelWrapper :network="network" :channel="channel" :active-channel="activeChannel">
<span class="name">{{ channel.name }}</span>
<span
v-if="channel.unread"
:class="{ highlight: channel.highlight }"
class="badge"
>{{ channel.unread | roundBadgeNumber }}</span>
<span v-if="channel.unread" :class="{highlight: channel.highlight}" class="badge">{{
channel.unread | roundBadgeNumber
}}</span>
<template v-if="channel.type === 'channel'">
<span
v-if="channel.state === 0"
@ -18,25 +12,13 @@
>
<span class="parted-channel-icon" />
</span>
<span
class="close-tooltip tooltipped tooltipped-w"
aria-label="Leave"
>
<button
class="close"
aria-label="Leave"
/>
<span class="close-tooltip tooltipped tooltipped-w" aria-label="Leave">
<button class="close" aria-label="Leave" />
</span>
</template>
<template v-else>
<span
class="close-tooltip tooltipped tooltipped-w"
aria-label="Close"
>
<button
class="close"
aria-label="Close"
/>
<span class="close-tooltip tooltipped tooltipped-w" aria-label="Close">
<button class="close" aria-label="Close" />
</span>
</template>
</ChannelWrapper>

View file

@ -1,11 +1,16 @@
<template>
<div
v-if="!network.isCollapsed || channel.highlight || channel.type === 'lobby' || (activeChannel && channel === activeChannel.channel)"
v-if="
!network.isCollapsed ||
channel.highlight ||
channel.type === 'lobby' ||
(activeChannel && channel === activeChannel.channel)
"
:class="[
'chan',
channel.type,
{ active: activeChannel && channel === activeChannel.channel },
{ 'parted-channel': channel.type === 'channel' && channel.state === 0 }
{active: activeChannel && channel === activeChannel.channel},
{'parted-channel': channel.type === 'channel' && channel.state === 0},
]"
:aria-label="getAriaLabel()"
:title="getAriaLabel()"
@ -16,11 +21,7 @@
:aria-selected="activeChannel && channel === activeChannel.channel"
role="tab"
>
<slot
:network="network"
:channel="channel"
:activeChannel="activeChannel"
/>
<slot :network="network" :channel="channel" :activeChannel="activeChannel" />
</div>
</template>

View file

@ -1,8 +1,5 @@
<template>
<div
id="chat-container"
class="window"
>
<div id="chat-container" class="window">
<div
id="chat"
:data-id="channel.id"
@ -21,38 +18,24 @@
role="tabpanel"
>
<div class="header">
<button
class="lt"
aria-label="Toggle channel list"
/>
<button class="lt" aria-label="Toggle channel list" />
<span class="title">{{ channel.name }}</span>
<span
:title="channel.topic"
class="topic"
><ParsedMessage
v-if="channel.topic"
:network="network"
:text="channel.topic"
<span :title="channel.topic" class="topic"
><ParsedMessage
v-if="channel.topic"
:network="network"
:text="channel.topic"
/></span>
<button
class="menu"
aria-label="Open the context menu"
/>
<button class="menu" aria-label="Open the context menu" />
<span
v-if="channel.type === 'channel'"
class="rt-tooltip tooltipped tooltipped-w"
aria-label="Toggle user list"
>
<button
class="rt"
aria-label="Toggle user list"
/>
<button class="rt" aria-label="Toggle user list" />
</span>
</div>
<div
v-if="channel.type === 'special'"
class="chat-content"
>
<div v-if="channel.type === 'special'" class="chat-content">
<div class="chat">
<div class="messages">
<div class="msg">
@ -65,26 +48,19 @@
</div>
</div>
</div>
<div
v-else
class="chat-content"
>
<div v-else class="chat-content">
<div
:class="['scroll-down tooltipped tooltipped-w tooltipped-no-touch', {'scroll-down-shown': !channel.scrolledToBottom}]"
:class="[
'scroll-down tooltipped tooltipped-w tooltipped-no-touch',
{'scroll-down-shown': !channel.scrolledToBottom},
]"
aria-label="Jump to recent messages"
@click="$refs.messageList.jumpToBottom()"
>
<div class="scroll-down-arrow" />
</div>
<MessageList
ref="messageList"
:network="network"
:channel="channel"
/>
<ChatUserList
v-if="channel.type === 'channel'"
:channel="channel"
/>
<MessageList ref="messageList" :network="network" :channel="channel" />
<ChatUserList v-if="channel.type === 'channel'" :channel="channel" />
</div>
</div>
</div>
@ -92,12 +68,11 @@
v-if="this.$root.currentUserVisibleError"
id="user-visible-error"
@click="hideUserVisibleError"
>{{ this.$root.currentUserVisibleError }}</div>
>
{{ this.$root.currentUserVisibleError }}
</div>
<span id="upload-progressbar" />
<ChatInput
:network="network"
:channel="channel"
/>
<ChatInput :network="network" :channel="channel" />
</div>
</template>
@ -126,10 +101,14 @@ export default {
computed: {
specialComponent() {
switch (this.channel.special) {
case "list_bans": return ListBans;
case "list_invites": return ListInvites;
case "list_channels": return ListChannels;
case "list_ignored": return ListIgnored;
case "list_bans":
return ListBans;
case "list_invites":
return ListInvites;
case "list_channels":
return ListChannels;
case "list_ignored":
return ListIgnored;
}
return undefined;

View file

@ -1,10 +1,5 @@
<template>
<form
id="form"
method="post"
action=""
@submit.prevent="onSubmit"
>
<form id="form" method="post" action="" @submit.prevent="onSubmit">
<span id="nick">{{ network.nick }}</span>
<textarea
id="input"
@ -23,12 +18,7 @@
aria-label="Upload file"
@click="openFileUpload"
>
<input
id="upload-input"
ref="uploadInput"
type="file"
multiple
>
<input id="upload-input" ref="uploadInput" type="file" multiple />
<button
id="upload"
type="button"
@ -80,7 +70,7 @@ const bracketWraps = {
"*": "*",
"`": "`",
"~": "~",
"_": "_",
_: "_",
};
export default {
@ -129,19 +119,22 @@ export default {
return;
}
if (this.channel.inputHistoryPosition === 0) {
this.channel.inputHistory[this.channel.inputHistoryPosition] = this.channel.pendingMessage;
const {channel} = this;
if (channel.inputHistoryPosition === 0) {
channel.inputHistory[channel.inputHistoryPosition] = channel.pendingMessage;
}
if (key === "up") {
if (this.channel.inputHistoryPosition < this.channel.inputHistory.length - 1) {
this.channel.inputHistoryPosition++;
if (channel.inputHistoryPosition < channel.inputHistory.length - 1) {
channel.inputHistoryPosition++;
}
} else if (this.channel.inputHistoryPosition > 0) {
this.channel.inputHistoryPosition--;
} else if (channel.inputHistoryPosition > 0) {
channel.inputHistoryPosition--;
}
this.channel.pendingMessage = this.$refs.input.value = this.channel.inputHistory[this.channel.inputHistoryPosition];
channel.pendingMessage = channel.inputHistory[channel.inputHistoryPosition];
this.$refs.input.value = channel.pendingMessage;
this.setInputSize();
return false;
@ -173,7 +166,8 @@ export default {
// Use scrollHeight to calculate how many lines there are in input, and ceil the value
// because some browsers tend to incorrently round the values when using high density
// displays or using page zoom feature
this.$refs.input.style.height = Math.ceil(this.$refs.input.scrollHeight / lineHeight) * lineHeight + "px";
this.$refs.input.style.height =
Math.ceil(this.$refs.input.scrollHeight / lineHeight) * lineHeight + "px";
});
},
getInputPlaceholder(channel) {
@ -219,7 +213,10 @@ export default {
const args = text.substr(1).split(" ");
const cmd = args.shift().toLowerCase();
if (Object.prototype.hasOwnProperty.call(commands, cmd) && commands[cmd].input(args)) {
if (
Object.prototype.hasOwnProperty.call(commands, cmd) &&
commands[cmd].input(args)
) {
return false;
}
}

View file

@ -1,14 +1,12 @@
<template>
<aside
ref="userlist"
class="userlist"
@mouseleave="removeHoverUser"
>
<aside ref="userlist" class="userlist" @mouseleave="removeHoverUser">
<div class="count">
<input
ref="input"
:value="userSearchInput"
:placeholder="channel.users.length + ' user' + (channel.users.length === 1 ? '' : 's')"
:placeholder="
channel.users.length + ' user' + (channel.users.length === 1 ? '' : 's')
"
type="search"
class="search"
aria-label="Search among the user list"
@ -19,7 +17,7 @@
@keydown.page-up="navigateUserList($event, -10)"
@keydown.page-down="navigateUserList($event, 10)"
@keydown.enter="selectUser"
>
/>
</div>
<div class="names">
<div
@ -84,15 +82,11 @@ export default {
// filteredUsers is computed, to avoid unnecessary filtering
// as it is shared between filtering and keybindings.
filteredUsers() {
return fuzzy.filter(
this.userSearchInput,
this.channel.users,
{
pre: "<b>",
post: "</b>",
extract: (u) => u.nick,
}
);
return fuzzy.filter(this.userSearchInput, this.channel.users, {
pre: "<b>",
post: "</b>",
extract: (u) => u.nick,
});
},
groupedUsers() {
const groups = {};

View file

@ -1,13 +1,7 @@
<template>
<div
:aria-label="localeDate"
class="date-marker-container tooltipped tooltipped-s"
>
<div :aria-label="localeDate" class="date-marker-container tooltipped tooltipped-s">
<div class="date-marker">
<span
:data-label="friendlyDate()"
class="date-marker-text"
/>
<span :data-label="friendlyDate()" class="date-marker-text" />
</div>
</div>
</template>

View file

@ -19,7 +19,7 @@
maxlength="200"
title="The channel name may not contain spaces"
required
>
/>
<input
v-model="inputPassword"
type="password"
@ -30,11 +30,8 @@
maxlength="200"
title="The channel password may not contain spaces"
autocomplete="new-password"
>
<button
type="submit"
class="btn btn-small"
>Join</button>
/>
<button type="submit" class="btn btn-small">Join</button>
</form>
</template>
@ -63,7 +60,9 @@ export default {
methods: {
onSubmit() {
const channelToFind = this.inputChannel.toLowerCase();
const existingChannel = this.network.channels.find((c) => c.name.toLowerCase() === channelToFind);
const existingChannel = this.network.channels.find(
(c) => c.name.toLowerCase() === channelToFind
);
if (existingChannel) {
const $ = require("jquery");

View file

@ -1,13 +1,8 @@
<template>
<div
v-if="link.shown"
v-show="link.canDisplay"
ref="container"
class="preview"
>
<div v-if="link.shown" v-show="link.canDisplay" ref="container" class="preview">
<div
ref="content"
:class="['toggle-content', 'toggle-type-' + link.type, { opened: isContentShown }]"
:class="['toggle-content', 'toggle-type-' + link.type, {opened: isContentShown}]"
>
<template v-if="link.type === 'link'">
<a
@ -25,7 +20,7 @@
@error="onThumbnailError"
@abort="onThumbnailError"
@load="onPreviewReady"
>
/>
</a>
<div class="toggle-text">
<div class="head">
@ -35,7 +30,8 @@
:title="link.head"
target="_blank"
rel="noopener"
>{{ link.head }}</a>
>{{ link.head }}</a
>
</div>
<button
@ -44,81 +40,48 @@
:aria-label="moreButtonLabel"
class="more"
@click="onMoreClick"
><span class="more-caret" /></button>
>
<span class="more-caret" />
</button>
</div>
<div class="body overflowable">
<a
:href="link.link"
:title="link.body"
target="_blank"
rel="noopener"
>{{ link.body }}</a>
<a :href="link.link" :title="link.body" target="_blank" rel="noopener">{{
link.body
}}</a>
</div>
</div>
</template>
<template v-else-if="link.type === 'image'">
<a
:href="link.link"
class="toggle-thumbnail"
target="_blank"
rel="noopener"
>
<img
:src="link.thumb"
decoding="async"
alt=""
@load="onPreviewReady"
>
<a :href="link.link" class="toggle-thumbnail" target="_blank" rel="noopener">
<img :src="link.thumb" decoding="async" alt="" @load="onPreviewReady" />
</a>
</template>
<template v-else-if="link.type === 'video'">
<video
preload="metadata"
controls
@canplay="onPreviewReady"
>
<source
:src="link.media"
:type="link.mediaType"
>
<video preload="metadata" controls @canplay="onPreviewReady">
<source :src="link.media" :type="link.mediaType" />
</video>
</template>
<template v-else-if="link.type === 'audio'">
<audio
controls
preload="metadata"
@canplay="onPreviewReady"
>
<source
:src="link.media"
:type="link.mediaType"
>
<audio controls preload="metadata" @canplay="onPreviewReady">
<source :src="link.media" :type="link.mediaType" />
</audio>
</template>
<template v-else-if="link.type === 'error'">
<em v-if="link.error === 'image-too-big'">
This image is larger than {{ link.maxSize | friendlysize }} and cannot be
previewed.
<a
:href="link.link"
target="_blank"
rel="noopener"
>Click here</a>
<a :href="link.link" target="_blank" rel="noopener">Click here</a>
to open it in a new window.
</em>
<template v-else-if="link.error === 'message'">
<div>
<em>
A preview could not be loaded.
<a
:href="link.link"
target="_blank"
rel="noopener"
>Click here</a>
<a :href="link.link" target="_blank" rel="noopener">Click here</a>
to open it in a new window.
</em>
<br>
<br />
<pre class="prefetch-error">{{ link.message }}</pre>
</div>
@ -127,7 +90,9 @@
:aria-label="moreButtonLabel"
class="more"
@click="onMoreClick"
><span class="more-caret" /></button>
>
<span class="more-caret" />
</button>
</template>
</template>
</div>
@ -217,27 +182,31 @@ export default {
return;
}
this.showMoreButton = this.$refs.content.offsetWidth >= this.$refs.container.offsetWidth;
this.showMoreButton =
this.$refs.content.offsetWidth >= this.$refs.container.offsetWidth;
});
},
updateShownState() {
let defaultState = true;
switch (this.link.type) {
case "error":
defaultState = this.link.error === "image-too-big" ? this.$root.settings.media : this.$root.settings.links;
break;
case "error":
defaultState =
this.link.error === "image-too-big"
? this.$root.settings.media
: this.$root.settings.links;
break;
case "loading":
defaultState = false;
break;
case "loading":
defaultState = false;
break;
case "link":
defaultState = this.$root.settings.links;
break;
case "link":
defaultState = this.$root.settings.links;
break;
default:
defaultState = this.$root.settings.media;
default:
defaultState = this.$root.settings.media;
}
this.link.shown = this.link.shown && defaultState;

View file

@ -1,7 +1,7 @@
<template>
<button
v-if="link.type !== 'loading'"
:class="['toggle-button', 'toggle-preview', { opened: link.shown }]"
:class="['toggle-button', 'toggle-preview', {opened: link.shown}]"
:aria-label="ariaLabel"
@click="onClick"
/>

View file

@ -4,26 +4,18 @@
:class="['msg', message.type, {self: message.self, highlight: message.highlight}]"
:data-from="message.from && message.from.nick"
>
<span
:aria-label="message.time | localetime"
class="time tooltipped tooltipped-e"
>{{ messageTime }} </span>
<span :aria-label="message.time | localetime" class="time tooltipped tooltipped-e"
>{{ messageTime }}
</span>
<template v-if="message.type === 'unhandled'">
<span class="from">[{{ message.command }}]</span>
<span class="content">
<span
v-for="(param, id) in message.params"
:key="id"
>{{ param }} </span>
<span v-for="(param, id) in message.params" :key="id">{{ param }} </span>
</span>
</template>
<template v-else-if="isAction()">
<span class="from"><span class="only-copy">*** </span></span>
<Component
:is="messageComponent"
:network="network"
:message="message"
/>
<Component :is="messageComponent" :network="network" :message="message" />
</template>
<template v-else-if="message.type === 'action'">
<span class="from"><span class="only-copy">* </span></span>
@ -41,20 +33,14 @@
</span>
</template>
<template v-else>
<span
v-if="message.type === 'message'"
class="from"
>
<span v-if="message.type === 'message'" class="from">
<template v-if="message.from && message.from.nick">
<span class="only-copy">&lt;</span>
<Username :user="message.from" />
<span class="only-copy">&gt; </span>
</template>
</span>
<span
v-else
class="from"
>
<span v-else class="from">
<template v-if="message.from && message.from.nick">
<span class="only-copy">-</span>
<Username :user="message.from" />
@ -62,10 +48,7 @@
</template>
</span>
<span class="content">
<ParsedMessage
:network="network"
:message="message"
/>
<ParsedMessage :network="network" :message="message" />
<LinkPreview
v-for="preview in message.previews"
:key="preview.link"
@ -100,7 +83,9 @@ export default {
},
computed: {
messageTime() {
const format = this.$root.settings.showSeconds ? constants.timeFormats.msgWithSeconds : constants.timeFormats.msgDefault;
const format = this.$root.settings.showSeconds
? constants.timeFormats.msgWithSeconds
: constants.timeFormats.msgDefault;
return moment(this.message.time).format(format);
},

View file

@ -1,14 +1,11 @@
<template>
<div :class="[ 'msg', 'condensed', { closed: isCollapsed } ]">
<div :class="['msg', 'condensed', {closed: isCollapsed}]">
<div class="condensed-summary">
<span class="time" />
<span class="from" />
<span
class="content"
@click="onCollapseClick"
>{{ condensedText }}<button
class="toggle-button"
aria-label="Toggle status messages"
<span class="content" @click="onCollapseClick"
>{{ condensedText
}}<button class="toggle-button" aria-label="Toggle status messages"
/></span>
</div>
<Message
@ -58,30 +55,60 @@ export default {
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 "chghost":
strings.push(obj[type] + (obj[type] > 1 ? " users have changed hostname" : " user has changed hostname"));
break;
case "join":
strings.push(obj[type] + (obj[type] > 1 ? " users have joined" : " user has joined"));
break;
case "part":
strings.push(obj[type] + (obj[type] > 1 ? " users have left" : " user has left"));
break;
case "nick":
strings.push(obj[type] + (obj[type] > 1 ? " users have changed nick" : " user has changed nick"));
break;
case "kick":
strings.push(obj[type] + (obj[type] > 1 ? " users were kicked" : " user was kicked"));
break;
case "mode":
strings.push(obj[type] + (obj[type] > 1 ? " modes were set" : " mode was set"));
break;
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 "chghost":
strings.push(
obj[type] +
(obj[type] > 1
? " users have changed hostname"
: " user has changed hostname")
);
break;
case "join":
strings.push(
obj[type] +
(obj[type] > 1 ? " users have joined" : " user has joined")
);
break;
case "part":
strings.push(
obj[type] + (obj[type] > 1 ? " users have left" : " user has left")
);
break;
case "nick":
strings.push(
obj[type] +
(obj[type] > 1
? " users have changed nick"
: " user has changed nick")
);
break;
case "kick":
strings.push(
obj[type] +
(obj[type] > 1 ? " users were kicked" : " user was kicked")
);
break;
case "mode":
strings.push(
obj[type] + (obj[type] > 1 ? " modes were set" : " mode was set")
);
break;
}
}
});

View file

@ -1,10 +1,6 @@
<template>
<div
ref="chat"
class="chat"
tabindex="-1"
>
<div :class="['show-more', { show: channel.moreHistoryAvailable }]">
<div ref="chat" class="chat" tabindex="-1">
<div :class="['show-more', {show: channel.moreHistoryAvailable}]">
<button
ref="loadMoreButton"
:disabled="channel.historyLoading || !$root.isConnected"
@ -85,7 +81,9 @@ export default {
// If actions are hidden, just return a message list with them excluded
if (this.$root.settings.statusMessages === "hidden") {
return this.channel.messages.filter((message) => !constants.condensedTypes.includes(message.type));
return this.channel.messages.filter(
(message) => !constants.condensedTypes.includes(message.type)
);
}
// If actions are not condensed, just return raw message list
@ -99,7 +97,11 @@ export default {
for (const message of this.channel.messages) {
// If this message is not condensable, or its an action affecting our user,
// then just append the message to container and be done with it
if (message.self || message.highlight || !constants.condensedTypes.includes(message.type)) {
if (
message.self ||
message.highlight ||
!constants.condensedTypes.includes(message.type)
) {
lastCondensedContainer = null;
condensed.push(message);
@ -199,7 +201,7 @@ export default {
return true;
}
return (new Date(previousMessage.time)).getDay() !== (new Date(message.time)).getDay();
return new Date(previousMessage.time).getDay() !== new Date(message.time).getDay();
},
shouldDisplayUnreadMarker(id) {
if (!this.unreadMarkerShown && id > this.channel.firstUnread) {

View file

@ -1,17 +1,10 @@
<template>
<span class="content">
<ParsedMessage
v-if="message.self"
:network="network"
:message="message"
/>
<ParsedMessage v-if="message.self" :network="network" :message="message" />
<template v-else>
<Username :user="message.from" />
is away
<i class="away-message">(<ParsedMessage
:network="network"
:message="message"
/>)</i>
<i class="away-message">(<ParsedMessage :network="network" :message="message" />)</i>
</template>
</span>
</template>

View file

@ -1,10 +1,6 @@
<template>
<span class="content">
<ParsedMessage
v-if="message.self"
:network="network"
:message="message"
/>
<ParsedMessage v-if="message.self" :network="network" :message="message" />
<template v-else>
<Username :user="message.from" />
is back

View file

@ -2,8 +2,12 @@
<span class="content">
<Username :user="message.from" />
has changed
<span v-if="message.new_ident">username to <b>{{ message.new_ident }}</b></span>
<span v-if="message.new_host">hostname to <i class="hostmask">{{ message.new_host }}</i></span>
<span v-if="message.new_ident"
>username to <b>{{ message.new_ident }}</b></span
>
<span v-if="message.new_host"
>hostname to <i class="hostmask">{{ message.new_host }}</i></span
>
</span>
</template>

View file

@ -1,7 +1,7 @@
<template>
<span class="content">
<Username :user="message.from" />&#32;
<span class="ctcp-message"><ParsedMessage :text="message.ctcpMessage" /></span>
<span class="ctcp-message"><ParsedMessage :text="message.ctcpMessage"/></span>
</span>
</template>

View file

@ -2,7 +2,7 @@
<span class="content">
<Username :user="message.from" />
sent a <abbr title="Client-to-client protocol">CTCP</abbr> request:
<span class="ctcp-message"><ParsedMessage :text="message.ctcpMessage" /></span>
<span class="ctcp-message"><ParsedMessage :text="message.ctcpMessage"/></span>
</span>
</template>

View file

@ -3,14 +3,8 @@
<Username :user="message.from" />
invited
<span v-if="message.invitedYou">you</span>
<Username
v-else
:user="message.target"
/>
to <ParsedMessage
:network="network"
:text="message.channel"
/>
<Username v-else :user="message.target" />
to <ParsedMessage :network="network" :text="message.channel" />
</span>
</template>

View file

@ -3,13 +3,9 @@
<Username :user="message.from" />
has kicked
<Username :user="message.target" />
<i
v-if="message.text"
class="part-reason"
> (<ParsedMessage
:network="network"
:message="message"
/>)</i>
<i v-if="message.text" class="part-reason">
(<ParsedMessage :network="network" :message="message" />)</i
>
</span>
</template>

View file

@ -1,9 +1,6 @@
<template>
<span class="content">
<span class="text"><ParsedMessage
:network="network"
:text="cleanText"
/></span>
<span class="text"><ParsedMessage :network="network" :text="cleanText"/></span>
</span>
</template>
@ -31,7 +28,7 @@ export default {
// Remove empty lines around the MOTD (but not within it)
return lines
.map((line) => line.replace(/\s*$/,""))
.map((line) => line.replace(/\s*$/, ""))
.join("\n")
.replace(/^[\r\n]+|[\r\n]+$/g, "");
},

View file

@ -1,13 +1,10 @@
<template>
<span class="content">
<Username :user="message.from" />
<i class="hostmask"> ({{ message.hostmask }})</i> has left the channel <i
v-if="message.text"
class="part-reason"
>(<ParsedMessage
:network="network"
:message="message"
/>)</i>
<i class="hostmask"> ({{ message.hostmask }})</i> has left the channel
<i v-if="message.text" class="part-reason"
>(<ParsedMessage :network="network" :message="message" />)</i
>
</span>
</template>

View file

@ -1,13 +1,10 @@
<template>
<span class="content">
<Username :user="message.from" />
<i class="hostmask"> ({{ message.hostmask }})</i> has quit <i
v-if="message.text"
class="quit-reason"
>(<ParsedMessage
:network="network"
:message="message"
/>)</i>
<i class="hostmask"> ({{ message.hostmask }})</i> has quit
<i v-if="message.text" class="quit-reason"
>(<ParsedMessage :network="network" :message="message" />)</i
>
</span>
</template>

View file

@ -1,13 +1,13 @@
<template>
<span class="content">
<template v-if="message.from && message.from.nick"><Username :user="message.from" /> has changed the topic to: </template>
<template v-else>The topic is: </template>
<span
v-if="message.text"
class="new-topic"
><ParsedMessage
:network="network"
:message="message"
<template v-if="message.from && message.from.nick"
><Username :user="message.from" /> has changed the topic to:
</template>
<template v-else
>The topic is:
</template>
<span v-if="message.text" class="new-topic"
><ParsedMessage :network="network" :message="message"
/></span>
</span>
</template>

View file

@ -21,17 +21,17 @@
:href="'https://ipinfo.io/' + message.whois.actual_ip"
target="_blank"
rel="noopener"
>{{ message.whois.actual_ip }}</a>
<i v-if="message.whois.actual_hostname != message.whois.actual_ip"> ({{ message.whois.actual_hostname }})</i>
>{{ message.whois.actual_ip }}</a
>
<i v-if="message.whois.actual_hostname != message.whois.actual_ip">
({{ message.whois.actual_hostname }})</i
>
</dd>
</template>
<template v-if="message.whois.real_name">
<dt>Real name:</dt>
<dd><ParsedMessage
:network="network"
:text="message.whois.real_name"
/></dd>
<dd><ParsedMessage :network="network" :text="message.whois.real_name" /></dd>
</template>
<template v-if="message.whois.registered_nick">
@ -41,10 +41,7 @@
<template v-if="message.whois.channels">
<dt>Channels:</dt>
<dd><ParsedMessage
:network="network"
:text="message.whois.channels"
/></dd>
<dd><ParsedMessage :network="network" :text="message.whois.channels" /></dd>
</template>
<template v-if="message.whois.modes">
@ -76,10 +73,7 @@
<template v-if="message.whois.away">
<dt>Away:</dt>
<dd><ParsedMessage
:network="network"
:text="message.whois.away"
/></dd>
<dd><ParsedMessage :network="network" :text="message.whois.away" /></dd>
</template>
<template v-if="message.whois.secure">
@ -89,7 +83,9 @@
<template v-if="message.whois.server">
<dt>Connected to:</dt>
<dd>{{ message.whois.server }} <i>({{ message.whois.server_info }})</i></dd>
<dd>
{{ message.whois.server }} <i>({{ message.whois.server_info }})</i>
</dd>
</template>
<template v-if="message.whois.logonTime">

View file

@ -1,8 +1,5 @@
<template>
<div
v-if="networks.length === 0"
class="empty"
>
<div v-if="networks.length === 0" class="empty">
You are not connected to any networks yet.
</div>
<Draggable

View file

@ -1,9 +1,5 @@
<template>
<ChannelWrapper
:network="network"
:channel="channel"
:active-channel="activeChannel"
>
<ChannelWrapper :network="network" :channel="channel" :active-channel="activeChannel">
<button
v-if="network.channels.length > 1"
:aria-controls="'network-' + network.uuid"
@ -11,16 +7,12 @@
:aria-expanded="!network.isCollapsed"
class="collapse-network"
@click.stop="onCollapseClick"
><span class="collapse-network-icon" /></button>
<span
v-else
class="collapse-network"
/>
>
<span class="collapse-network-icon" />
</button>
<span v-else class="collapse-network" />
<div class="lobby-wrap">
<span
:title="channel.name"
class="name"
>{{ channel.name }}</span>
<span :title="channel.name" class="name">{{ channel.name }}</span>
<span
v-if="network.status.connected && !network.status.secure"
class="not-secure-tooltip tooltipped tooltipped-w"
@ -35,18 +27,16 @@
>
<span class="not-connected-icon" />
</span>
<span
v-if="channel.unread"
:class="{ highlight: channel.highlight }"
class="badge"
>{{ channel.unread | roundBadgeNumber }}</span>
<span v-if="channel.unread" :class="{highlight: channel.highlight}" class="badge">{{
channel.unread | roundBadgeNumber
}}</span>
</div>
<span
:aria-label="joinChannelLabel"
class="add-channel-tooltip tooltipped tooltipped-w tooltipped-no-touch"
>
<button
:class="['add-channel', { opened: isJoinChannelShown }]"
:class="['add-channel', {opened: isJoinChannelShown}]"
:aria-controls="'join-channel-' + channel.id"
:aria-label="joinChannelLabel"
@click.stop="$emit('toggleJoinChannel')"

View file

@ -12,7 +12,9 @@ export default {
render(createElement, context) {
return parse(
createElement,
typeof context.props.text !== "undefined" ? context.props.text : context.props.message.text,
typeof context.props.text !== "undefined"
? context.props.text
: context.props.message.text,
context.props.message,
context.props.network
);

View file

@ -8,10 +8,7 @@
</tr>
</thead>
<tbody>
<tr
v-for="ban in channel.data"
:key="ban.hostmask"
>
<tr v-for="ban in channel.data" :key="ban.hostmask">
<td class="hostmask">{{ ban.hostmask }}</td>
<td class="banned_by">{{ ban.banned_by }}</td>
<td class="banned_at">{{ ban.banned_at | localetime }}</td>

View file

@ -1,9 +1,6 @@
<template>
<span v-if="channel.data.text">{{ channel.data.text }}</span>
<table
v-else
class="channel-list"
>
<table v-else class="channel-list">
<thead>
<tr>
<th class="channel">Channel</th>
@ -12,19 +9,10 @@
</tr>
</thead>
<tbody>
<tr
v-for="chan in channel.data"
:key="chan.channel"
>
<td class="channel"><ParsedMessage
:network="network"
:text="chan.channel"
/></td>
<tr v-for="chan in channel.data" :key="chan.channel">
<td class="channel"><ParsedMessage :network="network" :text="chan.channel" /></td>
<td class="users">{{ chan.num_users }}</td>
<td class="topic"><ParsedMessage
:network="network"
:text="chan.topic"
/></td>
<td class="topic"><ParsedMessage :network="network" :text="chan.topic" /></td>
</tr>
</tbody>
</table>

View file

@ -7,10 +7,7 @@
</tr>
</thead>
<tbody>
<tr
v-for="user in channel.data"
:key="user.hostmask"
>
<tr v-for="user in channel.data" :key="user.hostmask">
<td class="hostmask">{{ user.hostmask }}</td>
<td class="when">{{ user.when | localetime }}</td>
</tr>

View file

@ -8,10 +8,7 @@
</tr>
</thead>
<tbody>
<tr
v-for="invite in channel.data"
:key="invite.hostmask"
>
<tr v-for="invite in channel.data" :key="invite.hostmask">
<td class="hostmask">{{ invite.hostmask }}</td>
<td class="invitened_by">{{ invite.invited_by }}</td>
<td class="invitened_at">{{ invite.invited_at | localetime }}</td>

View file

@ -1,10 +1,11 @@
<template>
<span
:class="['user', $options.filters.colorClass(user.nick), { active: active }]"
:class="['user', $options.filters.colorClass(user.nick), {active: active}]"
:data-name="user.nick"
role="button"
v-on="onHover ? { mouseover: hover } : {}"
>{{ user.mode }}{{ user.nick }}</span>
v-on="onHover ? {mouseover: hover} : {}"
>{{ user.mode }}{{ user.nick }}</span
>
</template>
<script>

View file

@ -1,6 +1,6 @@
<template>
<span
:class="['user', $options.filters.colorClass(user.original.nick), { active: active }]"
:class="['user', $options.filters.colorClass(user.original.nick), {active: active}]"
:data-name="user.original.nick"
role="button"
@mouseover="hover"

View file

@ -33,8 +33,7 @@ const emojiStrategy = {
search(term, callback) {
// Trim colon from the matched term,
// as we are unable to get a clean string from match regex
term = term.replace(/:$/, ""),
callback(fuzzyGrep(term, emojiSearchTerms));
(term = term.replace(/:$/, "")), callback(fuzzyGrep(term, emojiSearchTerms));
},
template([string, original]) {
return `<span class="emoji">${emojiMap[original]}</span> ${string}`;
@ -52,8 +51,7 @@ const nicksStrategy = {
term = term.slice(1);
if (term[0] === "@") {
callback(completeNicks(term.slice(1), true)
.map((val) => ["@" + val[0], "@" + val[1]]));
callback(completeNicks(term.slice(1), true).map((val) => ["@" + val[0], "@" + val[1]]));
} else {
callback(completeNicks(term, true));
}
@ -118,10 +116,13 @@ const foregroundColorStrategy = {
.filter((i) => fuzzy.test(term, i[0]) || fuzzy.test(term, i[1]))
.map((i) => {
if (fuzzy.test(term, i[1])) {
return [i[0], fuzzy.match(term, i[1], {
pre: "<b>",
post: "</b>",
}).rendered];
return [
i[0],
fuzzy.match(term, i[1], {
pre: "<b>",
post: "</b>",
}).rendered,
];
}
return i;
@ -147,10 +148,13 @@ const backgroundColorStrategy = {
.filter((i) => fuzzy.test(term, i[0]) || fuzzy.test(term, i[1]))
.map((pair) => {
if (fuzzy.test(term, pair[1])) {
return [pair[0], fuzzy.match(term, pair[1], {
pre: "<b>",
post: "</b>",
}).rendered];
return [
pair[0],
fuzzy.match(term, pair[1], {
pre: "<b>",
post: "</b>",
}).rendered,
];
}
return pair;
@ -160,7 +164,10 @@ const backgroundColorStrategy = {
callback(matchingColorCodes);
},
template(value) {
return `<span class="irc-fg${parseInt(value[2], 10)} irc-bg irc-bg${parseInt(value[0], 10)}">${value[1]}</span>`;
return `<span class="irc-fg${parseInt(value[2], 10)} irc-bg irc-bg${parseInt(
value[0],
10
)}">${value[1]}</span>`;
},
replace(value) {
return "\x03$1," + value[0];
@ -185,46 +192,55 @@ function enableAutocomplete(inputRef) {
lastMatch = "";
});
Mousetrap(input.get(0)).bind("tab", (e) => {
if (vueApp.isAutoCompleting) {
return;
}
e.preventDefault();
const text = input.val();
if (input.get(0).selectionStart !== text.length) {
return;
}
if (tabCount === 0) {
lastMatch = text.split(/\s/).pop();
if (lastMatch.length === 0) {
Mousetrap(input.get(0)).bind(
"tab",
(e) => {
if (vueApp.isAutoCompleting) {
return;
}
currentMatches = completeNicks(lastMatch, false);
e.preventDefault();
if (currentMatches.length === 0) {
const text = input.val();
if (input.get(0).selectionStart !== text.length) {
return;
}
}
const position = input.get(0).selectionStart - lastMatch.length;
const newMatch = nicksStrategy.replace([0, currentMatches[tabCount % currentMatches.length]], position);
if (tabCount === 0) {
lastMatch = text.split(/\s/).pop();
input.val(text.substr(0, position) + newMatch);
if (lastMatch.length === 0) {
return;
}
// Propagate change to Vue model
input.get(0).dispatchEvent(new CustomEvent("input", {
detail: "autocomplete",
}));
currentMatches = completeNicks(lastMatch, false);
lastMatch = newMatch;
tabCount++;
}, "keydown");
if (currentMatches.length === 0) {
return;
}
}
const position = input.get(0).selectionStart - lastMatch.length;
const newMatch = nicksStrategy.replace(
[0, currentMatches[tabCount % currentMatches.length]],
position
);
input.val(text.substr(0, position) + newMatch);
// Propagate change to Vue model
input.get(0).dispatchEvent(
new CustomEvent("input", {
detail: "autocomplete",
})
);
lastMatch = newMatch;
tabCount++;
},
"keydown"
);
const editor = new Textarea(input.get(0));
textcomplete = new Textcomplete(editor, {
@ -265,14 +281,10 @@ function enableAutocomplete(inputRef) {
}
function fuzzyGrep(term, array) {
const results = fuzzy.filter(
term,
array,
{
pre: "<b>",
post: "</b>",
}
);
const results = fuzzy.filter(term, array, {
pre: "<b>",
post: "</b>",
});
return results.map((el) => [el.string, el.original]);
}
@ -303,10 +315,7 @@ function completeNicks(word, isFuzzy) {
return fuzzyGrep(word, users);
}
return $.grep(
users,
(w) => !w.toLowerCase().indexOf(word)
);
return $.grep(users, (w) => !w.toLowerCase().indexOf(word));
}
function completeCommands(word) {

View file

@ -19,17 +19,7 @@ const colorCodeMap = [
["15", "Light Grey"],
];
const condensedTypes = [
"away",
"back",
"chghost",
"join",
"part",
"quit",
"nick",
"kick",
"mode",
];
const condensedTypes = ["away", "back", "chghost", "join", "part", "quit", "nick", "kick", "mode"];
const condensedTypesQuery = "." + condensedTypes.join(", .");
const timeFormats = {

View file

@ -16,7 +16,11 @@ module.exports = class ContextMenu {
}
show() {
const contextMenu = showContextMenu(this.contextMenuItems, this.selectedElement, this.event);
const contextMenu = showContextMenu(
this.contextMenuItems,
this.selectedElement,
this.event
);
this.bindEvents(contextMenu);
return false;
}
@ -33,7 +37,8 @@ module.exports = class ContextMenu {
bindEvents(contextMenu) {
const contextMenuActions = this.contextMenuActions;
contextMenuActions.execute = (id, ...args) => contextMenuActions[id] && contextMenuActions[id](...args);
contextMenuActions.execute = (id, ...args) =>
contextMenuActions[id] && contextMenuActions[id](...args);
const clickItem = (item) => {
const itemData = item.attr("data-data");
@ -109,19 +114,25 @@ function showContextMenu(contextMenuItems, selectedElement, event) {
if (item.divider) {
contextMenu.append(templates.contextmenu_divider());
} else {
contextMenu.append(templates.contextmenu_item({
class: typeof item.className === "function" ? item.className(target) : item.className,
action: item.actionId,
text: typeof item.displayName === "function" ? item.displayName(target) : item.displayName,
data: typeof item.data === "function" ? item.data(target) : item.data,
}));
contextMenu.append(
templates.contextmenu_item({
class:
typeof item.className === "function"
? item.className(target)
: item.className,
action: item.actionId,
text:
typeof item.displayName === "function"
? item.displayName(target)
: item.displayName,
data: typeof item.data === "function" ? item.data(target) : item.data,
})
);
}
}
}
contextMenuContainer
.html(contextMenu)
.show();
contextMenuContainer.html(contextMenu).show();
contextMenu
.css(positionContextMenu(contextMenu, selectedElement, event))
@ -145,11 +156,11 @@ function positionContextMenu(contextMenu, selectedElement, e) {
offset = {left: e.pageX, top: e.pageY};
if ((window.innerWidth - offset.left) < menuWidth) {
if (window.innerWidth - offset.left < menuWidth) {
offset.left = window.innerWidth - menuWidth;
}
if ((window.innerHeight - offset.top) < menuHeight) {
if (window.innerHeight - offset.top < menuHeight) {
offset.top = window.innerHeight - menuHeight;
}

View file

@ -170,7 +170,9 @@ function addKickItem() {
}
addContextMenuItem({
check: (target) => utils.hasRoleInChannel(target.closest(".chan"), ["op"]) && target.closest(".chan").attr("data-type") === "channel",
check: (target) =>
utils.hasRoleInChannel(target.closest(".chan"), ["op"]) &&
target.closest(".chan").attr("data-type") === "channel",
className: "action-kick",
displayName: "Kick",
data: (target) => target.attr("data-name"),

View file

@ -4,10 +4,7 @@ const $ = require("jquery");
const Mousetrap = require("mousetrap");
const utils = require("./utils");
Mousetrap.bind([
"alt+up",
"alt+down",
], function(e, keys) {
Mousetrap.bind(["alt+up", "alt+down"], function(e, keys) {
const sidebar = $("#sidebar");
const channels = sidebar.find(".chan").not(".network.collapsed :not(.lobby)");
const index = channels.index(channels.filter(".active"));
@ -15,13 +12,13 @@ Mousetrap.bind([
let target;
switch (direction) {
case "up":
target = (channels.length + (index - 1 + channels.length)) % channels.length;
break;
case "up":
target = (channels.length + (index - 1 + channels.length)) % channels.length;
break;
case "down":
target = (channels.length + (index + 1 + channels.length)) % channels.length;
break;
case "down":
target = (channels.length + (index + 1 + channels.length)) % channels.length;
break;
}
target = channels.eq(target).click();
@ -30,10 +27,7 @@ Mousetrap.bind([
return false;
});
Mousetrap.bind([
"alt+shift+up",
"alt+shift+down",
], function(e, keys) {
Mousetrap.bind(["alt+shift+up", "alt+shift+down"], function(e, keys) {
const sidebar = $("#sidebar");
const lobbies = sidebar.find(".lobby");
const direction = keys.split("+").pop();
@ -41,23 +35,33 @@ Mousetrap.bind([
let target;
switch (direction) {
case "up":
if (index < 0) {
target = lobbies.index(sidebar.find(".channel").filter(".active").siblings(".lobby")[0]);
} else {
target = (lobbies.length + (index - 1 + lobbies.length)) % lobbies.length;
}
case "up":
if (index < 0) {
target = lobbies.index(
sidebar
.find(".channel")
.filter(".active")
.siblings(".lobby")[0]
);
} else {
target = (lobbies.length + (index - 1 + lobbies.length)) % lobbies.length;
}
break;
break;
case "down":
if (index < 0) {
index = lobbies.index(sidebar.find(".channel").filter(".active").siblings(".lobby")[0]);
}
case "down":
if (index < 0) {
index = lobbies.index(
sidebar
.find(".channel")
.filter(".active")
.siblings(".lobby")[0]
);
}
target = (lobbies.length + (index + 1 + lobbies.length)) % lobbies.length;
target = (lobbies.length + (index + 1 + lobbies.length)) % lobbies.length;
break;
break;
}
target = lobbies.eq(target).click();

View file

@ -13,5 +13,5 @@ module.exports = function(str) {
due to A being ascii 65 (100 0001)
while a being ascii 97 (110 0001)
*/
return "color-" + (1 + hash % 32);
return "color-" + (1 + (hash % 32));
};

View file

@ -3,10 +3,12 @@
// Return true if any section of "a" or "b" parts (defined by their start/end
// markers) intersect each other, false otherwise.
function anyIntersection(a, b) {
return a.start <= b.start && b.start < a.end ||
a.start < b.end && b.end <= a.end ||
b.start <= a.start && a.start < b.end ||
b.start < a.end && a.end <= b.end;
return (
(a.start <= b.start && b.start < a.end) ||
(a.start < b.end && b.end <= a.end) ||
(b.start <= a.start && a.start < b.end) ||
(b.start < a.end && a.end <= b.end)
);
}
module.exports = anyIntersection;

View file

@ -25,11 +25,17 @@ const linkify = LinkifyIt()
// Known schemes to detect in text
const commonSchemes = [
"sftp",
"smb", "file",
"irc", "ircs",
"svn", "git",
"steam", "mumble", "ts3server",
"svn+ssh", "ssh",
"smb",
"file",
"irc",
"ircs",
"svn",
"git",
"steam",
"mumble",
"ts3server",
"svn+ssh",
"ssh",
];
for (const schema of commonSchemes) {

View file

@ -26,24 +26,20 @@ function sortParts(a, b) {
// fragments will contain duplicate styling attributes.
function merge(textParts, styleFragments, cleanText) {
// Remove overlapping parts
textParts = textParts
.sort(sortParts)
.reduce((prev, curr) => {
const intersection = prev.some((p) => anyIntersection(p, curr));
textParts = textParts.sort(sortParts).reduce((prev, curr) => {
const intersection = prev.some((p) => anyIntersection(p, curr));
if (intersection) {
return prev;
}
if (intersection) {
return prev;
}
return prev.concat([curr]);
}, []);
return prev.concat([curr]);
}, []);
// Every section of the original text that has not been captured in a "part"
// is filled with "text" parts, dummy objects with start/end but no extra
// metadata.
const allParts = textParts
.concat(fill(textParts, cleanText))
.sort(sortParts); // Sort all parts identified based on their position in the original text
const allParts = textParts.concat(fill(textParts, cleanText)).sort(sortParts); // Sort all parts identified based on their position in the original text
// Distribute the style fragments within the text parts
return allParts.map((textPart) => {

View file

@ -33,7 +33,16 @@ function parseStyle(text) {
// At any given time, these carry style information since last time a styling
// control code was met.
let colorCodes, bold, textColor, bgColor, hexColor, hexBgColor, italic, underline, strikethrough, monospace;
let colorCodes,
bold,
textColor,
bgColor,
hexColor,
hexBgColor,
italic,
underline,
strikethrough,
monospace;
const resetStyle = () => {
bold = false;
@ -90,96 +99,96 @@ function parseStyle(text) {
// encountered since the previous styling character.
while (position < text.length) {
switch (text[position]) {
case RESET:
emitFragment();
resetStyle();
break;
case RESET:
emitFragment();
resetStyle();
break;
// Meeting a BOLD character means that the ongoing text is either going to
// be in bold or that the previous one was in bold and the following one
// must be reset.
// This same behavior applies to COLOR, REVERSE, ITALIC, and UNDERLINE.
case BOLD:
emitFragment();
bold = !bold;
break;
// Meeting a BOLD character means that the ongoing text is either going to
// be in bold or that the previous one was in bold and the following one
// must be reset.
// This same behavior applies to COLOR, REVERSE, ITALIC, and UNDERLINE.
case BOLD:
emitFragment();
bold = !bold;
break;
case COLOR:
emitFragment();
case COLOR:
emitFragment();
// Go one step further to find the corresponding color
colorCodes = text.slice(position + 1).match(colorRx);
// Go one step further to find the corresponding color
colorCodes = text.slice(position + 1).match(colorRx);
if (colorCodes) {
textColor = Number(colorCodes[1]);
if (colorCodes) {
textColor = Number(colorCodes[1]);
if (colorCodes[2]) {
bgColor = Number(colorCodes[2]);
if (colorCodes[2]) {
bgColor = Number(colorCodes[2]);
}
// Color code length is > 1, so bump the current position cursor by as
// much (and reset the start cursor for the current text block as well)
position += colorCodes[0].length;
start = position + 1;
} else {
// If no color codes were found, toggles back to no colors (like BOLD).
textColor = undefined;
bgColor = undefined;
}
// Color code length is > 1, so bump the current position cursor by as
// much (and reset the start cursor for the current text block as well)
position += colorCodes[0].length;
start = position + 1;
} else {
// If no color codes were found, toggles back to no colors (like BOLD).
textColor = undefined;
bgColor = undefined;
}
break;
break;
case HEX_COLOR:
emitFragment();
case HEX_COLOR:
emitFragment();
colorCodes = text.slice(position + 1).match(hexColorRx);
colorCodes = text.slice(position + 1).match(hexColorRx);
if (colorCodes) {
hexColor = colorCodes[1].toUpperCase();
if (colorCodes) {
hexColor = colorCodes[1].toUpperCase();
if (colorCodes[2]) {
hexBgColor = colorCodes[2].toUpperCase();
}
if (colorCodes[2]) {
hexBgColor = colorCodes[2].toUpperCase();
// Color code length is > 1, so bump the current position cursor by as
// much (and reset the start cursor for the current text block as well)
position += colorCodes[0].length;
start = position + 1;
} else {
// If no color codes were found, toggles back to no colors (like BOLD).
hexColor = undefined;
hexBgColor = undefined;
}
// Color code length is > 1, so bump the current position cursor by as
// much (and reset the start cursor for the current text block as well)
position += colorCodes[0].length;
start = position + 1;
} else {
// If no color codes were found, toggles back to no colors (like BOLD).
hexColor = undefined;
hexBgColor = undefined;
break;
case REVERSE: {
emitFragment();
const tmp = bgColor;
bgColor = textColor;
textColor = tmp;
break;
}
break;
case ITALIC:
emitFragment();
italic = !italic;
break;
case REVERSE: {
emitFragment();
const tmp = bgColor;
bgColor = textColor;
textColor = tmp;
break;
}
case UNDERLINE:
emitFragment();
underline = !underline;
break;
case ITALIC:
emitFragment();
italic = !italic;
break;
case STRIKETHROUGH:
emitFragment();
strikethrough = !strikethrough;
break;
case UNDERLINE:
emitFragment();
underline = !underline;
break;
case STRIKETHROUGH:
emitFragment();
strikethrough = !strikethrough;
break;
case MONOSPACE:
emitFragment();
monospace = !monospace;
break;
case MONOSPACE:
emitFragment();
monospace = !monospace;
break;
}
// Evaluate the next character at the next iteration
@ -192,25 +201,37 @@ function parseStyle(text) {
return result;
}
const properties = ["bold", "textColor", "bgColor", "hexColor", "hexBgColor", "italic", "underline", "strikethrough", "monospace"];
const properties = [
"bold",
"textColor",
"bgColor",
"hexColor",
"hexBgColor",
"italic",
"underline",
"strikethrough",
"monospace",
];
function prepare(text) {
return parseStyle(text)
// This optimizes fragments by combining them together when all their values
// for the properties defined above are equal.
.reduce((prev, curr) => {
if (prev.length) {
const lastEntry = prev[prev.length - 1];
return (
parseStyle(text)
// This optimizes fragments by combining them together when all their values
// for the properties defined above are equal.
.reduce((prev, curr) => {
if (prev.length) {
const lastEntry = prev[prev.length - 1];
if (properties.every((key) => curr[key] === lastEntry[key])) {
lastEntry.text += curr.text;
lastEntry.end += curr.text.length;
return prev;
if (properties.every((key) => curr[key] === lastEntry[key])) {
lastEntry.text += curr.text;
lastEntry.end += curr.text.length;
return prev;
}
}
}
return prev.concat([curr]);
}, []);
return prev.concat([curr]);
}, [])
);
}
module.exports = prepare;

View file

@ -9,7 +9,7 @@ const merge = require("./ircmessageparser/merge");
const colorClass = require("./colorClass");
const emojiMap = require("../fullnamemap.json");
const LinkPreviewToggle = require("../../../components/LinkPreviewToggle.vue").default;
const emojiModifiersRegex = /[\u{1f3fb}-\u{1f3ff}]/ug;
const emojiModifiersRegex = /[\u{1f3fb}-\u{1f3ff}]/gu;
// Create an HTML `span` with styling information for a given fragment
function createFragment(fragment, createElement) {
@ -80,7 +80,7 @@ module.exports = function parse(createElement, text, message = undefined, networ
const channelParts = findChannels(cleanText, channelPrefixes, userModes);
const linkParts = findLinks(cleanText);
const emojiParts = findEmoji(cleanText);
const nameParts = findNames(cleanText, message ? (message.users || []) : []);
const nameParts = findNames(cleanText, message ? message.users || [] : []);
const parts = channelParts
.concat(linkParts)
@ -90,65 +90,85 @@ module.exports = function parse(createElement, text, message = undefined, networ
// Merge the styling information with the channels / URLs / nicks / text objects and
// generate HTML strings with the resulting fragments
return merge(parts, styleFragments, cleanText).map((textPart) => {
const fragments = textPart.fragments.map((fragment) => createFragment(fragment, createElement));
const fragments = textPart.fragments.map((fragment) =>
createFragment(fragment, createElement)
);
// Wrap these potentially styled fragments with links and channel buttons
if (textPart.link) {
const preview = message && message.previews.find((p) => p.link === textPart.link);
const link = createElement("a", {
attrs: {
href: textPart.link,
target: "_blank",
rel: "noopener",
const link = createElement(
"a",
{
attrs: {
href: textPart.link,
target: "_blank",
rel: "noopener",
},
},
}, fragments);
fragments
);
if (!preview) {
return link;
}
return [link, createElement(LinkPreviewToggle, {
class: ["toggle-button", "toggle-preview"],
props: {
link: preview,
},
}, fragments)];
return [
link,
createElement(
LinkPreviewToggle,
{
class: ["toggle-button", "toggle-preview"],
props: {
link: preview,
},
},
fragments
),
];
} else if (textPart.channel) {
return createElement("span", {
class: [
"inline-channel",
],
attrs: {
"role": "button",
"tabindex": 0,
"data-chan": textPart.channel,
return createElement(
"span",
{
class: ["inline-channel"],
attrs: {
role: "button",
tabindex: 0,
"data-chan": textPart.channel,
},
},
}, fragments);
fragments
);
} else if (textPart.emoji) {
const emojiWithoutModifiers = textPart.emoji.replace(emojiModifiersRegex, "");
const title = emojiMap[emojiWithoutModifiers] ? `Emoji: ${emojiMap[emojiWithoutModifiers]}` : null;
const title = emojiMap[emojiWithoutModifiers]
? `Emoji: ${emojiMap[emojiWithoutModifiers]}`
: null;
return createElement("span", {
class: [
"emoji",
],
attrs: {
"role": "img",
"aria-label": title,
"title": title,
return createElement(
"span",
{
class: ["emoji"],
attrs: {
role: "img",
"aria-label": title,
title: title,
},
},
}, fragments);
fragments
);
} else if (textPart.nick) {
return createElement("span", {
class: [
"user",
colorClass(textPart.nick),
],
attrs: {
"role": "button",
"data-name": textPart.nick,
return createElement(
"span",
{
class: ["user", colorClass(textPart.nick)],
attrs: {
role: "button",
"data-name": textPart.nick,
},
},
}, fragments);
fragments
);
}
return fragments;

View file

@ -42,7 +42,8 @@
window.g_LoungeErrorHandler = function LoungeErrorHandler(e) {
var message = document.getElementById("loading-page-message");
message.textContent = "An error has occurred that prevented the client from loading correctly.";
message.textContent =
"An error has occurred that prevented the client from loading correctly.";
var summary = document.createElement("summary");
summary.textContent = "More details";

View file

@ -69,7 +69,11 @@ window.vueMounted = () => {
});
viewport.on("click", "#chat .menu", function(e) {
e.currentTarget = $(`#sidebar .chan[data-id="${$(this).closest(".chan").attr("data-id")}"]`)[0];
e.currentTarget = $(
`#sidebar .chan[data-id="${$(this)
.closest(".chan")
.attr("data-id")}"]`
)[0];
return contextMenuFactory.createContextMenu($(this), e).show();
});
@ -140,8 +144,7 @@ window.vueMounted = () => {
const lastActive = $("#windows > .active");
lastActive
.removeClass("active");
lastActive.removeClass("active");
const chan = $(target)
.addClass("active")

View file

@ -28,9 +28,7 @@ const noSync = ["syncSettings"];
// alwaysSync is reserved for settings that should be synced
// to the server regardless of the clients sync setting.
const alwaysSync = [
"highlights",
];
const alwaysSync = ["highlights"];
// Process usersettings from localstorage.
let userSettings = JSON.parse(storage.get("settings")) || false;
@ -46,7 +44,10 @@ if (!userSettings) {
}
// Make sure the setting in local storage has the same type that the code expects
if (typeof userSettings[key] !== "undefined" && typeof settings[key] === typeof userSettings[key]) {
if (
typeof userSettings[key] !== "undefined" &&
typeof settings[key] === typeof userSettings[key]
) {
settings[key] = userSettings[key];
}
}
@ -59,7 +60,10 @@ if (typeof userSettings.userStyles === "string" && !noCSSparamReg.test(window.lo
$userStyles.html(userSettings.userStyles);
}
if (typeof userSettings.theme === "string" && $theme.attr("href") !== `themes/${userSettings.theme}.css`) {
if (
typeof userSettings.theme === "string" &&
$theme.attr("href") !== `themes/${userSettings.theme}.css`
) {
$theme.attr("href", `themes/${userSettings.theme}.css`);
}
@ -99,7 +103,7 @@ function applySetting(name, value) {
} else if (name === "userStyles" && !noCSSparamReg.test(window.location.search)) {
$userStyles.html(value);
} else if (name === "desktopNotifications") {
if (("Notification" in window) && value && Notification.permission !== "granted") {
if ("Notification" in window && value && Notification.permission !== "granted") {
Notification.requestPermission(updateDesktopNotificationStatus);
} else if (!value) {
$warningBlocked.hide();
@ -172,8 +176,7 @@ function processSetting(name, value, save) {
} else if (name === "nickPostfix") {
$settings.find(`input[name=${name}]`).val(value);
} else if (name === "statusMessages") {
$settings.find(`input[name=${name}][value=${value}]`)
.prop("checked", true);
$settings.find(`input[name=${name}][value=${value}]`).prop("checked", true);
} else if (name === "theme") {
$settings.find("#theme-select").val(value);
} else if (typeof value === "boolean") {
@ -208,7 +211,7 @@ function initialize() {
// If browser does not support notifications
// display proper message in settings.
if (("Notification" in window)) {
if ("Notification" in window) {
$warningUnsupported.hide();
$windows.on("show", "#settings", updateDesktopNotificationStatus);
} else {

View file

@ -47,40 +47,52 @@ function openImageViewer(link, {pushState = true} = {}) {
// Only expanded thumbnails are being cycled through.
// Previous image
let previousImage = link.closest(".preview").prev(".preview")
.find(".toggle-content .toggle-thumbnail").last();
let previousImage = link
.closest(".preview")
.prev(".preview")
.find(".toggle-content .toggle-thumbnail")
.last();
if (!previousImage.length) {
previousImage = link.closest(".msg").prevAll()
.find(".toggle-content .toggle-thumbnail").last();
previousImage = link
.closest(".msg")
.prevAll()
.find(".toggle-content .toggle-thumbnail")
.last();
}
previousImage.addClass("previous-image");
// Next image
let nextImage = link.closest(".preview").next(".preview")
.find(".toggle-content .toggle-thumbnail").first();
let nextImage = link
.closest(".preview")
.next(".preview")
.find(".toggle-content .toggle-thumbnail")
.first();
if (!nextImage.length) {
nextImage = link.closest(".msg").nextAll()
.find(".toggle-content .toggle-thumbnail").first();
nextImage = link
.closest(".msg")
.nextAll()
.find(".toggle-content .toggle-thumbnail")
.first();
}
nextImage.addClass("next-image");
imageViewer.html(templates.image_viewer({
image: link.find("img").prop("src"),
link: link.prop("href"),
type: link.parent().hasClass("toggle-type-link") ? "link" : "image",
hasPreviousImage: previousImage.length > 0,
hasNextImage: nextImage.length > 0,
}));
imageViewer.html(
templates.image_viewer({
image: link.find("img").prop("src"),
link: link.prop("href"),
type: link.parent().hasClass("toggle-type-link") ? "link" : "image",
hasPreviousImage: previousImage.length > 0,
hasNextImage: nextImage.length > 0,
})
);
// Turn off transitionend listener before opening the viewer,
// which caused image viewer to become empty in rare cases
imageViewer
.off("transitionend")
.addClass("opened");
imageViewer.off("transitionend").addClass("opened");
// History management
if (pushState) {
@ -109,17 +121,14 @@ imageViewer.on("click", ".next-image-btn", function() {
});
function closeImageViewer({pushState = true} = {}) {
imageViewer
.removeClass("opened")
.one("transitionend", function() {
imageViewer.empty();
});
imageViewer.removeClass("opened").one("transitionend", function() {
imageViewer.empty();
});
// History management
if (pushState) {
const clickTarget =
"#sidebar " +
`.chan[data-id="${$("#sidebar .chan.active").attr("data-id")}"]`;
"#sidebar " + `.chan[data-id="${$("#sidebar .chan.active").attr("data-id")}"]`;
history.pushState({clickTarget}, null, null);
}
}

View file

@ -49,7 +49,7 @@ function onTouchStart(e) {
}
function onTouchMove(e) {
const touch = touchCurPos = e.touches.item(0);
const touch = (touchCurPos = e.touches.item(0));
let distX = touch.screenX - touchStartPos.screenX;
const distY = touch.screenY - touchStartPos.screenY;
@ -93,7 +93,7 @@ function onTouchEnd() {
const diff = touchCurPos.screenX - touchStartPos.screenX;
const absDiff = Math.abs(diff);
if (absDiff > menuWidth / 2 || Date.now() - touchStartTime < 180 && absDiff > 50) {
if (absDiff > menuWidth / 2 || (Date.now() - touchStartTime < 180 && absDiff > 50)) {
SlideoutMenu.toggle(diff > 0);
}

View file

@ -62,9 +62,12 @@ socket.on("auth", function(data) {
storage.remove("token");
const error = login.find(".error");
error.show().closest("form").one("submit", function() {
error.hide();
});
error
.show()
.closest("form")
.one("submit", function() {
error.hide();
});
} else if (user) {
token = storage.get("token");

View file

@ -43,10 +43,7 @@ socket.on("changelog", function(data) {
// When there is a button to refresh the checker available, display it when
// data is expired. Before that, server would return same information anyway.
if (data.expiresAt) {
setTimeout(
() => $("#version-checker #check-now").show(),
data.expiresAt - Date.now()
);
setTimeout(() => $("#version-checker #check-now").show(), data.expiresAt - Date.now());
}
});
@ -62,6 +59,7 @@ $("#help").on("click", "#check-now", () => {
// Given a status and latest release information, update the version checker
// (CSS class and content)
function renderVersionChecker({status, latest}) {
$("#version-checker").prop("class", status)
$("#version-checker")
.prop("class", status)
.html(templates.version_checker({latest, status}));
}

View file

@ -187,8 +187,7 @@ function parseOverrideParams(params, data) {
}
// When the network is locked, URL overrides should not affect disabled fields
if (data.lockNetwork &&
["host", "port", "tls", "rejectUnauthorized"].includes(key)) {
if (data.lockNetwork && ["host", "port", "tls", "rejectUnauthorized"].includes(key)) {
continue;
}
@ -198,20 +197,29 @@ function parseOverrideParams(params, data) {
}
if (key === "join") {
value = value.split(",").map((chan) => {
if (!chan.match(/^[#&!+]/)) {
return `#${chan}`;
}
value = value
.split(",")
.map((chan) => {
if (!chan.match(/^[#&!+]/)) {
return `#${chan}`;
}
return chan;
}).join(", ");
return chan;
})
.join(", ");
}
// Override server provided defaults with parameters passed in the URL if they match the data type
switch (typeof data.defaults[key]) {
case "boolean": data.defaults[key] = value === "1" || value === "true"; break;
case "number": data.defaults[key] = Number(value); break;
case "string": data.defaults[key] = String(value); break;
case "boolean":
data.defaults[key] = value === "1" || value === "true";
break;
case "number":
data.defaults[key] = Number(value);
break;
case "string":
data.defaults[key] = String(value);
break;
}
}
}

View file

@ -126,7 +126,10 @@ function mergeNetworkData(newNetworks) {
// Channels require extra care to be merged correctly
if (key === "channels") {
currentNetwork.channels = mergeChannelData(currentNetwork.channels, network.channels);
currentNetwork.channels = mergeChannelData(
currentNetwork.channels,
network.channels
);
} else {
currentNetwork[key] = network[key];
}

View file

@ -7,7 +7,8 @@ const {vueApp, initChannel} = require("../vue");
socket.on("join", function(data) {
initChannel(data.chan);
vueApp.networks.find((n) => n.uuid === data.network)
vueApp.networks
.find((n) => n.uuid === data.network)
.channels.splice(data.index || -1, 0, data.chan);
// Queries do not automatically focus, unless the user did a whois

View file

@ -32,7 +32,11 @@ socket.on("msg", function(data) {
// Display received notices and errors in currently active channel.
// Reloading the page will put them back into the lobby window.
// We only want to put errors/notices in active channel if they arrive on the same network
if (data.msg.showInActive && vueApp.activeChannel && vueApp.activeChannel.network === receivingChannel.network) {
if (
data.msg.showInActive &&
vueApp.activeChannel &&
vueApp.activeChannel.network === receivingChannel.network
) {
channel = vueApp.activeChannel.channel;
data.chan = channel.id;
@ -76,7 +80,7 @@ socket.on("msg", function(data) {
const user = channel.users.find((u) => u.nick === data.msg.from.nick);
if (user) {
user.lastMessage = (new Date(data.msg.time)).getTime() || Date.now();
user.lastMessage = new Date(data.msg.time).getTime() || Date.now();
}
}
@ -98,7 +102,11 @@ function notifyMessage(targetId, channel, activeChannel, msg) {
}
}
if (options.settings.desktopNotifications && ("Notification" in window) && Notification.permission === "granted") {
if (
options.settings.desktopNotifications &&
"Notification" in window &&
Notification.permission === "granted"
) {
let title;
let body;

View file

@ -17,7 +17,8 @@ socket.on("network", function(data) {
vueApp.networks.push(network);
vueApp.$nextTick(() => {
sidebar.find(".chan")
sidebar
.find(".chan")
.last()
.trigger("click");
});
@ -60,15 +61,19 @@ socket.on("channel:state", function(data) {
socket.on("network:info", function(data) {
$("#connect")
.html(templates.windows.connect(data))
.find("form").on("submit", function() {
const uuid = $(this).find("input[name=uuid]").val();
const newName = $(this).find("#connect\\:name").val();
.find("form")
.on("submit", function() {
const uuid = $(this)
.find("input[name=uuid]")
.val();
const newName = $(this)
.find("#connect\\:name")
.val();
const network = vueApp.networks.find((n) => n.uuid === uuid);
network.name = network.channels[0].name = newName;
sidebar.find(`.network[data-uuid="${uuid}"] .chan.lobby .name`)
.click();
sidebar.find(`.network[data-uuid="${uuid}"] .chan.lobby .name`).click();
});
utils.togglePasswordField("#connect .reveal-password");

View file

@ -23,7 +23,8 @@ socket.on("open", function(id) {
channel.channel.unread = 0;
if (channel.channel.messages.length > 0) {
channel.channel.firstUnread = channel.channel.messages[channel.channel.messages.length - 1].id;
channel.channel.firstUnread =
channel.channel.messages[channel.channel.messages.length - 1].id;
}
}

View file

@ -17,7 +17,10 @@ socket.on("part", function(data) {
const channel = findChannel(data.chan);
if (channel) {
channel.network.channels.splice(channel.network.channels.findIndex((c) => c.id === data.chan), 1);
channel.network.channels.splice(
channel.network.channels.findIndex((c) => c.id === data.chan),
1
);
}
utils.synchronizeNotifiedState();

View file

@ -4,7 +4,11 @@ const socket = require("../socket");
const options = require("../options");
function evaluateSetting(name, value) {
if (options.settings.syncSettings && options.settings[name] !== value && !options.noSync.includes(name)) {
if (
options.settings.syncSettings &&
options.settings[name] !== value &&
!options.noSync.includes(name)
) {
options.processSetting(name, value, true);
} else if (options.alwaysSync.includes(name)) {
options.processSetting(name, value, true);

View file

@ -7,21 +7,21 @@ socket.on("sync_sort", function(data) {
const order = data.order;
switch (data.type) {
case "networks":
vueApp.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
case "networks":
vueApp.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
break;
break;
case "channels": {
const network = vueApp.networks.find((n) => n.uuid === data.target);
case "channels": {
const network = vueApp.networks.find((n) => n.uuid === data.target);
if (!network) {
return;
if (!network) {
return;
}
network.channels.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
break;
}
network.channels.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
break;
}
}
});

View file

@ -128,10 +128,14 @@ class Uploader {
const xhr = new XMLHttpRequest();
this.xhr = xhr;
xhr.upload.addEventListener("progress", (e) => {
const percent = Math.floor(e.loaded / e.total * 1000) / 10;
this.setProgress(percent);
}, false);
xhr.upload.addEventListener(
"progress",
(e) => {
const percent = Math.floor((e.loaded / e.total) * 1000) / 10;
this.setProgress(percent);
},
false
);
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
@ -182,12 +186,12 @@ class Uploader {
}
insertUploadUrl(url) {
const fullURL = (new URL(url, location)).toString();
const fullURL = new URL(url, location).toString();
const textbox = document.getElementById("input");
const initStart = textbox.selectionStart;
// Get the text before the cursor, and add a space if it's not in the beginning
const headToCursor = initStart > 0 ? (textbox.value.substr(0, initStart) + " ") : "";
const headToCursor = initStart > 0 ? textbox.value.substr(0, initStart) + " " : "";
// Get the remaining text after the cursor
const cursorToTail = textbox.value.substr(initStart);
@ -220,9 +224,9 @@ function initialize() {
}
/**
* Called in the `configuration` socket event.
* Makes it so the user can be notified if a file is too large without waiting for the upload to finish server-side.
**/
* Called in the `configuration` socket event.
* Makes it so the user can be notified if a file is too large without waiting for the upload to finish server-side.
**/
function setMaxFileSize(kb) {
instance.maxFileSize = kb;
}

View file

@ -135,7 +135,7 @@ function move(array, old_index, new_index) {
if (new_index >= array.length) {
let k = new_index - array.length;
while ((k--) + 1) {
while (k-- + 1) {
this.push(undefined);
}
}
@ -152,7 +152,8 @@ function closeChan(chan) {
cmd = "/quit";
const server = chan.find(".name").html();
if (!confirm(`Are you sure you want to remove ${server}?`)) { // eslint-disable-line no-alert
// eslint-disable-next-line no-alert
if (!confirm(`Are you sure you want to remove ${server}?`)) {
return false;
}
}

View file

@ -11,7 +11,9 @@ let applicationServerKey;
if ("serviceWorker" in navigator) {
navigator.serviceWorker.addEventListener("message", (event) => {
if (event.data && event.data.type === "open") {
$("#sidebar").find(`.chan[data-target="#${event.data.channel}"]`).trigger("click");
$("#sidebar")
.find(`.chan[data-target="#${event.data.channel}"]`)
.trigger("click");
}
});
}
@ -47,73 +49,89 @@ module.exports.initialize = () => {
$("#pushNotificationsHttps").hide();
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("service-worker.js").then((registration) => {
module.exports.hasServiceWorker = true;
navigator.serviceWorker
.register("service-worker.js")
.then((registration) => {
module.exports.hasServiceWorker = true;
if (!registration.pushManager) {
return;
}
return registration.pushManager.getSubscription().then((subscription) => {
$("#pushNotificationsUnsupported").hide();
pushNotificationsButton
.prop("disabled", false)
.on("click", onPushButton);
clientSubscribed = !!subscription;
if (clientSubscribed) {
alternatePushButton();
if (!registration.pushManager) {
return;
}
return registration.pushManager.getSubscription().then((subscription) => {
$("#pushNotificationsUnsupported").hide();
pushNotificationsButton.prop("disabled", false).on("click", onPushButton);
clientSubscribed = !!subscription;
if (clientSubscribed) {
alternatePushButton();
}
});
})
.catch((err) => {
$("#pushNotificationsUnsupported span").text(err);
});
}).catch((err) => {
$("#pushNotificationsUnsupported span").text(err);
});
}
};
function onPushButton() {
pushNotificationsButton.prop("disabled", true);
navigator.serviceWorker.ready.then((registration) =>
registration.pushManager.getSubscription().then((existingSubscription) => {
if (existingSubscription) {
socket.emit("push:unregister");
navigator.serviceWorker.ready
.then((registration) =>
registration.pushManager
.getSubscription()
.then((existingSubscription) => {
if (existingSubscription) {
socket.emit("push:unregister");
return existingSubscription.unsubscribe();
}
return existingSubscription.unsubscribe();
}
return registration.pushManager.subscribe({
applicationServerKey: urlBase64ToUint8Array(applicationServerKey),
userVisibleOnly: true,
}).then((subscription) => {
const rawKey = subscription.getKey ? subscription.getKey("p256dh") : "";
const key = rawKey ? window.btoa(String.fromCharCode(...new Uint8Array(rawKey))) : "";
const rawAuthSecret = subscription.getKey ? subscription.getKey("auth") : "";
const authSecret = rawAuthSecret ? window.btoa(String.fromCharCode(...new Uint8Array(rawAuthSecret))) : "";
return registration.pushManager
.subscribe({
applicationServerKey: urlBase64ToUint8Array(applicationServerKey),
userVisibleOnly: true,
})
.then((subscription) => {
const rawKey = subscription.getKey ? subscription.getKey("p256dh") : "";
const key = rawKey
? window.btoa(String.fromCharCode(...new Uint8Array(rawKey)))
: "";
const rawAuthSecret = subscription.getKey
? subscription.getKey("auth")
: "";
const authSecret = rawAuthSecret
? window.btoa(String.fromCharCode(...new Uint8Array(rawAuthSecret)))
: "";
socket.emit("push:register", {
token: storage.get("token"),
endpoint: subscription.endpoint,
keys: {
p256dh: key,
auth: authSecret,
},
});
socket.emit("push:register", {
token: storage.get("token"),
endpoint: subscription.endpoint,
keys: {
p256dh: key,
auth: authSecret,
},
});
return true;
});
}).then((successful) => {
if (successful) {
alternatePushButton().prop("disabled", false);
}
})
).catch((err) => {
$("#pushNotificationsUnsupported")
.find("span").text(`An error has occurred: ${err}`).end()
.show();
});
return true;
});
})
.then((successful) => {
if (successful) {
alternatePushButton().prop("disabled", false);
}
})
)
.catch((err) => {
$("#pushNotificationsUnsupported")
.find("span")
.text(`An error has occurred: ${err}`)
.end()
.show();
});
return false;
}
@ -127,10 +145,8 @@ function alternatePushButton() {
}
function urlBase64ToUint8Array(base64String) {
const padding = "=".repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, "+")
.replace(/_/g, "/");
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
@ -143,5 +159,9 @@ function urlBase64ToUint8Array(base64String) {
}
function isAllowedServiceWorkersHost() {
return location.protocol === "https:" || location.hostname === "localhost" || location.hostname === "127.0.0.1";
return (
location.protocol === "https:" ||
location.hostname === "localhost" ||
location.hostname === "127.0.0.1"
);
}

View file

@ -10,11 +10,15 @@ self.addEventListener("install", function() {
});
self.addEventListener("activate", function(event) {
event.waitUntil(caches.keys().then((names) => Promise.all(
names
.filter((name) => name !== cacheName)
.map((name) => caches.delete(name))
)));
event.waitUntil(
caches
.keys()
.then((names) =>
Promise.all(
names.filter((name) => name !== cacheName).map((name) => caches.delete(name))
)
)
);
event.waitUntil(self.clients.claim());
});
@ -50,9 +54,7 @@ async function putInCache(request, response) {
async function cleanRedirect(response) {
// Not all browsers support the Response.body stream, so fall back
// to reading the entire body into memory as a blob.
const bodyPromise = "body" in response ?
Promise.resolve(response.body) :
response.blob();
const bodyPromise = "body" in response ? Promise.resolve(response.body) : response.blob();
const body = await bodyPromise;
@ -134,29 +136,33 @@ function showNotification(event, payload) {
self.addEventListener("notificationclick", function(event) {
event.notification.close();
event.waitUntil(clients.matchAll({
includeUncontrolled: true,
type: "window",
}).then((clientList) => {
if (clientList.length === 0) {
if (clients.openWindow) {
return clients.openWindow(`.#${event.notification.tag}`);
}
event.waitUntil(
clients
.matchAll({
includeUncontrolled: true,
type: "window",
})
.then((clientList) => {
if (clientList.length === 0) {
if (clients.openWindow) {
return clients.openWindow(`.#${event.notification.tag}`);
}
return;
}
return;
}
const client = findSuitableClient(clientList);
const client = findSuitableClient(clientList);
client.postMessage({
type: "open",
channel: event.notification.tag,
});
client.postMessage({
type: "open",
channel: event.notification.tag,
});
if ("focus" in client) {
client.focus();
}
}));
if ("focus" in client) {
client.focus();
}
})
);
});
function findSuitableClient(clientList) {

View file

@ -13,15 +13,18 @@ module.exports = requireViews.keys().reduce((acc, path) => {
// Split path by folders, and create a new property if necessary/
// First 2 characters are "./"/
// Last element in the array ends with `.tpl` and needs to be `require`d.
path.substr(2).split("/").forEach((key) => {
if (key.endsWith(".tpl")) { //
tmp[key.substr(0, key.length - 4)] = requireViews(path);
} else {
tmp[key] = tmp[key] || {};
}
path.substr(2)
.split("/")
.forEach((key) => {
if (key.endsWith(".tpl")) {
//
tmp[key.substr(0, key.length - 4)] = requireViews(path);
} else {
tmp[key] = tmp[key] || {};
}
tmp = tmp[key];
});
tmp = tmp[key];
});
return acc;
}, {});

View file

@ -11,7 +11,13 @@ const pkg = require("./package.json");
if (!require("semver").satisfies(process.version, pkg.engines.node)) {
/* eslint-disable no-console */
console.error("The Lounge requires Node.js " + pkg.engines.node + " (current version: " + process.version + ")");
console.error(
"The Lounge requires Node.js " +
pkg.engines.node +
" (current version: " +
process.version +
")"
);
console.error("Please upgrade Node.js in order to use The Lounge");
console.error("See https://thelounge.chat/docs/install-and-upgrade");
console.error();

View file

@ -16,6 +16,7 @@
"coverage": "run-s test:{client,server} && nyc --nycrc-path=test/.nycrc-report.json report",
"dev": "run-p watch start",
"format:prettier": "prettier --write \"**/*.*\"",
"lint:check-eslint": "eslint --print-config .eslintrc.yml | eslint-config-prettier-check",
"lint:eslint": "eslint . --ext .js,.vue --report-unused-disable-directives --color",
"lint:prettier": "prettier --list-different \"**/*.*\"",
"lint:stylelint": "stylelint --color \"client/**/*.css\"",
@ -84,6 +85,7 @@
"css.escape": "1.5.1",
"emoji-regex": "8.0.0",
"eslint": "6.0.1",
"eslint-config-prettier": "4.0.0",
"eslint-plugin-vue": "5.2.3",
"fuzzy": "0.1.3",
"graphql-request": "1.8.2",

View file

@ -81,13 +81,21 @@ if (!version) {
}
function isValidVersion(str) {
return (/^[0-9]+\.[0-9]+\.[0-9]+(-(pre|rc)+\.[0-9]+)?$/.test(str));
return /^[0-9]+\.[0-9]+\.[0-9]+(-(pre|rc)+\.[0-9]+)?$/.test(str);
}
if (!isValidVersion(version)) {
log.error(`Argument ${colors.bold("version")} is incorrect It must be either:`);
log.error(`- A keyword among: ${colors.green("major")}, ${colors.green("minor")}, ${colors.green("patch")}, ${colors.green("prerelease")}, ${colors.green("pre")}`);
log.error(`- An explicit version of format ${colors.green("x.y.z")} (stable) or ${colors.green("x.y.z-(pre|rc).n")} (pre-release).`);
log.error(
`- A keyword among: ${colors.green("major")}, ${colors.green("minor")}, ${colors.green(
"patch"
)}, ${colors.green("prerelease")}, ${colors.green("pre")}`
);
log.error(
`- An explicit version of format ${colors.green("x.y.z")} (stable) or ${colors.green(
"x.y.z-(pre|rc).n"
)} (pre-release).`
);
process.exit(1);
}
@ -99,11 +107,15 @@ function prereleaseTemplate(items) {
[See the full changelog](${items.fullChangelogUrl})
${prereleaseType(items.version) === "rc" ?
`This is a release candidate (RC) for v${stableVersion(items.version)} to ensure maximum stability for public release.
Bugs may be fixed, but no further features will be added until the next stable version.` :
`This is a pre-release for v${stableVersion(items.version)} to offer latest changes without having to wait for a stable release.
${
prereleaseType(items.version) === "rc"
? `This is a release candidate (RC) for v${stableVersion(
items.version
)} to ensure maximum stability for public release.
Bugs may be fixed, but no further features will be added until the next stable version.`
: `This is a pre-release for v${stableVersion(
items.version
)} to offer latest changes without having to wait for a stable release.
At this stage, features may still be added or modified until the first release candidate for this version gets released.`
}
@ -128,7 +140,9 @@ function stableTemplate(items) {
return `
## v${items.version} - ${items.date}
For more details, [see the full changelog](${items.fullChangelogUrl}) and [milestone](${items.milestone.url}?closed=1).
For more details, [see the full changelog](${items.fullChangelogUrl}) and [milestone](${
items.milestone.url
}?closed=1).
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@ DESCRIPTION, ANNOUNCEMENT, ETC. @@
@ -138,8 +152,10 @@ For more details, [see the full changelog](${items.fullChangelogUrl}) and [miles
### Changed
${isEmpty(items.dependencies) ? "" :
`- Update production dependencies to their latest versions:
${
isEmpty(items.dependencies)
? ""
: `- Update production dependencies to their latest versions:
${printDependencyList(items.dependencies)}`
}
@ -157,23 +173,31 @@ ${printList(items.security)}
### Documentation
${items.documentation.length === 0 ? "" :
`In the main repository:
${
items.documentation.length === 0
? ""
: `In the main repository:
${printList(items.documentation)}`
}
${items.websiteDocumentation.length === 0 ? "" :
`On the [website repository](https://github.com/thelounge/thelounge.github.io):
${
items.websiteDocumentation.length === 0
? ""
: `On the [website repository](https://github.com/thelounge/thelounge.github.io):
${printList(items.websiteDocumentation)}`
}
### Internals
${printList(items.internals)}${isEmpty(items.devDependencies) ? "" : `
${printList(items.internals)}${
isEmpty(items.devDependencies)
? ""
: `
- Update development dependencies to their latest versions:
${printDependencyList(items.devDependencies)}`}
${printDependencyList(items.devDependencies)}`
}
@@@@@@@@@@@@@@@@@@@
@@ UNCATEGORIZED @@
@ -375,7 +399,9 @@ class RepositoryFetcher {
const prQuery = `query fetchPullRequests($repositoryName: String!) {
repository(owner: "thelounge", name: $repositoryName) {
${numbers.map((number) => `
${numbers
.map(
(number) => `
PR${number}: pullRequest(number: ${number}) {
__typename
title
@ -398,7 +424,9 @@ class RepositoryFetcher {
}
}
}
`).join("")}
`
)
.join("")}
}
}`;
const data = await this.fetch(prQuery);
@ -458,9 +486,8 @@ function printAuthorLink({login, url}) {
// Builds a Markdown link for a given pull request or commit object
function printEntryLink(entry) {
const label = entry.__typename === "PullRequest"
? `#${entry.number}`
: `\`${entry.abbreviatedOid}\``;
const label =
entry.__typename === "PullRequest" ? `#${entry.number}` : `\`${entry.abbreviatedOid}\``;
return `[${label}](${entry.url})`;
}
@ -476,12 +503,16 @@ function printLine(entry) {
// Builds a Markdown list item for a given pull request
function printPullRequest(pullRequest) {
return `- ${pullRequest.title} (${printEntryLink(pullRequest)} ${printAuthorLink(pullRequest.author)})`;
return `- ${pullRequest.title} (${printEntryLink(pullRequest)} ${printAuthorLink(
pullRequest.author
)})`;
}
// Builds a Markdown list item for a commit made directly in `master`
function printCommit(commit) {
return `- ${commit.messageHeadline} (${printEntryLink(commit)} ${printAuthorLink(commit.author.user)})`;
return `- ${commit.messageHeadline} (${printEntryLink(commit)} ${printAuthorLink(
commit.author.user
)})`;
}
// Builds a Markdown list of all given items
@ -543,20 +574,23 @@ function hasLabel(labels, expected) {
}
function hasAnnotatedComment(comments, expected) {
return comments && comments.nodes.some(({authorAssociation, body}) =>
["OWNER", "MEMBER"].includes(authorAssociation) &&
body.split("\r\n").includes(`[${expected}]`)
return (
comments &&
comments.nodes.some(
({authorAssociation, body}) =>
["OWNER", "MEMBER"].includes(authorAssociation) &&
body.split("\r\n").includes(`[${expected}]`)
)
);
}
function isSkipped(entry) {
return (
(entry.__typename === "Commit" && (
(entry.__typename === "Commit" &&
// Version bump commits created by `yarn version`
isValidVersion(entry.messageHeadline) ||
// Commit message suggested by this script
entry.messageHeadline.startsWith("Add changelog entry for v")
)) ||
(isValidVersion(entry.messageHeadline) ||
// Commit message suggested by this script
entry.messageHeadline.startsWith("Add changelog entry for v"))) ||
hasLabelOrAnnotatedComment(entry, "Meta: Skip Changelog")
);
}
@ -608,76 +642,85 @@ function extractPackages({title, url}) {
return [];
}
return extracted[1]
.replace(/`/g, "")
.split(/, and |, | and /);
return extracted[1].replace(/`/g, "").split(/, and |, | and /);
}
// Given an array of entries (PRs or commits), separates them into sections,
// based on different information that describes them.
function parse(entries) {
return entries.reduce((result, entry) => {
let deps;
return entries.reduce(
(result, entry) => {
let deps;
if (isSkipped(entry)) {
result.skipped.push(entry);
} else if (isDependency(entry) && (deps = extractPackages(entry))) {
deps.forEach((packageName) => {
const dependencyType = whichDependencyType(packageName);
if (isSkipped(entry)) {
result.skipped.push(entry);
} else if (isDependency(entry) && (deps = extractPackages(entry))) {
deps.forEach((packageName) => {
const dependencyType = whichDependencyType(packageName);
if (dependencyType) {
if (!result[dependencyType][packageName]) {
result[dependencyType][packageName] = [];
if (dependencyType) {
if (!result[dependencyType][packageName]) {
result[dependencyType][packageName] = [];
}
result[dependencyType][packageName].push(entry);
} else {
log.info(
`${colors.bold(packageName)} was updated in ${colors.green(
"#" + entry.number
)} then removed since last release. Skipping. ${colors.gray(
entry.url
)}`
);
}
result[dependencyType][packageName].push(entry);
} else {
log.info(`${colors.bold(packageName)} was updated in ${colors.green("#" + entry.number)} then removed since last release. Skipping. ${colors.gray(entry.url)}`);
}
});
} else if (isDocumentation(entry)) {
result.documentation.push(entry);
} else if (isDeprecation(entry)) {
result.deprecations.push(entry);
} else if (isSecurity(entry)) {
result.security.push(entry);
} else if (isInternal(entry)) {
result.internals.push(entry);
} else {
if (isFeature(entry)) {
result.uncategorized.feature.push(entry);
} else if (isBug(entry)) {
result.uncategorized.bug.push(entry);
});
} else if (isDocumentation(entry)) {
result.documentation.push(entry);
} else if (isDeprecation(entry)) {
result.deprecations.push(entry);
} else if (isSecurity(entry)) {
result.security.push(entry);
} else if (isInternal(entry)) {
result.internals.push(entry);
} else {
result.uncategorized.other.push(entry);
if (isFeature(entry)) {
result.uncategorized.feature.push(entry);
} else if (isBug(entry)) {
result.uncategorized.bug.push(entry);
} else {
result.uncategorized.other.push(entry);
}
}
}
return result;
}, {
skipped: [],
dependencies: {},
devDependencies: {},
deprecations: [],
documentation: [],
internals: [],
security: [],
uncategorized: {
feature: [],
bug: [],
other: [],
return result;
},
unknownDependencies: new Set(),
});
{
skipped: [],
dependencies: {},
devDependencies: {},
deprecations: [],
documentation: [],
internals: [],
security: [],
uncategorized: {
feature: [],
bug: [],
other: [],
},
unknownDependencies: new Set(),
}
);
}
function dedupeEntries(changelog, items) {
const dedupe = (entries) =>
entries.filter((entry) => !changelog.includes(printEntryLink(entry)));
["deprecations", "documentation", "websiteDocumentation", "internals", "security"].forEach((type) => {
items[type] = dedupe(items[type]);
});
["deprecations", "documentation", "websiteDocumentation", "internals", "security"].forEach(
(type) => {
items[type] = dedupe(items[type]);
}
);
["dependencies", "devDependencies", "uncategorized"].forEach((type) => {
Object.entries(items[type]).forEach(([name, entries]) => {
@ -692,8 +735,8 @@ function extractContributors(entries) {
const set = Object.values(entries).reduce((memo, {__typename, author}) => {
if (__typename === "PullRequest" && author.__typename !== "Bot") {
memo.add("@" + author.login);
// Commit authors are *always* of type "User", so have to discriminate some
// other way. Making the assumption of a suffix for now, see how that goes.
// Commit authors are *always* of type "User", so have to discriminate some
// other way. Making the assumption of a suffix for now, see how that goes.
} else if (__typename === "Commit" && !author.user.login.endsWith("-bot")) {
memo.add("@" + author.user.login);
}
@ -701,8 +744,7 @@ function extractContributors(entries) {
return memo;
}, new Set());
return Array.from(set)
.sort((a, b) => a.localeCompare(b, "en", {sensitivity: "base"}));
return Array.from(set).sort((a, b) => a.localeCompare(b, "en", {sensitivity: "base"}));
}
const client = new GraphQLClient("https://api.github.com/graphql", {
@ -727,13 +769,17 @@ async function generateChangelogEntry(changelog, targetVersion) {
} else {
template = stableTemplate;
const codeCommitsAndPullRequests = await codeRepo.fetchCommitsAndPullRequestsSince("v" + previousVersion);
const codeCommitsAndPullRequests = await codeRepo.fetchCommitsAndPullRequestsSince(
"v" + previousVersion
);
items = parse(codeCommitsAndPullRequests);
items.milestone = await codeRepo.fetchMilestone(targetVersion);
const websiteRepo = new RepositoryFetcher(client, "thelounge.github.io");
const previousWebsiteVersion = await websiteRepo.fetchPreviousVersion(targetVersion);
const websiteCommitsAndPullRequests = await websiteRepo.fetchCommitsAndPullRequestsSince("v" + previousWebsiteVersion);
const websiteCommitsAndPullRequests = await websiteRepo.fetchCommitsAndPullRequestsSince(
"v" + previousWebsiteVersion
);
items.websiteDocumentation = websiteCommitsAndPullRequests;
contributors = extractContributors([
@ -781,11 +827,18 @@ function addToChangelog(changelog, newEntry) {
const changelog = await readFile(changelogPath, "utf8");
try {
({changelogEntry, skipped, contributors} = await generateChangelogEntry(changelog, version));
({changelogEntry, skipped, contributors} = await generateChangelogEntry(
changelog,
version
));
} catch (error) {
if (error.response && error.response.status === 401) {
log.error(`GitHub returned an error: ${colors.red(error.response.message)}`);
log.error(`Make sure your personal access token is set with ${colors.bold("public_repo")} scope.`);
log.error(
`Make sure your personal access token is set with ${colors.bold(
"public_repo"
)} scope.`
);
} else {
log.error(error);
}
@ -806,10 +859,14 @@ function addToChangelog(changelog, newEntry) {
// Step 3 (optional): Print a list of skipped entries if there are any
if (skipped.length > 0) {
const pad = Math.max(...skipped.map((entry) => (entry.title || entry.messageHeadline).length));
const pad = Math.max(
...skipped.map((entry) => (entry.title || entry.messageHeadline).length)
);
log.warn(`${skipped.length} ${skipped.length > 1 ? "entries were" : "entry was"} skipped:`);
skipped.forEach((entry) => {
log.warn(`- ${(entry.title || entry.messageHeadline).padEnd(pad)} ${colors.gray(entry.url)}`);
log.warn(
`- ${(entry.title || entry.messageHeadline).padEnd(pad)} ${colors.gray(entry.url)}`
);
});
}
@ -819,7 +876,11 @@ function addToChangelog(changelog, newEntry) {
if (isPrerelease(version)) {
log.info(`You can now run: ${colors.bold(commitCommand)}`);
} else {
log.info(`Please edit ${colors.bold("CHANGELOG.md")} to your liking then run: ${colors.bold(commitCommand)}`);
log.info(
`Please edit ${colors.bold("CHANGELOG.md")} to your liking then run: ${colors.bold(
commitCommand
)}`
);
}
log.info(`Finished in ${colors.bold(Date.now() - startTime)}ms.`);

View file

@ -15,15 +15,10 @@ const {join} = require("path");
const {spawnSync} = require("child_process");
function getGitUsername() {
return spawnSync("git", ["config", "user.name"], {encoding: "utf8"})
.stdout
.trim();
return spawnSync("git", ["config", "user.name"], {encoding: "utf8"}).stdout.trim();
}
const configContent = readFileSync(
join(__dirname, "..", "defaults", "config.js"),
"utf8"
);
const configContent = readFileSync(join(__dirname, "..", "defaults", "config.js"), "utf8");
const docPath = join(process.argv[2], "_includes", "config.js.md");
@ -42,7 +37,8 @@ const extractedDoc = configContent
}
return acc;
}, []).join("\n");
}, [])
.join("\n");
const infoBlockHeader = `<!--
DO NOT EDIT THIS FILE MANUALLY.
@ -62,10 +58,13 @@ writeFileSync(docPath, generatedContent);
log.info(
`${colors.bold(generatedContent.split("\n").length)} lines ` +
`(${colors.bold(generatedContent.length)} characters) ` +
`were written in ${colors.green(docPath)}.`
`(${colors.bold(generatedContent.length)} characters) ` +
`were written in ${colors.green(docPath)}.`
);
function getPrettyDate() {
return (new Date()).toISOString().split(".")[0].replace("T", " ");
return new Date()
.toISOString()
.split(".")[0]
.replace("T", " ");
}

View file

@ -5,7 +5,9 @@ const path = require("path");
const fs = require("fs");
(async () => {
const response = await got("https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json");
const response = await got(
"https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json"
);
const emojiStrategy = JSON.parse(response.body);
const emojiMap = {};
const fullNameEmojiMap = {};
@ -21,21 +23,13 @@ const fs = require("fs");
const emojiMapOutput = JSON.stringify(emojiMap, null, 2) + "\n";
const fullNameEmojiMapOutput = JSON.stringify(fullNameEmojiMap, null, 2) + "\n";
fs.writeFileSync(path.resolve(path.join(
__dirname,
"..",
"client",
"js",
"libs",
"simplemap.json"
)), emojiMapOutput);
fs.writeFileSync(
path.resolve(path.join(__dirname, "..", "client", "js", "libs", "simplemap.json")),
emojiMapOutput
);
fs.writeFileSync(path.resolve(path.join(
__dirname,
"..",
"client",
"js",
"libs",
"fullnamemap.json"
)), fullNameEmojiMapOutput);
fs.writeFileSync(
path.resolve(path.join(__dirname, "..", "client", "js", "libs", "fullnamemap.json")),
fullNameEmojiMapOutput
);
})();

View file

@ -159,18 +159,26 @@ Client.prototype.connect = function(args) {
return;
}
channels.push(client.createChannel({
name: chan.name,
key: chan.key || "",
type: chan.type,
}));
channels.push(
client.createChannel({
name: chan.name,
key: chan.key || "",
type: chan.type,
})
);
});
if (badName && client.name) {
log.warn("User '" + client.name + "' on network '" + args.name + "' has an invalid channel which has been ignored");
log.warn(
"User '" +
client.name +
"' on network '" +
args.name +
"' has an invalid channel which has been ignored"
);
}
// `join` is kept for backwards compatibility when updating from versions <2.0
// also used by the "connect" window
// `join` is kept for backwards compatibility when updating from versions <2.0
// also used by the "connect" window
} else if (args.join) {
channels = args.join
.replace(/,/g, " ")
@ -188,7 +196,9 @@ Client.prototype.connect = function(args) {
const network = new Network({
uuid: args.uuid,
name: String(args.name || (Helper.config.displayNetwork ? "" : Helper.config.defaults.name) || ""),
name: String(
args.name || (Helper.config.displayNetwork ? "" : Helper.config.defaults.name) || ""
),
host: String(args.host || ""),
port: parseInt(args.port, 10),
tls: !!args.tls,
@ -218,16 +228,18 @@ Client.prototype.connect = function(args) {
network.createIrcFramework(client);
events.forEach((plugin) => {
require(`./plugins/irc-events/${plugin}`).apply(client, [
network.irc,
network,
]);
require(`./plugins/irc-events/${plugin}`).apply(client, [network.irc, network]);
});
if (network.userDisconnected) {
network.channels[0].pushMessage(client, new Msg({
text: "You have manually disconnected from this network before, use the /connect command to connect again.",
}), true);
network.channels[0].pushMessage(
client,
new Msg({
text:
"You have manually disconnected from this network before, use the /connect command to connect again.",
}),
true
);
} else {
network.irc.connect();
}
@ -248,7 +260,10 @@ Client.prototype.generateToken = function(callback) {
};
Client.prototype.calculateTokenHash = function(token) {
return crypto.createHash("sha512").update(token).digest("hex");
return crypto
.createHash("sha512")
.update(token)
.digest("hex");
};
Client.prototype.updateSession = function(token, ip, request) {
@ -284,16 +299,20 @@ Client.prototype.updateSession = function(token, ip, request) {
Client.prototype.setPassword = function(hash, callback) {
const client = this;
client.manager.updateUser(client.name, {
password: hash,
}, function(err) {
if (err) {
return callback(false);
}
client.manager.updateUser(
client.name,
{
password: hash,
},
function(err) {
if (err) {
return callback(false);
}
client.config.password = hash;
return callback(true);
});
client.config.password = hash;
return callback(true);
}
);
};
Client.prototype.input = function(data) {
@ -321,10 +340,13 @@ Client.prototype.inputLine = function(data) {
// This is either a normal message or a command escaped with a leading '/'
if (text.charAt(0) !== "/" || text.charAt(1) === "/") {
if (target.chan.type === Chan.Type.LOBBY) {
target.chan.pushMessage(this, new Msg({
type: Msg.Type.ERROR,
text: "Messages can not be sent to lobbies.",
}));
target.chan.pushMessage(
this,
new Msg({
type: Msg.Type.ERROR,
text: "Messages can not be sent to lobbies.",
})
);
return;
}
@ -351,17 +373,25 @@ Client.prototype.inputLine = function(data) {
if (typeof plugin.input === "function" && (connected || plugin.allowDisconnected)) {
connected = true;
plugin.input(new PublicClient(client), {network: target.network, chan: target.chan}, cmd, args);
plugin.input(
new PublicClient(client),
{network: target.network, chan: target.chan},
cmd,
args
);
}
} else if (connected) {
irc.raw(text);
}
if (!connected) {
target.chan.pushMessage(this, new Msg({
type: Msg.Type.ERROR,
text: "You are not connected to the IRC network, unable to send your command.",
}));
target.chan.pushMessage(
this,
new Msg({
type: Msg.Type.ERROR,
text: "You are not connected to the IRC network, unable to send your command.",
})
);
}
};
@ -385,7 +415,10 @@ Client.prototype.compileCustomHighlights = function() {
return;
}
client.highlightRegex = new RegExp(`(?:^|[ .,+!?|/:<>(){}'"@&~-])(?:${highlightsTokens.join("|")})(?:$|[ .,+!?|/:<>(){}'"-])`, "i");
client.highlightRegex = new RegExp(
`(?:^|[ .,+!?|/:<>(){}'"@&~-])(?:${highlightsTokens.join("|")})(?:$|[ .,+!?|/:<>(){}'"-])`,
"i"
);
};
Client.prototype.more = function(data) {
@ -458,45 +491,45 @@ Client.prototype.sort = function(data) {
}
switch (data.type) {
case "networks":
this.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
case "networks":
this.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
// Sync order to connected clients
this.emit("sync_sort", {
order: this.networks.map((obj) => obj.uuid),
type: data.type,
});
// Sync order to connected clients
this.emit("sync_sort", {
order: this.networks.map((obj) => obj.uuid),
type: data.type,
});
break;
break;
case "channels": {
const network = _.find(this.networks, {uuid: data.target});
case "channels": {
const network = _.find(this.networks, {uuid: data.target});
if (!network) {
return;
}
network.channels.sort((a, b) => {
// Always sort lobby to the top regardless of what the client has sent
// Because there's a lot of code that presumes channels[0] is the lobby
if (a.type === Chan.Type.LOBBY) {
return -1;
} else if (b.type === Chan.Type.LOBBY) {
return 1;
if (!network) {
return;
}
return order.indexOf(a.id) - order.indexOf(b.id);
});
network.channels.sort((a, b) => {
// Always sort lobby to the top regardless of what the client has sent
// Because there's a lot of code that presumes channels[0] is the lobby
if (a.type === Chan.Type.LOBBY) {
return -1;
} else if (b.type === Chan.Type.LOBBY) {
return 1;
}
// Sync order to connected clients
this.emit("sync_sort", {
order: network.channels.map((obj) => obj.id),
type: data.type,
target: network.uuid,
});
return order.indexOf(a.id) - order.indexOf(b.id);
});
break;
}
// Sync order to connected clients
this.emit("sync_sort", {
order: network.channels.map((obj) => obj.id),
type: data.type,
target: network.uuid,
});
break;
}
}
this.save();
@ -581,9 +614,14 @@ Client.prototype.clientDetach = function(socketId) {
};
Client.prototype.registerPushSubscription = function(session, subscription, noSave) {
if (!_.isPlainObject(subscription) || !_.isPlainObject(subscription.keys)
|| typeof subscription.endpoint !== "string" || !/^https?:\/\//.test(subscription.endpoint)
|| typeof subscription.keys.p256dh !== "string" || typeof subscription.keys.auth !== "string") {
if (
!_.isPlainObject(subscription) ||
!_.isPlainObject(subscription.keys) ||
typeof subscription.endpoint !== "string" ||
!/^https?:\/\//.test(subscription.endpoint) ||
typeof subscription.keys.p256dh !== "string" ||
typeof subscription.keys.auth !== "string"
) {
session.pushSubscription = null;
return;
}
@ -614,13 +652,17 @@ Client.prototype.unregisterPushSubscription = function(token) {
});
};
Client.prototype.save = _.debounce(function SaveClient() {
if (Helper.config.public) {
return;
}
Client.prototype.save = _.debounce(
function SaveClient() {
if (Helper.config.public) {
return;
}
const client = this;
const json = {};
json.networks = this.networks.map((n) => n.export());
client.manager.updateUser(client.name, json);
}, 1000, {maxWait: 10000});
const client = this;
const json = {};
json.networks = this.networks.map((n) => n.export());
client.manager.updateUser(client.name, json);
},
1000,
{maxWait: 10000}
);

View file

@ -31,7 +31,9 @@ ClientManager.prototype.findClient = function(name) {
ClientManager.prototype.autoloadUsers = function() {
const users = this.getUsers();
const noUsersWarning = `There are currently no users. Create one with ${colors.bold("thelounge add <name>")}.`;
const noUsersWarning = `There are currently no users. Create one with ${colors.bold(
"thelounge add <name>"
)}.`;
if (users.length === 0) {
log.info(noUsersWarning);
@ -39,28 +41,35 @@ ClientManager.prototype.autoloadUsers = function() {
users.forEach((name) => this.loadUser(name));
fs.watch(Helper.getUsersPath(), _.debounce(() => {
const loaded = this.clients.map((c) => c.name);
const updatedUsers = this.getUsers();
fs.watch(
Helper.getUsersPath(),
_.debounce(
() => {
const loaded = this.clients.map((c) => c.name);
const updatedUsers = this.getUsers();
if (updatedUsers.length === 0) {
log.info(noUsersWarning);
}
if (updatedUsers.length === 0) {
log.info(noUsersWarning);
}
// Reload all users. Existing users will only have their passwords reloaded.
updatedUsers.forEach((name) => this.loadUser(name));
// Reload all users. Existing users will only have their passwords reloaded.
updatedUsers.forEach((name) => this.loadUser(name));
// Existing users removed since last time users were loaded
_.difference(loaded, updatedUsers).forEach((name) => {
const client = _.find(this.clients, {name});
// Existing users removed since last time users were loaded
_.difference(loaded, updatedUsers).forEach((name) => {
const client = _.find(this.clients, {name});
if (client) {
client.quit(true);
this.clients = _.without(this.clients, client);
log.info(`User ${colors.bold(name)} disconnected and removed.`);
}
});
}, 1000, {maxWait: 10000}));
if (client) {
client.quit(true);
this.clients = _.without(this.clients, client);
log.info(`User ${colors.bold(name)} disconnected and removed.`);
}
});
},
1000,
{maxWait: 10000}
)
);
};
ClientManager.prototype.loadUser = function(name) {

View file

@ -8,7 +8,8 @@ const program = require("commander");
const Helper = require("../helper");
const Utils = require("./utils");
program.version(Helper.getVersion(), "-v, --version")
program
.version(Helper.getVersion(), "-v, --version")
.option(
"-c, --config <key=value>",
"override entries of the configuration file, must be specified for each entry that needs to be overriden",
@ -26,13 +27,21 @@ if (process.getuid) {
const uid = process.getuid();
if (uid === 0) {
log.warn(`You are currently running The Lounge as root. ${colors.bold.red("We highly discourage running as root!")}`);
log.warn(
`You are currently running The Lounge as root. ${colors.bold.red(
"We highly discourage running as root!"
)}`
);
}
fs.stat(path.join(Helper.getHomePath(), "config.js"), (err, stat) => {
if (!err && stat.uid !== uid) {
log.warn("Config file owner does not match the user you are currently running The Lounge as.");
log.warn("To avoid issues, you should execute The Lounge commands under the same user.");
log.warn(
"Config file owner does not match the user you are currently running The Lounge as."
);
log.warn(
"To avoid issues, you should execute The Lounge commands under the same user."
);
}
});
}

View file

@ -30,41 +30,63 @@ program
packageJson(packageName, {
fullMetadata: true,
version: packageVersion,
}).then((json) => {
if (!("thelounge" in json)) {
log.error(`${colors.red(json.name + " v" + json.version)} does not have The Lounge metadata.`);
})
.then((json) => {
if (!("thelounge" in json)) {
log.error(
`${colors.red(
json.name + " v" + json.version
)} does not have The Lounge metadata.`
);
process.exit(1);
}
log.info(`Installing ${colors.green(json.name + " v" + json.version)}...`);
const packagesPath = Helper.getPackagesPath();
const packagesConfig = path.join(packagesPath, "package.json");
// Create node_modules folder, otherwise yarn will start walking upwards to find one
fsextra.ensureDirSync(path.join(packagesPath, "node_modules"));
// Create package.json with private set to true, if it doesn't exist already
if (!fs.existsSync(packagesConfig)) {
fs.writeFileSync(
packagesConfig,
JSON.stringify(
{
private: true,
description:
"Packages for The Lounge. All packages in node_modules directory will be automatically loaded.",
},
null,
"\t"
)
);
}
return Utils.executeYarnCommand(
"add",
"--production",
"--exact",
`${json.name}@${json.version}`
)
.then(() => {
log.info(
`${colors.green(
json.name + " v" + json.version
)} has been successfully installed.`
);
})
.catch((code) => {
throw `Failed to install ${colors.green(
json.name + " v" + json.version
)}. Exit code: ${code}`;
});
})
.catch((e) => {
log.error(`${e}`);
process.exit(1);
}
log.info(`Installing ${colors.green(json.name + " v" + json.version)}...`);
const packagesPath = Helper.getPackagesPath();
const packagesConfig = path.join(packagesPath, "package.json");
// Create node_modules folder, otherwise yarn will start walking upwards to find one
fsextra.ensureDirSync(path.join(packagesPath, "node_modules"));
// Create package.json with private set to true, if it doesn't exist already
if (!fs.existsSync(packagesConfig)) {
fs.writeFileSync(packagesConfig, JSON.stringify({
private: true,
description: "Packages for The Lounge. All packages in node_modules directory will be automatically loaded.",
}, null, "\t"));
}
return Utils.executeYarnCommand(
"add",
"--production",
"--exact",
`${json.name}@${json.version}`
).then(() => {
log.info(`${colors.green(json.name + " v" + json.version)} has been successfully installed.`);
}).catch((code) => {
throw `Failed to install ${colors.green(json.name + " v" + json.version)}. Exit code: ${code}`;
});
}).catch((e) => {
log.error(`${e}`);
process.exit(1);
});
});

View file

@ -24,13 +24,10 @@ function initalizeConfig() {
if (!fs.existsSync(Helper.getConfigPath())) {
fsextra.ensureDirSync(Helper.getHomePath());
fs.chmodSync(Helper.getHomePath(), "0700");
fsextra.copySync(path.resolve(path.join(
__dirname,
"..",
"..",
"defaults",
"config.js"
)), Helper.getConfigPath());
fsextra.copySync(
path.resolve(path.join(__dirname, "..", "..", "defaults", "config.js")),
Helper.getConfigPath()
);
log.info(`Configuration file created at ${colors.green(Helper.getConfigPath())}.`);
}

View file

@ -32,18 +32,20 @@ program
const packages = JSON.parse(fs.readFileSync(packagesConfig, "utf-8"));
if (!packages.dependencies || !Object.prototype.hasOwnProperty.call(packages.dependencies, packageName)) {
if (
!packages.dependencies ||
!Object.prototype.hasOwnProperty.call(packages.dependencies, packageName)
) {
log.warn(packageWasNotInstalled);
process.exit(1);
}
return Utils.executeYarnCommand(
"remove",
packageName
).then(() => {
log.info(`${colors.green(packageName)} has been successfully uninstalled.`);
}).catch((code) => {
log.error(`Failed to uninstall ${colors.green(packageName)}. Exit code: ${code}`);
process.exit(1);
});
return Utils.executeYarnCommand("remove", packageName)
.then(() => {
log.info(`${colors.green(packageName)} has been successfully uninstalled.`);
})
.catch((code) => {
log.error(`Failed to uninstall ${colors.green(packageName)}. Exit code: ${code}`);
process.exit(1);
});
});

View file

@ -18,11 +18,7 @@ program
const packagesPath = Helper.getPackagesPath();
const packagesConfig = path.join(packagesPath, "package.json");
const packagesList = JSON.parse(fs.readFileSync(packagesConfig)).dependencies;
const argsList = [
"upgrade",
"--production",
"--latest",
];
const argsList = ["upgrade", "--production", "--latest"];
let count = 0;
@ -54,9 +50,11 @@ program
return;
}
return Utils.executeYarnCommand(...argsList).then(() => {
log.info("Package(s) have been successfully upgraded.");
}).catch((code) => {
throw `Failed to upgrade package(s). Exit code ${code}`;
});
return Utils.executeYarnCommand(...argsList)
.then(() => {
log.info("Package(s) have been successfully upgraded.");
})
.catch((code) => {
throw `Failed to upgrade package(s). Exit code ${code}`;
});
});

View file

@ -21,7 +21,8 @@ program
const manager = new ClientManager();
const users = manager.getUsers();
if (users === undefined) { // There was an error, already logged
if (users === undefined) {
// There was an error, already logged
return;
}
@ -30,31 +31,37 @@ program
return;
}
log.prompt({
text: "Enter password:",
silent: true,
}, function(err, password) {
if (!password) {
log.error("Password cannot be empty.");
return;
}
log.prompt(
{
text: "Enter password:",
silent: true,
},
function(err, password) {
if (!password) {
log.error("Password cannot be empty.");
return;
}
if (!err) {
log.prompt({
text: "Save logs to disk?",
default: "yes",
}, function(err2, enableLog) {
if (!err2) {
add(
manager,
name,
password,
enableLog.charAt(0).toLowerCase() === "y"
);
}
});
if (!err) {
log.prompt(
{
text: "Save logs to disk?",
default: "yes",
},
function(err2, enableLog) {
if (!err2) {
add(
manager,
name,
password,
enableLog.charAt(0).toLowerCase() === "y"
);
}
}
);
}
}
});
);
});
function add(manager, name, password, enableLog) {

View file

@ -21,7 +21,8 @@ program
const ClientManager = require("../../clientManager");
const users = new ClientManager().getUsers();
if (users === undefined) { // There was an error, already logged
if (users === undefined) {
// There was an error, already logged
return;
}
@ -36,6 +37,10 @@ program
{stdio: "inherit"}
);
child_spawn.on("error", function() {
log.error(`Unable to open ${colors.green(Helper.getUserConfigPath(name))}. ${colors.bold("$EDITOR")} is not set, and ${colors.bold("vi")} was not found.`);
log.error(
`Unable to open ${colors.green(Helper.getUserConfigPath(name))}. ${colors.bold(
"$EDITOR"
)} is not set, and ${colors.bold("vi")} was not found.`
);
});
});

View file

@ -20,7 +20,8 @@ program
const ClientManager = require("../../clientManager");
const users = new ClientManager().getUsers();
if (users === undefined) { // There was an error, already logged
if (users === undefined) {
// There was an error, already logged
return;
}
@ -30,6 +31,10 @@ program
log.info(`${i + 1}. ${colors.bold(user)}`);
});
} else {
log.info(`There are currently no users. Create one with ${colors.bold("thelounge add <name>")}.`);
log.info(
`There are currently no users. Create one with ${colors.bold(
"thelounge add <name>"
)}.`
);
}
});

View file

@ -20,7 +20,8 @@ program
const ClientManager = require("../../clientManager");
const users = new ClientManager().getUsers();
if (users === undefined) { // There was an error, already logged
if (users === undefined) {
// There was an error, already logged
return;
}
@ -31,20 +32,20 @@ program
const file = Helper.getUserConfigPath(name);
const user = require(file);
log.prompt({
text: "Enter new password:",
silent: true,
}, function(err, password) {
if (err) {
return;
}
log.prompt(
{
text: "Enter new password:",
silent: true,
},
function(err, password) {
if (err) {
return;
}
user.password = Helper.password.hash(password);
user.sessions = {};
fs.writeFileSync(
file,
JSON.stringify(user, null, "\t")
);
log.info(`Successfully reset password for ${colors.bold(name)}.`);
});
user.password = Helper.password.hash(password);
user.sessions = {};
fs.writeFileSync(file, JSON.stringify(user, null, "\t"));
log.info(`Successfully reset password for ${colors.bold(name)}.`);
}
);
});

View file

@ -16,7 +16,9 @@ class Utils {
"",
" Environment variable:",
"",
` THELOUNGE_HOME Path for all configuration files and folders. Defaults to ${colors.green(Helper.expandHome(Utils.defaultHome()))}.`,
` THELOUNGE_HOME Path for all configuration files and folders. Defaults to ${colors.green(
Helper.expandHome(Utils.defaultHome())
)}.`,
"",
].forEach((e) => log.raw(e));
}
@ -32,8 +34,16 @@ class Utils {
console.log(); // eslint-disable-line no-console
log.warn(`Folder ${colors.bold.red(oldHome)} still exists.`);
log.warn(`In v3, we renamed the default configuration folder to ${colors.bold.green(".thelounge")} for consistency.`);
log.warn(`You might want to rename the folder from ${colors.bold.red(".lounge")} to ${colors.bold.green(".thelounge")} to keep existing configuration.`);
log.warn(
`In v3, we renamed the default configuration folder to ${colors.bold.green(
".thelounge"
)} for consistency.`
);
log.warn(
`You might want to rename the folder from ${colors.bold.red(
".lounge"
)} to ${colors.bold.green(".thelounge")} to keep existing configuration.`
);
log.warn("Make sure to look at the release notes to see other breaking changes.");
console.log(); // eslint-disable-line no-console
}
@ -43,12 +53,7 @@ class Utils {
return home;
}
const distConfig = path.resolve(path.join(
__dirname,
"..",
"..",
".thelounge_home"
));
const distConfig = path.resolve(path.join(__dirname, "..", "..", ".thelounge_home"));
home = fs.readFileSync(distConfig, "utf-8").trim();
@ -71,9 +76,11 @@ class Utils {
return undefined;
} else if (value === "null") {
return null;
} else if (/^-?[0-9]+$/.test(value)) { // Numbers like port
} else if (/^-?[0-9]+$/.test(value)) {
// Numbers like port
value = parseInt(value, 10);
} else if (/^\[.*\]$/.test(value)) { // Arrays
} else if (/^\[.*\]$/.test(value)) {
// Arrays
// Supporting arrays `[a,b]` and `[a, b]`
const array = value.slice(1, -1).split(/,\s*/);
@ -134,23 +141,29 @@ class Utils {
]);
add.stdout.on("data", (data) => {
data.toString().trim().split("\n").forEach((line) => {
line = JSON.parse(line);
data.toString()
.trim()
.split("\n")
.forEach((line) => {
line = JSON.parse(line);
if (line.type === "success") {
success = true;
}
});
if (line.type === "success") {
success = true;
}
});
});
add.stderr.on("data", (data) => {
data.toString().trim().split("\n").forEach((line) => {
const json = JSON.parse(line);
data.toString()
.trim()
.split("\n")
.forEach((line) => {
const json = JSON.parse(line);
if (json.type === "error") {
log.error(json.data);
}
});
if (json.type === "error") {
log.error(json.data);
}
});
});
add.on("error", (e) => {

View file

@ -50,12 +50,7 @@ const Helper = {
module.exports = Helper;
Helper.config = require(path.resolve(path.join(
__dirname,
"..",
"defaults",
"config.js"
)));
Helper.config = require(path.resolve(path.join(__dirname, "..", "defaults", "config.js")));
function getVersion() {
const gitCommit = getGitCommit();
@ -92,7 +87,10 @@ function getGitCommit() {
}
function getVersionCacheBust() {
const hash = crypto.createHash("sha256").update(Helper.getVersion()).digest("hex");
const hash = crypto
.createHash("sha256")
.update(Helper.getVersion())
.digest("hex");
return hash.substring(0, 10);
}
@ -111,8 +109,16 @@ function setHome(newPath) {
const userConfig = require(configPath);
if (_.isEmpty(userConfig)) {
log.warn(`The file located at ${colors.green(configPath)} does not appear to expose anything.`);
log.warn(`Make sure it is non-empty and the configuration is exported using ${colors.bold("module.exports = { ... }")}.`);
log.warn(
`The file located at ${colors.green(
configPath
)} does not appear to expose anything.`
);
log.warn(
`Make sure it is non-empty and the configuration is exported using ${colors.bold(
"module.exports = { ... }"
)}.`
);
log.warn("Using default configuration...");
}
@ -122,14 +128,24 @@ function setHome(newPath) {
if (!this.config.displayNetwork && !this.config.lockNetwork) {
this.config.lockNetwork = true;
log.warn(`${colors.bold("displayNetwork")} and ${colors.bold("lockNetwork")} are false, setting ${colors.bold("lockNetwork")} to true.`);
log.warn(
`${colors.bold("displayNetwork")} and ${colors.bold(
"lockNetwork"
)} are false, setting ${colors.bold("lockNetwork")} to true.`
);
}
const manifestPath = path.resolve(path.join(__dirname, "..", "public", "thelounge.webmanifest"));
const manifestPath = path.resolve(
path.join(__dirname, "..", "public", "thelounge.webmanifest")
);
// Check if manifest exists, if not, the app most likely was not built
if (!fs.existsSync(manifestPath)) {
log.error(`The client application was not built. Run ${colors.bold("NODE_ENV=production yarn build")} to resolve this.`);
log.error(
`The client application was not built. Run ${colors.bold(
"NODE_ENV=production yarn build"
)} to resolve this.`
);
process.exit(1);
}
@ -140,13 +156,27 @@ function setHome(newPath) {
// TODO: Remove in future release
if (["example", "crypto", "zenburn"].includes(this.config.theme)) {
if (this.config.theme === "example") {
log.warn(`The default theme ${colors.red("example")} was renamed to ${colors.green("default")} as of The Lounge v3.`);
log.warn(
`The default theme ${colors.red("example")} was renamed to ${colors.green(
"default"
)} as of The Lounge v3.`
);
} else {
log.warn(`The theme ${colors.red(this.config.theme)} was moved to a separate theme as of The Lounge v3.`);
log.warn(`Install it with ${colors.bold("thelounge install thelounge-theme-" + this.config.theme)}.`);
log.warn(
`The theme ${colors.red(
this.config.theme
)} was moved to a separate theme as of The Lounge v3.`
);
log.warn(
`Install it with ${colors.bold(
"thelounge install thelounge-theme-" + this.config.theme
)}.`
);
}
log.warn(`Falling back to theme ${colors.green("default")} will be removed in a future release.`);
log.warn(
`Falling back to theme ${colors.green("default")} will be removed in a future release.`
);
log.warn("Please update your configuration file accordingly.");
this.config.theme = "default";
@ -195,15 +225,18 @@ function ip2hex(address) {
return "00000000";
}
return address.split(".").map(function(octet) {
let hex = parseInt(octet, 10).toString(16);
return address
.split(".")
.map(function(octet) {
let hex = parseInt(octet, 10).toString(16);
if (hex.length === 1) {
hex = "0" + hex;
}
if (hex.length === 1) {
hex = "0" + hex;
}
return hex;
}).join("");
return hex;
})
.join("");
}
// Expand ~ into the current user home dir.
@ -246,7 +279,11 @@ function mergeConfig(oldConfig, newConfig) {
return _.mergeWith(oldConfig, newConfig, (objValue, srcValue, key) => {
// Do not override config variables if the type is incorrect (e.g. object changed into a string)
if (typeof objValue !== "undefined" && objValue !== null && typeof objValue !== typeof srcValue) {
if (
typeof objValue !== "undefined" &&
objValue !== null &&
typeof objValue !== typeof srcValue
) {
log.warn(`Incorrect type for "${colors.bold(key)}", please verify your config.`);
return objValue;
@ -296,5 +333,9 @@ function parseHostmask(hostmask) {
}
function compareHostmask(a, b) {
return (a.nick.toLowerCase() === b.nick.toLowerCase() || a.nick === "*") && (a.ident.toLowerCase() === b.ident.toLowerCase() || a.ident === "*") && (a.hostname.toLowerCase() === b.hostname.toLowerCase() || a.hostname === "*");
return (
(a.nick.toLowerCase() === b.nick.toLowerCase() || a.nick === "*") &&
(a.ident.toLowerCase() === b.ident.toLowerCase() || a.ident === "*") &&
(a.hostname.toLowerCase() === b.hostname.toLowerCase() || a.hostname === "*")
);
}

View file

@ -20,22 +20,31 @@ class Identification {
if (Helper.config.identd.enable) {
if (this.oidentdFile) {
log.warn("Using both identd and oidentd at the same time, this is most likely not intended.");
log.warn(
"Using both identd and oidentd at the same time, this is most likely not intended."
);
}
const server = net.createServer(this.serverConnection.bind(this));
server.on("error", (err) => log.error(`Identd server error: ${err}`));
server.listen({
port: Helper.config.identd.port || 113,
host: Helper.config.bind,
}, () => {
const address = server.address();
log.info(`Identd server available on ${colors.green(address.address + ":" + address.port)}`);
server.listen(
{
port: Helper.config.identd.port || 113,
host: Helper.config.bind,
},
() => {
const address = server.address();
log.info(
`Identd server available on ${colors.green(
address.address + ":" + address.port
)}`
);
startedCallback(this);
});
startedCallback(this);
}
);
} else {
startedCallback(this);
}
@ -61,7 +70,9 @@ class Identification {
for (const connection of this.connections.values()) {
if (connection.socket.remotePort === fport && connection.socket.localPort === lport) {
return socket.write(`${lport}, ${fport} : USERID : TheLounge : ${connection.user}\r\n`);
return socket.write(
`${lport}, ${fport} : USERID : TheLounge : ${connection.user}\r\n`
);
}
}
@ -92,9 +103,10 @@ class Identification {
let file = "# Warning: file generated by The Lounge: changes will be overwritten!\n";
this.connections.forEach((connection) => {
file += `fport ${connection.socket.remotePort}`
+ ` lport ${connection.socket.localPort}`
+ ` { reply "${connection.user}" }\n`;
file +=
`fport ${connection.socket.remotePort}` +
` lport ${connection.socket.localPort}` +
` { reply "${connection.user}" }\n`;
});
fs.writeFile(this.oidentdFile, file, {flag: "w+"}, function(err) {

View file

@ -4,7 +4,10 @@ const colors = require("chalk");
const read = require("read");
function timestamp() {
const datetime = (new Date()).toISOString().split(".")[0].replace("T", " ");
const datetime = new Date()
.toISOString()
.split(".")[0]
.replace("T", " ");
return colors.dim(datetime);
}

View file

@ -176,14 +176,13 @@ Chan.prototype.getFilteredClone = function(lastActiveChannel, lastMessage) {
if (lastMessage > -1) {
// When reconnecting, always send up to 100 messages to prevent message gaps on the client
// See https://github.com/thelounge/thelounge/issues/1883
newChannel[prop] = this[prop]
.filter((m) => m.id > lastMessage)
.slice(-100);
newChannel[prop] = this[prop].filter((m) => m.id > lastMessage).slice(-100);
newChannel.moreHistoryAvailable = this[prop].length > 100;
} else {
// If channel is active, send up to 100 last messages, for all others send just 1
// Client will automatically load more messages whenever needed based on last seen messages
const messagesToSend = lastActiveChannel === true || this.id === lastActiveChannel ? 100 : 1;
const messagesToSend =
lastActiveChannel === true || this.id === lastActiveChannel ? 100 : 1;
newChannel[prop] = this[prop].slice(-messagesToSend);
newChannel.moreHistoryAvailable = this[prop].length > messagesToSend;

View file

@ -42,12 +42,14 @@ class Msg {
return !!this.from.nick;
}
return this.type !== Msg.Type.MOTD &&
return (
this.type !== Msg.Type.MOTD &&
this.type !== Msg.Type.ERROR &&
this.type !== Msg.Type.TOPIC_SET_BY &&
this.type !== Msg.Type.MODE_CHANNEL &&
this.type !== Msg.Type.RAW &&
this.type !== Msg.Type.WHOIS;
this.type !== Msg.Type.WHOIS
);
}
}

View file

@ -89,11 +89,20 @@ Network.prototype.validate = function(client) {
if (Helper.config.lockNetwork) {
// This check is needed to prevent invalid user configurations
if (!Helper.config.public && this.host && this.host.length > 0 && this.host !== Helper.config.defaults.host) {
this.channels[0].pushMessage(client, new Msg({
type: Msg.Type.ERROR,
text: "Hostname you specified is not allowed.",
}), true);
if (
!Helper.config.public &&
this.host &&
this.host.length > 0 &&
this.host !== Helper.config.defaults.host
) {
this.channels[0].pushMessage(
client,
new Msg({
type: Msg.Type.ERROR,
text: "Hostname you specified is not allowed.",
}),
true
);
return false;
}
@ -105,10 +114,14 @@ Network.prototype.validate = function(client) {
}
if (this.host.length === 0) {
this.channels[0].pushMessage(client, new Msg({
type: Msg.Type.ERROR,
text: "You must specify a hostname to connect.",
}), true);
this.channels[0].pushMessage(
client,
new Msg({
type: Msg.Type.ERROR,
text: "You must specify a hostname to connect.",
}),
true
);
return false;
}
@ -149,7 +162,10 @@ Network.prototype.createIrcFramework = function(client) {
};
Network.prototype.createWebIrc = function(client) {
if (!Helper.config.webirc || !Object.prototype.hasOwnProperty.call(Helper.config.webirc, this.host)) {
if (
!Helper.config.webirc ||
!Object.prototype.hasOwnProperty.call(Helper.config.webirc, this.host)
) {
return null;
}
@ -213,7 +229,11 @@ Network.prototype.edit = function(client, args) {
}
}
if (connected && this.realname !== oldRealname && this.irc.network.cap.isEnabled("draft/setname")) {
if (
connected &&
this.realname !== oldRealname &&
this.irc.network.cap.isEnabled("draft/setname")
) {
this.irc.raw("SETNAME", this.realname);
}
@ -241,12 +261,10 @@ Network.prototype.setNick = function(nick) {
this.highlightRegex = new RegExp(
// Do not match characters and numbers (unless IRC color)
"(?:^|[^a-z0-9]|\x03[0-9]{1,2})" +
// Escape nickname, as it may contain regex stuff
_.escapeRegExp(nick) +
// Do not match characters and numbers
"(?:[^a-z0-9]|$)",
// Escape nickname, as it may contain regex stuff
_.escapeRegExp(nick) +
// Do not match characters and numbers
"(?:[^a-z0-9]|$)",
// Case insensitive search
"i"
@ -266,7 +284,9 @@ Network.prototype.getFilteredClone = function(lastActiveChannel, lastMessage) {
const filteredNetwork = Object.keys(this).reduce((newNetwork, prop) => {
if (prop === "channels") {
// Channels objects perform their own cloning
newNetwork[prop] = this[prop].map((channel) => channel.getFilteredClone(lastActiveChannel, lastMessage));
newNetwork[prop] = this[prop].map((channel) =>
channel.getFilteredClone(lastActiveChannel, lastMessage)
);
} else if (!filteredFromClient[prop]) {
// Some properties that are not useful for the client are skipped
newNetwork[prop] = this[prop];
@ -311,8 +331,10 @@ Network.prototype.addChannel = function(newChan) {
const compareChan = this.channels[i];
// Negative if the new chan is alphabetically before the next chan in the list, positive if after
if (newChan.name.localeCompare(compareChan.name, {sensitivity: "base"}) <= 0
|| (compareChan.type !== Chan.Type.CHANNEL && compareChan.type !== Chan.Type.QUERY)) {
if (
newChan.name.localeCompare(compareChan.name, {sensitivity: "base"}) <= 0 ||
(compareChan.type !== Chan.Type.CHANNEL && compareChan.type !== Chan.Type.QUERY)
) {
index = i;
break;
}

View file

@ -92,7 +92,9 @@ function advancedLdapAuth(user, password, callback) {
res.on("searchEntry", function(entry) {
found = true;
const bindDN = entry.objectName;
log.info(`Auth against LDAP ${config.ldap.url} with found bindDN ${bindDN}`);
log.info(
`Auth against LDAP ${config.ldap.url} with found bindDN ${bindDN}`
);
ldapclient.unbind();
ldapAuthCommon(user, bindDN, password, callback);
@ -105,7 +107,9 @@ function advancedLdapAuth(user, password, callback) {
ldapclient.unbind();
if (!found) {
log.warn(`LDAP Search did not find anything for: ${userDN} (${result.status})`);
log.warn(
`LDAP Search did not find anything for: ${userDN} (${result.status})`
);
callback(false);
}
});

View file

@ -13,7 +13,11 @@ function localAuth(manager, client, user, password, callback) {
// 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)`);
log.error(
`User ${colors.bold(
user
)} with no local password set tried to sign in. (Probably a LDAP user)`
);
return callback(false);
}
@ -25,13 +29,18 @@ function localAuth(manager, client, user, password, callback) {
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`);
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) => {
})
.catch((error) => {
log.error(`Error while checking users password. Error: ${error}`);
});
}
@ -40,4 +49,3 @@ module.exports = {
auth: localAuth,
isEnabled: () => true,
};

View file

@ -25,7 +25,7 @@ async function fetch() {
try {
const response = await got("https://api.github.com/repos/thelounge/thelounge/releases", {
headers: {
"Accept": "application/vnd.github.v3.html", // Request rendered markdown
Accept: "application/vnd.github.v3.html", // Request rendered markdown
"User-Agent": pkg.name + "; +" + pkg.repository.git, // Identify the client
},
});

View file

@ -7,10 +7,13 @@ exports.commands = ["slap", "me"];
exports.input = function({irc}, chan, cmd, args) {
if (chan.type !== Chan.Type.CHANNEL && chan.type !== Chan.Type.QUERY) {
chan.pushMessage(this, new Msg({
type: Msg.Type.ERROR,
text: `${cmd} command can only be used in channels and queries.`,
}));
chan.pushMessage(
this,
new Msg({
type: Msg.Type.ERROR,
text: `${cmd} command can only be used in channels and queries.`,
})
);
return;
}
@ -18,27 +21,27 @@ exports.input = function({irc}, chan, cmd, args) {
let text;
switch (cmd) {
case "slap":
text = "slaps " + args[0] + " around a bit with a large trout";
case "slap":
text = "slaps " + args[0] + " around a bit with a large trout";
/* fall through */
case "me":
if (args.length === 0) {
case "me":
if (args.length === 0) {
break;
}
text = text || args.join(" ");
irc.action(chan.name, text);
if (!irc.network.cap.isEnabled("echo-message")) {
irc.emit("action", {
nick: irc.user.nick,
target: chan.name,
message: text,
});
}
break;
}
text = text || args.join(" ");
irc.action(chan.name, text);
if (!irc.network.cap.isEnabled("echo-message")) {
irc.emit("action", {
nick: irc.user.nick,
target: chan.name,
message: text,
});
}
break;
}
return true;

View file

@ -9,7 +9,8 @@ exports.input = function(network, chan, cmd, args) {
reason = args.join(" ") || " ";
network.irc.raw("AWAY", reason);
} else { // back command
} else {
// back command
network.irc.raw("AWAY");
}

View file

@ -3,42 +3,44 @@
const Chan = require("../../models/chan");
const Msg = require("../../models/msg");
exports.commands = [
"ban",
"unban",
"banlist",
];
exports.commands = ["ban", "unban", "banlist"];
exports.input = function({irc}, chan, cmd, args) {
if (chan.type !== Chan.Type.CHANNEL) {
chan.pushMessage(this, new Msg({
type: Msg.Type.ERROR,
text: `${cmd} command can only be used in channels.`,
}));
chan.pushMessage(
this,
new Msg({
type: Msg.Type.ERROR,
text: `${cmd} command can only be used in channels.`,
})
);
return;
}
if (cmd !== "banlist" && args.length === 0) {
if (args.length === 0) {
chan.pushMessage(this, new Msg({
type: Msg.Type.ERROR,
text: `Usage: /${cmd} <nick>`,
}));
chan.pushMessage(
this,
new Msg({
type: Msg.Type.ERROR,
text: `Usage: /${cmd} <nick>`,
})
);
return;
}
}
switch (cmd) {
case "ban":
irc.ban(chan.name, args[0]);
break;
case "unban":
irc.unban(chan.name, args[0]);
break;
case "banlist":
irc.banlist(chan.name);
break;
case "ban":
irc.ban(chan.name, args[0]);
break;
case "unban":
irc.unban(chan.name, args[0]);
break;
case "banlist":
irc.banlist(chan.name);
break;
}
};

View file

@ -17,10 +17,13 @@ exports.input = function(network, chan, cmd, args) {
}
if (irc.connection && irc.connection.connected) {
chan.pushMessage(this, new Msg({
type: Msg.Type.ERROR,
text: "You are already connected.",
}));
chan.pushMessage(
this,
new Msg({
type: Msg.Type.ERROR,
text: "You are already connected.",
})
);
return;
}

Some files were not shown because too many files have changed in this diff Show more