mirror of
https://github.com/thelounge/thelounge.git
synced 2024-04-25 11:30:38 +02:00
Merge with master
This commit is contained in:
commit
2721395ada
|
@ -2,7 +2,7 @@
|
|||
root: true
|
||||
|
||||
parserOptions:
|
||||
ecmaVersion: 2018
|
||||
ecmaVersion: 2020
|
||||
|
||||
env:
|
||||
es6: true
|
||||
|
|
2
.github/ISSUE_TEMPLATE/Bug_Report.md
vendored
2
.github/ISSUE_TEMPLATE/Bug_Report.md
vendored
|
@ -4,7 +4,7 @@ about: Create a bug report
|
|||
labels: "Type: Bug"
|
||||
---
|
||||
|
||||
<!-- Have a question? Join #thelounge on freenode -->
|
||||
<!-- Have a question? Join #thelounge on Libera.Chat -->
|
||||
|
||||
- _Node version:_
|
||||
- _Browser version:_
|
||||
|
|
2
.github/ISSUE_TEMPLATE/Feature_Request.md
vendored
2
.github/ISSUE_TEMPLATE/Feature_Request.md
vendored
|
@ -4,7 +4,7 @@ about: Request a new 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. -->
|
||||
|
||||
### Feature Description
|
||||
|
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -13,4 +13,4 @@ contact_links:
|
|||
|
||||
- name: General support
|
||||
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
2
.github/SUPPORT.md
vendored
|
@ -6,6 +6,6 @@ need help, you have a few options:
|
|||
- 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)
|
||||
(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
|
||||
help you.
|
||||
|
|
45
CHANGELOG.md
45
CHANGELOG.md
|
@ -4,6 +4,51 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
<!-- 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]
|
||||
|
||||
[See the full changelog](https://github.com/thelounge/thelounge/compare/v4.2.0...v4.3.0-pre.1)
|
||||
|
|
|
@ -20,8 +20,8 @@
|
|||
</p>
|
||||
<p align="center">
|
||||
<a href="https://demo.thelounge.chat/"><img
|
||||
alt="#thelounge IRC channel on freenode"
|
||||
src="https://img.shields.io/badge/freenode-%23thelounge-415364.svg?colorA=ff9e18"></a>
|
||||
alt="#thelounge IRC channel on Libera.Chat"
|
||||
src="https://img.shields.io/badge/Libera.Chat-%23thelounge-415364.svg?colorA=ff9e18"></a>
|
||||
<a href="https://yarn.pm/thelounge"><img
|
||||
alt="npm version"
|
||||
src="https://img.shields.io/npm/v/thelounge.svg?colorA=333a41&maxAge=3600"></a>
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
- Contact us privately first, in a
|
||||
[responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure)
|
||||
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`.
|
||||
- By email, send us your report at <security@thelounge.chat>.
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
<template>
|
||||
<div id="viewport" :class="viewportClasses" role="tablist">
|
||||
<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>
|
||||
<Mentions />
|
||||
<ImageViewer ref="imageViewer" />
|
||||
|
@ -51,6 +56,7 @@ export default {
|
|||
Mousetrap.bind("esc", this.escapeKey);
|
||||
Mousetrap.bind("alt+u", this.toggleUserList);
|
||||
Mousetrap.bind("alt+s", this.toggleSidebar);
|
||||
Mousetrap.bind("alt+m", this.toggleMentions);
|
||||
|
||||
// Make a single throttled resize listener available to all components
|
||||
this.debouncedResize = throttle(() => {
|
||||
|
@ -72,6 +78,7 @@ export default {
|
|||
Mousetrap.unbind("esc", this.escapeKey);
|
||||
Mousetrap.unbind("alt+u", this.toggleUserList);
|
||||
Mousetrap.unbind("alt+s", this.toggleSidebar);
|
||||
Mousetrap.unbind("alt+m", this.toggleMentions);
|
||||
|
||||
window.removeEventListener("resize", this.debouncedResize);
|
||||
clearTimeout(this.dayChangeTimeout);
|
||||
|
@ -98,6 +105,11 @@ export default {
|
|||
|
||||
return false;
|
||||
},
|
||||
toggleMentions() {
|
||||
if (this.$store.state.networks.length !== 0) {
|
||||
eventbus.emit("mentions:toggle");
|
||||
}
|
||||
},
|
||||
msUntilNextDay() {
|
||||
// Compute how many milliseconds are remaining until the next day starts
|
||||
const today = new Date();
|
||||
|
|
|
@ -54,20 +54,25 @@ export default {
|
|||
methods: {
|
||||
getAriaLabel() {
|
||||
const extra = [];
|
||||
const type = this.channel.type;
|
||||
|
||||
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) {
|
||||
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 `${this.channel.name} (${extra.join(", ")})`;
|
||||
}
|
||||
|
||||
return this.channel.name;
|
||||
return `${type}: ${this.channel.name} ${extra.length ? `(${extra.join(", ")})` : ""}`;
|
||||
},
|
||||
click() {
|
||||
if (this.isFiltering) {
|
||||
|
|
|
@ -18,7 +18,9 @@
|
|||
>
|
||||
<div class="header">
|
||||
<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">
|
||||
<input
|
||||
ref="topicInput"
|
||||
|
@ -93,13 +95,13 @@
|
|||
>
|
||||
<div class="scroll-down-arrow" />
|
||||
</div>
|
||||
<ChatUserList v-if="channel.type === 'channel'" :channel="channel" />
|
||||
<MessageList
|
||||
ref="messageList"
|
||||
:network="network"
|
||||
:channel="channel"
|
||||
:focused="focused"
|
||||
/>
|
||||
<ChatUserList v-if="channel.type === 'channel'" :channel="channel" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -140,22 +140,53 @@ export default {
|
|||
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;
|
||||
|
||||
if (channel.inputHistoryPosition === 0) {
|
||||
channel.inputHistory[channel.inputHistoryPosition] = channel.pendingMessage;
|
||||
}
|
||||
|
||||
if (key === "up") {
|
||||
if (key === "up" && onRow === 0) {
|
||||
if (channel.inputHistoryPosition < channel.inputHistory.length - 1) {
|
||||
channel.inputHistoryPosition++;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else if (channel.inputHistoryPosition > 0) {
|
||||
} else if (key === "down" && channel.inputHistoryPosition > 0 && onRow === totalRows) {
|
||||
channel.inputHistoryPosition--;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
return false;
|
||||
|
@ -183,6 +214,10 @@ export default {
|
|||
},
|
||||
setInputSize() {
|
||||
this.$nextTick(() => {
|
||||
if (!this.$refs.input) {
|
||||
return;
|
||||
}
|
||||
|
||||
const style = window.getComputedStyle(this.$refs.input);
|
||||
const lineHeight = parseFloat(style.lineHeight, 10) || 1;
|
||||
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
<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">
|
||||
<input
|
||||
ref="input"
|
||||
|
|
|
@ -13,8 +13,12 @@
|
|||
:data-command="message.command"
|
||||
:data-from="message.from && message.from.nick"
|
||||
>
|
||||
<span :aria-label="messageTimeLocale" class="time tooltipped tooltipped-e"
|
||||
>{{ messageTime }}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
:aria-label="messageTimeLocale"
|
||||
class="time tooltipped tooltipped-e"
|
||||
>
|
||||
{{ messageTime }}
|
||||
</span>
|
||||
<template v-if="message.type === 'unhandled'">
|
||||
<span class="from">[{{ message.command }}]</span>
|
||||
|
@ -47,23 +51,23 @@
|
|||
<template v-else>
|
||||
<span v-if="message.type === 'message'" class="from">
|
||||
<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" />
|
||||
<span class="only-copy">> </span>
|
||||
<span class="only-copy" aria-hidden="true">> </span>
|
||||
</template>
|
||||
</span>
|
||||
<span v-else-if="message.type === 'plugin'" class="from">
|
||||
<template v-if="message.from && message.from.nick">
|
||||
<span class="only-copy">[</span>
|
||||
<span class="only-copy" aria-hidden="true">[</span>
|
||||
{{ message.from.nick }}
|
||||
<span class="only-copy">] </span>
|
||||
<span class="only-copy" aria-hidden="true">] </span>
|
||||
</template>
|
||||
</span>
|
||||
<span v-else class="from">
|
||||
<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" />
|
||||
<span class="only-copy">- </span>
|
||||
<span class="only-copy" aria-hidden="true">- </span>
|
||||
</template>
|
||||
</span>
|
||||
<span class="content" dir="auto">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<input
|
||||
ref="searchInputField"
|
||||
v-model="searchInput"
|
||||
type="text"
|
||||
type="search"
|
||||
name="search"
|
||||
class="input"
|
||||
placeholder="Search messages…"
|
||||
|
@ -104,6 +104,10 @@ export default {
|
|||
mounted() {
|
||||
this.searchInput = this.$route.query.q;
|
||||
this.searchOpened = this.onSearchPage;
|
||||
|
||||
if (!this.searchInput && this.searchOpened) {
|
||||
this.$refs.searchInputField.focus();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeSearch() {
|
||||
|
|
|
@ -6,17 +6,20 @@
|
|||
>username to <b>{{ message.new_ident }}</b></span
|
||||
>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default {
|
||||
name: "MessageTypeChangeHost",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<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">
|
||||
<i class="account"> [{{ message.account }}]</i>
|
||||
</template>
|
||||
|
@ -13,11 +13,13 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default {
|
||||
name: "MessageTypeJoin",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<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"
|
||||
>(<ParsedMessage :network="network" :message="message" />)</i
|
||||
>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<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"
|
||||
>(<ParsedMessage :network="network" :message="message" />)</i
|
||||
>
|
||||
|
|
|
@ -12,7 +12,12 @@
|
|||
</template>
|
||||
|
||||
<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">
|
||||
<dt>Actual host:</dt>
|
||||
|
|
|
@ -98,6 +98,80 @@
|
|||
</label>
|
||||
</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 v-else-if="config.lockNetwork && !$store.state.serverConfiguration.public">
|
||||
<h2>Network settings</h2>
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
<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.
|
||||
</div>
|
||||
<div v-else ref="networklist">
|
||||
<div v-else ref="networklist" role="navigation" aria-label="Network and Channel list">
|
||||
<div class="jump-to-input">
|
||||
<input
|
||||
ref="searchInput"
|
||||
|
@ -74,6 +79,7 @@
|
|||
}"
|
||||
class="network"
|
||||
role="region"
|
||||
aria-live="polite"
|
||||
>
|
||||
<NetworkLobby
|
||||
:network="network"
|
||||
|
|
|
@ -6,11 +6,13 @@
|
|||
:src="`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg.svg`"
|
||||
class="logo"
|
||||
alt="The Lounge"
|
||||
role="presentation"
|
||||
/>
|
||||
<img
|
||||
:src="`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg-inverted.svg`"
|
||||
class="logo-inverted"
|
||||
alt="The Lounge"
|
||||
role="presentation"
|
||||
/>
|
||||
<span
|
||||
v-if="isDevelopment"
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
<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_at">{{ localetime(ban.banned_at) }}</td>
|
||||
</tr>
|
||||
|
@ -18,10 +18,14 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import localetime from "../../js/helpers/localetime";
|
||||
|
||||
export default {
|
||||
name: "ListBans",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
},
|
||||
props: {
|
||||
network: Object,
|
||||
channel: Object,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
<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>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -16,10 +16,14 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import localetime from "../../js/helpers/localetime";
|
||||
|
||||
export default {
|
||||
name: "ListIgnored",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
},
|
||||
props: {
|
||||
network: Object,
|
||||
channel: Object,
|
||||
|
|
|
@ -9,7 +9,9 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
<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_at">{{ localetime(invite.invited_at) }}</td>
|
||||
</tr>
|
||||
|
@ -18,10 +20,14 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import localetime from "../../js/helpers/localetime";
|
||||
|
||||
export default {
|
||||
name: "ListInvites",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
},
|
||||
props: {
|
||||
network: Object,
|
||||
channel: Object,
|
||||
|
|
|
@ -189,6 +189,16 @@
|
|||
</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="subject">
|
||||
<span><kbd>Esc</kbd></span>
|
||||
|
@ -673,6 +683,15 @@
|
|||
</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="subject">
|
||||
<code>/topic [newtopic]</code>
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
type="checkbox"
|
||||
name="showSeconds"
|
||||
/>
|
||||
Show seconds in timestamp
|
||||
Include seconds in timestamp
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -101,7 +101,7 @@
|
|||
type="checkbox"
|
||||
name="use12hClock"
|
||||
/>
|
||||
Show 12-hour timestamps
|
||||
Use 12-hour timestamps
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="!$store.state.serverConfiguration.public && $store.state.settings.advanced">
|
||||
|
@ -306,6 +306,7 @@
|
|||
<input
|
||||
id="desktopNotifications"
|
||||
:checked="$store.state.settings.desktopNotifications"
|
||||
:disabled="$store.state.desktopNotificationState === 'nohttps'"
|
||||
type="checkbox"
|
||||
name="desktopNotifications"
|
||||
/>
|
||||
|
@ -316,6 +317,14 @@
|
|||
>
|
||||
<strong>Warning</strong>: Notifications are not supported by your browser.
|
||||
</div>
|
||||
<div
|
||||
v-if="$store.state.desktopNotificationState === 'nohttps'"
|
||||
id="warnBlockedDesktopNotifications"
|
||||
class="error"
|
||||
>
|
||||
<strong>Warning</strong>: Notifications are only supported over HTTPS
|
||||
connections.
|
||||
</div>
|
||||
<div
|
||||
v-if="$store.state.desktopNotificationState === 'blocked'"
|
||||
id="warnBlockedDesktopNotifications"
|
||||
|
|
|
@ -1147,6 +1147,7 @@ textarea.input {
|
|||
|
||||
#chat .chat-content {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
@ -1195,7 +1196,6 @@ textarea.input {
|
|||
}
|
||||
|
||||
#chat .show-more {
|
||||
margin-top: 50px;
|
||||
padding: 10px;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 0;
|
||||
|
@ -1508,8 +1508,11 @@ textarea.input {
|
|||
}
|
||||
|
||||
#chat .msg[data-type="notice"] .time,
|
||||
#chat .msg[data-type="wallops"] .time,
|
||||
#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;
|
||||
}
|
||||
|
||||
|
@ -1517,6 +1520,10 @@ textarea.input {
|
|||
content: "Notice: ";
|
||||
}
|
||||
|
||||
#chat .msg[data-type="wallops"] .from .user::before {
|
||||
content: "Wallops: ";
|
||||
}
|
||||
|
||||
#chat .msg[data-type="error"],
|
||||
#chat .msg[data-type="error"] .from {
|
||||
color: #e74c3c;
|
||||
|
@ -1840,19 +1847,23 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
|||
margin-top: 6px;
|
||||
}
|
||||
|
||||
#connect .tls input {
|
||||
#connect .tls input,
|
||||
#connect input[name="proxyEnabled"] {
|
||||
margin: 3px 10px 0 0;
|
||||
}
|
||||
|
||||
#connect\:host {
|
||||
#connect\:host,
|
||||
#connect\:proxyHost {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
#connect\:port {
|
||||
#connect\:port,
|
||||
#connect\:proxyPort {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
#connect\:portseparator {
|
||||
#connect\:portseparator,
|
||||
#connect\:proxyPortSeparator {
|
||||
width: 5%;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
|
@ -2624,6 +2635,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
|||
right: 0;
|
||||
transform: translateX(180px);
|
||||
transition: transform 0.2s;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#viewport.userlist-open #chat .userlist {
|
||||
|
|
|
@ -52,8 +52,8 @@
|
|||
<div id="loading">
|
||||
<div class="window">
|
||||
<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-inverted.svg" class="logo-inverted" 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="" width="256" height="170">
|
||||
<p id="loading-page-message">The Lounge requires a modern browser with JavaScript enabled.</p>
|
||||
</div>
|
||||
<div id="loading-reload-container">
|
||||
|
|
20
client/js/commands/search.js
Normal file
20
client/js/commands/search.js
Normal 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};
|
|
@ -1238,8 +1238,8 @@
|
|||
"credit_card": "💳",
|
||||
"receipt": "🧾",
|
||||
"chart": "💹",
|
||||
"email": "✉️",
|
||||
"envelope": "✉️",
|
||||
"email": "📧",
|
||||
"e_mail": "📧",
|
||||
"incoming_envelope": "📨",
|
||||
"envelope_with_arrow": "📩",
|
||||
|
|
|
@ -6,17 +6,24 @@ import socket from "../socket";
|
|||
import store from "../store";
|
||||
|
||||
socket.on("more", function (data) {
|
||||
const channel = store.getters.findChannel(data.chan);
|
||||
const channel = store.getters.findChannel(data.chan)?.channel;
|
||||
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
channel.channel.moreHistoryAvailable =
|
||||
data.totalMessages > channel.channel.messages.length + data.messages.length;
|
||||
channel.channel.messages.unshift(...data.messages);
|
||||
channel.inputHistory = channel.inputHistory.concat(
|
||||
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(() => {
|
||||
channel.channel.historyLoading = false;
|
||||
channel.historyLoading = false;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,6 +12,8 @@ function detectDesktopNotificationState() {
|
|||
return "unsupported";
|
||||
} else if (Notification.permission === "granted") {
|
||||
return "granted";
|
||||
} else if (!window.isSecureContext) {
|
||||
return "nohttps";
|
||||
}
|
||||
|
||||
return "blocked";
|
||||
|
@ -190,7 +192,14 @@ const store = new Vuex.Store({
|
|||
// TODO: This should be a mutation
|
||||
channel.pendingMessage = "";
|
||||
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.scrolledToBottom = true;
|
||||
channel.editTopic = false;
|
||||
|
|
|
@ -231,12 +231,12 @@ module.exports = {
|
|||
// - `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
|
||||
// Freenode by default:
|
||||
// Libera.Chat by default:
|
||||
//
|
||||
// ```js
|
||||
// defaults: {
|
||||
// name: "Freenode",
|
||||
// host: "chat.freenode.net",
|
||||
// name: "Libera.Chat",
|
||||
// host: "irc.libera.chat",
|
||||
// port: 6697,
|
||||
// password: "",
|
||||
// tls: true,
|
||||
|
@ -248,8 +248,8 @@ module.exports = {
|
|||
// }
|
||||
// ```
|
||||
defaults: {
|
||||
name: "Freenode",
|
||||
host: "chat.freenode.net",
|
||||
name: "Libera.Chat",
|
||||
host: "irc.libera.chat",
|
||||
port: 6697,
|
||||
password: "",
|
||||
tls: true,
|
||||
|
|
24
package.json
24
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "thelounge",
|
||||
"description": "The self-hosted Web IRC client",
|
||||
"version": "4.3.0-pre.1",
|
||||
"version": "4.3.0-pre.4",
|
||||
"preferGlobal": true,
|
||||
"bin": {
|
||||
"thelounge": "index.js"
|
||||
|
@ -43,19 +43,19 @@
|
|||
"bcryptjs": "2.4.3",
|
||||
"busboy": "0.3.1",
|
||||
"chalk": "4.1.1",
|
||||
"cheerio": "1.0.0-rc.5",
|
||||
"cheerio": "1.0.0-rc.10",
|
||||
"commander": "7.2.0",
|
||||
"content-disposition": "0.5.3",
|
||||
"express": "4.17.1",
|
||||
"file-type": "16.2.0",
|
||||
"filenamify": "4.2.0",
|
||||
"got": "11.8.1",
|
||||
"irc-framework": "4.9.0",
|
||||
"got": "11.8.2",
|
||||
"irc-framework": "4.11.0",
|
||||
"is-utf8": "0.2.1",
|
||||
"ldapjs": "2.2.3",
|
||||
"linkify-it": "3.0.2",
|
||||
"lodash": "4.17.20",
|
||||
"mime-types": "2.1.28",
|
||||
"lodash": "4.17.21",
|
||||
"mime-types": "2.1.31",
|
||||
"node-forge": "0.10.0",
|
||||
"package-json": "6.5.0",
|
||||
"read": "1.0.7",
|
||||
|
@ -73,8 +73,8 @@
|
|||
"better-sqlite3": "7.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.14.0",
|
||||
"@babel/preset-env": "7.14.0",
|
||||
"@babel/core": "7.14.6",
|
||||
"@babel/preset-env": "7.14.7",
|
||||
"@fortawesome/fontawesome-free": "5.15.3",
|
||||
"@vue/server-test-utils": "1.1.3",
|
||||
"@vue/test-utils": "1.1.3",
|
||||
|
@ -83,14 +83,14 @@
|
|||
"chai": "4.3.4",
|
||||
"copy-webpack-plugin": "7.0.0",
|
||||
"css-loader": "5.1.1",
|
||||
"cssnano": "4.1.10",
|
||||
"dayjs": "1.10.4",
|
||||
"emoji-regex": "9.2.1",
|
||||
"cssnano": "4.1.11",
|
||||
"dayjs": "1.10.5",
|
||||
"emoji-regex": "9.2.2",
|
||||
"eslint": "7.23.0",
|
||||
"eslint-config-prettier": "6.15.0",
|
||||
"eslint-plugin-vue": "7.5.0",
|
||||
"fuzzy": "0.1.3",
|
||||
"husky": "4.3.5",
|
||||
"husky": "4.3.8",
|
||||
"mini-css-extract-plugin": "1.3.6",
|
||||
"mocha": "8.2.1",
|
||||
"mousetrap": "1.6.5",
|
||||
|
|
0
scripts/generate-emoji.js
Normal file → Executable file
0
scripts/generate-emoji.js
Normal file → Executable file
|
@ -257,6 +257,12 @@ Client.prototype.connect = function (args, isStartup = false) {
|
|||
commands: args.commands || [],
|
||||
channels: channels,
|
||||
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
|
||||
|
|
|
@ -34,7 +34,7 @@ try {
|
|||
createPackagesFolder();
|
||||
|
||||
// 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");
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@ program
|
|||
.on("--help", Utils.extraHelp)
|
||||
.action(function (packageName) {
|
||||
const fs = require("fs");
|
||||
const fspromises = fs.promises;
|
||||
const path = require("path");
|
||||
const packageJson = require("package-json");
|
||||
|
||||
if (!fs.existsSync(Helper.getConfigPath())) {
|
||||
|
@ -21,22 +23,31 @@ program
|
|||
}
|
||||
|
||||
log.info("Retrieving information about the package...");
|
||||
let readFile = null;
|
||||
let isLocalFile = false;
|
||||
|
||||
const split = packageName.split("@");
|
||||
packageName = split[0];
|
||||
const packageVersion = split[1] || "latest";
|
||||
if (packageName.startsWith("file:")) {
|
||||
isLocalFile = true;
|
||||
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, {
|
||||
fullMetadata: true,
|
||||
version: packageVersion,
|
||||
})
|
||||
readFile = packageJson(packageName, {
|
||||
fullMetadata: true,
|
||||
version: packageVersion,
|
||||
});
|
||||
}
|
||||
|
||||
readFile
|
||||
.then((json) => {
|
||||
const humanVersion = isLocalFile ? packageName : `${json.name} v${json.version}`;
|
||||
|
||||
if (!("thelounge" in json)) {
|
||||
log.error(
|
||||
`${colors.red(
|
||||
json.name + " v" + json.version
|
||||
)} does not have The Lounge metadata.`
|
||||
);
|
||||
log.error(`${colors.red(humanVersion)} does not have The Lounge metadata.`);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
@ -47,7 +58,7 @@ program
|
|||
) {
|
||||
log.error(
|
||||
`${colors.red(
|
||||
json.name + " v" + json.version
|
||||
humanVersion
|
||||
)} does not support The Lounge v${Helper.getVersionNumber()}. Supported version(s): ${
|
||||
json.thelounge.supports
|
||||
}`
|
||||
|
@ -56,20 +67,23 @@ program
|
|||
process.exit(2);
|
||||
}
|
||||
|
||||
log.info(`Installing ${colors.green(json.name + " v" + json.version)}...`);
|
||||
|
||||
return Utils.executeYarnCommand("add", "--exact", `${json.name}@${json.version}`)
|
||||
log.info(`Installing ${colors.green(humanVersion)}...`);
|
||||
const yarnVersion = isLocalFile ? packageName : `${json.name}@${json.version}`;
|
||||
return Utils.executeYarnCommand("add", "--exact", yarnVersion)
|
||||
.then(() => {
|
||||
log.info(
|
||||
`${colors.green(
|
||||
json.name + " v" + json.version
|
||||
)} has been successfully installed.`
|
||||
);
|
||||
log.info(`${colors.green(humanVersion)} has been successfully installed.`);
|
||||
|
||||
if (isLocalFile) {
|
||||
// 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) => {
|
||||
throw `Failed to install ${colors.green(
|
||||
json.name + " v" + json.version
|
||||
)}. Exit code: ${code}`;
|
||||
throw `Failed to install ${colors.red(humanVersion)}. Exit code: ${code}`;
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
|
|
|
@ -79,6 +79,7 @@ Msg.Type = {
|
|||
WHOIS: "whois",
|
||||
RAW: "raw",
|
||||
PLUGIN: "plugin",
|
||||
WALLOPS: "wallops",
|
||||
};
|
||||
|
||||
module.exports = Msg;
|
||||
|
|
|
@ -46,6 +46,13 @@ function Network(attr) {
|
|||
PREFIX: ["!", "@", "%", "+"],
|
||||
NETWORK: "",
|
||||
},
|
||||
|
||||
proxyHost: "",
|
||||
proxyPort: 1080,
|
||||
proxyUsername: "",
|
||||
proxyPassword: "",
|
||||
proxyEnabled: false,
|
||||
|
||||
chanCache: [],
|
||||
ignoreList: [],
|
||||
keepNick: null,
|
||||
|
@ -90,6 +97,12 @@ Network.prototype.validate = function (client) {
|
|||
this.saslAccount = cleanString(this.saslAccount);
|
||||
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) {
|
||||
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.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) {
|
||||
delete this.irc.options.sasl_mechanism;
|
||||
delete this.irc.options.account;
|
||||
|
@ -274,6 +298,12 @@ Network.prototype.edit = function (client, args) {
|
|||
this.saslAccount = String(args.saslAccount || "");
|
||||
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
|
||||
this.commands = String(args.commands || "")
|
||||
.replace(/\r\n|\r|\n/g, "\n")
|
||||
|
@ -455,6 +485,12 @@ Network.prototype.exportForEdit = function () {
|
|||
"saslAccount",
|
||||
"saslPassword",
|
||||
"commands",
|
||||
|
||||
"proxyEnabled",
|
||||
"proxyHost",
|
||||
"proxyPort",
|
||||
"proxyUsername",
|
||||
"proxyPassword",
|
||||
];
|
||||
|
||||
if (!Helper.config.lockNetwork) {
|
||||
|
@ -491,6 +527,11 @@ Network.prototype.export = function () {
|
|||
"saslPassword",
|
||||
"commands",
|
||||
"ignoreList",
|
||||
|
||||
"proxyHost",
|
||||
"proxyPort",
|
||||
"proxyUsername",
|
||||
"proxyEnabled",
|
||||
]);
|
||||
|
||||
network.channels = this.channels
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const clientSideCommands = ["/collapse", "/expand"];
|
||||
const clientSideCommands = ["/collapse", "/expand", "/search"];
|
||||
|
||||
const passThroughCommands = [
|
||||
"/as",
|
||||
|
|
|
@ -3,10 +3,14 @@
|
|||
const Chan = require("../../models/chan");
|
||||
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) {
|
||||
if (cmd !== "mode") {
|
||||
if (cmd === "umode") {
|
||||
irc.raw("MODE", nick, ...args);
|
||||
|
||||
return;
|
||||
} else if (cmd !== "mode") {
|
||||
if (chan.type !== Chan.Type.CHANNEL) {
|
||||
chan.pushMessage(
|
||||
this,
|
||||
|
@ -19,7 +23,9 @@ exports.input = function ({irc, nick}, chan, cmd, args) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
const target = args.filter((arg) => arg !== "");
|
||||
|
||||
if (target.length === 0) {
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
|
@ -40,9 +46,13 @@ exports.input = function ({irc, nick}, chan, cmd, args) {
|
|||
devoice: "-v",
|
||||
}[cmd];
|
||||
|
||||
args.forEach(function (target) {
|
||||
irc.raw("MODE", chan.name, mode, target);
|
||||
});
|
||||
const limit = parseInt(irc.network.supports("MODES")) || target.length;
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -222,6 +222,7 @@ function parse(msg, chan, preview, res, client) {
|
|||
case "image/gif":
|
||||
case "image/jpg":
|
||||
case "image/jpeg":
|
||||
case "image/jxl":
|
||||
case "image/webp":
|
||||
case "image/avif":
|
||||
if (!Helper.config.prefetchStorage && Helper.config.disableMediaPreview) {
|
||||
|
|
|
@ -11,12 +11,6 @@ module.exports = function (irc, network) {
|
|||
const client = this;
|
||||
|
||||
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;
|
||||
handleMessage(data);
|
||||
});
|
||||
|
@ -33,7 +27,7 @@ module.exports = function (irc, network) {
|
|||
|
||||
irc.on("wallops", function (data) {
|
||||
data.from_server = true;
|
||||
data.type = Msg.Type.NOTICE;
|
||||
data.type = Msg.Type.WALLOPS;
|
||||
handleMessage(data);
|
||||
});
|
||||
|
||||
|
@ -44,6 +38,12 @@ module.exports = function (irc, network) {
|
|||
let showInActive = false;
|
||||
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
|
||||
const shouldIgnore =
|
||||
!self &&
|
||||
|
@ -51,8 +51,13 @@ module.exports = function (irc, network) {
|
|||
return Helper.compareHostmask(entry, data);
|
||||
});
|
||||
|
||||
// Server messages go to server window, no questions asked
|
||||
if (data.from_server) {
|
||||
// Server messages that aren't targeted at a channel go to the server window
|
||||
if (
|
||||
data.from_server &&
|
||||
(!data.target ||
|
||||
!network.getChannel(data.target) ||
|
||||
network.getChannel(data.target).type !== Chan.Type.CHANNEL)
|
||||
) {
|
||||
chan = network.channels[0];
|
||||
from = chan.getUser(data.nick);
|
||||
} else {
|
||||
|
|
|
@ -29,6 +29,7 @@ const inlineContentDispositionTypes = {
|
|||
"image/png": "image.png",
|
||||
"image/webp": "image.webp",
|
||||
"image/avif": "image.avif",
|
||||
"image/jxl": "image.jxl",
|
||||
"text/plain": "text.txt",
|
||||
"video/mp4": "video.mp4",
|
||||
"video/ogg": "video.ogv",
|
||||
|
|
|
@ -55,8 +55,8 @@ describe("cleanIrcMessage", function () {
|
|||
expected: "bold bold",
|
||||
},
|
||||
{
|
||||
input: "\x02irc\x0f://\x1dfreenode.net\x0f/\x034,8thelounge",
|
||||
expected: "irc://freenode.net/thelounge",
|
||||
input: "\x02irc\x0f://\x1dirc.example.com\x0f/\x034,8thelounge",
|
||||
expected: "irc://irc.example.com/thelounge",
|
||||
},
|
||||
{
|
||||
input: "\x02#\x038,9thelounge",
|
||||
|
|
|
@ -8,12 +8,12 @@ const {
|
|||
|
||||
describe("findLinks", () => {
|
||||
it("should find url", () => {
|
||||
const input = "irc://freenode.net/thelounge";
|
||||
const input = "irc://irc.example.com/thelounge";
|
||||
const expected = [
|
||||
{
|
||||
start: 0,
|
||||
end: 28,
|
||||
link: "irc://freenode.net/thelounge",
|
||||
end: 31,
|
||||
link: "irc://irc.example.com/thelounge",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -63,10 +63,10 @@ describe("IRC formatted message parser", () => {
|
|||
it("should find urls", async () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: "irc://freenode.net/thelounge",
|
||||
input: "irc://irc.example.com/thelounge",
|
||||
expected:
|
||||
'<a href="irc://freenode.net/thelounge" dir="auto" target="_blank" rel="noopener">' +
|
||||
"irc://freenode.net/thelounge" +
|
||||
'<a href="irc://irc.example.com/thelounge" dir="auto" target="_blank" rel="noopener">' +
|
||||
"irc://irc.example.com/thelounge" +
|
||||
"</a>",
|
||||
},
|
||||
{
|
||||
|
@ -416,12 +416,12 @@ describe("IRC formatted message parser", () => {
|
|||
it("should go bonkers like mirc", async () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: "\x02irc\x0f://\x1dfreenode.net\x0f/\x034,8thelounge",
|
||||
input: "\x02irc\x0f://\x1dirc.example.com\x0f/\x034,8thelounge",
|
||||
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-italic">freenode.net</span>' +
|
||||
'<span class="irc-italic">irc.example.com</span>' +
|
||||
"/" +
|
||||
'<span class="irc-fg4 irc-bg8">thelounge</span>' +
|
||||
"</a>",
|
||||
|
|
|
@ -17,15 +17,38 @@ describe("Commands", function () {
|
|||
});
|
||||
|
||||
const testableNetwork = {
|
||||
firstCommand: null,
|
||||
lastCommand: null,
|
||||
nick: "xPaw",
|
||||
irc: {
|
||||
network: {
|
||||
supports(type) {
|
||||
if (type.toUpperCase() === "MODES") {
|
||||
return "4";
|
||||
}
|
||||
},
|
||||
},
|
||||
raw(...args) {
|
||||
testableNetwork.firstCommand = testableNetwork.lastCommand;
|
||||
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 () {
|
||||
const test = function (expected, args) {
|
||||
ModeCommand.input(testableNetwork, channel, "mode", Array.from(args));
|
||||
|
@ -81,10 +104,34 @@ describe("Commands", function () {
|
|||
|
||||
ModeCommand.input(testableNetwork, channel, "devoice", ["xPaw"]);
|
||||
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge -v xPaw");
|
||||
});
|
||||
|
||||
// Multiple arguments are supported, sent as separate commands
|
||||
ModeCommand.input(testableNetwork, channel, "devoice", ["xPaw", "Max-P"]);
|
||||
expect(testableNetwork.lastCommand).to.equal("MODE #thelounge -v Max-P");
|
||||
it("should use ISUPPORT MODES on shorthand commands", function () {
|
||||
ModeCommand.input(testableNetwork, channel, "voice", ["xPaw", "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"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
2
test/fixtures/.thelounge/config.js
vendored
2
test/fixtures/.thelounge/config.js
vendored
|
@ -2,6 +2,8 @@
|
|||
|
||||
var config = require("../../../defaults/config.js");
|
||||
|
||||
config.defaults.name = "Example IRC Server";
|
||||
config.defaults.host = "irc.example.com";
|
||||
config.public = true;
|
||||
config.prefetch = true;
|
||||
config.host = config.bind = "127.0.0.1";
|
||||
|
|
|
@ -46,6 +46,10 @@ describe("Network", function () {
|
|||
saslPassword: "testpassword",
|
||||
commands: [],
|
||||
nick: "chillin`",
|
||||
proxyEnabled: false,
|
||||
proxyHost: "",
|
||||
proxyPort: 1080,
|
||||
proxyUsername: "",
|
||||
channels: [
|
||||
{name: "#thelounge", key: ""},
|
||||
{name: "&foobar", key: ""},
|
||||
|
@ -91,7 +95,7 @@ describe("Network", function () {
|
|||
rejectUnauthorized: false,
|
||||
});
|
||||
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.tls).to.be.true;
|
||||
expect(network.rejectUnauthorized).to.be.true;
|
||||
|
@ -103,7 +107,7 @@ describe("Network", function () {
|
|||
host: "some.fake.tld",
|
||||
});
|
||||
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;
|
||||
});
|
||||
|
@ -265,7 +269,7 @@ describe("Network", function () {
|
|||
// Lobby and initial channel
|
||||
expect(network.channels.length).to.equal(2);
|
||||
|
||||
const newChan = new Chan({name: "#freenode"});
|
||||
const newChan = new Chan({name: "#foo"});
|
||||
network.addChannel(newChan);
|
||||
|
||||
expect(network.channels.length).to.equal(3);
|
||||
|
@ -278,13 +282,13 @@ describe("Network", function () {
|
|||
|
||||
const network = new Network({
|
||||
channels: [chan1, chan2, chan3],
|
||||
name: "freenode",
|
||||
name: "foo",
|
||||
});
|
||||
|
||||
const newChan = new Chan({name: "#freenode"});
|
||||
const newChan = new Chan({name: "#foo"});
|
||||
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[2]).to.equal(newChan);
|
||||
expect(network.channels[3]).to.equal(chan2);
|
||||
|
@ -299,7 +303,7 @@ describe("Network", function () {
|
|||
channels: [chan1, chan2],
|
||||
});
|
||||
|
||||
const newChan = new Chan({name: "#freenode"});
|
||||
const newChan = new Chan({name: "#foo"});
|
||||
network.addChannel(newChan);
|
||||
|
||||
expect(network.channels[1]).to.equal(chan1);
|
||||
|
@ -393,7 +397,7 @@ describe("Network", function () {
|
|||
channels: [banlist, chan1, user1],
|
||||
});
|
||||
|
||||
const newChan = new Chan({name: "#freenode"});
|
||||
const newChan = new Chan({name: "#foo"});
|
||||
network.addChannel(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 () {
|
||||
const network = new Network({
|
||||
name: "freenode",
|
||||
name: "foo",
|
||||
channels: [],
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue