Merge remote-tracking branch 'origin/master' into richrd/message-search
This commit is contained in:
commit
283ef445e5
@ -55,9 +55,16 @@ rules:
|
||||
spaced-comment: [error, always]
|
||||
strict: off
|
||||
yoda: error
|
||||
vue/require-default-prop: off
|
||||
vue/component-tags-order:
|
||||
- error
|
||||
- order:
|
||||
- template
|
||||
- style
|
||||
- script
|
||||
vue/no-mutating-props: off
|
||||
vue/no-v-html: off
|
||||
vue/no-use-v-if-with-v-for: off
|
||||
vue/require-default-prop: off
|
||||
vue/v-slot-style: [error, longform]
|
||||
|
||||
plugins:
|
||||
- vue
|
||||
|
16
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
16
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
contact_links:
|
||||
- name: Docker container issues
|
||||
url: https://github.com/thelounge/thelounge-docker/issues
|
||||
about: Report issues related to the Docker container here
|
||||
|
||||
- name: Debian package issues
|
||||
url: https://github.com/thelounge/thelounge-deb/issues
|
||||
about: Report issues related to the Debian package here
|
||||
|
||||
- name: Arch Linux package issues
|
||||
url: https://github.com/thelounge/thelounge-archlinux/issues
|
||||
about: Report issues related to the Arch Linux package here
|
||||
|
||||
- name: General support
|
||||
url: https://demo.thelounge.chat/?join=%23thelounge
|
||||
about: "Join #thelounge on Freenode to ask a question before creating an issue"
|
28
.github/workflows/build.yml
vendored
28
.github/workflows/build.yml
vendored
@ -8,22 +8,34 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
node_version: [
|
||||
10.x, # EOL: April 2021
|
||||
12.x, # EOL: April 2022
|
||||
]
|
||||
exclude:
|
||||
- os: macOS-latest
|
||||
include:
|
||||
# EOL: April 2021
|
||||
- os: ubuntu-latest
|
||||
node_version: 10.x
|
||||
|
||||
# EOL: April 2022
|
||||
- os: ubuntu-latest
|
||||
node_version: 12.x
|
||||
|
||||
# EOL: April 2023
|
||||
- os: ubuntu-latest
|
||||
node_version: 14.x
|
||||
- os: macOS-latest
|
||||
node_version: 14.x
|
||||
- os: windows-latest
|
||||
node_version: 14.x
|
||||
|
||||
# EOL: June 2021
|
||||
- os: ubuntu-latest
|
||||
node_version: 15.x
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node_version }}
|
||||
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
registry-url: "https://registry.npmjs.org/"
|
||||
|
||||
|
@ -1,8 +1,5 @@
|
||||
extends: stylelint-config-standard
|
||||
|
||||
ignoreFiles:
|
||||
- client/css/bootstrap.css
|
||||
|
||||
rules:
|
||||
indentation: tab
|
||||
# complains about FontAwesome
|
||||
|
9
.vscode/extensions.json
vendored
Normal file
9
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"octref.vetur"
|
||||
],
|
||||
"unwantedRecommendations": []
|
||||
}
|
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"name": "Run Dev",
|
||||
"request": "launch",
|
||||
"command": "yarn dev",
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"vetur.format.enable": false,
|
||||
"prettier.useEditorConfig": true,
|
||||
"prettier.requireConfig": true,
|
||||
"prettier.disableLanguages": [],
|
||||
"prettier.packageManager": "yarn",
|
||||
"eslint.packageManager": "yarn",
|
||||
"eslint.codeActionsOnSave.mode": "all"
|
||||
}
|
142
CHANGELOG.md
142
CHANGELOG.md
@ -4,6 +4,148 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
<!-- New entries go after this line -->
|
||||
|
||||
## 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)
|
||||
|
||||
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.2.0 - 2020-08-19
|
||||
|
||||
For more details, [see the full changelog](https://github.com/thelounge/thelounge/compare/v4.1.0...v4.2.0) and [milestone](https://github.com/thelounge/thelounge/milestone/36?closed=1).
|
||||
|
||||
This is a minor release with one significant new feature: a mentions panel!
|
||||
|
||||
<p align="center">
|
||||
<img width="466" alt="Mentions panel" src="https://user-images.githubusercontent.com/613331/90796491-0fadf380-e318-11ea-8fda-51613a9a3221.png">
|
||||
</p>
|
||||
|
||||
Other notable additions include custom highlight exceptions, a new configuration option to not send preview requests to 3rd party websites, and uploaded images will have [EXIF](https://en.wikipedia.org/wiki/Exif) data automatically removed.
|
||||
|
||||
There's also a new section for configuring SASL on the Connect screen, and `SASL EXTERNAL` is now supported.
|
||||
|
||||
<p align="center">
|
||||
<img width="489" alt="SASL authentication" src="https://user-images.githubusercontent.com/613331/90796501-15a3d480-e318-11ea-9dab-c225816a6685.png">
|
||||
<img width="474" alt="SASL external (certfp)" src="https://user-images.githubusercontent.com/613331/90796504-15a3d480-e318-11ea-9636-c1025c9d2306.png">
|
||||
</p>
|
||||
|
||||
Along with other bugs, a Chrome bug causing lag when typing has been fixed. Additionally, the `node-sqlite3` dependency has been updated, and you no longer need to re-install The Lounge when you update Node.js.
|
||||
|
||||
And as an update for our Docker users, `thelounge-docker` now has support for ARM images; thanks [@williamboman](https://github.com/williamboman) and [@klausenbusk](https://github.com/klausenbusk)!
|
||||
|
||||
### Added
|
||||
|
||||
- Track mentions/highlights and add a window to view them ([#3858](https://github.com/thelounge/thelounge/pull/3858), [#3993](https://github.com/thelounge/thelounge/pull/3993), [#3862](https://github.com/thelounge/thelounge/pull/3862), [#3868](https://github.com/thelounge/thelounge/pull/3868), [#4003](https://github.com/thelounge/thelounge/pull/4003) by [@xPaw](https://github.com/xPaw))
|
||||
- Add an option to display 12-hour times ([#3787](https://github.com/thelounge/thelounge/pull/3787) by [@xPaw](https://github.com/xPaw))
|
||||
- Add clear channel history (available in channel context menu)([#3778](https://github.com/thelounge/thelounge/pull/3778) by [@xPaw](https://github.com/xPaw))
|
||||
- Add CertFP support; separate SASL configuration; merge `displayNetwork` and `lockNetwork` in The Lounge configuration file ([#3844](https://github.com/thelounge/thelounge/pull/3844) by [@xPaw](https://github.com/xPaw))
|
||||
- Add an indicator to `STATUSMSG` messages ([#3875](https://github.com/thelounge/thelounge/pull/3875) by [@xPaw](https://github.com/xPaw))
|
||||
- Add native app badges for highlights (Chrome 81+) ([#3845](https://github.com/thelounge/thelounge/pull/3845) by [@xPaw](https://github.com/xPaw))
|
||||
- Add generic monospace blocks for `INFO` and `HELP` numerics ([#3962](https://github.com/thelounge/thelounge/pull/3962) by [@xPaw](https://github.com/xPaw), [#4032](https://github.com/thelounge/thelounge/pull/4032) by [@xPaw](https://github.com/xPaw))
|
||||
- Add option to disable media preview ([#3983](https://github.com/thelounge/thelounge/pull/3983) by [@dalcde](https://github.com/dalcde))
|
||||
- Add custom highlight exceptions ([#3998](https://github.com/thelounge/thelounge/pull/3998) by [@Jay2k1](https://github.com/Jay2k1))
|
||||
- Add navigation in image viewer ([#3798](https://github.com/thelounge/thelounge/pull/3798) by [@richrd](https://github.com/richrd))
|
||||
- Render images in canvas before upload to remove EXIF data ([#3764](https://github.com/thelounge/thelounge/pull/3764) by [@xPaw](https://github.com/xPaw))
|
||||
|
||||
### Changed
|
||||
|
||||
- Disable link prefetching for urls with no schema specified ([#4014](https://github.com/thelounge/thelounge/pull/4014) by [@xPaw](https://github.com/xPaw))
|
||||
- Disable settings sync for browser notifications and notification sound ([#4028](https://github.com/thelounge/thelounge/pull/4028) by [@xPaw](https://github.com/xPaw))
|
||||
- Make usernames case-insensitive when logging in ([#3918](https://github.com/thelounge/thelounge/pull/3918) by [@ashwinikammar](https://github.com/ashwinikammar))
|
||||
- Separate active sessions section ([#3817](https://github.com/thelounge/thelounge/pull/3817) by [@xPaw](https://github.com/xPaw))
|
||||
- Add `role=group` to status messages setting ([#3790](https://github.com/thelounge/thelounge/pull/3790) by [@xPaw](https://github.com/xPaw))
|
||||
- Filter user loading at startup for "advanced" LDAP ([#3871](https://github.com/thelounge/thelounge/pull/3871) by [@ebardie](https://github.com/ebardie))
|
||||
- Reconnects now use exponential backoff
|
||||
- Update production dependencies to their latest versions:
|
||||
- `uuid` ([#3791](https://github.com/thelounge/thelounge/pull/3791), [#3837](https://github.com/thelounge/thelounge/pull/3837), [#3890](https://github.com/thelounge/thelounge/pull/3890), [#3919](https://github.com/thelounge/thelounge/pull/3919), [#3957](https://github.com/thelounge/thelounge/pull/3957), [#4004](https://github.com/thelounge/thelounge/pull/4004))
|
||||
- `yarn` ([#3792](https://github.com/thelounge/thelounge/pull/3792), [#3800](https://github.com/thelounge/thelounge/pull/3800))
|
||||
- `file-type` ([#3801](https://github.com/thelounge/thelounge/pull/3801), [#3896](https://github.com/thelounge/thelounge/pull/3896), [#3909](https://github.com/thelounge/thelounge/pull/3909), [#3920](https://github.com/thelounge/thelounge/pull/3920), [#3934](https://github.com/thelounge/thelounge/pull/3934), [#3940](https://github.com/thelounge/thelounge/pull/3940))
|
||||
- `commander` ([#3807](https://github.com/thelounge/thelounge/pull/3807), [#3992](https://github.com/thelounge/thelounge/pull/3992))
|
||||
- `got` ([#3829](https://github.com/thelounge/thelounge/pull/3829), [#3869](https://github.com/thelounge/thelounge/pull/3869), [#3898](https://github.com/thelounge/thelounge/pull/3898), [#3905](https://github.com/thelounge/thelounge/pull/3905), [#3932](https://github.com/thelounge/thelounge/pull/3932), [#3935](https://github.com/thelounge/thelounge/pull/3935), [#3972](https://github.com/thelounge/thelounge/pull/3972), [#3988](https://github.com/thelounge/thelounge/pull/3988))
|
||||
- `irc-framework` ([#3838](https://github.com/thelounge/thelounge/pull/3838), [#3984](https://github.com/thelounge/thelounge/pull/3984))
|
||||
- `chalk` ([#3839](https://github.com/thelounge/thelounge/pull/3839))
|
||||
- `semver` ([#3843](https://github.com/thelounge/thelounge/pull/3843), [#3863](https://github.com/thelounge/thelounge/pull/3863))
|
||||
- `web-push` ([#3904](https://github.com/thelounge/thelounge/pull/3904))
|
||||
- `linkify-it` ([#3917](https://github.com/thelounge/thelounge/pull/3917))
|
||||
- `sqlite3` ([#3886](https://github.com/thelounge/thelounge/pull/3886))
|
||||
- `ldapjs` ([#3931](https://github.com/thelounge/thelounge/pull/3931), [#3996](https://github.com/thelounge/thelounge/pull/3996))
|
||||
- `tlds` ([#4015](https://github.com/thelounge/thelounge/pull/4015))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix sending unhandled numerics to target channel ([#3789](https://github.com/thelounge/thelounge/pull/3789) by [@xPaw](https://github.com/xPaw))
|
||||
- Fix up first argument not being used as part message ([#3808](https://github.com/thelounge/thelounge/pull/3808) by [@xPaw](https://github.com/xPaw))
|
||||
- Pass in client manager object in update checker ([#3797](https://github.com/thelounge/thelounge/pull/3797) by [@xPaw](https://github.com/xPaw))
|
||||
- Do not handle navigation keybinds in inputs if not empty ([#3814](https://github.com/thelounge/thelounge/pull/3814) by [@xPaw](https://github.com/xPaw))
|
||||
- Fix body overscroll and overflow on iOS Safari ([#3828](https://github.com/thelounge/thelounge/pull/3828) by [@stevenengler](https://github.com/stevenengler))
|
||||
- Fix off-by-one color error in webmanifest ([#3867](https://github.com/thelounge/thelounge/pull/3867) by [@maxpoulin64](https://github.com/maxpoulin64))
|
||||
- Support multiple arguments in eventbus emit ([#3885](https://github.com/thelounge/thelounge/pull/3885) by [@xPaw](https://github.com/xPaw))
|
||||
- Fix msg id order when loading from sqlite ([#3888](https://github.com/thelounge/thelounge/pull/3888) by [@xPaw](https://github.com/xPaw))
|
||||
- Reply to the server if that's where CTCP VERSION originated ([#3906](https://github.com/thelounge/thelounge/pull/3906) by [@xPaw](https://github.com/xPaw))
|
||||
- Fix date marker not displaying sometimes ([#3978](https://github.com/thelounge/thelounge/pull/3978) by [@xPaw](https://github.com/xPaw))
|
||||
- Allow changing network name in private mode with lockNetwork ([#3977](https://github.com/thelounge/thelounge/pull/3977) by [@xPaw](https://github.com/xPaw))
|
||||
- Fix upload tokens expiring while uploading when TL is proxied ([#3986](https://github.com/thelounge/thelounge/pull/3986) by [@xPaw](https://github.com/xPaw))
|
||||
- Refresh notification permission state when push is enabled ([#3987](https://github.com/thelounge/thelounge/pull/3987) by [@xPaw](https://github.com/xPaw))
|
||||
- Fix mode message only making last nick clickable ([#4005](https://github.com/thelounge/thelounge/pull/4005) by [@xPaw](https://github.com/xPaw))
|
||||
- Sync changed network name to open clients ([#4038](https://github.com/thelounge/thelounge/pull/4038) by [@xPaw](https://github.com/xPaw))
|
||||
- Fix layout trashing in Chrome causing typing lag ([#3999](https://github.com/thelounge/thelounge/pull/3999) by [@xPaw](https://github.com/xPaw))
|
||||
- Fixed a rare bug in `irc-framework` that caused duplicate messages
|
||||
|
||||
### Internals
|
||||
|
||||
- Optimize user list updates for quit/part/kick events ([#3857](https://github.com/thelounge/thelounge/pull/3857) by [@xPaw](https://github.com/xPaw))
|
||||
- Remove "The Lounge" from connect in public ([#3816](https://github.com/thelounge/thelounge/pull/3816) by [@xPaw](https://github.com/xPaw))
|
||||
- Replace all uses of `fs-extra` with native methods ([#3810](https://github.com/thelounge/thelounge/pull/3810) by [@xPaw](https://github.com/xPaw))
|
||||
- Upgrade to `mocha@7` and remove `mochapack` ([#3826](https://github.com/thelounge/thelounge/pull/3826) by [@xPaw](https://github.com/xPaw))
|
||||
- Remove `intersection-observer` polyfill ([#3864](https://github.com/thelounge/thelounge/pull/3864) by [@xPaw](https://github.com/xPaw))
|
||||
- Safeguard nick randomizer up to allowed length ([#3870](https://github.com/thelounge/thelounge/pull/3870) by [@xPaw](https://github.com/xPaw))
|
||||
- Replace vue events with our own event bus ([#3872](https://github.com/thelounge/thelounge/pull/3872) by [@xPaw](https://github.com/xPaw))
|
||||
- Cleanup vue router route guards ([#3995](https://github.com/thelounge/thelounge/pull/3995) by [@xPaw](https://github.com/xPaw))
|
||||
- Use lodash where possible ([#4020](https://github.com/thelounge/thelounge/pull/4020) by [@xPaw](https://github.com/xPaw))
|
||||
- Replace dashes to underscores in emoji autocompletion ([#4029](https://github.com/thelounge/thelounge/pull/4029) by [@xPaw](https://github.com/xPaw))
|
||||
- Changes required for vue 3 ([#3889](https://github.com/thelounge/thelounge/pull/3889) by [@timmw](https://github.com/timmw))
|
||||
- Test node v14 ([#3976](https://github.com/thelounge/thelounge/pull/3976) by [@xPaw](https://github.com/xPaw))
|
||||
- Update development dependencies to their latest versions.
|
||||
|
||||
## v4.2.0-pre.2 - 2020-07-28 [Pre-release]
|
||||
|
||||
[See the full changelog](https://github.com/thelounge/thelounge/compare/v4.2.0-pre.1...v4.2.0-pre.2)
|
||||
|
||||
This is a pre-release for v4.2.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.2.0-pre.1 - 2020-05-17 [Pre-release]
|
||||
|
||||
[See the full changelog](https://github.com/thelounge/thelounge/compare/v4.1.0...v4.2.0-pre.1)
|
||||
|
||||
This is a pre-release for v4.2.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.1.0 - 2020-03-09
|
||||
|
||||
For more details, [see the full changelog](https://github.com/thelounge/thelounge/compare/v4.0.0...v4.1.0) and [milestone](https://github.com/thelounge/thelounge/milestone/35?closed=1).
|
||||
|
@ -85,5 +85,5 @@ Before submitting any change, make sure to:
|
||||
|
||||
- Read the [Contributing instructions](https://github.com/thelounge/thelounge/blob/master/.github/CONTRIBUTING.md#contributing)
|
||||
- Run `yarn test` to execute linters and test suite
|
||||
- Run `yarn build` if you change or add anything in `client/js` or `client/views`
|
||||
- Run `yarn build` if you change or add anything in `client/js` or `client/components`
|
||||
- `yarn dev` can be used to start The Lounge with hot module reloading
|
||||
|
@ -13,6 +13,7 @@
|
||||
|
||||
<script>
|
||||
const constants = require("../js/constants");
|
||||
import eventbus from "../js/eventbus";
|
||||
import Mousetrap from "mousetrap";
|
||||
import throttle from "lodash/throttle";
|
||||
import storage from "../js/localStorage";
|
||||
@ -53,14 +54,14 @@ export default {
|
||||
|
||||
// Make a single throttled resize listener available to all components
|
||||
this.debouncedResize = throttle(() => {
|
||||
this.$root.$emit("resize");
|
||||
eventbus.emit("resize");
|
||||
}, 100);
|
||||
|
||||
window.addEventListener("resize", this.debouncedResize, {passive: true});
|
||||
|
||||
// Emit a daychange event every time the day changes so date markers know when to update themselves
|
||||
const emitDayChange = () => {
|
||||
this.$root.$emit("daychange");
|
||||
eventbus.emit("daychange");
|
||||
// This should always be 24h later but re-computing exact value just in case
|
||||
this.dayChangeTimeout = setTimeout(emitDayChange, this.msUntilNextDay());
|
||||
};
|
||||
@ -77,7 +78,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
escapeKey() {
|
||||
this.$root.$emit("escapekey");
|
||||
eventbus.emit("escapekey");
|
||||
},
|
||||
toggleSidebar(e) {
|
||||
if (isIgnoredKeybind(e)) {
|
||||
|
@ -30,6 +30,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventbus from "../js/eventbus";
|
||||
import isChannelCollapsed from "../js/helpers/isChannelCollapsed";
|
||||
|
||||
export default {
|
||||
@ -74,7 +75,7 @@ export default {
|
||||
this.$root.switchToChannel(this.channel);
|
||||
},
|
||||
openContextMenu(event) {
|
||||
this.$root.$emit("contextmenu:channel", {
|
||||
eventbus.emit("contextmenu:channel", {
|
||||
event: event,
|
||||
channel: this.channel,
|
||||
network: this.network,
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div id="chat-container" class="window" :data-current-channel="channel.name">
|
||||
<div id="chat-container" class="window" :data-current-channel="channel.name" lang="">
|
||||
<div
|
||||
id="chat"
|
||||
:class="{
|
||||
@ -25,6 +25,7 @@
|
||||
:value="channel.topic"
|
||||
class="topic-input"
|
||||
placeholder="Set channel topic"
|
||||
enterkeyhint="done"
|
||||
@keyup.enter="saveTopic"
|
||||
@keyup.esc="channel.editTopic = false"
|
||||
/>
|
||||
@ -69,7 +70,7 @@
|
||||
<div class="chat">
|
||||
<div class="messages">
|
||||
<div class="msg">
|
||||
<Component
|
||||
<component
|
||||
:is="specialComponent"
|
||||
:network="network"
|
||||
:channel="channel"
|
||||
@ -107,6 +108,7 @@
|
||||
|
||||
<script>
|
||||
import socket from "../js/socket";
|
||||
import eventbus from "../js/eventbus";
|
||||
import ParsedMessage from "./ParsedMessage.vue";
|
||||
import MessageList from "./MessageList.vue";
|
||||
import ChatInput from "./ChatInput.vue";
|
||||
@ -204,14 +206,14 @@ export default {
|
||||
}
|
||||
},
|
||||
openContextMenu(event) {
|
||||
this.$root.$emit("contextmenu:channel", {
|
||||
eventbus.emit("contextmenu:channel", {
|
||||
event: event,
|
||||
channel: this.channel,
|
||||
network: this.network,
|
||||
});
|
||||
},
|
||||
openMentions() {
|
||||
this.$root.$emit("mentions:toggle", {
|
||||
eventbus.emit("mentions:toggle", {
|
||||
event: event,
|
||||
});
|
||||
},
|
||||
|
@ -7,6 +7,7 @@
|
||||
ref="input"
|
||||
dir="auto"
|
||||
class="mousetrap"
|
||||
enterkeyhint="send"
|
||||
:value="channel.pendingMessage"
|
||||
:placeholder="getInputPlaceholder(channel)"
|
||||
:aria-label="getInputPlaceholder(channel)"
|
||||
@ -24,6 +25,7 @@
|
||||
id="upload-input"
|
||||
ref="uploadInput"
|
||||
type="file"
|
||||
aria-labelledby="upload"
|
||||
multiple
|
||||
@change="onUploadInputChange"
|
||||
/>
|
||||
@ -56,6 +58,7 @@ import autocompletion from "../js/autocompletion";
|
||||
import commands from "../js/commands/index";
|
||||
import socket from "../js/socket";
|
||||
import upload from "../js/upload";
|
||||
import eventbus from "../js/eventbus";
|
||||
|
||||
const formattingHotkeys = {
|
||||
"mod+k": "\x03",
|
||||
@ -101,7 +104,7 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on("escapekey", this.blurInput);
|
||||
eventbus.on("escapekey", this.blurInput);
|
||||
|
||||
if (this.$store.state.settings.autocomplete) {
|
||||
autocompletionRef = autocompletion(this.$refs.input);
|
||||
@ -163,7 +166,7 @@ export default {
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.$root.$off("escapekey", this.blurInput);
|
||||
eventbus.off("escapekey", this.blurInput);
|
||||
|
||||
if (autocompletionRef) {
|
||||
autocompletionRef.destroy();
|
||||
|
@ -32,7 +32,7 @@
|
||||
:on-hover="hoverUser"
|
||||
:active="user.original === activeUser"
|
||||
:user="user.original"
|
||||
v-html="user.original.mode + user.string"
|
||||
v-html="user.string"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
@ -98,18 +98,25 @@ export default {
|
||||
const result = this.filteredUsers;
|
||||
|
||||
for (const user of result) {
|
||||
if (!groups[user.original.mode]) {
|
||||
groups[user.original.mode] = [];
|
||||
const mode = user.original.modes[0] || "";
|
||||
|
||||
if (!groups[mode]) {
|
||||
groups[mode] = [];
|
||||
}
|
||||
|
||||
groups[user.original.mode].push(user);
|
||||
// Prepend user mode to search result
|
||||
user.string = mode + user.string;
|
||||
|
||||
groups[mode].push(user);
|
||||
}
|
||||
} else {
|
||||
for (const user of this.channel.users) {
|
||||
if (!groups[user.mode]) {
|
||||
groups[user.mode] = [user];
|
||||
const mode = user.modes[0] || "";
|
||||
|
||||
if (!groups[mode]) {
|
||||
groups[mode] = [user];
|
||||
} else {
|
||||
groups[user.mode].push(user);
|
||||
groups[mode].push(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,8 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import eventbus from "../js/eventbus";
|
||||
|
||||
export default {
|
||||
name: "ConfirmDialog",
|
||||
data() {
|
||||
@ -60,12 +62,12 @@ export default {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on("escapekey", this.close);
|
||||
this.$root.$on("confirm-dialog", this.open);
|
||||
eventbus.on("escapekey", this.close);
|
||||
eventbus.on("confirm-dialog", this.open);
|
||||
},
|
||||
destroyed() {
|
||||
this.$root.$off("escapekey", this.close);
|
||||
this.$root.$off("confirm-dialog", this.open);
|
||||
eventbus.off("escapekey", this.close);
|
||||
eventbus.off("confirm-dialog", this.open);
|
||||
},
|
||||
methods: {
|
||||
open(data, callback) {
|
||||
|
@ -39,6 +39,7 @@
|
||||
|
||||
<script>
|
||||
import {generateUserContextMenu, generateChannelContextMenu} from "../js/helpers/contextMenu.js";
|
||||
import eventbus from "../js/eventbus";
|
||||
|
||||
export default {
|
||||
name: "ContextMenu",
|
||||
@ -58,14 +59,14 @@ export default {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on("escapekey", this.close);
|
||||
this.$root.$on("contextmenu:user", this.openUserContextMenu);
|
||||
this.$root.$on("contextmenu:channel", this.openChannelContextMenu);
|
||||
eventbus.on("escapekey", this.close);
|
||||
eventbus.on("contextmenu:user", this.openUserContextMenu);
|
||||
eventbus.on("contextmenu:channel", this.openChannelContextMenu);
|
||||
},
|
||||
destroyed() {
|
||||
this.$root.$off("escapekey", this.close);
|
||||
this.$root.$off("contextmenu:user", this.openUserContextMenu);
|
||||
this.$root.$off("contextmenu:channel", this.openChannelContextMenu);
|
||||
eventbus.off("escapekey", this.close);
|
||||
eventbus.off("contextmenu:user", this.openUserContextMenu);
|
||||
eventbus.off("contextmenu:channel", this.openChannelContextMenu);
|
||||
|
||||
this.close();
|
||||
},
|
||||
@ -75,19 +76,17 @@ export default {
|
||||
this.open(data.event, items);
|
||||
},
|
||||
openUserContextMenu(data) {
|
||||
const activeChannel = this.$store.state.activeChannel;
|
||||
// If there's an active network and channel use them
|
||||
let {network, channel} = activeChannel ? activeChannel : {network: null, channel: null};
|
||||
const {network, channel} = this.$store.state.activeChannel;
|
||||
|
||||
// Use network and channel from event if specified
|
||||
network = data.network ? data.network : network;
|
||||
channel = data.channel ? data.channel : channel;
|
||||
|
||||
const defaultUser = {nick: data.user.nick};
|
||||
let user = channel ? channel.users.find((u) => u.nick === data.user.nick) : defaultUser;
|
||||
user = user ? user : defaultUser;
|
||||
|
||||
const items = generateUserContextMenu(this.$root, channel, network, user);
|
||||
const items = generateUserContextMenu(
|
||||
this.$root,
|
||||
channel,
|
||||
network,
|
||||
channel.users.find((u) => u.nick === data.user.nick) || {
|
||||
nick: data.user.nick,
|
||||
modes: [],
|
||||
}
|
||||
);
|
||||
this.open(data.event, items);
|
||||
},
|
||||
open(event, items) {
|
||||
|
@ -9,6 +9,7 @@
|
||||
<script>
|
||||
import dayjs from "dayjs";
|
||||
import calendar from "dayjs/plugin/calendar";
|
||||
import eventbus from "../js/eventbus";
|
||||
|
||||
dayjs.extend(calendar);
|
||||
|
||||
@ -24,11 +25,11 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
if (this.hoursPassed() < 48) {
|
||||
this.$root.$on("daychange", this.dayChange);
|
||||
eventbus.on("daychange", this.dayChange);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$root.$off("daychange", this.dayChange);
|
||||
eventbus.off("daychange", this.dayChange);
|
||||
},
|
||||
methods: {
|
||||
hoursPassed() {
|
||||
@ -38,7 +39,7 @@ export default {
|
||||
this.$forceUpdate();
|
||||
|
||||
if (this.hoursPassed() >= 48) {
|
||||
this.$root.$off("daychange", this.dayChange);
|
||||
eventbus.off("daychange", this.dayChange);
|
||||
}
|
||||
},
|
||||
friendlyDate() {
|
||||
|
@ -40,6 +40,7 @@
|
||||
|
||||
<script>
|
||||
import Mousetrap from "mousetrap";
|
||||
import eventbus from "../js/eventbus";
|
||||
|
||||
export default {
|
||||
name: "ImageViewer",
|
||||
@ -79,8 +80,8 @@ export default {
|
||||
link(newLink, oldLink) {
|
||||
// TODO: history.pushState
|
||||
if (newLink === null) {
|
||||
this.$root.$off("escapekey", this.closeViewer);
|
||||
this.$root.$off("resize", this.correctPosition);
|
||||
eventbus.off("escapekey", this.closeViewer);
|
||||
eventbus.off("resize", this.correctPosition);
|
||||
Mousetrap.unbind("left", this.previous);
|
||||
Mousetrap.unbind("right", this.next);
|
||||
return;
|
||||
@ -89,8 +90,8 @@ export default {
|
||||
this.setPrevNextImages();
|
||||
|
||||
if (!oldLink) {
|
||||
this.$root.$on("escapekey", this.closeViewer);
|
||||
this.$root.$on("resize", this.correctPosition);
|
||||
eventbus.on("escapekey", this.closeViewer);
|
||||
eventbus.on("resize", this.correctPosition);
|
||||
Mousetrap.bind("left", this.previous);
|
||||
Mousetrap.bind("right", this.next);
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
method="post"
|
||||
action=""
|
||||
autocomplete="off"
|
||||
@keydown.esc.prevent="$emit('toggleJoinChannel')"
|
||||
@keydown.esc.prevent="$emit('toggle-join-channel')"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<input
|
||||
@ -81,7 +81,7 @@ export default {
|
||||
|
||||
this.inputChannel = "";
|
||||
this.inputPassword = "";
|
||||
this.$emit("toggleJoinChannel");
|
||||
this.$emit("toggle-join-channel");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -130,6 +130,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventbus from "../js/eventbus";
|
||||
import friendlysize from "../js/helpers/friendlysize";
|
||||
|
||||
export default {
|
||||
@ -167,12 +168,12 @@ export default {
|
||||
this.updateShownState();
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on("resize", this.handleResize);
|
||||
eventbus.on("resize", this.handleResize);
|
||||
|
||||
this.onPreviewUpdate();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$root.$off("resize", this.handleResize);
|
||||
eventbus.off("resize", this.handleResize);
|
||||
},
|
||||
destroyed() {
|
||||
// Let this preview go through load/canplay events again,
|
||||
|
@ -22,7 +22,7 @@ export default {
|
||||
onClick() {
|
||||
this.link.shown = !this.link.shown;
|
||||
|
||||
this.$parent.$emit("linkPreviewToggle", this.link, this.$parent.message);
|
||||
this.$parent.$emit("toggle-link-preview", this.link, this.$parent.message);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -3,36 +3,49 @@
|
||||
v-if="isOpen"
|
||||
id="mentions-popup-container"
|
||||
@click="containerClick"
|
||||
@contextmenu.prevent="containerClick"
|
||||
@contextmenu="containerClick"
|
||||
>
|
||||
<div class="mentions-popup">
|
||||
<div class="mentions-popup-title">
|
||||
Recent mentions
|
||||
<button
|
||||
v-if="resolvedMessages.length"
|
||||
class="btn hide-all-mentions"
|
||||
@click="hideAllMentions()"
|
||||
>
|
||||
Hide all
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="resolvedMessages.length === 0">
|
||||
<p v-if="isLoading">Loading…</p>
|
||||
<p v-else>There are no recent mentions.</p>
|
||||
<p v-else>You have no recent mentions.</p>
|
||||
</template>
|
||||
<template v-for="message in resolvedMessages" v-else>
|
||||
<div :key="message.id" :class="['msg', message.type]">
|
||||
<div :key="message.msgId" :class="['msg', message.type]">
|
||||
<div class="mentions-info">
|
||||
<div>
|
||||
<span class="from">
|
||||
<Username :user="message.from" />
|
||||
<template v-if="message.channel">
|
||||
in {{ message.channel.channel.name }} on
|
||||
{{ message.channel.network.name }}
|
||||
</template>
|
||||
<template v-else>
|
||||
in unknown channel
|
||||
</template>
|
||||
<template v-else> in unknown channel </template>
|
||||
</span>
|
||||
<span :title="message.time | localetime" class="time">
|
||||
<span :title="message.localetime" class="time">
|
||||
{{ messageTime(message.time) }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="close-tooltip tooltipped tooltipped-w" aria-label="Close">
|
||||
<button
|
||||
class="msg-hide"
|
||||
aria-label="Hide this mention"
|
||||
@click="hideMention(message)"
|
||||
></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" dir="auto">
|
||||
<ParsedMessage :network="null" :message="message" />
|
||||
</div>
|
||||
@ -54,16 +67,23 @@
|
||||
right: 80px;
|
||||
top: 55px;
|
||||
max-height: 400px;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
z-index: 2;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mentions-popup > .mentions-popup-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.mentions-popup .mentions-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mentions-popup .msg {
|
||||
margin-bottom: 15px;
|
||||
user-select: text;
|
||||
@ -78,6 +98,8 @@
|
||||
border-radius: 5px;
|
||||
padding: 6px;
|
||||
margin-top: 2px;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word; /* Webkit-specific */
|
||||
}
|
||||
|
||||
.mentions-popup .msg-hide::before {
|
||||
@ -89,6 +111,21 @@
|
||||
content: "×";
|
||||
}
|
||||
|
||||
.mentions-popup .msg-hide:hover {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
.mentions-popup .hide-all-mentions {
|
||||
margin: 0;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
@media (min-height: 500px) {
|
||||
.mentions-popup {
|
||||
max-height: 60vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mentions-popup {
|
||||
border-radius: 0;
|
||||
@ -108,6 +145,8 @@
|
||||
import Username from "./Username.vue";
|
||||
import ParsedMessage from "./ParsedMessage.vue";
|
||||
import socket from "../js/socket";
|
||||
import eventbus from "../js/eventbus";
|
||||
import localetime from "../js/helpers/localetime";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
@ -130,6 +169,7 @@ export default {
|
||||
const messages = this.$store.state.mentions.slice().reverse();
|
||||
|
||||
for (const message of messages) {
|
||||
message.localetime = localetime(message.time);
|
||||
message.channel = this.$store.getters.findChannel(message.chanId);
|
||||
}
|
||||
|
||||
@ -142,10 +182,10 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on("mentions:toggle", this.openPopup);
|
||||
eventbus.on("mentions:toggle", this.openPopup);
|
||||
},
|
||||
destroyed() {
|
||||
this.$root.$off("mentions:toggle", this.openPopup);
|
||||
eventbus.off("mentions:toggle", this.openPopup);
|
||||
},
|
||||
methods: {
|
||||
messageTime(time) {
|
||||
@ -159,6 +199,10 @@ export default {
|
||||
|
||||
socket.emit("mentions:hide", message.msgId);
|
||||
},
|
||||
hideAllMentions() {
|
||||
this.$store.state.mentions = [];
|
||||
socket.emit("mentions:hide_all");
|
||||
},
|
||||
containerClick(event) {
|
||||
if (event.currentTarget === event.target) {
|
||||
this.isOpen = false;
|
||||
|
@ -6,6 +6,7 @@
|
||||
{self: message.self, highlight: message.highlight, 'previous-source': isPreviousSource},
|
||||
]"
|
||||
:data-type="message.type"
|
||||
:data-command="message.command"
|
||||
:data-from="message.from && message.from.nick"
|
||||
>
|
||||
<span :aria-label="messageTimeLocale" class="time tooltipped tooltipped-e"
|
||||
@ -19,7 +20,7 @@
|
||||
</template>
|
||||
<template v-else-if="isAction()">
|
||||
<span class="from"><span class="only-copy">*** </span></span>
|
||||
<Component :is="messageComponent" :network="network" :message="message" />
|
||||
<component :is="messageComponent" :network="network" :message="message" />
|
||||
</template>
|
||||
<template v-else-if="message.type === 'action'">
|
||||
<span class="from"><span class="only-copy">* </span></span>
|
||||
@ -68,6 +69,12 @@
|
||||
class="msg-shown-in-active tooltipped tooltipped-e"
|
||||
><span></span
|
||||
></span>
|
||||
<span
|
||||
v-if="message.statusmsgGroup"
|
||||
:aria-label="`This message was only shown to users with ${message.statusmsgGroup} mode`"
|
||||
class="msg-statusmsg tooltipped tooltipped-e"
|
||||
><span>{{ message.statusmsgGroup }}</span></span
|
||||
>
|
||||
<ParsedMessage :network="network" :message="message" />
|
||||
<LinkPreview
|
||||
v-for="preview in message.previews"
|
||||
|
@ -47,7 +47,7 @@
|
||||
:message="message"
|
||||
:keep-scroll-position="keepScrollPosition"
|
||||
:is-previous-source="isPreviousSource(message, id)"
|
||||
@linkPreviewToggle="onLinkPreviewToggle"
|
||||
@toggle-link-preview="onLinkPreviewToggle"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
@ -56,12 +56,15 @@
|
||||
|
||||
<script>
|
||||
const constants = require("../js/constants");
|
||||
import eventbus from "../js/eventbus";
|
||||
import clipboard from "../js/clipboard";
|
||||
import socket from "../js/socket";
|
||||
import Message from "./Message.vue";
|
||||
import MessageCondensed from "./MessageCondensed.vue";
|
||||
import DateMarker from "./DateMarker.vue";
|
||||
|
||||
let unreadMarkerShown = false;
|
||||
|
||||
export default {
|
||||
name: "MessageList",
|
||||
components: {
|
||||
@ -173,7 +176,7 @@ export default {
|
||||
mounted() {
|
||||
this.$refs.chat.addEventListener("scroll", this.handleScroll, {passive: true});
|
||||
|
||||
this.$root.$on("resize", this.handleResize);
|
||||
eventbus.on("resize", this.handleResize);
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.historyObserver) {
|
||||
@ -182,10 +185,10 @@ export default {
|
||||
});
|
||||
},
|
||||
beforeUpdate() {
|
||||
this.unreadMarkerShown = false;
|
||||
unreadMarkerShown = false;
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$root.$off("resize", this.handleResize);
|
||||
eventbus.off("resize", this.handleResize);
|
||||
this.$refs.chat.removeEventListener("scroll", this.handleScroll);
|
||||
},
|
||||
destroyed() {
|
||||
@ -201,11 +204,18 @@ export default {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new Date(previousMessage.time).getDay() !== new Date(message.time).getDay();
|
||||
const oldDate = new Date(previousMessage.time);
|
||||
const newDate = new Date(message.time);
|
||||
|
||||
return (
|
||||
oldDate.getDate() !== newDate.getDate() ||
|
||||
oldDate.getMonth() !== newDate.getMonth() ||
|
||||
oldDate.getFullYear() !== newDate.getFullYear()
|
||||
);
|
||||
},
|
||||
shouldDisplayUnreadMarker(id) {
|
||||
if (!this.unreadMarkerShown && id > this.channel.firstUnread) {
|
||||
this.unreadMarkerShown = true;
|
||||
if (!unreadMarkerShown && id > this.channel.firstUnread) {
|
||||
unreadMarkerShown = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,12 @@
|
||||
<span class="content">
|
||||
<Username :user="message.from" />
|
||||
<i class="hostmask"> ({{ message.hostmask }})</i>
|
||||
<template v-if="message.account">
|
||||
<i class="account"> [{{ message.account }}]</i>
|
||||
</template>
|
||||
<template v-if="message.gecos">
|
||||
<i class="realname"> {{ message.gecos }}</i>
|
||||
</template>
|
||||
has joined the channel
|
||||
</span>
|
||||
</template>
|
||||
|
@ -8,7 +8,7 @@
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
|
||||
export default {
|
||||
name: "MessageTypeMOTD",
|
||||
name: "MessageTypeMonospaceBlock",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
},
|
@ -81,6 +81,11 @@
|
||||
<dd>Yes</dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.certfp">
|
||||
<dt>Certificate:</dt>
|
||||
<dd>{{ message.whois.certfp }}</dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.server">
|
||||
<dt>Connected to:</dt>
|
||||
<dd>
|
||||
|
@ -11,7 +11,9 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
Connect
|
||||
<template v-if="config.lockNetwork">to {{ defaults.name }}</template>
|
||||
<template v-if="config.lockNetwork && $store.state.serverConfiguration.public">
|
||||
to {{ defaults.name }}
|
||||
</template>
|
||||
</template>
|
||||
</h1>
|
||||
<template v-if="!config.lockNetwork">
|
||||
@ -97,6 +99,36 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="config.lockNetwork && !$store.state.serverConfiguration.public">
|
||||
<h2>Network settings</h2>
|
||||
<div class="connect-row">
|
||||
<label for="connect:name">Name</label>
|
||||
<input
|
||||
id="connect:name"
|
||||
v-model="defaults.name"
|
||||
class="input"
|
||||
name="name"
|
||||
maxlength="100"
|
||||
/>
|
||||
</div>
|
||||
<div class="connect-row">
|
||||
<label for="connect:password">Password</label>
|
||||
<RevealPassword
|
||||
v-slot:default="slotProps"
|
||||
class="input-wrap password-container"
|
||||
>
|
||||
<input
|
||||
id="connect:password"
|
||||
v-model="defaults.password"
|
||||
class="input"
|
||||
:type="slotProps.isVisible ? 'text' : 'password'"
|
||||
placeholder="Server password (optional)"
|
||||
name="password"
|
||||
maxlength="300"
|
||||
/>
|
||||
</RevealPassword>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<h2>User preferences</h2>
|
||||
<div class="connect-row">
|
||||
@ -135,6 +167,16 @@
|
||||
maxlength="300"
|
||||
/>
|
||||
</div>
|
||||
<div class="connect-row">
|
||||
<label for="connect:leaveMessage">Leave message</label>
|
||||
<input
|
||||
id="connect:leaveMessage"
|
||||
v-model="defaults.leaveMessage"
|
||||
class="input"
|
||||
name="leaveMessage"
|
||||
placeholder="The Lounge - https://thelounge.chat"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="defaults.uuid && !$store.state.serverConfiguration.public">
|
||||
<div class="connect-row">
|
||||
<label for="connect:commands">
|
||||
@ -270,9 +312,7 @@ the server tab on new connection"
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="defaults.sasl === 'external'" class="connect-sasl-external">
|
||||
<p>
|
||||
The Lounge automatically generates and manages the client certificate.
|
||||
</p>
|
||||
<p>The Lounge automatically generates and manages the client certificate.</p>
|
||||
<p>
|
||||
On the IRC server, you will need to tell the services to attach the
|
||||
certificate fingerprint (certfp) to your account, for example:
|
||||
|
@ -46,9 +46,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-results">
|
||||
No results found.
|
||||
</div>
|
||||
<div v-else class="no-results">No results found.</div>
|
||||
</div>
|
||||
<Draggable
|
||||
v-else
|
||||
@ -84,13 +82,13 @@
|
||||
$store.state.activeChannel &&
|
||||
network.channels[0] === $store.state.activeChannel.channel
|
||||
"
|
||||
@toggleJoinChannel="network.isJoinChannelShown = !network.isJoinChannelShown"
|
||||
@toggle-join-channel="network.isJoinChannelShown = !network.isJoinChannelShown"
|
||||
/>
|
||||
<JoinChannel
|
||||
v-if="network.isJoinChannelShown"
|
||||
:network="network"
|
||||
:channel="network.channels[0]"
|
||||
@toggleJoinChannel="network.isJoinChannelShown = !network.isJoinChannelShown"
|
||||
@toggle-join-channel="network.isJoinChannelShown = !network.isJoinChannelShown"
|
||||
/>
|
||||
|
||||
<Draggable
|
||||
@ -106,8 +104,8 @@
|
||||
@start="onDragStart"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<template v-for="(channel, index) in network.channels">
|
||||
<Channel
|
||||
v-for="(channel, index) in network.channels"
|
||||
v-if="index > 0"
|
||||
:key="channel.id"
|
||||
:channel="channel"
|
||||
@ -117,6 +115,7 @@
|
||||
channel === $store.state.activeChannel.channel
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</Draggable>
|
||||
</div>
|
||||
</Draggable>
|
||||
|
@ -39,7 +39,7 @@
|
||||
:class="['add-channel', {opened: isJoinChannelShown}]"
|
||||
:aria-controls="'join-channel-' + channel.id"
|
||||
:aria-label="joinChannelLabel"
|
||||
@click.stop="$emit('toggleJoinChannel')"
|
||||
@click.stop="$emit('toggle-join-channel')"
|
||||
/>
|
||||
</span>
|
||||
</ChannelWrapper>
|
||||
|
@ -6,11 +6,12 @@
|
||||
v-on="onHover ? {mouseenter: hover} : {}"
|
||||
@click.prevent="openContextMenu"
|
||||
@contextmenu.prevent="openContextMenu"
|
||||
><slot>{{ user.mode }}{{ user.nick }}</slot></span
|
||||
><slot>{{ mode }}{{ user.nick }}</slot></span
|
||||
>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventbus from "../js/eventbus";
|
||||
import colorClass from "../js/helpers/colorClass";
|
||||
|
||||
export default {
|
||||
@ -23,6 +24,14 @@ export default {
|
||||
network: Object,
|
||||
},
|
||||
computed: {
|
||||
mode() {
|
||||
// Message objects have a singular mode, but user objects have modes array
|
||||
if (this.user.modes) {
|
||||
return this.user.modes[0];
|
||||
}
|
||||
|
||||
return this.user.mode;
|
||||
},
|
||||
nickColor() {
|
||||
return colorClass(this.user.nick);
|
||||
},
|
||||
@ -32,7 +41,7 @@ export default {
|
||||
return this.onHover(this.user);
|
||||
},
|
||||
openContextMenu(event) {
|
||||
this.$root.$emit("contextmenu:user", {
|
||||
eventbus.emit("contextmenu:user", {
|
||||
event: event,
|
||||
user: this.user,
|
||||
network: this.network,
|
||||
|
@ -1,13 +1,9 @@
|
||||
<template>
|
||||
<div id="version-checker" :class="[$store.state.versionStatus]">
|
||||
<p v-if="$store.state.versionStatus === 'loading'">
|
||||
Checking for updates…
|
||||
</p>
|
||||
<p v-if="$store.state.versionStatus === 'loading'">Checking for updates…</p>
|
||||
<p v-if="$store.state.versionStatus === 'new-version'">
|
||||
The Lounge <b>{{ $store.state.versionData.latest.version }}</b>
|
||||
<template v-if="$store.state.versionData.latest.prerelease">
|
||||
(pre-release)
|
||||
</template>
|
||||
<template v-if="$store.state.versionData.latest.prerelease"> (pre-release) </template>
|
||||
is now available.
|
||||
<br />
|
||||
|
||||
@ -20,9 +16,7 @@
|
||||
<code>thelounge upgrade</code> on the server to upgrade packages.
|
||||
</p>
|
||||
<template v-if="$store.state.versionStatus === 'up-to-date'">
|
||||
<p>
|
||||
The Lounge is up to date!
|
||||
</p>
|
||||
<p>The Lounge is up to date!</p>
|
||||
|
||||
<button
|
||||
v-if="$store.state.versionDataExpired"
|
||||
@ -34,9 +28,7 @@
|
||||
</button>
|
||||
</template>
|
||||
<template v-if="$store.state.versionStatus === 'error'">
|
||||
<p>
|
||||
Information about latest release could not be retrieved.
|
||||
</p>
|
||||
<p>Information about latest release could not be retrieved.</p>
|
||||
|
||||
<button id="check-now" class="btn btn-small" @click="checkNow">Try again</button>
|
||||
</template>
|
||||
|
@ -318,9 +318,7 @@
|
||||
<kbd>↓</kbd> keys to highlight an item, and insert it by pressing <kbd>Tab</kbd> or
|
||||
<kbd>Enter</kbd> (or by clicking the desired item).
|
||||
</p>
|
||||
<p>
|
||||
Autocompletion can be disabled in settings.
|
||||
</p>
|
||||
<p>Autocompletion can be disabled in settings.</p>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
@ -474,9 +472,7 @@
|
||||
<code>/disconnect [message]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Disconnect from the current network with an optionally-provided message.
|
||||
</p>
|
||||
<p>Disconnect from the current network with an optionally-provided message.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -610,9 +606,7 @@
|
||||
<code>/op nick [...nick]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Give op (<code>+o</code>) to one or several users in the current channel.
|
||||
</p>
|
||||
<p>Give op (<code>+o</code>) to one or several users in the current channel.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -656,9 +650,7 @@
|
||||
<code>/quit [message]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Disconnect from the current network with an optional message.
|
||||
</p>
|
||||
<p>Disconnect from the current network with an optional message.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -733,9 +725,7 @@
|
||||
<code>/whois nick</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Retrieve information about the given user on the current network.
|
||||
</p>
|
||||
<p>Retrieve information about the given user on the current network.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -172,8 +172,14 @@
|
||||
</div>
|
||||
<div v-if="$store.state.settings.advanced">
|
||||
<label class="opt">
|
||||
<label for="nickPostfix" class="sr-only">
|
||||
Nick autocomplete postfix (for example a comma)
|
||||
<label for="nickPostfix" class="opt">
|
||||
Nick autocomplete postfix
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-delay"
|
||||
aria-label="Nick autocomplete postfix (for example a comma)"
|
||||
>
|
||||
<button class="extra-help" />
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="nickPostfix"
|
||||
@ -271,9 +277,7 @@ This may break orientation if your browser does not support that."
|
||||
<template v-else-if="$store.state.pushNotificationState === 'loading'">
|
||||
Loading…
|
||||
</template>
|
||||
<template v-else>
|
||||
Subscribe to push notifications
|
||||
</template>
|
||||
<template v-else> Subscribe to push notifications </template>
|
||||
</button>
|
||||
<div v-if="$store.state.pushNotificationState === 'nohttps'" class="error">
|
||||
<strong>Warning</strong>: Push notifications are only supported over HTTPS
|
||||
@ -351,8 +355,15 @@ This may break orientation if your browser does not support that."
|
||||
|
||||
<div v-if="!$store.state.serverConfiguration.public && $store.state.settings.advanced">
|
||||
<label class="opt">
|
||||
<label for="highlights" class="sr-only">
|
||||
Custom highlights (comma-separated keywords)
|
||||
<label for="highlights" class="opt">
|
||||
Custom highlights
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-delay"
|
||||
aria-label="If a message contains any of these comma-separated
|
||||
expressions, it will trigger a highlight."
|
||||
>
|
||||
<button class="extra-help" />
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="highlights"
|
||||
@ -360,7 +371,31 @@ This may break orientation if your browser does not support that."
|
||||
type="text"
|
||||
name="highlights"
|
||||
class="input"
|
||||
placeholder="Custom highlights (comma-separated keywords)"
|
||||
placeholder="Comma-separated, e.g.: word, some more words, anotherword"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="!$store.state.serverConfiguration.public && $store.state.settings.advanced">
|
||||
<label class="opt">
|
||||
<label for="highlightExceptions" class="opt">
|
||||
Highlight exceptions
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-delay"
|
||||
aria-label="If a message contains any of these comma-separated
|
||||
expressions, it will not trigger a highlight even if it contains
|
||||
your nickname or expressions defined in custom highlights."
|
||||
>
|
||||
<button class="extra-help" />
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="highlightExceptions"
|
||||
:value="$store.state.settings.highlightExceptions"
|
||||
type="text"
|
||||
name="highlightExceptions"
|
||||
class="input"
|
||||
placeholder="Comma-separated, e.g.: word, some more words, anotherword"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
@ -376,9 +411,7 @@ This may break orientation if your browser does not support that."
|
||||
>
|
||||
<h2 id="label-change-password">Change password</h2>
|
||||
<div class="password-container">
|
||||
<label for="old_password_input" class="sr-only">
|
||||
Enter current password
|
||||
</label>
|
||||
<label for="old_password_input" class="sr-only"> Enter current password </label>
|
||||
<RevealPassword v-slot:default="slotProps">
|
||||
<input
|
||||
id="old_password_input"
|
||||
@ -404,9 +437,7 @@ This may break orientation if your browser does not support that."
|
||||
</RevealPassword>
|
||||
</div>
|
||||
<div class="password-container">
|
||||
<label for="verify_password_input" class="sr-only">
|
||||
Repeat new password
|
||||
</label>
|
||||
<label for="verify_password_input" class="sr-only"> Repeat new password </label>
|
||||
<RevealPassword v-slot:default="slotProps">
|
||||
<input
|
||||
id="verify_password_input"
|
||||
|
@ -144,7 +144,7 @@ button {
|
||||
|
||||
code,
|
||||
pre,
|
||||
#chat .msg[data-type="motd"] .text,
|
||||
#chat .msg[data-type="monospace_block"] .text,
|
||||
.irc-monospace,
|
||||
textarea#user-specified-css-input {
|
||||
font-family: Consolas, Menlo, Monaco, "Lucida Console", "DejaVu Sans Mono", "Courier New", monospace;
|
||||
@ -304,7 +304,9 @@ p {
|
||||
#chat .msg[data-type="topic"] .from::before,
|
||||
#chat .msg[data-type="mode_channel"] .from::before,
|
||||
#chat .msg[data-type="mode"] .from::before,
|
||||
#chat .msg[data-type="motd"] .from::before,
|
||||
#chat .msg[data-command="motd"] .from::before,
|
||||
#chat .msg[data-command="help"] .from::before,
|
||||
#chat .msg[data-command="info"] .from::before,
|
||||
#chat .msg[data-type="ctcp"] .from::before,
|
||||
#chat .msg[data-type="ctcp_request"] .from::before,
|
||||
#chat .msg[data-type="whois"] .from::before,
|
||||
@ -312,6 +314,7 @@ p {
|
||||
#chat .msg[data-type="action"] .from::before,
|
||||
#chat .msg[data-type="plugin"] .from::before,
|
||||
#chat .msg[data-type="raw"] .from::before,
|
||||
#chat .msg-statusmsg span::before,
|
||||
#chat .msg-shown-in-active span::before,
|
||||
#chat .toggle-button::after,
|
||||
#chat .toggle-content .more-caret::before,
|
||||
@ -349,9 +352,10 @@ p {
|
||||
.context-menu-disconnect::before { content: "\f127"; /* https://fontawesome.com/icons/unlink?style=solid */ }
|
||||
.context-menu-connect::before { content: "\f0c1"; /* https://fontawesome.com/icons/link?style=solid */ }
|
||||
.context-menu-action-whois::before { content: "\f05a"; /* http://fontawesome.io/icon/info-circle/ */ }
|
||||
.context-menu-action-ignore::before { content: "\f506"; /* https://fontawesome.com/icons/user-slash?style=solid */ }
|
||||
.context-menu-action-kick::before { content: "\f05e"; /* http://fontawesome.io/icon/ban/ */ }
|
||||
.context-menu-action-op::before { content: "\f1fa"; /* http://fontawesome.io/icon/at/ */ }
|
||||
.context-menu-action-voice::before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ }
|
||||
.context-menu-action-set-mode::before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ }
|
||||
.context-menu-action-revoke-mode::before { content: "\f068"; /* http://fontawesome.io/icon/minus/ */ }
|
||||
.context-menu-network::before { content: "\f233"; /* https://fontawesome.com/icons/server?style=solid */ }
|
||||
.context-menu-edit::before { content: "\f303"; /* https://fontawesome.com/icons/pencil-alt?style=solid */ }
|
||||
.context-menu-clear-history::before { content: "\f1f8"; /* https://fontawesome.com/icons/trash?style=solid */ }
|
||||
@ -428,11 +432,21 @@ p {
|
||||
color: #2ecc40;
|
||||
}
|
||||
|
||||
#chat .msg[data-type="motd"] .from::before {
|
||||
#chat .msg[data-command="motd"] .from::before {
|
||||
content: "\f02e"; /* https://fontawesome.com/icons/bookmark?style=solid */
|
||||
color: var(--body-color-muted);
|
||||
}
|
||||
|
||||
#chat .msg[data-command="help"] .from::before {
|
||||
content: "\f059"; /* https://fontawesome.com/icons/question-circle?style=solid */
|
||||
color: var(--body-color-muted);
|
||||
}
|
||||
|
||||
#chat .msg[data-command="info"] .from::before {
|
||||
content: "\f05a"; /* https://fontawesome.com/icons/info-circle?style=solid */
|
||||
color: var(--body-color-muted);
|
||||
}
|
||||
|
||||
#chat .msg[data-type="ctcp"] .from::before,
|
||||
#chat .msg[data-type="ctcp_request"] .from::before {
|
||||
content: "\f15c"; /* https://fontawesome.com/icons/file-alt?style=solid */
|
||||
@ -479,16 +493,25 @@ p {
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
#chat .msg-statusmsg,
|
||||
#chat .msg-shown-in-active {
|
||||
cursor: help;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#chat .msg-statusmsg span::before,
|
||||
#chat .msg-shown-in-active span::before {
|
||||
font-size: 10px;
|
||||
content: "\f06e"; /* https://fontawesome.com/icons/eye?style=solid */
|
||||
}
|
||||
|
||||
#chat .msg-statusmsg {
|
||||
border-radius: 2px;
|
||||
padding: 2px 4px;
|
||||
background-color: #ff9e18;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
#chat .toggle-button {
|
||||
display: inline-block;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
@ -1448,11 +1471,11 @@ textarea.input {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
#chat.hide-motd .msg[data-type="motd"] {
|
||||
#chat.hide-motd .msg[data-command="motd"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#chat .msg[data-type="motd"] .text {
|
||||
#chat .msg[data-type="monospace_block"] .text {
|
||||
background: #f6f6f6;
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
@ -2020,6 +2043,14 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
.window#chat-container {
|
||||
/*
|
||||
Chat has its own scrollbar, so remove the one on parent
|
||||
This caused a performance issue in Chrome
|
||||
*/
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#version-checker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -2812,7 +2843,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
||||
.header .topic,
|
||||
#chat .msg[data-type="action"] .content,
|
||||
#chat .msg[data-type="message"] .content,
|
||||
#chat .msg[data-type="motd"] .content,
|
||||
#chat .msg[data-type="monospace_block"] .content,
|
||||
#chat .msg[data-type="notice"] .content,
|
||||
#chat .ctcp-message,
|
||||
#chat .part-reason,
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
|
51
client/js/eventbus.js
Normal file
51
client/js/eventbus.js
Normal file
@ -0,0 +1,51 @@
|
||||
const events = new Map();
|
||||
|
||||
class EventBus {
|
||||
/**
|
||||
* Register an event handler for the given type.
|
||||
*
|
||||
* @param {String} type Type of event to listen for.
|
||||
* @param {Function} handler Function to call in response to given event.
|
||||
*/
|
||||
on(type, handler) {
|
||||
if (events.has(type)) {
|
||||
events.get(type).push(handler);
|
||||
} else {
|
||||
events.set(type, [handler]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an event handler for the given type.
|
||||
*
|
||||
* @param {String} type Type of event to unregister `handler` from.
|
||||
* @param {Function} handler Handler function to remove.
|
||||
*/
|
||||
off(type, handler) {
|
||||
if (events.has(type)) {
|
||||
events.set(
|
||||
type,
|
||||
events.get(type).filter((item) => item !== handler)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke all handlers for the given type.
|
||||
*
|
||||
* @param {String} type The event type to invoke.
|
||||
* @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler.
|
||||
*/
|
||||
emit(type, ...evt) {
|
||||
if (events.has(type)) {
|
||||
events
|
||||
.get(type)
|
||||
.slice()
|
||||
.map((handler) => {
|
||||
handler(...evt);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new EventBus();
|
@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
import socket from "../socket";
|
||||
import eventbus from "../eventbus";
|
||||
|
||||
export function generateChannelContextMenu($root, channel, network) {
|
||||
const typeMap = {
|
||||
@ -115,7 +116,8 @@ export function generateChannelContextMenu($root, channel, network) {
|
||||
|
||||
// Add menu items for queries
|
||||
if (channel.type === "query") {
|
||||
items.push({
|
||||
items.push(
|
||||
{
|
||||
label: "User information",
|
||||
type: "item",
|
||||
class: "action-whois",
|
||||
@ -126,7 +128,19 @@ export function generateChannelContextMenu($root, channel, network) {
|
||||
text: "/whois " + channel.name,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Ignore user",
|
||||
type: "item",
|
||||
class: "action-ignore",
|
||||
action() {
|
||||
socket.emit("input", {
|
||||
target: channel.id,
|
||||
text: "/ignore " + channel.name,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (channel.type === "channel" || channel.type === "query") {
|
||||
@ -135,7 +149,7 @@ export function generateChannelContextMenu($root, channel, network) {
|
||||
type: "item",
|
||||
class: "clear-history",
|
||||
action() {
|
||||
$root.$emit(
|
||||
eventbus.emit(
|
||||
"confirm-dialog",
|
||||
{
|
||||
title: "Clear history",
|
||||
@ -203,6 +217,17 @@ export function generateUserContextMenu($root, channel, network, user) {
|
||||
class: "action-whois",
|
||||
action: whois,
|
||||
},
|
||||
{
|
||||
label: "Ignore user",
|
||||
type: "item",
|
||||
class: "action-ignore",
|
||||
action() {
|
||||
socket.emit("input", {
|
||||
target: channel.id,
|
||||
text: "/ignore " + user.nick,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Direct messages",
|
||||
type: "item",
|
||||
@ -222,7 +247,85 @@ export function generateUserContextMenu($root, channel, network, user) {
|
||||
},
|
||||
];
|
||||
|
||||
if (currentChannelUser.mode === "@") {
|
||||
// Bail because we're in a query or we don't have a special mode.
|
||||
if (!currentChannelUser.modes || currentChannelUser.modes.length < 1) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// Names of the modes we are able to change
|
||||
const modes = {
|
||||
"~": ["owner", "q"],
|
||||
"&": ["admin", "a"],
|
||||
"@": ["operator", "o"],
|
||||
"%": ["half-op", "h"],
|
||||
"+": ["voice", "v"],
|
||||
};
|
||||
|
||||
// Labels for the mode changes. For example .rev(['admin', 'a']) => 'Revoke admin (-a)'
|
||||
const modeTextTemplate = {
|
||||
revoke: (m) => `Revoke ${m[0]} (-${m[1]})`,
|
||||
give: (m) => `Give ${m[0]} (+${m[1]})`,
|
||||
};
|
||||
|
||||
const networkModes = network.serverOptions.PREFIX;
|
||||
|
||||
/**
|
||||
* Determine whether the prefix of mode p1 has access to perform actions on p2.
|
||||
*
|
||||
* EXAMPLE:
|
||||
* compare('@', '@') => true
|
||||
* compare('&', '@') => true
|
||||
* compare('+', '~') => false
|
||||
* @param {string} p1 The mode performing an action
|
||||
* @param {string} p2 The target mode
|
||||
*
|
||||
* @return {boolean} whether p1 can perform an action on p2
|
||||
*/
|
||||
function compare(p1, p2) {
|
||||
// The modes ~ and @ can perform actions on their own mode. The others on modes below.
|
||||
return "~@".indexOf(p1) > -1
|
||||
? networkModes.indexOf(p1) <= networkModes.indexOf(p2)
|
||||
: networkModes.indexOf(p1) < networkModes.indexOf(p2);
|
||||
}
|
||||
|
||||
networkModes.forEach((prefix) => {
|
||||
if (!compare(currentChannelUser.modes[0], prefix)) {
|
||||
// Our highest mode is below the current mode. Bail.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user.modes.includes(prefix)) {
|
||||
// The target doesn't already have this mode, therefore we can set it.
|
||||
items.push({
|
||||
label: modeTextTemplate.give(modes[prefix]),
|
||||
type: "item",
|
||||
class: "action-set-mode",
|
||||
action() {
|
||||
socket.emit("input", {
|
||||
target: channel.id,
|
||||
text: "/mode +" + modes[prefix][1] + " " + user.nick,
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
label: modeTextTemplate.revoke(modes[prefix]),
|
||||
type: "item",
|
||||
class: "action-revoke-mode",
|
||||
action() {
|
||||
socket.emit("input", {
|
||||
target: channel.id,
|
||||
text: "/mode -" + modes[prefix][1] + " " + user.nick,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Determine if we are half-op or op depending on the network modes so we can kick.
|
||||
if (!compare(networkModes.indexOf("%") > -1 ? "%" : "@", currentChannelUser.modes[0])) {
|
||||
if (user.modes.length === 0 || compare(currentChannelUser.modes[0], user.modes[0])) {
|
||||
// Check if the target user has no mode or a mode lower than ours.
|
||||
items.push({
|
||||
label: "Kick",
|
||||
type: "item",
|
||||
@ -234,57 +337,6 @@ export function generateUserContextMenu($root, channel, network, user) {
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (user.mode === "@") {
|
||||
items.push({
|
||||
label: "Revoke operator (-o)",
|
||||
type: "item",
|
||||
class: "action-op",
|
||||
action() {
|
||||
socket.emit("input", {
|
||||
target: channel.id,
|
||||
text: "/deop " + user.nick,
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
label: "Give operator (+o)",
|
||||
type: "item",
|
||||
class: "action-op",
|
||||
action() {
|
||||
socket.emit("input", {
|
||||
target: channel.id,
|
||||
text: "/op " + user.nick,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (user.mode === "+") {
|
||||
items.push({
|
||||
label: "Revoke voice (-v)",
|
||||
type: "item",
|
||||
class: "action-voice",
|
||||
action() {
|
||||
socket.emit("input", {
|
||||
target: channel.id,
|
||||
text: "/devoice " + user.nick,
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
label: "Give voice (+v)",
|
||||
type: "item",
|
||||
class: "action-voice",
|
||||
action() {
|
||||
socket.emit("input", {
|
||||
target: channel.id,
|
||||
text: "/voice " + user.nick,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,11 +6,13 @@ LinkifyIt.prototype.normalize = function normalize(match) {
|
||||
if (!match.schema) {
|
||||
match.schema = "http:";
|
||||
match.url = "http://" + match.url;
|
||||
match.noschema = true;
|
||||
}
|
||||
|
||||
if (match.schema === "//") {
|
||||
match.schema = "http:";
|
||||
match.url = "http:" + match.url;
|
||||
match.noschema = true;
|
||||
}
|
||||
|
||||
if (match.schema === "mailto:" && !/^mailto:/i.test(match.url)) {
|
||||
@ -34,6 +36,8 @@ const commonSchemes = [
|
||||
"ts3server",
|
||||
"svn+ssh",
|
||||
"ssh",
|
||||
"gopher",
|
||||
"gemini",
|
||||
];
|
||||
|
||||
for (const schema of commonSchemes) {
|
||||
@ -47,11 +51,28 @@ function findLinks(text) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return matches.map((url) => ({
|
||||
return matches.map(returnUrl);
|
||||
}
|
||||
|
||||
function findLinksWithSchema(text) {
|
||||
const matches = linkify.match(text);
|
||||
|
||||
if (!matches) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return matches.filter((url) => !url.noschema).map(returnUrl);
|
||||
}
|
||||
|
||||
function returnUrl(url) {
|
||||
return {
|
||||
start: url.index,
|
||||
end: url.lastIndex,
|
||||
link: url.url,
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = findLinks;
|
||||
module.exports = {
|
||||
findLinks,
|
||||
findLinksWithSchema,
|
||||
};
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
import parseStyle from "./ircmessageparser/parseStyle";
|
||||
import findChannels from "./ircmessageparser/findChannels";
|
||||
import findLinks from "./ircmessageparser/findLinks";
|
||||
import {findLinks} from "./ircmessageparser/findLinks";
|
||||
import findEmoji from "./ircmessageparser/findEmoji";
|
||||
import findNames from "./ircmessageparser/findNames";
|
||||
import merge from "./ircmessageparser/merge";
|
||||
|
@ -613,7 +613,7 @@
|
||||
"dragon_face": "🐲",
|
||||
"dragon": "🐉",
|
||||
"sauropod": "🦕",
|
||||
"t-rex": "🦖",
|
||||
"t_rex": "🦖",
|
||||
"whale": "🐳",
|
||||
"whale2": "🐋",
|
||||
"dolphin": "🐬",
|
||||
@ -1082,7 +1082,7 @@
|
||||
"game_die": "🎲",
|
||||
"jigsaw": "🧩",
|
||||
"teddy_bear": "🧸",
|
||||
"pi_ata": "🪅",
|
||||
"pinata": "🪅",
|
||||
"nesting_dolls": "🪆",
|
||||
"spades": "♠️",
|
||||
"hearts": "♥️",
|
||||
@ -1240,7 +1240,7 @@
|
||||
"chart": "💹",
|
||||
"email": "✉️",
|
||||
"envelope": "✉️",
|
||||
"e-mail": "📧",
|
||||
"e_mail": "📧",
|
||||
"incoming_envelope": "📨",
|
||||
"envelope_with_arrow": "📩",
|
||||
"outbox_tray": "📤",
|
||||
@ -1376,7 +1376,7 @@
|
||||
"no_bicycles": "🚳",
|
||||
"no_smoking": "🚭",
|
||||
"do_not_litter": "🚯",
|
||||
"non-potable_water": "🚱",
|
||||
"non_potable_water": "🚱",
|
||||
"no_pedestrians": "🚷",
|
||||
"no_mobile_phones": "📵",
|
||||
"underage": "🔞",
|
||||
|
@ -33,12 +33,63 @@ const router = new VueRouter({
|
||||
next();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Connect",
|
||||
path: "/connect",
|
||||
component: Connect,
|
||||
props: (route) => ({queryParams: route.query}),
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
path: "/settings",
|
||||
component: Settings,
|
||||
},
|
||||
{
|
||||
name: "Help",
|
||||
path: "/help",
|
||||
component: Help,
|
||||
},
|
||||
{
|
||||
name: "Changelog",
|
||||
path: "/changelog",
|
||||
component: Changelog,
|
||||
},
|
||||
{
|
||||
name: "NetworkEdit",
|
||||
path: "/edit-network/:uuid",
|
||||
component: NetworkEdit,
|
||||
},
|
||||
{
|
||||
name: "RoutedChat",
|
||||
path: "/chan-:id",
|
||||
component: RoutedChat,
|
||||
},
|
||||
{
|
||||
name: "SearchResults",
|
||||
path: "/search/:uuid/:target/:term",
|
||||
component: SearchResults,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
// If user is not yet signed in, wait for appLoaded state to change
|
||||
// unless they are trying to open SignIn (which can be triggered in auth.js)
|
||||
if (!store.state.appLoaded && to.name !== "SignIn") {
|
||||
store.watch(
|
||||
(state) => state.appLoaded,
|
||||
() => next()
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
// Disallow navigating to non-existing routes
|
||||
if (store.state.appLoaded && !to.matched.length) {
|
||||
if (!to.matched.length) {
|
||||
next(false);
|
||||
return;
|
||||
}
|
||||
@ -49,6 +100,12 @@ router.beforeEach((to, from, next) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disallow navigating to invalid networks
|
||||
if (to.name === "NetworkEdit" && !store.getters.findNetwork(to.params.uuid)) {
|
||||
next(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle closing image viewer with the browser back button
|
||||
if (!router.app.$refs.app) {
|
||||
next();
|
||||
@ -92,47 +149,6 @@ router.afterEach((to) => {
|
||||
}
|
||||
});
|
||||
|
||||
function initialize() {
|
||||
router.addRoutes([
|
||||
{
|
||||
name: "Connect",
|
||||
path: "/connect",
|
||||
component: Connect,
|
||||
props: (route) => ({queryParams: route.query}),
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
path: "/settings",
|
||||
component: Settings,
|
||||
},
|
||||
{
|
||||
name: "Help",
|
||||
path: "/help",
|
||||
component: Help,
|
||||
},
|
||||
{
|
||||
name: "Changelog",
|
||||
path: "/changelog",
|
||||
component: Changelog,
|
||||
},
|
||||
{
|
||||
name: "NetworkEdit",
|
||||
path: "/edit-network/:uuid",
|
||||
component: NetworkEdit,
|
||||
},
|
||||
{
|
||||
name: "RoutedChat",
|
||||
path: "/chan-:id",
|
||||
component: RoutedChat,
|
||||
},
|
||||
{
|
||||
name: "SearchResults",
|
||||
path: "/search/:uuid/:target/:term",
|
||||
component: SearchResults,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function navigate(routeName, params = {}) {
|
||||
if (router.currentRoute.name) {
|
||||
router.push({name: routeName, params}).catch(() => {});
|
||||
@ -162,4 +178,4 @@ if ("serviceWorker" in navigator) {
|
||||
});
|
||||
}
|
||||
|
||||
export {initialize, router, navigate, switchToChannel};
|
||||
export {router, navigate, switchToChannel};
|
||||
|
@ -31,6 +31,7 @@ export const config = normalizeConfig({
|
||||
},
|
||||
desktopNotifications: {
|
||||
default: false,
|
||||
sync: "never",
|
||||
apply(store, value) {
|
||||
store.commit("refreshDesktopNotificationState", null, {root: true});
|
||||
|
||||
@ -45,6 +46,10 @@ export const config = normalizeConfig({
|
||||
default: "",
|
||||
sync: "always",
|
||||
},
|
||||
highlightExceptions: {
|
||||
default: "",
|
||||
sync: "always",
|
||||
},
|
||||
awayMessage: {
|
||||
default: "",
|
||||
sync: "always",
|
||||
@ -57,6 +62,7 @@ export const config = normalizeConfig({
|
||||
},
|
||||
notification: {
|
||||
default: true,
|
||||
sync: "never",
|
||||
},
|
||||
notifyAllMessages: {
|
||||
default: false,
|
||||
|
@ -5,7 +5,7 @@ socket.on("disconnect", handleDisconnect);
|
||||
socket.on("connect_error", handleDisconnect);
|
||||
socket.on("error", handleDisconnect);
|
||||
|
||||
socket.on("reconnecting", function (attempt) {
|
||||
socket.io.on("reconnect_attempt", function (attempt) {
|
||||
store.commit("currentUserVisibleError", `Reconnecting… (attempt ${attempt})`);
|
||||
updateLoadingMessage();
|
||||
});
|
||||
|
@ -1,8 +1,9 @@
|
||||
"use strict";
|
||||
|
||||
import Vue from "vue";
|
||||
import socket from "../socket";
|
||||
import storage from "../localStorage";
|
||||
import {router, switchToChannel, navigate, initialize as routerInitialize} from "../router";
|
||||
import {router, switchToChannel, navigate} from "../router";
|
||||
import store from "../store";
|
||||
import parseIrcUri from "../helpers/parseIrcUri";
|
||||
|
||||
@ -16,10 +17,6 @@ socket.on("init", function (data) {
|
||||
}
|
||||
|
||||
if (!store.state.appLoaded) {
|
||||
// Routes are initialized after networks data is merged
|
||||
// so the route guard for channels works correctly on page load
|
||||
routerInitialize();
|
||||
|
||||
store.commit("appLoaded");
|
||||
|
||||
socket.emit("setting:get");
|
||||
@ -28,7 +25,12 @@ socket.on("init", function (data) {
|
||||
window.g_TheLoungeRemoveLoading();
|
||||
}
|
||||
|
||||
// TODO: Review this code and make it better
|
||||
Vue.nextTick(() => {
|
||||
// If we handled query parameters like irc:// links or just general
|
||||
// connect parameters in public mode, then nothing to do here
|
||||
if (!handleQueryParams()) {
|
||||
// If we are on an unknown route or still on SignIn component
|
||||
// then we can open last known channel on server, or Connect window if none
|
||||
if (!router.currentRoute.name || router.currentRoute.name === "SignIn") {
|
||||
const channel = store.getters.findChannel(data.active);
|
||||
|
||||
@ -42,10 +44,8 @@ socket.on("init", function (data) {
|
||||
navigate("Connect");
|
||||
}
|
||||
}
|
||||
|
||||
if ("URLSearchParams" in window) {
|
||||
handleQueryParams();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -154,6 +154,10 @@ function mergeChannelData(oldChannels, newChannels) {
|
||||
}
|
||||
|
||||
function handleQueryParams() {
|
||||
if (!("URLSearchParams" in window)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(document.location.search);
|
||||
|
||||
const cleanParams = () => {
|
||||
@ -169,11 +173,17 @@ function handleQueryParams() {
|
||||
|
||||
cleanParams();
|
||||
router.push({name: "Connect", query: queryParams});
|
||||
|
||||
return true;
|
||||
} else if (document.body.classList.contains("public") && document.location.search) {
|
||||
// Set default connection settings from url params
|
||||
const queryParams = Object.fromEntries(params.entries());
|
||||
|
||||
cleanParams();
|
||||
router.push({name: "Connect", query: queryParams});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -64,3 +64,8 @@ socket.on("network:info", function (data) {
|
||||
Vue.set(network, key, data[key]);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("network:name", function (data) {
|
||||
const network = store.getters.findNetwork(data.uuid);
|
||||
network.name = network.channels[0].name = data.name;
|
||||
});
|
||||
|
@ -82,7 +82,7 @@ function loadFromLocalStorage() {
|
||||
}
|
||||
|
||||
// Older The Lounge versions converted highlights to an array, turn it back into a string
|
||||
if (typeof storedSettings.highlights === "object") {
|
||||
if (storedSettings.highlights !== null && typeof storedSettings.highlights === "object") {
|
||||
storedSettings.highlights = storedSettings.highlights.join(", ");
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ class Uploader {
|
||||
init() {
|
||||
this.xhr = null;
|
||||
this.fileQueue = [];
|
||||
this.tokenKeepAlive = null;
|
||||
|
||||
document.addEventListener("dragenter", (e) => this.dragEnter(e));
|
||||
document.addEventListener("dragover", (e) => this.dragOver(e));
|
||||
@ -131,10 +132,17 @@ class Uploader {
|
||||
uploadNextFileInQueue(token) {
|
||||
const file = this.fileQueue.shift();
|
||||
|
||||
// Tell the server that we are still upload to this token
|
||||
// so it does not become invalidated and fail the upload.
|
||||
// This issue only happens if The Lounge is proxied through other software
|
||||
// as it may buffer the upload before the upload request will be processed by The Lounge.
|
||||
this.tokenKeepAlive = setInterval(() => socket.emit("upload:ping", token), 40 * 1000);
|
||||
|
||||
if (
|
||||
store.state.settings.uploadCanvas &&
|
||||
file.type.startsWith("image/") &&
|
||||
!file.type.includes("svg")
|
||||
!file.type.includes("svg") &&
|
||||
file.type !== "image/gif"
|
||||
) {
|
||||
this.renderImage(file, (newFile) => this.performUpload(token, newFile));
|
||||
} else {
|
||||
@ -219,6 +227,11 @@ class Uploader {
|
||||
handleResponse(response) {
|
||||
this.setProgress(0);
|
||||
|
||||
if (this.tokenKeepAlive) {
|
||||
clearInterval(this.tokenKeepAlive);
|
||||
this.tokenKeepAlive = null;
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
store.commit("currentUserVisibleError", response.error);
|
||||
return;
|
||||
|
@ -9,6 +9,7 @@ import App from "../components/App.vue";
|
||||
import storage from "./localStorage";
|
||||
import {router, navigate} from "./router";
|
||||
import socket from "./socket";
|
||||
import eventbus from "./eventbus";
|
||||
|
||||
import "./socket-events";
|
||||
import "./webpush";
|
||||
@ -18,7 +19,7 @@ const favicon = document.getElementById("favicon");
|
||||
const faviconNormal = favicon.getAttribute("href");
|
||||
const faviconAlerted = favicon.dataset.other;
|
||||
|
||||
const vueApp = new Vue({
|
||||
new Vue({
|
||||
el: "#viewport",
|
||||
router,
|
||||
mounted() {
|
||||
@ -30,7 +31,7 @@ const vueApp = new Vue({
|
||||
},
|
||||
closeChannel(channel) {
|
||||
if (channel.type === "lobby") {
|
||||
this.$root.$emit(
|
||||
eventbus.emit(
|
||||
"confirm-dialog",
|
||||
{
|
||||
title: "Remove network",
|
||||
@ -75,7 +76,7 @@ store.watch(
|
||||
(sidebarOpen) => {
|
||||
if (window.innerWidth > constants.mobileViewportPixels) {
|
||||
storage.set("thelounge.state.sidebar", sidebarOpen);
|
||||
vueApp.$emit("resize");
|
||||
eventbus.emit("resize");
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -84,7 +85,7 @@ store.watch(
|
||||
(state) => state.userlistOpen,
|
||||
(userlistOpen) => {
|
||||
storage.set("thelounge.state.userlist", userlistOpen);
|
||||
vueApp.$emit("resize");
|
||||
eventbus.emit("resize");
|
||||
}
|
||||
);
|
||||
|
||||
@ -100,6 +101,14 @@ store.watch(
|
||||
(_, getters) => getters.highlightCount,
|
||||
(highlightCount) => {
|
||||
favicon.setAttribute("href", highlightCount > 0 ? faviconAlerted : faviconNormal);
|
||||
|
||||
if (navigator.setAppBadge) {
|
||||
if (highlightCount > 0) {
|
||||
navigator.setAppBadge(highlightCount);
|
||||
} else {
|
||||
navigator.clearAppBadge();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -73,11 +73,13 @@ function togglePushSubscription() {
|
||||
.then((subscription) => {
|
||||
socket.emit("push:register", subscription.toJSON());
|
||||
store.commit("pushNotificationState", "subscribed");
|
||||
store.commit("refreshDesktopNotificationState");
|
||||
});
|
||||
})
|
||||
)
|
||||
.catch((err) => {
|
||||
store.commit("pushNotificationState", "unsupported");
|
||||
store.commit("refreshDesktopNotificationState");
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
});
|
||||
}
|
||||
|
@ -124,7 +124,7 @@ body {
|
||||
color: #f92772;
|
||||
}
|
||||
|
||||
#chat .msg[data-type="motd"] .text,
|
||||
#chat .msg[data-type="monospace_block"] .text,
|
||||
code,
|
||||
.irc-monospace {
|
||||
background: #28333d;
|
||||
|
@ -110,15 +110,27 @@ module.exports = {
|
||||
// This value is set to `false` by default.
|
||||
prefetch: false,
|
||||
|
||||
// ### `disableMediaPreview`
|
||||
//
|
||||
// When set to `true`, The Lounge will not preview media (images, video and
|
||||
// audio) hosted on third-party sites. This ensures the client does not
|
||||
// make any requests to external sites. If `prefetchStorage` is enabled,
|
||||
// images proxied via the The Lounge will be previewed.
|
||||
//
|
||||
// This has no effect if `prefetch` is set to `false`.
|
||||
//
|
||||
// This value is set to `false` by default.
|
||||
disableMediaPreview: false,
|
||||
|
||||
// ### `prefetchStorage`
|
||||
|
||||
// When set to `true`, The Lounge will store and proxy prefetched images and
|
||||
// thumbnails on the filesystem rather than directly display the content at
|
||||
// the original URLs.
|
||||
//
|
||||
// This improves security and privacy by not exposing the client IP address,
|
||||
// always loading images from The Lounge and making all assets secure, which
|
||||
// resolves mixed content warnings.
|
||||
// This option primarily exists to resolve mixed content warnings by not
|
||||
// loading images from http hosts. This option does not work for video
|
||||
// or audio as The Lounge will only load these from https hosts.
|
||||
//
|
||||
// If storage is enabled, The Lounge will fetch and store images and thumbnails
|
||||
// in the `${THELOUNGE_HOME}/storage` folder.
|
||||
@ -138,6 +150,15 @@ module.exports = {
|
||||
// This value is set to `2048` kilobytes by default.
|
||||
prefetchMaxImageSize: 2048,
|
||||
|
||||
// ### prefetchMaxSearchSize
|
||||
//
|
||||
// This value sets the maximum request size made to find the Open Graph tags
|
||||
// for link previews. For some sites like YouTube this can easily exceed 300
|
||||
// kilobytes.
|
||||
//
|
||||
// This value is set to `50` kilobytes by default.
|
||||
prefetchMaxSearchSize: 50,
|
||||
|
||||
// ### `fileUpload`
|
||||
//
|
||||
// Allow uploading files to the server hosting The Lounge.
|
||||
@ -206,6 +227,7 @@ module.exports = {
|
||||
// numbers from 0 to 9. For example, `Guest%%%` may become `Guest123`.
|
||||
// - `username`: User name.
|
||||
// - `realname`: Real name.
|
||||
// - `leaveMessage`: Network specific leave message (overrides global leaveMessage)
|
||||
// - `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
|
||||
@ -236,6 +258,7 @@ module.exports = {
|
||||
username: "thelounge",
|
||||
realname: "The Lounge User",
|
||||
join: "#thelounge",
|
||||
leaveMessage: "",
|
||||
},
|
||||
|
||||
// ### `lockNetwork`
|
||||
|
117
package.json
117
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "thelounge",
|
||||
"description": "The self-hosted Web IRC client",
|
||||
"version": "4.1.0",
|
||||
"version": "4.3.0-pre.1",
|
||||
"preferGlobal": true,
|
||||
"bin": {
|
||||
"thelounge": "index.js"
|
||||
@ -42,82 +42,83 @@
|
||||
"dependencies": {
|
||||
"bcryptjs": "2.4.3",
|
||||
"busboy": "0.3.1",
|
||||
"chalk": "4.0.0",
|
||||
"cheerio": "1.0.0-rc.3",
|
||||
"commander": "5.0.0",
|
||||
"chalk": "4.1.0",
|
||||
"cheerio": "1.0.0-rc.5",
|
||||
"commander": "7.2.0",
|
||||
"content-disposition": "0.5.3",
|
||||
"express": "4.17.1",
|
||||
"file-type": "14.1.4",
|
||||
"filenamify": "4.1.0",
|
||||
"got": "10.7.0",
|
||||
"irc-framework": "4.8.1",
|
||||
"file-type": "16.2.0",
|
||||
"filenamify": "4.2.0",
|
||||
"got": "11.8.1",
|
||||
"irc-framework": "4.9.0",
|
||||
"is-utf8": "0.2.1",
|
||||
"ldapjs": "2.0.0-pre.5",
|
||||
"linkify-it": "2.2.0",
|
||||
"lodash": "4.17.15",
|
||||
"mime-types": "2.1.26",
|
||||
"node-forge": "0.9.1",
|
||||
"ldapjs": "2.2.3",
|
||||
"linkify-it": "3.0.2",
|
||||
"lodash": "4.17.20",
|
||||
"mime-types": "2.1.28",
|
||||
"node-forge": "0.10.0",
|
||||
"package-json": "6.5.0",
|
||||
"read": "1.0.7",
|
||||
"read-chunk": "3.2.0",
|
||||
"semver": "7.3.2",
|
||||
"socket.io": "2.3.0",
|
||||
"tlds": "1.207.0",
|
||||
"ua-parser-js": "0.7.21",
|
||||
"uuid": "7.0.3",
|
||||
"web-push": "3.4.3",
|
||||
"yarn": "1.22.4"
|
||||
"semver": "7.3.4",
|
||||
"socket.io": "3.1.2",
|
||||
"tlds": "1.216.0",
|
||||
"ua-parser-js": "0.7.23",
|
||||
"uuid": "8.3.2",
|
||||
"web-push": "3.4.4",
|
||||
"yarn": "1.22.10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sqlite3": "4.1.1"
|
||||
"sqlite3": "5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.9.0",
|
||||
"@babel/preset-env": "7.9.5",
|
||||
"@fortawesome/fontawesome-free": "5.13.0",
|
||||
"@vue/server-test-utils": "1.0.0-beta.33",
|
||||
"@vue/test-utils": "1.0.0-beta.33",
|
||||
"babel-loader": "8.1.0",
|
||||
"@babel/core": "7.13.14",
|
||||
"@babel/preset-env": "7.13.12",
|
||||
"@fortawesome/fontawesome-free": "5.15.3",
|
||||
"@vue/server-test-utils": "1.1.3",
|
||||
"@vue/test-utils": "1.1.3",
|
||||
"babel-loader": "8.2.2",
|
||||
"babel-plugin-istanbul": "6.0.0",
|
||||
"chai": "4.2.0",
|
||||
"copy-webpack-plugin": "5.1.1",
|
||||
"css-loader": "3.5.2",
|
||||
"chai": "4.3.4",
|
||||
"copy-webpack-plugin": "7.0.0",
|
||||
"css-loader": "5.1.1",
|
||||
"cssnano": "4.1.10",
|
||||
"dayjs": "1.8.24",
|
||||
"emoji-regex": "9.0.0",
|
||||
"eslint": "6.8.0",
|
||||
"eslint-config-prettier": "6.10.1",
|
||||
"eslint-plugin-vue": "6.2.2",
|
||||
"dayjs": "1.10.4",
|
||||
"emoji-regex": "9.2.1",
|
||||
"eslint": "7.23.0",
|
||||
"eslint-config-prettier": "6.15.0",
|
||||
"eslint-plugin-vue": "7.5.0",
|
||||
"fuzzy": "0.1.3",
|
||||
"graphql-request": "1.8.2",
|
||||
"husky": "4.2.5",
|
||||
"mini-css-extract-plugin": "0.9.0",
|
||||
"mocha": "7.1.1",
|
||||
"husky": "4.3.5",
|
||||
"mini-css-extract-plugin": "1.3.6",
|
||||
"mocha": "8.2.1",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"npm-run-all": "4.1.5",
|
||||
"nyc": "15.0.1",
|
||||
"postcss-import": "12.0.1",
|
||||
"postcss-loader": "3.0.0",
|
||||
"nyc": "15.1.0",
|
||||
"postcss": "8.2.5",
|
||||
"postcss-import": "14.0.0",
|
||||
"postcss-loader": "5.0.0",
|
||||
"postcss-preset-env": "6.7.0",
|
||||
"prettier": "2.0.4",
|
||||
"pretty-quick": "2.0.1",
|
||||
"prettier": "2.2.1",
|
||||
"pretty-quick": "3.1.0",
|
||||
"primer-tooltips": "2.0.0",
|
||||
"sinon": "9.0.2",
|
||||
"socket.io-client": "2.3.0",
|
||||
"stylelint": "13.3.2",
|
||||
"sinon": "9.2.4",
|
||||
"socket.io-client": "3.1.1",
|
||||
"stylelint": "13.9.0",
|
||||
"stylelint-config-standard": "20.0.0",
|
||||
"textcomplete": "0.18.1",
|
||||
"textcomplete": "0.18.2",
|
||||
"undate": "0.3.0",
|
||||
"vue": "2.6.11",
|
||||
"vue-loader": "15.9.1",
|
||||
"vue-router": "3.1.6",
|
||||
"vue-server-renderer": "2.6.11",
|
||||
"vue-template-compiler": "2.6.11",
|
||||
"vuedraggable": "2.23.2",
|
||||
"vuex": "3.2.0",
|
||||
"webpack": "4.42.1",
|
||||
"webpack-cli": "3.3.11",
|
||||
"webpack-dev-middleware": "3.7.2",
|
||||
"vue": "2.6.12",
|
||||
"vue-loader": "15.9.6",
|
||||
"vue-router": "3.5.1",
|
||||
"vue-server-renderer": "2.6.12",
|
||||
"vue-template-compiler": "2.6.12",
|
||||
"vuedraggable": "2.24.3",
|
||||
"vuex": "3.6.2",
|
||||
"webpack": "5.21.2",
|
||||
"webpack-cli": "4.5.0",
|
||||
"webpack-dev-middleware": "4.1.0",
|
||||
"webpack-hot-middleware": "2.25.0"
|
||||
},
|
||||
"husky": {
|
||||
|
@ -10,8 +10,8 @@
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"depTypeList": ["devDependencies"],
|
||||
"extends": ["schedule:weekends"]
|
||||
"depTypeList": ["dependencies", "devDependencies"],
|
||||
"extends": ["schedule:monthly"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ const _ = require("lodash");
|
||||
const colors = require("chalk");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const GraphQLClient = require("graphql-request").GraphQLClient;
|
||||
const got = require("got");
|
||||
const dayjs = require("dayjs");
|
||||
const semver = require("semver");
|
||||
const util = require("util");
|
||||
@ -236,19 +236,31 @@ function fullChangelogUrl(v1, v2) {
|
||||
// This class is a facade to fetching details about commits / PRs / tags / etc.
|
||||
// for a given repository of our organization.
|
||||
class RepositoryFetcher {
|
||||
// Holds a GraphQLClient and the name of the repository within the
|
||||
// organization https://github.com/thelounge.
|
||||
constructor(graphqlClient, repositoryName) {
|
||||
this.graphqlClient = graphqlClient;
|
||||
// Holds a Github token and repository name
|
||||
constructor(githubToken, repositoryName) {
|
||||
this.githubToken = githubToken;
|
||||
this.repositoryName = repositoryName;
|
||||
}
|
||||
|
||||
// Base function that actually makes the GraphQL API call
|
||||
async fetch(query, variables = {}) {
|
||||
return this.graphqlClient.request(
|
||||
query,
|
||||
Object.assign(variables, {repositoryName: this.repositoryName})
|
||||
);
|
||||
const response = await got
|
||||
.post("https://api.github.com/graphql", {
|
||||
json: {
|
||||
query: query,
|
||||
variables: Object.assign(variables, {repositoryName: this.repositoryName}),
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.githubToken}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
|
||||
if (!response.errors && response.data) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
throw new Error(`GraphQL request returned no data: ${JSON.stringify(response)}`);
|
||||
}
|
||||
|
||||
// Returns the git commit that is attached to a given tag
|
||||
@ -789,12 +801,6 @@ function extractContributors(entries) {
|
||||
return Array.from(set).sort((a, b) => a.localeCompare(b, "en", {sensitivity: "base"}));
|
||||
}
|
||||
|
||||
const client = new GraphQLClient("https://api.github.com/graphql", {
|
||||
headers: {
|
||||
Authorization: `bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Main function. Given a version string (i.e. not a tag!), returns a changelog
|
||||
// entry and the list of contributors, for both pre-releases and stable
|
||||
// releases. Templates are located at the top of this file.
|
||||
@ -803,7 +809,7 @@ async function generateChangelogEntry(changelog, targetVersion) {
|
||||
let template;
|
||||
let contributors = [];
|
||||
|
||||
const codeRepo = new RepositoryFetcher(client, "thelounge");
|
||||
const codeRepo = new RepositoryFetcher(token, "thelounge");
|
||||
const previousVersion = await codeRepo.fetchPreviousVersion(targetVersion);
|
||||
|
||||
if (isPrerelease(targetVersion)) {
|
||||
@ -817,7 +823,7 @@ async function generateChangelogEntry(changelog, targetVersion) {
|
||||
items = parse(codeCommitsAndPullRequests);
|
||||
items.milestone = await codeRepo.fetchMilestone(targetVersion);
|
||||
|
||||
const websiteRepo = new RepositoryFetcher(client, "thelounge.github.io");
|
||||
const websiteRepo = new RepositoryFetcher(token, "thelounge.github.io");
|
||||
const previousWebsiteVersion = await websiteRepo.fetchPreviousVersion(targetVersion);
|
||||
const websiteCommitsAndPullRequests = await websiteRepo.fetchCommitsAndPullRequestsSince(
|
||||
"v" + previousWebsiteVersion
|
||||
|
@ -19,7 +19,14 @@ const emojiModifiersRegex = /[\u{1f3fb}-\u{1f3ff}]|\u{fe0f}/gu;
|
||||
const cleanEmoji = emoji.emoji.replace(emojiModifiersRegex, "");
|
||||
fullNameEmojiMap[cleanEmoji] = emoji.description;
|
||||
|
||||
for (const alias of emoji.aliases) {
|
||||
for (let alias of emoji.aliases) {
|
||||
if (alias !== "-1") {
|
||||
// Replace dashes to underscores except for :-1:
|
||||
// This removes autocompletion prompt for :-P
|
||||
// prompting for :non-potable_water:
|
||||
alias = alias.replace(/-/g, "_");
|
||||
}
|
||||
|
||||
emojiMap[alias] = emoji.emoji;
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ const events = [
|
||||
"ctcp",
|
||||
"chghost",
|
||||
"error",
|
||||
"help",
|
||||
"info",
|
||||
"invite",
|
||||
"join",
|
||||
"kick",
|
||||
@ -60,6 +62,7 @@ function Client(manager, name, config = {}) {
|
||||
manager: manager,
|
||||
messageStorage: [],
|
||||
highlightRegex: null,
|
||||
highlightExceptionRegex: null,
|
||||
});
|
||||
|
||||
const client = this;
|
||||
@ -81,15 +84,15 @@ function Client(manager, name, config = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof client.config.sessions !== "object") {
|
||||
if (!_.isPlainObject(client.config.sessions)) {
|
||||
client.config.sessions = {};
|
||||
}
|
||||
|
||||
if (typeof client.config.clientSettings !== "object") {
|
||||
if (!_.isPlainObject(client.config.clientSettings)) {
|
||||
client.config.clientSettings = {};
|
||||
}
|
||||
|
||||
if (typeof client.config.browser !== "object") {
|
||||
if (!_.isPlainObject(client.config.browser)) {
|
||||
client.config.browser = {};
|
||||
}
|
||||
|
||||
@ -238,6 +241,7 @@ Client.prototype.connect = function (args, isStartup = false) {
|
||||
nick: String(args.nick || ""),
|
||||
username: String(args.username || ""),
|
||||
realname: String(args.realname || ""),
|
||||
leaveMessage: String(args.leaveMessage || ""),
|
||||
sasl: String(args.sasl || ""),
|
||||
saslAccount: String(args.saslAccount || ""),
|
||||
saslPassword: String(args.saslPassword || ""),
|
||||
@ -422,30 +426,32 @@ Client.prototype.inputLine = function (data) {
|
||||
};
|
||||
|
||||
Client.prototype.compileCustomHighlights = function () {
|
||||
const client = this;
|
||||
this.highlightRegex = compileHighlightRegex(this.config.clientSettings.highlights);
|
||||
this.highlightExceptionRegex = compileHighlightRegex(
|
||||
this.config.clientSettings.highlightExceptions
|
||||
);
|
||||
};
|
||||
|
||||
if (typeof client.config.clientSettings.highlights !== "string") {
|
||||
client.highlightRegex = null;
|
||||
return;
|
||||
function compileHighlightRegex(customHighlightString) {
|
||||
if (typeof customHighlightString !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure we don't have empty string in the list of highlights
|
||||
// otherwise, users get notifications for everything
|
||||
const highlightsTokens = client.config.clientSettings.highlights
|
||||
// Ensure we don't have empty strings in the list of highlights
|
||||
const highlightsTokens = customHighlightString
|
||||
.split(",")
|
||||
.map((highlight) => escapeRegExp(highlight.trim()))
|
||||
.filter((highlight) => highlight.length > 0);
|
||||
|
||||
if (highlightsTokens.length === 0) {
|
||||
client.highlightRegex = null;
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
client.highlightRegex = new RegExp(
|
||||
return new RegExp(
|
||||
`(?:^|[ .,+!?|/:<>(){}'"@&~-])(?:${highlightsTokens.join("|")})(?:$|[ .,+!?|/:<>(){}'"-])`,
|
||||
"i"
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Client.prototype.more = function (data) {
|
||||
const client = this;
|
||||
@ -632,11 +638,11 @@ Client.prototype.names = function (data) {
|
||||
|
||||
Client.prototype.quit = function (signOut) {
|
||||
const sockets = this.manager.sockets.sockets;
|
||||
const room = sockets.adapter.rooms[this.id];
|
||||
const room = sockets.adapter.rooms.get(this.id);
|
||||
|
||||
if (room && room.sockets) {
|
||||
for (const user in room.sockets) {
|
||||
const socket = sockets.connected[user];
|
||||
if (room) {
|
||||
for (const user of room) {
|
||||
const socket = sockets.sockets.get(user);
|
||||
|
||||
if (socket) {
|
||||
if (signOut) {
|
||||
@ -649,7 +655,7 @@ Client.prototype.quit = function (signOut) {
|
||||
}
|
||||
|
||||
this.networks.forEach((network) => {
|
||||
network.quit(Helper.config.leaveMessage);
|
||||
network.quit();
|
||||
network.destroy();
|
||||
});
|
||||
|
||||
|
@ -34,18 +34,40 @@ ClientManager.prototype.init = function (identHandler, sockets) {
|
||||
};
|
||||
|
||||
ClientManager.prototype.findClient = function (name) {
|
||||
return this.clients.find((u) => u.name === name);
|
||||
name = name.toLowerCase();
|
||||
return this.clients.find((u) => u.name.toLowerCase() === name);
|
||||
};
|
||||
|
||||
ClientManager.prototype.loadUsers = function () {
|
||||
const users = this.getUsers();
|
||||
let users = this.getUsers();
|
||||
|
||||
if (users.length === 0) {
|
||||
log.info(
|
||||
`There are currently no users. Create one with ${colors.bold("thelounge add <name>")}.`
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const alreadySeenUsers = new Set();
|
||||
users = users.filter((user) => {
|
||||
user = user.toLowerCase();
|
||||
|
||||
if (alreadySeenUsers.has(user)) {
|
||||
log.error(
|
||||
`There is more than one user named "${colors.bold(
|
||||
user
|
||||
)}". Usernames are now case insensitive, duplicate users will not load.`
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
alreadySeenUsers.add(user);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// This callback is used by Auth plugins to load users they deem acceptable
|
||||
const callbackLoadUser = (user) => {
|
||||
this.loadUser(user);
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
const log = require("../log");
|
||||
const colors = require("chalk");
|
||||
const semver = require("semver");
|
||||
const program = require("commander");
|
||||
const Helper = require("../helper");
|
||||
const Utils = require("./utils");
|
||||
@ -40,6 +41,21 @@ program
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
json.thelounge.supports &&
|
||||
!semver.satisfies(Helper.getVersionNumber(), json.thelounge.supports)
|
||||
) {
|
||||
log.error(
|
||||
`${colors.red(
|
||||
json.name + " v" + json.version
|
||||
)} does not support The Lounge v${Helper.getVersionNumber()}. Supported version(s): ${
|
||||
json.thelounge.supports
|
||||
}`
|
||||
);
|
||||
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
log.info(`Installing ${colors.green(json.name + " v" + json.version)}...`);
|
||||
|
||||
return Utils.executeYarnCommand("add", "--exact", `${json.name}@${json.version}`)
|
||||
|
@ -11,7 +11,9 @@ program
|
||||
.command("add <name>")
|
||||
.description("Add a new user")
|
||||
.on("--help", Utils.extraHelp)
|
||||
.action(function (name) {
|
||||
.option("--password [password]", "new password, will be prompted if not specified")
|
||||
.option("--save-logs", "if password is specified, this enables saving logs to disk")
|
||||
.action(function (name, cmdObj) {
|
||||
if (!fs.existsSync(Helper.getUsersPath())) {
|
||||
log.error(`${Helper.getUsersPath()} does not exist.`);
|
||||
return;
|
||||
@ -31,6 +33,11 @@ program
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmdObj.password) {
|
||||
add(manager, name, cmdObj.password, !!cmdObj.saveLogs);
|
||||
return;
|
||||
}
|
||||
|
||||
log.prompt(
|
||||
{
|
||||
text: "Enter password:",
|
||||
|
@ -11,7 +11,8 @@ program
|
||||
.command("reset <name>")
|
||||
.description("Reset user password")
|
||||
.on("--help", Utils.extraHelp)
|
||||
.action(function (name) {
|
||||
.option("--password [password]", "new password, will be prompted if not specified")
|
||||
.action(function (name, cmdObj) {
|
||||
if (!fs.existsSync(Helper.getUsersPath())) {
|
||||
log.error(`${Helper.getUsersPath()} does not exist.`);
|
||||
return;
|
||||
@ -30,9 +31,10 @@ program
|
||||
return;
|
||||
}
|
||||
|
||||
const pathReal = Helper.getUserConfigPath(name);
|
||||
const pathTemp = pathReal + ".tmp";
|
||||
const user = JSON.parse(fs.readFileSync(pathReal, "utf-8"));
|
||||
if (cmdObj.password) {
|
||||
change(name, cmdObj.password);
|
||||
return;
|
||||
}
|
||||
|
||||
log.prompt(
|
||||
{
|
||||
@ -44,6 +46,16 @@ program
|
||||
return;
|
||||
}
|
||||
|
||||
change(name, password);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
function change(name, password) {
|
||||
const pathReal = Helper.getUserConfigPath(name);
|
||||
const pathTemp = pathReal + ".tmp";
|
||||
const user = JSON.parse(fs.readFileSync(pathReal, "utf-8"));
|
||||
|
||||
user.password = Helper.password.hash(password);
|
||||
user.sessions = {};
|
||||
|
||||
@ -55,6 +67,4 @@ program
|
||||
fs.renameSync(pathTemp, pathReal);
|
||||
|
||||
log.info(`Successfully reset password for ${colors.bold(name)}.`);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ const Helper = {
|
||||
setHome,
|
||||
getVersion,
|
||||
getVersionCacheBust,
|
||||
getVersionNumber,
|
||||
getGitCommit,
|
||||
ip2hex,
|
||||
mergeConfig,
|
||||
@ -60,6 +61,10 @@ function getVersion() {
|
||||
return gitCommit ? `source (${gitCommit} / ${version})` : version;
|
||||
}
|
||||
|
||||
function getVersionNumber() {
|
||||
return pkg.version;
|
||||
}
|
||||
|
||||
let _gitCommit;
|
||||
|
||||
function getGitCommit() {
|
||||
|
@ -43,7 +43,7 @@ class Msg {
|
||||
}
|
||||
|
||||
return (
|
||||
this.type !== Msg.Type.MOTD &&
|
||||
this.type !== Msg.Type.MONOSPACE_BLOCK &&
|
||||
this.type !== Msg.Type.ERROR &&
|
||||
this.type !== Msg.Type.TOPIC_SET_BY &&
|
||||
this.type !== Msg.Type.MODE_CHANNEL &&
|
||||
@ -66,7 +66,7 @@ Msg.Type = {
|
||||
MESSAGE: "message",
|
||||
MODE: "mode",
|
||||
MODE_CHANNEL: "mode_channel",
|
||||
MOTD: "motd",
|
||||
MONOSPACE_BLOCK: "monospace_block",
|
||||
NICK: "nick",
|
||||
NOTICE: "notice",
|
||||
PART: "part",
|
||||
|
@ -35,6 +35,7 @@ function Network(attr) {
|
||||
commands: [],
|
||||
username: "",
|
||||
realname: "",
|
||||
leaveMessage: "",
|
||||
sasl: "",
|
||||
saslAccount: "",
|
||||
saslPassword: "",
|
||||
@ -82,6 +83,7 @@ Network.prototype.validate = function (client) {
|
||||
|
||||
this.username = cleanString(this.username) || "thelounge";
|
||||
this.realname = cleanString(this.realname) || "The Lounge User";
|
||||
this.leaveMessage = cleanString(this.leaveMessage);
|
||||
this.password = cleanString(this.password);
|
||||
this.host = cleanString(this.host).toLowerCase();
|
||||
this.name = cleanString(this.name);
|
||||
@ -120,7 +122,12 @@ Network.prototype.validate = function (client) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Helper.config.public) {
|
||||
this.name = Helper.config.defaults.name;
|
||||
// Sync lobby channel name
|
||||
this.channels[0].name = Helper.config.defaults.name;
|
||||
}
|
||||
|
||||
this.host = Helper.config.defaults.host;
|
||||
this.port = Helper.config.defaults.port;
|
||||
this.tls = Helper.config.defaults.tls;
|
||||
@ -168,8 +175,10 @@ Network.prototype.createIrcFramework = function (client) {
|
||||
enable_echomessage: true,
|
||||
enable_setname: true,
|
||||
auto_reconnect: true,
|
||||
auto_reconnect_wait: 10000 + Math.floor(Math.random() * 1000), // If multiple users are connected to the same network, randomize their reconnections a little
|
||||
auto_reconnect_max_retries: 360, // At least one hour (plus timeouts) worth of reconnections
|
||||
|
||||
// Exponential backoff maxes out at 300 seconds after 9 reconnects,
|
||||
// it will keep trying for well over an hour (plus the timeouts)
|
||||
auto_reconnect_max_retries: 30,
|
||||
});
|
||||
|
||||
this.setIrcFrameworkOptions(client);
|
||||
@ -197,8 +206,7 @@ Network.prototype.setIrcFrameworkOptions = function (client) {
|
||||
this.irc.options.tls = this.tls;
|
||||
this.irc.options.rejectUnauthorized = this.rejectUnauthorized;
|
||||
this.irc.options.webirc = this.createWebIrc(client);
|
||||
|
||||
this.irc.options.client_certificate = this.tls ? ClientCertificate.get(this.uuid) : null;
|
||||
this.irc.options.client_certificate = null;
|
||||
|
||||
if (!this.sasl) {
|
||||
delete this.irc.options.sasl_mechanism;
|
||||
@ -206,6 +214,7 @@ Network.prototype.setIrcFrameworkOptions = function (client) {
|
||||
} else if (this.sasl === "external") {
|
||||
this.irc.options.sasl_mechanism = "EXTERNAL";
|
||||
this.irc.options.account = {};
|
||||
this.irc.options.client_certificate = ClientCertificate.get(this.uuid);
|
||||
} else if (this.sasl === "plain") {
|
||||
delete this.irc.options.sasl_mechanism;
|
||||
this.irc.options.account = {
|
||||
@ -246,6 +255,7 @@ Network.prototype.createWebIrc = function (client) {
|
||||
};
|
||||
|
||||
Network.prototype.edit = function (client, args) {
|
||||
const oldNetworkName = this.name;
|
||||
const oldNick = this.nick;
|
||||
const oldRealname = this.realname;
|
||||
|
||||
@ -259,6 +269,7 @@ Network.prototype.edit = function (client, args) {
|
||||
this.password = String(args.password || "");
|
||||
this.username = String(args.username || "");
|
||||
this.realname = String(args.realname || "");
|
||||
this.leaveMessage = String(args.leaveMessage || "");
|
||||
this.sasl = String(args.sasl || "");
|
||||
this.saslAccount = String(args.saslAccount || "");
|
||||
this.saslPassword = String(args.saslPassword || "");
|
||||
@ -272,6 +283,14 @@ Network.prototype.edit = function (client, args) {
|
||||
// Sync lobby channel name
|
||||
this.channels[0].name = this.name;
|
||||
|
||||
if (this.name !== oldNetworkName) {
|
||||
// Send updated network name to all connected clients
|
||||
client.emit("network:name", {
|
||||
uuid: this.uuid,
|
||||
name: this.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.validate(client)) {
|
||||
return;
|
||||
}
|
||||
@ -420,7 +439,7 @@ Network.prototype.quit = function (quitMessage) {
|
||||
// https://ircv3.net/specs/extensions/sts#rescheduling-expiry-on-disconnect
|
||||
STSPolicies.refreshExpiration(this.host);
|
||||
|
||||
this.irc.quit(quitMessage || Helper.config.leaveMessage);
|
||||
this.irc.quit(quitMessage || this.leaveMessage || Helper.config.leaveMessage);
|
||||
};
|
||||
|
||||
Network.prototype.exportForEdit = function () {
|
||||
@ -431,6 +450,7 @@ Network.prototype.exportForEdit = function () {
|
||||
"password",
|
||||
"username",
|
||||
"realname",
|
||||
"leaveMessage",
|
||||
"sasl",
|
||||
"saslAccount",
|
||||
"saslPassword",
|
||||
@ -465,6 +485,7 @@ Network.prototype.export = function () {
|
||||
"password",
|
||||
"username",
|
||||
"realname",
|
||||
"leaveMessage",
|
||||
"sasl",
|
||||
"saslAccount",
|
||||
"saslPassword",
|
||||
|
@ -8,25 +8,28 @@ function User(attr, prefixLookup) {
|
||||
_.defaults(this, attr, {
|
||||
modes: [],
|
||||
away: "",
|
||||
mode: "",
|
||||
nick: "",
|
||||
lastMessage: 0,
|
||||
});
|
||||
|
||||
Object.defineProperty(this, "mode", {
|
||||
get() {
|
||||
return this.modes[0] || "";
|
||||
},
|
||||
});
|
||||
|
||||
this.setModes(this.modes, prefixLookup);
|
||||
}
|
||||
|
||||
User.prototype.setModes = function (modes, prefixLookup) {
|
||||
// irc-framework sets character mode, but The Lounge works with symbols
|
||||
this.modes = modes.map((mode) => prefixLookup[mode]);
|
||||
|
||||
this.mode = this.modes[0] || "";
|
||||
};
|
||||
|
||||
User.prototype.toJSON = function () {
|
||||
return {
|
||||
nick: this.nick,
|
||||
mode: this.mode,
|
||||
modes: this.modes,
|
||||
lastMessage: this.lastMessage,
|
||||
};
|
||||
};
|
||||
|
@ -13,19 +13,6 @@ module.exports = (app) => {
|
||||
"webpack-hot-middleware/client?path=storage/__webpack_hmr"
|
||||
);
|
||||
|
||||
// Enable hot module reload support in mini-css-extract-plugin
|
||||
for (const rule of webpackConfig.module.rules) {
|
||||
if (!Array.isArray(rule.use)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const use of rule.use) {
|
||||
if (use.options && typeof use.options.hmr !== "undefined") {
|
||||
use.options.hmr = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const compiler = webpack(webpackConfig);
|
||||
|
||||
app.use(
|
||||
|
@ -48,7 +48,7 @@ exports.input = function (network, chan, cmd, args) {
|
||||
});
|
||||
this.save();
|
||||
} else {
|
||||
const partMessage = args.join(" ") || Helper.config.leaveMessage;
|
||||
const partMessage = args.join(" ") || network.leaveMessage || Helper.config.leaveMessage;
|
||||
network.irc.part(target.name, partMessage);
|
||||
}
|
||||
|
||||
|
@ -178,14 +178,9 @@ module.exports = function (irc, network) {
|
||||
network.channels[0].pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
text:
|
||||
"Disconnected from the network. Reconnecting in " +
|
||||
Math.round(data.wait / 1000) +
|
||||
" seconds… (Attempt " +
|
||||
data.attempt +
|
||||
" of " +
|
||||
data.max_retries +
|
||||
")",
|
||||
text: `Disconnected from the network. Reconnecting in ${Math.round(
|
||||
data.wait / 1000
|
||||
)} seconds… (Attempt ${data.attempt})`,
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
@ -8,7 +8,7 @@ const pkg = require("../../../package.json");
|
||||
|
||||
const ctcpResponses = {
|
||||
CLIENTINFO: () =>
|
||||
Object.getOwnPropertyNames(ctcpResponses) // TODO: This is currently handled by irc-framework
|
||||
Object.getOwnPropertyNames(ctcpResponses)
|
||||
.filter((key) => key !== "CLIENTINFO" && typeof ctcpResponses[key] === "function")
|
||||
.join(" "),
|
||||
PING: ({message}) => message.substring(5),
|
||||
@ -67,17 +67,18 @@ module.exports = function (irc, network) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = data.from_server ? data.hostname : data.nick;
|
||||
const response = ctcpResponses[data.type];
|
||||
|
||||
if (response) {
|
||||
irc.ctcpResponse(data.nick, data.type, response(data));
|
||||
irc.ctcpResponse(target, data.type, response(data));
|
||||
}
|
||||
|
||||
// Let user know someone is making a CTCP request against their nick
|
||||
const msg = new Msg({
|
||||
type: Msg.Type.CTCP_REQUEST,
|
||||
time: data.time,
|
||||
from: new User({nick: data.nick}),
|
||||
from: new User({nick: target}),
|
||||
hostmask: data.ident + "@" + data.hostname,
|
||||
ctcpMessage: data.message,
|
||||
});
|
||||
|
20
src/plugins/irc-events/help.js
Normal file
20
src/plugins/irc-events/help.js
Normal file
@ -0,0 +1,20 @@
|
||||
"use strict";
|
||||
|
||||
const Msg = require("../../models/msg");
|
||||
|
||||
module.exports = function (irc, network) {
|
||||
const client = this;
|
||||
|
||||
irc.on("help", function (data) {
|
||||
const lobby = network.channels[0];
|
||||
|
||||
if (data.help) {
|
||||
const msg = new Msg({
|
||||
type: Msg.Type.MONOSPACE_BLOCK,
|
||||
command: "help",
|
||||
text: data.help,
|
||||
});
|
||||
lobby.pushMessage(client, msg, true);
|
||||
}
|
||||
});
|
||||
};
|
20
src/plugins/irc-events/info.js
Normal file
20
src/plugins/irc-events/info.js
Normal file
@ -0,0 +1,20 @@
|
||||
"use strict";
|
||||
|
||||
const Msg = require("../../models/msg");
|
||||
|
||||
module.exports = function (irc, network) {
|
||||
const client = this;
|
||||
|
||||
irc.on("info", function (data) {
|
||||
const lobby = network.channels[0];
|
||||
|
||||
if (data.info) {
|
||||
const msg = new Msg({
|
||||
type: Msg.Type.MONOSPACE_BLOCK,
|
||||
command: "info",
|
||||
text: data.info,
|
||||
});
|
||||
lobby.pushMessage(client, msg, true);
|
||||
}
|
||||
});
|
||||
};
|
@ -41,6 +41,8 @@ module.exports = function (irc, network) {
|
||||
time: data.time,
|
||||
from: user,
|
||||
hostmask: data.ident + "@" + data.hostname,
|
||||
gecos: data.gecos,
|
||||
account: data.account,
|
||||
type: Msg.Type.JOIN,
|
||||
self: data.nick === irc.user.nick,
|
||||
});
|
||||
|
@ -5,22 +5,18 @@ const got = require("got");
|
||||
const URL = require("url").URL;
|
||||
const mime = require("mime-types");
|
||||
const Helper = require("../../helper");
|
||||
const cleanIrcMessage = require("../../../client/js/helpers/ircmessageparser/cleanIrcMessage");
|
||||
const findLinks = require("../../../client/js/helpers/ircmessageparser/findLinks");
|
||||
const {findLinksWithSchema} = require("../../../client/js/helpers/ircmessageparser/findLinks");
|
||||
const storage = require("../storage");
|
||||
const currentFetchPromises = new Map();
|
||||
const imageTypeRegex = /^image\/.+/;
|
||||
const mediaTypeRegex = /^(audio|video)\/.+/;
|
||||
|
||||
module.exports = function (client, chan, msg) {
|
||||
module.exports = function (client, chan, msg, cleanText) {
|
||||
if (!Helper.config.prefetch) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all IRC formatting characters before searching for links
|
||||
const cleanText = cleanIrcMessage(msg.text);
|
||||
|
||||
msg.previews = findLinks(cleanText).reduce((cleanLinks, link) => {
|
||||
msg.previews = findLinksWithSchema(cleanText).reduce((cleanLinks, link) => {
|
||||
const url = normalizeURL(link.link);
|
||||
|
||||
// If the URL is invalid and cannot be normalized, don't fetch it
|
||||
@ -84,11 +80,6 @@ function parseHtml(preview, res, client) {
|
||||
$('meta[property="og:description"]').attr("content") ||
|
||||
$('meta[name="description"]').attr("content") ||
|
||||
"";
|
||||
let thumb =
|
||||
$('meta[property="og:image"]').attr("content") ||
|
||||
$('meta[name="twitter:image:src"]').attr("content") ||
|
||||
$('link[rel="image_src"]').attr("href") ||
|
||||
"";
|
||||
|
||||
if (preview.head.length) {
|
||||
preview.head = preview.head.substr(0, 100);
|
||||
@ -98,6 +89,17 @@ function parseHtml(preview, res, client) {
|
||||
preview.body = preview.body.substr(0, 300);
|
||||
}
|
||||
|
||||
if (!Helper.config.prefetchStorage && Helper.config.disableMediaPreview) {
|
||||
resolve(res);
|
||||
return;
|
||||
}
|
||||
|
||||
let thumb =
|
||||
$('meta[property="og:image"]').attr("content") ||
|
||||
$('meta[name="twitter:image:src"]').attr("content") ||
|
||||
$('link[rel="image_src"]').attr("href") ||
|
||||
"";
|
||||
|
||||
// Make sure thumbnail is a valid and absolute url
|
||||
if (thumb.length) {
|
||||
thumb = normalizeURL(thumb, preview.link) || "";
|
||||
@ -127,7 +129,25 @@ function parseHtml(preview, res, client) {
|
||||
|
||||
function parseHtmlMedia($, preview, client) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (Helper.config.disableMediaPreview) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
let foundMedia = false;
|
||||
const openGraphType = $('meta[property="og:type"]').attr("content");
|
||||
|
||||
// Certain news websites may include video and audio tags,
|
||||
// despite actually being an article (as indicated by og:type).
|
||||
// If there is og:type tag, we will only select video or audio if it matches
|
||||
if (
|
||||
openGraphType &&
|
||||
!openGraphType.startsWith("video") &&
|
||||
!openGraphType.startsWith("music")
|
||||
) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
["video", "audio"].forEach((type) => {
|
||||
if (foundMedia) {
|
||||
@ -203,6 +223,11 @@ function parse(msg, chan, preview, res, client) {
|
||||
case "image/jpg":
|
||||
case "image/jpeg":
|
||||
case "image/webp":
|
||||
case "image/avif":
|
||||
if (!Helper.config.prefetchStorage && Helper.config.disableMediaPreview) {
|
||||
return removePreview(msg, preview);
|
||||
}
|
||||
|
||||
if (res.size > Helper.config.prefetchMaxImageSize * 1024) {
|
||||
preview.type = "error";
|
||||
preview.error = "image-too-big";
|
||||
@ -228,6 +253,10 @@ function parse(msg, chan, preview, res, client) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (Helper.config.disableMediaPreview) {
|
||||
return removePreview(msg, preview);
|
||||
}
|
||||
|
||||
preview.type = "audio";
|
||||
preview.media = preview.link;
|
||||
preview.mediaType = res.type;
|
||||
@ -241,6 +270,10 @@ function parse(msg, chan, preview, res, client) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (Helper.config.disableMediaPreview) {
|
||||
return removePreview(msg, preview);
|
||||
}
|
||||
|
||||
preview.type = "video";
|
||||
preview.media = preview.link;
|
||||
preview.mediaType = res.type;
|
||||
@ -354,7 +387,9 @@ function fetch(uri, headers) {
|
||||
retry: 0,
|
||||
timeout: 5000,
|
||||
headers: getRequestHeaders(headers),
|
||||
https: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
|
||||
gotStream
|
||||
@ -372,9 +407,14 @@ function fetch(uri, headers) {
|
||||
// We don't need to download the file any further after we received content-type header
|
||||
gotStream.destroy();
|
||||
} else {
|
||||
// if not image, limit download to 50kb, since we need only meta tags
|
||||
// twitter.com sends opengraph meta tags within ~20kb of data for individual tweets
|
||||
limit = 1024 * 50;
|
||||
// if not image, limit download to the max search size, since we need only meta tags
|
||||
// twitter.com sends opengraph meta tags within ~20kb of data for individual tweets, the default is set to 50.
|
||||
// for sites like Youtube the og tags are in the first 300K and hence this is configurable by the admin
|
||||
limit =
|
||||
"prefetchMaxSearchSize" in Helper.config
|
||||
? Helper.config.prefetchMaxSearchSize * 1024
|
||||
: // set to the previous size if config option is unset
|
||||
50 * 1024;
|
||||
}
|
||||
})
|
||||
.on("error", (e) => reject(e))
|
||||
|
@ -115,6 +115,9 @@ module.exports = function (irc, network) {
|
||||
msg.showInActive = true;
|
||||
}
|
||||
|
||||
// remove IRC formatting for custom highlight testing
|
||||
const cleanMessage = cleanIrcMessage(data.message);
|
||||
|
||||
// Self messages in channels are never highlighted
|
||||
// Non-self messages are highlighted as soon as the nick is detected
|
||||
if (!msg.highlight && !msg.self) {
|
||||
@ -122,10 +125,19 @@ module.exports = function (irc, network) {
|
||||
|
||||
// If we still don't have a highlight, test against custom highlights if there's any
|
||||
if (!msg.highlight && client.highlightRegex) {
|
||||
msg.highlight = client.highlightRegex.test(data.message);
|
||||
msg.highlight = client.highlightRegex.test(cleanMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// if highlight exceptions match, do not highlight at all
|
||||
if (msg.highlight && client.highlightExceptionRegex) {
|
||||
msg.highlight = !client.highlightExceptionRegex.test(cleanMessage);
|
||||
}
|
||||
|
||||
if (data.group) {
|
||||
msg.statusmsgGroup = data.group;
|
||||
}
|
||||
|
||||
let match;
|
||||
|
||||
while ((match = nickRegExp.exec(data.message))) {
|
||||
@ -136,7 +148,7 @@ module.exports = function (irc, network) {
|
||||
|
||||
// No prefetch URLs unless are simple MESSAGE or ACTION types
|
||||
if ([Msg.Type.MESSAGE, Msg.Type.ACTION].includes(data.type)) {
|
||||
LinkPrefetch(client, chan, msg);
|
||||
LinkPrefetch(client, chan, msg, cleanMessage);
|
||||
}
|
||||
|
||||
chan.pushMessage(client, msg, !msg.self);
|
||||
@ -144,7 +156,7 @@ module.exports = function (irc, network) {
|
||||
// Do not send notifications for messages older than 15 minutes (znc buffer for example)
|
||||
if (msg.highlight && (!data.time || data.time > Date.now() - 900000)) {
|
||||
let title = chan.name;
|
||||
let body = cleanIrcMessage(data.message);
|
||||
let body = cleanMessage;
|
||||
|
||||
if (msg.type === Msg.Type.ACTION) {
|
||||
// For actions, do not include colon in the message
|
||||
|
@ -60,17 +60,17 @@ module.exports = function (irc, network) {
|
||||
self: data.nick === irc.user.nick,
|
||||
});
|
||||
|
||||
for (const param of data.raw_params) {
|
||||
const users = [];
|
||||
|
||||
for (const param of data.raw_params) {
|
||||
if (targetChan.findUser(param)) {
|
||||
users.push(param);
|
||||
}
|
||||
}
|
||||
|
||||
if (users.length > 0) {
|
||||
msg.users = users;
|
||||
}
|
||||
}
|
||||
|
||||
targetChan.pushMessage(client, msg);
|
||||
|
||||
@ -117,9 +117,6 @@ module.exports = function (irc, network) {
|
||||
return userModeSortPriority[a] - userModeSortPriority[b];
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: remove in future
|
||||
user.mode = (user.modes && user.modes[0]) || "";
|
||||
});
|
||||
|
||||
if (!usersUpdated) {
|
||||
|
@ -10,7 +10,8 @@ module.exports = function (irc, network) {
|
||||
|
||||
if (data.motd) {
|
||||
const msg = new Msg({
|
||||
type: Msg.Type.MOTD,
|
||||
type: Msg.Type.MONOSPACE_BLOCK,
|
||||
command: "motd",
|
||||
text: data.motd,
|
||||
});
|
||||
lobby.pushMessage(client, msg);
|
||||
@ -18,7 +19,8 @@ module.exports = function (irc, network) {
|
||||
|
||||
if (data.error) {
|
||||
const msg = new Msg({
|
||||
type: Msg.Type.MOTD,
|
||||
type: Msg.Type.MONOSPACE_BLOCK,
|
||||
command: "motd",
|
||||
text: data.error,
|
||||
});
|
||||
lobby.pushMessage(client, msg);
|
||||
|
@ -183,8 +183,7 @@ class MessageStorage {
|
||||
}
|
||||
|
||||
resolve(
|
||||
rows
|
||||
.map((row) => {
|
||||
rows.reverse().map((row) => {
|
||||
const msg = JSON.parse(row.msg);
|
||||
msg.time = row.time;
|
||||
msg.type = row.type;
|
||||
@ -194,7 +193,6 @@ class MessageStorage {
|
||||
|
||||
return newMsg;
|
||||
})
|
||||
.reverse()
|
||||
);
|
||||
}
|
||||
)
|
||||
|
@ -4,6 +4,7 @@ const _ = require("lodash");
|
||||
const log = require("../../log");
|
||||
const colors = require("chalk");
|
||||
const path = require("path");
|
||||
const semver = require("semver");
|
||||
const Helper = require("../../helper");
|
||||
const themes = require("./themes");
|
||||
const packageMap = new Map();
|
||||
@ -93,6 +94,13 @@ function loadPackage(packageName) {
|
||||
throw "'thelounge' is not present in package.json";
|
||||
}
|
||||
|
||||
if (
|
||||
packageInfo.thelounge.supports &&
|
||||
!semver.satisfies(Helper.getVersionNumber(), packageInfo.thelounge.supports)
|
||||
) {
|
||||
throw `v${packageInfo.version} does not support this version of The Lounge. Supports: ${packageInfo.thelounge.supports}`;
|
||||
}
|
||||
|
||||
packageFile = require(packagePath);
|
||||
} catch (e) {
|
||||
log.error(`Package ${colors.bold(packageName)} could not be loaded: ${colors.red(e)}`);
|
||||
|
@ -10,23 +10,28 @@ const readChunk = require("read-chunk");
|
||||
const crypto = require("crypto");
|
||||
const isUtf8 = require("is-utf8");
|
||||
const log = require("../log");
|
||||
const contentDisposition = require("content-disposition");
|
||||
|
||||
const whitelist = [
|
||||
"application/ogg",
|
||||
"audio/midi",
|
||||
"audio/mpeg",
|
||||
"audio/ogg",
|
||||
"audio/vnd.wave",
|
||||
"image/bmp",
|
||||
"image/gif",
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"text/plain",
|
||||
"video/mp4",
|
||||
"video/ogg",
|
||||
"video/webm",
|
||||
];
|
||||
// Map of allowed mime types to their respecive default filenames
|
||||
// that will be rendered in browser without forcing them to be downloaded
|
||||
const inlineContentDispositionTypes = {
|
||||
"application/ogg": "media.ogx",
|
||||
"audio/midi": "audio.midi",
|
||||
"audio/mpeg": "audio.mp3",
|
||||
"audio/ogg": "audio.ogg",
|
||||
"audio/vnd.wave": "audio.wav",
|
||||
"audio/flac": "audio.flac",
|
||||
"image/bmp": "image.bmp",
|
||||
"image/gif": "image.gif",
|
||||
"image/jpeg": "image.jpg",
|
||||
"image/png": "image.png",
|
||||
"image/webp": "image.webp",
|
||||
"image/avif": "image.avif",
|
||||
"text/plain": "text.txt",
|
||||
"video/mp4": "video.mp4",
|
||||
"video/ogg": "video.ogv",
|
||||
"video/webm": "video.webm",
|
||||
};
|
||||
|
||||
const uploadTokens = new Map();
|
||||
|
||||
@ -35,17 +40,33 @@ class Uploader {
|
||||
socket.on("upload:auth", () => {
|
||||
const token = uuidv4();
|
||||
|
||||
uploadTokens.set(token, true);
|
||||
|
||||
socket.emit("upload:auth", token);
|
||||
|
||||
// Invalidate the token in one minute
|
||||
setTimeout(() => uploadTokens.delete(token), 60 * 1000);
|
||||
const timeout = Uploader.createTokenTimeout(token);
|
||||
|
||||
uploadTokens.set(token, timeout);
|
||||
});
|
||||
|
||||
socket.on("upload:ping", (token) => {
|
||||
if (typeof token !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
let timeout = uploadTokens.get(token);
|
||||
|
||||
if (!timeout) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(timeout);
|
||||
timeout = Uploader.createTokenTimeout(token);
|
||||
uploadTokens.set(token, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
static isValidType(mimeType) {
|
||||
return whitelist.includes(mimeType);
|
||||
static createTokenTimeout(token) {
|
||||
return setTimeout(() => uploadTokens.delete(token), 60 * 1000);
|
||||
}
|
||||
|
||||
static router(express) {
|
||||
@ -72,8 +93,21 @@ class Uploader {
|
||||
return res.status(404).send("Not found");
|
||||
}
|
||||
|
||||
// Force a download in the browser if it's not a whitelisted type (binary or otherwise unknown)
|
||||
const contentDisposition = Uploader.isValidType(detectedMimeType) ? "inline" : "attachment";
|
||||
// Force a download in the browser if it's not an allowed type (binary or otherwise unknown)
|
||||
let slug = req.params.slug;
|
||||
const isInline = detectedMimeType in inlineContentDispositionTypes;
|
||||
let disposition = isInline ? "inline" : "attachment";
|
||||
|
||||
if (!slug && isInline) {
|
||||
slug = inlineContentDispositionTypes[detectedMimeType];
|
||||
}
|
||||
|
||||
if (slug) {
|
||||
disposition = contentDisposition(slug.trim(), {
|
||||
fallback: false,
|
||||
type: disposition,
|
||||
});
|
||||
}
|
||||
|
||||
if (detectedMimeType === "audio/vnd.wave") {
|
||||
// Send a more common mime type for wave audio files
|
||||
@ -81,7 +115,7 @@ class Uploader {
|
||||
detectedMimeType = "audio/wav";
|
||||
}
|
||||
|
||||
res.setHeader("Content-Disposition", contentDisposition);
|
||||
res.setHeader("Content-Disposition", disposition);
|
||||
res.setHeader("Cache-Control", "max-age=86400");
|
||||
res.contentType(detectedMimeType);
|
||||
|
||||
|
@ -167,6 +167,7 @@ module.exports = function (options = {}) {
|
||||
cookie: false,
|
||||
serveClient: false,
|
||||
transports: Helper.config.transports,
|
||||
pingTimeout: 60000,
|
||||
});
|
||||
|
||||
sockets.on("connect", (socket) => {
|
||||
@ -363,13 +364,13 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
||||
});
|
||||
|
||||
socket.on("input", (data) => {
|
||||
if (typeof data === "object") {
|
||||
if (_.isPlainObject(data)) {
|
||||
client.input(data);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("more", (data) => {
|
||||
if (typeof data === "object") {
|
||||
if (_.isPlainObject(data)) {
|
||||
const history = client.more(data);
|
||||
|
||||
if (history !== null) {
|
||||
@ -379,7 +380,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
||||
});
|
||||
|
||||
socket.on("network:new", (data) => {
|
||||
if (typeof data === "object") {
|
||||
if (_.isPlainObject(data)) {
|
||||
// prevent people from overriding webirc settings
|
||||
data.uuid = null;
|
||||
data.commands = null;
|
||||
@ -404,7 +405,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
||||
});
|
||||
|
||||
socket.on("network:edit", (data) => {
|
||||
if (typeof data !== "object") {
|
||||
if (!_.isPlainObject(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -418,14 +419,14 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
||||
});
|
||||
|
||||
socket.on("history:clear", (data) => {
|
||||
if (typeof data === "object") {
|
||||
if (_.isPlainObject(data)) {
|
||||
client.clearHistory(data);
|
||||
}
|
||||
});
|
||||
|
||||
if (!Helper.config.public && !Helper.config.ldap.enable) {
|
||||
socket.on("change-password", (data) => {
|
||||
if (typeof data === "object") {
|
||||
if (_.isPlainObject(data)) {
|
||||
const old = data.old_password;
|
||||
const p1 = data.new_password;
|
||||
const p2 = data.verify_password;
|
||||
@ -475,13 +476,13 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
||||
});
|
||||
|
||||
socket.on("sort", (data) => {
|
||||
if (typeof data === "object") {
|
||||
if (_.isPlainObject(data)) {
|
||||
client.sort(data);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("names", (data) => {
|
||||
if (typeof data === "object") {
|
||||
if (_.isPlainObject(data)) {
|
||||
client.names(data);
|
||||
}
|
||||
});
|
||||
@ -496,7 +497,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
||||
});
|
||||
|
||||
socket.on("msg:preview:toggle", (data) => {
|
||||
if (typeof data !== "object") {
|
||||
if (!_.isPlainObject(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -546,7 +547,10 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
||||
client.mentions.findIndex((m) => m.msgId === msgId),
|
||||
1
|
||||
);
|
||||
// TODO: emit to other clients?
|
||||
});
|
||||
|
||||
socket.on("mentions:hide_all", () => {
|
||||
client.mentions = [];
|
||||
});
|
||||
|
||||
if (!Helper.config.public) {
|
||||
@ -594,7 +598,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
||||
|
||||
if (!Helper.config.public) {
|
||||
socket.on("setting:set", (newSetting) => {
|
||||
if (!newSetting || typeof newSetting !== "object") {
|
||||
if (!_.isPlainObject(newSetting)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -618,7 +622,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
||||
|
||||
client.save();
|
||||
|
||||
if (newSetting.name === "highlights") {
|
||||
if (newSetting.name === "highlights" || newSetting.name === "highlightExceptions") {
|
||||
client.compileCustomHighlights();
|
||||
} else if (newSetting.name === "awayMessage") {
|
||||
if (typeof newSetting.value !== "string") {
|
||||
@ -649,7 +653,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
||||
|
||||
socket.on("sign-out", (tokenToSignOut) => {
|
||||
// If no token provided, sign same client out
|
||||
if (!tokenToSignOut) {
|
||||
if (!tokenToSignOut || typeof tokenToSignOut !== "string") {
|
||||
tokenToSignOut = token;
|
||||
}
|
||||
|
||||
@ -666,7 +670,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const socketToRemove = manager.sockets.of("/").connected[socketId];
|
||||
const socketToRemove = manager.sockets.of("/").sockets.get(socketId);
|
||||
|
||||
socketToRemove.emit("sign-out");
|
||||
socketToRemove.disconnect();
|
||||
@ -755,7 +759,7 @@ function getServerConfiguration() {
|
||||
}
|
||||
|
||||
function performAuthentication(data) {
|
||||
if (typeof data !== "object") {
|
||||
if (!_.isPlainObject(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -810,6 +814,10 @@ function performAuthentication(data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data.user !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
const authCallback = (success) => {
|
||||
// Authorization failed
|
||||
if (!success) {
|
||||
|
@ -1,7 +1,10 @@
|
||||
"use strict";
|
||||
|
||||
const expect = require("chai").expect;
|
||||
const findLinks = require("../../../../../client/js/helpers/ircmessageparser/findLinks");
|
||||
const {
|
||||
findLinks,
|
||||
findLinksWithSchema,
|
||||
} = require("../../../../../client/js/helpers/ircmessageparser/findLinks");
|
||||
|
||||
describe("findLinks", () => {
|
||||
it("should find url", () => {
|
||||
@ -354,4 +357,24 @@ describe("findLinks", () => {
|
||||
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
|
||||
it("should not return urls with no schema if flag is specified", () => {
|
||||
const input = "https://example.global //example.com http://example.group example.py";
|
||||
const expected = [
|
||||
{
|
||||
link: "https://example.global",
|
||||
start: 0,
|
||||
end: 22,
|
||||
},
|
||||
{
|
||||
end: 57,
|
||||
link: "http://example.group",
|
||||
start: 37,
|
||||
},
|
||||
];
|
||||
|
||||
const actual = findLinksWithSchema(input);
|
||||
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
|
@ -40,6 +40,7 @@ describe("Network", function () {
|
||||
password: "",
|
||||
username: "",
|
||||
realname: "",
|
||||
leaveMessage: "",
|
||||
sasl: "plain",
|
||||
saslAccount: "testaccount",
|
||||
saslPassword: "testpassword",
|
||||
@ -109,10 +110,18 @@ describe("Network", function () {
|
||||
|
||||
it("editing a network should enforce correct types", function () {
|
||||
let saveCalled = false;
|
||||
let nameEmitCalled = false;
|
||||
|
||||
const network = new Network();
|
||||
network.edit(
|
||||
{
|
||||
emit(name, data) {
|
||||
if (name === "network:name") {
|
||||
nameEmitCalled = true;
|
||||
expect(data.uuid).to.equal(network.uuid);
|
||||
expect(data.name).to.equal("Lounge Test Network");
|
||||
}
|
||||
},
|
||||
save() {
|
||||
saveCalled = true;
|
||||
},
|
||||
@ -133,12 +142,13 @@ describe("Network", function () {
|
||||
commands: "/command 1 2 3\r\n/ping HELLO\r\r\r\r/whois test\r\n\r\n",
|
||||
ip: "newIp",
|
||||
hostname: "newHostname",
|
||||
guid: "newGuid",
|
||||
uuid: "newuuid",
|
||||
}
|
||||
);
|
||||
|
||||
expect(saveCalled).to.be.true;
|
||||
expect(network.guid).to.not.equal("newGuid");
|
||||
expect(nameEmitCalled).to.be.true;
|
||||
expect(network.uuid).to.not.equal("newuuid");
|
||||
expect(network.ip).to.be.undefined;
|
||||
expect(network.hostname).to.be.undefined;
|
||||
|
||||
|
@ -49,7 +49,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: url,
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
expect(message.previews).to.deep.equal([
|
||||
{
|
||||
@ -86,7 +86,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: url,
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
expect(message.previews).to.deep.equal([
|
||||
{
|
||||
@ -122,7 +122,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: url,
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
app.get("/truncate", function (req, res) {
|
||||
res.send(
|
||||
@ -146,7 +146,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: "http://localhost:" + this.port + "/basic-og",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
app.get("/basic-og", function (req, res) {
|
||||
res.send("<title>test</title><meta property='og:title' content='opengraph test'>");
|
||||
@ -163,7 +163,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: "http://localhost:" + this.port + "/duplicate-tags",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
app.get("/duplicate-tags", function (req, res) {
|
||||
res.send(
|
||||
@ -183,7 +183,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: "http://localhost:" + this.port + "/description-og",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
app.get("/description-og", function (req, res) {
|
||||
res.send(
|
||||
@ -203,7 +203,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: "http://localhost:" + this.port + "/thumb",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
app.get("/thumb", function (req, res) {
|
||||
res.send(
|
||||
@ -222,13 +222,77 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
});
|
||||
});
|
||||
|
||||
describe("test disableMediaPreview", function () {
|
||||
beforeEach(function (done) {
|
||||
Helper.config.disableMediaPreview = true;
|
||||
done();
|
||||
});
|
||||
afterEach(function (done) {
|
||||
Helper.config.disableMediaPreview = false;
|
||||
done();
|
||||
});
|
||||
it("should ignore og:image if disableMediaPreview", function (done) {
|
||||
const port = this.port;
|
||||
|
||||
app.get("/nonexistent-test-image.png", function () {
|
||||
throw "Should not fetch image";
|
||||
});
|
||||
|
||||
app.get("/thumb", function (req, res) {
|
||||
res.send(
|
||||
"<title>Google</title><meta property='og:image' content='http://localhost:" +
|
||||
port +
|
||||
"/nonexistent-test-image.png'>"
|
||||
);
|
||||
});
|
||||
const message = this.irc.createMessage({
|
||||
text: "http://localhost:" + port + "/thumb",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
this.irc.once("msg:preview", function (data) {
|
||||
expect(data.preview.head).to.equal("Google");
|
||||
expect(data.preview.type).to.equal("link");
|
||||
expect(data.preview.thumb).to.equal("");
|
||||
done();
|
||||
});
|
||||
});
|
||||
it("should ignore og:video if disableMediaPreview", function (done) {
|
||||
const port = this.port;
|
||||
|
||||
app.get("/nonexistent-video.mpr", function () {
|
||||
throw "Should not fetch video";
|
||||
});
|
||||
|
||||
app.get("/thumb", function (req, res) {
|
||||
res.send(
|
||||
"<title>Google</title><meta property='og:video:type' content='video/mp4'><meta property='og:video' content='http://localhost:" +
|
||||
port +
|
||||
"/nonexistent-video.mp4'>"
|
||||
);
|
||||
});
|
||||
const message = this.irc.createMessage({
|
||||
text: "http://localhost:" + port + "/thumb",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
this.irc.once("msg:preview", function (data) {
|
||||
expect(data.preview.head).to.equal("Google");
|
||||
expect(data.preview.type).to.equal("link");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should find image_src", function (done) {
|
||||
const port = this.port;
|
||||
const message = this.irc.createMessage({
|
||||
text: "http://localhost:" + this.port + "/thumb-image-src",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
app.get("/thumb-image-src", function (req, res) {
|
||||
res.send(
|
||||
@ -250,7 +314,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: "http://localhost:" + this.port + "/thumb-image-src",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
app.get("/thumb-image-src", function (req, res) {
|
||||
res.send("<link rel='image_src' href='//localhost:" + port + "/real-test-image.png'>");
|
||||
@ -270,7 +334,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: "http://localhost:" + this.port + "/relative-thumb",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
app.get("/relative-thumb", function (req, res) {
|
||||
res.send(
|
||||
@ -294,7 +358,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: "http://localhost:" + this.port + "/thumb-no-title",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
app.get("/thumb-no-title", function (req, res) {
|
||||
res.send(
|
||||
@ -320,7 +384,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: "http://localhost:" + this.port + "/body-no-title",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
app.get("/body-no-title", function (req, res) {
|
||||
res.send("<meta name='description' content='hello world'>");
|
||||
@ -341,7 +405,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: "http://localhost:" + this.port + "/thumb-404",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
app.get("/thumb-404", function (req, res) {
|
||||
res.send(
|
||||
@ -365,7 +429,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: "http://localhost:" + port + "/real-test-image.png",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
this.irc.once("msg:preview", function (data) {
|
||||
expect(data.preview.type).to.equal("image");
|
||||
@ -384,7 +448,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: "http://localhost:" + port + "/one http://localhost:" + this.port + "/two",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
expect(message.previews).to.eql([
|
||||
{
|
||||
@ -447,7 +511,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: "http://localhost:" + this.port + "/language-check",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
});
|
||||
|
||||
it("should send accept text/html for initial request", function (done) {
|
||||
@ -463,7 +527,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: "http://localhost:" + this.port + "/accept-header-html",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
});
|
||||
|
||||
it("should send accept */* for meta image", function (done) {
|
||||
@ -487,7 +551,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: "http://localhost:" + port + "/accept-header-thumb",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
});
|
||||
|
||||
it("should not add slash to url", function (done) {
|
||||
@ -496,7 +560,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
text: "http://localhost:" + port + "",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
this.irc.once("msg:preview", function (data) {
|
||||
expect(data.preview.link).to.equal("http://localhost:" + port + "");
|
||||
@ -527,7 +591,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
"/unicodeq/?q=🙈-emoji-test",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
app.get("/unicode/:q", function (req, res) {
|
||||
res.send(`<title>${req.params.q}</title>`);
|
||||
@ -559,19 +623,15 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch protocol-aware links", function (done) {
|
||||
it("should not fetch links without a schema", function () {
|
||||
const port = this.port;
|
||||
const message = this.irc.createMessage({
|
||||
text: "//localhost:" + port + "",
|
||||
text: `//localhost:${port} localhost:${port} //localhost:${port}/test localhost:${port}/test`,
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
this.irc.once("msg:preview", function (data) {
|
||||
expect(data.preview.link).to.equal("http://localhost:" + port + "");
|
||||
expect(data.preview.type).to.equal("error");
|
||||
done();
|
||||
});
|
||||
expect(message.previews).to.be.empty;
|
||||
});
|
||||
|
||||
it("should de-duplicate links", function (done) {
|
||||
@ -587,7 +647,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
"",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
expect(message.previews).to.deep.equal([
|
||||
{
|
||||
@ -635,9 +695,9 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
|
||||
this.irc.config.browser.language = "very nice language";
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
process.nextTick(() => link(this.irc, this.network.channels[0], message));
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
process.nextTick(() => link(this.irc, this.network.channels[0], message, message.text));
|
||||
|
||||
app.get("/basic-og-once", function (req, res) {
|
||||
requests++;
|
||||
@ -674,11 +734,11 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
||||
let responses = 0;
|
||||
|
||||
this.irc.config.browser.language = "first language";
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
setTimeout(() => {
|
||||
this.irc.config.browser.language = "second language";
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
}, 100);
|
||||
|
||||
app.get("/basic-og-once-lang", function (req, res) {
|
||||
|
@ -76,7 +76,7 @@ describe("Image storage", function () {
|
||||
text: "http://localhost:" + port + "/thumb",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
this.app.get("/thumb", function (req, res) {
|
||||
res.send(
|
||||
@ -100,7 +100,7 @@ describe("Image storage", function () {
|
||||
text: "http://localhost:" + port + "/real-test-image.png",
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
this.irc.once("msg:preview", function (data) {
|
||||
expect(data.preview.type).to.equal("image");
|
||||
@ -124,7 +124,7 @@ describe("Image storage", function () {
|
||||
);
|
||||
});
|
||||
|
||||
link(this.irc, this.network.channels[0], message);
|
||||
link(this.irc, this.network.channels[0], message, message.text);
|
||||
|
||||
this.irc.once("msg:preview", function (data) {
|
||||
expect(data.preview.type).to.equal("link");
|
||||
|
@ -19,6 +19,10 @@ describe("public folder", function () {
|
||||
expect(fs.existsSync(path.join(publicFolder, "thelounge.webmanifest"))).to.be.true;
|
||||
});
|
||||
|
||||
it("audio files are copied", function () {
|
||||
expect(fs.existsSync(path.join(publicFolder, "audio", "pop.wav"))).to.be.true;
|
||||
});
|
||||
|
||||
it("index HTML file is not copied", function () {
|
||||
expect(fs.existsSync(path.join(publicFolder, "index.html"))).to.be.false;
|
||||
expect(fs.existsSync(path.join(publicFolder, "index.html.tpl"))).to.be.false;
|
||||
@ -32,6 +36,8 @@ describe("public folder", function () {
|
||||
it("style files are built", function () {
|
||||
expect(fs.existsSync(path.join(publicFolder, "css", "style.css"))).to.be.true;
|
||||
expect(fs.existsSync(path.join(publicFolder, "css", "style.css.map"))).to.be.true;
|
||||
expect(fs.existsSync(path.join(publicFolder, "themes", "default.css"))).to.be.true;
|
||||
expect(fs.existsSync(path.join(publicFolder, "themes", "morning.css"))).to.be.true;
|
||||
});
|
||||
|
||||
it("style files contain expected content", function (done) {
|
||||
@ -55,4 +61,15 @@ describe("public folder", function () {
|
||||
expect(fs.existsSync(path.join(publicFolder, "js", "loading-error-handlers.js"))).to.be
|
||||
.true;
|
||||
});
|
||||
|
||||
it("service worker has cacheName set", function (done) {
|
||||
fs.readFile(path.join(publicFolder, "service-worker.js"), "utf8", function (err, contents) {
|
||||
expect(err).to.be.null;
|
||||
|
||||
expect(contents.includes("const cacheName")).to.be.true;
|
||||
expect(contents.includes("__HASH__")).to.be.false;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -22,7 +22,10 @@ describe("Custom highlights", function () {
|
||||
},
|
||||
"test",
|
||||
{
|
||||
clientSettings: {highlights: "foo, @all, sp ace , 고"},
|
||||
clientSettings: {
|
||||
highlights: "foo, @all, sp ace , 고",
|
||||
highlightExceptions: "foo bar, bar @all, test sp ace test",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -96,4 +99,53 @@ describe("Custom highlights", function () {
|
||||
client.compileCustomHighlights();
|
||||
expect(client.highlightRegex).to.be.null;
|
||||
});
|
||||
|
||||
// tests for highlight exceptions
|
||||
it("should NOT highlight due to highlight exceptions", function () {
|
||||
const teststrings = [
|
||||
"foo bar baz",
|
||||
"test foo bar",
|
||||
"foo bar @all test",
|
||||
"with a test sp ace test",
|
||||
];
|
||||
|
||||
for (const teststring of teststrings) {
|
||||
expect(teststring).to.match(client.highlightExceptionRegex);
|
||||
}
|
||||
});
|
||||
|
||||
it("should highlight regardless of highlight exceptions", function () {
|
||||
const teststrings = [
|
||||
"Hey foo hello",
|
||||
"hey Foo: hi",
|
||||
"hey Foo, hi",
|
||||
"<foo> testing",
|
||||
"foo",
|
||||
"hey @all test",
|
||||
"testing foo's stuff",
|
||||
'"foo"',
|
||||
'"@all"',
|
||||
"foo!",
|
||||
"www.foo.bar",
|
||||
"www.bar.foo/page",
|
||||
"고",
|
||||
"test 고",
|
||||
"고!",
|
||||
"www.고.com",
|
||||
"hey @Foo",
|
||||
"hey ~Foo",
|
||||
"hey +Foo",
|
||||
"hello &foo",
|
||||
"@all",
|
||||
"@all wtf",
|
||||
"wtfbar @all",
|
||||
"@@all",
|
||||
"@고",
|
||||
"f00 sp ace: bar",
|
||||
];
|
||||
|
||||
for (const teststring of teststrings) {
|
||||
expect(teststring).to.not.match(client.highlightExceptionRegex);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -41,7 +41,7 @@ const config = {
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
options: {
|
||||
hmr: false,
|
||||
esModule: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -91,9 +91,11 @@ const config = {
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "css/style.css",
|
||||
}),
|
||||
new CopyPlugin([
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: "./node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff*",
|
||||
from:
|
||||
"./node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff*",
|
||||
to: "fonts/[name].[ext]",
|
||||
},
|
||||
{
|
||||
@ -103,7 +105,9 @@ const config = {
|
||||
{
|
||||
from: "./client/*",
|
||||
to: "[name].[ext]",
|
||||
ignore: ["index.html.tpl", "service-worker.js"],
|
||||
globOptions: {
|
||||
ignore: ["**/index.html.tpl", "**/service-worker.js"],
|
||||
},
|
||||
},
|
||||
{
|
||||
from: "./client/service-worker.js",
|
||||
@ -111,7 +115,10 @@ const config = {
|
||||
transform(content) {
|
||||
return content
|
||||
.toString()
|
||||
.replace("__HASH__", isProduction ? Helper.getVersionCacheBust() : "dev");
|
||||
.replace(
|
||||
"__HASH__",
|
||||
isProduction ? Helper.getVersionCacheBust() : "dev"
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -126,7 +133,8 @@ const config = {
|
||||
from: "./client/themes/*",
|
||||
to: "themes/[name].[ext]",
|
||||
},
|
||||
]),
|
||||
],
|
||||
}),
|
||||
// socket.io uses debug, we don't need it
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/debug/,
|
||||
|
Loading…
Reference in New Issue
Block a user