Format js/vue with prettier

This commit is contained in:
Alistair McKinlay 2019-07-17 10:33:59 +01:00
parent 48eeb11391
commit 133e7bf710
148 changed files with 4775 additions and 3855 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,5 @@
<template> <template>
<form <form id="form" method="post" action="" @submit.prevent="onSubmit">
id="form"
method="post"
action=""
@submit.prevent="onSubmit"
>
<span id="nick">{{ network.nick }}</span> <span id="nick">{{ network.nick }}</span>
<textarea <textarea
id="input" id="input"
@ -23,12 +18,7 @@
aria-label="Upload file" aria-label="Upload file"
@click="openFileUpload" @click="openFileUpload"
> >
<input <input id="upload-input" ref="uploadInput" type="file" multiple />
id="upload-input"
ref="uploadInput"
type="file"
multiple
>
<button <button
id="upload" id="upload"
type="button" type="button"
@ -80,7 +70,7 @@ const bracketWraps = {
"*": "*", "*": "*",
"`": "`", "`": "`",
"~": "~", "~": "~",
"_": "_", _: "_",
}; };
export default { export default {
@ -130,7 +120,9 @@ export default {
} }
if (this.channel.inputHistoryPosition === 0) { if (this.channel.inputHistoryPosition === 0) {
this.channel.inputHistory[this.channel.inputHistoryPosition] = this.channel.pendingMessage; this.channel.inputHistory[
this.channel.inputHistoryPosition
] = this.channel.pendingMessage;
} }
if (key === "up") { if (key === "up") {
@ -141,7 +133,9 @@ export default {
this.channel.inputHistoryPosition--; this.channel.inputHistoryPosition--;
} }
this.channel.pendingMessage = this.$refs.input.value = this.channel.inputHistory[this.channel.inputHistoryPosition]; this.channel.pendingMessage = this.$refs.input.value = this.channel.inputHistory[
this.channel.inputHistoryPosition
];
this.setInputSize(); this.setInputSize();
return false; return false;
@ -173,7 +167,8 @@ export default {
// Use scrollHeight to calculate how many lines there are in input, and ceil the value // 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 // because some browsers tend to incorrently round the values when using high density
// displays or using page zoom feature // 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) { getInputPlaceholder(channel) {
@ -219,7 +214,10 @@ export default {
const args = text.substr(1).split(" "); const args = text.substr(1).split(" ");
const cmd = args.shift().toLowerCase(); 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; return false;
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,14 +1,11 @@
<template> <template>
<div :class="[ 'msg', 'condensed', { closed: isCollapsed } ]"> <div :class="['msg', 'condensed', {closed: isCollapsed}]">
<div class="condensed-summary"> <div class="condensed-summary">
<span class="time" /> <span class="time" />
<span class="from" /> <span class="from" />
<span <span class="content" @click="onCollapseClick"
class="content" >{{ condensedText
@click="onCollapseClick" }}<button class="toggle-button" aria-label="Toggle status messages"
>{{ condensedText }}<button
class="toggle-button"
aria-label="Toggle status messages"
/></span> /></span>
</div> </div>
<Message <Message
@ -58,30 +55,60 @@ export default {
constants.condensedTypes.forEach((type) => { constants.condensedTypes.forEach((type) => {
if (obj[type]) { if (obj[type]) {
switch (type) { switch (type) {
case "away": case "away":
strings.push(obj[type] + (obj[type] > 1 ? " users have gone away" : " user has gone away")); strings.push(
break; obj[type] +
case "back": (obj[type] > 1
strings.push(obj[type] + (obj[type] > 1 ? " users have come back" : " user has come back")); ? " users have gone away"
break; : " user has gone away")
case "chghost": );
strings.push(obj[type] + (obj[type] > 1 ? " users have changed hostname" : " user has changed hostname")); break;
break; case "back":
case "join": strings.push(
strings.push(obj[type] + (obj[type] > 1 ? " users have joined" : " user has joined")); obj[type] +
break; (obj[type] > 1
case "part": ? " users have come back"
strings.push(obj[type] + (obj[type] > 1 ? " users have left" : " user has left")); : " user has come back")
break; );
case "nick": break;
strings.push(obj[type] + (obj[type] > 1 ? " users have changed nick" : " user has changed nick")); case "chghost":
break; strings.push(
case "kick": obj[type] +
strings.push(obj[type] + (obj[type] > 1 ? " users were kicked" : " user was kicked")); (obj[type] > 1
break; ? " users have changed hostname"
case "mode": : " user has changed hostname")
strings.push(obj[type] + (obj[type] > 1 ? " modes were set" : " mode was set")); );
break; 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> <template>
<div <div ref="chat" class="chat" tabindex="-1">
ref="chat" <div :class="['show-more', {show: channel.moreHistoryAvailable}]">
class="chat"
tabindex="-1"
>
<div :class="['show-more', { show: channel.moreHistoryAvailable }]">
<button <button
ref="loadMoreButton" ref="loadMoreButton"
:disabled="channel.historyLoading || !$root.isConnected" :disabled="channel.historyLoading || !$root.isConnected"
@ -85,7 +81,9 @@ export default {
// If actions are hidden, just return a message list with them excluded // If actions are hidden, just return a message list with them excluded
if (this.$root.settings.statusMessages === "hidden") { 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 // If actions are not condensed, just return raw message list
@ -99,7 +97,11 @@ export default {
for (const message of this.channel.messages) { for (const message of this.channel.messages) {
// If this message is not condensable, or its an action affecting our user, // 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 // 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; lastCondensedContainer = null;
condensed.push(message); condensed.push(message);
@ -199,7 +201,7 @@ export default {
return true; 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) { shouldDisplayUnreadMarker(id) {
if (!this.unreadMarkerShown && id > this.channel.firstUnread) { if (!this.unreadMarkerShown && id > this.channel.firstUnread) {

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@
<span class="content"> <span class="content">
<Username :user="message.from" /> <Username :user="message.from" />
sent a <abbr title="Client-to-client protocol">CTCP</abbr> request: 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> </span>
</template> </template>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,7 +12,9 @@ export default {
render(createElement, context) { render(createElement, context) {
return parse( return parse(
createElement, 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.message,
context.props.network context.props.network
); );

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
<template> <template>
<span <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" :data-name="user.original.nick"
role="button" role="button"
@mouseover="hover" @mouseover="hover"

View file

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

View file

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

View file

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

View file

@ -170,7 +170,9 @@ function addKickItem() {
} }
addContextMenuItem({ 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", className: "action-kick",
displayName: "Kick", displayName: "Kick",
data: (target) => target.attr("data-name"), data: (target) => target.attr("data-name"),

View file

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

View file

@ -13,5 +13,5 @@ module.exports = function(str) {
due to A being ascii 65 (100 0001) due to A being ascii 65 (100 0001)
while a being ascii 97 (110 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 // Return true if any section of "a" or "b" parts (defined by their start/end
// markers) intersect each other, false otherwise. // markers) intersect each other, false otherwise.
function anyIntersection(a, b) { function anyIntersection(a, b) {
return a.start <= b.start && b.start < a.end || return (
a.start < b.end && b.end <= a.end || (a.start <= b.start && b.start < a.end) ||
b.start <= a.start && a.start < b.end || (a.start < b.end && b.end <= a.end) ||
b.start < a.end && a.end <= b.end; (b.start <= a.start && a.start < b.end) ||
(b.start < a.end && a.end <= b.end)
);
} }
module.exports = anyIntersection; module.exports = anyIntersection;

View file

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

View file

@ -26,24 +26,20 @@ function sortParts(a, b) {
// fragments will contain duplicate styling attributes. // fragments will contain duplicate styling attributes.
function merge(textParts, styleFragments, cleanText) { function merge(textParts, styleFragments, cleanText) {
// Remove overlapping parts // Remove overlapping parts
textParts = textParts textParts = textParts.sort(sortParts).reduce((prev, curr) => {
.sort(sortParts) const intersection = prev.some((p) => anyIntersection(p, curr));
.reduce((prev, curr) => {
const intersection = prev.some((p) => anyIntersection(p, curr));
if (intersection) { if (intersection) {
return prev; return prev;
} }
return prev.concat([curr]); return prev.concat([curr]);
}, []); }, []);
// Every section of the original text that has not been captured in a "part" // 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 // is filled with "text" parts, dummy objects with start/end but no extra
// metadata. // metadata.
const allParts = textParts const allParts = textParts.concat(fill(textParts, cleanText)).sort(sortParts); // Sort all parts identified based on their position in the original text
.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 // Distribute the style fragments within the text parts
return allParts.map((textPart) => { 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 // At any given time, these carry style information since last time a styling
// control code was met. // 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 = () => { const resetStyle = () => {
bold = false; bold = false;
@ -90,96 +99,96 @@ function parseStyle(text) {
// encountered since the previous styling character. // encountered since the previous styling character.
while (position < text.length) { while (position < text.length) {
switch (text[position]) { switch (text[position]) {
case RESET: case RESET:
emitFragment(); emitFragment();
resetStyle(); resetStyle();
break; break;
// Meeting a BOLD character means that the ongoing text is either going to // 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 // be in bold or that the previous one was in bold and the following one
// must be reset. // must be reset.
// This same behavior applies to COLOR, REVERSE, ITALIC, and UNDERLINE. // This same behavior applies to COLOR, REVERSE, ITALIC, and UNDERLINE.
case BOLD: case BOLD:
emitFragment(); emitFragment();
bold = !bold; bold = !bold;
break; break;
case COLOR: case COLOR:
emitFragment(); emitFragment();
// Go one step further to find the corresponding color // Go one step further to find the corresponding color
colorCodes = text.slice(position + 1).match(colorRx); colorCodes = text.slice(position + 1).match(colorRx);
if (colorCodes) { if (colorCodes) {
textColor = Number(colorCodes[1]); textColor = Number(colorCodes[1]);
if (colorCodes[2]) { if (colorCodes[2]) {
bgColor = Number(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 break;
// 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; case HEX_COLOR:
emitFragment();
case HEX_COLOR: colorCodes = text.slice(position + 1).match(hexColorRx);
emitFragment();
colorCodes = text.slice(position + 1).match(hexColorRx); if (colorCodes) {
hexColor = colorCodes[1].toUpperCase();
if (colorCodes) { if (colorCodes[2]) {
hexColor = colorCodes[1].toUpperCase(); hexBgColor = colorCodes[2].toUpperCase();
}
if (colorCodes[2]) { // Color code length is > 1, so bump the current position cursor by as
hexBgColor = colorCodes[2].toUpperCase(); // 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 break;
// much (and reset the start cursor for the current text block as well)
position += colorCodes[0].length; case REVERSE: {
start = position + 1; emitFragment();
} else { const tmp = bgColor;
// If no color codes were found, toggles back to no colors (like BOLD). bgColor = textColor;
hexColor = undefined; textColor = tmp;
hexBgColor = undefined; break;
} }
break; case ITALIC:
emitFragment();
italic = !italic;
break;
case REVERSE: { case UNDERLINE:
emitFragment(); emitFragment();
const tmp = bgColor; underline = !underline;
bgColor = textColor; break;
textColor = tmp;
break;
}
case ITALIC: case STRIKETHROUGH:
emitFragment(); emitFragment();
italic = !italic; strikethrough = !strikethrough;
break; break;
case UNDERLINE: case MONOSPACE:
emitFragment(); emitFragment();
underline = !underline; monospace = !monospace;
break; break;
case STRIKETHROUGH:
emitFragment();
strikethrough = !strikethrough;
break;
case MONOSPACE:
emitFragment();
monospace = !monospace;
break;
} }
// Evaluate the next character at the next iteration // Evaluate the next character at the next iteration
@ -192,25 +201,37 @@ function parseStyle(text) {
return result; 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) { function prepare(text) {
return parseStyle(text) return (
// This optimizes fragments by combining them together when all their values parseStyle(text)
// for the properties defined above are equal. // This optimizes fragments by combining them together when all their values
.reduce((prev, curr) => { // for the properties defined above are equal.
if (prev.length) { .reduce((prev, curr) => {
const lastEntry = prev[prev.length - 1]; if (prev.length) {
const lastEntry = prev[prev.length - 1];
if (properties.every((key) => curr[key] === lastEntry[key])) { if (properties.every((key) => curr[key] === lastEntry[key])) {
lastEntry.text += curr.text; lastEntry.text += curr.text;
lastEntry.end += curr.text.length; lastEntry.end += curr.text.length;
return prev; return prev;
}
} }
}
return prev.concat([curr]); return prev.concat([curr]);
}, []); }, [])
);
} }
module.exports = prepare; module.exports = prepare;

View file

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

View file

@ -42,7 +42,8 @@
window.g_LoungeErrorHandler = function LoungeErrorHandler(e) { window.g_LoungeErrorHandler = function LoungeErrorHandler(e) {
var message = document.getElementById("loading-page-message"); 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"); var summary = document.createElement("summary");
summary.textContent = "More details"; summary.textContent = "More details";

View file

@ -69,7 +69,11 @@ window.vueMounted = () => {
}); });
viewport.on("click", "#chat .menu", function(e) { 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(); return contextMenuFactory.createContextMenu($(this), e).show();
}); });
@ -140,8 +144,7 @@ window.vueMounted = () => {
const lastActive = $("#windows > .active"); const lastActive = $("#windows > .active");
lastActive lastActive.removeClass("active");
.removeClass("active");
const chan = $(target) const chan = $(target)
.addClass("active") .addClass("active")

View file

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

View file

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

View file

@ -49,7 +49,7 @@ function onTouchStart(e) {
} }
function onTouchMove(e) { function onTouchMove(e) {
const touch = touchCurPos = e.touches.item(0); const touch = (touchCurPos = e.touches.item(0));
let distX = touch.screenX - touchStartPos.screenX; let distX = touch.screenX - touchStartPos.screenX;
const distY = touch.screenY - touchStartPos.screenY; const distY = touch.screenY - touchStartPos.screenY;
@ -93,7 +93,7 @@ function onTouchEnd() {
const diff = touchCurPos.screenX - touchStartPos.screenX; const diff = touchCurPos.screenX - touchStartPos.screenX;
const absDiff = Math.abs(diff); 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); SlideoutMenu.toggle(diff > 0);
} }

View file

@ -62,9 +62,12 @@ socket.on("auth", function(data) {
storage.remove("token"); storage.remove("token");
const error = login.find(".error"); const error = login.find(".error");
error.show().closest("form").one("submit", function() { error
error.hide(); .show()
}); .closest("form")
.one("submit", function() {
error.hide();
});
} else if (user) { } else if (user) {
token = storage.get("token"); 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 // When there is a button to refresh the checker available, display it when
// data is expired. Before that, server would return same information anyway. // data is expired. Before that, server would return same information anyway.
if (data.expiresAt) { if (data.expiresAt) {
setTimeout( setTimeout(() => $("#version-checker #check-now").show(), data.expiresAt - Date.now());
() => $("#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 // Given a status and latest release information, update the version checker
// (CSS class and content) // (CSS class and content)
function renderVersionChecker({status, latest}) { function renderVersionChecker({status, latest}) {
$("#version-checker").prop("class", status) $("#version-checker")
.prop("class", status)
.html(templates.version_checker({latest, 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 // When the network is locked, URL overrides should not affect disabled fields
if (data.lockNetwork && if (data.lockNetwork && ["host", "port", "tls", "rejectUnauthorized"].includes(key)) {
["host", "port", "tls", "rejectUnauthorized"].includes(key)) {
continue; continue;
} }
@ -198,20 +197,29 @@ function parseOverrideParams(params, data) {
} }
if (key === "join") { if (key === "join") {
value = value.split(",").map((chan) => { value = value
if (!chan.match(/^[#&!+]/)) { .split(",")
return `#${chan}`; .map((chan) => {
} if (!chan.match(/^[#&!+]/)) {
return `#${chan}`;
}
return chan; return chan;
}).join(", "); })
.join(", ");
} }
// Override server provided defaults with parameters passed in the URL if they match the data type // Override server provided defaults with parameters passed in the URL if they match the data type
switch (typeof data.defaults[key]) { switch (typeof data.defaults[key]) {
case "boolean": data.defaults[key] = value === "1" || value === "true"; break; case "boolean":
case "number": data.defaults[key] = Number(value); break; data.defaults[key] = value === "1" || value === "true";
case "string": data.defaults[key] = String(value); break; 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 // Channels require extra care to be merged correctly
if (key === "channels") { if (key === "channels") {
currentNetwork.channels = mergeChannelData(currentNetwork.channels, network.channels); currentNetwork.channels = mergeChannelData(
currentNetwork.channels,
network.channels
);
} else { } else {
currentNetwork[key] = network[key]; currentNetwork[key] = network[key];
} }

View file

@ -7,7 +7,8 @@ const {vueApp, initChannel} = require("../vue");
socket.on("join", function(data) { socket.on("join", function(data) {
initChannel(data.chan); 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); .channels.splice(data.index || -1, 0, data.chan);
// Queries do not automatically focus, unless the user did a whois // 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. // Display received notices and errors in currently active channel.
// Reloading the page will put them back into the lobby window. // 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 // 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; channel = vueApp.activeChannel.channel;
data.chan = channel.id; 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); const user = channel.users.find((u) => u.nick === data.msg.from.nick);
if (user) { 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 title;
let body; let body;

View file

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

View file

@ -23,7 +23,8 @@ socket.on("open", function(id) {
channel.channel.unread = 0; channel.channel.unread = 0;
if (channel.channel.messages.length > 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); const channel = findChannel(data.chan);
if (channel) { 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(); utils.synchronizeNotifiedState();

View file

@ -4,7 +4,11 @@ const socket = require("../socket");
const options = require("../options"); const options = require("../options");
function evaluateSetting(name, value) { 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); options.processSetting(name, value, true);
} else if (options.alwaysSync.includes(name)) { } else if (options.alwaysSync.includes(name)) {
options.processSetting(name, value, true); options.processSetting(name, value, true);

View file

@ -7,21 +7,21 @@ socket.on("sync_sort", function(data) {
const order = data.order; const order = data.order;
switch (data.type) { switch (data.type) {
case "networks": case "networks":
vueApp.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid)); vueApp.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
break; break;
case "channels": { case "channels": {
const network = vueApp.networks.find((n) => n.uuid === data.target); const network = vueApp.networks.find((n) => n.uuid === data.target);
if (!network) { if (!network) {
return; 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(); const xhr = new XMLHttpRequest();
this.xhr = xhr; this.xhr = xhr;
xhr.upload.addEventListener("progress", (e) => { xhr.upload.addEventListener(
const percent = Math.floor(e.loaded / e.total * 1000) / 10; "progress",
this.setProgress(percent); (e) => {
}, false); const percent = Math.floor((e.loaded / e.total) * 1000) / 10;
this.setProgress(percent);
},
false
);
xhr.onreadystatechange = () => { xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.readyState === XMLHttpRequest.DONE) {
@ -182,12 +186,12 @@ class Uploader {
} }
insertUploadUrl(url) { insertUploadUrl(url) {
const fullURL = (new URL(url, location)).toString(); const fullURL = new URL(url, location).toString();
const textbox = document.getElementById("input"); const textbox = document.getElementById("input");
const initStart = textbox.selectionStart; const initStart = textbox.selectionStart;
// Get the text before the cursor, and add a space if it's not in the beginning // 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 // Get the remaining text after the cursor
const cursorToTail = textbox.value.substr(initStart); const cursorToTail = textbox.value.substr(initStart);
@ -220,9 +224,9 @@ function initialize() {
} }
/** /**
* Called in the `configuration` socket event. * 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. * 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) { function setMaxFileSize(kb) {
instance.maxFileSize = kb; instance.maxFileSize = kb;
} }

View file

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

View file

@ -11,7 +11,9 @@ let applicationServerKey;
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker.addEventListener("message", (event) => { navigator.serviceWorker.addEventListener("message", (event) => {
if (event.data && event.data.type === "open") { 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(); $("#pushNotificationsHttps").hide();
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("service-worker.js").then((registration) => { navigator.serviceWorker
module.exports.hasServiceWorker = true; .register("service-worker.js")
.then((registration) => {
module.exports.hasServiceWorker = true;
if (!registration.pushManager) { if (!registration.pushManager) {
return; return;
}
return registration.pushManager.getSubscription().then((subscription) => {
$("#pushNotificationsUnsupported").hide();
pushNotificationsButton
.prop("disabled", false)
.on("click", onPushButton);
clientSubscribed = !!subscription;
if (clientSubscribed) {
alternatePushButton();
} }
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() { function onPushButton() {
pushNotificationsButton.prop("disabled", true); pushNotificationsButton.prop("disabled", true);
navigator.serviceWorker.ready.then((registration) => navigator.serviceWorker.ready
registration.pushManager.getSubscription().then((existingSubscription) => { .then((registration) =>
if (existingSubscription) { registration.pushManager
socket.emit("push:unregister"); .getSubscription()
.then((existingSubscription) => {
if (existingSubscription) {
socket.emit("push:unregister");
return existingSubscription.unsubscribe(); return existingSubscription.unsubscribe();
} }
return registration.pushManager.subscribe({ return registration.pushManager
applicationServerKey: urlBase64ToUint8Array(applicationServerKey), .subscribe({
userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(applicationServerKey),
}).then((subscription) => { userVisibleOnly: true,
const rawKey = subscription.getKey ? subscription.getKey("p256dh") : ""; })
const key = rawKey ? window.btoa(String.fromCharCode(...new Uint8Array(rawKey))) : ""; .then((subscription) => {
const rawAuthSecret = subscription.getKey ? subscription.getKey("auth") : ""; const rawKey = subscription.getKey ? subscription.getKey("p256dh") : "";
const authSecret = rawAuthSecret ? window.btoa(String.fromCharCode(...new Uint8Array(rawAuthSecret))) : ""; 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", { socket.emit("push:register", {
token: storage.get("token"), token: storage.get("token"),
endpoint: subscription.endpoint, endpoint: subscription.endpoint,
keys: { keys: {
p256dh: key, p256dh: key,
auth: authSecret, auth: authSecret,
}, },
}); });
return true; return true;
}); });
}).then((successful) => { })
if (successful) { .then((successful) => {
alternatePushButton().prop("disabled", false); if (successful) {
} alternatePushButton().prop("disabled", false);
}) }
).catch((err) => { })
$("#pushNotificationsUnsupported") )
.find("span").text(`An error has occurred: ${err}`).end() .catch((err) => {
.show(); $("#pushNotificationsUnsupported")
}); .find("span")
.text(`An error has occurred: ${err}`)
.end()
.show();
});
return false; return false;
} }
@ -127,10 +145,8 @@ function alternatePushButton() {
} }
function urlBase64ToUint8Array(base64String) { function urlBase64ToUint8Array(base64String) {
const padding = "=".repeat((4 - base64String.length % 4) % 4); const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding) const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
.replace(/-/g, "+")
.replace(/_/g, "/");
const rawData = window.atob(base64); const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length); const outputArray = new Uint8Array(rawData.length);
@ -143,5 +159,9 @@ function urlBase64ToUint8Array(base64String) {
} }
function isAllowedServiceWorkersHost() { 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) { self.addEventListener("activate", function(event) {
event.waitUntil(caches.keys().then((names) => Promise.all( event.waitUntil(
names caches
.filter((name) => name !== cacheName) .keys()
.map((name) => caches.delete(name)) .then((names) =>
))); Promise.all(
names.filter((name) => name !== cacheName).map((name) => caches.delete(name))
)
)
);
event.waitUntil(self.clients.claim()); event.waitUntil(self.clients.claim());
}); });
@ -50,9 +54,7 @@ async function putInCache(request, response) {
async function cleanRedirect(response) { async function cleanRedirect(response) {
// Not all browsers support the Response.body stream, so fall back // Not all browsers support the Response.body stream, so fall back
// to reading the entire body into memory as a blob. // to reading the entire body into memory as a blob.
const bodyPromise = "body" in response ? const bodyPromise = "body" in response ? Promise.resolve(response.body) : response.blob();
Promise.resolve(response.body) :
response.blob();
const body = await bodyPromise; const body = await bodyPromise;
@ -134,29 +136,33 @@ function showNotification(event, payload) {
self.addEventListener("notificationclick", function(event) { self.addEventListener("notificationclick", function(event) {
event.notification.close(); event.notification.close();
event.waitUntil(clients.matchAll({ event.waitUntil(
includeUncontrolled: true, clients
type: "window", .matchAll({
}).then((clientList) => { includeUncontrolled: true,
if (clientList.length === 0) { type: "window",
if (clients.openWindow) { })
return clients.openWindow(`.#${event.notification.tag}`); .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({ client.postMessage({
type: "open", type: "open",
channel: event.notification.tag, channel: event.notification.tag,
}); });
if ("focus" in client) { if ("focus" in client) {
client.focus(); client.focus();
} }
})); })
);
}); });
function findSuitableClient(clientList) { 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/ // Split path by folders, and create a new property if necessary/
// First 2 characters are "./"/ // First 2 characters are "./"/
// Last element in the array ends with `.tpl` and needs to be `require`d. // Last element in the array ends with `.tpl` and needs to be `require`d.
path.substr(2).split("/").forEach((key) => { path.substr(2)
if (key.endsWith(".tpl")) { // .split("/")
tmp[key.substr(0, key.length - 4)] = requireViews(path); .forEach((key) => {
} else { if (key.endsWith(".tpl")) {
tmp[key] = tmp[key] || {}; //
} tmp[key.substr(0, key.length - 4)] = requireViews(path);
} else {
tmp[key] = tmp[key] || {};
}
tmp = tmp[key]; tmp = tmp[key];
}); });
return acc; return acc;
}, {}); }, {});

View file

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

View file

@ -81,13 +81,21 @@ if (!version) {
} }
function isValidVersion(str) { 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)) { if (!isValidVersion(version)) {
log.error(`Argument ${colors.bold("version")} is incorrect It must be either:`); 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(
log.error(`- An explicit version of format ${colors.green("x.y.z")} (stable) or ${colors.green("x.y.z-(pre|rc).n")} (pre-release).`); `- 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); process.exit(1);
} }
@ -99,11 +107,15 @@ function prereleaseTemplate(items) {
[See the full changelog](${items.fullChangelogUrl}) [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. prereleaseType(items.version) === "rc"
Bugs may be fixed, but no further features will be added until the next stable version.` : ? `This is a release candidate (RC) for v${stableVersion(
items.version
`This is a pre-release for v${stableVersion(items.version)} to offer latest changes without having to wait for a stable release. )} 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.` 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 ` return `
## v${items.version} - ${items.date} ## 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. @@ @@ DESCRIPTION, ANNOUNCEMENT, ETC. @@
@ -138,8 +152,10 @@ For more details, [see the full changelog](${items.fullChangelogUrl}) and [miles
### Changed ### Changed
${isEmpty(items.dependencies) ? "" : ${
`- Update production dependencies to their latest versions: isEmpty(items.dependencies)
? ""
: `- Update production dependencies to their latest versions:
${printDependencyList(items.dependencies)}` ${printDependencyList(items.dependencies)}`
} }
@ -157,23 +173,31 @@ ${printList(items.security)}
### Documentation ### Documentation
${items.documentation.length === 0 ? "" : ${
`In the main repository: items.documentation.length === 0
? ""
: `In the main repository:
${printList(items.documentation)}` ${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)}` ${printList(items.websiteDocumentation)}`
} }
### Internals ### Internals
${printList(items.internals)}${isEmpty(items.devDependencies) ? "" : ` ${printList(items.internals)}${
isEmpty(items.devDependencies)
? ""
: `
- Update development dependencies to their latest versions: - Update development dependencies to their latest versions:
${printDependencyList(items.devDependencies)}`} ${printDependencyList(items.devDependencies)}`
}
@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@
@@ UNCATEGORIZED @@ @@ UNCATEGORIZED @@
@ -375,7 +399,9 @@ class RepositoryFetcher {
const prQuery = `query fetchPullRequests($repositoryName: String!) { const prQuery = `query fetchPullRequests($repositoryName: String!) {
repository(owner: "thelounge", name: $repositoryName) { repository(owner: "thelounge", name: $repositoryName) {
${numbers.map((number) => ` ${numbers
.map(
(number) => `
PR${number}: pullRequest(number: ${number}) { PR${number}: pullRequest(number: ${number}) {
__typename __typename
title title
@ -398,7 +424,9 @@ class RepositoryFetcher {
} }
} }
} }
`).join("")} `
)
.join("")}
} }
}`; }`;
const data = await this.fetch(prQuery); 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 // Builds a Markdown link for a given pull request or commit object
function printEntryLink(entry) { function printEntryLink(entry) {
const label = entry.__typename === "PullRequest" const label =
? `#${entry.number}` entry.__typename === "PullRequest" ? `#${entry.number}` : `\`${entry.abbreviatedOid}\``;
: `\`${entry.abbreviatedOid}\``;
return `[${label}](${entry.url})`; return `[${label}](${entry.url})`;
} }
@ -476,12 +503,16 @@ function printLine(entry) {
// Builds a Markdown list item for a given pull request // Builds a Markdown list item for a given pull request
function printPullRequest(pullRequest) { 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` // Builds a Markdown list item for a commit made directly in `master`
function printCommit(commit) { 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 // Builds a Markdown list of all given items
@ -543,20 +574,23 @@ function hasLabel(labels, expected) {
} }
function hasAnnotatedComment(comments, expected) { function hasAnnotatedComment(comments, expected) {
return comments && comments.nodes.some(({authorAssociation, body}) => return (
["OWNER", "MEMBER"].includes(authorAssociation) && comments &&
body.split("\r\n").includes(`[${expected}]`) comments.nodes.some(
({authorAssociation, body}) =>
["OWNER", "MEMBER"].includes(authorAssociation) &&
body.split("\r\n").includes(`[${expected}]`)
)
); );
} }
function isSkipped(entry) { function isSkipped(entry) {
return ( return (
(entry.__typename === "Commit" && ( (entry.__typename === "Commit" &&
// Version bump commits created by `yarn version` // Version bump commits created by `yarn version`
isValidVersion(entry.messageHeadline) || (isValidVersion(entry.messageHeadline) ||
// Commit message suggested by this script // Commit message suggested by this script
entry.messageHeadline.startsWith("Add changelog entry for v") entry.messageHeadline.startsWith("Add changelog entry for v"))) ||
)) ||
hasLabelOrAnnotatedComment(entry, "Meta: Skip Changelog") hasLabelOrAnnotatedComment(entry, "Meta: Skip Changelog")
); );
} }
@ -608,76 +642,85 @@ function extractPackages({title, url}) {
return []; return [];
} }
return extracted[1] return extracted[1].replace(/`/g, "").split(/, and |, | and /);
.replace(/`/g, "")
.split(/, and |, | and /);
} }
// Given an array of entries (PRs or commits), separates them into sections, // Given an array of entries (PRs or commits), separates them into sections,
// based on different information that describes them. // based on different information that describes them.
function parse(entries) { function parse(entries) {
return entries.reduce((result, entry) => { return entries.reduce(
let deps; (result, entry) => {
let deps;
if (isSkipped(entry)) { if (isSkipped(entry)) {
result.skipped.push(entry); result.skipped.push(entry);
} else if (isDependency(entry) && (deps = extractPackages(entry))) { } else if (isDependency(entry) && (deps = extractPackages(entry))) {
deps.forEach((packageName) => { deps.forEach((packageName) => {
const dependencyType = whichDependencyType(packageName); const dependencyType = whichDependencyType(packageName);
if (dependencyType) { if (dependencyType) {
if (!result[dependencyType][packageName]) { if (!result[dependencyType][packageName]) {
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 if (isDocumentation(entry)) {
} else { result.documentation.push(entry);
log.info(`${colors.bold(packageName)} was updated in ${colors.green("#" + entry.number)} then removed since last release. Skipping. ${colors.gray(entry.url)}`); } else if (isDeprecation(entry)) {
} result.deprecations.push(entry);
}); } else if (isSecurity(entry)) {
} else if (isDocumentation(entry)) { result.security.push(entry);
result.documentation.push(entry); } else if (isInternal(entry)) {
} else if (isDeprecation(entry)) { result.internals.push(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 { } 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; return result;
}, {
skipped: [],
dependencies: {},
devDependencies: {},
deprecations: [],
documentation: [],
internals: [],
security: [],
uncategorized: {
feature: [],
bug: [],
other: [],
}, },
unknownDependencies: new Set(), {
}); skipped: [],
dependencies: {},
devDependencies: {},
deprecations: [],
documentation: [],
internals: [],
security: [],
uncategorized: {
feature: [],
bug: [],
other: [],
},
unknownDependencies: new Set(),
}
);
} }
function dedupeEntries(changelog, items) { function dedupeEntries(changelog, items) {
const dedupe = (entries) => const dedupe = (entries) =>
entries.filter((entry) => !changelog.includes(printEntryLink(entry))); entries.filter((entry) => !changelog.includes(printEntryLink(entry)));
["deprecations", "documentation", "websiteDocumentation", "internals", "security"].forEach((type) => { ["deprecations", "documentation", "websiteDocumentation", "internals", "security"].forEach(
items[type] = dedupe(items[type]); (type) => {
}); items[type] = dedupe(items[type]);
}
);
["dependencies", "devDependencies", "uncategorized"].forEach((type) => { ["dependencies", "devDependencies", "uncategorized"].forEach((type) => {
Object.entries(items[type]).forEach(([name, entries]) => { Object.entries(items[type]).forEach(([name, entries]) => {
@ -692,8 +735,8 @@ function extractContributors(entries) {
const set = Object.values(entries).reduce((memo, {__typename, author}) => { const set = Object.values(entries).reduce((memo, {__typename, author}) => {
if (__typename === "PullRequest" && author.__typename !== "Bot") { if (__typename === "PullRequest" && author.__typename !== "Bot") {
memo.add("@" + author.login); memo.add("@" + author.login);
// Commit authors are *always* of type "User", so have to discriminate some // 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. // other way. Making the assumption of a suffix for now, see how that goes.
} else if (__typename === "Commit" && !author.user.login.endsWith("-bot")) { } else if (__typename === "Commit" && !author.user.login.endsWith("-bot")) {
memo.add("@" + author.user.login); memo.add("@" + author.user.login);
} }
@ -701,8 +744,7 @@ function extractContributors(entries) {
return memo; return memo;
}, new Set()); }, new Set());
return Array.from(set) return Array.from(set).sort((a, b) => a.localeCompare(b, "en", {sensitivity: "base"}));
.sort((a, b) => a.localeCompare(b, "en", {sensitivity: "base"}));
} }
const client = new GraphQLClient("https://api.github.com/graphql", { const client = new GraphQLClient("https://api.github.com/graphql", {
@ -727,13 +769,17 @@ async function generateChangelogEntry(changelog, targetVersion) {
} else { } else {
template = stableTemplate; template = stableTemplate;
const codeCommitsAndPullRequests = await codeRepo.fetchCommitsAndPullRequestsSince("v" + previousVersion); const codeCommitsAndPullRequests = await codeRepo.fetchCommitsAndPullRequestsSince(
"v" + previousVersion
);
items = parse(codeCommitsAndPullRequests); items = parse(codeCommitsAndPullRequests);
items.milestone = await codeRepo.fetchMilestone(targetVersion); items.milestone = await codeRepo.fetchMilestone(targetVersion);
const websiteRepo = new RepositoryFetcher(client, "thelounge.github.io"); const websiteRepo = new RepositoryFetcher(client, "thelounge.github.io");
const previousWebsiteVersion = await websiteRepo.fetchPreviousVersion(targetVersion); const previousWebsiteVersion = await websiteRepo.fetchPreviousVersion(targetVersion);
const websiteCommitsAndPullRequests = await websiteRepo.fetchCommitsAndPullRequestsSince("v" + previousWebsiteVersion); const websiteCommitsAndPullRequests = await websiteRepo.fetchCommitsAndPullRequestsSince(
"v" + previousWebsiteVersion
);
items.websiteDocumentation = websiteCommitsAndPullRequests; items.websiteDocumentation = websiteCommitsAndPullRequests;
contributors = extractContributors([ contributors = extractContributors([
@ -781,11 +827,18 @@ function addToChangelog(changelog, newEntry) {
const changelog = await readFile(changelogPath, "utf8"); const changelog = await readFile(changelogPath, "utf8");
try { try {
({changelogEntry, skipped, contributors} = await generateChangelogEntry(changelog, version)); ({changelogEntry, skipped, contributors} = await generateChangelogEntry(
changelog,
version
));
} catch (error) { } catch (error) {
if (error.response && error.response.status === 401) { if (error.response && error.response.status === 401) {
log.error(`GitHub returned an error: ${colors.red(error.response.message)}`); 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 { } else {
log.error(error); log.error(error);
} }
@ -806,10 +859,14 @@ function addToChangelog(changelog, newEntry) {
// Step 3 (optional): Print a list of skipped entries if there are any // Step 3 (optional): Print a list of skipped entries if there are any
if (skipped.length > 0) { 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:`); log.warn(`${skipped.length} ${skipped.length > 1 ? "entries were" : "entry was"} skipped:`);
skipped.forEach((entry) => { 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)) { if (isPrerelease(version)) {
log.info(`You can now run: ${colors.bold(commitCommand)}`); log.info(`You can now run: ${colors.bold(commitCommand)}`);
} else { } 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.`); 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"); const {spawnSync} = require("child_process");
function getGitUsername() { function getGitUsername() {
return spawnSync("git", ["config", "user.name"], {encoding: "utf8"}) return spawnSync("git", ["config", "user.name"], {encoding: "utf8"}).stdout.trim();
.stdout
.trim();
} }
const configContent = readFileSync( const configContent = readFileSync(join(__dirname, "..", "defaults", "config.js"), "utf8");
join(__dirname, "..", "defaults", "config.js"),
"utf8"
);
const docPath = join(process.argv[2], "_includes", "config.js.md"); const docPath = join(process.argv[2], "_includes", "config.js.md");
@ -42,7 +37,8 @@ const extractedDoc = configContent
} }
return acc; return acc;
}, []).join("\n"); }, [])
.join("\n");
const infoBlockHeader = `<!-- const infoBlockHeader = `<!--
DO NOT EDIT THIS FILE MANUALLY. DO NOT EDIT THIS FILE MANUALLY.
@ -62,10 +58,13 @@ writeFileSync(docPath, generatedContent);
log.info( log.info(
`${colors.bold(generatedContent.split("\n").length)} lines ` + `${colors.bold(generatedContent.split("\n").length)} lines ` +
`(${colors.bold(generatedContent.length)} characters) ` + `(${colors.bold(generatedContent.length)} characters) ` +
`were written in ${colors.green(docPath)}.` `were written in ${colors.green(docPath)}.`
); );
function getPrettyDate() { 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"); const fs = require("fs");
(async () => { (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 emojiStrategy = JSON.parse(response.body);
const emojiMap = {}; const emojiMap = {};
const fullNameEmojiMap = {}; const fullNameEmojiMap = {};
@ -21,21 +23,13 @@ const fs = require("fs");
const emojiMapOutput = JSON.stringify(emojiMap, null, 2) + "\n"; const emojiMapOutput = JSON.stringify(emojiMap, null, 2) + "\n";
const fullNameEmojiMapOutput = JSON.stringify(fullNameEmojiMap, null, 2) + "\n"; const fullNameEmojiMapOutput = JSON.stringify(fullNameEmojiMap, null, 2) + "\n";
fs.writeFileSync(path.resolve(path.join( fs.writeFileSync(
__dirname, path.resolve(path.join(__dirname, "..", "client", "js", "libs", "simplemap.json")),
"..", emojiMapOutput
"client", );
"js",
"libs",
"simplemap.json"
)), emojiMapOutput);
fs.writeFileSync(path.resolve(path.join( fs.writeFileSync(
__dirname, path.resolve(path.join(__dirname, "..", "client", "js", "libs", "fullnamemap.json")),
"..", fullNameEmojiMapOutput
"client", );
"js",
"libs",
"fullnamemap.json"
)), fullNameEmojiMapOutput);
})(); })();

View file

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

View file

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

View file

@ -8,7 +8,8 @@ const program = require("commander");
const Helper = require("../helper"); const Helper = require("../helper");
const Utils = require("./utils"); const Utils = require("./utils");
program.version(Helper.getVersion(), "-v, --version") program
.version(Helper.getVersion(), "-v, --version")
.option( .option(
"-c, --config <key=value>", "-c, --config <key=value>",
"override entries of the configuration file, must be specified for each entry that needs to be overriden", "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(); const uid = process.getuid();
if (uid === 0) { 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) => { fs.stat(path.join(Helper.getHomePath(), "config.js"), (err, stat) => {
if (!err && stat.uid !== uid) { if (!err && stat.uid !== uid) {
log.warn("Config file owner does not match the user you are currently running The Lounge as."); log.warn(
log.warn("To avoid issues, you should execute The Lounge commands under the same user."); "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, { packageJson(packageName, {
fullMetadata: true, fullMetadata: true,
version: packageVersion, version: packageVersion,
}).then((json) => { })
if (!("thelounge" in json)) { .then((json) => {
log.error(`${colors.red(json.name + " v" + json.version)} does not have The Lounge metadata.`); 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); 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())) { if (!fs.existsSync(Helper.getConfigPath())) {
fsextra.ensureDirSync(Helper.getHomePath()); fsextra.ensureDirSync(Helper.getHomePath());
fs.chmodSync(Helper.getHomePath(), "0700"); fs.chmodSync(Helper.getHomePath(), "0700");
fsextra.copySync(path.resolve(path.join( fsextra.copySync(
__dirname, path.resolve(path.join(__dirname, "..", "..", "defaults", "config.js")),
"..", Helper.getConfigPath()
"..", );
"defaults",
"config.js"
)), Helper.getConfigPath());
log.info(`Configuration file created at ${colors.green(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")); 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); log.warn(packageWasNotInstalled);
process.exit(1); process.exit(1);
} }
return Utils.executeYarnCommand( return Utils.executeYarnCommand("remove", packageName)
"remove", .then(() => {
packageName log.info(`${colors.green(packageName)} has been successfully uninstalled.`);
).then(() => { })
log.info(`${colors.green(packageName)} has been successfully uninstalled.`); .catch((code) => {
}).catch((code) => { log.error(`Failed to uninstall ${colors.green(packageName)}. Exit code: ${code}`);
log.error(`Failed to uninstall ${colors.green(packageName)}. Exit code: ${code}`); process.exit(1);
process.exit(1); });
});
}); });

View file

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

View file

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

View file

@ -21,7 +21,8 @@ program
const ClientManager = require("../../clientManager"); const ClientManager = require("../../clientManager");
const users = new ClientManager().getUsers(); const users = new ClientManager().getUsers();
if (users === undefined) { // There was an error, already logged if (users === undefined) {
// There was an error, already logged
return; return;
} }
@ -36,6 +37,10 @@ program
{stdio: "inherit"} {stdio: "inherit"}
); );
child_spawn.on("error", function() { 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 ClientManager = require("../../clientManager");
const users = new ClientManager().getUsers(); const users = new ClientManager().getUsers();
if (users === undefined) { // There was an error, already logged if (users === undefined) {
// There was an error, already logged
return; return;
} }
@ -30,6 +31,10 @@ program
log.info(`${i + 1}. ${colors.bold(user)}`); log.info(`${i + 1}. ${colors.bold(user)}`);
}); });
} else { } 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 ClientManager = require("../../clientManager");
const users = new ClientManager().getUsers(); const users = new ClientManager().getUsers();
if (users === undefined) { // There was an error, already logged if (users === undefined) {
// There was an error, already logged
return; return;
} }
@ -31,20 +32,20 @@ program
const file = Helper.getUserConfigPath(name); const file = Helper.getUserConfigPath(name);
const user = require(file); const user = require(file);
log.prompt({ log.prompt(
text: "Enter new password:", {
silent: true, text: "Enter new password:",
}, function(err, password) { silent: true,
if (err) { },
return; function(err, password) {
} if (err) {
return;
}
user.password = Helper.password.hash(password); user.password = Helper.password.hash(password);
user.sessions = {}; user.sessions = {};
fs.writeFileSync( fs.writeFileSync(file, JSON.stringify(user, null, "\t"));
file, log.info(`Successfully reset password for ${colors.bold(name)}.`);
JSON.stringify(user, null, "\t") }
); );
log.info(`Successfully reset password for ${colors.bold(name)}.`);
});
}); });

View file

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

View file

@ -50,12 +50,7 @@ const Helper = {
module.exports = Helper; module.exports = Helper;
Helper.config = require(path.resolve(path.join( Helper.config = require(path.resolve(path.join(__dirname, "..", "defaults", "config.js")));
__dirname,
"..",
"defaults",
"config.js"
)));
function getVersion() { function getVersion() {
const gitCommit = getGitCommit(); const gitCommit = getGitCommit();
@ -92,7 +87,10 @@ function getGitCommit() {
} }
function getVersionCacheBust() { 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); return hash.substring(0, 10);
} }
@ -111,8 +109,16 @@ function setHome(newPath) {
const userConfig = require(configPath); const userConfig = require(configPath);
if (_.isEmpty(userConfig)) { if (_.isEmpty(userConfig)) {
log.warn(`The file located at ${colors.green(configPath)} does not appear to expose anything.`); log.warn(
log.warn(`Make sure it is non-empty and the configuration is exported using ${colors.bold("module.exports = { ... }")}.`); `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..."); log.warn("Using default configuration...");
} }
@ -122,14 +128,24 @@ function setHome(newPath) {
if (!this.config.displayNetwork && !this.config.lockNetwork) { if (!this.config.displayNetwork && !this.config.lockNetwork) {
this.config.lockNetwork = true; 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 // Check if manifest exists, if not, the app most likely was not built
if (!fs.existsSync(manifestPath)) { 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); process.exit(1);
} }
@ -140,13 +156,27 @@ function setHome(newPath) {
// TODO: Remove in future release // TODO: Remove in future release
if (["example", "crypto", "zenburn"].includes(this.config.theme)) { if (["example", "crypto", "zenburn"].includes(this.config.theme)) {
if (this.config.theme === "example") { 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 { } else {
log.warn(`The theme ${colors.red(this.config.theme)} was moved to a separate theme as of The Lounge v3.`); log.warn(
log.warn(`Install it with ${colors.bold("thelounge install thelounge-theme-" + this.config.theme)}.`); `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."); log.warn("Please update your configuration file accordingly.");
this.config.theme = "default"; this.config.theme = "default";
@ -195,15 +225,18 @@ function ip2hex(address) {
return "00000000"; return "00000000";
} }
return address.split(".").map(function(octet) { return address
let hex = parseInt(octet, 10).toString(16); .split(".")
.map(function(octet) {
let hex = parseInt(octet, 10).toString(16);
if (hex.length === 1) { if (hex.length === 1) {
hex = "0" + hex; hex = "0" + hex;
} }
return hex; return hex;
}).join(""); })
.join("");
} }
// Expand ~ into the current user home dir. // Expand ~ into the current user home dir.
@ -246,7 +279,11 @@ function mergeConfig(oldConfig, newConfig) {
return _.mergeWith(oldConfig, newConfig, (objValue, srcValue, key) => { return _.mergeWith(oldConfig, newConfig, (objValue, srcValue, key) => {
// Do not override config variables if the type is incorrect (e.g. object changed into a string) // 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.`); log.warn(`Incorrect type for "${colors.bold(key)}", please verify your config.`);
return objValue; return objValue;
@ -296,5 +333,9 @@ function parseHostmask(hostmask) {
} }
function compareHostmask(a, b) { 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 (Helper.config.identd.enable) {
if (this.oidentdFile) { 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)); const server = net.createServer(this.serverConnection.bind(this));
server.on("error", (err) => log.error(`Identd server error: ${err}`)); server.on("error", (err) => log.error(`Identd server error: ${err}`));
server.listen({ server.listen(
port: Helper.config.identd.port || 113, {
host: Helper.config.bind, 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)}`); () => {
const address = server.address();
log.info(
`Identd server available on ${colors.green(
address.address + ":" + address.port
)}`
);
startedCallback(this); startedCallback(this);
}); }
);
} else { } else {
startedCallback(this); startedCallback(this);
} }
@ -61,7 +70,9 @@ class Identification {
for (const connection of this.connections.values()) { for (const connection of this.connections.values()) {
if (connection.socket.remotePort === fport && connection.socket.localPort === lport) { 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"; let file = "# Warning: file generated by The Lounge: changes will be overwritten!\n";
this.connections.forEach((connection) => { this.connections.forEach((connection) => {
file += `fport ${connection.socket.remotePort}` file +=
+ ` lport ${connection.socket.localPort}` `fport ${connection.socket.remotePort}` +
+ ` { reply "${connection.user}" }\n`; ` lport ${connection.socket.localPort}` +
` { reply "${connection.user}" }\n`;
}); });
fs.writeFile(this.oidentdFile, file, {flag: "w+"}, function(err) { fs.writeFile(this.oidentdFile, file, {flag: "w+"}, function(err) {

View file

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

View file

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

View file

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

View file

@ -92,7 +92,9 @@ function advancedLdapAuth(user, password, callback) {
res.on("searchEntry", function(entry) { res.on("searchEntry", function(entry) {
found = true; found = true;
const bindDN = entry.objectName; 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(); ldapclient.unbind();
ldapAuthCommon(user, bindDN, password, callback); ldapAuthCommon(user, bindDN, password, callback);
@ -105,7 +107,9 @@ function advancedLdapAuth(user, password, callback) {
ldapclient.unbind(); ldapclient.unbind();
if (!found) { 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); 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 this user has no password set, fail the authentication
if (!client.config.password) { 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); return callback(false);
} }
@ -25,13 +29,18 @@ function localAuth(manager, client, user, password, callback) {
client.setPassword(hash, (success) => { client.setPassword(hash, (success) => {
if (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); callback(matching);
}).catch((error) => { })
.catch((error) => {
log.error(`Error while checking users password. Error: ${error}`); log.error(`Error while checking users password. Error: ${error}`);
}); });
} }
@ -40,4 +49,3 @@ module.exports = {
auth: localAuth, auth: localAuth,
isEnabled: () => true, isEnabled: () => true,
}; };

View file

@ -25,7 +25,7 @@ async function fetch() {
try { try {
const response = await got("https://api.github.com/repos/thelounge/thelounge/releases", { const response = await got("https://api.github.com/repos/thelounge/thelounge/releases", {
headers: { 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 "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) { exports.input = function({irc}, chan, cmd, args) {
if (chan.type !== Chan.Type.CHANNEL && chan.type !== Chan.Type.QUERY) { if (chan.type !== Chan.Type.CHANNEL && chan.type !== Chan.Type.QUERY) {
chan.pushMessage(this, new Msg({ chan.pushMessage(
type: Msg.Type.ERROR, this,
text: `${cmd} command can only be used in channels and queries.`, new Msg({
})); type: Msg.Type.ERROR,
text: `${cmd} command can only be used in channels and queries.`,
})
);
return; return;
} }
@ -18,27 +21,27 @@ exports.input = function({irc}, chan, cmd, args) {
let text; let text;
switch (cmd) { switch (cmd) {
case "slap": case "slap":
text = "slaps " + args[0] + " around a bit with a large trout"; text = "slaps " + args[0] + " around a bit with a large trout";
/* fall through */ /* fall through */
case "me": case "me":
if (args.length === 0) { 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; 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; return true;

View file

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

View file

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

View file

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

View file

@ -6,18 +6,24 @@ exports.commands = ["ctcp"];
exports.input = function({irc}, chan, cmd, args) { exports.input = function({irc}, chan, cmd, args) {
if (args.length < 2) { if (args.length < 2) {
chan.pushMessage(this, new Msg({ chan.pushMessage(
type: Msg.Type.ERROR, this,
text: "Usage: /ctcp <nick> <ctcp_type>", new Msg({
})); type: Msg.Type.ERROR,
text: "Usage: /ctcp <nick> <ctcp_type>",
})
);
return; return;
} }
chan.pushMessage(this, new Msg({ chan.pushMessage(
type: Msg.Type.CTCP_REQUEST, this,
ctcpMessage: `"${args.slice(1).join(" ")}" to ${args[0]}`, new Msg({
from: chan.getUser(irc.user.nick), type: Msg.Type.CTCP_REQUEST,
})); ctcpMessage: `"${args.slice(1).join(" ")}" to ${args[0]}`,
from: chan.getUser(irc.user.nick),
})
);
irc.ctcpRequest(...args); irc.ctcpRequest(...args);
}; };

View file

@ -4,11 +4,7 @@ const Chan = require("../../models/chan");
const Msg = require("../../models/msg"); const Msg = require("../../models/msg");
const Helper = require("../../helper"); const Helper = require("../../helper");
exports.commands = [ exports.commands = ["ignore", "unignore", "ignorelist"];
"ignore",
"unignore",
"ignorelist",
];
exports.input = function(network, chan, cmd, args) { exports.input = function(network, chan, cmd, args) {
const client = this; const client = this;
@ -16,10 +12,13 @@ exports.input = function(network, chan, cmd, args) {
let hostmask; let hostmask;
if (cmd !== "ignorelist" && (args.length === 0 || args[0].trim().length === 0)) { if (cmd !== "ignorelist" && (args.length === 0 || args[0].trim().length === 0)) {
chan.pushMessage(client, new Msg({ chan.pushMessage(
type: Msg.Type.ERROR, client,
text: `Usage: /${cmd} <nick>[!ident][@host]`, new Msg({
})); type: Msg.Type.ERROR,
text: `Usage: /${cmd} <nick>[!ident][@host]`,
})
);
return; return;
} }
@ -31,95 +30,115 @@ exports.input = function(network, chan, cmd, args) {
} }
switch (cmd) { switch (cmd) {
case "ignore": { case "ignore": {
// IRC nicks are case insensitive // IRC nicks are case insensitive
if (hostmask.nick.toLowerCase() === network.nick.toLowerCase()) { if (hostmask.nick.toLowerCase() === network.nick.toLowerCase()) {
chan.pushMessage(client, new Msg({ chan.pushMessage(
type: Msg.Type.ERROR, client,
text: "You can't ignore yourself", new Msg({
})); type: Msg.Type.ERROR,
} else if (!network.ignoreList.some(function(entry) { text: "You can't ignore yourself",
return Helper.compareHostmask(entry, hostmask); })
})) { );
hostmask.when = Date.now(); } else if (
network.ignoreList.push(hostmask); !network.ignoreList.some(function(entry) {
return Helper.compareHostmask(entry, hostmask);
})
) {
hostmask.when = Date.now();
network.ignoreList.push(hostmask);
client.save(); client.save();
chan.pushMessage(client, new Msg({ chan.pushMessage(
type: Msg.Type.ERROR, client,
text: `\u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f added to ignorelist`, new Msg({
})); type: Msg.Type.ERROR,
} else { text: `\u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f added to ignorelist`,
chan.pushMessage(client, new Msg({ })
type: Msg.Type.ERROR, );
text: "The specified user/hostmask is already ignored",
}));
}
break;
}
case "unignore": {
const idx = network.ignoreList.findIndex(function(entry) {
return Helper.compareHostmask(entry, hostmask);
});
// Check if the entry exists before removing it, otherwise
// let the user know.
if (idx !== -1) {
network.ignoreList.splice(idx, 1);
client.save();
chan.pushMessage(client, new Msg({
type: Msg.Type.ERROR,
text: `Successfully removed \u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f from ignorelist`,
}));
} else {
chan.pushMessage(client, new Msg({
type: Msg.Type.ERROR,
text: "The specified user/hostmask is not ignored",
}));
}
break;
}
case "ignorelist":
if (network.ignoreList.length === 0) {
chan.pushMessage(client, new Msg({
type: Msg.Type.ERROR,
text: "Ignorelist is empty",
}));
} else {
const chanName = "Ignored users";
const ignored = network.ignoreList.map((data) => ({
hostmask: `${data.nick}!${data.ident}@${data.hostname}`,
when: data.when,
}));
let newChan = network.getChannel(chanName);
if (typeof newChan === "undefined") {
newChan = client.createChannel({
type: Chan.Type.SPECIAL,
special: Chan.SpecialType.IGNORELIST,
name: chanName,
data: ignored,
});
client.emit("join", {
network: network.uuid,
chan: newChan.getFilteredClone(true),
index: network.addChannel(newChan),
});
} else { } else {
newChan.data = ignored; chan.pushMessage(
client,
client.emit("msg:special", { new Msg({
chan: newChan.id, type: Msg.Type.ERROR,
data: ignored, text: "The specified user/hostmask is already ignored",
}); })
);
} }
break;
} }
break; case "unignore": {
const idx = network.ignoreList.findIndex(function(entry) {
return Helper.compareHostmask(entry, hostmask);
});
// Check if the entry exists before removing it, otherwise
// let the user know.
if (idx !== -1) {
network.ignoreList.splice(idx, 1);
client.save();
chan.pushMessage(
client,
new Msg({
type: Msg.Type.ERROR,
text: `Successfully removed \u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f from ignorelist`,
})
);
} else {
chan.pushMessage(
client,
new Msg({
type: Msg.Type.ERROR,
text: "The specified user/hostmask is not ignored",
})
);
}
break;
}
case "ignorelist":
if (network.ignoreList.length === 0) {
chan.pushMessage(
client,
new Msg({
type: Msg.Type.ERROR,
text: "Ignorelist is empty",
})
);
} else {
const chanName = "Ignored users";
const ignored = network.ignoreList.map((data) => ({
hostmask: `${data.nick}!${data.ident}@${data.hostname}`,
when: data.when,
}));
let newChan = network.getChannel(chanName);
if (typeof newChan === "undefined") {
newChan = client.createChannel({
type: Chan.Type.SPECIAL,
special: Chan.SpecialType.IGNORELIST,
name: chanName,
data: ignored,
});
client.emit("join", {
network: network.uuid,
chan: newChan.getFilteredClone(true),
index: network.addChannel(newChan),
});
} else {
newChan.data = ignored;
client.emit("msg:special", {
chan: newChan.id,
data: ignored,
});
}
}
break;
} }
}; };

View file

@ -3,10 +3,7 @@
const Chan = require("../../models/chan"); const Chan = require("../../models/chan");
const Msg = require("../../models/msg"); const Msg = require("../../models/msg");
exports.commands = [ exports.commands = ["invite", "invitelist"];
"invite",
"invitelist",
];
exports.input = function({irc}, chan, cmd, args) { exports.input = function({irc}, chan, cmd, args) {
if (cmd === "invitelist") { if (cmd === "invitelist") {
@ -16,12 +13,15 @@ exports.input = function({irc}, chan, cmd, args) {
if (args.length === 2) { if (args.length === 2) {
irc.raw("INVITE", args[0], args[1]); // Channel provided in the command irc.raw("INVITE", args[0], args[1]); // Channel provided in the command
} else if (args.length === 1 && chan.type === Chan.Type.CHANNEL) { } else if (args.length === 1 && chan.type === Chan.Type.CHANNEL) {
irc.raw("INVITE", args[0], chan.name); // Current channel irc.raw("INVITE", args[0], chan.name); // Current channel
} else { } else {
chan.pushMessage(this, new Msg({ chan.pushMessage(
type: Msg.Type.ERROR, this,
text: `${cmd} command can only be used in channels or by specifying a target.`, new Msg({
})); type: Msg.Type.ERROR,
text: `${cmd} command can only be used in channels or by specifying a target.`,
})
);
} }
}; };

View file

@ -7,10 +7,13 @@ exports.commands = ["kick"];
exports.input = function({irc}, chan, cmd, args) { exports.input = function({irc}, chan, cmd, args) {
if (chan.type !== Chan.Type.CHANNEL) { if (chan.type !== Chan.Type.CHANNEL) {
chan.pushMessage(this, new Msg({ chan.pushMessage(
type: Msg.Type.ERROR, this,
text: `${cmd} command can only be used in channels.`, new Msg({
})); type: Msg.Type.ERROR,
text: `${cmd} command can only be used in channels.`,
})
);
return; return;
} }

View file

@ -3,32 +3,30 @@
const Chan = require("../../models/chan"); const Chan = require("../../models/chan");
const Msg = require("../../models/msg"); const Msg = require("../../models/msg");
exports.commands = [ exports.commands = ["mode", "op", "deop", "hop", "dehop", "voice", "devoice"];
"mode",
"op",
"deop",
"hop",
"dehop",
"voice",
"devoice",
];
exports.input = function({irc, nick}, chan, cmd, args) { exports.input = function({irc, nick}, chan, cmd, args) {
if (cmd !== "mode") { if (cmd !== "mode") {
if (chan.type !== Chan.Type.CHANNEL) { if (chan.type !== Chan.Type.CHANNEL) {
chan.pushMessage(this, new Msg({ chan.pushMessage(
type: Msg.Type.ERROR, this,
text: `${cmd} command can only be used in channels.`, new Msg({
})); type: Msg.Type.ERROR,
text: `${cmd} command can only be used in channels.`,
})
);
return; return;
} }
if (args.length === 0) { if (args.length === 0) {
chan.pushMessage(this, new Msg({ chan.pushMessage(
type: Msg.Type.ERROR, this,
text: `Usage: /${cmd} <nick> [...nick]`, new Msg({
})); type: Msg.Type.ERROR,
text: `Usage: /${cmd} <nick> [...nick]`,
})
);
return; return;
} }
@ -50,7 +48,9 @@ exports.input = function({irc, nick}, chan, cmd, args) {
} }
if (args.length === 0 || args[0][0] === "+" || args[0][0] === "-") { if (args.length === 0 || args[0][0] === "+" || args[0][0] === "-") {
args.unshift(chan.type === Chan.Type.CHANNEL || chan.type === Chan.Type.QUERY ? chan.name : nick); args.unshift(
chan.type === Chan.Type.CHANNEL || chan.type === Chan.Type.QUERY ? chan.name : nick
);
} }
irc.raw("MODE", ...args); irc.raw("MODE", ...args);

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