mirror of
https://github.com/thelounge/thelounge
synced 2024-11-10 06:34:21 +00:00
Format js/vue with prettier
This commit is contained in:
parent
48eeb11391
commit
133e7bf710
148 changed files with 4775 additions and 3855 deletions
|
@ -1,8 +1,5 @@
|
|||
<template>
|
||||
<div
|
||||
id="viewport"
|
||||
role="tablist"
|
||||
>
|
||||
<div id="viewport" role="tablist">
|
||||
<aside id="sidebar">
|
||||
<div class="scrollable-area">
|
||||
<div class="logo-container">
|
||||
|
@ -10,62 +7,55 @@
|
|||
:src="`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg.svg`"
|
||||
class="logo"
|
||||
alt="The Lounge"
|
||||
>
|
||||
/>
|
||||
<img
|
||||
:src="`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg-inverted.svg`"
|
||||
:src="
|
||||
`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg-inverted.svg`
|
||||
"
|
||||
class="logo-inverted"
|
||||
alt="The Lounge"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
<NetworkList
|
||||
:networks="networks"
|
||||
:active-channel="activeChannel"
|
||||
/>
|
||||
<NetworkList :networks="networks" :active-channel="activeChannel" />
|
||||
</div>
|
||||
<footer id="footer">
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-touch"
|
||||
aria-label="Sign in"
|
||||
><button
|
||||
class="icon sign-in"
|
||||
data-target="#sign-in"
|
||||
aria-label="Sign in"
|
||||
role="tab"
|
||||
aria-controls="sign-in"
|
||||
aria-selected="false"
|
||||
<span class="tooltipped tooltipped-n tooltipped-no-touch" aria-label="Sign in"
|
||||
><button
|
||||
class="icon sign-in"
|
||||
data-target="#sign-in"
|
||||
aria-label="Sign in"
|
||||
role="tab"
|
||||
aria-controls="sign-in"
|
||||
aria-selected="false"
|
||||
/></span>
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-touch"
|
||||
aria-label="Connect to network"
|
||||
><button
|
||||
class="icon connect"
|
||||
data-target="#connect"
|
||||
aria-label="Connect to network"
|
||||
role="tab"
|
||||
aria-controls="connect"
|
||||
aria-selected="false"
|
||||
><button
|
||||
class="icon connect"
|
||||
data-target="#connect"
|
||||
aria-label="Connect to network"
|
||||
role="tab"
|
||||
aria-controls="connect"
|
||||
aria-selected="false"
|
||||
/></span>
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-touch"
|
||||
aria-label="Settings"
|
||||
><button
|
||||
class="icon settings"
|
||||
data-target="#settings"
|
||||
aria-label="Settings"
|
||||
role="tab"
|
||||
aria-controls="settings"
|
||||
aria-selected="false"
|
||||
<span class="tooltipped tooltipped-n tooltipped-no-touch" aria-label="Settings"
|
||||
><button
|
||||
class="icon settings"
|
||||
data-target="#settings"
|
||||
aria-label="Settings"
|
||||
role="tab"
|
||||
aria-controls="settings"
|
||||
aria-selected="false"
|
||||
/></span>
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-touch"
|
||||
aria-label="Help"
|
||||
><button
|
||||
class="icon help"
|
||||
data-target="#help"
|
||||
aria-label="Help"
|
||||
role="tab"
|
||||
aria-controls="help"
|
||||
aria-selected="false"
|
||||
<span class="tooltipped tooltipped-n tooltipped-no-touch" aria-label="Help"
|
||||
><button
|
||||
class="icon help"
|
||||
data-target="#help"
|
||||
aria-label="Help"
|
||||
role="tab"
|
||||
aria-controls="help"
|
||||
aria-selected="false"
|
||||
/></span>
|
||||
</footer>
|
||||
</aside>
|
||||
|
@ -76,35 +66,11 @@
|
|||
:network="activeChannel.network"
|
||||
:channel="activeChannel.channel"
|
||||
/>
|
||||
<div
|
||||
id="sign-in"
|
||||
class="window"
|
||||
role="tabpanel"
|
||||
aria-label="Sign-in"
|
||||
/>
|
||||
<div
|
||||
id="connect"
|
||||
class="window"
|
||||
role="tabpanel"
|
||||
aria-label="Connect"
|
||||
/>
|
||||
<div
|
||||
id="settings"
|
||||
class="window"
|
||||
role="tabpanel"
|
||||
aria-label="Settings"
|
||||
/>
|
||||
<div
|
||||
id="help"
|
||||
class="window"
|
||||
role="tabpanel"
|
||||
aria-label="Help"
|
||||
/>
|
||||
<div
|
||||
id="changelog"
|
||||
class="window"
|
||||
aria-label="Changelog"
|
||||
/>
|
||||
<div id="sign-in" class="window" role="tabpanel" aria-label="Sign-in" />
|
||||
<div id="connect" class="window" role="tabpanel" aria-label="Connect" />
|
||||
<div id="settings" class="window" role="tabpanel" aria-label="Settings" />
|
||||
<div id="help" class="window" role="tabpanel" aria-label="Help" />
|
||||
<div id="changelog" class="window" aria-label="Changelog" />
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
<template>
|
||||
<ChannelWrapper
|
||||
:network="network"
|
||||
:channel="channel"
|
||||
:active-channel="activeChannel"
|
||||
>
|
||||
<ChannelWrapper :network="network" :channel="channel" :active-channel="activeChannel">
|
||||
<span class="name">{{ channel.name }}</span>
|
||||
<span
|
||||
v-if="channel.unread"
|
||||
:class="{ highlight: channel.highlight }"
|
||||
class="badge"
|
||||
>{{ channel.unread | roundBadgeNumber }}</span>
|
||||
<span v-if="channel.unread" :class="{highlight: channel.highlight}" class="badge">{{
|
||||
channel.unread | roundBadgeNumber
|
||||
}}</span>
|
||||
<template v-if="channel.type === 'channel'">
|
||||
<span
|
||||
v-if="channel.state === 0"
|
||||
|
@ -18,25 +12,13 @@
|
|||
>
|
||||
<span class="parted-channel-icon" />
|
||||
</span>
|
||||
<span
|
||||
class="close-tooltip tooltipped tooltipped-w"
|
||||
aria-label="Leave"
|
||||
>
|
||||
<button
|
||||
class="close"
|
||||
aria-label="Leave"
|
||||
/>
|
||||
<span class="close-tooltip tooltipped tooltipped-w" aria-label="Leave">
|
||||
<button class="close" aria-label="Leave" />
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span
|
||||
class="close-tooltip tooltipped tooltipped-w"
|
||||
aria-label="Close"
|
||||
>
|
||||
<button
|
||||
class="close"
|
||||
aria-label="Close"
|
||||
/>
|
||||
<span class="close-tooltip tooltipped tooltipped-w" aria-label="Close">
|
||||
<button class="close" aria-label="Close" />
|
||||
</span>
|
||||
</template>
|
||||
</ChannelWrapper>
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="!network.isCollapsed || channel.highlight || channel.type === 'lobby' || (activeChannel && channel === activeChannel.channel)"
|
||||
v-if="
|
||||
!network.isCollapsed ||
|
||||
channel.highlight ||
|
||||
channel.type === 'lobby' ||
|
||||
(activeChannel && channel === activeChannel.channel)
|
||||
"
|
||||
:class="[
|
||||
'chan',
|
||||
channel.type,
|
||||
{ active: activeChannel && channel === activeChannel.channel },
|
||||
{ 'parted-channel': channel.type === 'channel' && channel.state === 0 }
|
||||
{active: activeChannel && channel === activeChannel.channel},
|
||||
{'parted-channel': channel.type === 'channel' && channel.state === 0},
|
||||
]"
|
||||
:aria-label="getAriaLabel()"
|
||||
:title="getAriaLabel()"
|
||||
|
@ -16,11 +21,7 @@
|
|||
:aria-selected="activeChannel && channel === activeChannel.channel"
|
||||
role="tab"
|
||||
>
|
||||
<slot
|
||||
:network="network"
|
||||
:channel="channel"
|
||||
:activeChannel="activeChannel"
|
||||
/>
|
||||
<slot :network="network" :channel="channel" :activeChannel="activeChannel" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<template>
|
||||
<div
|
||||
id="chat-container"
|
||||
class="window"
|
||||
>
|
||||
<div id="chat-container" class="window">
|
||||
<div
|
||||
id="chat"
|
||||
:data-id="channel.id"
|
||||
|
@ -21,38 +18,24 @@
|
|||
role="tabpanel"
|
||||
>
|
||||
<div class="header">
|
||||
<button
|
||||
class="lt"
|
||||
aria-label="Toggle channel list"
|
||||
/>
|
||||
<button class="lt" aria-label="Toggle channel list" />
|
||||
<span class="title">{{ channel.name }}</span>
|
||||
<span
|
||||
:title="channel.topic"
|
||||
class="topic"
|
||||
><ParsedMessage
|
||||
v-if="channel.topic"
|
||||
:network="network"
|
||||
:text="channel.topic"
|
||||
<span :title="channel.topic" class="topic"
|
||||
><ParsedMessage
|
||||
v-if="channel.topic"
|
||||
:network="network"
|
||||
:text="channel.topic"
|
||||
/></span>
|
||||
<button
|
||||
class="menu"
|
||||
aria-label="Open the context menu"
|
||||
/>
|
||||
<button class="menu" aria-label="Open the context menu" />
|
||||
<span
|
||||
v-if="channel.type === 'channel'"
|
||||
class="rt-tooltip tooltipped tooltipped-w"
|
||||
aria-label="Toggle user list"
|
||||
>
|
||||
<button
|
||||
class="rt"
|
||||
aria-label="Toggle user list"
|
||||
/>
|
||||
<button class="rt" aria-label="Toggle user list" />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="channel.type === 'special'"
|
||||
class="chat-content"
|
||||
>
|
||||
<div v-if="channel.type === 'special'" class="chat-content">
|
||||
<div class="chat">
|
||||
<div class="messages">
|
||||
<div class="msg">
|
||||
|
@ -65,26 +48,19 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="chat-content"
|
||||
>
|
||||
<div v-else class="chat-content">
|
||||
<div
|
||||
:class="['scroll-down tooltipped tooltipped-w tooltipped-no-touch', {'scroll-down-shown': !channel.scrolledToBottom}]"
|
||||
:class="[
|
||||
'scroll-down tooltipped tooltipped-w tooltipped-no-touch',
|
||||
{'scroll-down-shown': !channel.scrolledToBottom},
|
||||
]"
|
||||
aria-label="Jump to recent messages"
|
||||
@click="$refs.messageList.jumpToBottom()"
|
||||
>
|
||||
<div class="scroll-down-arrow" />
|
||||
</div>
|
||||
<MessageList
|
||||
ref="messageList"
|
||||
:network="network"
|
||||
:channel="channel"
|
||||
/>
|
||||
<ChatUserList
|
||||
v-if="channel.type === 'channel'"
|
||||
:channel="channel"
|
||||
/>
|
||||
<MessageList ref="messageList" :network="network" :channel="channel" />
|
||||
<ChatUserList v-if="channel.type === 'channel'" :channel="channel" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -92,12 +68,11 @@
|
|||
v-if="this.$root.currentUserVisibleError"
|
||||
id="user-visible-error"
|
||||
@click="hideUserVisibleError"
|
||||
>{{ this.$root.currentUserVisibleError }}</div>
|
||||
>
|
||||
{{ this.$root.currentUserVisibleError }}
|
||||
</div>
|
||||
<span id="upload-progressbar" />
|
||||
<ChatInput
|
||||
:network="network"
|
||||
:channel="channel"
|
||||
/>
|
||||
<ChatInput :network="network" :channel="channel" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -126,10 +101,14 @@ export default {
|
|||
computed: {
|
||||
specialComponent() {
|
||||
switch (this.channel.special) {
|
||||
case "list_bans": return ListBans;
|
||||
case "list_invites": return ListInvites;
|
||||
case "list_channels": return ListChannels;
|
||||
case "list_ignored": return ListIgnored;
|
||||
case "list_bans":
|
||||
return ListBans;
|
||||
case "list_invites":
|
||||
return ListInvites;
|
||||
case "list_channels":
|
||||
return ListChannels;
|
||||
case "list_ignored":
|
||||
return ListIgnored;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
<template>
|
||||
<form
|
||||
id="form"
|
||||
method="post"
|
||||
action=""
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<form id="form" method="post" action="" @submit.prevent="onSubmit">
|
||||
<span id="nick">{{ network.nick }}</span>
|
||||
<textarea
|
||||
id="input"
|
||||
|
@ -23,12 +18,7 @@
|
|||
aria-label="Upload file"
|
||||
@click="openFileUpload"
|
||||
>
|
||||
<input
|
||||
id="upload-input"
|
||||
ref="uploadInput"
|
||||
type="file"
|
||||
multiple
|
||||
>
|
||||
<input id="upload-input" ref="uploadInput" type="file" multiple />
|
||||
<button
|
||||
id="upload"
|
||||
type="button"
|
||||
|
@ -80,7 +70,7 @@ const bracketWraps = {
|
|||
"*": "*",
|
||||
"`": "`",
|
||||
"~": "~",
|
||||
"_": "_",
|
||||
_: "_",
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -130,7 +120,9 @@ export default {
|
|||
}
|
||||
|
||||
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") {
|
||||
|
@ -141,7 +133,9 @@ export default {
|
|||
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();
|
||||
|
||||
return false;
|
||||
|
@ -173,7 +167,8 @@ export default {
|
|||
// Use scrollHeight to calculate how many lines there are in input, and ceil the value
|
||||
// because some browsers tend to incorrently round the values when using high density
|
||||
// displays or using page zoom feature
|
||||
this.$refs.input.style.height = Math.ceil(this.$refs.input.scrollHeight / lineHeight) * lineHeight + "px";
|
||||
this.$refs.input.style.height =
|
||||
Math.ceil(this.$refs.input.scrollHeight / lineHeight) * lineHeight + "px";
|
||||
});
|
||||
},
|
||||
getInputPlaceholder(channel) {
|
||||
|
@ -219,7 +214,10 @@ export default {
|
|||
const args = text.substr(1).split(" ");
|
||||
const cmd = args.shift().toLowerCase();
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(commands, cmd) && commands[cmd].input(args)) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(commands, cmd) &&
|
||||
commands[cmd].input(args)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
<template>
|
||||
<aside
|
||||
ref="userlist"
|
||||
class="userlist"
|
||||
@mouseleave="removeHoverUser"
|
||||
>
|
||||
<aside ref="userlist" class="userlist" @mouseleave="removeHoverUser">
|
||||
<div class="count">
|
||||
<input
|
||||
ref="input"
|
||||
:value="userSearchInput"
|
||||
:placeholder="channel.users.length + ' user' + (channel.users.length === 1 ? '' : 's')"
|
||||
:placeholder="
|
||||
channel.users.length + ' user' + (channel.users.length === 1 ? '' : 's')
|
||||
"
|
||||
type="search"
|
||||
class="search"
|
||||
aria-label="Search among the user list"
|
||||
|
@ -19,7 +17,7 @@
|
|||
@keydown.page-up="navigateUserList($event, -10)"
|
||||
@keydown.page-down="navigateUserList($event, 10)"
|
||||
@keydown.enter="selectUser"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
<div class="names">
|
||||
<div
|
||||
|
@ -84,15 +82,11 @@ export default {
|
|||
// filteredUsers is computed, to avoid unnecessary filtering
|
||||
// as it is shared between filtering and keybindings.
|
||||
filteredUsers() {
|
||||
return fuzzy.filter(
|
||||
this.userSearchInput,
|
||||
this.channel.users,
|
||||
{
|
||||
pre: "<b>",
|
||||
post: "</b>",
|
||||
extract: (u) => u.nick,
|
||||
}
|
||||
);
|
||||
return fuzzy.filter(this.userSearchInput, this.channel.users, {
|
||||
pre: "<b>",
|
||||
post: "</b>",
|
||||
extract: (u) => u.nick,
|
||||
});
|
||||
},
|
||||
groupedUsers() {
|
||||
const groups = {};
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
:aria-label="localeDate"
|
||||
class="date-marker-container tooltipped tooltipped-s"
|
||||
>
|
||||
<div :aria-label="localeDate" class="date-marker-container tooltipped tooltipped-s">
|
||||
<div class="date-marker">
|
||||
<span
|
||||
:data-label="friendlyDate()"
|
||||
class="date-marker-text"
|
||||
/>
|
||||
<span :data-label="friendlyDate()" class="date-marker-text" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
maxlength="200"
|
||||
title="The channel name may not contain spaces"
|
||||
required
|
||||
>
|
||||
/>
|
||||
<input
|
||||
v-model="inputPassword"
|
||||
type="password"
|
||||
|
@ -30,11 +30,8 @@
|
|||
maxlength="200"
|
||||
title="The channel password may not contain spaces"
|
||||
autocomplete="new-password"
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-small"
|
||||
>Join</button>
|
||||
/>
|
||||
<button type="submit" class="btn btn-small">Join</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
|
@ -63,7 +60,9 @@ export default {
|
|||
methods: {
|
||||
onSubmit() {
|
||||
const channelToFind = this.inputChannel.toLowerCase();
|
||||
const existingChannel = this.network.channels.find((c) => c.name.toLowerCase() === channelToFind);
|
||||
const existingChannel = this.network.channels.find(
|
||||
(c) => c.name.toLowerCase() === channelToFind
|
||||
);
|
||||
|
||||
if (existingChannel) {
|
||||
const $ = require("jquery");
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="link.shown"
|
||||
v-show="link.canDisplay"
|
||||
ref="container"
|
||||
class="preview"
|
||||
>
|
||||
<div v-if="link.shown" v-show="link.canDisplay" ref="container" class="preview">
|
||||
<div
|
||||
ref="content"
|
||||
:class="['toggle-content', 'toggle-type-' + link.type, { opened: isContentShown }]"
|
||||
:class="['toggle-content', 'toggle-type-' + link.type, {opened: isContentShown}]"
|
||||
>
|
||||
<template v-if="link.type === 'link'">
|
||||
<a
|
||||
|
@ -25,7 +20,7 @@
|
|||
@error="onThumbnailError"
|
||||
@abort="onThumbnailError"
|
||||
@load="onPreviewReady"
|
||||
>
|
||||
/>
|
||||
</a>
|
||||
<div class="toggle-text">
|
||||
<div class="head">
|
||||
|
@ -35,7 +30,8 @@
|
|||
:title="link.head"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ link.head }}</a>
|
||||
>{{ link.head }}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
@ -44,81 +40,48 @@
|
|||
:aria-label="moreButtonLabel"
|
||||
class="more"
|
||||
@click="onMoreClick"
|
||||
><span class="more-caret" /></button>
|
||||
>
|
||||
<span class="more-caret" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="body overflowable">
|
||||
<a
|
||||
:href="link.link"
|
||||
:title="link.body"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ link.body }}</a>
|
||||
<a :href="link.link" :title="link.body" target="_blank" rel="noopener">{{
|
||||
link.body
|
||||
}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="link.type === 'image'">
|
||||
<a
|
||||
:href="link.link"
|
||||
class="toggle-thumbnail"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<img
|
||||
:src="link.thumb"
|
||||
decoding="async"
|
||||
alt=""
|
||||
@load="onPreviewReady"
|
||||
>
|
||||
<a :href="link.link" class="toggle-thumbnail" target="_blank" rel="noopener">
|
||||
<img :src="link.thumb" decoding="async" alt="" @load="onPreviewReady" />
|
||||
</a>
|
||||
</template>
|
||||
<template v-else-if="link.type === 'video'">
|
||||
<video
|
||||
preload="metadata"
|
||||
controls
|
||||
@canplay="onPreviewReady"
|
||||
>
|
||||
<source
|
||||
:src="link.media"
|
||||
:type="link.mediaType"
|
||||
>
|
||||
<video preload="metadata" controls @canplay="onPreviewReady">
|
||||
<source :src="link.media" :type="link.mediaType" />
|
||||
</video>
|
||||
</template>
|
||||
<template v-else-if="link.type === 'audio'">
|
||||
<audio
|
||||
controls
|
||||
preload="metadata"
|
||||
@canplay="onPreviewReady"
|
||||
>
|
||||
<source
|
||||
:src="link.media"
|
||||
:type="link.mediaType"
|
||||
>
|
||||
<audio controls preload="metadata" @canplay="onPreviewReady">
|
||||
<source :src="link.media" :type="link.mediaType" />
|
||||
</audio>
|
||||
</template>
|
||||
<template v-else-if="link.type === 'error'">
|
||||
<em v-if="link.error === 'image-too-big'">
|
||||
This image is larger than {{ link.maxSize | friendlysize }} and cannot be
|
||||
previewed.
|
||||
<a
|
||||
:href="link.link"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>Click here</a>
|
||||
<a :href="link.link" target="_blank" rel="noopener">Click here</a>
|
||||
to open it in a new window.
|
||||
</em>
|
||||
<template v-else-if="link.error === 'message'">
|
||||
<div>
|
||||
<em>
|
||||
A preview could not be loaded.
|
||||
<a
|
||||
:href="link.link"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>Click here</a>
|
||||
<a :href="link.link" target="_blank" rel="noopener">Click here</a>
|
||||
to open it in a new window.
|
||||
</em>
|
||||
<br>
|
||||
<br />
|
||||
<pre class="prefetch-error">{{ link.message }}</pre>
|
||||
</div>
|
||||
|
||||
|
@ -127,7 +90,9 @@
|
|||
:aria-label="moreButtonLabel"
|
||||
class="more"
|
||||
@click="onMoreClick"
|
||||
><span class="more-caret" /></button>
|
||||
>
|
||||
<span class="more-caret" />
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -217,27 +182,31 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
this.showMoreButton = this.$refs.content.offsetWidth >= this.$refs.container.offsetWidth;
|
||||
this.showMoreButton =
|
||||
this.$refs.content.offsetWidth >= this.$refs.container.offsetWidth;
|
||||
});
|
||||
},
|
||||
updateShownState() {
|
||||
let defaultState = true;
|
||||
|
||||
switch (this.link.type) {
|
||||
case "error":
|
||||
defaultState = this.link.error === "image-too-big" ? this.$root.settings.media : this.$root.settings.links;
|
||||
break;
|
||||
case "error":
|
||||
defaultState =
|
||||
this.link.error === "image-too-big"
|
||||
? this.$root.settings.media
|
||||
: this.$root.settings.links;
|
||||
break;
|
||||
|
||||
case "loading":
|
||||
defaultState = false;
|
||||
break;
|
||||
case "loading":
|
||||
defaultState = false;
|
||||
break;
|
||||
|
||||
case "link":
|
||||
defaultState = this.$root.settings.links;
|
||||
break;
|
||||
case "link":
|
||||
defaultState = this.$root.settings.links;
|
||||
break;
|
||||
|
||||
default:
|
||||
defaultState = this.$root.settings.media;
|
||||
default:
|
||||
defaultState = this.$root.settings.media;
|
||||
}
|
||||
|
||||
this.link.shown = this.link.shown && defaultState;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<button
|
||||
v-if="link.type !== 'loading'"
|
||||
:class="['toggle-button', 'toggle-preview', { opened: link.shown }]"
|
||||
:class="['toggle-button', 'toggle-preview', {opened: link.shown}]"
|
||||
:aria-label="ariaLabel"
|
||||
@click="onClick"
|
||||
/>
|
||||
|
|
|
@ -4,26 +4,18 @@
|
|||
:class="['msg', message.type, {self: message.self, highlight: message.highlight}]"
|
||||
:data-from="message.from && message.from.nick"
|
||||
>
|
||||
<span
|
||||
:aria-label="message.time | localetime"
|
||||
class="time tooltipped tooltipped-e"
|
||||
>{{ messageTime }} </span>
|
||||
<span :aria-label="message.time | localetime" class="time tooltipped tooltipped-e"
|
||||
>{{ messageTime }}
|
||||
</span>
|
||||
<template v-if="message.type === 'unhandled'">
|
||||
<span class="from">[{{ message.command }}]</span>
|
||||
<span class="content">
|
||||
<span
|
||||
v-for="(param, id) in message.params"
|
||||
:key="id"
|
||||
>{{ param }} </span>
|
||||
<span v-for="(param, id) in message.params" :key="id">{{ param }} </span>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="isAction()">
|
||||
<span class="from"><span class="only-copy">*** </span></span>
|
||||
<Component
|
||||
:is="messageComponent"
|
||||
:network="network"
|
||||
:message="message"
|
||||
/>
|
||||
<Component :is="messageComponent" :network="network" :message="message" />
|
||||
</template>
|
||||
<template v-else-if="message.type === 'action'">
|
||||
<span class="from"><span class="only-copy">* </span></span>
|
||||
|
@ -41,20 +33,14 @@
|
|||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span
|
||||
v-if="message.type === 'message'"
|
||||
class="from"
|
||||
>
|
||||
<span v-if="message.type === 'message'" class="from">
|
||||
<template v-if="message.from && message.from.nick">
|
||||
<span class="only-copy"><</span>
|
||||
<Username :user="message.from" />
|
||||
<span class="only-copy">> </span>
|
||||
</template>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="from"
|
||||
>
|
||||
<span v-else class="from">
|
||||
<template v-if="message.from && message.from.nick">
|
||||
<span class="only-copy">-</span>
|
||||
<Username :user="message.from" />
|
||||
|
@ -62,10 +48,7 @@
|
|||
</template>
|
||||
</span>
|
||||
<span class="content">
|
||||
<ParsedMessage
|
||||
:network="network"
|
||||
:message="message"
|
||||
/>
|
||||
<ParsedMessage :network="network" :message="message" />
|
||||
<LinkPreview
|
||||
v-for="preview in message.previews"
|
||||
:key="preview.link"
|
||||
|
@ -100,7 +83,9 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
messageTime() {
|
||||
const format = this.$root.settings.showSeconds ? constants.timeFormats.msgWithSeconds : constants.timeFormats.msgDefault;
|
||||
const format = this.$root.settings.showSeconds
|
||||
? constants.timeFormats.msgWithSeconds
|
||||
: constants.timeFormats.msgDefault;
|
||||
|
||||
return moment(this.message.time).format(format);
|
||||
},
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
<template>
|
||||
<div :class="[ 'msg', 'condensed', { closed: isCollapsed } ]">
|
||||
<div :class="['msg', 'condensed', {closed: isCollapsed}]">
|
||||
<div class="condensed-summary">
|
||||
<span class="time" />
|
||||
<span class="from" />
|
||||
<span
|
||||
class="content"
|
||||
@click="onCollapseClick"
|
||||
>{{ condensedText }}<button
|
||||
class="toggle-button"
|
||||
aria-label="Toggle status messages"
|
||||
<span class="content" @click="onCollapseClick"
|
||||
>{{ condensedText
|
||||
}}<button class="toggle-button" aria-label="Toggle status messages"
|
||||
/></span>
|
||||
</div>
|
||||
<Message
|
||||
|
@ -58,30 +55,60 @@ export default {
|
|||
constants.condensedTypes.forEach((type) => {
|
||||
if (obj[type]) {
|
||||
switch (type) {
|
||||
case "away":
|
||||
strings.push(obj[type] + (obj[type] > 1 ? " users have gone away" : " user has gone away"));
|
||||
break;
|
||||
case "back":
|
||||
strings.push(obj[type] + (obj[type] > 1 ? " users have come back" : " user has come back"));
|
||||
break;
|
||||
case "chghost":
|
||||
strings.push(obj[type] + (obj[type] > 1 ? " users have changed hostname" : " user has changed hostname"));
|
||||
break;
|
||||
case "join":
|
||||
strings.push(obj[type] + (obj[type] > 1 ? " users have joined" : " user has joined"));
|
||||
break;
|
||||
case "part":
|
||||
strings.push(obj[type] + (obj[type] > 1 ? " users have left" : " user has left"));
|
||||
break;
|
||||
case "nick":
|
||||
strings.push(obj[type] + (obj[type] > 1 ? " users have changed nick" : " user has changed nick"));
|
||||
break;
|
||||
case "kick":
|
||||
strings.push(obj[type] + (obj[type] > 1 ? " users were kicked" : " user was kicked"));
|
||||
break;
|
||||
case "mode":
|
||||
strings.push(obj[type] + (obj[type] > 1 ? " modes were set" : " mode was set"));
|
||||
break;
|
||||
case "away":
|
||||
strings.push(
|
||||
obj[type] +
|
||||
(obj[type] > 1
|
||||
? " users have gone away"
|
||||
: " user has gone away")
|
||||
);
|
||||
break;
|
||||
case "back":
|
||||
strings.push(
|
||||
obj[type] +
|
||||
(obj[type] > 1
|
||||
? " users have come back"
|
||||
: " user has come back")
|
||||
);
|
||||
break;
|
||||
case "chghost":
|
||||
strings.push(
|
||||
obj[type] +
|
||||
(obj[type] > 1
|
||||
? " users have changed hostname"
|
||||
: " user has changed hostname")
|
||||
);
|
||||
break;
|
||||
case "join":
|
||||
strings.push(
|
||||
obj[type] +
|
||||
(obj[type] > 1 ? " users have joined" : " user has joined")
|
||||
);
|
||||
break;
|
||||
case "part":
|
||||
strings.push(
|
||||
obj[type] + (obj[type] > 1 ? " users have left" : " user has left")
|
||||
);
|
||||
break;
|
||||
case "nick":
|
||||
strings.push(
|
||||
obj[type] +
|
||||
(obj[type] > 1
|
||||
? " users have changed nick"
|
||||
: " user has changed nick")
|
||||
);
|
||||
break;
|
||||
case "kick":
|
||||
strings.push(
|
||||
obj[type] +
|
||||
(obj[type] > 1 ? " users were kicked" : " user was kicked")
|
||||
);
|
||||
break;
|
||||
case "mode":
|
||||
strings.push(
|
||||
obj[type] + (obj[type] > 1 ? " modes were set" : " mode was set")
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
ref="chat"
|
||||
class="chat"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div :class="['show-more', { show: channel.moreHistoryAvailable }]">
|
||||
<div ref="chat" class="chat" tabindex="-1">
|
||||
<div :class="['show-more', {show: channel.moreHistoryAvailable}]">
|
||||
<button
|
||||
ref="loadMoreButton"
|
||||
:disabled="channel.historyLoading || !$root.isConnected"
|
||||
|
@ -85,7 +81,9 @@ export default {
|
|||
|
||||
// If actions are hidden, just return a message list with them excluded
|
||||
if (this.$root.settings.statusMessages === "hidden") {
|
||||
return this.channel.messages.filter((message) => !constants.condensedTypes.includes(message.type));
|
||||
return this.channel.messages.filter(
|
||||
(message) => !constants.condensedTypes.includes(message.type)
|
||||
);
|
||||
}
|
||||
|
||||
// If actions are not condensed, just return raw message list
|
||||
|
@ -99,7 +97,11 @@ export default {
|
|||
for (const message of this.channel.messages) {
|
||||
// If this message is not condensable, or its an action affecting our user,
|
||||
// then just append the message to container and be done with it
|
||||
if (message.self || message.highlight || !constants.condensedTypes.includes(message.type)) {
|
||||
if (
|
||||
message.self ||
|
||||
message.highlight ||
|
||||
!constants.condensedTypes.includes(message.type)
|
||||
) {
|
||||
lastCondensedContainer = null;
|
||||
|
||||
condensed.push(message);
|
||||
|
@ -199,7 +201,7 @@ export default {
|
|||
return true;
|
||||
}
|
||||
|
||||
return (new Date(previousMessage.time)).getDay() !== (new Date(message.time)).getDay();
|
||||
return new Date(previousMessage.time).getDay() !== new Date(message.time).getDay();
|
||||
},
|
||||
shouldDisplayUnreadMarker(id) {
|
||||
if (!this.unreadMarkerShown && id > this.channel.firstUnread) {
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<ParsedMessage
|
||||
v-if="message.self"
|
||||
:network="network"
|
||||
:message="message"
|
||||
/>
|
||||
<ParsedMessage v-if="message.self" :network="network" :message="message" />
|
||||
<template v-else>
|
||||
<Username :user="message.from" />
|
||||
is away
|
||||
<i class="away-message">(<ParsedMessage
|
||||
:network="network"
|
||||
:message="message"
|
||||
/>)</i>
|
||||
<i class="away-message">(<ParsedMessage :network="network" :message="message" />)</i>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<ParsedMessage
|
||||
v-if="message.self"
|
||||
:network="network"
|
||||
:message="message"
|
||||
/>
|
||||
<ParsedMessage v-if="message.self" :network="network" :message="message" />
|
||||
<template v-else>
|
||||
<Username :user="message.from" />
|
||||
is back
|
||||
|
|
|
@ -2,8 +2,12 @@
|
|||
<span class="content">
|
||||
<Username :user="message.from" />
|
||||
has changed
|
||||
<span v-if="message.new_ident">username to <b>{{ message.new_ident }}</b></span>
|
||||
<span v-if="message.new_host">hostname to <i class="hostmask">{{ message.new_host }}</i></span>
|
||||
<span v-if="message.new_ident"
|
||||
>username to <b>{{ message.new_ident }}</b></span
|
||||
>
|
||||
<span v-if="message.new_host"
|
||||
>hostname to <i class="hostmask">{{ message.new_host }}</i></span
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<Username :user="message.from" /> 
|
||||
<span class="ctcp-message"><ParsedMessage :text="message.ctcpMessage" /></span>
|
||||
<span class="ctcp-message"><ParsedMessage :text="message.ctcpMessage"/></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<span class="content">
|
||||
<Username :user="message.from" />
|
||||
sent a <abbr title="Client-to-client protocol">CTCP</abbr> request:
|
||||
<span class="ctcp-message"><ParsedMessage :text="message.ctcpMessage" /></span>
|
||||
<span class="ctcp-message"><ParsedMessage :text="message.ctcpMessage"/></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -3,14 +3,8 @@
|
|||
<Username :user="message.from" />
|
||||
invited
|
||||
<span v-if="message.invitedYou">you</span>
|
||||
<Username
|
||||
v-else
|
||||
:user="message.target"
|
||||
/>
|
||||
to <ParsedMessage
|
||||
:network="network"
|
||||
:text="message.channel"
|
||||
/>
|
||||
<Username v-else :user="message.target" />
|
||||
to <ParsedMessage :network="network" :text="message.channel" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -3,13 +3,9 @@
|
|||
<Username :user="message.from" />
|
||||
has kicked
|
||||
<Username :user="message.target" />
|
||||
<i
|
||||
v-if="message.text"
|
||||
class="part-reason"
|
||||
> (<ParsedMessage
|
||||
:network="network"
|
||||
:message="message"
|
||||
/>)</i>
|
||||
<i v-if="message.text" class="part-reason">
|
||||
(<ParsedMessage :network="network" :message="message" />)</i
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<span class="text"><ParsedMessage
|
||||
:network="network"
|
||||
:text="cleanText"
|
||||
/></span>
|
||||
<span class="text"><ParsedMessage :network="network" :text="cleanText"/></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
@ -31,7 +28,7 @@ export default {
|
|||
|
||||
// Remove empty lines around the MOTD (but not within it)
|
||||
return lines
|
||||
.map((line) => line.replace(/\s*$/,""))
|
||||
.map((line) => line.replace(/\s*$/, ""))
|
||||
.join("\n")
|
||||
.replace(/^[\r\n]+|[\r\n]+$/g, "");
|
||||
},
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<Username :user="message.from" />
|
||||
<i class="hostmask"> ({{ message.hostmask }})</i> has left the channel <i
|
||||
v-if="message.text"
|
||||
class="part-reason"
|
||||
>(<ParsedMessage
|
||||
:network="network"
|
||||
:message="message"
|
||||
/>)</i>
|
||||
<i class="hostmask"> ({{ message.hostmask }})</i> has left the channel
|
||||
<i v-if="message.text" class="part-reason"
|
||||
>(<ParsedMessage :network="network" :message="message" />)</i
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<Username :user="message.from" />
|
||||
<i class="hostmask"> ({{ message.hostmask }})</i> has quit <i
|
||||
v-if="message.text"
|
||||
class="quit-reason"
|
||||
>(<ParsedMessage
|
||||
:network="network"
|
||||
:message="message"
|
||||
/>)</i>
|
||||
<i class="hostmask"> ({{ message.hostmask }})</i> has quit
|
||||
<i v-if="message.text" class="quit-reason"
|
||||
>(<ParsedMessage :network="network" :message="message" />)</i
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<template v-if="message.from && message.from.nick"><Username :user="message.from" /> has changed the topic to: </template>
|
||||
<template v-else>The topic is: </template>
|
||||
<span
|
||||
v-if="message.text"
|
||||
class="new-topic"
|
||||
><ParsedMessage
|
||||
:network="network"
|
||||
:message="message"
|
||||
<template v-if="message.from && message.from.nick"
|
||||
><Username :user="message.from" /> has changed the topic to:
|
||||
</template>
|
||||
<template v-else
|
||||
>The topic is:
|
||||
</template>
|
||||
<span v-if="message.text" class="new-topic"
|
||||
><ParsedMessage :network="network" :message="message"
|
||||
/></span>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
@ -21,17 +21,17 @@
|
|||
:href="'https://ipinfo.io/' + message.whois.actual_ip"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ message.whois.actual_ip }}</a>
|
||||
<i v-if="message.whois.actual_hostname != message.whois.actual_ip"> ({{ message.whois.actual_hostname }})</i>
|
||||
>{{ message.whois.actual_ip }}</a
|
||||
>
|
||||
<i v-if="message.whois.actual_hostname != message.whois.actual_ip">
|
||||
({{ message.whois.actual_hostname }})</i
|
||||
>
|
||||
</dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.real_name">
|
||||
<dt>Real name:</dt>
|
||||
<dd><ParsedMessage
|
||||
:network="network"
|
||||
:text="message.whois.real_name"
|
||||
/></dd>
|
||||
<dd><ParsedMessage :network="network" :text="message.whois.real_name" /></dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.registered_nick">
|
||||
|
@ -41,10 +41,7 @@
|
|||
|
||||
<template v-if="message.whois.channels">
|
||||
<dt>Channels:</dt>
|
||||
<dd><ParsedMessage
|
||||
:network="network"
|
||||
:text="message.whois.channels"
|
||||
/></dd>
|
||||
<dd><ParsedMessage :network="network" :text="message.whois.channels" /></dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.modes">
|
||||
|
@ -76,10 +73,7 @@
|
|||
|
||||
<template v-if="message.whois.away">
|
||||
<dt>Away:</dt>
|
||||
<dd><ParsedMessage
|
||||
:network="network"
|
||||
:text="message.whois.away"
|
||||
/></dd>
|
||||
<dd><ParsedMessage :network="network" :text="message.whois.away" /></dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.secure">
|
||||
|
@ -89,7 +83,9 @@
|
|||
|
||||
<template v-if="message.whois.server">
|
||||
<dt>Connected to:</dt>
|
||||
<dd>{{ message.whois.server }} <i>({{ message.whois.server_info }})</i></dd>
|
||||
<dd>
|
||||
{{ message.whois.server }} <i>({{ message.whois.server_info }})</i>
|
||||
</dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.logonTime">
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="networks.length === 0"
|
||||
class="empty"
|
||||
>
|
||||
<div v-if="networks.length === 0" class="empty">
|
||||
You are not connected to any networks yet.
|
||||
</div>
|
||||
<Draggable
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
<template>
|
||||
<ChannelWrapper
|
||||
:network="network"
|
||||
:channel="channel"
|
||||
:active-channel="activeChannel"
|
||||
>
|
||||
<ChannelWrapper :network="network" :channel="channel" :active-channel="activeChannel">
|
||||
<button
|
||||
v-if="network.channels.length > 1"
|
||||
:aria-controls="'network-' + network.uuid"
|
||||
|
@ -11,16 +7,12 @@
|
|||
:aria-expanded="!network.isCollapsed"
|
||||
class="collapse-network"
|
||||
@click.stop="onCollapseClick"
|
||||
><span class="collapse-network-icon" /></button>
|
||||
<span
|
||||
v-else
|
||||
class="collapse-network"
|
||||
/>
|
||||
>
|
||||
<span class="collapse-network-icon" />
|
||||
</button>
|
||||
<span v-else class="collapse-network" />
|
||||
<div class="lobby-wrap">
|
||||
<span
|
||||
:title="channel.name"
|
||||
class="name"
|
||||
>{{ channel.name }}</span>
|
||||
<span :title="channel.name" class="name">{{ channel.name }}</span>
|
||||
<span
|
||||
v-if="network.status.connected && !network.status.secure"
|
||||
class="not-secure-tooltip tooltipped tooltipped-w"
|
||||
|
@ -35,18 +27,16 @@
|
|||
>
|
||||
<span class="not-connected-icon" />
|
||||
</span>
|
||||
<span
|
||||
v-if="channel.unread"
|
||||
:class="{ highlight: channel.highlight }"
|
||||
class="badge"
|
||||
>{{ channel.unread | roundBadgeNumber }}</span>
|
||||
<span v-if="channel.unread" :class="{highlight: channel.highlight}" class="badge">{{
|
||||
channel.unread | roundBadgeNumber
|
||||
}}</span>
|
||||
</div>
|
||||
<span
|
||||
:aria-label="joinChannelLabel"
|
||||
class="add-channel-tooltip tooltipped tooltipped-w tooltipped-no-touch"
|
||||
>
|
||||
<button
|
||||
:class="['add-channel', { opened: isJoinChannelShown }]"
|
||||
:class="['add-channel', {opened: isJoinChannelShown}]"
|
||||
:aria-controls="'join-channel-' + channel.id"
|
||||
:aria-label="joinChannelLabel"
|
||||
@click.stop="$emit('toggleJoinChannel')"
|
||||
|
|
|
@ -12,7 +12,9 @@ export default {
|
|||
render(createElement, context) {
|
||||
return parse(
|
||||
createElement,
|
||||
typeof context.props.text !== "undefined" ? context.props.text : context.props.message.text,
|
||||
typeof context.props.text !== "undefined"
|
||||
? context.props.text
|
||||
: context.props.message.text,
|
||||
context.props.message,
|
||||
context.props.network
|
||||
);
|
||||
|
|
|
@ -8,10 +8,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="ban in channel.data"
|
||||
:key="ban.hostmask"
|
||||
>
|
||||
<tr v-for="ban in channel.data" :key="ban.hostmask">
|
||||
<td class="hostmask">{{ ban.hostmask }}</td>
|
||||
<td class="banned_by">{{ ban.banned_by }}</td>
|
||||
<td class="banned_at">{{ ban.banned_at | localetime }}</td>
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
<template>
|
||||
<span v-if="channel.data.text">{{ channel.data.text }}</span>
|
||||
<table
|
||||
v-else
|
||||
class="channel-list"
|
||||
>
|
||||
<table v-else class="channel-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="channel">Channel</th>
|
||||
|
@ -12,19 +9,10 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="chan in channel.data"
|
||||
:key="chan.channel"
|
||||
>
|
||||
<td class="channel"><ParsedMessage
|
||||
:network="network"
|
||||
:text="chan.channel"
|
||||
/></td>
|
||||
<tr v-for="chan in channel.data" :key="chan.channel">
|
||||
<td class="channel"><ParsedMessage :network="network" :text="chan.channel" /></td>
|
||||
<td class="users">{{ chan.num_users }}</td>
|
||||
<td class="topic"><ParsedMessage
|
||||
:network="network"
|
||||
:text="chan.topic"
|
||||
/></td>
|
||||
<td class="topic"><ParsedMessage :network="network" :text="chan.topic" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -7,10 +7,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="user in channel.data"
|
||||
:key="user.hostmask"
|
||||
>
|
||||
<tr v-for="user in channel.data" :key="user.hostmask">
|
||||
<td class="hostmask">{{ user.hostmask }}</td>
|
||||
<td class="when">{{ user.when | localetime }}</td>
|
||||
</tr>
|
||||
|
|
|
@ -8,10 +8,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="invite in channel.data"
|
||||
:key="invite.hostmask"
|
||||
>
|
||||
<tr v-for="invite in channel.data" :key="invite.hostmask">
|
||||
<td class="hostmask">{{ invite.hostmask }}</td>
|
||||
<td class="invitened_by">{{ invite.invited_by }}</td>
|
||||
<td class="invitened_at">{{ invite.invited_at | localetime }}</td>
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<template>
|
||||
<span
|
||||
:class="['user', $options.filters.colorClass(user.nick), { active: active }]"
|
||||
:class="['user', $options.filters.colorClass(user.nick), {active: active}]"
|
||||
:data-name="user.nick"
|
||||
role="button"
|
||||
v-on="onHover ? { mouseover: hover } : {}"
|
||||
>{{ user.mode }}{{ user.nick }}</span>
|
||||
v-on="onHover ? {mouseover: hover} : {}"
|
||||
>{{ user.mode }}{{ user.nick }}</span
|
||||
>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<span
|
||||
:class="['user', $options.filters.colorClass(user.original.nick), { active: active }]"
|
||||
:class="['user', $options.filters.colorClass(user.original.nick), {active: active}]"
|
||||
:data-name="user.original.nick"
|
||||
role="button"
|
||||
@mouseover="hover"
|
||||
|
|
|
@ -33,8 +33,7 @@ const emojiStrategy = {
|
|||
search(term, callback) {
|
||||
// Trim colon from the matched term,
|
||||
// as we are unable to get a clean string from match regex
|
||||
term = term.replace(/:$/, ""),
|
||||
callback(fuzzyGrep(term, emojiSearchTerms));
|
||||
(term = term.replace(/:$/, "")), callback(fuzzyGrep(term, emojiSearchTerms));
|
||||
},
|
||||
template([string, original]) {
|
||||
return `<span class="emoji">${emojiMap[original]}</span> ${string}`;
|
||||
|
@ -52,8 +51,7 @@ const nicksStrategy = {
|
|||
term = term.slice(1);
|
||||
|
||||
if (term[0] === "@") {
|
||||
callback(completeNicks(term.slice(1), true)
|
||||
.map((val) => ["@" + val[0], "@" + val[1]]));
|
||||
callback(completeNicks(term.slice(1), true).map((val) => ["@" + val[0], "@" + val[1]]));
|
||||
} else {
|
||||
callback(completeNicks(term, true));
|
||||
}
|
||||
|
@ -118,10 +116,13 @@ const foregroundColorStrategy = {
|
|||
.filter((i) => fuzzy.test(term, i[0]) || fuzzy.test(term, i[1]))
|
||||
.map((i) => {
|
||||
if (fuzzy.test(term, i[1])) {
|
||||
return [i[0], fuzzy.match(term, i[1], {
|
||||
pre: "<b>",
|
||||
post: "</b>",
|
||||
}).rendered];
|
||||
return [
|
||||
i[0],
|
||||
fuzzy.match(term, i[1], {
|
||||
pre: "<b>",
|
||||
post: "</b>",
|
||||
}).rendered,
|
||||
];
|
||||
}
|
||||
|
||||
return i;
|
||||
|
@ -147,10 +148,13 @@ const backgroundColorStrategy = {
|
|||
.filter((i) => fuzzy.test(term, i[0]) || fuzzy.test(term, i[1]))
|
||||
.map((pair) => {
|
||||
if (fuzzy.test(term, pair[1])) {
|
||||
return [pair[0], fuzzy.match(term, pair[1], {
|
||||
pre: "<b>",
|
||||
post: "</b>",
|
||||
}).rendered];
|
||||
return [
|
||||
pair[0],
|
||||
fuzzy.match(term, pair[1], {
|
||||
pre: "<b>",
|
||||
post: "</b>",
|
||||
}).rendered,
|
||||
];
|
||||
}
|
||||
|
||||
return pair;
|
||||
|
@ -160,7 +164,10 @@ const backgroundColorStrategy = {
|
|||
callback(matchingColorCodes);
|
||||
},
|
||||
template(value) {
|
||||
return `<span class="irc-fg${parseInt(value[2], 10)} irc-bg irc-bg${parseInt(value[0], 10)}">${value[1]}</span>`;
|
||||
return `<span class="irc-fg${parseInt(value[2], 10)} irc-bg irc-bg${parseInt(
|
||||
value[0],
|
||||
10
|
||||
)}">${value[1]}</span>`;
|
||||
},
|
||||
replace(value) {
|
||||
return "\x03$1," + value[0];
|
||||
|
@ -185,46 +192,55 @@ function enableAutocomplete(inputRef) {
|
|||
lastMatch = "";
|
||||
});
|
||||
|
||||
Mousetrap(input.get(0)).bind("tab", (e) => {
|
||||
if (vueApp.isAutoCompleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const text = input.val();
|
||||
|
||||
if (input.get(0).selectionStart !== text.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tabCount === 0) {
|
||||
lastMatch = text.split(/\s/).pop();
|
||||
|
||||
if (lastMatch.length === 0) {
|
||||
Mousetrap(input.get(0)).bind(
|
||||
"tab",
|
||||
(e) => {
|
||||
if (vueApp.isAutoCompleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentMatches = completeNicks(lastMatch, false);
|
||||
e.preventDefault();
|
||||
|
||||
if (currentMatches.length === 0) {
|
||||
const text = input.val();
|
||||
|
||||
if (input.get(0).selectionStart !== text.length) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const position = input.get(0).selectionStart - lastMatch.length;
|
||||
const newMatch = nicksStrategy.replace([0, currentMatches[tabCount % currentMatches.length]], position);
|
||||
if (tabCount === 0) {
|
||||
lastMatch = text.split(/\s/).pop();
|
||||
|
||||
input.val(text.substr(0, position) + newMatch);
|
||||
if (lastMatch.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Propagate change to Vue model
|
||||
input.get(0).dispatchEvent(new CustomEvent("input", {
|
||||
detail: "autocomplete",
|
||||
}));
|
||||
currentMatches = completeNicks(lastMatch, false);
|
||||
|
||||
lastMatch = newMatch;
|
||||
tabCount++;
|
||||
}, "keydown");
|
||||
if (currentMatches.length === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const position = input.get(0).selectionStart - lastMatch.length;
|
||||
const newMatch = nicksStrategy.replace(
|
||||
[0, currentMatches[tabCount % currentMatches.length]],
|
||||
position
|
||||
);
|
||||
|
||||
input.val(text.substr(0, position) + newMatch);
|
||||
|
||||
// Propagate change to Vue model
|
||||
input.get(0).dispatchEvent(
|
||||
new CustomEvent("input", {
|
||||
detail: "autocomplete",
|
||||
})
|
||||
);
|
||||
|
||||
lastMatch = newMatch;
|
||||
tabCount++;
|
||||
},
|
||||
"keydown"
|
||||
);
|
||||
|
||||
const editor = new Textarea(input.get(0));
|
||||
textcomplete = new Textcomplete(editor, {
|
||||
|
@ -265,14 +281,10 @@ function enableAutocomplete(inputRef) {
|
|||
}
|
||||
|
||||
function fuzzyGrep(term, array) {
|
||||
const results = fuzzy.filter(
|
||||
term,
|
||||
array,
|
||||
{
|
||||
pre: "<b>",
|
||||
post: "</b>",
|
||||
}
|
||||
);
|
||||
const results = fuzzy.filter(term, array, {
|
||||
pre: "<b>",
|
||||
post: "</b>",
|
||||
});
|
||||
return results.map((el) => [el.string, el.original]);
|
||||
}
|
||||
|
||||
|
@ -303,10 +315,7 @@ function completeNicks(word, isFuzzy) {
|
|||
return fuzzyGrep(word, users);
|
||||
}
|
||||
|
||||
return $.grep(
|
||||
users,
|
||||
(w) => !w.toLowerCase().indexOf(word)
|
||||
);
|
||||
return $.grep(users, (w) => !w.toLowerCase().indexOf(word));
|
||||
}
|
||||
|
||||
function completeCommands(word) {
|
||||
|
|
|
@ -19,17 +19,7 @@ const colorCodeMap = [
|
|||
["15", "Light Grey"],
|
||||
];
|
||||
|
||||
const condensedTypes = [
|
||||
"away",
|
||||
"back",
|
||||
"chghost",
|
||||
"join",
|
||||
"part",
|
||||
"quit",
|
||||
"nick",
|
||||
"kick",
|
||||
"mode",
|
||||
];
|
||||
const condensedTypes = ["away", "back", "chghost", "join", "part", "quit", "nick", "kick", "mode"];
|
||||
const condensedTypesQuery = "." + condensedTypes.join(", .");
|
||||
|
||||
const timeFormats = {
|
||||
|
|
|
@ -16,7 +16,11 @@ module.exports = class ContextMenu {
|
|||
}
|
||||
|
||||
show() {
|
||||
const contextMenu = showContextMenu(this.contextMenuItems, this.selectedElement, this.event);
|
||||
const contextMenu = showContextMenu(
|
||||
this.contextMenuItems,
|
||||
this.selectedElement,
|
||||
this.event
|
||||
);
|
||||
this.bindEvents(contextMenu);
|
||||
return false;
|
||||
}
|
||||
|
@ -33,7 +37,8 @@ module.exports = class ContextMenu {
|
|||
bindEvents(contextMenu) {
|
||||
const contextMenuActions = this.contextMenuActions;
|
||||
|
||||
contextMenuActions.execute = (id, ...args) => contextMenuActions[id] && contextMenuActions[id](...args);
|
||||
contextMenuActions.execute = (id, ...args) =>
|
||||
contextMenuActions[id] && contextMenuActions[id](...args);
|
||||
|
||||
const clickItem = (item) => {
|
||||
const itemData = item.attr("data-data");
|
||||
|
@ -109,19 +114,25 @@ function showContextMenu(contextMenuItems, selectedElement, event) {
|
|||
if (item.divider) {
|
||||
contextMenu.append(templates.contextmenu_divider());
|
||||
} else {
|
||||
contextMenu.append(templates.contextmenu_item({
|
||||
class: typeof item.className === "function" ? item.className(target) : item.className,
|
||||
action: item.actionId,
|
||||
text: typeof item.displayName === "function" ? item.displayName(target) : item.displayName,
|
||||
data: typeof item.data === "function" ? item.data(target) : item.data,
|
||||
}));
|
||||
contextMenu.append(
|
||||
templates.contextmenu_item({
|
||||
class:
|
||||
typeof item.className === "function"
|
||||
? item.className(target)
|
||||
: item.className,
|
||||
action: item.actionId,
|
||||
text:
|
||||
typeof item.displayName === "function"
|
||||
? item.displayName(target)
|
||||
: item.displayName,
|
||||
data: typeof item.data === "function" ? item.data(target) : item.data,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contextMenuContainer
|
||||
.html(contextMenu)
|
||||
.show();
|
||||
contextMenuContainer.html(contextMenu).show();
|
||||
|
||||
contextMenu
|
||||
.css(positionContextMenu(contextMenu, selectedElement, event))
|
||||
|
@ -145,11 +156,11 @@ function positionContextMenu(contextMenu, selectedElement, e) {
|
|||
|
||||
offset = {left: e.pageX, top: e.pageY};
|
||||
|
||||
if ((window.innerWidth - offset.left) < menuWidth) {
|
||||
if (window.innerWidth - offset.left < menuWidth) {
|
||||
offset.left = window.innerWidth - menuWidth;
|
||||
}
|
||||
|
||||
if ((window.innerHeight - offset.top) < menuHeight) {
|
||||
if (window.innerHeight - offset.top < menuHeight) {
|
||||
offset.top = window.innerHeight - menuHeight;
|
||||
}
|
||||
|
||||
|
|
|
@ -170,7 +170,9 @@ function addKickItem() {
|
|||
}
|
||||
|
||||
addContextMenuItem({
|
||||
check: (target) => utils.hasRoleInChannel(target.closest(".chan"), ["op"]) && target.closest(".chan").attr("data-type") === "channel",
|
||||
check: (target) =>
|
||||
utils.hasRoleInChannel(target.closest(".chan"), ["op"]) &&
|
||||
target.closest(".chan").attr("data-type") === "channel",
|
||||
className: "action-kick",
|
||||
displayName: "Kick",
|
||||
data: (target) => target.attr("data-name"),
|
||||
|
|
|
@ -4,10 +4,7 @@ const $ = require("jquery");
|
|||
const Mousetrap = require("mousetrap");
|
||||
const utils = require("./utils");
|
||||
|
||||
Mousetrap.bind([
|
||||
"alt+up",
|
||||
"alt+down",
|
||||
], function(e, keys) {
|
||||
Mousetrap.bind(["alt+up", "alt+down"], function(e, keys) {
|
||||
const sidebar = $("#sidebar");
|
||||
const channels = sidebar.find(".chan").not(".network.collapsed :not(.lobby)");
|
||||
const index = channels.index(channels.filter(".active"));
|
||||
|
@ -15,13 +12,13 @@ Mousetrap.bind([
|
|||
let target;
|
||||
|
||||
switch (direction) {
|
||||
case "up":
|
||||
target = (channels.length + (index - 1 + channels.length)) % channels.length;
|
||||
break;
|
||||
case "up":
|
||||
target = (channels.length + (index - 1 + channels.length)) % channels.length;
|
||||
break;
|
||||
|
||||
case "down":
|
||||
target = (channels.length + (index + 1 + channels.length)) % channels.length;
|
||||
break;
|
||||
case "down":
|
||||
target = (channels.length + (index + 1 + channels.length)) % channels.length;
|
||||
break;
|
||||
}
|
||||
|
||||
target = channels.eq(target).click();
|
||||
|
@ -30,10 +27,7 @@ Mousetrap.bind([
|
|||
return false;
|
||||
});
|
||||
|
||||
Mousetrap.bind([
|
||||
"alt+shift+up",
|
||||
"alt+shift+down",
|
||||
], function(e, keys) {
|
||||
Mousetrap.bind(["alt+shift+up", "alt+shift+down"], function(e, keys) {
|
||||
const sidebar = $("#sidebar");
|
||||
const lobbies = sidebar.find(".lobby");
|
||||
const direction = keys.split("+").pop();
|
||||
|
@ -41,23 +35,33 @@ Mousetrap.bind([
|
|||
let target;
|
||||
|
||||
switch (direction) {
|
||||
case "up":
|
||||
if (index < 0) {
|
||||
target = lobbies.index(sidebar.find(".channel").filter(".active").siblings(".lobby")[0]);
|
||||
} else {
|
||||
target = (lobbies.length + (index - 1 + lobbies.length)) % lobbies.length;
|
||||
}
|
||||
case "up":
|
||||
if (index < 0) {
|
||||
target = lobbies.index(
|
||||
sidebar
|
||||
.find(".channel")
|
||||
.filter(".active")
|
||||
.siblings(".lobby")[0]
|
||||
);
|
||||
} else {
|
||||
target = (lobbies.length + (index - 1 + lobbies.length)) % lobbies.length;
|
||||
}
|
||||
|
||||
break;
|
||||
break;
|
||||
|
||||
case "down":
|
||||
if (index < 0) {
|
||||
index = lobbies.index(sidebar.find(".channel").filter(".active").siblings(".lobby")[0]);
|
||||
}
|
||||
case "down":
|
||||
if (index < 0) {
|
||||
index = lobbies.index(
|
||||
sidebar
|
||||
.find(".channel")
|
||||
.filter(".active")
|
||||
.siblings(".lobby")[0]
|
||||
);
|
||||
}
|
||||
|
||||
target = (lobbies.length + (index + 1 + lobbies.length)) % lobbies.length;
|
||||
target = (lobbies.length + (index + 1 + lobbies.length)) % lobbies.length;
|
||||
|
||||
break;
|
||||
break;
|
||||
}
|
||||
|
||||
target = lobbies.eq(target).click();
|
||||
|
|
|
@ -13,5 +13,5 @@ module.exports = function(str) {
|
|||
due to A being ascii 65 (100 0001)
|
||||
while a being ascii 97 (110 0001)
|
||||
*/
|
||||
return "color-" + (1 + hash % 32);
|
||||
return "color-" + (1 + (hash % 32));
|
||||
};
|
||||
|
|
|
@ -3,10 +3,12 @@
|
|||
// Return true if any section of "a" or "b" parts (defined by their start/end
|
||||
// markers) intersect each other, false otherwise.
|
||||
function anyIntersection(a, b) {
|
||||
return a.start <= b.start && b.start < a.end ||
|
||||
a.start < b.end && b.end <= a.end ||
|
||||
b.start <= a.start && a.start < b.end ||
|
||||
b.start < a.end && a.end <= b.end;
|
||||
return (
|
||||
(a.start <= b.start && b.start < a.end) ||
|
||||
(a.start < b.end && b.end <= a.end) ||
|
||||
(b.start <= a.start && a.start < b.end) ||
|
||||
(b.start < a.end && a.end <= b.end)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = anyIntersection;
|
||||
|
|
|
@ -25,11 +25,17 @@ const linkify = LinkifyIt()
|
|||
// Known schemes to detect in text
|
||||
const commonSchemes = [
|
||||
"sftp",
|
||||
"smb", "file",
|
||||
"irc", "ircs",
|
||||
"svn", "git",
|
||||
"steam", "mumble", "ts3server",
|
||||
"svn+ssh", "ssh",
|
||||
"smb",
|
||||
"file",
|
||||
"irc",
|
||||
"ircs",
|
||||
"svn",
|
||||
"git",
|
||||
"steam",
|
||||
"mumble",
|
||||
"ts3server",
|
||||
"svn+ssh",
|
||||
"ssh",
|
||||
];
|
||||
|
||||
for (const schema of commonSchemes) {
|
||||
|
|
|
@ -26,24 +26,20 @@ function sortParts(a, b) {
|
|||
// fragments will contain duplicate styling attributes.
|
||||
function merge(textParts, styleFragments, cleanText) {
|
||||
// Remove overlapping parts
|
||||
textParts = textParts
|
||||
.sort(sortParts)
|
||||
.reduce((prev, curr) => {
|
||||
const intersection = prev.some((p) => anyIntersection(p, curr));
|
||||
textParts = textParts.sort(sortParts).reduce((prev, curr) => {
|
||||
const intersection = prev.some((p) => anyIntersection(p, curr));
|
||||
|
||||
if (intersection) {
|
||||
return prev;
|
||||
}
|
||||
if (intersection) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return prev.concat([curr]);
|
||||
}, []);
|
||||
return prev.concat([curr]);
|
||||
}, []);
|
||||
|
||||
// Every section of the original text that has not been captured in a "part"
|
||||
// is filled with "text" parts, dummy objects with start/end but no extra
|
||||
// metadata.
|
||||
const allParts = textParts
|
||||
.concat(fill(textParts, cleanText))
|
||||
.sort(sortParts); // Sort all parts identified based on their position in the original text
|
||||
const allParts = textParts.concat(fill(textParts, cleanText)).sort(sortParts); // Sort all parts identified based on their position in the original text
|
||||
|
||||
// Distribute the style fragments within the text parts
|
||||
return allParts.map((textPart) => {
|
||||
|
|
|
@ -33,7 +33,16 @@ function parseStyle(text) {
|
|||
|
||||
// At any given time, these carry style information since last time a styling
|
||||
// control code was met.
|
||||
let colorCodes, bold, textColor, bgColor, hexColor, hexBgColor, italic, underline, strikethrough, monospace;
|
||||
let colorCodes,
|
||||
bold,
|
||||
textColor,
|
||||
bgColor,
|
||||
hexColor,
|
||||
hexBgColor,
|
||||
italic,
|
||||
underline,
|
||||
strikethrough,
|
||||
monospace;
|
||||
|
||||
const resetStyle = () => {
|
||||
bold = false;
|
||||
|
@ -90,96 +99,96 @@ function parseStyle(text) {
|
|||
// encountered since the previous styling character.
|
||||
while (position < text.length) {
|
||||
switch (text[position]) {
|
||||
case RESET:
|
||||
emitFragment();
|
||||
resetStyle();
|
||||
break;
|
||||
case RESET:
|
||||
emitFragment();
|
||||
resetStyle();
|
||||
break;
|
||||
|
||||
// Meeting a BOLD character means that the ongoing text is either going to
|
||||
// be in bold or that the previous one was in bold and the following one
|
||||
// must be reset.
|
||||
// This same behavior applies to COLOR, REVERSE, ITALIC, and UNDERLINE.
|
||||
case BOLD:
|
||||
emitFragment();
|
||||
bold = !bold;
|
||||
break;
|
||||
// Meeting a BOLD character means that the ongoing text is either going to
|
||||
// be in bold or that the previous one was in bold and the following one
|
||||
// must be reset.
|
||||
// This same behavior applies to COLOR, REVERSE, ITALIC, and UNDERLINE.
|
||||
case BOLD:
|
||||
emitFragment();
|
||||
bold = !bold;
|
||||
break;
|
||||
|
||||
case COLOR:
|
||||
emitFragment();
|
||||
case COLOR:
|
||||
emitFragment();
|
||||
|
||||
// Go one step further to find the corresponding color
|
||||
colorCodes = text.slice(position + 1).match(colorRx);
|
||||
// Go one step further to find the corresponding color
|
||||
colorCodes = text.slice(position + 1).match(colorRx);
|
||||
|
||||
if (colorCodes) {
|
||||
textColor = Number(colorCodes[1]);
|
||||
if (colorCodes) {
|
||||
textColor = Number(colorCodes[1]);
|
||||
|
||||
if (colorCodes[2]) {
|
||||
bgColor = Number(colorCodes[2]);
|
||||
if (colorCodes[2]) {
|
||||
bgColor = Number(colorCodes[2]);
|
||||
}
|
||||
|
||||
// Color code length is > 1, so bump the current position cursor by as
|
||||
// much (and reset the start cursor for the current text block as well)
|
||||
position += colorCodes[0].length;
|
||||
start = position + 1;
|
||||
} else {
|
||||
// If no color codes were found, toggles back to no colors (like BOLD).
|
||||
textColor = undefined;
|
||||
bgColor = undefined;
|
||||
}
|
||||
|
||||
// Color code length is > 1, so bump the current position cursor by as
|
||||
// much (and reset the start cursor for the current text block as well)
|
||||
position += colorCodes[0].length;
|
||||
start = position + 1;
|
||||
} else {
|
||||
// If no color codes were found, toggles back to no colors (like BOLD).
|
||||
textColor = undefined;
|
||||
bgColor = undefined;
|
||||
}
|
||||
break;
|
||||
|
||||
break;
|
||||
case HEX_COLOR:
|
||||
emitFragment();
|
||||
|
||||
case HEX_COLOR:
|
||||
emitFragment();
|
||||
colorCodes = text.slice(position + 1).match(hexColorRx);
|
||||
|
||||
colorCodes = text.slice(position + 1).match(hexColorRx);
|
||||
if (colorCodes) {
|
||||
hexColor = colorCodes[1].toUpperCase();
|
||||
|
||||
if (colorCodes) {
|
||||
hexColor = colorCodes[1].toUpperCase();
|
||||
if (colorCodes[2]) {
|
||||
hexBgColor = colorCodes[2].toUpperCase();
|
||||
}
|
||||
|
||||
if (colorCodes[2]) {
|
||||
hexBgColor = colorCodes[2].toUpperCase();
|
||||
// Color code length is > 1, so bump the current position cursor by as
|
||||
// much (and reset the start cursor for the current text block as well)
|
||||
position += colorCodes[0].length;
|
||||
start = position + 1;
|
||||
} else {
|
||||
// If no color codes were found, toggles back to no colors (like BOLD).
|
||||
hexColor = undefined;
|
||||
hexBgColor = undefined;
|
||||
}
|
||||
|
||||
// Color code length is > 1, so bump the current position cursor by as
|
||||
// much (and reset the start cursor for the current text block as well)
|
||||
position += colorCodes[0].length;
|
||||
start = position + 1;
|
||||
} else {
|
||||
// If no color codes were found, toggles back to no colors (like BOLD).
|
||||
hexColor = undefined;
|
||||
hexBgColor = undefined;
|
||||
break;
|
||||
|
||||
case REVERSE: {
|
||||
emitFragment();
|
||||
const tmp = bgColor;
|
||||
bgColor = textColor;
|
||||
textColor = tmp;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case ITALIC:
|
||||
emitFragment();
|
||||
italic = !italic;
|
||||
break;
|
||||
|
||||
case REVERSE: {
|
||||
emitFragment();
|
||||
const tmp = bgColor;
|
||||
bgColor = textColor;
|
||||
textColor = tmp;
|
||||
break;
|
||||
}
|
||||
case UNDERLINE:
|
||||
emitFragment();
|
||||
underline = !underline;
|
||||
break;
|
||||
|
||||
case ITALIC:
|
||||
emitFragment();
|
||||
italic = !italic;
|
||||
break;
|
||||
case STRIKETHROUGH:
|
||||
emitFragment();
|
||||
strikethrough = !strikethrough;
|
||||
break;
|
||||
|
||||
case UNDERLINE:
|
||||
emitFragment();
|
||||
underline = !underline;
|
||||
break;
|
||||
|
||||
case STRIKETHROUGH:
|
||||
emitFragment();
|
||||
strikethrough = !strikethrough;
|
||||
break;
|
||||
|
||||
case MONOSPACE:
|
||||
emitFragment();
|
||||
monospace = !monospace;
|
||||
break;
|
||||
case MONOSPACE:
|
||||
emitFragment();
|
||||
monospace = !monospace;
|
||||
break;
|
||||
}
|
||||
|
||||
// Evaluate the next character at the next iteration
|
||||
|
@ -192,25 +201,37 @@ function parseStyle(text) {
|
|||
return result;
|
||||
}
|
||||
|
||||
const properties = ["bold", "textColor", "bgColor", "hexColor", "hexBgColor", "italic", "underline", "strikethrough", "monospace"];
|
||||
const properties = [
|
||||
"bold",
|
||||
"textColor",
|
||||
"bgColor",
|
||||
"hexColor",
|
||||
"hexBgColor",
|
||||
"italic",
|
||||
"underline",
|
||||
"strikethrough",
|
||||
"monospace",
|
||||
];
|
||||
|
||||
function prepare(text) {
|
||||
return parseStyle(text)
|
||||
// This optimizes fragments by combining them together when all their values
|
||||
// for the properties defined above are equal.
|
||||
.reduce((prev, curr) => {
|
||||
if (prev.length) {
|
||||
const lastEntry = prev[prev.length - 1];
|
||||
return (
|
||||
parseStyle(text)
|
||||
// This optimizes fragments by combining them together when all their values
|
||||
// for the properties defined above are equal.
|
||||
.reduce((prev, curr) => {
|
||||
if (prev.length) {
|
||||
const lastEntry = prev[prev.length - 1];
|
||||
|
||||
if (properties.every((key) => curr[key] === lastEntry[key])) {
|
||||
lastEntry.text += curr.text;
|
||||
lastEntry.end += curr.text.length;
|
||||
return prev;
|
||||
if (properties.every((key) => curr[key] === lastEntry[key])) {
|
||||
lastEntry.text += curr.text;
|
||||
lastEntry.end += curr.text.length;
|
||||
return prev;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return prev.concat([curr]);
|
||||
}, []);
|
||||
return prev.concat([curr]);
|
||||
}, [])
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = prepare;
|
||||
|
|
|
@ -9,7 +9,7 @@ const merge = require("./ircmessageparser/merge");
|
|||
const colorClass = require("./colorClass");
|
||||
const emojiMap = require("../fullnamemap.json");
|
||||
const LinkPreviewToggle = require("../../../components/LinkPreviewToggle.vue").default;
|
||||
const emojiModifiersRegex = /[\u{1f3fb}-\u{1f3ff}]/ug;
|
||||
const emojiModifiersRegex = /[\u{1f3fb}-\u{1f3ff}]/gu;
|
||||
|
||||
// Create an HTML `span` with styling information for a given fragment
|
||||
function createFragment(fragment, createElement) {
|
||||
|
@ -80,7 +80,7 @@ module.exports = function parse(createElement, text, message = undefined, networ
|
|||
const channelParts = findChannels(cleanText, channelPrefixes, userModes);
|
||||
const linkParts = findLinks(cleanText);
|
||||
const emojiParts = findEmoji(cleanText);
|
||||
const nameParts = findNames(cleanText, message ? (message.users || []) : []);
|
||||
const nameParts = findNames(cleanText, message ? message.users || [] : []);
|
||||
|
||||
const parts = channelParts
|
||||
.concat(linkParts)
|
||||
|
@ -90,65 +90,85 @@ module.exports = function parse(createElement, text, message = undefined, networ
|
|||
// Merge the styling information with the channels / URLs / nicks / text objects and
|
||||
// generate HTML strings with the resulting fragments
|
||||
return merge(parts, styleFragments, cleanText).map((textPart) => {
|
||||
const fragments = textPart.fragments.map((fragment) => createFragment(fragment, createElement));
|
||||
const fragments = textPart.fragments.map((fragment) =>
|
||||
createFragment(fragment, createElement)
|
||||
);
|
||||
|
||||
// Wrap these potentially styled fragments with links and channel buttons
|
||||
if (textPart.link) {
|
||||
const preview = message && message.previews.find((p) => p.link === textPart.link);
|
||||
const link = createElement("a", {
|
||||
attrs: {
|
||||
href: textPart.link,
|
||||
target: "_blank",
|
||||
rel: "noopener",
|
||||
const link = createElement(
|
||||
"a",
|
||||
{
|
||||
attrs: {
|
||||
href: textPart.link,
|
||||
target: "_blank",
|
||||
rel: "noopener",
|
||||
},
|
||||
},
|
||||
}, fragments);
|
||||
fragments
|
||||
);
|
||||
|
||||
if (!preview) {
|
||||
return link;
|
||||
}
|
||||
|
||||
return [link, createElement(LinkPreviewToggle, {
|
||||
class: ["toggle-button", "toggle-preview"],
|
||||
props: {
|
||||
link: preview,
|
||||
},
|
||||
}, fragments)];
|
||||
return [
|
||||
link,
|
||||
createElement(
|
||||
LinkPreviewToggle,
|
||||
{
|
||||
class: ["toggle-button", "toggle-preview"],
|
||||
props: {
|
||||
link: preview,
|
||||
},
|
||||
},
|
||||
fragments
|
||||
),
|
||||
];
|
||||
} else if (textPart.channel) {
|
||||
return createElement("span", {
|
||||
class: [
|
||||
"inline-channel",
|
||||
],
|
||||
attrs: {
|
||||
"role": "button",
|
||||
"tabindex": 0,
|
||||
"data-chan": textPart.channel,
|
||||
return createElement(
|
||||
"span",
|
||||
{
|
||||
class: ["inline-channel"],
|
||||
attrs: {
|
||||
role: "button",
|
||||
tabindex: 0,
|
||||
"data-chan": textPart.channel,
|
||||
},
|
||||
},
|
||||
}, fragments);
|
||||
fragments
|
||||
);
|
||||
} else if (textPart.emoji) {
|
||||
const emojiWithoutModifiers = textPart.emoji.replace(emojiModifiersRegex, "");
|
||||
const title = emojiMap[emojiWithoutModifiers] ? `Emoji: ${emojiMap[emojiWithoutModifiers]}` : null;
|
||||
const title = emojiMap[emojiWithoutModifiers]
|
||||
? `Emoji: ${emojiMap[emojiWithoutModifiers]}`
|
||||
: null;
|
||||
|
||||
return createElement("span", {
|
||||
class: [
|
||||
"emoji",
|
||||
],
|
||||
attrs: {
|
||||
"role": "img",
|
||||
"aria-label": title,
|
||||
"title": title,
|
||||
return createElement(
|
||||
"span",
|
||||
{
|
||||
class: ["emoji"],
|
||||
attrs: {
|
||||
role: "img",
|
||||
"aria-label": title,
|
||||
title: title,
|
||||
},
|
||||
},
|
||||
}, fragments);
|
||||
fragments
|
||||
);
|
||||
} else if (textPart.nick) {
|
||||
return createElement("span", {
|
||||
class: [
|
||||
"user",
|
||||
colorClass(textPart.nick),
|
||||
],
|
||||
attrs: {
|
||||
"role": "button",
|
||||
"data-name": textPart.nick,
|
||||
return createElement(
|
||||
"span",
|
||||
{
|
||||
class: ["user", colorClass(textPart.nick)],
|
||||
attrs: {
|
||||
role: "button",
|
||||
"data-name": textPart.nick,
|
||||
},
|
||||
},
|
||||
}, fragments);
|
||||
fragments
|
||||
);
|
||||
}
|
||||
|
||||
return fragments;
|
||||
|
|
|
@ -42,7 +42,8 @@
|
|||
|
||||
window.g_LoungeErrorHandler = function LoungeErrorHandler(e) {
|
||||
var message = document.getElementById("loading-page-message");
|
||||
message.textContent = "An error has occurred that prevented the client from loading correctly.";
|
||||
message.textContent =
|
||||
"An error has occurred that prevented the client from loading correctly.";
|
||||
|
||||
var summary = document.createElement("summary");
|
||||
summary.textContent = "More details";
|
||||
|
|
|
@ -69,7 +69,11 @@ window.vueMounted = () => {
|
|||
});
|
||||
|
||||
viewport.on("click", "#chat .menu", function(e) {
|
||||
e.currentTarget = $(`#sidebar .chan[data-id="${$(this).closest(".chan").attr("data-id")}"]`)[0];
|
||||
e.currentTarget = $(
|
||||
`#sidebar .chan[data-id="${$(this)
|
||||
.closest(".chan")
|
||||
.attr("data-id")}"]`
|
||||
)[0];
|
||||
return contextMenuFactory.createContextMenu($(this), e).show();
|
||||
});
|
||||
|
||||
|
@ -140,8 +144,7 @@ window.vueMounted = () => {
|
|||
|
||||
const lastActive = $("#windows > .active");
|
||||
|
||||
lastActive
|
||||
.removeClass("active");
|
||||
lastActive.removeClass("active");
|
||||
|
||||
const chan = $(target)
|
||||
.addClass("active")
|
||||
|
|
|
@ -28,9 +28,7 @@ const noSync = ["syncSettings"];
|
|||
|
||||
// alwaysSync is reserved for settings that should be synced
|
||||
// to the server regardless of the clients sync setting.
|
||||
const alwaysSync = [
|
||||
"highlights",
|
||||
];
|
||||
const alwaysSync = ["highlights"];
|
||||
|
||||
// Process usersettings from localstorage.
|
||||
let userSettings = JSON.parse(storage.get("settings")) || false;
|
||||
|
@ -46,7 +44,10 @@ if (!userSettings) {
|
|||
}
|
||||
|
||||
// Make sure the setting in local storage has the same type that the code expects
|
||||
if (typeof userSettings[key] !== "undefined" && typeof settings[key] === typeof userSettings[key]) {
|
||||
if (
|
||||
typeof userSettings[key] !== "undefined" &&
|
||||
typeof settings[key] === typeof userSettings[key]
|
||||
) {
|
||||
settings[key] = userSettings[key];
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +60,10 @@ if (typeof userSettings.userStyles === "string" && !noCSSparamReg.test(window.lo
|
|||
$userStyles.html(userSettings.userStyles);
|
||||
}
|
||||
|
||||
if (typeof userSettings.theme === "string" && $theme.attr("href") !== `themes/${userSettings.theme}.css`) {
|
||||
if (
|
||||
typeof userSettings.theme === "string" &&
|
||||
$theme.attr("href") !== `themes/${userSettings.theme}.css`
|
||||
) {
|
||||
$theme.attr("href", `themes/${userSettings.theme}.css`);
|
||||
}
|
||||
|
||||
|
@ -99,7 +103,7 @@ function applySetting(name, value) {
|
|||
} else if (name === "userStyles" && !noCSSparamReg.test(window.location.search)) {
|
||||
$userStyles.html(value);
|
||||
} else if (name === "desktopNotifications") {
|
||||
if (("Notification" in window) && value && Notification.permission !== "granted") {
|
||||
if ("Notification" in window && value && Notification.permission !== "granted") {
|
||||
Notification.requestPermission(updateDesktopNotificationStatus);
|
||||
} else if (!value) {
|
||||
$warningBlocked.hide();
|
||||
|
@ -172,8 +176,7 @@ function processSetting(name, value, save) {
|
|||
} else if (name === "nickPostfix") {
|
||||
$settings.find(`input[name=${name}]`).val(value);
|
||||
} else if (name === "statusMessages") {
|
||||
$settings.find(`input[name=${name}][value=${value}]`)
|
||||
.prop("checked", true);
|
||||
$settings.find(`input[name=${name}][value=${value}]`).prop("checked", true);
|
||||
} else if (name === "theme") {
|
||||
$settings.find("#theme-select").val(value);
|
||||
} else if (typeof value === "boolean") {
|
||||
|
@ -208,7 +211,7 @@ function initialize() {
|
|||
|
||||
// If browser does not support notifications
|
||||
// display proper message in settings.
|
||||
if (("Notification" in window)) {
|
||||
if ("Notification" in window) {
|
||||
$warningUnsupported.hide();
|
||||
$windows.on("show", "#settings", updateDesktopNotificationStatus);
|
||||
} else {
|
||||
|
|
|
@ -47,40 +47,52 @@ function openImageViewer(link, {pushState = true} = {}) {
|
|||
// Only expanded thumbnails are being cycled through.
|
||||
|
||||
// Previous image
|
||||
let previousImage = link.closest(".preview").prev(".preview")
|
||||
.find(".toggle-content .toggle-thumbnail").last();
|
||||
let previousImage = link
|
||||
.closest(".preview")
|
||||
.prev(".preview")
|
||||
.find(".toggle-content .toggle-thumbnail")
|
||||
.last();
|
||||
|
||||
if (!previousImage.length) {
|
||||
previousImage = link.closest(".msg").prevAll()
|
||||
.find(".toggle-content .toggle-thumbnail").last();
|
||||
previousImage = link
|
||||
.closest(".msg")
|
||||
.prevAll()
|
||||
.find(".toggle-content .toggle-thumbnail")
|
||||
.last();
|
||||
}
|
||||
|
||||
previousImage.addClass("previous-image");
|
||||
|
||||
// Next image
|
||||
let nextImage = link.closest(".preview").next(".preview")
|
||||
.find(".toggle-content .toggle-thumbnail").first();
|
||||
let nextImage = link
|
||||
.closest(".preview")
|
||||
.next(".preview")
|
||||
.find(".toggle-content .toggle-thumbnail")
|
||||
.first();
|
||||
|
||||
if (!nextImage.length) {
|
||||
nextImage = link.closest(".msg").nextAll()
|
||||
.find(".toggle-content .toggle-thumbnail").first();
|
||||
nextImage = link
|
||||
.closest(".msg")
|
||||
.nextAll()
|
||||
.find(".toggle-content .toggle-thumbnail")
|
||||
.first();
|
||||
}
|
||||
|
||||
nextImage.addClass("next-image");
|
||||
|
||||
imageViewer.html(templates.image_viewer({
|
||||
image: link.find("img").prop("src"),
|
||||
link: link.prop("href"),
|
||||
type: link.parent().hasClass("toggle-type-link") ? "link" : "image",
|
||||
hasPreviousImage: previousImage.length > 0,
|
||||
hasNextImage: nextImage.length > 0,
|
||||
}));
|
||||
imageViewer.html(
|
||||
templates.image_viewer({
|
||||
image: link.find("img").prop("src"),
|
||||
link: link.prop("href"),
|
||||
type: link.parent().hasClass("toggle-type-link") ? "link" : "image",
|
||||
hasPreviousImage: previousImage.length > 0,
|
||||
hasNextImage: nextImage.length > 0,
|
||||
})
|
||||
);
|
||||
|
||||
// Turn off transitionend listener before opening the viewer,
|
||||
// which caused image viewer to become empty in rare cases
|
||||
imageViewer
|
||||
.off("transitionend")
|
||||
.addClass("opened");
|
||||
imageViewer.off("transitionend").addClass("opened");
|
||||
|
||||
// History management
|
||||
if (pushState) {
|
||||
|
@ -109,17 +121,14 @@ imageViewer.on("click", ".next-image-btn", function() {
|
|||
});
|
||||
|
||||
function closeImageViewer({pushState = true} = {}) {
|
||||
imageViewer
|
||||
.removeClass("opened")
|
||||
.one("transitionend", function() {
|
||||
imageViewer.empty();
|
||||
});
|
||||
imageViewer.removeClass("opened").one("transitionend", function() {
|
||||
imageViewer.empty();
|
||||
});
|
||||
|
||||
// History management
|
||||
if (pushState) {
|
||||
const clickTarget =
|
||||
"#sidebar " +
|
||||
`.chan[data-id="${$("#sidebar .chan.active").attr("data-id")}"]`;
|
||||
"#sidebar " + `.chan[data-id="${$("#sidebar .chan.active").attr("data-id")}"]`;
|
||||
history.pushState({clickTarget}, null, null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ function onTouchStart(e) {
|
|||
}
|
||||
|
||||
function onTouchMove(e) {
|
||||
const touch = touchCurPos = e.touches.item(0);
|
||||
const touch = (touchCurPos = e.touches.item(0));
|
||||
let distX = touch.screenX - touchStartPos.screenX;
|
||||
const distY = touch.screenY - touchStartPos.screenY;
|
||||
|
||||
|
@ -93,7 +93,7 @@ function onTouchEnd() {
|
|||
const diff = touchCurPos.screenX - touchStartPos.screenX;
|
||||
const absDiff = Math.abs(diff);
|
||||
|
||||
if (absDiff > menuWidth / 2 || Date.now() - touchStartTime < 180 && absDiff > 50) {
|
||||
if (absDiff > menuWidth / 2 || (Date.now() - touchStartTime < 180 && absDiff > 50)) {
|
||||
SlideoutMenu.toggle(diff > 0);
|
||||
}
|
||||
|
||||
|
|
|
@ -62,9 +62,12 @@ socket.on("auth", function(data) {
|
|||
storage.remove("token");
|
||||
|
||||
const error = login.find(".error");
|
||||
error.show().closest("form").one("submit", function() {
|
||||
error.hide();
|
||||
});
|
||||
error
|
||||
.show()
|
||||
.closest("form")
|
||||
.one("submit", function() {
|
||||
error.hide();
|
||||
});
|
||||
} else if (user) {
|
||||
token = storage.get("token");
|
||||
|
||||
|
|
|
@ -43,10 +43,7 @@ socket.on("changelog", function(data) {
|
|||
// When there is a button to refresh the checker available, display it when
|
||||
// data is expired. Before that, server would return same information anyway.
|
||||
if (data.expiresAt) {
|
||||
setTimeout(
|
||||
() => $("#version-checker #check-now").show(),
|
||||
data.expiresAt - Date.now()
|
||||
);
|
||||
setTimeout(() => $("#version-checker #check-now").show(), data.expiresAt - Date.now());
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -62,6 +59,7 @@ $("#help").on("click", "#check-now", () => {
|
|||
// Given a status and latest release information, update the version checker
|
||||
// (CSS class and content)
|
||||
function renderVersionChecker({status, latest}) {
|
||||
$("#version-checker").prop("class", status)
|
||||
$("#version-checker")
|
||||
.prop("class", status)
|
||||
.html(templates.version_checker({latest, status}));
|
||||
}
|
||||
|
|
|
@ -187,8 +187,7 @@ function parseOverrideParams(params, data) {
|
|||
}
|
||||
|
||||
// When the network is locked, URL overrides should not affect disabled fields
|
||||
if (data.lockNetwork &&
|
||||
["host", "port", "tls", "rejectUnauthorized"].includes(key)) {
|
||||
if (data.lockNetwork && ["host", "port", "tls", "rejectUnauthorized"].includes(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -198,20 +197,29 @@ function parseOverrideParams(params, data) {
|
|||
}
|
||||
|
||||
if (key === "join") {
|
||||
value = value.split(",").map((chan) => {
|
||||
if (!chan.match(/^[#&!+]/)) {
|
||||
return `#${chan}`;
|
||||
}
|
||||
value = value
|
||||
.split(",")
|
||||
.map((chan) => {
|
||||
if (!chan.match(/^[#&!+]/)) {
|
||||
return `#${chan}`;
|
||||
}
|
||||
|
||||
return chan;
|
||||
}).join(", ");
|
||||
return chan;
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
// Override server provided defaults with parameters passed in the URL if they match the data type
|
||||
switch (typeof data.defaults[key]) {
|
||||
case "boolean": data.defaults[key] = value === "1" || value === "true"; break;
|
||||
case "number": data.defaults[key] = Number(value); break;
|
||||
case "string": data.defaults[key] = String(value); break;
|
||||
case "boolean":
|
||||
data.defaults[key] = value === "1" || value === "true";
|
||||
break;
|
||||
case "number":
|
||||
data.defaults[key] = Number(value);
|
||||
break;
|
||||
case "string":
|
||||
data.defaults[key] = String(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,7 +126,10 @@ function mergeNetworkData(newNetworks) {
|
|||
|
||||
// Channels require extra care to be merged correctly
|
||||
if (key === "channels") {
|
||||
currentNetwork.channels = mergeChannelData(currentNetwork.channels, network.channels);
|
||||
currentNetwork.channels = mergeChannelData(
|
||||
currentNetwork.channels,
|
||||
network.channels
|
||||
);
|
||||
} else {
|
||||
currentNetwork[key] = network[key];
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@ const {vueApp, initChannel} = require("../vue");
|
|||
socket.on("join", function(data) {
|
||||
initChannel(data.chan);
|
||||
|
||||
vueApp.networks.find((n) => n.uuid === data.network)
|
||||
vueApp.networks
|
||||
.find((n) => n.uuid === data.network)
|
||||
.channels.splice(data.index || -1, 0, data.chan);
|
||||
|
||||
// Queries do not automatically focus, unless the user did a whois
|
||||
|
|
|
@ -32,7 +32,11 @@ socket.on("msg", function(data) {
|
|||
// Display received notices and errors in currently active channel.
|
||||
// Reloading the page will put them back into the lobby window.
|
||||
// We only want to put errors/notices in active channel if they arrive on the same network
|
||||
if (data.msg.showInActive && vueApp.activeChannel && vueApp.activeChannel.network === receivingChannel.network) {
|
||||
if (
|
||||
data.msg.showInActive &&
|
||||
vueApp.activeChannel &&
|
||||
vueApp.activeChannel.network === receivingChannel.network
|
||||
) {
|
||||
channel = vueApp.activeChannel.channel;
|
||||
|
||||
data.chan = channel.id;
|
||||
|
@ -76,7 +80,7 @@ socket.on("msg", function(data) {
|
|||
const user = channel.users.find((u) => u.nick === data.msg.from.nick);
|
||||
|
||||
if (user) {
|
||||
user.lastMessage = (new Date(data.msg.time)).getTime() || Date.now();
|
||||
user.lastMessage = new Date(data.msg.time).getTime() || Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,7 +102,11 @@ function notifyMessage(targetId, channel, activeChannel, msg) {
|
|||
}
|
||||
}
|
||||
|
||||
if (options.settings.desktopNotifications && ("Notification" in window) && Notification.permission === "granted") {
|
||||
if (
|
||||
options.settings.desktopNotifications &&
|
||||
"Notification" in window &&
|
||||
Notification.permission === "granted"
|
||||
) {
|
||||
let title;
|
||||
let body;
|
||||
|
||||
|
|
|
@ -17,7 +17,8 @@ socket.on("network", function(data) {
|
|||
vueApp.networks.push(network);
|
||||
|
||||
vueApp.$nextTick(() => {
|
||||
sidebar.find(".chan")
|
||||
sidebar
|
||||
.find(".chan")
|
||||
.last()
|
||||
.trigger("click");
|
||||
});
|
||||
|
@ -60,15 +61,19 @@ socket.on("channel:state", function(data) {
|
|||
socket.on("network:info", function(data) {
|
||||
$("#connect")
|
||||
.html(templates.windows.connect(data))
|
||||
.find("form").on("submit", function() {
|
||||
const uuid = $(this).find("input[name=uuid]").val();
|
||||
const newName = $(this).find("#connect\\:name").val();
|
||||
.find("form")
|
||||
.on("submit", function() {
|
||||
const uuid = $(this)
|
||||
.find("input[name=uuid]")
|
||||
.val();
|
||||
const newName = $(this)
|
||||
.find("#connect\\:name")
|
||||
.val();
|
||||
|
||||
const network = vueApp.networks.find((n) => n.uuid === uuid);
|
||||
network.name = network.channels[0].name = newName;
|
||||
|
||||
sidebar.find(`.network[data-uuid="${uuid}"] .chan.lobby .name`)
|
||||
.click();
|
||||
sidebar.find(`.network[data-uuid="${uuid}"] .chan.lobby .name`).click();
|
||||
});
|
||||
|
||||
utils.togglePasswordField("#connect .reveal-password");
|
||||
|
|
|
@ -23,7 +23,8 @@ socket.on("open", function(id) {
|
|||
channel.channel.unread = 0;
|
||||
|
||||
if (channel.channel.messages.length > 0) {
|
||||
channel.channel.firstUnread = channel.channel.messages[channel.channel.messages.length - 1].id;
|
||||
channel.channel.firstUnread =
|
||||
channel.channel.messages[channel.channel.messages.length - 1].id;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,10 @@ socket.on("part", function(data) {
|
|||
const channel = findChannel(data.chan);
|
||||
|
||||
if (channel) {
|
||||
channel.network.channels.splice(channel.network.channels.findIndex((c) => c.id === data.chan), 1);
|
||||
channel.network.channels.splice(
|
||||
channel.network.channels.findIndex((c) => c.id === data.chan),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
utils.synchronizeNotifiedState();
|
||||
|
|
|
@ -4,7 +4,11 @@ const socket = require("../socket");
|
|||
const options = require("../options");
|
||||
|
||||
function evaluateSetting(name, value) {
|
||||
if (options.settings.syncSettings && options.settings[name] !== value && !options.noSync.includes(name)) {
|
||||
if (
|
||||
options.settings.syncSettings &&
|
||||
options.settings[name] !== value &&
|
||||
!options.noSync.includes(name)
|
||||
) {
|
||||
options.processSetting(name, value, true);
|
||||
} else if (options.alwaysSync.includes(name)) {
|
||||
options.processSetting(name, value, true);
|
||||
|
|
|
@ -7,21 +7,21 @@ socket.on("sync_sort", function(data) {
|
|||
const order = data.order;
|
||||
|
||||
switch (data.type) {
|
||||
case "networks":
|
||||
vueApp.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
|
||||
case "networks":
|
||||
vueApp.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
|
||||
|
||||
break;
|
||||
break;
|
||||
|
||||
case "channels": {
|
||||
const network = vueApp.networks.find((n) => n.uuid === data.target);
|
||||
case "channels": {
|
||||
const network = vueApp.networks.find((n) => n.uuid === data.target);
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
network.channels.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -128,10 +128,14 @@ class Uploader {
|
|||
const xhr = new XMLHttpRequest();
|
||||
this.xhr = xhr;
|
||||
|
||||
xhr.upload.addEventListener("progress", (e) => {
|
||||
const percent = Math.floor(e.loaded / e.total * 1000) / 10;
|
||||
this.setProgress(percent);
|
||||
}, false);
|
||||
xhr.upload.addEventListener(
|
||||
"progress",
|
||||
(e) => {
|
||||
const percent = Math.floor((e.loaded / e.total) * 1000) / 10;
|
||||
this.setProgress(percent);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
|
@ -182,12 +186,12 @@ class Uploader {
|
|||
}
|
||||
|
||||
insertUploadUrl(url) {
|
||||
const fullURL = (new URL(url, location)).toString();
|
||||
const fullURL = new URL(url, location).toString();
|
||||
const textbox = document.getElementById("input");
|
||||
const initStart = textbox.selectionStart;
|
||||
|
||||
// Get the text before the cursor, and add a space if it's not in the beginning
|
||||
const headToCursor = initStart > 0 ? (textbox.value.substr(0, initStart) + " ") : "";
|
||||
const headToCursor = initStart > 0 ? textbox.value.substr(0, initStart) + " " : "";
|
||||
|
||||
// Get the remaining text after the cursor
|
||||
const cursorToTail = textbox.value.substr(initStart);
|
||||
|
@ -220,9 +224,9 @@ function initialize() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Called in the `configuration` socket event.
|
||||
* Makes it so the user can be notified if a file is too large without waiting for the upload to finish server-side.
|
||||
**/
|
||||
* Called in the `configuration` socket event.
|
||||
* Makes it so the user can be notified if a file is too large without waiting for the upload to finish server-side.
|
||||
**/
|
||||
function setMaxFileSize(kb) {
|
||||
instance.maxFileSize = kb;
|
||||
}
|
||||
|
|
|
@ -135,7 +135,7 @@ function move(array, old_index, new_index) {
|
|||
if (new_index >= array.length) {
|
||||
let k = new_index - array.length;
|
||||
|
||||
while ((k--) + 1) {
|
||||
while (k-- + 1) {
|
||||
this.push(undefined);
|
||||
}
|
||||
}
|
||||
|
@ -152,7 +152,8 @@ function closeChan(chan) {
|
|||
cmd = "/quit";
|
||||
const server = chan.find(".name").html();
|
||||
|
||||
if (!confirm(`Are you sure you want to remove ${server}?`)) { // eslint-disable-line no-alert
|
||||
if (!confirm(`Are you sure you want to remove ${server}?`)) {
|
||||
// eslint-disable-line no-alert
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,9 @@ let applicationServerKey;
|
|||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.addEventListener("message", (event) => {
|
||||
if (event.data && event.data.type === "open") {
|
||||
$("#sidebar").find(`.chan[data-target="#${event.data.channel}"]`).trigger("click");
|
||||
$("#sidebar")
|
||||
.find(`.chan[data-target="#${event.data.channel}"]`)
|
||||
.trigger("click");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -47,73 +49,89 @@ module.exports.initialize = () => {
|
|||
$("#pushNotificationsHttps").hide();
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("service-worker.js").then((registration) => {
|
||||
module.exports.hasServiceWorker = true;
|
||||
navigator.serviceWorker
|
||||
.register("service-worker.js")
|
||||
.then((registration) => {
|
||||
module.exports.hasServiceWorker = true;
|
||||
|
||||
if (!registration.pushManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
return registration.pushManager.getSubscription().then((subscription) => {
|
||||
$("#pushNotificationsUnsupported").hide();
|
||||
|
||||
pushNotificationsButton
|
||||
.prop("disabled", false)
|
||||
.on("click", onPushButton);
|
||||
|
||||
clientSubscribed = !!subscription;
|
||||
|
||||
if (clientSubscribed) {
|
||||
alternatePushButton();
|
||||
if (!registration.pushManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
return registration.pushManager.getSubscription().then((subscription) => {
|
||||
$("#pushNotificationsUnsupported").hide();
|
||||
|
||||
pushNotificationsButton.prop("disabled", false).on("click", onPushButton);
|
||||
|
||||
clientSubscribed = !!subscription;
|
||||
|
||||
if (clientSubscribed) {
|
||||
alternatePushButton();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
$("#pushNotificationsUnsupported span").text(err);
|
||||
});
|
||||
}).catch((err) => {
|
||||
$("#pushNotificationsUnsupported span").text(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function onPushButton() {
|
||||
pushNotificationsButton.prop("disabled", true);
|
||||
|
||||
navigator.serviceWorker.ready.then((registration) =>
|
||||
registration.pushManager.getSubscription().then((existingSubscription) => {
|
||||
if (existingSubscription) {
|
||||
socket.emit("push:unregister");
|
||||
navigator.serviceWorker.ready
|
||||
.then((registration) =>
|
||||
registration.pushManager
|
||||
.getSubscription()
|
||||
.then((existingSubscription) => {
|
||||
if (existingSubscription) {
|
||||
socket.emit("push:unregister");
|
||||
|
||||
return existingSubscription.unsubscribe();
|
||||
}
|
||||
return existingSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
return registration.pushManager.subscribe({
|
||||
applicationServerKey: urlBase64ToUint8Array(applicationServerKey),
|
||||
userVisibleOnly: true,
|
||||
}).then((subscription) => {
|
||||
const rawKey = subscription.getKey ? subscription.getKey("p256dh") : "";
|
||||
const key = rawKey ? window.btoa(String.fromCharCode(...new Uint8Array(rawKey))) : "";
|
||||
const rawAuthSecret = subscription.getKey ? subscription.getKey("auth") : "";
|
||||
const authSecret = rawAuthSecret ? window.btoa(String.fromCharCode(...new Uint8Array(rawAuthSecret))) : "";
|
||||
return registration.pushManager
|
||||
.subscribe({
|
||||
applicationServerKey: urlBase64ToUint8Array(applicationServerKey),
|
||||
userVisibleOnly: true,
|
||||
})
|
||||
.then((subscription) => {
|
||||
const rawKey = subscription.getKey ? subscription.getKey("p256dh") : "";
|
||||
const key = rawKey
|
||||
? window.btoa(String.fromCharCode(...new Uint8Array(rawKey)))
|
||||
: "";
|
||||
const rawAuthSecret = subscription.getKey
|
||||
? subscription.getKey("auth")
|
||||
: "";
|
||||
const authSecret = rawAuthSecret
|
||||
? window.btoa(String.fromCharCode(...new Uint8Array(rawAuthSecret)))
|
||||
: "";
|
||||
|
||||
socket.emit("push:register", {
|
||||
token: storage.get("token"),
|
||||
endpoint: subscription.endpoint,
|
||||
keys: {
|
||||
p256dh: key,
|
||||
auth: authSecret,
|
||||
},
|
||||
});
|
||||
socket.emit("push:register", {
|
||||
token: storage.get("token"),
|
||||
endpoint: subscription.endpoint,
|
||||
keys: {
|
||||
p256dh: key,
|
||||
auth: authSecret,
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
}).then((successful) => {
|
||||
if (successful) {
|
||||
alternatePushButton().prop("disabled", false);
|
||||
}
|
||||
})
|
||||
).catch((err) => {
|
||||
$("#pushNotificationsUnsupported")
|
||||
.find("span").text(`An error has occurred: ${err}`).end()
|
||||
.show();
|
||||
});
|
||||
return true;
|
||||
});
|
||||
})
|
||||
.then((successful) => {
|
||||
if (successful) {
|
||||
alternatePushButton().prop("disabled", false);
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((err) => {
|
||||
$("#pushNotificationsUnsupported")
|
||||
.find("span")
|
||||
.text(`An error has occurred: ${err}`)
|
||||
.end()
|
||||
.show();
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -127,10 +145,8 @@ function alternatePushButton() {
|
|||
}
|
||||
|
||||
function urlBase64ToUint8Array(base64String) {
|
||||
const padding = "=".repeat((4 - base64String.length % 4) % 4);
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/-/g, "+")
|
||||
.replace(/_/g, "/");
|
||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
||||
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
@ -143,5 +159,9 @@ function urlBase64ToUint8Array(base64String) {
|
|||
}
|
||||
|
||||
function isAllowedServiceWorkersHost() {
|
||||
return location.protocol === "https:" || location.hostname === "localhost" || location.hostname === "127.0.0.1";
|
||||
return (
|
||||
location.protocol === "https:" ||
|
||||
location.hostname === "localhost" ||
|
||||
location.hostname === "127.0.0.1"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,11 +10,15 @@ self.addEventListener("install", function() {
|
|||
});
|
||||
|
||||
self.addEventListener("activate", function(event) {
|
||||
event.waitUntil(caches.keys().then((names) => Promise.all(
|
||||
names
|
||||
.filter((name) => name !== cacheName)
|
||||
.map((name) => caches.delete(name))
|
||||
)));
|
||||
event.waitUntil(
|
||||
caches
|
||||
.keys()
|
||||
.then((names) =>
|
||||
Promise.all(
|
||||
names.filter((name) => name !== cacheName).map((name) => caches.delete(name))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
@ -50,9 +54,7 @@ async function putInCache(request, response) {
|
|||
async function cleanRedirect(response) {
|
||||
// Not all browsers support the Response.body stream, so fall back
|
||||
// to reading the entire body into memory as a blob.
|
||||
const bodyPromise = "body" in response ?
|
||||
Promise.resolve(response.body) :
|
||||
response.blob();
|
||||
const bodyPromise = "body" in response ? Promise.resolve(response.body) : response.blob();
|
||||
|
||||
const body = await bodyPromise;
|
||||
|
||||
|
@ -134,29 +136,33 @@ function showNotification(event, payload) {
|
|||
self.addEventListener("notificationclick", function(event) {
|
||||
event.notification.close();
|
||||
|
||||
event.waitUntil(clients.matchAll({
|
||||
includeUncontrolled: true,
|
||||
type: "window",
|
||||
}).then((clientList) => {
|
||||
if (clientList.length === 0) {
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow(`.#${event.notification.tag}`);
|
||||
}
|
||||
event.waitUntil(
|
||||
clients
|
||||
.matchAll({
|
||||
includeUncontrolled: true,
|
||||
type: "window",
|
||||
})
|
||||
.then((clientList) => {
|
||||
if (clientList.length === 0) {
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow(`.#${event.notification.tag}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const client = findSuitableClient(clientList);
|
||||
const client = findSuitableClient(clientList);
|
||||
|
||||
client.postMessage({
|
||||
type: "open",
|
||||
channel: event.notification.tag,
|
||||
});
|
||||
client.postMessage({
|
||||
type: "open",
|
||||
channel: event.notification.tag,
|
||||
});
|
||||
|
||||
if ("focus" in client) {
|
||||
client.focus();
|
||||
}
|
||||
}));
|
||||
if ("focus" in client) {
|
||||
client.focus();
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
function findSuitableClient(clientList) {
|
||||
|
|
|
@ -13,15 +13,18 @@ module.exports = requireViews.keys().reduce((acc, path) => {
|
|||
// Split path by folders, and create a new property if necessary/
|
||||
// First 2 characters are "./"/
|
||||
// Last element in the array ends with `.tpl` and needs to be `require`d.
|
||||
path.substr(2).split("/").forEach((key) => {
|
||||
if (key.endsWith(".tpl")) { //
|
||||
tmp[key.substr(0, key.length - 4)] = requireViews(path);
|
||||
} else {
|
||||
tmp[key] = tmp[key] || {};
|
||||
}
|
||||
path.substr(2)
|
||||
.split("/")
|
||||
.forEach((key) => {
|
||||
if (key.endsWith(".tpl")) {
|
||||
//
|
||||
tmp[key.substr(0, key.length - 4)] = requireViews(path);
|
||||
} else {
|
||||
tmp[key] = tmp[key] || {};
|
||||
}
|
||||
|
||||
tmp = tmp[key];
|
||||
});
|
||||
tmp = tmp[key];
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
|
8
index.js
8
index.js
|
@ -11,7 +11,13 @@ const pkg = require("./package.json");
|
|||
|
||||
if (!require("semver").satisfies(process.version, pkg.engines.node)) {
|
||||
/* eslint-disable no-console */
|
||||
console.error("The Lounge requires Node.js " + pkg.engines.node + " (current version: " + process.version + ")");
|
||||
console.error(
|
||||
"The Lounge requires Node.js " +
|
||||
pkg.engines.node +
|
||||
" (current version: " +
|
||||
process.version +
|
||||
")"
|
||||
);
|
||||
console.error("Please upgrade Node.js in order to use The Lounge");
|
||||
console.error("See https://thelounge.chat/docs/install-and-upgrade");
|
||||
console.error();
|
||||
|
|
|
@ -81,13 +81,21 @@ if (!version) {
|
|||
}
|
||||
|
||||
function isValidVersion(str) {
|
||||
return (/^[0-9]+\.[0-9]+\.[0-9]+(-(pre|rc)+\.[0-9]+)?$/.test(str));
|
||||
return /^[0-9]+\.[0-9]+\.[0-9]+(-(pre|rc)+\.[0-9]+)?$/.test(str);
|
||||
}
|
||||
|
||||
if (!isValidVersion(version)) {
|
||||
log.error(`Argument ${colors.bold("version")} is incorrect It must be either:`);
|
||||
log.error(`- A keyword among: ${colors.green("major")}, ${colors.green("minor")}, ${colors.green("patch")}, ${colors.green("prerelease")}, ${colors.green("pre")}`);
|
||||
log.error(`- An explicit version of format ${colors.green("x.y.z")} (stable) or ${colors.green("x.y.z-(pre|rc).n")} (pre-release).`);
|
||||
log.error(
|
||||
`- A keyword among: ${colors.green("major")}, ${colors.green("minor")}, ${colors.green(
|
||||
"patch"
|
||||
)}, ${colors.green("prerelease")}, ${colors.green("pre")}`
|
||||
);
|
||||
log.error(
|
||||
`- An explicit version of format ${colors.green("x.y.z")} (stable) or ${colors.green(
|
||||
"x.y.z-(pre|rc).n"
|
||||
)} (pre-release).`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
@ -99,11 +107,15 @@ function prereleaseTemplate(items) {
|
|||
|
||||
[See the full changelog](${items.fullChangelogUrl})
|
||||
|
||||
${prereleaseType(items.version) === "rc" ?
|
||||
`This is a release candidate (RC) for v${stableVersion(items.version)} to ensure maximum stability for public release.
|
||||
Bugs may be fixed, but no further features will be added until the next stable version.` :
|
||||
|
||||
`This is a pre-release for v${stableVersion(items.version)} to offer latest changes without having to wait for a stable release.
|
||||
${
|
||||
prereleaseType(items.version) === "rc"
|
||||
? `This is a release candidate (RC) for v${stableVersion(
|
||||
items.version
|
||||
)} to ensure maximum stability for public release.
|
||||
Bugs may be fixed, but no further features will be added until the next stable version.`
|
||||
: `This is a pre-release for v${stableVersion(
|
||||
items.version
|
||||
)} to offer latest changes without having to wait for a stable release.
|
||||
At this stage, features may still be added or modified until the first release candidate for this version gets released.`
|
||||
}
|
||||
|
||||
|
@ -128,7 +140,9 @@ function stableTemplate(items) {
|
|||
return `
|
||||
## v${items.version} - ${items.date}
|
||||
|
||||
For more details, [see the full changelog](${items.fullChangelogUrl}) and [milestone](${items.milestone.url}?closed=1).
|
||||
For more details, [see the full changelog](${items.fullChangelogUrl}) and [milestone](${
|
||||
items.milestone.url
|
||||
}?closed=1).
|
||||
|
||||
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
||||
@@ DESCRIPTION, ANNOUNCEMENT, ETC. @@
|
||||
|
@ -138,8 +152,10 @@ For more details, [see the full changelog](${items.fullChangelogUrl}) and [miles
|
|||
|
||||
### Changed
|
||||
|
||||
${isEmpty(items.dependencies) ? "" :
|
||||
`- Update production dependencies to their latest versions:
|
||||
${
|
||||
isEmpty(items.dependencies)
|
||||
? ""
|
||||
: `- Update production dependencies to their latest versions:
|
||||
${printDependencyList(items.dependencies)}`
|
||||
}
|
||||
|
||||
|
@ -157,23 +173,31 @@ ${printList(items.security)}
|
|||
|
||||
### Documentation
|
||||
|
||||
${items.documentation.length === 0 ? "" :
|
||||
`In the main repository:
|
||||
${
|
||||
items.documentation.length === 0
|
||||
? ""
|
||||
: `In the main repository:
|
||||
|
||||
${printList(items.documentation)}`
|
||||
}
|
||||
|
||||
${items.websiteDocumentation.length === 0 ? "" :
|
||||
`On the [website repository](https://github.com/thelounge/thelounge.github.io):
|
||||
${
|
||||
items.websiteDocumentation.length === 0
|
||||
? ""
|
||||
: `On the [website repository](https://github.com/thelounge/thelounge.github.io):
|
||||
|
||||
${printList(items.websiteDocumentation)}`
|
||||
}
|
||||
|
||||
### Internals
|
||||
|
||||
${printList(items.internals)}${isEmpty(items.devDependencies) ? "" : `
|
||||
${printList(items.internals)}${
|
||||
isEmpty(items.devDependencies)
|
||||
? ""
|
||||
: `
|
||||
- Update development dependencies to their latest versions:
|
||||
${printDependencyList(items.devDependencies)}`}
|
||||
${printDependencyList(items.devDependencies)}`
|
||||
}
|
||||
|
||||
@@@@@@@@@@@@@@@@@@@
|
||||
@@ UNCATEGORIZED @@
|
||||
|
@ -375,7 +399,9 @@ class RepositoryFetcher {
|
|||
|
||||
const prQuery = `query fetchPullRequests($repositoryName: String!) {
|
||||
repository(owner: "thelounge", name: $repositoryName) {
|
||||
${numbers.map((number) => `
|
||||
${numbers
|
||||
.map(
|
||||
(number) => `
|
||||
PR${number}: pullRequest(number: ${number}) {
|
||||
__typename
|
||||
title
|
||||
|
@ -398,7 +424,9 @@ class RepositoryFetcher {
|
|||
}
|
||||
}
|
||||
}
|
||||
`).join("")}
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
}
|
||||
}`;
|
||||
const data = await this.fetch(prQuery);
|
||||
|
@ -458,9 +486,8 @@ function printAuthorLink({login, url}) {
|
|||
|
||||
// Builds a Markdown link for a given pull request or commit object
|
||||
function printEntryLink(entry) {
|
||||
const label = entry.__typename === "PullRequest"
|
||||
? `#${entry.number}`
|
||||
: `\`${entry.abbreviatedOid}\``;
|
||||
const label =
|
||||
entry.__typename === "PullRequest" ? `#${entry.number}` : `\`${entry.abbreviatedOid}\``;
|
||||
|
||||
return `[${label}](${entry.url})`;
|
||||
}
|
||||
|
@ -476,12 +503,16 @@ function printLine(entry) {
|
|||
|
||||
// Builds a Markdown list item for a given pull request
|
||||
function printPullRequest(pullRequest) {
|
||||
return `- ${pullRequest.title} (${printEntryLink(pullRequest)} ${printAuthorLink(pullRequest.author)})`;
|
||||
return `- ${pullRequest.title} (${printEntryLink(pullRequest)} ${printAuthorLink(
|
||||
pullRequest.author
|
||||
)})`;
|
||||
}
|
||||
|
||||
// Builds a Markdown list item for a commit made directly in `master`
|
||||
function printCommit(commit) {
|
||||
return `- ${commit.messageHeadline} (${printEntryLink(commit)} ${printAuthorLink(commit.author.user)})`;
|
||||
return `- ${commit.messageHeadline} (${printEntryLink(commit)} ${printAuthorLink(
|
||||
commit.author.user
|
||||
)})`;
|
||||
}
|
||||
|
||||
// Builds a Markdown list of all given items
|
||||
|
@ -543,20 +574,23 @@ function hasLabel(labels, expected) {
|
|||
}
|
||||
|
||||
function hasAnnotatedComment(comments, expected) {
|
||||
return comments && comments.nodes.some(({authorAssociation, body}) =>
|
||||
["OWNER", "MEMBER"].includes(authorAssociation) &&
|
||||
body.split("\r\n").includes(`[${expected}]`)
|
||||
return (
|
||||
comments &&
|
||||
comments.nodes.some(
|
||||
({authorAssociation, body}) =>
|
||||
["OWNER", "MEMBER"].includes(authorAssociation) &&
|
||||
body.split("\r\n").includes(`[${expected}]`)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function isSkipped(entry) {
|
||||
return (
|
||||
(entry.__typename === "Commit" && (
|
||||
(entry.__typename === "Commit" &&
|
||||
// Version bump commits created by `yarn version`
|
||||
isValidVersion(entry.messageHeadline) ||
|
||||
// Commit message suggested by this script
|
||||
entry.messageHeadline.startsWith("Add changelog entry for v")
|
||||
)) ||
|
||||
(isValidVersion(entry.messageHeadline) ||
|
||||
// Commit message suggested by this script
|
||||
entry.messageHeadline.startsWith("Add changelog entry for v"))) ||
|
||||
hasLabelOrAnnotatedComment(entry, "Meta: Skip Changelog")
|
||||
);
|
||||
}
|
||||
|
@ -608,76 +642,85 @@ function extractPackages({title, url}) {
|
|||
return [];
|
||||
}
|
||||
|
||||
return extracted[1]
|
||||
.replace(/`/g, "")
|
||||
.split(/, and |, | and /);
|
||||
return extracted[1].replace(/`/g, "").split(/, and |, | and /);
|
||||
}
|
||||
|
||||
// Given an array of entries (PRs or commits), separates them into sections,
|
||||
// based on different information that describes them.
|
||||
function parse(entries) {
|
||||
return entries.reduce((result, entry) => {
|
||||
let deps;
|
||||
return entries.reduce(
|
||||
(result, entry) => {
|
||||
let deps;
|
||||
|
||||
if (isSkipped(entry)) {
|
||||
result.skipped.push(entry);
|
||||
} else if (isDependency(entry) && (deps = extractPackages(entry))) {
|
||||
deps.forEach((packageName) => {
|
||||
const dependencyType = whichDependencyType(packageName);
|
||||
if (isSkipped(entry)) {
|
||||
result.skipped.push(entry);
|
||||
} else if (isDependency(entry) && (deps = extractPackages(entry))) {
|
||||
deps.forEach((packageName) => {
|
||||
const dependencyType = whichDependencyType(packageName);
|
||||
|
||||
if (dependencyType) {
|
||||
if (!result[dependencyType][packageName]) {
|
||||
result[dependencyType][packageName] = [];
|
||||
if (dependencyType) {
|
||||
if (!result[dependencyType][packageName]) {
|
||||
result[dependencyType][packageName] = [];
|
||||
}
|
||||
|
||||
result[dependencyType][packageName].push(entry);
|
||||
} else {
|
||||
log.info(
|
||||
`${colors.bold(packageName)} was updated in ${colors.green(
|
||||
"#" + entry.number
|
||||
)} then removed since last release. Skipping. ${colors.gray(
|
||||
entry.url
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
result[dependencyType][packageName].push(entry);
|
||||
} else {
|
||||
log.info(`${colors.bold(packageName)} was updated in ${colors.green("#" + entry.number)} then removed since last release. Skipping. ${colors.gray(entry.url)}`);
|
||||
}
|
||||
});
|
||||
} else if (isDocumentation(entry)) {
|
||||
result.documentation.push(entry);
|
||||
} else if (isDeprecation(entry)) {
|
||||
result.deprecations.push(entry);
|
||||
} else if (isSecurity(entry)) {
|
||||
result.security.push(entry);
|
||||
} else if (isInternal(entry)) {
|
||||
result.internals.push(entry);
|
||||
} else {
|
||||
if (isFeature(entry)) {
|
||||
result.uncategorized.feature.push(entry);
|
||||
} else if (isBug(entry)) {
|
||||
result.uncategorized.bug.push(entry);
|
||||
});
|
||||
} else if (isDocumentation(entry)) {
|
||||
result.documentation.push(entry);
|
||||
} else if (isDeprecation(entry)) {
|
||||
result.deprecations.push(entry);
|
||||
} else if (isSecurity(entry)) {
|
||||
result.security.push(entry);
|
||||
} else if (isInternal(entry)) {
|
||||
result.internals.push(entry);
|
||||
} else {
|
||||
result.uncategorized.other.push(entry);
|
||||
if (isFeature(entry)) {
|
||||
result.uncategorized.feature.push(entry);
|
||||
} else if (isBug(entry)) {
|
||||
result.uncategorized.bug.push(entry);
|
||||
} else {
|
||||
result.uncategorized.other.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}, {
|
||||
skipped: [],
|
||||
dependencies: {},
|
||||
devDependencies: {},
|
||||
deprecations: [],
|
||||
documentation: [],
|
||||
internals: [],
|
||||
security: [],
|
||||
uncategorized: {
|
||||
feature: [],
|
||||
bug: [],
|
||||
other: [],
|
||||
return result;
|
||||
},
|
||||
unknownDependencies: new Set(),
|
||||
});
|
||||
{
|
||||
skipped: [],
|
||||
dependencies: {},
|
||||
devDependencies: {},
|
||||
deprecations: [],
|
||||
documentation: [],
|
||||
internals: [],
|
||||
security: [],
|
||||
uncategorized: {
|
||||
feature: [],
|
||||
bug: [],
|
||||
other: [],
|
||||
},
|
||||
unknownDependencies: new Set(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function dedupeEntries(changelog, items) {
|
||||
const dedupe = (entries) =>
|
||||
entries.filter((entry) => !changelog.includes(printEntryLink(entry)));
|
||||
|
||||
["deprecations", "documentation", "websiteDocumentation", "internals", "security"].forEach((type) => {
|
||||
items[type] = dedupe(items[type]);
|
||||
});
|
||||
["deprecations", "documentation", "websiteDocumentation", "internals", "security"].forEach(
|
||||
(type) => {
|
||||
items[type] = dedupe(items[type]);
|
||||
}
|
||||
);
|
||||
|
||||
["dependencies", "devDependencies", "uncategorized"].forEach((type) => {
|
||||
Object.entries(items[type]).forEach(([name, entries]) => {
|
||||
|
@ -692,8 +735,8 @@ function extractContributors(entries) {
|
|||
const set = Object.values(entries).reduce((memo, {__typename, author}) => {
|
||||
if (__typename === "PullRequest" && author.__typename !== "Bot") {
|
||||
memo.add("@" + author.login);
|
||||
// Commit authors are *always* of type "User", so have to discriminate some
|
||||
// other way. Making the assumption of a suffix for now, see how that goes.
|
||||
// Commit authors are *always* of type "User", so have to discriminate some
|
||||
// other way. Making the assumption of a suffix for now, see how that goes.
|
||||
} else if (__typename === "Commit" && !author.user.login.endsWith("-bot")) {
|
||||
memo.add("@" + author.user.login);
|
||||
}
|
||||
|
@ -701,8 +744,7 @@ function extractContributors(entries) {
|
|||
return memo;
|
||||
}, new Set());
|
||||
|
||||
return Array.from(set)
|
||||
.sort((a, b) => a.localeCompare(b, "en", {sensitivity: "base"}));
|
||||
return Array.from(set).sort((a, b) => a.localeCompare(b, "en", {sensitivity: "base"}));
|
||||
}
|
||||
|
||||
const client = new GraphQLClient("https://api.github.com/graphql", {
|
||||
|
@ -727,13 +769,17 @@ async function generateChangelogEntry(changelog, targetVersion) {
|
|||
} else {
|
||||
template = stableTemplate;
|
||||
|
||||
const codeCommitsAndPullRequests = await codeRepo.fetchCommitsAndPullRequestsSince("v" + previousVersion);
|
||||
const codeCommitsAndPullRequests = await codeRepo.fetchCommitsAndPullRequestsSince(
|
||||
"v" + previousVersion
|
||||
);
|
||||
items = parse(codeCommitsAndPullRequests);
|
||||
items.milestone = await codeRepo.fetchMilestone(targetVersion);
|
||||
|
||||
const websiteRepo = new RepositoryFetcher(client, "thelounge.github.io");
|
||||
const previousWebsiteVersion = await websiteRepo.fetchPreviousVersion(targetVersion);
|
||||
const websiteCommitsAndPullRequests = await websiteRepo.fetchCommitsAndPullRequestsSince("v" + previousWebsiteVersion);
|
||||
const websiteCommitsAndPullRequests = await websiteRepo.fetchCommitsAndPullRequestsSince(
|
||||
"v" + previousWebsiteVersion
|
||||
);
|
||||
items.websiteDocumentation = websiteCommitsAndPullRequests;
|
||||
|
||||
contributors = extractContributors([
|
||||
|
@ -781,11 +827,18 @@ function addToChangelog(changelog, newEntry) {
|
|||
const changelog = await readFile(changelogPath, "utf8");
|
||||
|
||||
try {
|
||||
({changelogEntry, skipped, contributors} = await generateChangelogEntry(changelog, version));
|
||||
({changelogEntry, skipped, contributors} = await generateChangelogEntry(
|
||||
changelog,
|
||||
version
|
||||
));
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 401) {
|
||||
log.error(`GitHub returned an error: ${colors.red(error.response.message)}`);
|
||||
log.error(`Make sure your personal access token is set with ${colors.bold("public_repo")} scope.`);
|
||||
log.error(
|
||||
`Make sure your personal access token is set with ${colors.bold(
|
||||
"public_repo"
|
||||
)} scope.`
|
||||
);
|
||||
} else {
|
||||
log.error(error);
|
||||
}
|
||||
|
@ -806,10 +859,14 @@ function addToChangelog(changelog, newEntry) {
|
|||
|
||||
// Step 3 (optional): Print a list of skipped entries if there are any
|
||||
if (skipped.length > 0) {
|
||||
const pad = Math.max(...skipped.map((entry) => (entry.title || entry.messageHeadline).length));
|
||||
const pad = Math.max(
|
||||
...skipped.map((entry) => (entry.title || entry.messageHeadline).length)
|
||||
);
|
||||
log.warn(`${skipped.length} ${skipped.length > 1 ? "entries were" : "entry was"} skipped:`);
|
||||
skipped.forEach((entry) => {
|
||||
log.warn(`- ${(entry.title || entry.messageHeadline).padEnd(pad)} ${colors.gray(entry.url)}`);
|
||||
log.warn(
|
||||
`- ${(entry.title || entry.messageHeadline).padEnd(pad)} ${colors.gray(entry.url)}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -819,7 +876,11 @@ function addToChangelog(changelog, newEntry) {
|
|||
if (isPrerelease(version)) {
|
||||
log.info(`You can now run: ${colors.bold(commitCommand)}`);
|
||||
} else {
|
||||
log.info(`Please edit ${colors.bold("CHANGELOG.md")} to your liking then run: ${colors.bold(commitCommand)}`);
|
||||
log.info(
|
||||
`Please edit ${colors.bold("CHANGELOG.md")} to your liking then run: ${colors.bold(
|
||||
commitCommand
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
log.info(`Finished in ${colors.bold(Date.now() - startTime)}ms.`);
|
||||
|
|
|
@ -15,15 +15,10 @@ const {join} = require("path");
|
|||
const {spawnSync} = require("child_process");
|
||||
|
||||
function getGitUsername() {
|
||||
return spawnSync("git", ["config", "user.name"], {encoding: "utf8"})
|
||||
.stdout
|
||||
.trim();
|
||||
return spawnSync("git", ["config", "user.name"], {encoding: "utf8"}).stdout.trim();
|
||||
}
|
||||
|
||||
const configContent = readFileSync(
|
||||
join(__dirname, "..", "defaults", "config.js"),
|
||||
"utf8"
|
||||
);
|
||||
const configContent = readFileSync(join(__dirname, "..", "defaults", "config.js"), "utf8");
|
||||
|
||||
const docPath = join(process.argv[2], "_includes", "config.js.md");
|
||||
|
||||
|
@ -42,7 +37,8 @@ const extractedDoc = configContent
|
|||
}
|
||||
|
||||
return acc;
|
||||
}, []).join("\n");
|
||||
}, [])
|
||||
.join("\n");
|
||||
|
||||
const infoBlockHeader = `<!--
|
||||
DO NOT EDIT THIS FILE MANUALLY.
|
||||
|
@ -62,10 +58,13 @@ writeFileSync(docPath, generatedContent);
|
|||
|
||||
log.info(
|
||||
`${colors.bold(generatedContent.split("\n").length)} lines ` +
|
||||
`(${colors.bold(generatedContent.length)} characters) ` +
|
||||
`were written in ${colors.green(docPath)}.`
|
||||
`(${colors.bold(generatedContent.length)} characters) ` +
|
||||
`were written in ${colors.green(docPath)}.`
|
||||
);
|
||||
|
||||
function getPrettyDate() {
|
||||
return (new Date()).toISOString().split(".")[0].replace("T", " ");
|
||||
return new Date()
|
||||
.toISOString()
|
||||
.split(".")[0]
|
||||
.replace("T", " ");
|
||||
}
|
||||
|
|
|
@ -5,7 +5,9 @@ const path = require("path");
|
|||
const fs = require("fs");
|
||||
|
||||
(async () => {
|
||||
const response = await got("https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json");
|
||||
const response = await got(
|
||||
"https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json"
|
||||
);
|
||||
const emojiStrategy = JSON.parse(response.body);
|
||||
const emojiMap = {};
|
||||
const fullNameEmojiMap = {};
|
||||
|
@ -21,21 +23,13 @@ const fs = require("fs");
|
|||
const emojiMapOutput = JSON.stringify(emojiMap, null, 2) + "\n";
|
||||
const fullNameEmojiMapOutput = JSON.stringify(fullNameEmojiMap, null, 2) + "\n";
|
||||
|
||||
fs.writeFileSync(path.resolve(path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"client",
|
||||
"js",
|
||||
"libs",
|
||||
"simplemap.json"
|
||||
)), emojiMapOutput);
|
||||
fs.writeFileSync(
|
||||
path.resolve(path.join(__dirname, "..", "client", "js", "libs", "simplemap.json")),
|
||||
emojiMapOutput
|
||||
);
|
||||
|
||||
fs.writeFileSync(path.resolve(path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"client",
|
||||
"js",
|
||||
"libs",
|
||||
"fullnamemap.json"
|
||||
)), fullNameEmojiMapOutput);
|
||||
fs.writeFileSync(
|
||||
path.resolve(path.join(__dirname, "..", "client", "js", "libs", "fullnamemap.json")),
|
||||
fullNameEmojiMapOutput
|
||||
);
|
||||
})();
|
||||
|
|
200
src/client.js
200
src/client.js
|
@ -159,18 +159,26 @@ Client.prototype.connect = function(args) {
|
|||
return;
|
||||
}
|
||||
|
||||
channels.push(client.createChannel({
|
||||
name: chan.name,
|
||||
key: chan.key || "",
|
||||
type: chan.type,
|
||||
}));
|
||||
channels.push(
|
||||
client.createChannel({
|
||||
name: chan.name,
|
||||
key: chan.key || "",
|
||||
type: chan.type,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (badName && client.name) {
|
||||
log.warn("User '" + client.name + "' on network '" + args.name + "' has an invalid channel which has been ignored");
|
||||
log.warn(
|
||||
"User '" +
|
||||
client.name +
|
||||
"' on network '" +
|
||||
args.name +
|
||||
"' has an invalid channel which has been ignored"
|
||||
);
|
||||
}
|
||||
// `join` is kept for backwards compatibility when updating from versions <2.0
|
||||
// also used by the "connect" window
|
||||
// `join` is kept for backwards compatibility when updating from versions <2.0
|
||||
// also used by the "connect" window
|
||||
} else if (args.join) {
|
||||
channels = args.join
|
||||
.replace(/,/g, " ")
|
||||
|
@ -188,7 +196,9 @@ Client.prototype.connect = function(args) {
|
|||
|
||||
const network = new Network({
|
||||
uuid: args.uuid,
|
||||
name: String(args.name || (Helper.config.displayNetwork ? "" : Helper.config.defaults.name) || ""),
|
||||
name: String(
|
||||
args.name || (Helper.config.displayNetwork ? "" : Helper.config.defaults.name) || ""
|
||||
),
|
||||
host: String(args.host || ""),
|
||||
port: parseInt(args.port, 10),
|
||||
tls: !!args.tls,
|
||||
|
@ -218,16 +228,18 @@ Client.prototype.connect = function(args) {
|
|||
network.createIrcFramework(client);
|
||||
|
||||
events.forEach((plugin) => {
|
||||
require(`./plugins/irc-events/${plugin}`).apply(client, [
|
||||
network.irc,
|
||||
network,
|
||||
]);
|
||||
require(`./plugins/irc-events/${plugin}`).apply(client, [network.irc, network]);
|
||||
});
|
||||
|
||||
if (network.userDisconnected) {
|
||||
network.channels[0].pushMessage(client, new Msg({
|
||||
text: "You have manually disconnected from this network before, use the /connect command to connect again.",
|
||||
}), true);
|
||||
network.channels[0].pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
text:
|
||||
"You have manually disconnected from this network before, use the /connect command to connect again.",
|
||||
}),
|
||||
true
|
||||
);
|
||||
} else {
|
||||
network.irc.connect();
|
||||
}
|
||||
|
@ -248,7 +260,10 @@ Client.prototype.generateToken = function(callback) {
|
|||
};
|
||||
|
||||
Client.prototype.calculateTokenHash = function(token) {
|
||||
return crypto.createHash("sha512").update(token).digest("hex");
|
||||
return crypto
|
||||
.createHash("sha512")
|
||||
.update(token)
|
||||
.digest("hex");
|
||||
};
|
||||
|
||||
Client.prototype.updateSession = function(token, ip, request) {
|
||||
|
@ -284,16 +299,20 @@ Client.prototype.updateSession = function(token, ip, request) {
|
|||
Client.prototype.setPassword = function(hash, callback) {
|
||||
const client = this;
|
||||
|
||||
client.manager.updateUser(client.name, {
|
||||
password: hash,
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
return callback(false);
|
||||
}
|
||||
client.manager.updateUser(
|
||||
client.name,
|
||||
{
|
||||
password: hash,
|
||||
},
|
||||
function(err) {
|
||||
if (err) {
|
||||
return callback(false);
|
||||
}
|
||||
|
||||
client.config.password = hash;
|
||||
return callback(true);
|
||||
});
|
||||
client.config.password = hash;
|
||||
return callback(true);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
Client.prototype.input = function(data) {
|
||||
|
@ -321,10 +340,13 @@ Client.prototype.inputLine = function(data) {
|
|||
// This is either a normal message or a command escaped with a leading '/'
|
||||
if (text.charAt(0) !== "/" || text.charAt(1) === "/") {
|
||||
if (target.chan.type === Chan.Type.LOBBY) {
|
||||
target.chan.pushMessage(this, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "Messages can not be sent to lobbies.",
|
||||
}));
|
||||
target.chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "Messages can not be sent to lobbies.",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -351,17 +373,25 @@ Client.prototype.inputLine = function(data) {
|
|||
|
||||
if (typeof plugin.input === "function" && (connected || plugin.allowDisconnected)) {
|
||||
connected = true;
|
||||
plugin.input(new PublicClient(client), {network: target.network, chan: target.chan}, cmd, args);
|
||||
plugin.input(
|
||||
new PublicClient(client),
|
||||
{network: target.network, chan: target.chan},
|
||||
cmd,
|
||||
args
|
||||
);
|
||||
}
|
||||
} else if (connected) {
|
||||
irc.raw(text);
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
target.chan.pushMessage(this, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "You are not connected to the IRC network, unable to send your command.",
|
||||
}));
|
||||
target.chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "You are not connected to the IRC network, unable to send your command.",
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -385,7 +415,10 @@ Client.prototype.compileCustomHighlights = function() {
|
|||
return;
|
||||
}
|
||||
|
||||
client.highlightRegex = new RegExp(`(?:^|[ .,+!?|/:<>(){}'"@&~-])(?:${highlightsTokens.join("|")})(?:$|[ .,+!?|/:<>(){}'"-])`, "i");
|
||||
client.highlightRegex = new RegExp(
|
||||
`(?:^|[ .,+!?|/:<>(){}'"@&~-])(?:${highlightsTokens.join("|")})(?:$|[ .,+!?|/:<>(){}'"-])`,
|
||||
"i"
|
||||
);
|
||||
};
|
||||
|
||||
Client.prototype.more = function(data) {
|
||||
|
@ -458,45 +491,45 @@ Client.prototype.sort = function(data) {
|
|||
}
|
||||
|
||||
switch (data.type) {
|
||||
case "networks":
|
||||
this.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
|
||||
case "networks":
|
||||
this.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
|
||||
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort", {
|
||||
order: this.networks.map((obj) => obj.uuid),
|
||||
type: data.type,
|
||||
});
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort", {
|
||||
order: this.networks.map((obj) => obj.uuid),
|
||||
type: data.type,
|
||||
});
|
||||
|
||||
break;
|
||||
break;
|
||||
|
||||
case "channels": {
|
||||
const network = _.find(this.networks, {uuid: data.target});
|
||||
case "channels": {
|
||||
const network = _.find(this.networks, {uuid: data.target});
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.sort((a, b) => {
|
||||
// Always sort lobby to the top regardless of what the client has sent
|
||||
// Because there's a lot of code that presumes channels[0] is the lobby
|
||||
if (a.type === Chan.Type.LOBBY) {
|
||||
return -1;
|
||||
} else if (b.type === Chan.Type.LOBBY) {
|
||||
return 1;
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
return order.indexOf(a.id) - order.indexOf(b.id);
|
||||
});
|
||||
network.channels.sort((a, b) => {
|
||||
// Always sort lobby to the top regardless of what the client has sent
|
||||
// Because there's a lot of code that presumes channels[0] is the lobby
|
||||
if (a.type === Chan.Type.LOBBY) {
|
||||
return -1;
|
||||
} else if (b.type === Chan.Type.LOBBY) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort", {
|
||||
order: network.channels.map((obj) => obj.id),
|
||||
type: data.type,
|
||||
target: network.uuid,
|
||||
});
|
||||
return order.indexOf(a.id) - order.indexOf(b.id);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort", {
|
||||
order: network.channels.map((obj) => obj.id),
|
||||
type: data.type,
|
||||
target: network.uuid,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.save();
|
||||
|
@ -581,9 +614,14 @@ Client.prototype.clientDetach = function(socketId) {
|
|||
};
|
||||
|
||||
Client.prototype.registerPushSubscription = function(session, subscription, noSave) {
|
||||
if (!_.isPlainObject(subscription) || !_.isPlainObject(subscription.keys)
|
||||
|| typeof subscription.endpoint !== "string" || !/^https?:\/\//.test(subscription.endpoint)
|
||||
|| typeof subscription.keys.p256dh !== "string" || typeof subscription.keys.auth !== "string") {
|
||||
if (
|
||||
!_.isPlainObject(subscription) ||
|
||||
!_.isPlainObject(subscription.keys) ||
|
||||
typeof subscription.endpoint !== "string" ||
|
||||
!/^https?:\/\//.test(subscription.endpoint) ||
|
||||
typeof subscription.keys.p256dh !== "string" ||
|
||||
typeof subscription.keys.auth !== "string"
|
||||
) {
|
||||
session.pushSubscription = null;
|
||||
return;
|
||||
}
|
||||
|
@ -614,13 +652,17 @@ Client.prototype.unregisterPushSubscription = function(token) {
|
|||
});
|
||||
};
|
||||
|
||||
Client.prototype.save = _.debounce(function SaveClient() {
|
||||
if (Helper.config.public) {
|
||||
return;
|
||||
}
|
||||
Client.prototype.save = _.debounce(
|
||||
function SaveClient() {
|
||||
if (Helper.config.public) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = this;
|
||||
const json = {};
|
||||
json.networks = this.networks.map((n) => n.export());
|
||||
client.manager.updateUser(client.name, json);
|
||||
}, 1000, {maxWait: 10000});
|
||||
const client = this;
|
||||
const json = {};
|
||||
json.networks = this.networks.map((n) => n.export());
|
||||
client.manager.updateUser(client.name, json);
|
||||
},
|
||||
1000,
|
||||
{maxWait: 10000}
|
||||
);
|
||||
|
|
|
@ -31,7 +31,9 @@ ClientManager.prototype.findClient = function(name) {
|
|||
|
||||
ClientManager.prototype.autoloadUsers = function() {
|
||||
const users = this.getUsers();
|
||||
const noUsersWarning = `There are currently no users. Create one with ${colors.bold("thelounge add <name>")}.`;
|
||||
const noUsersWarning = `There are currently no users. Create one with ${colors.bold(
|
||||
"thelounge add <name>"
|
||||
)}.`;
|
||||
|
||||
if (users.length === 0) {
|
||||
log.info(noUsersWarning);
|
||||
|
@ -39,28 +41,35 @@ ClientManager.prototype.autoloadUsers = function() {
|
|||
|
||||
users.forEach((name) => this.loadUser(name));
|
||||
|
||||
fs.watch(Helper.getUsersPath(), _.debounce(() => {
|
||||
const loaded = this.clients.map((c) => c.name);
|
||||
const updatedUsers = this.getUsers();
|
||||
fs.watch(
|
||||
Helper.getUsersPath(),
|
||||
_.debounce(
|
||||
() => {
|
||||
const loaded = this.clients.map((c) => c.name);
|
||||
const updatedUsers = this.getUsers();
|
||||
|
||||
if (updatedUsers.length === 0) {
|
||||
log.info(noUsersWarning);
|
||||
}
|
||||
if (updatedUsers.length === 0) {
|
||||
log.info(noUsersWarning);
|
||||
}
|
||||
|
||||
// Reload all users. Existing users will only have their passwords reloaded.
|
||||
updatedUsers.forEach((name) => this.loadUser(name));
|
||||
// Reload all users. Existing users will only have their passwords reloaded.
|
||||
updatedUsers.forEach((name) => this.loadUser(name));
|
||||
|
||||
// Existing users removed since last time users were loaded
|
||||
_.difference(loaded, updatedUsers).forEach((name) => {
|
||||
const client = _.find(this.clients, {name});
|
||||
// Existing users removed since last time users were loaded
|
||||
_.difference(loaded, updatedUsers).forEach((name) => {
|
||||
const client = _.find(this.clients, {name});
|
||||
|
||||
if (client) {
|
||||
client.quit(true);
|
||||
this.clients = _.without(this.clients, client);
|
||||
log.info(`User ${colors.bold(name)} disconnected and removed.`);
|
||||
}
|
||||
});
|
||||
}, 1000, {maxWait: 10000}));
|
||||
if (client) {
|
||||
client.quit(true);
|
||||
this.clients = _.without(this.clients, client);
|
||||
log.info(`User ${colors.bold(name)} disconnected and removed.`);
|
||||
}
|
||||
});
|
||||
},
|
||||
1000,
|
||||
{maxWait: 10000}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
ClientManager.prototype.loadUser = function(name) {
|
||||
|
|
|
@ -8,7 +8,8 @@ const program = require("commander");
|
|||
const Helper = require("../helper");
|
||||
const Utils = require("./utils");
|
||||
|
||||
program.version(Helper.getVersion(), "-v, --version")
|
||||
program
|
||||
.version(Helper.getVersion(), "-v, --version")
|
||||
.option(
|
||||
"-c, --config <key=value>",
|
||||
"override entries of the configuration file, must be specified for each entry that needs to be overriden",
|
||||
|
@ -26,13 +27,21 @@ if (process.getuid) {
|
|||
const uid = process.getuid();
|
||||
|
||||
if (uid === 0) {
|
||||
log.warn(`You are currently running The Lounge as root. ${colors.bold.red("We highly discourage running as root!")}`);
|
||||
log.warn(
|
||||
`You are currently running The Lounge as root. ${colors.bold.red(
|
||||
"We highly discourage running as root!"
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
fs.stat(path.join(Helper.getHomePath(), "config.js"), (err, stat) => {
|
||||
if (!err && stat.uid !== uid) {
|
||||
log.warn("Config file owner does not match the user you are currently running The Lounge as.");
|
||||
log.warn("To avoid issues, you should execute The Lounge commands under the same user.");
|
||||
log.warn(
|
||||
"Config file owner does not match the user you are currently running The Lounge as."
|
||||
);
|
||||
log.warn(
|
||||
"To avoid issues, you should execute The Lounge commands under the same user."
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -30,41 +30,63 @@ program
|
|||
packageJson(packageName, {
|
||||
fullMetadata: true,
|
||||
version: packageVersion,
|
||||
}).then((json) => {
|
||||
if (!("thelounge" in json)) {
|
||||
log.error(`${colors.red(json.name + " v" + json.version)} does not have The Lounge metadata.`);
|
||||
})
|
||||
.then((json) => {
|
||||
if (!("thelounge" in json)) {
|
||||
log.error(
|
||||
`${colors.red(
|
||||
json.name + " v" + json.version
|
||||
)} does not have The Lounge metadata.`
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log.info(`Installing ${colors.green(json.name + " v" + json.version)}...`);
|
||||
|
||||
const packagesPath = Helper.getPackagesPath();
|
||||
const packagesConfig = path.join(packagesPath, "package.json");
|
||||
|
||||
// Create node_modules folder, otherwise yarn will start walking upwards to find one
|
||||
fsextra.ensureDirSync(path.join(packagesPath, "node_modules"));
|
||||
|
||||
// Create package.json with private set to true, if it doesn't exist already
|
||||
if (!fs.existsSync(packagesConfig)) {
|
||||
fs.writeFileSync(
|
||||
packagesConfig,
|
||||
JSON.stringify(
|
||||
{
|
||||
private: true,
|
||||
description:
|
||||
"Packages for The Lounge. All packages in node_modules directory will be automatically loaded.",
|
||||
},
|
||||
null,
|
||||
"\t"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Utils.executeYarnCommand(
|
||||
"add",
|
||||
"--production",
|
||||
"--exact",
|
||||
`${json.name}@${json.version}`
|
||||
)
|
||||
.then(() => {
|
||||
log.info(
|
||||
`${colors.green(
|
||||
json.name + " v" + json.version
|
||||
)} has been successfully installed.`
|
||||
);
|
||||
})
|
||||
.catch((code) => {
|
||||
throw `Failed to install ${colors.green(
|
||||
json.name + " v" + json.version
|
||||
)}. Exit code: ${code}`;
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
log.error(`${e}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log.info(`Installing ${colors.green(json.name + " v" + json.version)}...`);
|
||||
|
||||
const packagesPath = Helper.getPackagesPath();
|
||||
const packagesConfig = path.join(packagesPath, "package.json");
|
||||
|
||||
// Create node_modules folder, otherwise yarn will start walking upwards to find one
|
||||
fsextra.ensureDirSync(path.join(packagesPath, "node_modules"));
|
||||
|
||||
// Create package.json with private set to true, if it doesn't exist already
|
||||
if (!fs.existsSync(packagesConfig)) {
|
||||
fs.writeFileSync(packagesConfig, JSON.stringify({
|
||||
private: true,
|
||||
description: "Packages for The Lounge. All packages in node_modules directory will be automatically loaded.",
|
||||
}, null, "\t"));
|
||||
}
|
||||
|
||||
return Utils.executeYarnCommand(
|
||||
"add",
|
||||
"--production",
|
||||
"--exact",
|
||||
`${json.name}@${json.version}`
|
||||
).then(() => {
|
||||
log.info(`${colors.green(json.name + " v" + json.version)} has been successfully installed.`);
|
||||
}).catch((code) => {
|
||||
throw `Failed to install ${colors.green(json.name + " v" + json.version)}. Exit code: ${code}`;
|
||||
});
|
||||
}).catch((e) => {
|
||||
log.error(`${e}`);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,13 +24,10 @@ function initalizeConfig() {
|
|||
if (!fs.existsSync(Helper.getConfigPath())) {
|
||||
fsextra.ensureDirSync(Helper.getHomePath());
|
||||
fs.chmodSync(Helper.getHomePath(), "0700");
|
||||
fsextra.copySync(path.resolve(path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"defaults",
|
||||
"config.js"
|
||||
)), Helper.getConfigPath());
|
||||
fsextra.copySync(
|
||||
path.resolve(path.join(__dirname, "..", "..", "defaults", "config.js")),
|
||||
Helper.getConfigPath()
|
||||
);
|
||||
log.info(`Configuration file created at ${colors.green(Helper.getConfigPath())}.`);
|
||||
}
|
||||
|
||||
|
|
|
@ -32,18 +32,20 @@ program
|
|||
|
||||
const packages = JSON.parse(fs.readFileSync(packagesConfig, "utf-8"));
|
||||
|
||||
if (!packages.dependencies || !Object.prototype.hasOwnProperty.call(packages.dependencies, packageName)) {
|
||||
if (
|
||||
!packages.dependencies ||
|
||||
!Object.prototype.hasOwnProperty.call(packages.dependencies, packageName)
|
||||
) {
|
||||
log.warn(packageWasNotInstalled);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return Utils.executeYarnCommand(
|
||||
"remove",
|
||||
packageName
|
||||
).then(() => {
|
||||
log.info(`${colors.green(packageName)} has been successfully uninstalled.`);
|
||||
}).catch((code) => {
|
||||
log.error(`Failed to uninstall ${colors.green(packageName)}. Exit code: ${code}`);
|
||||
process.exit(1);
|
||||
});
|
||||
return Utils.executeYarnCommand("remove", packageName)
|
||||
.then(() => {
|
||||
log.info(`${colors.green(packageName)} has been successfully uninstalled.`);
|
||||
})
|
||||
.catch((code) => {
|
||||
log.error(`Failed to uninstall ${colors.green(packageName)}. Exit code: ${code}`);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,11 +18,7 @@ program
|
|||
const packagesPath = Helper.getPackagesPath();
|
||||
const packagesConfig = path.join(packagesPath, "package.json");
|
||||
const packagesList = JSON.parse(fs.readFileSync(packagesConfig)).dependencies;
|
||||
const argsList = [
|
||||
"upgrade",
|
||||
"--production",
|
||||
"--latest",
|
||||
];
|
||||
const argsList = ["upgrade", "--production", "--latest"];
|
||||
|
||||
let count = 0;
|
||||
|
||||
|
@ -54,9 +50,11 @@ program
|
|||
return;
|
||||
}
|
||||
|
||||
return Utils.executeYarnCommand(...argsList).then(() => {
|
||||
log.info("Package(s) have been successfully upgraded.");
|
||||
}).catch((code) => {
|
||||
throw `Failed to upgrade package(s). Exit code ${code}`;
|
||||
});
|
||||
return Utils.executeYarnCommand(...argsList)
|
||||
.then(() => {
|
||||
log.info("Package(s) have been successfully upgraded.");
|
||||
})
|
||||
.catch((code) => {
|
||||
throw `Failed to upgrade package(s). Exit code ${code}`;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,7 +21,8 @@ program
|
|||
const manager = new ClientManager();
|
||||
const users = manager.getUsers();
|
||||
|
||||
if (users === undefined) { // There was an error, already logged
|
||||
if (users === undefined) {
|
||||
// There was an error, already logged
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -30,31 +31,37 @@ program
|
|||
return;
|
||||
}
|
||||
|
||||
log.prompt({
|
||||
text: "Enter password:",
|
||||
silent: true,
|
||||
}, function(err, password) {
|
||||
if (!password) {
|
||||
log.error("Password cannot be empty.");
|
||||
return;
|
||||
}
|
||||
log.prompt(
|
||||
{
|
||||
text: "Enter password:",
|
||||
silent: true,
|
||||
},
|
||||
function(err, password) {
|
||||
if (!password) {
|
||||
log.error("Password cannot be empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!err) {
|
||||
log.prompt({
|
||||
text: "Save logs to disk?",
|
||||
default: "yes",
|
||||
}, function(err2, enableLog) {
|
||||
if (!err2) {
|
||||
add(
|
||||
manager,
|
||||
name,
|
||||
password,
|
||||
enableLog.charAt(0).toLowerCase() === "y"
|
||||
);
|
||||
}
|
||||
});
|
||||
if (!err) {
|
||||
log.prompt(
|
||||
{
|
||||
text: "Save logs to disk?",
|
||||
default: "yes",
|
||||
},
|
||||
function(err2, enableLog) {
|
||||
if (!err2) {
|
||||
add(
|
||||
manager,
|
||||
name,
|
||||
password,
|
||||
enableLog.charAt(0).toLowerCase() === "y"
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
|
||||
function add(manager, name, password, enableLog) {
|
||||
|
|
|
@ -21,7 +21,8 @@ program
|
|||
const ClientManager = require("../../clientManager");
|
||||
const users = new ClientManager().getUsers();
|
||||
|
||||
if (users === undefined) { // There was an error, already logged
|
||||
if (users === undefined) {
|
||||
// There was an error, already logged
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -36,6 +37,10 @@ program
|
|||
{stdio: "inherit"}
|
||||
);
|
||||
child_spawn.on("error", function() {
|
||||
log.error(`Unable to open ${colors.green(Helper.getUserConfigPath(name))}. ${colors.bold("$EDITOR")} is not set, and ${colors.bold("vi")} was not found.`);
|
||||
log.error(
|
||||
`Unable to open ${colors.green(Helper.getUserConfigPath(name))}. ${colors.bold(
|
||||
"$EDITOR"
|
||||
)} is not set, and ${colors.bold("vi")} was not found.`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,7 +20,8 @@ program
|
|||
const ClientManager = require("../../clientManager");
|
||||
const users = new ClientManager().getUsers();
|
||||
|
||||
if (users === undefined) { // There was an error, already logged
|
||||
if (users === undefined) {
|
||||
// There was an error, already logged
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -30,6 +31,10 @@ program
|
|||
log.info(`${i + 1}. ${colors.bold(user)}`);
|
||||
});
|
||||
} else {
|
||||
log.info(`There are currently no users. Create one with ${colors.bold("thelounge add <name>")}.`);
|
||||
log.info(
|
||||
`There are currently no users. Create one with ${colors.bold(
|
||||
"thelounge add <name>"
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -20,7 +20,8 @@ program
|
|||
const ClientManager = require("../../clientManager");
|
||||
const users = new ClientManager().getUsers();
|
||||
|
||||
if (users === undefined) { // There was an error, already logged
|
||||
if (users === undefined) {
|
||||
// There was an error, already logged
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -31,20 +32,20 @@ program
|
|||
|
||||
const file = Helper.getUserConfigPath(name);
|
||||
const user = require(file);
|
||||
log.prompt({
|
||||
text: "Enter new password:",
|
||||
silent: true,
|
||||
}, function(err, password) {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
log.prompt(
|
||||
{
|
||||
text: "Enter new password:",
|
||||
silent: true,
|
||||
},
|
||||
function(err, password) {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
user.password = Helper.password.hash(password);
|
||||
user.sessions = {};
|
||||
fs.writeFileSync(
|
||||
file,
|
||||
JSON.stringify(user, null, "\t")
|
||||
);
|
||||
log.info(`Successfully reset password for ${colors.bold(name)}.`);
|
||||
});
|
||||
user.password = Helper.password.hash(password);
|
||||
user.sessions = {};
|
||||
fs.writeFileSync(file, JSON.stringify(user, null, "\t"));
|
||||
log.info(`Successfully reset password for ${colors.bold(name)}.`);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -16,7 +16,9 @@ class Utils {
|
|||
"",
|
||||
" Environment variable:",
|
||||
"",
|
||||
` THELOUNGE_HOME Path for all configuration files and folders. Defaults to ${colors.green(Helper.expandHome(Utils.defaultHome()))}.`,
|
||||
` THELOUNGE_HOME Path for all configuration files and folders. Defaults to ${colors.green(
|
||||
Helper.expandHome(Utils.defaultHome())
|
||||
)}.`,
|
||||
"",
|
||||
].forEach((e) => log.raw(e));
|
||||
}
|
||||
|
@ -32,8 +34,16 @@ class Utils {
|
|||
|
||||
console.log(); // eslint-disable-line no-console
|
||||
log.warn(`Folder ${colors.bold.red(oldHome)} still exists.`);
|
||||
log.warn(`In v3, we renamed the default configuration folder to ${colors.bold.green(".thelounge")} for consistency.`);
|
||||
log.warn(`You might want to rename the folder from ${colors.bold.red(".lounge")} to ${colors.bold.green(".thelounge")} to keep existing configuration.`);
|
||||
log.warn(
|
||||
`In v3, we renamed the default configuration folder to ${colors.bold.green(
|
||||
".thelounge"
|
||||
)} for consistency.`
|
||||
);
|
||||
log.warn(
|
||||
`You might want to rename the folder from ${colors.bold.red(
|
||||
".lounge"
|
||||
)} to ${colors.bold.green(".thelounge")} to keep existing configuration.`
|
||||
);
|
||||
log.warn("Make sure to look at the release notes to see other breaking changes.");
|
||||
console.log(); // eslint-disable-line no-console
|
||||
}
|
||||
|
@ -43,12 +53,7 @@ class Utils {
|
|||
return home;
|
||||
}
|
||||
|
||||
const distConfig = path.resolve(path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
".thelounge_home"
|
||||
));
|
||||
const distConfig = path.resolve(path.join(__dirname, "..", "..", ".thelounge_home"));
|
||||
|
||||
home = fs.readFileSync(distConfig, "utf-8").trim();
|
||||
|
||||
|
@ -71,9 +76,11 @@ class Utils {
|
|||
return undefined;
|
||||
} else if (value === "null") {
|
||||
return null;
|
||||
} else if (/^-?[0-9]+$/.test(value)) { // Numbers like port
|
||||
} else if (/^-?[0-9]+$/.test(value)) {
|
||||
// Numbers like port
|
||||
value = parseInt(value, 10);
|
||||
} else if (/^\[.*\]$/.test(value)) { // Arrays
|
||||
} else if (/^\[.*\]$/.test(value)) {
|
||||
// Arrays
|
||||
// Supporting arrays `[a,b]` and `[a, b]`
|
||||
const array = value.slice(1, -1).split(/,\s*/);
|
||||
|
||||
|
@ -134,23 +141,29 @@ class Utils {
|
|||
]);
|
||||
|
||||
add.stdout.on("data", (data) => {
|
||||
data.toString().trim().split("\n").forEach((line) => {
|
||||
line = JSON.parse(line);
|
||||
data.toString()
|
||||
.trim()
|
||||
.split("\n")
|
||||
.forEach((line) => {
|
||||
line = JSON.parse(line);
|
||||
|
||||
if (line.type === "success") {
|
||||
success = true;
|
||||
}
|
||||
});
|
||||
if (line.type === "success") {
|
||||
success = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
add.stderr.on("data", (data) => {
|
||||
data.toString().trim().split("\n").forEach((line) => {
|
||||
const json = JSON.parse(line);
|
||||
data.toString()
|
||||
.trim()
|
||||
.split("\n")
|
||||
.forEach((line) => {
|
||||
const json = JSON.parse(line);
|
||||
|
||||
if (json.type === "error") {
|
||||
log.error(json.data);
|
||||
}
|
||||
});
|
||||
if (json.type === "error") {
|
||||
log.error(json.data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
add.on("error", (e) => {
|
||||
|
|
|
@ -50,12 +50,7 @@ const Helper = {
|
|||
|
||||
module.exports = Helper;
|
||||
|
||||
Helper.config = require(path.resolve(path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"defaults",
|
||||
"config.js"
|
||||
)));
|
||||
Helper.config = require(path.resolve(path.join(__dirname, "..", "defaults", "config.js")));
|
||||
|
||||
function getVersion() {
|
||||
const gitCommit = getGitCommit();
|
||||
|
@ -92,7 +87,10 @@ function getGitCommit() {
|
|||
}
|
||||
|
||||
function getVersionCacheBust() {
|
||||
const hash = crypto.createHash("sha256").update(Helper.getVersion()).digest("hex");
|
||||
const hash = crypto
|
||||
.createHash("sha256")
|
||||
.update(Helper.getVersion())
|
||||
.digest("hex");
|
||||
|
||||
return hash.substring(0, 10);
|
||||
}
|
||||
|
@ -111,8 +109,16 @@ function setHome(newPath) {
|
|||
const userConfig = require(configPath);
|
||||
|
||||
if (_.isEmpty(userConfig)) {
|
||||
log.warn(`The file located at ${colors.green(configPath)} does not appear to expose anything.`);
|
||||
log.warn(`Make sure it is non-empty and the configuration is exported using ${colors.bold("module.exports = { ... }")}.`);
|
||||
log.warn(
|
||||
`The file located at ${colors.green(
|
||||
configPath
|
||||
)} does not appear to expose anything.`
|
||||
);
|
||||
log.warn(
|
||||
`Make sure it is non-empty and the configuration is exported using ${colors.bold(
|
||||
"module.exports = { ... }"
|
||||
)}.`
|
||||
);
|
||||
log.warn("Using default configuration...");
|
||||
}
|
||||
|
||||
|
@ -122,14 +128,24 @@ function setHome(newPath) {
|
|||
if (!this.config.displayNetwork && !this.config.lockNetwork) {
|
||||
this.config.lockNetwork = true;
|
||||
|
||||
log.warn(`${colors.bold("displayNetwork")} and ${colors.bold("lockNetwork")} are false, setting ${colors.bold("lockNetwork")} to true.`);
|
||||
log.warn(
|
||||
`${colors.bold("displayNetwork")} and ${colors.bold(
|
||||
"lockNetwork"
|
||||
)} are false, setting ${colors.bold("lockNetwork")} to true.`
|
||||
);
|
||||
}
|
||||
|
||||
const manifestPath = path.resolve(path.join(__dirname, "..", "public", "thelounge.webmanifest"));
|
||||
const manifestPath = path.resolve(
|
||||
path.join(__dirname, "..", "public", "thelounge.webmanifest")
|
||||
);
|
||||
|
||||
// Check if manifest exists, if not, the app most likely was not built
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
log.error(`The client application was not built. Run ${colors.bold("NODE_ENV=production yarn build")} to resolve this.`);
|
||||
log.error(
|
||||
`The client application was not built. Run ${colors.bold(
|
||||
"NODE_ENV=production yarn build"
|
||||
)} to resolve this.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
@ -140,13 +156,27 @@ function setHome(newPath) {
|
|||
// TODO: Remove in future release
|
||||
if (["example", "crypto", "zenburn"].includes(this.config.theme)) {
|
||||
if (this.config.theme === "example") {
|
||||
log.warn(`The default theme ${colors.red("example")} was renamed to ${colors.green("default")} as of The Lounge v3.`);
|
||||
log.warn(
|
||||
`The default theme ${colors.red("example")} was renamed to ${colors.green(
|
||||
"default"
|
||||
)} as of The Lounge v3.`
|
||||
);
|
||||
} else {
|
||||
log.warn(`The theme ${colors.red(this.config.theme)} was moved to a separate theme as of The Lounge v3.`);
|
||||
log.warn(`Install it with ${colors.bold("thelounge install thelounge-theme-" + this.config.theme)}.`);
|
||||
log.warn(
|
||||
`The theme ${colors.red(
|
||||
this.config.theme
|
||||
)} was moved to a separate theme as of The Lounge v3.`
|
||||
);
|
||||
log.warn(
|
||||
`Install it with ${colors.bold(
|
||||
"thelounge install thelounge-theme-" + this.config.theme
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
|
||||
log.warn(`Falling back to theme ${colors.green("default")} will be removed in a future release.`);
|
||||
log.warn(
|
||||
`Falling back to theme ${colors.green("default")} will be removed in a future release.`
|
||||
);
|
||||
log.warn("Please update your configuration file accordingly.");
|
||||
|
||||
this.config.theme = "default";
|
||||
|
@ -195,15 +225,18 @@ function ip2hex(address) {
|
|||
return "00000000";
|
||||
}
|
||||
|
||||
return address.split(".").map(function(octet) {
|
||||
let hex = parseInt(octet, 10).toString(16);
|
||||
return address
|
||||
.split(".")
|
||||
.map(function(octet) {
|
||||
let hex = parseInt(octet, 10).toString(16);
|
||||
|
||||
if (hex.length === 1) {
|
||||
hex = "0" + hex;
|
||||
}
|
||||
if (hex.length === 1) {
|
||||
hex = "0" + hex;
|
||||
}
|
||||
|
||||
return hex;
|
||||
}).join("");
|
||||
return hex;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
// Expand ~ into the current user home dir.
|
||||
|
@ -246,7 +279,11 @@ function mergeConfig(oldConfig, newConfig) {
|
|||
|
||||
return _.mergeWith(oldConfig, newConfig, (objValue, srcValue, key) => {
|
||||
// Do not override config variables if the type is incorrect (e.g. object changed into a string)
|
||||
if (typeof objValue !== "undefined" && objValue !== null && typeof objValue !== typeof srcValue) {
|
||||
if (
|
||||
typeof objValue !== "undefined" &&
|
||||
objValue !== null &&
|
||||
typeof objValue !== typeof srcValue
|
||||
) {
|
||||
log.warn(`Incorrect type for "${colors.bold(key)}", please verify your config.`);
|
||||
|
||||
return objValue;
|
||||
|
@ -296,5 +333,9 @@ function parseHostmask(hostmask) {
|
|||
}
|
||||
|
||||
function compareHostmask(a, b) {
|
||||
return (a.nick.toLowerCase() === b.nick.toLowerCase() || a.nick === "*") && (a.ident.toLowerCase() === b.ident.toLowerCase() || a.ident === "*") && (a.hostname.toLowerCase() === b.hostname.toLowerCase() || a.hostname === "*");
|
||||
return (
|
||||
(a.nick.toLowerCase() === b.nick.toLowerCase() || a.nick === "*") &&
|
||||
(a.ident.toLowerCase() === b.ident.toLowerCase() || a.ident === "*") &&
|
||||
(a.hostname.toLowerCase() === b.hostname.toLowerCase() || a.hostname === "*")
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,22 +20,31 @@ class Identification {
|
|||
|
||||
if (Helper.config.identd.enable) {
|
||||
if (this.oidentdFile) {
|
||||
log.warn("Using both identd and oidentd at the same time, this is most likely not intended.");
|
||||
log.warn(
|
||||
"Using both identd and oidentd at the same time, this is most likely not intended."
|
||||
);
|
||||
}
|
||||
|
||||
const server = net.createServer(this.serverConnection.bind(this));
|
||||
|
||||
server.on("error", (err) => log.error(`Identd server error: ${err}`));
|
||||
|
||||
server.listen({
|
||||
port: Helper.config.identd.port || 113,
|
||||
host: Helper.config.bind,
|
||||
}, () => {
|
||||
const address = server.address();
|
||||
log.info(`Identd server available on ${colors.green(address.address + ":" + address.port)}`);
|
||||
server.listen(
|
||||
{
|
||||
port: Helper.config.identd.port || 113,
|
||||
host: Helper.config.bind,
|
||||
},
|
||||
() => {
|
||||
const address = server.address();
|
||||
log.info(
|
||||
`Identd server available on ${colors.green(
|
||||
address.address + ":" + address.port
|
||||
)}`
|
||||
);
|
||||
|
||||
startedCallback(this);
|
||||
});
|
||||
startedCallback(this);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
startedCallback(this);
|
||||
}
|
||||
|
@ -61,7 +70,9 @@ class Identification {
|
|||
|
||||
for (const connection of this.connections.values()) {
|
||||
if (connection.socket.remotePort === fport && connection.socket.localPort === lport) {
|
||||
return socket.write(`${lport}, ${fport} : USERID : TheLounge : ${connection.user}\r\n`);
|
||||
return socket.write(
|
||||
`${lport}, ${fport} : USERID : TheLounge : ${connection.user}\r\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,9 +103,10 @@ class Identification {
|
|||
let file = "# Warning: file generated by The Lounge: changes will be overwritten!\n";
|
||||
|
||||
this.connections.forEach((connection) => {
|
||||
file += `fport ${connection.socket.remotePort}`
|
||||
+ ` lport ${connection.socket.localPort}`
|
||||
+ ` { reply "${connection.user}" }\n`;
|
||||
file +=
|
||||
`fport ${connection.socket.remotePort}` +
|
||||
` lport ${connection.socket.localPort}` +
|
||||
` { reply "${connection.user}" }\n`;
|
||||
});
|
||||
|
||||
fs.writeFile(this.oidentdFile, file, {flag: "w+"}, function(err) {
|
||||
|
|
|
@ -4,7 +4,10 @@ const colors = require("chalk");
|
|||
const read = require("read");
|
||||
|
||||
function timestamp() {
|
||||
const datetime = (new Date()).toISOString().split(".")[0].replace("T", " ");
|
||||
const datetime = new Date()
|
||||
.toISOString()
|
||||
.split(".")[0]
|
||||
.replace("T", " ");
|
||||
|
||||
return colors.dim(datetime);
|
||||
}
|
||||
|
|
|
@ -176,14 +176,13 @@ Chan.prototype.getFilteredClone = function(lastActiveChannel, lastMessage) {
|
|||
if (lastMessage > -1) {
|
||||
// When reconnecting, always send up to 100 messages to prevent message gaps on the client
|
||||
// See https://github.com/thelounge/thelounge/issues/1883
|
||||
newChannel[prop] = this[prop]
|
||||
.filter((m) => m.id > lastMessage)
|
||||
.slice(-100);
|
||||
newChannel[prop] = this[prop].filter((m) => m.id > lastMessage).slice(-100);
|
||||
newChannel.moreHistoryAvailable = this[prop].length > 100;
|
||||
} else {
|
||||
// If channel is active, send up to 100 last messages, for all others send just 1
|
||||
// Client will automatically load more messages whenever needed based on last seen messages
|
||||
const messagesToSend = lastActiveChannel === true || this.id === lastActiveChannel ? 100 : 1;
|
||||
const messagesToSend =
|
||||
lastActiveChannel === true || this.id === lastActiveChannel ? 100 : 1;
|
||||
|
||||
newChannel[prop] = this[prop].slice(-messagesToSend);
|
||||
newChannel.moreHistoryAvailable = this[prop].length > messagesToSend;
|
||||
|
|
|
@ -42,12 +42,14 @@ class Msg {
|
|||
return !!this.from.nick;
|
||||
}
|
||||
|
||||
return this.type !== Msg.Type.MOTD &&
|
||||
return (
|
||||
this.type !== Msg.Type.MOTD &&
|
||||
this.type !== Msg.Type.ERROR &&
|
||||
this.type !== Msg.Type.TOPIC_SET_BY &&
|
||||
this.type !== Msg.Type.MODE_CHANNEL &&
|
||||
this.type !== Msg.Type.RAW &&
|
||||
this.type !== Msg.Type.WHOIS;
|
||||
this.type !== Msg.Type.WHOIS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -89,11 +89,20 @@ Network.prototype.validate = function(client) {
|
|||
|
||||
if (Helper.config.lockNetwork) {
|
||||
// This check is needed to prevent invalid user configurations
|
||||
if (!Helper.config.public && this.host && this.host.length > 0 && this.host !== Helper.config.defaults.host) {
|
||||
this.channels[0].pushMessage(client, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "Hostname you specified is not allowed.",
|
||||
}), true);
|
||||
if (
|
||||
!Helper.config.public &&
|
||||
this.host &&
|
||||
this.host.length > 0 &&
|
||||
this.host !== Helper.config.defaults.host
|
||||
) {
|
||||
this.channels[0].pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "Hostname you specified is not allowed.",
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -105,10 +114,14 @@ Network.prototype.validate = function(client) {
|
|||
}
|
||||
|
||||
if (this.host.length === 0) {
|
||||
this.channels[0].pushMessage(client, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "You must specify a hostname to connect.",
|
||||
}), true);
|
||||
this.channels[0].pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "You must specify a hostname to connect.",
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -149,7 +162,10 @@ Network.prototype.createIrcFramework = function(client) {
|
|||
};
|
||||
|
||||
Network.prototype.createWebIrc = function(client) {
|
||||
if (!Helper.config.webirc || !Object.prototype.hasOwnProperty.call(Helper.config.webirc, this.host)) {
|
||||
if (
|
||||
!Helper.config.webirc ||
|
||||
!Object.prototype.hasOwnProperty.call(Helper.config.webirc, this.host)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -213,7 +229,11 @@ Network.prototype.edit = function(client, args) {
|
|||
}
|
||||
}
|
||||
|
||||
if (connected && this.realname !== oldRealname && this.irc.network.cap.isEnabled("draft/setname")) {
|
||||
if (
|
||||
connected &&
|
||||
this.realname !== oldRealname &&
|
||||
this.irc.network.cap.isEnabled("draft/setname")
|
||||
) {
|
||||
this.irc.raw("SETNAME", this.realname);
|
||||
}
|
||||
|
||||
|
@ -241,12 +261,10 @@ Network.prototype.setNick = function(nick) {
|
|||
this.highlightRegex = new RegExp(
|
||||
// Do not match characters and numbers (unless IRC color)
|
||||
"(?:^|[^a-z0-9]|\x03[0-9]{1,2})" +
|
||||
|
||||
// Escape nickname, as it may contain regex stuff
|
||||
_.escapeRegExp(nick) +
|
||||
|
||||
// Do not match characters and numbers
|
||||
"(?:[^a-z0-9]|$)",
|
||||
// Escape nickname, as it may contain regex stuff
|
||||
_.escapeRegExp(nick) +
|
||||
// Do not match characters and numbers
|
||||
"(?:[^a-z0-9]|$)",
|
||||
|
||||
// Case insensitive search
|
||||
"i"
|
||||
|
@ -266,7 +284,9 @@ Network.prototype.getFilteredClone = function(lastActiveChannel, lastMessage) {
|
|||
const filteredNetwork = Object.keys(this).reduce((newNetwork, prop) => {
|
||||
if (prop === "channels") {
|
||||
// Channels objects perform their own cloning
|
||||
newNetwork[prop] = this[prop].map((channel) => channel.getFilteredClone(lastActiveChannel, lastMessage));
|
||||
newNetwork[prop] = this[prop].map((channel) =>
|
||||
channel.getFilteredClone(lastActiveChannel, lastMessage)
|
||||
);
|
||||
} else if (!filteredFromClient[prop]) {
|
||||
// Some properties that are not useful for the client are skipped
|
||||
newNetwork[prop] = this[prop];
|
||||
|
@ -311,8 +331,10 @@ Network.prototype.addChannel = function(newChan) {
|
|||
const compareChan = this.channels[i];
|
||||
|
||||
// Negative if the new chan is alphabetically before the next chan in the list, positive if after
|
||||
if (newChan.name.localeCompare(compareChan.name, {sensitivity: "base"}) <= 0
|
||||
|| (compareChan.type !== Chan.Type.CHANNEL && compareChan.type !== Chan.Type.QUERY)) {
|
||||
if (
|
||||
newChan.name.localeCompare(compareChan.name, {sensitivity: "base"}) <= 0 ||
|
||||
(compareChan.type !== Chan.Type.CHANNEL && compareChan.type !== Chan.Type.QUERY)
|
||||
) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -92,7 +92,9 @@ function advancedLdapAuth(user, password, callback) {
|
|||
res.on("searchEntry", function(entry) {
|
||||
found = true;
|
||||
const bindDN = entry.objectName;
|
||||
log.info(`Auth against LDAP ${config.ldap.url} with found bindDN ${bindDN}`);
|
||||
log.info(
|
||||
`Auth against LDAP ${config.ldap.url} with found bindDN ${bindDN}`
|
||||
);
|
||||
ldapclient.unbind();
|
||||
|
||||
ldapAuthCommon(user, bindDN, password, callback);
|
||||
|
@ -105,7 +107,9 @@ function advancedLdapAuth(user, password, callback) {
|
|||
ldapclient.unbind();
|
||||
|
||||
if (!found) {
|
||||
log.warn(`LDAP Search did not find anything for: ${userDN} (${result.status})`);
|
||||
log.warn(
|
||||
`LDAP Search did not find anything for: ${userDN} (${result.status})`
|
||||
);
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -13,7 +13,11 @@ function localAuth(manager, client, user, password, callback) {
|
|||
|
||||
// If this user has no password set, fail the authentication
|
||||
if (!client.config.password) {
|
||||
log.error(`User ${colors.bold(user)} with no local password set tried to sign in. (Probably a LDAP user)`);
|
||||
log.error(
|
||||
`User ${colors.bold(
|
||||
user
|
||||
)} with no local password set tried to sign in. (Probably a LDAP user)`
|
||||
);
|
||||
return callback(false);
|
||||
}
|
||||
|
||||
|
@ -25,13 +29,18 @@ function localAuth(manager, client, user, password, callback) {
|
|||
|
||||
client.setPassword(hash, (success) => {
|
||||
if (success) {
|
||||
log.info(`User ${colors.bold(client.name)} logged in and their hashed password has been updated to match new security requirements`);
|
||||
log.info(
|
||||
`User ${colors.bold(
|
||||
client.name
|
||||
)} logged in and their hashed password has been updated to match new security requirements`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
callback(matching);
|
||||
}).catch((error) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error(`Error while checking users password. Error: ${error}`);
|
||||
});
|
||||
}
|
||||
|
@ -40,4 +49,3 @@ module.exports = {
|
|||
auth: localAuth,
|
||||
isEnabled: () => true,
|
||||
};
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ async function fetch() {
|
|||
try {
|
||||
const response = await got("https://api.github.com/repos/thelounge/thelounge/releases", {
|
||||
headers: {
|
||||
"Accept": "application/vnd.github.v3.html", // Request rendered markdown
|
||||
Accept: "application/vnd.github.v3.html", // Request rendered markdown
|
||||
"User-Agent": pkg.name + "; +" + pkg.repository.git, // Identify the client
|
||||
},
|
||||
});
|
||||
|
|
|
@ -7,10 +7,13 @@ exports.commands = ["slap", "me"];
|
|||
|
||||
exports.input = function({irc}, chan, cmd, args) {
|
||||
if (chan.type !== Chan.Type.CHANNEL && chan.type !== Chan.Type.QUERY) {
|
||||
chan.pushMessage(this, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `${cmd} command can only be used in channels and queries.`,
|
||||
}));
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `${cmd} command can only be used in channels and queries.`,
|
||||
})
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -18,27 +21,27 @@ exports.input = function({irc}, chan, cmd, args) {
|
|||
let text;
|
||||
|
||||
switch (cmd) {
|
||||
case "slap":
|
||||
text = "slaps " + args[0] + " around a bit with a large trout";
|
||||
case "slap":
|
||||
text = "slaps " + args[0] + " around a bit with a large trout";
|
||||
/* fall through */
|
||||
case "me":
|
||||
if (args.length === 0) {
|
||||
case "me":
|
||||
if (args.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
text = text || args.join(" ");
|
||||
|
||||
irc.action(chan.name, text);
|
||||
|
||||
if (!irc.network.cap.isEnabled("echo-message")) {
|
||||
irc.emit("action", {
|
||||
nick: irc.user.nick,
|
||||
target: chan.name,
|
||||
message: text,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
text = text || args.join(" ");
|
||||
|
||||
irc.action(chan.name, text);
|
||||
|
||||
if (!irc.network.cap.isEnabled("echo-message")) {
|
||||
irc.emit("action", {
|
||||
nick: irc.user.nick,
|
||||
target: chan.name,
|
||||
message: text,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
@ -9,7 +9,8 @@ exports.input = function(network, chan, cmd, args) {
|
|||
reason = args.join(" ") || " ";
|
||||
|
||||
network.irc.raw("AWAY", reason);
|
||||
} else { // back command
|
||||
} else {
|
||||
// back command
|
||||
network.irc.raw("AWAY");
|
||||
}
|
||||
|
||||
|
|
|
@ -3,42 +3,44 @@
|
|||
const Chan = require("../../models/chan");
|
||||
const Msg = require("../../models/msg");
|
||||
|
||||
exports.commands = [
|
||||
"ban",
|
||||
"unban",
|
||||
"banlist",
|
||||
];
|
||||
exports.commands = ["ban", "unban", "banlist"];
|
||||
|
||||
exports.input = function({irc}, chan, cmd, args) {
|
||||
if (chan.type !== Chan.Type.CHANNEL) {
|
||||
chan.pushMessage(this, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `${cmd} command can only be used in channels.`,
|
||||
}));
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `${cmd} command can only be used in channels.`,
|
||||
})
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd !== "banlist" && args.length === 0) {
|
||||
if (args.length === 0) {
|
||||
chan.pushMessage(this, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `Usage: /${cmd} <nick>`,
|
||||
}));
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `Usage: /${cmd} <nick>`,
|
||||
})
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (cmd) {
|
||||
case "ban":
|
||||
irc.ban(chan.name, args[0]);
|
||||
break;
|
||||
case "unban":
|
||||
irc.unban(chan.name, args[0]);
|
||||
break;
|
||||
case "banlist":
|
||||
irc.banlist(chan.name);
|
||||
break;
|
||||
case "ban":
|
||||
irc.ban(chan.name, args[0]);
|
||||
break;
|
||||
case "unban":
|
||||
irc.unban(chan.name, args[0]);
|
||||
break;
|
||||
case "banlist":
|
||||
irc.banlist(chan.name);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -17,10 +17,13 @@ exports.input = function(network, chan, cmd, args) {
|
|||
}
|
||||
|
||||
if (irc.connection && irc.connection.connected) {
|
||||
chan.pushMessage(this, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "You are already connected.",
|
||||
}));
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "You are already connected.",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,18 +6,24 @@ exports.commands = ["ctcp"];
|
|||
|
||||
exports.input = function({irc}, chan, cmd, args) {
|
||||
if (args.length < 2) {
|
||||
chan.pushMessage(this, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "Usage: /ctcp <nick> <ctcp_type>",
|
||||
}));
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "Usage: /ctcp <nick> <ctcp_type>",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
chan.pushMessage(this, new Msg({
|
||||
type: Msg.Type.CTCP_REQUEST,
|
||||
ctcpMessage: `"${args.slice(1).join(" ")}" to ${args[0]}`,
|
||||
from: chan.getUser(irc.user.nick),
|
||||
}));
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
type: Msg.Type.CTCP_REQUEST,
|
||||
ctcpMessage: `"${args.slice(1).join(" ")}" to ${args[0]}`,
|
||||
from: chan.getUser(irc.user.nick),
|
||||
})
|
||||
);
|
||||
|
||||
irc.ctcpRequest(...args);
|
||||
};
|
||||
|
|
|
@ -4,11 +4,7 @@ const Chan = require("../../models/chan");
|
|||
const Msg = require("../../models/msg");
|
||||
const Helper = require("../../helper");
|
||||
|
||||
exports.commands = [
|
||||
"ignore",
|
||||
"unignore",
|
||||
"ignorelist",
|
||||
];
|
||||
exports.commands = ["ignore", "unignore", "ignorelist"];
|
||||
|
||||
exports.input = function(network, chan, cmd, args) {
|
||||
const client = this;
|
||||
|
@ -16,10 +12,13 @@ exports.input = function(network, chan, cmd, args) {
|
|||
let hostmask;
|
||||
|
||||
if (cmd !== "ignorelist" && (args.length === 0 || args[0].trim().length === 0)) {
|
||||
chan.pushMessage(client, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `Usage: /${cmd} <nick>[!ident][@host]`,
|
||||
}));
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `Usage: /${cmd} <nick>[!ident][@host]`,
|
||||
})
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -31,95 +30,115 @@ exports.input = function(network, chan, cmd, args) {
|
|||
}
|
||||
|
||||
switch (cmd) {
|
||||
case "ignore": {
|
||||
// IRC nicks are case insensitive
|
||||
if (hostmask.nick.toLowerCase() === network.nick.toLowerCase()) {
|
||||
chan.pushMessage(client, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "You can't ignore yourself",
|
||||
}));
|
||||
} else if (!network.ignoreList.some(function(entry) {
|
||||
return Helper.compareHostmask(entry, hostmask);
|
||||
})) {
|
||||
hostmask.when = Date.now();
|
||||
network.ignoreList.push(hostmask);
|
||||
case "ignore": {
|
||||
// IRC nicks are case insensitive
|
||||
if (hostmask.nick.toLowerCase() === network.nick.toLowerCase()) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: "You can't ignore yourself",
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
!network.ignoreList.some(function(entry) {
|
||||
return Helper.compareHostmask(entry, hostmask);
|
||||
})
|
||||
) {
|
||||
hostmask.when = Date.now();
|
||||
network.ignoreList.push(hostmask);
|
||||
|
||||
client.save();
|
||||
chan.pushMessage(client, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `\u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f added to ignorelist`,
|
||||
}));
|
||||
} else {
|
||||
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),
|
||||
});
|
||||
client.save();
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `\u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f added to ignorelist`,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
newChan.data = ignored;
|
||||
|
||||
client.emit("msg:special", {
|
||||
chan: newChan.id,
|
||||
data: ignored,
|
||||
});
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,10 +3,7 @@
|
|||
const Chan = require("../../models/chan");
|
||||
const Msg = require("../../models/msg");
|
||||
|
||||
exports.commands = [
|
||||
"invite",
|
||||
"invitelist",
|
||||
];
|
||||
exports.commands = ["invite", "invitelist"];
|
||||
|
||||
exports.input = function({irc}, chan, cmd, args) {
|
||||
if (cmd === "invitelist") {
|
||||
|
@ -16,12 +13,15 @@ exports.input = function({irc}, chan, cmd, args) {
|
|||
|
||||
if (args.length === 2) {
|
||||
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
|
||||
} else {
|
||||
chan.pushMessage(this, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `${cmd} command can only be used in channels or by specifying a target.`,
|
||||
}));
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `${cmd} command can only be used in channels or by specifying a target.`,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,10 +7,13 @@ exports.commands = ["kick"];
|
|||
|
||||
exports.input = function({irc}, chan, cmd, args) {
|
||||
if (chan.type !== Chan.Type.CHANNEL) {
|
||||
chan.pushMessage(this, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `${cmd} command can only be used in channels.`,
|
||||
}));
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `${cmd} command can only be used in channels.`,
|
||||
})
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -3,32 +3,30 @@
|
|||
const Chan = require("../../models/chan");
|
||||
const Msg = require("../../models/msg");
|
||||
|
||||
exports.commands = [
|
||||
"mode",
|
||||
"op",
|
||||
"deop",
|
||||
"hop",
|
||||
"dehop",
|
||||
"voice",
|
||||
"devoice",
|
||||
];
|
||||
exports.commands = ["mode", "op", "deop", "hop", "dehop", "voice", "devoice"];
|
||||
|
||||
exports.input = function({irc, nick}, chan, cmd, args) {
|
||||
if (cmd !== "mode") {
|
||||
if (chan.type !== Chan.Type.CHANNEL) {
|
||||
chan.pushMessage(this, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `${cmd} command can only be used in channels.`,
|
||||
}));
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `${cmd} command can only be used in channels.`,
|
||||
})
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
chan.pushMessage(this, new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `Usage: /${cmd} <nick> [...nick]`,
|
||||
}));
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `Usage: /${cmd} <nick> [...nick]`,
|
||||
})
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -50,7 +48,9 @@ exports.input = function({irc, nick}, chan, cmd, args) {
|
|||
}
|
||||
|
||||
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);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue