Merge with master

This commit is contained in:
Max Leiter 2021-07-18 23:35:00 -07:00
commit 2721395ada
No known key found for this signature in database
GPG key ID: C6237BF113E61F28
54 changed files with 1991 additions and 1803 deletions

View file

@ -2,7 +2,7 @@
root: true root: true
parserOptions: parserOptions:
ecmaVersion: 2018 ecmaVersion: 2020
env: env:
es6: true es6: true

View file

@ -4,7 +4,7 @@ about: Create a bug report
labels: "Type: Bug" labels: "Type: Bug"
--- ---
<!-- Have a question? Join #thelounge on freenode --> <!-- Have a question? Join #thelounge on Libera.Chat -->
- _Node version:_ - _Node version:_
- _Browser version:_ - _Browser version:_

View file

@ -4,7 +4,7 @@ about: Request a new feature
labels: "Type: Feature" labels: "Type: Feature"
--- ---
<!-- Have a question? Join #thelounge on freenode. --> <!-- Have a question? Join #thelounge on Libera.Chat. -->
<!-- Make sure to check the existing issues prior to submitting your suggestion. --> <!-- Make sure to check the existing issues prior to submitting your suggestion. -->
### Feature Description ### Feature Description

View file

@ -13,4 +13,4 @@ contact_links:
- name: General support - name: General support
url: https://demo.thelounge.chat/?join=%23thelounge url: https://demo.thelounge.chat/?join=%23thelounge
about: "Join #thelounge on Freenode to ask a question before creating an issue" about: "Join #thelounge on Libera.Chat to ask a question before creating an issue"

2
.github/SUPPORT.md vendored
View file

@ -6,6 +6,6 @@ need help, you have a few options:
- Check out [existing questions on Stack Overflow](https://stackoverflow.com/questions/tagged/thelounge) - Check out [existing questions on Stack Overflow](https://stackoverflow.com/questions/tagged/thelounge)
to see if yours has been answered before. If not, feel free to [ask for a new question](https://stackoverflow.com/questions/ask?tags=thelounge) to see if yours has been answered before. If not, feel free to [ask for a new question](https://stackoverflow.com/questions/ask?tags=thelounge)
(using `thelounge` tag so that other people can easily find it). (using `thelounge` tag so that other people can easily find it).
- Find us on the Freenode channel `#thelounge`. You might not get an answer - Find us on the Libera.Chat channel `#thelounge`. You might not get an answer
right away, but this channel is full of nice people who will be happy to right away, but this channel is full of nice people who will be happy to
help you. help you.

View file

@ -4,6 +4,51 @@ All notable changes to this project will be documented in this file.
<!-- New entries go after this line --> <!-- New entries go after this line -->
## v4.3.0-pre.4 - 2021-07-01 [Pre-release]
[See the full changelog](https://github.com/thelounge/thelounge/compare/v4.3.0-pre.3...v4.3.0-pre.4)
This is a pre-release for v4.3.0 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.
Please refer to the commit list given above for a complete list of changes, or wait for the stable release to get a thoroughly prepared change log entry.
As with all pre-releases, this version requires explicit use of the `next` tag to be installed:
```sh
yarn global add thelounge@next
```
## v4.3.0-pre.3 - 2021-06-29 [Pre-release]
[See the full changelog](https://github.com/thelounge/thelounge/compare/v4.3.0-pre.2...v4.3.0-pre.3)
This is a pre-release for v4.3.0 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.
Please refer to the commit list given above for a complete list of changes, or wait for the stable release to get a thoroughly prepared change log entry.
As with all pre-releases, this version requires explicit use of the `next` tag to be installed:
```sh
yarn global add thelounge@next
```
## v4.3.0-pre.2 - 2021-06-07 [Pre-release]
[See the full changelog](https://github.com/thelounge/thelounge/compare/v4.3.0-pre.1...v4.3.0-pre.2)
This is a pre-release for v4.3.0 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.
Please refer to the commit list given above for a complete list of changes, or wait for the stable release to get a thoroughly prepared change log entry.
As with all pre-releases, this version requires explicit use of the `next` tag to be installed:
```sh
yarn global add thelounge@next
```
## v4.3.0-pre.1 - 2021-03-02 [Pre-release] ## v4.3.0-pre.1 - 2021-03-02 [Pre-release]
[See the full changelog](https://github.com/thelounge/thelounge/compare/v4.2.0...v4.3.0-pre.1) [See the full changelog](https://github.com/thelounge/thelounge/compare/v4.2.0...v4.3.0-pre.1)

View file

@ -20,8 +20,8 @@
</p> </p>
<p align="center"> <p align="center">
<a href="https://demo.thelounge.chat/"><img <a href="https://demo.thelounge.chat/"><img
alt="#thelounge IRC channel on freenode" alt="#thelounge IRC channel on Libera.Chat"
src="https://img.shields.io/badge/freenode-%23thelounge-415364.svg?colorA=ff9e18"></a> src="https://img.shields.io/badge/Libera.Chat-%23thelounge-415364.svg?colorA=ff9e18"></a>
<a href="https://yarn.pm/thelounge"><img <a href="https://yarn.pm/thelounge"><img
alt="npm version" alt="npm version"
src="https://img.shields.io/npm/v/thelounge.svg?colorA=333a41&maxAge=3600"></a> src="https://img.shields.io/npm/v/thelounge.svg?colorA=333a41&maxAge=3600"></a>

View file

@ -4,6 +4,6 @@
- Contact us privately first, in a - Contact us privately first, in a
[responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure) [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure)
manner. manner.
- On IRC, send a private message to any voiced user on our Freenode channel, - On IRC, send a private message to any voiced user on our Libera.Chat channel,
`#thelounge`. `#thelounge`.
- By email, send us your report at <security@thelounge.chat>. - By email, send us your report at <security@thelounge.chat>.

View file

@ -1,7 +1,12 @@
<template> <template>
<div id="viewport" :class="viewportClasses" role="tablist"> <div id="viewport" :class="viewportClasses" role="tablist">
<Sidebar v-if="$store.state.appLoaded" :overlay="$refs.overlay" /> <Sidebar v-if="$store.state.appLoaded" :overlay="$refs.overlay" />
<div id="sidebar-overlay" ref="overlay" @click="$store.commit('sidebarOpen', false)" /> <div
id="sidebar-overlay"
ref="overlay"
aria-hidden="true"
@click="$store.commit('sidebarOpen', false)"
/>
<router-view ref="window"></router-view> <router-view ref="window"></router-view>
<Mentions /> <Mentions />
<ImageViewer ref="imageViewer" /> <ImageViewer ref="imageViewer" />
@ -51,6 +56,7 @@ export default {
Mousetrap.bind("esc", this.escapeKey); Mousetrap.bind("esc", this.escapeKey);
Mousetrap.bind("alt+u", this.toggleUserList); Mousetrap.bind("alt+u", this.toggleUserList);
Mousetrap.bind("alt+s", this.toggleSidebar); Mousetrap.bind("alt+s", this.toggleSidebar);
Mousetrap.bind("alt+m", this.toggleMentions);
// Make a single throttled resize listener available to all components // Make a single throttled resize listener available to all components
this.debouncedResize = throttle(() => { this.debouncedResize = throttle(() => {
@ -72,6 +78,7 @@ export default {
Mousetrap.unbind("esc", this.escapeKey); Mousetrap.unbind("esc", this.escapeKey);
Mousetrap.unbind("alt+u", this.toggleUserList); Mousetrap.unbind("alt+u", this.toggleUserList);
Mousetrap.unbind("alt+s", this.toggleSidebar); Mousetrap.unbind("alt+s", this.toggleSidebar);
Mousetrap.unbind("alt+m", this.toggleMentions);
window.removeEventListener("resize", this.debouncedResize); window.removeEventListener("resize", this.debouncedResize);
clearTimeout(this.dayChangeTimeout); clearTimeout(this.dayChangeTimeout);
@ -98,6 +105,11 @@ export default {
return false; return false;
}, },
toggleMentions() {
if (this.$store.state.networks.length !== 0) {
eventbus.emit("mentions:toggle");
}
},
msUntilNextDay() { msUntilNextDay() {
// Compute how many milliseconds are remaining until the next day starts // Compute how many milliseconds are remaining until the next day starts
const today = new Date(); const today = new Date();

View file

@ -54,20 +54,25 @@ export default {
methods: { methods: {
getAriaLabel() { getAriaLabel() {
const extra = []; const extra = [];
const type = this.channel.type;
if (this.channel.unread > 0) { if (this.channel.unread > 0) {
extra.push(`${this.channel.unread} unread`); if (this.channel.unread > 1) {
extra.push(`${this.channel.unread} unread messages`);
} else {
extra.push(`${this.channel.unread} unread message`);
}
} }
if (this.channel.highlight > 0) { if (this.channel.highlight > 0) {
extra.push(`${this.channel.highlight} mention`); if (this.channel.highlight > 1) {
extra.push(`${this.channel.highlight} mentions`);
} else {
extra.push(`${this.channel.highlight} mention`);
}
} }
if (extra.length > 0) { return `${type}: ${this.channel.name} ${extra.length ? `(${extra.join(", ")})` : ""}`;
return `${this.channel.name} (${extra.join(", ")})`;
}
return this.channel.name;
}, },
click() { click() {
if (this.isFiltering) { if (this.isFiltering) {

View file

@ -18,7 +18,9 @@
> >
<div class="header"> <div class="header">
<SidebarToggle /> <SidebarToggle />
<span class="title">{{ channel.name }}</span> <span class="title" :aria-label="'Currently open ' + channel.type">{{
channel.name
}}</span>
<div v-if="channel.editTopic === true" class="topic-container"> <div v-if="channel.editTopic === true" class="topic-container">
<input <input
ref="topicInput" ref="topicInput"
@ -93,13 +95,13 @@
> >
<div class="scroll-down-arrow" /> <div class="scroll-down-arrow" />
</div> </div>
<ChatUserList v-if="channel.type === 'channel'" :channel="channel" />
<MessageList <MessageList
ref="messageList" ref="messageList"
:network="network" :network="network"
:channel="channel" :channel="channel"
:focused="focused" :focused="focused"
/> />
<ChatUserList v-if="channel.type === 'channel'" :channel="channel" />
</div> </div>
</div> </div>
</div> </div>

View file

@ -140,22 +140,53 @@ export default {
return; return;
} }
const oldValue = this.$refs.input.value;
const oldPosition = this.$refs.input.selectionStart;
const onRow = (oldValue.slice(null, oldPosition).match(/\n/g) || []).length;
const totalRows = (oldValue.match(/\n/g) || []).length;
const {channel} = this; const {channel} = this;
if (channel.inputHistoryPosition === 0) { if (channel.inputHistoryPosition === 0) {
channel.inputHistory[channel.inputHistoryPosition] = channel.pendingMessage; channel.inputHistory[channel.inputHistoryPosition] = channel.pendingMessage;
} }
if (key === "up") { if (key === "up" && onRow === 0) {
if (channel.inputHistoryPosition < channel.inputHistory.length - 1) { if (channel.inputHistoryPosition < channel.inputHistory.length - 1) {
channel.inputHistoryPosition++; channel.inputHistoryPosition++;
} else {
return;
} }
} else if (channel.inputHistoryPosition > 0) { } else if (key === "down" && channel.inputHistoryPosition > 0 && onRow === totalRows) {
channel.inputHistoryPosition--; channel.inputHistoryPosition--;
} else {
return;
} }
channel.pendingMessage = channel.inputHistory[channel.inputHistoryPosition]; channel.pendingMessage = channel.inputHistory[channel.inputHistoryPosition];
this.$refs.input.value = channel.pendingMessage; const newValue = channel.pendingMessage;
this.$refs.input.value = newValue;
let newPosition;
if (key === "up") {
const lastIndexOfNewLine = newValue.lastIndexOf("\n");
const lastLine = newValue.slice(null, lastIndexOfNewLine);
newPosition =
oldPosition > lastLine.length
? newValue.length
: lastIndexOfNewLine + oldPosition + 1;
} else {
const lastPositionOnFirstLine =
newValue.indexOf("\n") === -1 ? newValue.length + 1 : newValue.indexOf("\n");
const relativeRowPos = oldPosition - oldValue.lastIndexOf("\n") - 1;
newPosition =
relativeRowPos > lastPositionOnFirstLine
? lastPositionOnFirstLine
: relativeRowPos;
}
this.$refs.input.setSelectionRange(newPosition, newPosition);
this.setInputSize(); this.setInputSize();
return false; return false;
@ -183,6 +214,10 @@ export default {
}, },
setInputSize() { setInputSize() {
this.$nextTick(() => { this.$nextTick(() => {
if (!this.$refs.input) {
return;
}
const style = window.getComputedStyle(this.$refs.input); const style = window.getComputedStyle(this.$refs.input);
const lineHeight = parseFloat(style.lineHeight, 10) || 1; const lineHeight = parseFloat(style.lineHeight, 10) || 1;

View file

@ -1,5 +1,10 @@
<template> <template>
<aside ref="userlist" class="userlist" @mouseleave="removeHoverUser"> <aside
ref="userlist"
class="userlist"
:aria-label="'User list for ' + channel.name"
@mouseleave="removeHoverUser"
>
<div class="count"> <div class="count">
<input <input
ref="input" ref="input"

View file

@ -13,8 +13,12 @@
:data-command="message.command" :data-command="message.command"
:data-from="message.from && message.from.nick" :data-from="message.from && message.from.nick"
> >
<span :aria-label="messageTimeLocale" class="time tooltipped tooltipped-e" <span
>{{ messageTime }} aria-hidden="true"
:aria-label="messageTimeLocale"
class="time tooltipped tooltipped-e"
>
{{ messageTime }}
</span> </span>
<template v-if="message.type === 'unhandled'"> <template v-if="message.type === 'unhandled'">
<span class="from">[{{ message.command }}]</span> <span class="from">[{{ message.command }}]</span>
@ -47,23 +51,23 @@
<template v-else> <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"> <template v-if="message.from && message.from.nick">
<span class="only-copy">&lt;</span> <span class="only-copy" aria-hidden="true">&lt;</span>
<Username :user="message.from" :network="network" :channel="channel" /> <Username :user="message.from" :network="network" :channel="channel" />
<span class="only-copy">&gt; </span> <span class="only-copy" aria-hidden="true">&gt; </span>
</template> </template>
</span> </span>
<span v-else-if="message.type === 'plugin'" class="from"> <span v-else-if="message.type === 'plugin'" class="from">
<template v-if="message.from && message.from.nick"> <template v-if="message.from && message.from.nick">
<span class="only-copy">[</span> <span class="only-copy" aria-hidden="true">[</span>
{{ message.from.nick }} {{ message.from.nick }}
<span class="only-copy">] </span> <span class="only-copy" aria-hidden="true">] </span>
</template> </template>
</span> </span>
<span v-else class="from"> <span v-else class="from">
<template v-if="message.from && message.from.nick"> <template v-if="message.from && message.from.nick">
<span class="only-copy">-</span> <span class="only-copy" aria-hidden="true">-</span>
<Username :user="message.from" :network="network" :channel="channel" /> <Username :user="message.from" :network="network" :channel="channel" />
<span class="only-copy">- </span> <span class="only-copy" aria-hidden="true">- </span>
</template> </template>
</span> </span>
<span class="content" dir="auto"> <span class="content" dir="auto">

View file

@ -4,7 +4,7 @@
<input <input
ref="searchInputField" ref="searchInputField"
v-model="searchInput" v-model="searchInput"
type="text" type="search"
name="search" name="search"
class="input" class="input"
placeholder="Search messages…" placeholder="Search messages…"
@ -104,6 +104,10 @@ export default {
mounted() { mounted() {
this.searchInput = this.$route.query.q; this.searchInput = this.$route.query.q;
this.searchOpened = this.onSearchPage; this.searchOpened = this.onSearchPage;
if (!this.searchInput && this.searchOpened) {
this.$refs.searchInputField.focus();
}
}, },
methods: { methods: {
closeSearch() { closeSearch() {

View file

@ -6,17 +6,20 @@
>username to <b>{{ message.new_ident }}</b></span >username to <b>{{ message.new_ident }}</b></span
> >
<span v-if="message.new_host" <span v-if="message.new_host"
>hostname to <i class="hostmask">{{ message.new_host }}</i></span >hostname to
> <i class="hostmask"><ParsedMessage :network="network" :text="message.new_host" /></i
></span>
</span> </span>
</template> </template>
<script> <script>
import ParsedMessage from "../ParsedMessage.vue";
import Username from "../Username.vue"; import Username from "../Username.vue";
export default { export default {
name: "MessageTypeChangeHost", name: "MessageTypeChangeHost",
components: { components: {
ParsedMessage,
Username, Username,
}, },
props: { props: {

View file

@ -1,7 +1,7 @@
<template> <template>
<span class="content"> <span class="content">
<Username :user="message.from" /> <Username :user="message.from" />
<i class="hostmask"> ({{ message.hostmask }})</i> <i class="hostmask"> (<ParsedMessage :network="network" :text="message.hostmask" />)</i>
<template v-if="message.account"> <template v-if="message.account">
<i class="account"> [{{ message.account }}]</i> <i class="account"> [{{ message.account }}]</i>
</template> </template>
@ -13,11 +13,13 @@
</template> </template>
<script> <script>
import ParsedMessage from "../ParsedMessage.vue";
import Username from "../Username.vue"; import Username from "../Username.vue";
export default { export default {
name: "MessageTypeJoin", name: "MessageTypeJoin",
components: { components: {
ParsedMessage,
Username, Username,
}, },
props: { props: {

View file

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

View file

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

View file

@ -12,7 +12,12 @@
</template> </template>
<dt>Host mask:</dt> <dt>Host mask:</dt>
<dd class="hostmask">{{ message.whois.ident }}@{{ message.whois.hostname }}</dd> <dd class="hostmask">
<ParsedMessage
:network="network"
:text="message.whois.ident + '@' + message.whois.hostname"
/>
</dd>
<template v-if="message.whois.actual_hostname"> <template v-if="message.whois.actual_hostname">
<dt>Actual host:</dt> <dt>Actual host:</dt>

View file

@ -98,6 +98,80 @@
</label> </label>
</div> </div>
</div> </div>
<h2>Proxy Settings</h2>
<div class="connect-row">
<label></label>
<div class="input-wrap">
<label for="connect:proxyEnabled">
<input
id="connect:proxyEnabled"
v-model="defaults.proxyEnabled"
type="checkbox"
name="proxyEnabled"
/>
Enable Proxy
</label>
</div>
</div>
<template v-if="defaults.proxyEnabled">
<div class="connect-row">
<label for="connect:proxyHost">SOCKS Address</label>
<div class="input-wrap">
<input
id="connect:proxyHost"
v-model="defaults.proxyHost"
class="input"
name="proxyHost"
aria-label="Proxy host"
maxlength="255"
/>
<span id="connect:proxyPortSeparator">:</span>
<input
id="connect:proxyPort"
v-model="defaults.proxyPort"
class="input"
type="number"
min="1"
max="65535"
name="proxyPort"
aria-label="SOCKS port"
/>
</div>
</div>
<div class="connect-row">
<label for="connect:proxyUsername">Proxy username</label>
<input
id="connect:proxyUsername"
ref="proxyUsernameInput"
v-model="defaults.proxyUsername"
class="input username"
name="proxyUsername"
maxlength="100"
placeholder="Proxy username"
/>
</div>
<div class="connect-row">
<label for="connect:proxyPassword">Proxy password</label>
<RevealPassword
v-slot:default="slotProps"
class="input-wrap password-container"
>
<input
id="connect:proxyPassword"
ref="proxyPassword"
v-model="defaults.proxyPassword"
class="input"
:type="slotProps.isVisible ? 'text' : 'password'"
placeholder="Proxy password"
name="password"
maxlength="300"
/>
</RevealPassword>
</div>
</template>
</template> </template>
<template v-else-if="config.lockNetwork && !$store.state.serverConfiguration.public"> <template v-else-if="config.lockNetwork && !$store.state.serverConfiguration.public">
<h2>Network settings</h2> <h2>Network settings</h2>

View file

@ -1,8 +1,13 @@
<template> <template>
<div v-if="$store.state.networks.length === 0" class="empty"> <div
v-if="$store.state.networks.length === 0"
class="empty"
role="navigation"
aria-label="Network and Channel list"
>
You are not connected to any networks yet. You are not connected to any networks yet.
</div> </div>
<div v-else ref="networklist"> <div v-else ref="networklist" role="navigation" aria-label="Network and Channel list">
<div class="jump-to-input"> <div class="jump-to-input">
<input <input
ref="searchInput" ref="searchInput"
@ -74,6 +79,7 @@
}" }"
class="network" class="network"
role="region" role="region"
aria-live="polite"
> >
<NetworkLobby <NetworkLobby
:network="network" :network="network"

View file

@ -6,11 +6,13 @@
:src="`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg.svg`" :src="`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg.svg`"
class="logo" class="logo"
alt="The Lounge" alt="The Lounge"
role="presentation"
/> />
<img <img
:src="`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg-inverted.svg`" :src="`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg-inverted.svg`"
class="logo-inverted" class="logo-inverted"
alt="The Lounge" alt="The Lounge"
role="presentation"
/> />
<span <span
v-if="isDevelopment" v-if="isDevelopment"

View file

@ -9,7 +9,7 @@
</thead> </thead>
<tbody> <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="hostmask"><ParsedMessage :network="network" :text="ban.hostmask" /></td>
<td class="banned_by">{{ ban.banned_by }}</td> <td class="banned_by">{{ ban.banned_by }}</td>
<td class="banned_at">{{ localetime(ban.banned_at) }}</td> <td class="banned_at">{{ localetime(ban.banned_at) }}</td>
</tr> </tr>
@ -18,10 +18,14 @@
</template> </template>
<script> <script>
import ParsedMessage from "../ParsedMessage.vue";
import localetime from "../../js/helpers/localetime"; import localetime from "../../js/helpers/localetime";
export default { export default {
name: "ListBans", name: "ListBans",
components: {
ParsedMessage,
},
props: { props: {
network: Object, network: Object,
channel: Object, channel: Object,

View file

@ -8,7 +8,7 @@
</thead> </thead>
<tbody> <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="hostmask"><ParsedMessage :network="network" :text="user.hostmask" /></td>
<td class="when">{{ localetime(user.when) }}</td> <td class="when">{{ localetime(user.when) }}</td>
</tr> </tr>
</tbody> </tbody>
@ -16,10 +16,14 @@
</template> </template>
<script> <script>
import ParsedMessage from "../ParsedMessage.vue";
import localetime from "../../js/helpers/localetime"; import localetime from "../../js/helpers/localetime";
export default { export default {
name: "ListIgnored", name: "ListIgnored",
components: {
ParsedMessage,
},
props: { props: {
network: Object, network: Object,
channel: Object, channel: Object,

View file

@ -9,7 +9,9 @@
</thead> </thead>
<tbody> <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="hostmask">
<ParsedMessage :network="network" :text="invite.hostmask" />
</td>
<td class="invitened_by">{{ invite.invited_by }}</td> <td class="invitened_by">{{ invite.invited_by }}</td>
<td class="invitened_at">{{ localetime(invite.invited_at) }}</td> <td class="invitened_at">{{ localetime(invite.invited_at) }}</td>
</tr> </tr>
@ -18,10 +20,14 @@
</template> </template>
<script> <script>
import ParsedMessage from "../ParsedMessage.vue";
import localetime from "../../js/helpers/localetime"; import localetime from "../../js/helpers/localetime";
export default { export default {
name: "ListInvites", name: "ListInvites",
components: {
ParsedMessage,
},
props: { props: {
network: Object, network: Object,
channel: Object, channel: Object,

View file

@ -189,6 +189,16 @@
</div> </div>
</div> </div>
<div class="help-item">
<div class="subject">
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>M</kbd></span>
<span v-else><kbd></kbd> <kbd>M</kbd></span>
</div>
<div class="description">
<p>Toggle recent mentions popup.</p>
</div>
</div>
<div class="help-item"> <div class="help-item">
<div class="subject"> <div class="subject">
<span><kbd>Esc</kbd></span> <span><kbd>Esc</kbd></span>
@ -673,6 +683,15 @@
</div> </div>
</div> </div>
<div class="help-item">
<div class="subject">
<code>/search query</code>
</div>
<div class="description">
<p>Search for messages in the current channel / user</p>
</div>
</div>
<div class="help-item"> <div class="help-item">
<div class="subject"> <div class="subject">
<code>/topic [newtopic]</code> <code>/topic [newtopic]</code>

View file

@ -91,7 +91,7 @@
type="checkbox" type="checkbox"
name="showSeconds" name="showSeconds"
/> />
Show seconds in timestamp Include seconds in timestamp
</label> </label>
</div> </div>
<div> <div>
@ -101,7 +101,7 @@
type="checkbox" type="checkbox"
name="use12hClock" name="use12hClock"
/> />
Show 12-hour timestamps Use 12-hour timestamps
</label> </label>
</div> </div>
<div v-if="!$store.state.serverConfiguration.public && $store.state.settings.advanced"> <div v-if="!$store.state.serverConfiguration.public && $store.state.settings.advanced">
@ -306,6 +306,7 @@
<input <input
id="desktopNotifications" id="desktopNotifications"
:checked="$store.state.settings.desktopNotifications" :checked="$store.state.settings.desktopNotifications"
:disabled="$store.state.desktopNotificationState === 'nohttps'"
type="checkbox" type="checkbox"
name="desktopNotifications" name="desktopNotifications"
/> />
@ -316,6 +317,14 @@
> >
<strong>Warning</strong>: Notifications are not supported by your browser. <strong>Warning</strong>: Notifications are not supported by your browser.
</div> </div>
<div
v-if="$store.state.desktopNotificationState === 'nohttps'"
id="warnBlockedDesktopNotifications"
class="error"
>
<strong>Warning</strong>: Notifications are only supported over HTTPS
connections.
</div>
<div <div
v-if="$store.state.desktopNotificationState === 'blocked'" v-if="$store.state.desktopNotificationState === 'blocked'"
id="warnBlockedDesktopNotifications" id="warnBlockedDesktopNotifications"

View file

@ -1147,6 +1147,7 @@ textarea.input {
#chat .chat-content { #chat .chat-content {
display: flex; display: flex;
flex-direction: row-reverse;
flex-grow: 1; flex-grow: 1;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@ -1195,7 +1196,6 @@ textarea.input {
} }
#chat .show-more { #chat .show-more {
margin-top: 50px;
padding: 10px; padding: 10px;
padding-top: 15px; padding-top: 15px;
padding-bottom: 0; padding-bottom: 0;
@ -1508,8 +1508,11 @@ textarea.input {
} }
#chat .msg[data-type="notice"] .time, #chat .msg[data-type="notice"] .time,
#chat .msg[data-type="wallops"] .time,
#chat .msg[data-type="notice"] .content, #chat .msg[data-type="notice"] .content,
#chat .msg[data-type="notice"] .user { #chat .msg[data-type="wallops"] .content,
#chat .msg[data-type="notice"] .user,
#chat .msg[data-type="wallops"] .user {
color: #0074d9; color: #0074d9;
} }
@ -1517,6 +1520,10 @@ textarea.input {
content: "Notice: "; content: "Notice: ";
} }
#chat .msg[data-type="wallops"] .from .user::before {
content: "Wallops: ";
}
#chat .msg[data-type="error"], #chat .msg[data-type="error"],
#chat .msg[data-type="error"] .from { #chat .msg[data-type="error"] .from {
color: #e74c3c; color: #e74c3c;
@ -1840,19 +1847,23 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
margin-top: 6px; margin-top: 6px;
} }
#connect .tls input { #connect .tls input,
#connect input[name="proxyEnabled"] {
margin: 3px 10px 0 0; margin: 3px 10px 0 0;
} }
#connect\:host { #connect\:host,
#connect\:proxyHost {
width: 70%; width: 70%;
} }
#connect\:port { #connect\:port,
#connect\:proxyPort {
width: 25%; width: 25%;
} }
#connect\:portseparator { #connect\:portseparator,
#connect\:proxyPortSeparator {
width: 5%; width: 5%;
text-align: center; text-align: center;
display: inline-block; display: inline-block;
@ -2624,6 +2635,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
right: 0; right: 0;
transform: translateX(180px); transform: translateX(180px);
transition: transform 0.2s; transition: transform 0.2s;
z-index: 1;
} }
#viewport.userlist-open #chat .userlist { #viewport.userlist-open #chat .userlist {

View file

@ -52,8 +52,8 @@
<div id="loading"> <div id="loading">
<div class="window"> <div class="window">
<div id="loading-status-container"> <div id="loading-status-container">
<img src="img/logo-vertical-transparent-bg.svg" class="logo" alt="The Lounge" width="256" height="170"> <img src="img/logo-vertical-transparent-bg.svg" class="logo" alt="" width="256" height="170">
<img src="img/logo-vertical-transparent-bg-inverted.svg" class="logo-inverted" alt="The Lounge" width="256" height="170"> <img src="img/logo-vertical-transparent-bg-inverted.svg" class="logo-inverted" alt="" width="256" height="170">
<p id="loading-page-message">The Lounge requires a modern browser with JavaScript enabled.</p> <p id="loading-page-message">The Lounge requires a modern browser with JavaScript enabled.</p>
</div> </div>
<div id="loading-reload-container"> <div id="loading-reload-container">

View file

@ -0,0 +1,20 @@
"use strict";
import store from "../store";
import {router} from "../router";
function input(args) {
router.push({
name: "SearchResults",
params: {
id: store.state.activeChannel.channel.id,
},
query: {
q: args.join(" "),
},
});
return true;
}
export default {input};

View file

@ -1238,8 +1238,8 @@
"credit_card": "💳", "credit_card": "💳",
"receipt": "🧾", "receipt": "🧾",
"chart": "💹", "chart": "💹",
"email": "✉️",
"envelope": "✉️", "envelope": "✉️",
"email": "📧",
"e_mail": "📧", "e_mail": "📧",
"incoming_envelope": "📨", "incoming_envelope": "📨",
"envelope_with_arrow": "📩", "envelope_with_arrow": "📩",

View file

@ -6,17 +6,24 @@ import socket from "../socket";
import store from "../store"; import store from "../store";
socket.on("more", function (data) { socket.on("more", function (data) {
const channel = store.getters.findChannel(data.chan); const channel = store.getters.findChannel(data.chan)?.channel;
if (!channel) { if (!channel) {
return; return;
} }
channel.channel.moreHistoryAvailable = channel.inputHistory = channel.inputHistory.concat(
data.totalMessages > channel.channel.messages.length + data.messages.length; data.messages
channel.channel.messages.unshift(...data.messages); .filter((m) => m.self && m.text && m.type === "message")
.map((m) => m.text)
.reverse()
.slice(null, 100 - channel.inputHistory.length)
);
channel.moreHistoryAvailable =
data.totalMessages > channel.messages.length + data.messages.length;
channel.messages.unshift(...data.messages);
Vue.nextTick(() => { Vue.nextTick(() => {
channel.channel.historyLoading = false; channel.historyLoading = false;
}); });
}); });

View file

@ -12,6 +12,8 @@ function detectDesktopNotificationState() {
return "unsupported"; return "unsupported";
} else if (Notification.permission === "granted") { } else if (Notification.permission === "granted") {
return "granted"; return "granted";
} else if (!window.isSecureContext) {
return "nohttps";
} }
return "blocked"; return "blocked";
@ -190,7 +192,14 @@ const store = new Vuex.Store({
// TODO: This should be a mutation // TODO: This should be a mutation
channel.pendingMessage = ""; channel.pendingMessage = "";
channel.inputHistoryPosition = 0; channel.inputHistoryPosition = 0;
channel.inputHistory = [""];
channel.inputHistory = [""].concat(
channel.messages
.filter((m) => m.self && m.text && m.type === "message")
.map((m) => m.text)
.reverse()
.slice(null, 99)
);
channel.historyLoading = false; channel.historyLoading = false;
channel.scrolledToBottom = true; channel.scrolledToBottom = true;
channel.editTopic = false; channel.editTopic = false;

View file

@ -231,12 +231,12 @@ module.exports = {
// - `join`: Comma-separated list of channels to auto-join once connected. // - `join`: Comma-separated list of channels to auto-join once connected.
// //
// This value is set to connect to the official channel of The Lounge on // This value is set to connect to the official channel of The Lounge on
// Freenode by default: // Libera.Chat by default:
// //
// ```js // ```js
// defaults: { // defaults: {
// name: "Freenode", // name: "Libera.Chat",
// host: "chat.freenode.net", // host: "irc.libera.chat",
// port: 6697, // port: 6697,
// password: "", // password: "",
// tls: true, // tls: true,
@ -248,8 +248,8 @@ module.exports = {
// } // }
// ``` // ```
defaults: { defaults: {
name: "Freenode", name: "Libera.Chat",
host: "chat.freenode.net", host: "irc.libera.chat",
port: 6697, port: 6697,
password: "", password: "",
tls: true, tls: true,

View file

@ -1,7 +1,7 @@
{ {
"name": "thelounge", "name": "thelounge",
"description": "The self-hosted Web IRC client", "description": "The self-hosted Web IRC client",
"version": "4.3.0-pre.1", "version": "4.3.0-pre.4",
"preferGlobal": true, "preferGlobal": true,
"bin": { "bin": {
"thelounge": "index.js" "thelounge": "index.js"
@ -43,19 +43,19 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"busboy": "0.3.1", "busboy": "0.3.1",
"chalk": "4.1.1", "chalk": "4.1.1",
"cheerio": "1.0.0-rc.5", "cheerio": "1.0.0-rc.10",
"commander": "7.2.0", "commander": "7.2.0",
"content-disposition": "0.5.3", "content-disposition": "0.5.3",
"express": "4.17.1", "express": "4.17.1",
"file-type": "16.2.0", "file-type": "16.2.0",
"filenamify": "4.2.0", "filenamify": "4.2.0",
"got": "11.8.1", "got": "11.8.2",
"irc-framework": "4.9.0", "irc-framework": "4.11.0",
"is-utf8": "0.2.1", "is-utf8": "0.2.1",
"ldapjs": "2.2.3", "ldapjs": "2.2.3",
"linkify-it": "3.0.2", "linkify-it": "3.0.2",
"lodash": "4.17.20", "lodash": "4.17.21",
"mime-types": "2.1.28", "mime-types": "2.1.31",
"node-forge": "0.10.0", "node-forge": "0.10.0",
"package-json": "6.5.0", "package-json": "6.5.0",
"read": "1.0.7", "read": "1.0.7",
@ -73,8 +73,8 @@
"better-sqlite3": "7.4.0" "better-sqlite3": "7.4.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.14.0", "@babel/core": "7.14.6",
"@babel/preset-env": "7.14.0", "@babel/preset-env": "7.14.7",
"@fortawesome/fontawesome-free": "5.15.3", "@fortawesome/fontawesome-free": "5.15.3",
"@vue/server-test-utils": "1.1.3", "@vue/server-test-utils": "1.1.3",
"@vue/test-utils": "1.1.3", "@vue/test-utils": "1.1.3",
@ -83,14 +83,14 @@
"chai": "4.3.4", "chai": "4.3.4",
"copy-webpack-plugin": "7.0.0", "copy-webpack-plugin": "7.0.0",
"css-loader": "5.1.1", "css-loader": "5.1.1",
"cssnano": "4.1.10", "cssnano": "4.1.11",
"dayjs": "1.10.4", "dayjs": "1.10.5",
"emoji-regex": "9.2.1", "emoji-regex": "9.2.2",
"eslint": "7.23.0", "eslint": "7.23.0",
"eslint-config-prettier": "6.15.0", "eslint-config-prettier": "6.15.0",
"eslint-plugin-vue": "7.5.0", "eslint-plugin-vue": "7.5.0",
"fuzzy": "0.1.3", "fuzzy": "0.1.3",
"husky": "4.3.5", "husky": "4.3.8",
"mini-css-extract-plugin": "1.3.6", "mini-css-extract-plugin": "1.3.6",
"mocha": "8.2.1", "mocha": "8.2.1",
"mousetrap": "1.6.5", "mousetrap": "1.6.5",

0
scripts/generate-emoji.js Normal file → Executable file
View file

View file

@ -257,6 +257,12 @@ Client.prototype.connect = function (args, isStartup = false) {
commands: args.commands || [], commands: args.commands || [],
channels: channels, channels: channels,
ignoreList: args.ignoreList ? args.ignoreList : [], ignoreList: args.ignoreList ? args.ignoreList : [],
proxyEnabled: !!args.proxyEnabled,
proxyHost: String(args.proxyHost || ""),
proxyPort: parseInt(args.proxyPort, 10),
proxyUsername: String(args.proxyUsername || ""),
proxyPassword: String(args.proxyPassword || ""),
}); });
// Set network lobby channel id // Set network lobby channel id

View file

@ -34,7 +34,7 @@ try {
createPackagesFolder(); createPackagesFolder();
// Merge config key-values passed as CLI options into the main config // Merge config key-values passed as CLI options into the main config
Helper.mergeConfig(Helper.config, program.config); Helper.mergeConfig(Helper.config, program.opts().config);
require("./start"); require("./start");

View file

@ -13,6 +13,8 @@ program
.on("--help", Utils.extraHelp) .on("--help", Utils.extraHelp)
.action(function (packageName) { .action(function (packageName) {
const fs = require("fs"); const fs = require("fs");
const fspromises = fs.promises;
const path = require("path");
const packageJson = require("package-json"); const packageJson = require("package-json");
if (!fs.existsSync(Helper.getConfigPath())) { if (!fs.existsSync(Helper.getConfigPath())) {
@ -21,22 +23,31 @@ program
} }
log.info("Retrieving information about the package..."); log.info("Retrieving information about the package...");
let readFile = null;
let isLocalFile = false;
const split = packageName.split("@"); if (packageName.startsWith("file:")) {
packageName = split[0]; isLocalFile = true;
const packageVersion = split[1] || "latest"; readFile = fspromises
.readFile(path.join(packageName.substr("file:".length), "package.json"), "utf-8")
.then((data) => JSON.parse(data));
} else {
const split = packageName.split("@");
packageName = split[0];
const packageVersion = split[1] || "latest";
packageJson(packageName, { readFile = packageJson(packageName, {
fullMetadata: true, fullMetadata: true,
version: packageVersion, version: packageVersion,
}) });
}
readFile
.then((json) => { .then((json) => {
const humanVersion = isLocalFile ? packageName : `${json.name} v${json.version}`;
if (!("thelounge" in json)) { if (!("thelounge" in json)) {
log.error( log.error(`${colors.red(humanVersion)} does not have The Lounge metadata.`);
`${colors.red(
json.name + " v" + json.version
)} does not have The Lounge metadata.`
);
process.exit(1); process.exit(1);
} }
@ -47,7 +58,7 @@ program
) { ) {
log.error( log.error(
`${colors.red( `${colors.red(
json.name + " v" + json.version humanVersion
)} does not support The Lounge v${Helper.getVersionNumber()}. Supported version(s): ${ )} does not support The Lounge v${Helper.getVersionNumber()}. Supported version(s): ${
json.thelounge.supports json.thelounge.supports
}` }`
@ -56,20 +67,23 @@ program
process.exit(2); process.exit(2);
} }
log.info(`Installing ${colors.green(json.name + " v" + json.version)}...`); log.info(`Installing ${colors.green(humanVersion)}...`);
const yarnVersion = isLocalFile ? packageName : `${json.name}@${json.version}`;
return Utils.executeYarnCommand("add", "--exact", `${json.name}@${json.version}`) return Utils.executeYarnCommand("add", "--exact", yarnVersion)
.then(() => { .then(() => {
log.info( log.info(`${colors.green(humanVersion)} has been successfully installed.`);
`${colors.green(
json.name + " v" + json.version if (isLocalFile) {
)} has been successfully installed.` // yarn v1 is buggy if a local filepath is used and doesn't update
); // the lockfile properly. We need to run an install in that case
// even though that's supposed to be done by the add subcommand
return Utils.executeYarnCommand("install").catch((err) => {
throw `Failed to update lockfile after package install ${err}`;
});
}
}) })
.catch((code) => { .catch((code) => {
throw `Failed to install ${colors.green( throw `Failed to install ${colors.red(humanVersion)}. Exit code: ${code}`;
json.name + " v" + json.version
)}. Exit code: ${code}`;
}); });
}) })
.catch((e) => { .catch((e) => {

View file

@ -79,6 +79,7 @@ Msg.Type = {
WHOIS: "whois", WHOIS: "whois",
RAW: "raw", RAW: "raw",
PLUGIN: "plugin", PLUGIN: "plugin",
WALLOPS: "wallops",
}; };
module.exports = Msg; module.exports = Msg;

View file

@ -46,6 +46,13 @@ function Network(attr) {
PREFIX: ["!", "@", "%", "+"], PREFIX: ["!", "@", "%", "+"],
NETWORK: "", NETWORK: "",
}, },
proxyHost: "",
proxyPort: 1080,
proxyUsername: "",
proxyPassword: "",
proxyEnabled: false,
chanCache: [], chanCache: [],
ignoreList: [], ignoreList: [],
keepNick: null, keepNick: null,
@ -90,6 +97,12 @@ Network.prototype.validate = function (client) {
this.saslAccount = cleanString(this.saslAccount); this.saslAccount = cleanString(this.saslAccount);
this.saslPassword = cleanString(this.saslPassword); this.saslPassword = cleanString(this.saslPassword);
this.proxyHost = cleanString(this.proxyHost);
this.proxyPort = this.proxyPort || 1080;
this.proxyUsername = cleanString(this.proxyUsername);
this.proxyPassword = cleanString(this.proxyPassword);
this.proxyEnabled = !!this.proxyEnabled;
if (!this.port) { if (!this.port) {
this.port = this.tls ? 6697 : 6667; this.port = this.tls ? 6697 : 6667;
} }
@ -208,6 +221,17 @@ Network.prototype.setIrcFrameworkOptions = function (client) {
this.irc.options.webirc = this.createWebIrc(client); this.irc.options.webirc = this.createWebIrc(client);
this.irc.options.client_certificate = null; this.irc.options.client_certificate = null;
if (this.proxyEnabled) {
this.irc.options.socks = {
host: this.proxyHost,
port: this.proxyPort,
user: this.proxyUsername,
pass: this.proxyPassword,
};
} else {
delete this.irc.options.socks;
}
if (!this.sasl) { if (!this.sasl) {
delete this.irc.options.sasl_mechanism; delete this.irc.options.sasl_mechanism;
delete this.irc.options.account; delete this.irc.options.account;
@ -274,6 +298,12 @@ Network.prototype.edit = function (client, args) {
this.saslAccount = String(args.saslAccount || ""); this.saslAccount = String(args.saslAccount || "");
this.saslPassword = String(args.saslPassword || ""); this.saslPassword = String(args.saslPassword || "");
this.proxyHost = String(args.proxyHost || "");
this.proxyPort = parseInt(args.proxyPort, 10);
this.proxyUsername = String(args.proxyUsername || "");
this.proxyPassword = String(args.proxyPassword || "");
this.proxyEnabled = !!args.proxyEnabled;
// Split commands into an array // Split commands into an array
this.commands = String(args.commands || "") this.commands = String(args.commands || "")
.replace(/\r\n|\r|\n/g, "\n") .replace(/\r\n|\r|\n/g, "\n")
@ -455,6 +485,12 @@ Network.prototype.exportForEdit = function () {
"saslAccount", "saslAccount",
"saslPassword", "saslPassword",
"commands", "commands",
"proxyEnabled",
"proxyHost",
"proxyPort",
"proxyUsername",
"proxyPassword",
]; ];
if (!Helper.config.lockNetwork) { if (!Helper.config.lockNetwork) {
@ -491,6 +527,11 @@ Network.prototype.export = function () {
"saslPassword", "saslPassword",
"commands", "commands",
"ignoreList", "ignoreList",
"proxyHost",
"proxyPort",
"proxyUsername",
"proxyEnabled",
]); ]);
network.channels = this.channels network.channels = this.channels

View file

@ -1,4 +1,4 @@
const clientSideCommands = ["/collapse", "/expand"]; const clientSideCommands = ["/collapse", "/expand", "/search"];
const passThroughCommands = [ const passThroughCommands = [
"/as", "/as",

View file

@ -3,10 +3,14 @@
const Chan = require("../../models/chan"); const Chan = require("../../models/chan");
const Msg = require("../../models/msg"); const Msg = require("../../models/msg");
exports.commands = ["mode", "op", "deop", "hop", "dehop", "voice", "devoice"]; exports.commands = ["mode", "umode", "op", "deop", "hop", "dehop", "voice", "devoice"];
exports.input = function ({irc, nick}, chan, cmd, args) { exports.input = function ({irc, nick}, chan, cmd, args) {
if (cmd !== "mode") { if (cmd === "umode") {
irc.raw("MODE", nick, ...args);
return;
} else if (cmd !== "mode") {
if (chan.type !== Chan.Type.CHANNEL) { if (chan.type !== Chan.Type.CHANNEL) {
chan.pushMessage( chan.pushMessage(
this, this,
@ -19,7 +23,9 @@ exports.input = function ({irc, nick}, chan, cmd, args) {
return; return;
} }
if (args.length === 0) { const target = args.filter((arg) => arg !== "");
if (target.length === 0) {
chan.pushMessage( chan.pushMessage(
this, this,
new Msg({ new Msg({
@ -40,9 +46,13 @@ exports.input = function ({irc, nick}, chan, cmd, args) {
devoice: "-v", devoice: "-v",
}[cmd]; }[cmd];
args.forEach(function (target) { const limit = parseInt(irc.network.supports("MODES")) || target.length;
irc.raw("MODE", chan.name, mode, target);
}); for (let i = 0; i < target.length; i += limit) {
const targets = target.slice(i, i + limit);
const amode = `${mode[0]}${mode[1].repeat(targets.length)}`;
irc.raw("MODE", chan.name, amode, ...targets);
}
return; return;
} }

View file

@ -222,6 +222,7 @@ function parse(msg, chan, preview, res, client) {
case "image/gif": case "image/gif":
case "image/jpg": case "image/jpg":
case "image/jpeg": case "image/jpeg":
case "image/jxl":
case "image/webp": case "image/webp":
case "image/avif": case "image/avif":
if (!Helper.config.prefetchStorage && Helper.config.disableMediaPreview) { if (!Helper.config.prefetchStorage && Helper.config.disableMediaPreview) {

View file

@ -11,12 +11,6 @@ module.exports = function (irc, network) {
const client = this; const client = this;
irc.on("notice", function (data) { irc.on("notice", function (data) {
// Some servers send notices without any nickname
if (!data.nick) {
data.from_server = true;
data.nick = data.hostname || network.host;
}
data.type = Msg.Type.NOTICE; data.type = Msg.Type.NOTICE;
handleMessage(data); handleMessage(data);
}); });
@ -33,7 +27,7 @@ module.exports = function (irc, network) {
irc.on("wallops", function (data) { irc.on("wallops", function (data) {
data.from_server = true; data.from_server = true;
data.type = Msg.Type.NOTICE; data.type = Msg.Type.WALLOPS;
handleMessage(data); handleMessage(data);
}); });
@ -44,6 +38,12 @@ module.exports = function (irc, network) {
let showInActive = false; let showInActive = false;
const self = data.nick === irc.user.nick; const self = data.nick === irc.user.nick;
// Some servers send messages without any nickname
if (!data.nick) {
data.from_server = true;
data.nick = data.hostname || network.host;
}
// Check if the sender is in our ignore list // Check if the sender is in our ignore list
const shouldIgnore = const shouldIgnore =
!self && !self &&
@ -51,8 +51,13 @@ module.exports = function (irc, network) {
return Helper.compareHostmask(entry, data); return Helper.compareHostmask(entry, data);
}); });
// Server messages go to server window, no questions asked // Server messages that aren't targeted at a channel go to the server window
if (data.from_server) { if (
data.from_server &&
(!data.target ||
!network.getChannel(data.target) ||
network.getChannel(data.target).type !== Chan.Type.CHANNEL)
) {
chan = network.channels[0]; chan = network.channels[0];
from = chan.getUser(data.nick); from = chan.getUser(data.nick);
} else { } else {

View file

@ -29,6 +29,7 @@ const inlineContentDispositionTypes = {
"image/png": "image.png", "image/png": "image.png",
"image/webp": "image.webp", "image/webp": "image.webp",
"image/avif": "image.avif", "image/avif": "image.avif",
"image/jxl": "image.jxl",
"text/plain": "text.txt", "text/plain": "text.txt",
"video/mp4": "video.mp4", "video/mp4": "video.mp4",
"video/ogg": "video.ogv", "video/ogg": "video.ogv",

View file

@ -55,8 +55,8 @@ describe("cleanIrcMessage", function () {
expected: "bold bold", expected: "bold bold",
}, },
{ {
input: "\x02irc\x0f://\x1dfreenode.net\x0f/\x034,8thelounge", input: "\x02irc\x0f://\x1dirc.example.com\x0f/\x034,8thelounge",
expected: "irc://freenode.net/thelounge", expected: "irc://irc.example.com/thelounge",
}, },
{ {
input: "\x02#\x038,9thelounge", input: "\x02#\x038,9thelounge",

View file

@ -8,12 +8,12 @@ const {
describe("findLinks", () => { describe("findLinks", () => {
it("should find url", () => { it("should find url", () => {
const input = "irc://freenode.net/thelounge"; const input = "irc://irc.example.com/thelounge";
const expected = [ const expected = [
{ {
start: 0, start: 0,
end: 28, end: 31,
link: "irc://freenode.net/thelounge", link: "irc://irc.example.com/thelounge",
}, },
]; ];

View file

@ -63,10 +63,10 @@ describe("IRC formatted message parser", () => {
it("should find urls", async () => { it("should find urls", async () => {
const testCases = [ const testCases = [
{ {
input: "irc://freenode.net/thelounge", input: "irc://irc.example.com/thelounge",
expected: expected:
'<a href="irc://freenode.net/thelounge" dir="auto" target="_blank" rel="noopener">' + '<a href="irc://irc.example.com/thelounge" dir="auto" target="_blank" rel="noopener">' +
"irc://freenode.net/thelounge" + "irc://irc.example.com/thelounge" +
"</a>", "</a>",
}, },
{ {
@ -416,12 +416,12 @@ describe("IRC formatted message parser", () => {
it("should go bonkers like mirc", async () => { it("should go bonkers like mirc", async () => {
const testCases = [ const testCases = [
{ {
input: "\x02irc\x0f://\x1dfreenode.net\x0f/\x034,8thelounge", input: "\x02irc\x0f://\x1dirc.example.com\x0f/\x034,8thelounge",
expected: expected:
'<a href="irc://freenode.net/thelounge" dir="auto" target="_blank" rel="noopener">' + '<a href="irc://irc.example.com/thelounge" dir="auto" target="_blank" rel="noopener">' +
'<span class="irc-bold">irc</span>' + '<span class="irc-bold">irc</span>' +
"://" + "://" +
'<span class="irc-italic">freenode.net</span>' + '<span class="irc-italic">irc.example.com</span>' +
"/" + "/" +
'<span class="irc-fg4 irc-bg8">thelounge</span>' + '<span class="irc-fg4 irc-bg8">thelounge</span>' +
"</a>", "</a>",

View file

@ -17,15 +17,38 @@ describe("Commands", function () {
}); });
const testableNetwork = { const testableNetwork = {
firstCommand: null,
lastCommand: null, lastCommand: null,
nick: "xPaw", nick: "xPaw",
irc: { irc: {
network: {
supports(type) {
if (type.toUpperCase() === "MODES") {
return "4";
}
},
},
raw(...args) { raw(...args) {
testableNetwork.firstCommand = testableNetwork.lastCommand;
testableNetwork.lastCommand = args.join(" "); testableNetwork.lastCommand = args.join(" ");
}, },
}, },
}; };
const testableNetworkNoSupports = Object.assign({}, testableNetwork, {
irc: {
network: {
supports() {
return null;
},
},
raw(...args) {
testableNetworkNoSupports.firstCommand = testableNetworkNoSupports.lastCommand;
testableNetworkNoSupports.lastCommand = args.join(" ");
},
},
});
it("should not mess with the given target", function () { it("should not mess with the given target", function () {
const test = function (expected, args) { const test = function (expected, args) {
ModeCommand.input(testableNetwork, channel, "mode", Array.from(args)); ModeCommand.input(testableNetwork, channel, "mode", Array.from(args));
@ -81,10 +104,34 @@ describe("Commands", function () {
ModeCommand.input(testableNetwork, channel, "devoice", ["xPaw"]); ModeCommand.input(testableNetwork, channel, "devoice", ["xPaw"]);
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge -v xPaw"); expect(testableNetwork.lastCommand).to.equal("MODE #thelounge -v xPaw");
});
// Multiple arguments are supported, sent as separate commands it("should use ISUPPORT MODES on shorthand commands", function () {
ModeCommand.input(testableNetwork, channel, "devoice", ["xPaw", "Max-P"]); ModeCommand.input(testableNetwork, channel, "voice", ["xPaw", "Max-P"]);
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge -v Max-P"); expect(testableNetwork.lastCommand).to.equal("MODE #thelounge +vv xPaw Max-P");
// since the limit for modes on tests is 4, it should send two commands
ModeCommand.input(testableNetwork, channel, "devoice", [
"xPaw",
"Max-P",
"hey",
"idk",
"thelounge",
]);
expect(testableNetwork.firstCommand).to.equal(
"MODE #thelounge -vvvv xPaw Max-P hey idk"
);
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge -v thelounge");
});
it("should fallback to all modes at once for shorthand commands", function () {
ModeCommand.input(testableNetworkNoSupports, channel, "voice", ["xPaw"]);
expect(testableNetworkNoSupports.lastCommand).to.equal("MODE #thelounge +v xPaw");
ModeCommand.input(testableNetworkNoSupports, channel, "devoice", ["xPaw", "Max-P"]);
expect(testableNetworkNoSupports.lastCommand).to.equal(
"MODE #thelounge -vv xPaw Max-P"
);
}); });
}); });
}); });

View file

@ -2,6 +2,8 @@
var config = require("../../../defaults/config.js"); var config = require("../../../defaults/config.js");
config.defaults.name = "Example IRC Server";
config.defaults.host = "irc.example.com";
config.public = true; config.public = true;
config.prefetch = true; config.prefetch = true;
config.host = config.bind = "127.0.0.1"; config.host = config.bind = "127.0.0.1";

View file

@ -46,6 +46,10 @@ describe("Network", function () {
saslPassword: "testpassword", saslPassword: "testpassword",
commands: [], commands: [],
nick: "chillin`", nick: "chillin`",
proxyEnabled: false,
proxyHost: "",
proxyPort: 1080,
proxyUsername: "",
channels: [ channels: [
{name: "#thelounge", key: ""}, {name: "#thelounge", key: ""},
{name: "&foobar", key: ""}, {name: "&foobar", key: ""},
@ -91,7 +95,7 @@ describe("Network", function () {
rejectUnauthorized: false, rejectUnauthorized: false,
}); });
expect(network.validate()).to.be.true; expect(network.validate()).to.be.true;
expect(network.host).to.equal("chat.freenode.net"); expect(network.host).to.equal("irc.example.com");
expect(network.port).to.equal(6697); expect(network.port).to.equal(6697);
expect(network.tls).to.be.true; expect(network.tls).to.be.true;
expect(network.rejectUnauthorized).to.be.true; expect(network.rejectUnauthorized).to.be.true;
@ -103,7 +107,7 @@ describe("Network", function () {
host: "some.fake.tld", host: "some.fake.tld",
}); });
expect(network2.validate()).to.be.true; expect(network2.validate()).to.be.true;
expect(network2.host).to.equal("chat.freenode.net"); expect(network2.host).to.equal("irc.example.com");
Helper.config.lockNetwork = false; Helper.config.lockNetwork = false;
}); });
@ -265,7 +269,7 @@ describe("Network", function () {
// Lobby and initial channel // Lobby and initial channel
expect(network.channels.length).to.equal(2); expect(network.channels.length).to.equal(2);
const newChan = new Chan({name: "#freenode"}); const newChan = new Chan({name: "#foo"});
network.addChannel(newChan); network.addChannel(newChan);
expect(network.channels.length).to.equal(3); expect(network.channels.length).to.equal(3);
@ -278,13 +282,13 @@ describe("Network", function () {
const network = new Network({ const network = new Network({
channels: [chan1, chan2, chan3], channels: [chan1, chan2, chan3],
name: "freenode", name: "foo",
}); });
const newChan = new Chan({name: "#freenode"}); const newChan = new Chan({name: "#foo"});
network.addChannel(newChan); network.addChannel(newChan);
expect(network.channels[0].name).to.equal("freenode"); expect(network.channels[0].name).to.equal("foo");
expect(network.channels[1]).to.equal(chan1); expect(network.channels[1]).to.equal(chan1);
expect(network.channels[2]).to.equal(newChan); expect(network.channels[2]).to.equal(newChan);
expect(network.channels[3]).to.equal(chan2); expect(network.channels[3]).to.equal(chan2);
@ -299,7 +303,7 @@ describe("Network", function () {
channels: [chan1, chan2], channels: [chan1, chan2],
}); });
const newChan = new Chan({name: "#freenode"}); const newChan = new Chan({name: "#foo"});
network.addChannel(newChan); network.addChannel(newChan);
expect(network.channels[1]).to.equal(chan1); expect(network.channels[1]).to.equal(chan1);
@ -393,7 +397,7 @@ describe("Network", function () {
channels: [banlist, chan1, user1], channels: [banlist, chan1, user1],
}); });
const newChan = new Chan({name: "#freenode"}); const newChan = new Chan({name: "#foo"});
network.addChannel(newChan); network.addChannel(newChan);
expect(network.channels[1]).to.equal(newChan); expect(network.channels[1]).to.equal(newChan);
@ -404,7 +408,7 @@ describe("Network", function () {
it("should never add something in front of the lobby", function () { it("should never add something in front of the lobby", function () {
const network = new Network({ const network = new Network({
name: "freenode", name: "foo",
channels: [], channels: [],
}); });

3086
yarn.lock

File diff suppressed because it is too large Load diff