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]
|
spaced-comment: [error, always]
|
||||||
strict: off
|
strict: off
|
||||||
yoda: error
|
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-v-html: off
|
||||||
vue/no-use-v-if-with-v-for: off
|
vue/require-default-prop: off
|
||||||
|
vue/v-slot-style: [error, longform]
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- vue
|
- 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:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
include:
|
||||||
node_version: [
|
# EOL: April 2021
|
||||||
10.x, # EOL: April 2021
|
- os: ubuntu-latest
|
||||||
12.x, # EOL: April 2022
|
|
||||||
]
|
|
||||||
exclude:
|
|
||||||
- os: macOS-latest
|
|
||||||
node_version: 10.x
|
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 }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@master
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node_version }}
|
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
|
- uses: actions/checkout@master
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
registry-url: "https://registry.npmjs.org/"
|
registry-url: "https://registry.npmjs.org/"
|
||||||
|
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
extends: stylelint-config-standard
|
extends: stylelint-config-standard
|
||||||
|
|
||||||
ignoreFiles:
|
|
||||||
- client/css/bootstrap.css
|
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
indentation: tab
|
indentation: tab
|
||||||
# complains about FontAwesome
|
# 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 -->
|
<!-- 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
|
## 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).
|
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)
|
- 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 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
|
- `yarn dev` can be used to start The Lounge with hot module reloading
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
const constants = require("../js/constants");
|
const constants = require("../js/constants");
|
||||||
|
import eventbus from "../js/eventbus";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import throttle from "lodash/throttle";
|
import throttle from "lodash/throttle";
|
||||||
import storage from "../js/localStorage";
|
import storage from "../js/localStorage";
|
||||||
@ -53,14 +54,14 @@ export default {
|
|||||||
|
|
||||||
// Make a single throttled resize listener available to all components
|
// Make a single throttled resize listener available to all components
|
||||||
this.debouncedResize = throttle(() => {
|
this.debouncedResize = throttle(() => {
|
||||||
this.$root.$emit("resize");
|
eventbus.emit("resize");
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
window.addEventListener("resize", this.debouncedResize, {passive: true});
|
window.addEventListener("resize", this.debouncedResize, {passive: true});
|
||||||
|
|
||||||
// Emit a daychange event every time the day changes so date markers know when to update themselves
|
// Emit a daychange event every time the day changes so date markers know when to update themselves
|
||||||
const emitDayChange = () => {
|
const emitDayChange = () => {
|
||||||
this.$root.$emit("daychange");
|
eventbus.emit("daychange");
|
||||||
// This should always be 24h later but re-computing exact value just in case
|
// This should always be 24h later but re-computing exact value just in case
|
||||||
this.dayChangeTimeout = setTimeout(emitDayChange, this.msUntilNextDay());
|
this.dayChangeTimeout = setTimeout(emitDayChange, this.msUntilNextDay());
|
||||||
};
|
};
|
||||||
@ -77,7 +78,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
escapeKey() {
|
escapeKey() {
|
||||||
this.$root.$emit("escapekey");
|
eventbus.emit("escapekey");
|
||||||
},
|
},
|
||||||
toggleSidebar(e) {
|
toggleSidebar(e) {
|
||||||
if (isIgnoredKeybind(e)) {
|
if (isIgnoredKeybind(e)) {
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import eventbus from "../js/eventbus";
|
||||||
import isChannelCollapsed from "../js/helpers/isChannelCollapsed";
|
import isChannelCollapsed from "../js/helpers/isChannelCollapsed";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -74,7 +75,7 @@ export default {
|
|||||||
this.$root.switchToChannel(this.channel);
|
this.$root.switchToChannel(this.channel);
|
||||||
},
|
},
|
||||||
openContextMenu(event) {
|
openContextMenu(event) {
|
||||||
this.$root.$emit("contextmenu:channel", {
|
eventbus.emit("contextmenu:channel", {
|
||||||
event: event,
|
event: event,
|
||||||
channel: this.channel,
|
channel: this.channel,
|
||||||
network: this.network,
|
network: this.network,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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
|
<div
|
||||||
id="chat"
|
id="chat"
|
||||||
:class="{
|
:class="{
|
||||||
@ -25,6 +25,7 @@
|
|||||||
:value="channel.topic"
|
:value="channel.topic"
|
||||||
class="topic-input"
|
class="topic-input"
|
||||||
placeholder="Set channel topic"
|
placeholder="Set channel topic"
|
||||||
|
enterkeyhint="done"
|
||||||
@keyup.enter="saveTopic"
|
@keyup.enter="saveTopic"
|
||||||
@keyup.esc="channel.editTopic = false"
|
@keyup.esc="channel.editTopic = false"
|
||||||
/>
|
/>
|
||||||
@ -69,7 +70,7 @@
|
|||||||
<div class="chat">
|
<div class="chat">
|
||||||
<div class="messages">
|
<div class="messages">
|
||||||
<div class="msg">
|
<div class="msg">
|
||||||
<Component
|
<component
|
||||||
:is="specialComponent"
|
:is="specialComponent"
|
||||||
:network="network"
|
:network="network"
|
||||||
:channel="channel"
|
:channel="channel"
|
||||||
@ -107,6 +108,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import socket from "../js/socket";
|
import socket from "../js/socket";
|
||||||
|
import eventbus from "../js/eventbus";
|
||||||
import ParsedMessage from "./ParsedMessage.vue";
|
import ParsedMessage from "./ParsedMessage.vue";
|
||||||
import MessageList from "./MessageList.vue";
|
import MessageList from "./MessageList.vue";
|
||||||
import ChatInput from "./ChatInput.vue";
|
import ChatInput from "./ChatInput.vue";
|
||||||
@ -204,14 +206,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
openContextMenu(event) {
|
openContextMenu(event) {
|
||||||
this.$root.$emit("contextmenu:channel", {
|
eventbus.emit("contextmenu:channel", {
|
||||||
event: event,
|
event: event,
|
||||||
channel: this.channel,
|
channel: this.channel,
|
||||||
network: this.network,
|
network: this.network,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
openMentions() {
|
openMentions() {
|
||||||
this.$root.$emit("mentions:toggle", {
|
eventbus.emit("mentions:toggle", {
|
||||||
event: event,
|
event: event,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
ref="input"
|
ref="input"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mousetrap"
|
class="mousetrap"
|
||||||
|
enterkeyhint="send"
|
||||||
:value="channel.pendingMessage"
|
:value="channel.pendingMessage"
|
||||||
:placeholder="getInputPlaceholder(channel)"
|
:placeholder="getInputPlaceholder(channel)"
|
||||||
:aria-label="getInputPlaceholder(channel)"
|
:aria-label="getInputPlaceholder(channel)"
|
||||||
@ -24,6 +25,7 @@
|
|||||||
id="upload-input"
|
id="upload-input"
|
||||||
ref="uploadInput"
|
ref="uploadInput"
|
||||||
type="file"
|
type="file"
|
||||||
|
aria-labelledby="upload"
|
||||||
multiple
|
multiple
|
||||||
@change="onUploadInputChange"
|
@change="onUploadInputChange"
|
||||||
/>
|
/>
|
||||||
@ -56,6 +58,7 @@ import autocompletion from "../js/autocompletion";
|
|||||||
import commands from "../js/commands/index";
|
import commands from "../js/commands/index";
|
||||||
import socket from "../js/socket";
|
import socket from "../js/socket";
|
||||||
import upload from "../js/upload";
|
import upload from "../js/upload";
|
||||||
|
import eventbus from "../js/eventbus";
|
||||||
|
|
||||||
const formattingHotkeys = {
|
const formattingHotkeys = {
|
||||||
"mod+k": "\x03",
|
"mod+k": "\x03",
|
||||||
@ -101,7 +104,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$root.$on("escapekey", this.blurInput);
|
eventbus.on("escapekey", this.blurInput);
|
||||||
|
|
||||||
if (this.$store.state.settings.autocomplete) {
|
if (this.$store.state.settings.autocomplete) {
|
||||||
autocompletionRef = autocompletion(this.$refs.input);
|
autocompletionRef = autocompletion(this.$refs.input);
|
||||||
@ -163,7 +166,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
this.$root.$off("escapekey", this.blurInput);
|
eventbus.off("escapekey", this.blurInput);
|
||||||
|
|
||||||
if (autocompletionRef) {
|
if (autocompletionRef) {
|
||||||
autocompletionRef.destroy();
|
autocompletionRef.destroy();
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
:on-hover="hoverUser"
|
:on-hover="hoverUser"
|
||||||
:active="user.original === activeUser"
|
:active="user.original === activeUser"
|
||||||
:user="user.original"
|
:user="user.original"
|
||||||
v-html="user.original.mode + user.string"
|
v-html="user.string"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@ -98,18 +98,25 @@ export default {
|
|||||||
const result = this.filteredUsers;
|
const result = this.filteredUsers;
|
||||||
|
|
||||||
for (const user of result) {
|
for (const user of result) {
|
||||||
if (!groups[user.original.mode]) {
|
const mode = user.original.modes[0] || "";
|
||||||
groups[user.original.mode] = [];
|
|
||||||
|
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 {
|
} else {
|
||||||
for (const user of this.channel.users) {
|
for (const user of this.channel.users) {
|
||||||
if (!groups[user.mode]) {
|
const mode = user.modes[0] || "";
|
||||||
groups[user.mode] = [user];
|
|
||||||
|
if (!groups[mode]) {
|
||||||
|
groups[mode] = [user];
|
||||||
} else {
|
} else {
|
||||||
groups[user.mode].push(user);
|
groups[mode].push(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,8 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import eventbus from "../js/eventbus";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ConfirmDialog",
|
name: "ConfirmDialog",
|
||||||
data() {
|
data() {
|
||||||
@ -60,12 +62,12 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$root.$on("escapekey", this.close);
|
eventbus.on("escapekey", this.close);
|
||||||
this.$root.$on("confirm-dialog", this.open);
|
eventbus.on("confirm-dialog", this.open);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
this.$root.$off("escapekey", this.close);
|
eventbus.off("escapekey", this.close);
|
||||||
this.$root.$off("confirm-dialog", this.open);
|
eventbus.off("confirm-dialog", this.open);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
open(data, callback) {
|
open(data, callback) {
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {generateUserContextMenu, generateChannelContextMenu} from "../js/helpers/contextMenu.js";
|
import {generateUserContextMenu, generateChannelContextMenu} from "../js/helpers/contextMenu.js";
|
||||||
|
import eventbus from "../js/eventbus";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ContextMenu",
|
name: "ContextMenu",
|
||||||
@ -58,14 +59,14 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$root.$on("escapekey", this.close);
|
eventbus.on("escapekey", this.close);
|
||||||
this.$root.$on("contextmenu:user", this.openUserContextMenu);
|
eventbus.on("contextmenu:user", this.openUserContextMenu);
|
||||||
this.$root.$on("contextmenu:channel", this.openChannelContextMenu);
|
eventbus.on("contextmenu:channel", this.openChannelContextMenu);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
this.$root.$off("escapekey", this.close);
|
eventbus.off("escapekey", this.close);
|
||||||
this.$root.$off("contextmenu:user", this.openUserContextMenu);
|
eventbus.off("contextmenu:user", this.openUserContextMenu);
|
||||||
this.$root.$off("contextmenu:channel", this.openChannelContextMenu);
|
eventbus.off("contextmenu:channel", this.openChannelContextMenu);
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
},
|
},
|
||||||
@ -75,19 +76,17 @@ export default {
|
|||||||
this.open(data.event, items);
|
this.open(data.event, items);
|
||||||
},
|
},
|
||||||
openUserContextMenu(data) {
|
openUserContextMenu(data) {
|
||||||
const activeChannel = this.$store.state.activeChannel;
|
const {network, channel} = this.$store.state.activeChannel;
|
||||||
// If there's an active network and channel use them
|
|
||||||
let {network, channel} = activeChannel ? activeChannel : {network: null, channel: null};
|
|
||||||
|
|
||||||
// Use network and channel from event if specified
|
const items = generateUserContextMenu(
|
||||||
network = data.network ? data.network : network;
|
this.$root,
|
||||||
channel = data.channel ? data.channel : channel;
|
channel,
|
||||||
|
network,
|
||||||
const defaultUser = {nick: data.user.nick};
|
channel.users.find((u) => u.nick === data.user.nick) || {
|
||||||
let user = channel ? channel.users.find((u) => u.nick === data.user.nick) : defaultUser;
|
nick: data.user.nick,
|
||||||
user = user ? user : defaultUser;
|
modes: [],
|
||||||
|
}
|
||||||
const items = generateUserContextMenu(this.$root, channel, network, user);
|
);
|
||||||
this.open(data.event, items);
|
this.open(data.event, items);
|
||||||
},
|
},
|
||||||
open(event, items) {
|
open(event, items) {
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import calendar from "dayjs/plugin/calendar";
|
import calendar from "dayjs/plugin/calendar";
|
||||||
|
import eventbus from "../js/eventbus";
|
||||||
|
|
||||||
dayjs.extend(calendar);
|
dayjs.extend(calendar);
|
||||||
|
|
||||||
@ -24,11 +25,11 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.hoursPassed() < 48) {
|
if (this.hoursPassed() < 48) {
|
||||||
this.$root.$on("daychange", this.dayChange);
|
eventbus.on("daychange", this.dayChange);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$root.$off("daychange", this.dayChange);
|
eventbus.off("daychange", this.dayChange);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
hoursPassed() {
|
hoursPassed() {
|
||||||
@ -38,7 +39,7 @@ export default {
|
|||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
|
|
||||||
if (this.hoursPassed() >= 48) {
|
if (this.hoursPassed() >= 48) {
|
||||||
this.$root.$off("daychange", this.dayChange);
|
eventbus.off("daychange", this.dayChange);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
friendlyDate() {
|
friendlyDate() {
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
|
import eventbus from "../js/eventbus";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ImageViewer",
|
name: "ImageViewer",
|
||||||
@ -79,8 +80,8 @@ export default {
|
|||||||
link(newLink, oldLink) {
|
link(newLink, oldLink) {
|
||||||
// TODO: history.pushState
|
// TODO: history.pushState
|
||||||
if (newLink === null) {
|
if (newLink === null) {
|
||||||
this.$root.$off("escapekey", this.closeViewer);
|
eventbus.off("escapekey", this.closeViewer);
|
||||||
this.$root.$off("resize", this.correctPosition);
|
eventbus.off("resize", this.correctPosition);
|
||||||
Mousetrap.unbind("left", this.previous);
|
Mousetrap.unbind("left", this.previous);
|
||||||
Mousetrap.unbind("right", this.next);
|
Mousetrap.unbind("right", this.next);
|
||||||
return;
|
return;
|
||||||
@ -89,8 +90,8 @@ export default {
|
|||||||
this.setPrevNextImages();
|
this.setPrevNextImages();
|
||||||
|
|
||||||
if (!oldLink) {
|
if (!oldLink) {
|
||||||
this.$root.$on("escapekey", this.closeViewer);
|
eventbus.on("escapekey", this.closeViewer);
|
||||||
this.$root.$on("resize", this.correctPosition);
|
eventbus.on("resize", this.correctPosition);
|
||||||
Mousetrap.bind("left", this.previous);
|
Mousetrap.bind("left", this.previous);
|
||||||
Mousetrap.bind("right", this.next);
|
Mousetrap.bind("right", this.next);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
method="post"
|
method="post"
|
||||||
action=""
|
action=""
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@keydown.esc.prevent="$emit('toggleJoinChannel')"
|
@keydown.esc.prevent="$emit('toggle-join-channel')"
|
||||||
@submit.prevent="onSubmit"
|
@submit.prevent="onSubmit"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@ -81,7 +81,7 @@ export default {
|
|||||||
|
|
||||||
this.inputChannel = "";
|
this.inputChannel = "";
|
||||||
this.inputPassword = "";
|
this.inputPassword = "";
|
||||||
this.$emit("toggleJoinChannel");
|
this.$emit("toggle-join-channel");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -130,6 +130,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import eventbus from "../js/eventbus";
|
||||||
import friendlysize from "../js/helpers/friendlysize";
|
import friendlysize from "../js/helpers/friendlysize";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -167,12 +168,12 @@ export default {
|
|||||||
this.updateShownState();
|
this.updateShownState();
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$root.$on("resize", this.handleResize);
|
eventbus.on("resize", this.handleResize);
|
||||||
|
|
||||||
this.onPreviewUpdate();
|
this.onPreviewUpdate();
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$root.$off("resize", this.handleResize);
|
eventbus.off("resize", this.handleResize);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
// Let this preview go through load/canplay events again,
|
// Let this preview go through load/canplay events again,
|
||||||
|
@ -22,7 +22,7 @@ export default {
|
|||||||
onClick() {
|
onClick() {
|
||||||
this.link.shown = !this.link.shown;
|
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"
|
v-if="isOpen"
|
||||||
id="mentions-popup-container"
|
id="mentions-popup-container"
|
||||||
@click="containerClick"
|
@click="containerClick"
|
||||||
@contextmenu.prevent="containerClick"
|
@contextmenu="containerClick"
|
||||||
>
|
>
|
||||||
<div class="mentions-popup">
|
<div class="mentions-popup">
|
||||||
<div class="mentions-popup-title">
|
<div class="mentions-popup-title">
|
||||||
Recent mentions
|
Recent mentions
|
||||||
|
<button
|
||||||
|
v-if="resolvedMessages.length"
|
||||||
|
class="btn hide-all-mentions"
|
||||||
|
@click="hideAllMentions()"
|
||||||
|
>
|
||||||
|
Hide all
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="resolvedMessages.length === 0">
|
<template v-if="resolvedMessages.length === 0">
|
||||||
<p v-if="isLoading">Loading…</p>
|
<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>
|
||||||
<template v-for="message in resolvedMessages" v-else>
|
<template v-for="message in resolvedMessages" v-else>
|
||||||
<div :key="message.id" :class="['msg', message.type]">
|
<div :key="message.msgId" :class="['msg', message.type]">
|
||||||
<span class="from">
|
<div class="mentions-info">
|
||||||
<Username :user="message.from" />
|
<div>
|
||||||
<template v-if="message.channel">
|
<span class="from">
|
||||||
in {{ message.channel.channel.name }} on
|
<Username :user="message.from" />
|
||||||
{{ message.channel.network.name }}
|
<template v-if="message.channel">
|
||||||
</template>
|
in {{ message.channel.channel.name }} on
|
||||||
<template v-else>
|
{{ message.channel.network.name }}
|
||||||
in unknown channel
|
</template>
|
||||||
</template>
|
<template v-else> in unknown channel </template>
|
||||||
</span>
|
</span>
|
||||||
<span :title="message.time | localetime" class="time">
|
<span :title="message.localetime" class="time">
|
||||||
{{ messageTime(message.time) }}
|
{{ messageTime(message.time) }}
|
||||||
</span>
|
</span>
|
||||||
<button
|
</div>
|
||||||
class="msg-hide"
|
<div>
|
||||||
aria-label="Hide this mention"
|
<span class="close-tooltip tooltipped tooltipped-w" aria-label="Close">
|
||||||
@click="hideMention(message)"
|
<button
|
||||||
></button>
|
class="msg-hide"
|
||||||
|
aria-label="Hide this mention"
|
||||||
|
@click="hideMention(message)"
|
||||||
|
></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="content" dir="auto">
|
<div class="content" dir="auto">
|
||||||
<ParsedMessage :network="null" :message="message" />
|
<ParsedMessage :network="null" :message="message" />
|
||||||
</div>
|
</div>
|
||||||
@ -54,16 +67,23 @@
|
|||||||
right: 80px;
|
right: 80px;
|
||||||
top: 55px;
|
top: 55px;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: scroll;
|
overflow-y: auto;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mentions-popup > .mentions-popup-title {
|
.mentions-popup > .mentions-popup-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mentions-popup .mentions-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.mentions-popup .msg {
|
.mentions-popup .msg {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
@ -78,6 +98,8 @@
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word; /* Webkit-specific */
|
||||||
}
|
}
|
||||||
|
|
||||||
.mentions-popup .msg-hide::before {
|
.mentions-popup .msg-hide::before {
|
||||||
@ -89,6 +111,21 @@
|
|||||||
content: "×";
|
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) {
|
@media (max-width: 768px) {
|
||||||
.mentions-popup {
|
.mentions-popup {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@ -108,6 +145,8 @@
|
|||||||
import Username from "./Username.vue";
|
import Username from "./Username.vue";
|
||||||
import ParsedMessage from "./ParsedMessage.vue";
|
import ParsedMessage from "./ParsedMessage.vue";
|
||||||
import socket from "../js/socket";
|
import socket from "../js/socket";
|
||||||
|
import eventbus from "../js/eventbus";
|
||||||
|
import localetime from "../js/helpers/localetime";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
|
||||||
@ -130,6 +169,7 @@ export default {
|
|||||||
const messages = this.$store.state.mentions.slice().reverse();
|
const messages = this.$store.state.mentions.slice().reverse();
|
||||||
|
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
|
message.localetime = localetime(message.time);
|
||||||
message.channel = this.$store.getters.findChannel(message.chanId);
|
message.channel = this.$store.getters.findChannel(message.chanId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,10 +182,10 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$root.$on("mentions:toggle", this.openPopup);
|
eventbus.on("mentions:toggle", this.openPopup);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
this.$root.$off("mentions:toggle", this.openPopup);
|
eventbus.off("mentions:toggle", this.openPopup);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
messageTime(time) {
|
messageTime(time) {
|
||||||
@ -159,6 +199,10 @@ export default {
|
|||||||
|
|
||||||
socket.emit("mentions:hide", message.msgId);
|
socket.emit("mentions:hide", message.msgId);
|
||||||
},
|
},
|
||||||
|
hideAllMentions() {
|
||||||
|
this.$store.state.mentions = [];
|
||||||
|
socket.emit("mentions:hide_all");
|
||||||
|
},
|
||||||
containerClick(event) {
|
containerClick(event) {
|
||||||
if (event.currentTarget === event.target) {
|
if (event.currentTarget === event.target) {
|
||||||
this.isOpen = false;
|
this.isOpen = false;
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
{self: message.self, highlight: message.highlight, 'previous-source': isPreviousSource},
|
{self: message.self, highlight: message.highlight, 'previous-source': isPreviousSource},
|
||||||
]"
|
]"
|
||||||
:data-type="message.type"
|
:data-type="message.type"
|
||||||
|
:data-command="message.command"
|
||||||
:data-from="message.from && message.from.nick"
|
:data-from="message.from && message.from.nick"
|
||||||
>
|
>
|
||||||
<span :aria-label="messageTimeLocale" class="time tooltipped tooltipped-e"
|
<span :aria-label="messageTimeLocale" class="time tooltipped tooltipped-e"
|
||||||
@ -19,7 +20,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="isAction()">
|
<template v-else-if="isAction()">
|
||||||
<span class="from"><span class="only-copy">*** </span></span>
|
<span class="from"><span class="only-copy">*** </span></span>
|
||||||
<Component :is="messageComponent" :network="network" :message="message" />
|
<component :is="messageComponent" :network="network" :message="message" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="message.type === 'action'">
|
<template v-else-if="message.type === 'action'">
|
||||||
<span class="from"><span class="only-copy">* </span></span>
|
<span class="from"><span class="only-copy">* </span></span>
|
||||||
@ -68,6 +69,12 @@
|
|||||||
class="msg-shown-in-active tooltipped tooltipped-e"
|
class="msg-shown-in-active tooltipped tooltipped-e"
|
||||||
><span></span
|
><span></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" />
|
<ParsedMessage :network="network" :message="message" />
|
||||||
<LinkPreview
|
<LinkPreview
|
||||||
v-for="preview in message.previews"
|
v-for="preview in message.previews"
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
:message="message"
|
:message="message"
|
||||||
:keep-scroll-position="keepScrollPosition"
|
:keep-scroll-position="keepScrollPosition"
|
||||||
:is-previous-source="isPreviousSource(message, id)"
|
:is-previous-source="isPreviousSource(message, id)"
|
||||||
@linkPreviewToggle="onLinkPreviewToggle"
|
@toggle-link-preview="onLinkPreviewToggle"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -56,12 +56,15 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
const constants = require("../js/constants");
|
const constants = require("../js/constants");
|
||||||
|
import eventbus from "../js/eventbus";
|
||||||
import clipboard from "../js/clipboard";
|
import clipboard from "../js/clipboard";
|
||||||
import socket from "../js/socket";
|
import socket from "../js/socket";
|
||||||
import Message from "./Message.vue";
|
import Message from "./Message.vue";
|
||||||
import MessageCondensed from "./MessageCondensed.vue";
|
import MessageCondensed from "./MessageCondensed.vue";
|
||||||
import DateMarker from "./DateMarker.vue";
|
import DateMarker from "./DateMarker.vue";
|
||||||
|
|
||||||
|
let unreadMarkerShown = false;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "MessageList",
|
name: "MessageList",
|
||||||
components: {
|
components: {
|
||||||
@ -173,7 +176,7 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.$refs.chat.addEventListener("scroll", this.handleScroll, {passive: true});
|
this.$refs.chat.addEventListener("scroll", this.handleScroll, {passive: true});
|
||||||
|
|
||||||
this.$root.$on("resize", this.handleResize);
|
eventbus.on("resize", this.handleResize);
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.historyObserver) {
|
if (this.historyObserver) {
|
||||||
@ -182,10 +185,10 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
beforeUpdate() {
|
beforeUpdate() {
|
||||||
this.unreadMarkerShown = false;
|
unreadMarkerShown = false;
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$root.$off("resize", this.handleResize);
|
eventbus.off("resize", this.handleResize);
|
||||||
this.$refs.chat.removeEventListener("scroll", this.handleScroll);
|
this.$refs.chat.removeEventListener("scroll", this.handleScroll);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
@ -201,11 +204,18 @@ export default {
|
|||||||
return true;
|
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) {
|
shouldDisplayUnreadMarker(id) {
|
||||||
if (!this.unreadMarkerShown && id > this.channel.firstUnread) {
|
if (!unreadMarkerShown && id > this.channel.firstUnread) {
|
||||||
this.unreadMarkerShown = true;
|
unreadMarkerShown = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,12 @@
|
|||||||
<span class="content">
|
<span class="content">
|
||||||
<Username :user="message.from" />
|
<Username :user="message.from" />
|
||||||
<i class="hostmask"> ({{ message.hostmask }})</i>
|
<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
|
has joined the channel
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
import ParsedMessage from "../ParsedMessage.vue";
|
import ParsedMessage from "../ParsedMessage.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "MessageTypeMOTD",
|
name: "MessageTypeMonospaceBlock",
|
||||||
components: {
|
components: {
|
||||||
ParsedMessage,
|
ParsedMessage,
|
||||||
},
|
},
|
@ -81,6 +81,11 @@
|
|||||||
<dd>Yes</dd>
|
<dd>Yes</dd>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="message.whois.certfp">
|
||||||
|
<dt>Certificate:</dt>
|
||||||
|
<dd>{{ message.whois.certfp }}</dd>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-if="message.whois.server">
|
<template v-if="message.whois.server">
|
||||||
<dt>Connected to:</dt>
|
<dt>Connected to:</dt>
|
||||||
<dd>
|
<dd>
|
||||||
|
@ -11,7 +11,9 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
Connect
|
Connect
|
||||||
<template v-if="config.lockNetwork">to {{ defaults.name }}</template>
|
<template v-if="config.lockNetwork && $store.state.serverConfiguration.public">
|
||||||
|
to {{ defaults.name }}
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</h1>
|
</h1>
|
||||||
<template v-if="!config.lockNetwork">
|
<template v-if="!config.lockNetwork">
|
||||||
@ -97,6 +99,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<h2>User preferences</h2>
|
||||||
<div class="connect-row">
|
<div class="connect-row">
|
||||||
@ -135,6 +167,16 @@
|
|||||||
maxlength="300"
|
maxlength="300"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<template v-if="defaults.uuid && !$store.state.serverConfiguration.public">
|
||||||
<div class="connect-row">
|
<div class="connect-row">
|
||||||
<label for="connect:commands">
|
<label for="connect:commands">
|
||||||
@ -270,9 +312,7 @@ the server tab on new connection"
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else-if="defaults.sasl === 'external'" class="connect-sasl-external">
|
<div v-else-if="defaults.sasl === 'external'" class="connect-sasl-external">
|
||||||
<p>
|
<p>The Lounge automatically generates and manages the client certificate.</p>
|
||||||
The Lounge automatically generates and manages the client certificate.
|
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
On the IRC server, you will need to tell the services to attach the
|
On the IRC server, you will need to tell the services to attach the
|
||||||
certificate fingerprint (certfp) to your account, for example:
|
certificate fingerprint (certfp) to your account, for example:
|
||||||
|
@ -46,9 +46,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="no-results">
|
<div v-else class="no-results">No results found.</div>
|
||||||
No results found.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Draggable
|
<Draggable
|
||||||
v-else
|
v-else
|
||||||
@ -84,13 +82,13 @@
|
|||||||
$store.state.activeChannel &&
|
$store.state.activeChannel &&
|
||||||
network.channels[0] === $store.state.activeChannel.channel
|
network.channels[0] === $store.state.activeChannel.channel
|
||||||
"
|
"
|
||||||
@toggleJoinChannel="network.isJoinChannelShown = !network.isJoinChannelShown"
|
@toggle-join-channel="network.isJoinChannelShown = !network.isJoinChannelShown"
|
||||||
/>
|
/>
|
||||||
<JoinChannel
|
<JoinChannel
|
||||||
v-if="network.isJoinChannelShown"
|
v-if="network.isJoinChannelShown"
|
||||||
:network="network"
|
:network="network"
|
||||||
:channel="network.channels[0]"
|
:channel="network.channels[0]"
|
||||||
@toggleJoinChannel="network.isJoinChannelShown = !network.isJoinChannelShown"
|
@toggle-join-channel="network.isJoinChannelShown = !network.isJoinChannelShown"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Draggable
|
<Draggable
|
||||||
@ -106,17 +104,18 @@
|
|||||||
@start="onDragStart"
|
@start="onDragStart"
|
||||||
@end="onDragEnd"
|
@end="onDragEnd"
|
||||||
>
|
>
|
||||||
<Channel
|
<template v-for="(channel, index) in network.channels">
|
||||||
v-for="(channel, index) in network.channels"
|
<Channel
|
||||||
v-if="index > 0"
|
v-if="index > 0"
|
||||||
:key="channel.id"
|
:key="channel.id"
|
||||||
:channel="channel"
|
:channel="channel"
|
||||||
:network="network"
|
:network="network"
|
||||||
:active="
|
:active="
|
||||||
$store.state.activeChannel &&
|
$store.state.activeChannel &&
|
||||||
channel === $store.state.activeChannel.channel
|
channel === $store.state.activeChannel.channel
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
</Draggable>
|
</Draggable>
|
||||||
</div>
|
</div>
|
||||||
</Draggable>
|
</Draggable>
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
:class="['add-channel', {opened: isJoinChannelShown}]"
|
:class="['add-channel', {opened: isJoinChannelShown}]"
|
||||||
:aria-controls="'join-channel-' + channel.id"
|
:aria-controls="'join-channel-' + channel.id"
|
||||||
:aria-label="joinChannelLabel"
|
:aria-label="joinChannelLabel"
|
||||||
@click.stop="$emit('toggleJoinChannel')"
|
@click.stop="$emit('toggle-join-channel')"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</ChannelWrapper>
|
</ChannelWrapper>
|
||||||
|
@ -6,11 +6,12 @@
|
|||||||
v-on="onHover ? {mouseenter: hover} : {}"
|
v-on="onHover ? {mouseenter: hover} : {}"
|
||||||
@click.prevent="openContextMenu"
|
@click.prevent="openContextMenu"
|
||||||
@contextmenu.prevent="openContextMenu"
|
@contextmenu.prevent="openContextMenu"
|
||||||
><slot>{{ user.mode }}{{ user.nick }}</slot></span
|
><slot>{{ mode }}{{ user.nick }}</slot></span
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import eventbus from "../js/eventbus";
|
||||||
import colorClass from "../js/helpers/colorClass";
|
import colorClass from "../js/helpers/colorClass";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -23,6 +24,14 @@ export default {
|
|||||||
network: Object,
|
network: Object,
|
||||||
},
|
},
|
||||||
computed: {
|
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() {
|
nickColor() {
|
||||||
return colorClass(this.user.nick);
|
return colorClass(this.user.nick);
|
||||||
},
|
},
|
||||||
@ -32,7 +41,7 @@ export default {
|
|||||||
return this.onHover(this.user);
|
return this.onHover(this.user);
|
||||||
},
|
},
|
||||||
openContextMenu(event) {
|
openContextMenu(event) {
|
||||||
this.$root.$emit("contextmenu:user", {
|
eventbus.emit("contextmenu:user", {
|
||||||
event: event,
|
event: event,
|
||||||
user: this.user,
|
user: this.user,
|
||||||
network: this.network,
|
network: this.network,
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="version-checker" :class="[$store.state.versionStatus]">
|
<div id="version-checker" :class="[$store.state.versionStatus]">
|
||||||
<p v-if="$store.state.versionStatus === 'loading'">
|
<p v-if="$store.state.versionStatus === 'loading'">Checking for updates…</p>
|
||||||
Checking for updates…
|
|
||||||
</p>
|
|
||||||
<p v-if="$store.state.versionStatus === 'new-version'">
|
<p v-if="$store.state.versionStatus === 'new-version'">
|
||||||
The Lounge <b>{{ $store.state.versionData.latest.version }}</b>
|
The Lounge <b>{{ $store.state.versionData.latest.version }}</b>
|
||||||
<template v-if="$store.state.versionData.latest.prerelease">
|
<template v-if="$store.state.versionData.latest.prerelease"> (pre-release) </template>
|
||||||
(pre-release)
|
|
||||||
</template>
|
|
||||||
is now available.
|
is now available.
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
@ -20,9 +16,7 @@
|
|||||||
<code>thelounge upgrade</code> on the server to upgrade packages.
|
<code>thelounge upgrade</code> on the server to upgrade packages.
|
||||||
</p>
|
</p>
|
||||||
<template v-if="$store.state.versionStatus === 'up-to-date'">
|
<template v-if="$store.state.versionStatus === 'up-to-date'">
|
||||||
<p>
|
<p>The Lounge is up to date!</p>
|
||||||
The Lounge is up to date!
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="$store.state.versionDataExpired"
|
v-if="$store.state.versionDataExpired"
|
||||||
@ -34,9 +28,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="$store.state.versionStatus === 'error'">
|
<template v-if="$store.state.versionStatus === 'error'">
|
||||||
<p>
|
<p>Information about latest release could not be retrieved.</p>
|
||||||
Information about latest release could not be retrieved.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button id="check-now" class="btn btn-small" @click="checkNow">Try again</button>
|
<button id="check-now" class="btn btn-small" @click="checkNow">Try again</button>
|
||||||
</template>
|
</template>
|
||||||
|
@ -318,9 +318,7 @@
|
|||||||
<kbd>↓</kbd> keys to highlight an item, and insert it by pressing <kbd>Tab</kbd> or
|
<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).
|
<kbd>Enter</kbd> (or by clicking the desired item).
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>Autocompletion can be disabled in settings.</p>
|
||||||
Autocompletion can be disabled in settings.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="help-item">
|
<div class="help-item">
|
||||||
<div class="subject">
|
<div class="subject">
|
||||||
@ -474,9 +472,7 @@
|
|||||||
<code>/disconnect [message]</code>
|
<code>/disconnect [message]</code>
|
||||||
</div>
|
</div>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<p>
|
<p>Disconnect from the current network with an optionally-provided message.</p>
|
||||||
Disconnect from the current network with an optionally-provided message.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -610,9 +606,7 @@
|
|||||||
<code>/op nick [...nick]</code>
|
<code>/op nick [...nick]</code>
|
||||||
</div>
|
</div>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<p>
|
<p>Give op (<code>+o</code>) to one or several users in the current channel.</p>
|
||||||
Give op (<code>+o</code>) to one or several users in the current channel.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -656,9 +650,7 @@
|
|||||||
<code>/quit [message]</code>
|
<code>/quit [message]</code>
|
||||||
</div>
|
</div>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<p>
|
<p>Disconnect from the current network with an optional message.</p>
|
||||||
Disconnect from the current network with an optional message.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -733,9 +725,7 @@
|
|||||||
<code>/whois nick</code>
|
<code>/whois nick</code>
|
||||||
</div>
|
</div>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<p>
|
<p>Retrieve information about the given user on the current network.</p>
|
||||||
Retrieve information about the given user on the current network.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -172,8 +172,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="$store.state.settings.advanced">
|
<div v-if="$store.state.settings.advanced">
|
||||||
<label class="opt">
|
<label class="opt">
|
||||||
<label for="nickPostfix" class="sr-only">
|
<label for="nickPostfix" class="opt">
|
||||||
Nick autocomplete postfix (for example a comma)
|
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>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="nickPostfix"
|
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'">
|
<template v-else-if="$store.state.pushNotificationState === 'loading'">
|
||||||
Loading…
|
Loading…
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else> Subscribe to push notifications </template>
|
||||||
Subscribe to push notifications
|
|
||||||
</template>
|
|
||||||
</button>
|
</button>
|
||||||
<div v-if="$store.state.pushNotificationState === 'nohttps'" class="error">
|
<div v-if="$store.state.pushNotificationState === 'nohttps'" class="error">
|
||||||
<strong>Warning</strong>: Push notifications are only supported over HTTPS
|
<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">
|
<div v-if="!$store.state.serverConfiguration.public && $store.state.settings.advanced">
|
||||||
<label class="opt">
|
<label class="opt">
|
||||||
<label for="highlights" class="sr-only">
|
<label for="highlights" class="opt">
|
||||||
Custom highlights (comma-separated keywords)
|
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>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="highlights"
|
id="highlights"
|
||||||
@ -360,7 +371,31 @@ This may break orientation if your browser does not support that."
|
|||||||
type="text"
|
type="text"
|
||||||
name="highlights"
|
name="highlights"
|
||||||
class="input"
|
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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -376,9 +411,7 @@ This may break orientation if your browser does not support that."
|
|||||||
>
|
>
|
||||||
<h2 id="label-change-password">Change password</h2>
|
<h2 id="label-change-password">Change password</h2>
|
||||||
<div class="password-container">
|
<div class="password-container">
|
||||||
<label for="old_password_input" class="sr-only">
|
<label for="old_password_input" class="sr-only"> Enter current password </label>
|
||||||
Enter current password
|
|
||||||
</label>
|
|
||||||
<RevealPassword v-slot:default="slotProps">
|
<RevealPassword v-slot:default="slotProps">
|
||||||
<input
|
<input
|
||||||
id="old_password_input"
|
id="old_password_input"
|
||||||
@ -404,9 +437,7 @@ This may break orientation if your browser does not support that."
|
|||||||
</RevealPassword>
|
</RevealPassword>
|
||||||
</div>
|
</div>
|
||||||
<div class="password-container">
|
<div class="password-container">
|
||||||
<label for="verify_password_input" class="sr-only">
|
<label for="verify_password_input" class="sr-only"> Repeat new password </label>
|
||||||
Repeat new password
|
|
||||||
</label>
|
|
||||||
<RevealPassword v-slot:default="slotProps">
|
<RevealPassword v-slot:default="slotProps">
|
||||||
<input
|
<input
|
||||||
id="verify_password_input"
|
id="verify_password_input"
|
||||||
|
@ -144,7 +144,7 @@ button {
|
|||||||
|
|
||||||
code,
|
code,
|
||||||
pre,
|
pre,
|
||||||
#chat .msg[data-type="motd"] .text,
|
#chat .msg[data-type="monospace_block"] .text,
|
||||||
.irc-monospace,
|
.irc-monospace,
|
||||||
textarea#user-specified-css-input {
|
textarea#user-specified-css-input {
|
||||||
font-family: Consolas, Menlo, Monaco, "Lucida Console", "DejaVu Sans Mono", "Courier New", monospace;
|
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="topic"] .from::before,
|
||||||
#chat .msg[data-type="mode_channel"] .from::before,
|
#chat .msg[data-type="mode_channel"] .from::before,
|
||||||
#chat .msg[data-type="mode"] .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"] .from::before,
|
||||||
#chat .msg[data-type="ctcp_request"] .from::before,
|
#chat .msg[data-type="ctcp_request"] .from::before,
|
||||||
#chat .msg[data-type="whois"] .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="action"] .from::before,
|
||||||
#chat .msg[data-type="plugin"] .from::before,
|
#chat .msg[data-type="plugin"] .from::before,
|
||||||
#chat .msg[data-type="raw"] .from::before,
|
#chat .msg[data-type="raw"] .from::before,
|
||||||
|
#chat .msg-statusmsg span::before,
|
||||||
#chat .msg-shown-in-active span::before,
|
#chat .msg-shown-in-active span::before,
|
||||||
#chat .toggle-button::after,
|
#chat .toggle-button::after,
|
||||||
#chat .toggle-content .more-caret::before,
|
#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-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-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-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-kick::before { content: "\f05e"; /* http://fontawesome.io/icon/ban/ */ }
|
||||||
.context-menu-action-op::before { content: "\f1fa"; /* http://fontawesome.io/icon/at/ */ }
|
.context-menu-action-set-mode::before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ }
|
||||||
.context-menu-action-voice::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-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-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 */ }
|
.context-menu-clear-history::before { content: "\f1f8"; /* https://fontawesome.com/icons/trash?style=solid */ }
|
||||||
@ -428,11 +432,21 @@ p {
|
|||||||
color: #2ecc40;
|
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 */
|
content: "\f02e"; /* https://fontawesome.com/icons/bookmark?style=solid */
|
||||||
color: var(--body-color-muted);
|
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"] .from::before,
|
||||||
#chat .msg[data-type="ctcp_request"] .from::before {
|
#chat .msg[data-type="ctcp_request"] .from::before {
|
||||||
content: "\f15c"; /* https://fontawesome.com/icons/file-alt?style=solid */
|
content: "\f15c"; /* https://fontawesome.com/icons/file-alt?style=solid */
|
||||||
@ -479,16 +493,25 @@ p {
|
|||||||
padding: 1px;
|
padding: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#chat .msg-statusmsg,
|
||||||
#chat .msg-shown-in-active {
|
#chat .msg-shown-in-active {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#chat .msg-statusmsg span::before,
|
||||||
#chat .msg-shown-in-active span::before {
|
#chat .msg-shown-in-active span::before {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
content: "\f06e"; /* https://fontawesome.com/icons/eye?style=solid */
|
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 {
|
#chat .toggle-button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
transition: opacity 0.2s, transform 0.2s;
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
@ -1448,11 +1471,11 @@ textarea.input {
|
|||||||
width: 50px;
|
width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat.hide-motd .msg[data-type="motd"] {
|
#chat.hide-motd .msg[data-command="motd"] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat .msg[data-type="motd"] .text {
|
#chat .msg[data-type="monospace_block"] .text {
|
||||||
background: #f6f6f6;
|
background: #f6f6f6;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -2020,6 +2043,14 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
|||||||
padding-bottom: 7px;
|
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 {
|
#version-checker {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -2812,7 +2843,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
|||||||
.header .topic,
|
.header .topic,
|
||||||
#chat .msg[data-type="action"] .content,
|
#chat .msg[data-type="action"] .content,
|
||||||
#chat .msg[data-type="message"] .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 .msg[data-type="notice"] .content,
|
||||||
#chat .ctcp-message,
|
#chat .ctcp-message,
|
||||||
#chat .part-reason,
|
#chat .part-reason,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
|
||||||
<meta charset="utf-8">
|
<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";
|
"use strict";
|
||||||
|
|
||||||
import socket from "../socket";
|
import socket from "../socket";
|
||||||
|
import eventbus from "../eventbus";
|
||||||
|
|
||||||
export function generateChannelContextMenu($root, channel, network) {
|
export function generateChannelContextMenu($root, channel, network) {
|
||||||
const typeMap = {
|
const typeMap = {
|
||||||
@ -115,18 +116,31 @@ export function generateChannelContextMenu($root, channel, network) {
|
|||||||
|
|
||||||
// Add menu items for queries
|
// Add menu items for queries
|
||||||
if (channel.type === "query") {
|
if (channel.type === "query") {
|
||||||
items.push({
|
items.push(
|
||||||
label: "User information",
|
{
|
||||||
type: "item",
|
label: "User information",
|
||||||
class: "action-whois",
|
type: "item",
|
||||||
action() {
|
class: "action-whois",
|
||||||
$root.switchToChannel(channel);
|
action() {
|
||||||
socket.emit("input", {
|
$root.switchToChannel(channel);
|
||||||
target: channel.id,
|
socket.emit("input", {
|
||||||
text: "/whois " + channel.name,
|
target: channel.id,
|
||||||
});
|
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") {
|
if (channel.type === "channel" || channel.type === "query") {
|
||||||
@ -135,7 +149,7 @@ export function generateChannelContextMenu($root, channel, network) {
|
|||||||
type: "item",
|
type: "item",
|
||||||
class: "clear-history",
|
class: "clear-history",
|
||||||
action() {
|
action() {
|
||||||
$root.$emit(
|
eventbus.emit(
|
||||||
"confirm-dialog",
|
"confirm-dialog",
|
||||||
{
|
{
|
||||||
title: "Clear history",
|
title: "Clear history",
|
||||||
@ -203,6 +217,17 @@ export function generateUserContextMenu($root, channel, network, user) {
|
|||||||
class: "action-whois",
|
class: "action-whois",
|
||||||
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",
|
label: "Direct messages",
|
||||||
type: "item",
|
type: "item",
|
||||||
@ -222,66 +247,93 @@ 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.
|
||||||
items.push({
|
if (!currentChannelUser.modes || currentChannelUser.modes.length < 1) {
|
||||||
label: "Kick",
|
return items;
|
||||||
type: "item",
|
}
|
||||||
class: "action-kick",
|
|
||||||
action() {
|
|
||||||
socket.emit("input", {
|
|
||||||
target: channel.id,
|
|
||||||
text: "/kick " + user.nick,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (user.mode === "@") {
|
// 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({
|
items.push({
|
||||||
label: "Revoke operator (-o)",
|
label: modeTextTemplate.give(modes[prefix]),
|
||||||
type: "item",
|
type: "item",
|
||||||
class: "action-op",
|
class: "action-set-mode",
|
||||||
action() {
|
action() {
|
||||||
socket.emit("input", {
|
socket.emit("input", {
|
||||||
target: channel.id,
|
target: channel.id,
|
||||||
text: "/deop " + user.nick,
|
text: "/mode +" + modes[prefix][1] + " " + user.nick,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
items.push({
|
items.push({
|
||||||
label: "Give operator (+o)",
|
label: modeTextTemplate.revoke(modes[prefix]),
|
||||||
type: "item",
|
type: "item",
|
||||||
class: "action-op",
|
class: "action-revoke-mode",
|
||||||
action() {
|
action() {
|
||||||
socket.emit("input", {
|
socket.emit("input", {
|
||||||
target: channel.id,
|
target: channel.id,
|
||||||
text: "/op " + user.nick,
|
text: "/mode -" + modes[prefix][1] + " " + user.nick,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (user.mode === "+") {
|
// 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({
|
items.push({
|
||||||
label: "Revoke voice (-v)",
|
label: "Kick",
|
||||||
type: "item",
|
type: "item",
|
||||||
class: "action-voice",
|
class: "action-kick",
|
||||||
action() {
|
action() {
|
||||||
socket.emit("input", {
|
socket.emit("input", {
|
||||||
target: channel.id,
|
target: channel.id,
|
||||||
text: "/devoice " + user.nick,
|
text: "/kick " + 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) {
|
if (!match.schema) {
|
||||||
match.schema = "http:";
|
match.schema = "http:";
|
||||||
match.url = "http://" + match.url;
|
match.url = "http://" + match.url;
|
||||||
|
match.noschema = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (match.schema === "//") {
|
if (match.schema === "//") {
|
||||||
match.schema = "http:";
|
match.schema = "http:";
|
||||||
match.url = "http:" + match.url;
|
match.url = "http:" + match.url;
|
||||||
|
match.noschema = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (match.schema === "mailto:" && !/^mailto:/i.test(match.url)) {
|
if (match.schema === "mailto:" && !/^mailto:/i.test(match.url)) {
|
||||||
@ -34,6 +36,8 @@ const commonSchemes = [
|
|||||||
"ts3server",
|
"ts3server",
|
||||||
"svn+ssh",
|
"svn+ssh",
|
||||||
"ssh",
|
"ssh",
|
||||||
|
"gopher",
|
||||||
|
"gemini",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const schema of commonSchemes) {
|
for (const schema of commonSchemes) {
|
||||||
@ -47,11 +51,28 @@ function findLinks(text) {
|
|||||||
return [];
|
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,
|
start: url.index,
|
||||||
end: url.lastIndex,
|
end: url.lastIndex,
|
||||||
link: url.url,
|
link: url.url,
|
||||||
}));
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = findLinks;
|
module.exports = {
|
||||||
|
findLinks,
|
||||||
|
findLinksWithSchema,
|
||||||
|
};
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import parseStyle from "./ircmessageparser/parseStyle";
|
import parseStyle from "./ircmessageparser/parseStyle";
|
||||||
import findChannels from "./ircmessageparser/findChannels";
|
import findChannels from "./ircmessageparser/findChannels";
|
||||||
import findLinks from "./ircmessageparser/findLinks";
|
import {findLinks} from "./ircmessageparser/findLinks";
|
||||||
import findEmoji from "./ircmessageparser/findEmoji";
|
import findEmoji from "./ircmessageparser/findEmoji";
|
||||||
import findNames from "./ircmessageparser/findNames";
|
import findNames from "./ircmessageparser/findNames";
|
||||||
import merge from "./ircmessageparser/merge";
|
import merge from "./ircmessageparser/merge";
|
||||||
|
@ -613,7 +613,7 @@
|
|||||||
"dragon_face": "🐲",
|
"dragon_face": "🐲",
|
||||||
"dragon": "🐉",
|
"dragon": "🐉",
|
||||||
"sauropod": "🦕",
|
"sauropod": "🦕",
|
||||||
"t-rex": "🦖",
|
"t_rex": "🦖",
|
||||||
"whale": "🐳",
|
"whale": "🐳",
|
||||||
"whale2": "🐋",
|
"whale2": "🐋",
|
||||||
"dolphin": "🐬",
|
"dolphin": "🐬",
|
||||||
@ -1082,7 +1082,7 @@
|
|||||||
"game_die": "🎲",
|
"game_die": "🎲",
|
||||||
"jigsaw": "🧩",
|
"jigsaw": "🧩",
|
||||||
"teddy_bear": "🧸",
|
"teddy_bear": "🧸",
|
||||||
"pi_ata": "🪅",
|
"pinata": "🪅",
|
||||||
"nesting_dolls": "🪆",
|
"nesting_dolls": "🪆",
|
||||||
"spades": "♠️",
|
"spades": "♠️",
|
||||||
"hearts": "♥️",
|
"hearts": "♥️",
|
||||||
@ -1240,7 +1240,7 @@
|
|||||||
"chart": "💹",
|
"chart": "💹",
|
||||||
"email": "✉️",
|
"email": "✉️",
|
||||||
"envelope": "✉️",
|
"envelope": "✉️",
|
||||||
"e-mail": "📧",
|
"e_mail": "📧",
|
||||||
"incoming_envelope": "📨",
|
"incoming_envelope": "📨",
|
||||||
"envelope_with_arrow": "📩",
|
"envelope_with_arrow": "📩",
|
||||||
"outbox_tray": "📤",
|
"outbox_tray": "📤",
|
||||||
@ -1376,7 +1376,7 @@
|
|||||||
"no_bicycles": "🚳",
|
"no_bicycles": "🚳",
|
||||||
"no_smoking": "🚭",
|
"no_smoking": "🚭",
|
||||||
"do_not_litter": "🚯",
|
"do_not_litter": "🚯",
|
||||||
"non-potable_water": "🚱",
|
"non_potable_water": "🚱",
|
||||||
"no_pedestrians": "🚷",
|
"no_pedestrians": "🚷",
|
||||||
"no_mobile_phones": "📵",
|
"no_mobile_phones": "📵",
|
||||||
"underage": "🔞",
|
"underage": "🔞",
|
||||||
|
@ -33,12 +33,63 @@ const router = new VueRouter({
|
|||||||
next();
|
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) => {
|
router.beforeEach((to, from, next) => {
|
||||||
// Disallow navigating to non-existing routes
|
// Disallow navigating to non-existing routes
|
||||||
if (store.state.appLoaded && !to.matched.length) {
|
if (!to.matched.length) {
|
||||||
next(false);
|
next(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -49,6 +100,12 @@ router.beforeEach((to, from, next) => {
|
|||||||
return;
|
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
|
// Handle closing image viewer with the browser back button
|
||||||
if (!router.app.$refs.app) {
|
if (!router.app.$refs.app) {
|
||||||
next();
|
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 = {}) {
|
function navigate(routeName, params = {}) {
|
||||||
if (router.currentRoute.name) {
|
if (router.currentRoute.name) {
|
||||||
router.push({name: routeName, params}).catch(() => {});
|
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: {
|
desktopNotifications: {
|
||||||
default: false,
|
default: false,
|
||||||
|
sync: "never",
|
||||||
apply(store, value) {
|
apply(store, value) {
|
||||||
store.commit("refreshDesktopNotificationState", null, {root: true});
|
store.commit("refreshDesktopNotificationState", null, {root: true});
|
||||||
|
|
||||||
@ -45,6 +46,10 @@ export const config = normalizeConfig({
|
|||||||
default: "",
|
default: "",
|
||||||
sync: "always",
|
sync: "always",
|
||||||
},
|
},
|
||||||
|
highlightExceptions: {
|
||||||
|
default: "",
|
||||||
|
sync: "always",
|
||||||
|
},
|
||||||
awayMessage: {
|
awayMessage: {
|
||||||
default: "",
|
default: "",
|
||||||
sync: "always",
|
sync: "always",
|
||||||
@ -57,6 +62,7 @@ export const config = normalizeConfig({
|
|||||||
},
|
},
|
||||||
notification: {
|
notification: {
|
||||||
default: true,
|
default: true,
|
||||||
|
sync: "never",
|
||||||
},
|
},
|
||||||
notifyAllMessages: {
|
notifyAllMessages: {
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -5,7 +5,7 @@ socket.on("disconnect", handleDisconnect);
|
|||||||
socket.on("connect_error", handleDisconnect);
|
socket.on("connect_error", handleDisconnect);
|
||||||
socket.on("error", handleDisconnect);
|
socket.on("error", handleDisconnect);
|
||||||
|
|
||||||
socket.on("reconnecting", function (attempt) {
|
socket.io.on("reconnect_attempt", function (attempt) {
|
||||||
store.commit("currentUserVisibleError", `Reconnecting… (attempt ${attempt})`);
|
store.commit("currentUserVisibleError", `Reconnecting… (attempt ${attempt})`);
|
||||||
updateLoadingMessage();
|
updateLoadingMessage();
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
import Vue from "vue";
|
||||||
import socket from "../socket";
|
import socket from "../socket";
|
||||||
import storage from "../localStorage";
|
import storage from "../localStorage";
|
||||||
import {router, switchToChannel, navigate, initialize as routerInitialize} from "../router";
|
import {router, switchToChannel, navigate} from "../router";
|
||||||
import store from "../store";
|
import store from "../store";
|
||||||
import parseIrcUri from "../helpers/parseIrcUri";
|
import parseIrcUri from "../helpers/parseIrcUri";
|
||||||
|
|
||||||
@ -16,10 +17,6 @@ socket.on("init", function (data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!store.state.appLoaded) {
|
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");
|
store.commit("appLoaded");
|
||||||
|
|
||||||
socket.emit("setting:get");
|
socket.emit("setting:get");
|
||||||
@ -28,24 +25,27 @@ socket.on("init", function (data) {
|
|||||||
window.g_TheLoungeRemoveLoading();
|
window.g_TheLoungeRemoveLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Review this code and make it better
|
Vue.nextTick(() => {
|
||||||
if (!router.currentRoute.name || router.currentRoute.name === "SignIn") {
|
// If we handled query parameters like irc:// links or just general
|
||||||
const channel = store.getters.findChannel(data.active);
|
// 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);
|
||||||
|
|
||||||
if (channel) {
|
if (channel) {
|
||||||
switchToChannel(channel.channel);
|
switchToChannel(channel.channel);
|
||||||
} else if (store.state.networks.length > 0) {
|
} else if (store.state.networks.length > 0) {
|
||||||
// Server is telling us to open a channel that does not exist
|
// Server is telling us to open a channel that does not exist
|
||||||
// For example, it can be unset if you first open the page after server start
|
// For example, it can be unset if you first open the page after server start
|
||||||
switchToChannel(store.state.networks[0].channels[0]);
|
switchToChannel(store.state.networks[0].channels[0]);
|
||||||
} else {
|
} else {
|
||||||
navigate("Connect");
|
navigate("Connect");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
if ("URLSearchParams" in window) {
|
|
||||||
handleQueryParams();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -154,6 +154,10 @@ function mergeChannelData(oldChannels, newChannels) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleQueryParams() {
|
function handleQueryParams() {
|
||||||
|
if (!("URLSearchParams" in window)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams(document.location.search);
|
const params = new URLSearchParams(document.location.search);
|
||||||
|
|
||||||
const cleanParams = () => {
|
const cleanParams = () => {
|
||||||
@ -169,11 +173,17 @@ function handleQueryParams() {
|
|||||||
|
|
||||||
cleanParams();
|
cleanParams();
|
||||||
router.push({name: "Connect", query: queryParams});
|
router.push({name: "Connect", query: queryParams});
|
||||||
|
|
||||||
|
return true;
|
||||||
} else if (document.body.classList.contains("public") && document.location.search) {
|
} else if (document.body.classList.contains("public") && document.location.search) {
|
||||||
// Set default connection settings from url params
|
// Set default connection settings from url params
|
||||||
const queryParams = Object.fromEntries(params.entries());
|
const queryParams = Object.fromEntries(params.entries());
|
||||||
|
|
||||||
cleanParams();
|
cleanParams();
|
||||||
router.push({name: "Connect", query: queryParams});
|
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]);
|
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
|
// 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(", ");
|
storedSettings.highlights = storedSettings.highlights.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ class Uploader {
|
|||||||
init() {
|
init() {
|
||||||
this.xhr = null;
|
this.xhr = null;
|
||||||
this.fileQueue = [];
|
this.fileQueue = [];
|
||||||
|
this.tokenKeepAlive = null;
|
||||||
|
|
||||||
document.addEventListener("dragenter", (e) => this.dragEnter(e));
|
document.addEventListener("dragenter", (e) => this.dragEnter(e));
|
||||||
document.addEventListener("dragover", (e) => this.dragOver(e));
|
document.addEventListener("dragover", (e) => this.dragOver(e));
|
||||||
@ -131,10 +132,17 @@ class Uploader {
|
|||||||
uploadNextFileInQueue(token) {
|
uploadNextFileInQueue(token) {
|
||||||
const file = this.fileQueue.shift();
|
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 (
|
if (
|
||||||
store.state.settings.uploadCanvas &&
|
store.state.settings.uploadCanvas &&
|
||||||
file.type.startsWith("image/") &&
|
file.type.startsWith("image/") &&
|
||||||
!file.type.includes("svg")
|
!file.type.includes("svg") &&
|
||||||
|
file.type !== "image/gif"
|
||||||
) {
|
) {
|
||||||
this.renderImage(file, (newFile) => this.performUpload(token, newFile));
|
this.renderImage(file, (newFile) => this.performUpload(token, newFile));
|
||||||
} else {
|
} else {
|
||||||
@ -219,6 +227,11 @@ class Uploader {
|
|||||||
handleResponse(response) {
|
handleResponse(response) {
|
||||||
this.setProgress(0);
|
this.setProgress(0);
|
||||||
|
|
||||||
|
if (this.tokenKeepAlive) {
|
||||||
|
clearInterval(this.tokenKeepAlive);
|
||||||
|
this.tokenKeepAlive = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
store.commit("currentUserVisibleError", response.error);
|
store.commit("currentUserVisibleError", response.error);
|
||||||
return;
|
return;
|
||||||
|
@ -9,6 +9,7 @@ import App from "../components/App.vue";
|
|||||||
import storage from "./localStorage";
|
import storage from "./localStorage";
|
||||||
import {router, navigate} from "./router";
|
import {router, navigate} from "./router";
|
||||||
import socket from "./socket";
|
import socket from "./socket";
|
||||||
|
import eventbus from "./eventbus";
|
||||||
|
|
||||||
import "./socket-events";
|
import "./socket-events";
|
||||||
import "./webpush";
|
import "./webpush";
|
||||||
@ -18,7 +19,7 @@ const favicon = document.getElementById("favicon");
|
|||||||
const faviconNormal = favicon.getAttribute("href");
|
const faviconNormal = favicon.getAttribute("href");
|
||||||
const faviconAlerted = favicon.dataset.other;
|
const faviconAlerted = favicon.dataset.other;
|
||||||
|
|
||||||
const vueApp = new Vue({
|
new Vue({
|
||||||
el: "#viewport",
|
el: "#viewport",
|
||||||
router,
|
router,
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -30,7 +31,7 @@ const vueApp = new Vue({
|
|||||||
},
|
},
|
||||||
closeChannel(channel) {
|
closeChannel(channel) {
|
||||||
if (channel.type === "lobby") {
|
if (channel.type === "lobby") {
|
||||||
this.$root.$emit(
|
eventbus.emit(
|
||||||
"confirm-dialog",
|
"confirm-dialog",
|
||||||
{
|
{
|
||||||
title: "Remove network",
|
title: "Remove network",
|
||||||
@ -75,7 +76,7 @@ store.watch(
|
|||||||
(sidebarOpen) => {
|
(sidebarOpen) => {
|
||||||
if (window.innerWidth > constants.mobileViewportPixels) {
|
if (window.innerWidth > constants.mobileViewportPixels) {
|
||||||
storage.set("thelounge.state.sidebar", sidebarOpen);
|
storage.set("thelounge.state.sidebar", sidebarOpen);
|
||||||
vueApp.$emit("resize");
|
eventbus.emit("resize");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -84,7 +85,7 @@ store.watch(
|
|||||||
(state) => state.userlistOpen,
|
(state) => state.userlistOpen,
|
||||||
(userlistOpen) => {
|
(userlistOpen) => {
|
||||||
storage.set("thelounge.state.userlist", userlistOpen);
|
storage.set("thelounge.state.userlist", userlistOpen);
|
||||||
vueApp.$emit("resize");
|
eventbus.emit("resize");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -100,6 +101,14 @@ store.watch(
|
|||||||
(_, getters) => getters.highlightCount,
|
(_, getters) => getters.highlightCount,
|
||||||
(highlightCount) => {
|
(highlightCount) => {
|
||||||
favicon.setAttribute("href", highlightCount > 0 ? faviconAlerted : faviconNormal);
|
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) => {
|
.then((subscription) => {
|
||||||
socket.emit("push:register", subscription.toJSON());
|
socket.emit("push:register", subscription.toJSON());
|
||||||
store.commit("pushNotificationState", "subscribed");
|
store.commit("pushNotificationState", "subscribed");
|
||||||
|
store.commit("refreshDesktopNotificationState");
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
store.commit("pushNotificationState", "unsupported");
|
store.commit("pushNotificationState", "unsupported");
|
||||||
|
store.commit("refreshDesktopNotificationState");
|
||||||
console.error(err); // eslint-disable-line no-console
|
console.error(err); // eslint-disable-line no-console
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ body {
|
|||||||
color: #f92772;
|
color: #f92772;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat .msg[data-type="motd"] .text,
|
#chat .msg[data-type="monospace_block"] .text,
|
||||||
code,
|
code,
|
||||||
.irc-monospace {
|
.irc-monospace {
|
||||||
background: #28333d;
|
background: #28333d;
|
||||||
|
@ -110,15 +110,27 @@ module.exports = {
|
|||||||
// This value is set to `false` by default.
|
// This value is set to `false` by default.
|
||||||
prefetch: false,
|
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`
|
// ### `prefetchStorage`
|
||||||
|
|
||||||
// When set to `true`, The Lounge will store and proxy prefetched images and
|
// When set to `true`, The Lounge will store and proxy prefetched images and
|
||||||
// thumbnails on the filesystem rather than directly display the content at
|
// thumbnails on the filesystem rather than directly display the content at
|
||||||
// the original URLs.
|
// the original URLs.
|
||||||
//
|
//
|
||||||
// This improves security and privacy by not exposing the client IP address,
|
// This option primarily exists to resolve mixed content warnings by not
|
||||||
// always loading images from The Lounge and making all assets secure, which
|
// loading images from http hosts. This option does not work for video
|
||||||
// resolves mixed content warnings.
|
// 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
|
// If storage is enabled, The Lounge will fetch and store images and thumbnails
|
||||||
// in the `${THELOUNGE_HOME}/storage` folder.
|
// in the `${THELOUNGE_HOME}/storage` folder.
|
||||||
@ -138,6 +150,15 @@ module.exports = {
|
|||||||
// This value is set to `2048` kilobytes by default.
|
// This value is set to `2048` kilobytes by default.
|
||||||
prefetchMaxImageSize: 2048,
|
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`
|
// ### `fileUpload`
|
||||||
//
|
//
|
||||||
// Allow uploading files to the server hosting The Lounge.
|
// 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`.
|
// numbers from 0 to 9. For example, `Guest%%%` may become `Guest123`.
|
||||||
// - `username`: User name.
|
// - `username`: User name.
|
||||||
// - `realname`: Real name.
|
// - `realname`: Real name.
|
||||||
|
// - `leaveMessage`: Network specific leave message (overrides global leaveMessage)
|
||||||
// - `join`: Comma-separated list of channels to auto-join once connected.
|
// - `join`: Comma-separated list of channels to auto-join once connected.
|
||||||
//
|
//
|
||||||
// This value is set to connect to the official channel of The Lounge on
|
// This value is set to connect to the official channel of The Lounge on
|
||||||
@ -236,6 +258,7 @@ module.exports = {
|
|||||||
username: "thelounge",
|
username: "thelounge",
|
||||||
realname: "The Lounge User",
|
realname: "The Lounge User",
|
||||||
join: "#thelounge",
|
join: "#thelounge",
|
||||||
|
leaveMessage: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
// ### `lockNetwork`
|
// ### `lockNetwork`
|
||||||
|
117
package.json
117
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "thelounge",
|
"name": "thelounge",
|
||||||
"description": "The self-hosted Web IRC client",
|
"description": "The self-hosted Web IRC client",
|
||||||
"version": "4.1.0",
|
"version": "4.3.0-pre.1",
|
||||||
"preferGlobal": true,
|
"preferGlobal": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"thelounge": "index.js"
|
"thelounge": "index.js"
|
||||||
@ -42,82 +42,83 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"busboy": "0.3.1",
|
"busboy": "0.3.1",
|
||||||
"chalk": "4.0.0",
|
"chalk": "4.1.0",
|
||||||
"cheerio": "1.0.0-rc.3",
|
"cheerio": "1.0.0-rc.5",
|
||||||
"commander": "5.0.0",
|
"commander": "7.2.0",
|
||||||
|
"content-disposition": "0.5.3",
|
||||||
"express": "4.17.1",
|
"express": "4.17.1",
|
||||||
"file-type": "14.1.4",
|
"file-type": "16.2.0",
|
||||||
"filenamify": "4.1.0",
|
"filenamify": "4.2.0",
|
||||||
"got": "10.7.0",
|
"got": "11.8.1",
|
||||||
"irc-framework": "4.8.1",
|
"irc-framework": "4.9.0",
|
||||||
"is-utf8": "0.2.1",
|
"is-utf8": "0.2.1",
|
||||||
"ldapjs": "2.0.0-pre.5",
|
"ldapjs": "2.2.3",
|
||||||
"linkify-it": "2.2.0",
|
"linkify-it": "3.0.2",
|
||||||
"lodash": "4.17.15",
|
"lodash": "4.17.20",
|
||||||
"mime-types": "2.1.26",
|
"mime-types": "2.1.28",
|
||||||
"node-forge": "0.9.1",
|
"node-forge": "0.10.0",
|
||||||
"package-json": "6.5.0",
|
"package-json": "6.5.0",
|
||||||
"read": "1.0.7",
|
"read": "1.0.7",
|
||||||
"read-chunk": "3.2.0",
|
"read-chunk": "3.2.0",
|
||||||
"semver": "7.3.2",
|
"semver": "7.3.4",
|
||||||
"socket.io": "2.3.0",
|
"socket.io": "3.1.2",
|
||||||
"tlds": "1.207.0",
|
"tlds": "1.216.0",
|
||||||
"ua-parser-js": "0.7.21",
|
"ua-parser-js": "0.7.23",
|
||||||
"uuid": "7.0.3",
|
"uuid": "8.3.2",
|
||||||
"web-push": "3.4.3",
|
"web-push": "3.4.4",
|
||||||
"yarn": "1.22.4"
|
"yarn": "1.22.10"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"sqlite3": "4.1.1"
|
"sqlite3": "5.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.9.0",
|
"@babel/core": "7.13.14",
|
||||||
"@babel/preset-env": "7.9.5",
|
"@babel/preset-env": "7.13.12",
|
||||||
"@fortawesome/fontawesome-free": "5.13.0",
|
"@fortawesome/fontawesome-free": "5.15.3",
|
||||||
"@vue/server-test-utils": "1.0.0-beta.33",
|
"@vue/server-test-utils": "1.1.3",
|
||||||
"@vue/test-utils": "1.0.0-beta.33",
|
"@vue/test-utils": "1.1.3",
|
||||||
"babel-loader": "8.1.0",
|
"babel-loader": "8.2.2",
|
||||||
"babel-plugin-istanbul": "6.0.0",
|
"babel-plugin-istanbul": "6.0.0",
|
||||||
"chai": "4.2.0",
|
"chai": "4.3.4",
|
||||||
"copy-webpack-plugin": "5.1.1",
|
"copy-webpack-plugin": "7.0.0",
|
||||||
"css-loader": "3.5.2",
|
"css-loader": "5.1.1",
|
||||||
"cssnano": "4.1.10",
|
"cssnano": "4.1.10",
|
||||||
"dayjs": "1.8.24",
|
"dayjs": "1.10.4",
|
||||||
"emoji-regex": "9.0.0",
|
"emoji-regex": "9.2.1",
|
||||||
"eslint": "6.8.0",
|
"eslint": "7.23.0",
|
||||||
"eslint-config-prettier": "6.10.1",
|
"eslint-config-prettier": "6.15.0",
|
||||||
"eslint-plugin-vue": "6.2.2",
|
"eslint-plugin-vue": "7.5.0",
|
||||||
"fuzzy": "0.1.3",
|
"fuzzy": "0.1.3",
|
||||||
"graphql-request": "1.8.2",
|
"husky": "4.3.5",
|
||||||
"husky": "4.2.5",
|
"mini-css-extract-plugin": "1.3.6",
|
||||||
"mini-css-extract-plugin": "0.9.0",
|
"mocha": "8.2.1",
|
||||||
"mocha": "7.1.1",
|
|
||||||
"mousetrap": "1.6.5",
|
"mousetrap": "1.6.5",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"npm-run-all": "4.1.5",
|
"npm-run-all": "4.1.5",
|
||||||
"nyc": "15.0.1",
|
"nyc": "15.1.0",
|
||||||
"postcss-import": "12.0.1",
|
"postcss": "8.2.5",
|
||||||
"postcss-loader": "3.0.0",
|
"postcss-import": "14.0.0",
|
||||||
|
"postcss-loader": "5.0.0",
|
||||||
"postcss-preset-env": "6.7.0",
|
"postcss-preset-env": "6.7.0",
|
||||||
"prettier": "2.0.4",
|
"prettier": "2.2.1",
|
||||||
"pretty-quick": "2.0.1",
|
"pretty-quick": "3.1.0",
|
||||||
"primer-tooltips": "2.0.0",
|
"primer-tooltips": "2.0.0",
|
||||||
"sinon": "9.0.2",
|
"sinon": "9.2.4",
|
||||||
"socket.io-client": "2.3.0",
|
"socket.io-client": "3.1.1",
|
||||||
"stylelint": "13.3.2",
|
"stylelint": "13.9.0",
|
||||||
"stylelint-config-standard": "20.0.0",
|
"stylelint-config-standard": "20.0.0",
|
||||||
"textcomplete": "0.18.1",
|
"textcomplete": "0.18.2",
|
||||||
"undate": "0.3.0",
|
"undate": "0.3.0",
|
||||||
"vue": "2.6.11",
|
"vue": "2.6.12",
|
||||||
"vue-loader": "15.9.1",
|
"vue-loader": "15.9.6",
|
||||||
"vue-router": "3.1.6",
|
"vue-router": "3.5.1",
|
||||||
"vue-server-renderer": "2.6.11",
|
"vue-server-renderer": "2.6.12",
|
||||||
"vue-template-compiler": "2.6.11",
|
"vue-template-compiler": "2.6.12",
|
||||||
"vuedraggable": "2.23.2",
|
"vuedraggable": "2.24.3",
|
||||||
"vuex": "3.2.0",
|
"vuex": "3.6.2",
|
||||||
"webpack": "4.42.1",
|
"webpack": "5.21.2",
|
||||||
"webpack-cli": "3.3.11",
|
"webpack-cli": "4.5.0",
|
||||||
"webpack-dev-middleware": "3.7.2",
|
"webpack-dev-middleware": "4.1.0",
|
||||||
"webpack-hot-middleware": "2.25.0"
|
"webpack-hot-middleware": "2.25.0"
|
||||||
},
|
},
|
||||||
"husky": {
|
"husky": {
|
||||||
|
@ -10,8 +10,8 @@
|
|||||||
},
|
},
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"depTypeList": ["devDependencies"],
|
"depTypeList": ["dependencies", "devDependencies"],
|
||||||
"extends": ["schedule:weekends"]
|
"extends": ["schedule:monthly"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ const _ = require("lodash");
|
|||||||
const colors = require("chalk");
|
const colors = require("chalk");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const GraphQLClient = require("graphql-request").GraphQLClient;
|
const got = require("got");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const semver = require("semver");
|
const semver = require("semver");
|
||||||
const util = require("util");
|
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.
|
// This class is a facade to fetching details about commits / PRs / tags / etc.
|
||||||
// for a given repository of our organization.
|
// for a given repository of our organization.
|
||||||
class RepositoryFetcher {
|
class RepositoryFetcher {
|
||||||
// Holds a GraphQLClient and the name of the repository within the
|
// Holds a Github token and repository name
|
||||||
// organization https://github.com/thelounge.
|
constructor(githubToken, repositoryName) {
|
||||||
constructor(graphqlClient, repositoryName) {
|
this.githubToken = githubToken;
|
||||||
this.graphqlClient = graphqlClient;
|
|
||||||
this.repositoryName = repositoryName;
|
this.repositoryName = repositoryName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base function that actually makes the GraphQL API call
|
// Base function that actually makes the GraphQL API call
|
||||||
async fetch(query, variables = {}) {
|
async fetch(query, variables = {}) {
|
||||||
return this.graphqlClient.request(
|
const response = await got
|
||||||
query,
|
.post("https://api.github.com/graphql", {
|
||||||
Object.assign(variables, {repositoryName: this.repositoryName})
|
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
|
// 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"}));
|
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
|
// 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
|
// entry and the list of contributors, for both pre-releases and stable
|
||||||
// releases. Templates are located at the top of this file.
|
// releases. Templates are located at the top of this file.
|
||||||
@ -803,7 +809,7 @@ async function generateChangelogEntry(changelog, targetVersion) {
|
|||||||
let template;
|
let template;
|
||||||
let contributors = [];
|
let contributors = [];
|
||||||
|
|
||||||
const codeRepo = new RepositoryFetcher(client, "thelounge");
|
const codeRepo = new RepositoryFetcher(token, "thelounge");
|
||||||
const previousVersion = await codeRepo.fetchPreviousVersion(targetVersion);
|
const previousVersion = await codeRepo.fetchPreviousVersion(targetVersion);
|
||||||
|
|
||||||
if (isPrerelease(targetVersion)) {
|
if (isPrerelease(targetVersion)) {
|
||||||
@ -817,7 +823,7 @@ async function generateChangelogEntry(changelog, targetVersion) {
|
|||||||
items = parse(codeCommitsAndPullRequests);
|
items = parse(codeCommitsAndPullRequests);
|
||||||
items.milestone = await codeRepo.fetchMilestone(targetVersion);
|
items.milestone = await codeRepo.fetchMilestone(targetVersion);
|
||||||
|
|
||||||
const websiteRepo = new RepositoryFetcher(client, "thelounge.github.io");
|
const websiteRepo = new RepositoryFetcher(token, "thelounge.github.io");
|
||||||
const previousWebsiteVersion = await websiteRepo.fetchPreviousVersion(targetVersion);
|
const previousWebsiteVersion = await websiteRepo.fetchPreviousVersion(targetVersion);
|
||||||
const websiteCommitsAndPullRequests = await websiteRepo.fetchCommitsAndPullRequestsSince(
|
const websiteCommitsAndPullRequests = await websiteRepo.fetchCommitsAndPullRequestsSince(
|
||||||
"v" + previousWebsiteVersion
|
"v" + previousWebsiteVersion
|
||||||
|
@ -19,7 +19,14 @@ const emojiModifiersRegex = /[\u{1f3fb}-\u{1f3ff}]|\u{fe0f}/gu;
|
|||||||
const cleanEmoji = emoji.emoji.replace(emojiModifiersRegex, "");
|
const cleanEmoji = emoji.emoji.replace(emojiModifiersRegex, "");
|
||||||
fullNameEmojiMap[cleanEmoji] = emoji.description;
|
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;
|
emojiMap[alias] = emoji.emoji;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,8 @@ const events = [
|
|||||||
"ctcp",
|
"ctcp",
|
||||||
"chghost",
|
"chghost",
|
||||||
"error",
|
"error",
|
||||||
|
"help",
|
||||||
|
"info",
|
||||||
"invite",
|
"invite",
|
||||||
"join",
|
"join",
|
||||||
"kick",
|
"kick",
|
||||||
@ -60,6 +62,7 @@ function Client(manager, name, config = {}) {
|
|||||||
manager: manager,
|
manager: manager,
|
||||||
messageStorage: [],
|
messageStorage: [],
|
||||||
highlightRegex: null,
|
highlightRegex: null,
|
||||||
|
highlightExceptionRegex: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = this;
|
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 = {};
|
client.config.sessions = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof client.config.clientSettings !== "object") {
|
if (!_.isPlainObject(client.config.clientSettings)) {
|
||||||
client.config.clientSettings = {};
|
client.config.clientSettings = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof client.config.browser !== "object") {
|
if (!_.isPlainObject(client.config.browser)) {
|
||||||
client.config.browser = {};
|
client.config.browser = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,6 +241,7 @@ Client.prototype.connect = function (args, isStartup = false) {
|
|||||||
nick: String(args.nick || ""),
|
nick: String(args.nick || ""),
|
||||||
username: String(args.username || ""),
|
username: String(args.username || ""),
|
||||||
realname: String(args.realname || ""),
|
realname: String(args.realname || ""),
|
||||||
|
leaveMessage: String(args.leaveMessage || ""),
|
||||||
sasl: String(args.sasl || ""),
|
sasl: String(args.sasl || ""),
|
||||||
saslAccount: String(args.saslAccount || ""),
|
saslAccount: String(args.saslAccount || ""),
|
||||||
saslPassword: String(args.saslPassword || ""),
|
saslPassword: String(args.saslPassword || ""),
|
||||||
@ -422,30 +426,32 @@ Client.prototype.inputLine = function (data) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Client.prototype.compileCustomHighlights = function () {
|
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") {
|
function compileHighlightRegex(customHighlightString) {
|
||||||
client.highlightRegex = null;
|
if (typeof customHighlightString !== "string") {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we don't have empty string in the list of highlights
|
// Ensure we don't have empty strings in the list of highlights
|
||||||
// otherwise, users get notifications for everything
|
const highlightsTokens = customHighlightString
|
||||||
const highlightsTokens = client.config.clientSettings.highlights
|
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((highlight) => escapeRegExp(highlight.trim()))
|
.map((highlight) => escapeRegExp(highlight.trim()))
|
||||||
.filter((highlight) => highlight.length > 0);
|
.filter((highlight) => highlight.length > 0);
|
||||||
|
|
||||||
if (highlightsTokens.length === 0) {
|
if (highlightsTokens.length === 0) {
|
||||||
client.highlightRegex = null;
|
return null;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client.highlightRegex = new RegExp(
|
return new RegExp(
|
||||||
`(?:^|[ .,+!?|/:<>(){}'"@&~-])(?:${highlightsTokens.join("|")})(?:$|[ .,+!?|/:<>(){}'"-])`,
|
`(?:^|[ .,+!?|/:<>(){}'"@&~-])(?:${highlightsTokens.join("|")})(?:$|[ .,+!?|/:<>(){}'"-])`,
|
||||||
"i"
|
"i"
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
Client.prototype.more = function (data) {
|
Client.prototype.more = function (data) {
|
||||||
const client = this;
|
const client = this;
|
||||||
@ -632,11 +638,11 @@ Client.prototype.names = function (data) {
|
|||||||
|
|
||||||
Client.prototype.quit = function (signOut) {
|
Client.prototype.quit = function (signOut) {
|
||||||
const sockets = this.manager.sockets.sockets;
|
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) {
|
if (room) {
|
||||||
for (const user in room.sockets) {
|
for (const user of room) {
|
||||||
const socket = sockets.connected[user];
|
const socket = sockets.sockets.get(user);
|
||||||
|
|
||||||
if (socket) {
|
if (socket) {
|
||||||
if (signOut) {
|
if (signOut) {
|
||||||
@ -649,7 +655,7 @@ Client.prototype.quit = function (signOut) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.networks.forEach((network) => {
|
this.networks.forEach((network) => {
|
||||||
network.quit(Helper.config.leaveMessage);
|
network.quit();
|
||||||
network.destroy();
|
network.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -34,18 +34,40 @@ ClientManager.prototype.init = function (identHandler, sockets) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ClientManager.prototype.findClient = function (name) {
|
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 () {
|
ClientManager.prototype.loadUsers = function () {
|
||||||
const users = this.getUsers();
|
let users = this.getUsers();
|
||||||
|
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
log.info(
|
log.info(
|
||||||
`There are currently no users. Create one with ${colors.bold("thelounge add <name>")}.`
|
`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
|
// This callback is used by Auth plugins to load users they deem acceptable
|
||||||
const callbackLoadUser = (user) => {
|
const callbackLoadUser = (user) => {
|
||||||
this.loadUser(user);
|
this.loadUser(user);
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const log = require("../log");
|
const log = require("../log");
|
||||||
const colors = require("chalk");
|
const colors = require("chalk");
|
||||||
|
const semver = require("semver");
|
||||||
const program = require("commander");
|
const program = require("commander");
|
||||||
const Helper = require("../helper");
|
const Helper = require("../helper");
|
||||||
const Utils = require("./utils");
|
const Utils = require("./utils");
|
||||||
@ -40,6 +41,21 @@ program
|
|||||||
process.exit(1);
|
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)}...`);
|
log.info(`Installing ${colors.green(json.name + " v" + json.version)}...`);
|
||||||
|
|
||||||
return Utils.executeYarnCommand("add", "--exact", `${json.name}@${json.version}`)
|
return Utils.executeYarnCommand("add", "--exact", `${json.name}@${json.version}`)
|
||||||
|
@ -11,7 +11,9 @@ program
|
|||||||
.command("add <name>")
|
.command("add <name>")
|
||||||
.description("Add a new user")
|
.description("Add a new user")
|
||||||
.on("--help", Utils.extraHelp)
|
.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())) {
|
if (!fs.existsSync(Helper.getUsersPath())) {
|
||||||
log.error(`${Helper.getUsersPath()} does not exist.`);
|
log.error(`${Helper.getUsersPath()} does not exist.`);
|
||||||
return;
|
return;
|
||||||
@ -31,6 +33,11 @@ program
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cmdObj.password) {
|
||||||
|
add(manager, name, cmdObj.password, !!cmdObj.saveLogs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
log.prompt(
|
log.prompt(
|
||||||
{
|
{
|
||||||
text: "Enter password:",
|
text: "Enter password:",
|
||||||
|
@ -11,7 +11,8 @@ program
|
|||||||
.command("reset <name>")
|
.command("reset <name>")
|
||||||
.description("Reset user password")
|
.description("Reset user password")
|
||||||
.on("--help", Utils.extraHelp)
|
.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())) {
|
if (!fs.existsSync(Helper.getUsersPath())) {
|
||||||
log.error(`${Helper.getUsersPath()} does not exist.`);
|
log.error(`${Helper.getUsersPath()} does not exist.`);
|
||||||
return;
|
return;
|
||||||
@ -30,9 +31,10 @@ program
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathReal = Helper.getUserConfigPath(name);
|
if (cmdObj.password) {
|
||||||
const pathTemp = pathReal + ".tmp";
|
change(name, cmdObj.password);
|
||||||
const user = JSON.parse(fs.readFileSync(pathReal, "utf-8"));
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
log.prompt(
|
log.prompt(
|
||||||
{
|
{
|
||||||
@ -44,17 +46,25 @@ program
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
user.password = Helper.password.hash(password);
|
change(name, password);
|
||||||
user.sessions = {};
|
|
||||||
|
|
||||||
const newUser = JSON.stringify(user, null, "\t");
|
|
||||||
|
|
||||||
// Write to a temp file first, in case the write fails
|
|
||||||
// we do not lose the original file (for example when disk is full)
|
|
||||||
fs.writeFileSync(pathTemp, newUser);
|
|
||||||
fs.renameSync(pathTemp, pathReal);
|
|
||||||
|
|
||||||
log.info(`Successfully reset password for ${colors.bold(name)}.`);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 = {};
|
||||||
|
|
||||||
|
const newUser = JSON.stringify(user, null, "\t");
|
||||||
|
|
||||||
|
// Write to a temp file first, in case the write fails
|
||||||
|
// we do not lose the original file (for example when disk is full)
|
||||||
|
fs.writeFileSync(pathTemp, newUser);
|
||||||
|
fs.renameSync(pathTemp, pathReal);
|
||||||
|
|
||||||
|
log.info(`Successfully reset password for ${colors.bold(name)}.`);
|
||||||
|
}
|
||||||
|
@ -36,6 +36,7 @@ const Helper = {
|
|||||||
setHome,
|
setHome,
|
||||||
getVersion,
|
getVersion,
|
||||||
getVersionCacheBust,
|
getVersionCacheBust,
|
||||||
|
getVersionNumber,
|
||||||
getGitCommit,
|
getGitCommit,
|
||||||
ip2hex,
|
ip2hex,
|
||||||
mergeConfig,
|
mergeConfig,
|
||||||
@ -60,6 +61,10 @@ function getVersion() {
|
|||||||
return gitCommit ? `source (${gitCommit} / ${version})` : version;
|
return gitCommit ? `source (${gitCommit} / ${version})` : version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getVersionNumber() {
|
||||||
|
return pkg.version;
|
||||||
|
}
|
||||||
|
|
||||||
let _gitCommit;
|
let _gitCommit;
|
||||||
|
|
||||||
function getGitCommit() {
|
function getGitCommit() {
|
||||||
|
@ -43,7 +43,7 @@ class Msg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.type !== Msg.Type.MOTD &&
|
this.type !== Msg.Type.MONOSPACE_BLOCK &&
|
||||||
this.type !== Msg.Type.ERROR &&
|
this.type !== Msg.Type.ERROR &&
|
||||||
this.type !== Msg.Type.TOPIC_SET_BY &&
|
this.type !== Msg.Type.TOPIC_SET_BY &&
|
||||||
this.type !== Msg.Type.MODE_CHANNEL &&
|
this.type !== Msg.Type.MODE_CHANNEL &&
|
||||||
@ -66,7 +66,7 @@ Msg.Type = {
|
|||||||
MESSAGE: "message",
|
MESSAGE: "message",
|
||||||
MODE: "mode",
|
MODE: "mode",
|
||||||
MODE_CHANNEL: "mode_channel",
|
MODE_CHANNEL: "mode_channel",
|
||||||
MOTD: "motd",
|
MONOSPACE_BLOCK: "monospace_block",
|
||||||
NICK: "nick",
|
NICK: "nick",
|
||||||
NOTICE: "notice",
|
NOTICE: "notice",
|
||||||
PART: "part",
|
PART: "part",
|
||||||
|
@ -35,6 +35,7 @@ function Network(attr) {
|
|||||||
commands: [],
|
commands: [],
|
||||||
username: "",
|
username: "",
|
||||||
realname: "",
|
realname: "",
|
||||||
|
leaveMessage: "",
|
||||||
sasl: "",
|
sasl: "",
|
||||||
saslAccount: "",
|
saslAccount: "",
|
||||||
saslPassword: "",
|
saslPassword: "",
|
||||||
@ -82,6 +83,7 @@ Network.prototype.validate = function (client) {
|
|||||||
|
|
||||||
this.username = cleanString(this.username) || "thelounge";
|
this.username = cleanString(this.username) || "thelounge";
|
||||||
this.realname = cleanString(this.realname) || "The Lounge User";
|
this.realname = cleanString(this.realname) || "The Lounge User";
|
||||||
|
this.leaveMessage = cleanString(this.leaveMessage);
|
||||||
this.password = cleanString(this.password);
|
this.password = cleanString(this.password);
|
||||||
this.host = cleanString(this.host).toLowerCase();
|
this.host = cleanString(this.host).toLowerCase();
|
||||||
this.name = cleanString(this.name);
|
this.name = cleanString(this.name);
|
||||||
@ -120,7 +122,12 @@ Network.prototype.validate = function (client) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.name = Helper.config.defaults.name;
|
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.host = Helper.config.defaults.host;
|
||||||
this.port = Helper.config.defaults.port;
|
this.port = Helper.config.defaults.port;
|
||||||
this.tls = Helper.config.defaults.tls;
|
this.tls = Helper.config.defaults.tls;
|
||||||
@ -168,8 +175,10 @@ Network.prototype.createIrcFramework = function (client) {
|
|||||||
enable_echomessage: true,
|
enable_echomessage: true,
|
||||||
enable_setname: true,
|
enable_setname: true,
|
||||||
auto_reconnect: 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);
|
this.setIrcFrameworkOptions(client);
|
||||||
@ -197,8 +206,7 @@ Network.prototype.setIrcFrameworkOptions = function (client) {
|
|||||||
this.irc.options.tls = this.tls;
|
this.irc.options.tls = this.tls;
|
||||||
this.irc.options.rejectUnauthorized = this.rejectUnauthorized;
|
this.irc.options.rejectUnauthorized = this.rejectUnauthorized;
|
||||||
this.irc.options.webirc = this.createWebIrc(client);
|
this.irc.options.webirc = this.createWebIrc(client);
|
||||||
|
this.irc.options.client_certificate = null;
|
||||||
this.irc.options.client_certificate = this.tls ? ClientCertificate.get(this.uuid) : null;
|
|
||||||
|
|
||||||
if (!this.sasl) {
|
if (!this.sasl) {
|
||||||
delete this.irc.options.sasl_mechanism;
|
delete this.irc.options.sasl_mechanism;
|
||||||
@ -206,6 +214,7 @@ Network.prototype.setIrcFrameworkOptions = function (client) {
|
|||||||
} else if (this.sasl === "external") {
|
} else if (this.sasl === "external") {
|
||||||
this.irc.options.sasl_mechanism = "EXTERNAL";
|
this.irc.options.sasl_mechanism = "EXTERNAL";
|
||||||
this.irc.options.account = {};
|
this.irc.options.account = {};
|
||||||
|
this.irc.options.client_certificate = ClientCertificate.get(this.uuid);
|
||||||
} else if (this.sasl === "plain") {
|
} else if (this.sasl === "plain") {
|
||||||
delete this.irc.options.sasl_mechanism;
|
delete this.irc.options.sasl_mechanism;
|
||||||
this.irc.options.account = {
|
this.irc.options.account = {
|
||||||
@ -246,6 +255,7 @@ Network.prototype.createWebIrc = function (client) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Network.prototype.edit = function (client, args) {
|
Network.prototype.edit = function (client, args) {
|
||||||
|
const oldNetworkName = this.name;
|
||||||
const oldNick = this.nick;
|
const oldNick = this.nick;
|
||||||
const oldRealname = this.realname;
|
const oldRealname = this.realname;
|
||||||
|
|
||||||
@ -259,6 +269,7 @@ Network.prototype.edit = function (client, args) {
|
|||||||
this.password = String(args.password || "");
|
this.password = String(args.password || "");
|
||||||
this.username = String(args.username || "");
|
this.username = String(args.username || "");
|
||||||
this.realname = String(args.realname || "");
|
this.realname = String(args.realname || "");
|
||||||
|
this.leaveMessage = String(args.leaveMessage || "");
|
||||||
this.sasl = String(args.sasl || "");
|
this.sasl = String(args.sasl || "");
|
||||||
this.saslAccount = String(args.saslAccount || "");
|
this.saslAccount = String(args.saslAccount || "");
|
||||||
this.saslPassword = String(args.saslPassword || "");
|
this.saslPassword = String(args.saslPassword || "");
|
||||||
@ -272,6 +283,14 @@ Network.prototype.edit = function (client, args) {
|
|||||||
// Sync lobby channel name
|
// Sync lobby channel name
|
||||||
this.channels[0].name = this.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)) {
|
if (!this.validate(client)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -420,7 +439,7 @@ Network.prototype.quit = function (quitMessage) {
|
|||||||
// https://ircv3.net/specs/extensions/sts#rescheduling-expiry-on-disconnect
|
// https://ircv3.net/specs/extensions/sts#rescheduling-expiry-on-disconnect
|
||||||
STSPolicies.refreshExpiration(this.host);
|
STSPolicies.refreshExpiration(this.host);
|
||||||
|
|
||||||
this.irc.quit(quitMessage || Helper.config.leaveMessage);
|
this.irc.quit(quitMessage || this.leaveMessage || Helper.config.leaveMessage);
|
||||||
};
|
};
|
||||||
|
|
||||||
Network.prototype.exportForEdit = function () {
|
Network.prototype.exportForEdit = function () {
|
||||||
@ -431,6 +450,7 @@ Network.prototype.exportForEdit = function () {
|
|||||||
"password",
|
"password",
|
||||||
"username",
|
"username",
|
||||||
"realname",
|
"realname",
|
||||||
|
"leaveMessage",
|
||||||
"sasl",
|
"sasl",
|
||||||
"saslAccount",
|
"saslAccount",
|
||||||
"saslPassword",
|
"saslPassword",
|
||||||
@ -465,6 +485,7 @@ Network.prototype.export = function () {
|
|||||||
"password",
|
"password",
|
||||||
"username",
|
"username",
|
||||||
"realname",
|
"realname",
|
||||||
|
"leaveMessage",
|
||||||
"sasl",
|
"sasl",
|
||||||
"saslAccount",
|
"saslAccount",
|
||||||
"saslPassword",
|
"saslPassword",
|
||||||
|
@ -8,25 +8,28 @@ function User(attr, prefixLookup) {
|
|||||||
_.defaults(this, attr, {
|
_.defaults(this, attr, {
|
||||||
modes: [],
|
modes: [],
|
||||||
away: "",
|
away: "",
|
||||||
mode: "",
|
|
||||||
nick: "",
|
nick: "",
|
||||||
lastMessage: 0,
|
lastMessage: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(this, "mode", {
|
||||||
|
get() {
|
||||||
|
return this.modes[0] || "";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.setModes(this.modes, prefixLookup);
|
this.setModes(this.modes, prefixLookup);
|
||||||
}
|
}
|
||||||
|
|
||||||
User.prototype.setModes = function (modes, prefixLookup) {
|
User.prototype.setModes = function (modes, prefixLookup) {
|
||||||
// irc-framework sets character mode, but The Lounge works with symbols
|
// irc-framework sets character mode, but The Lounge works with symbols
|
||||||
this.modes = modes.map((mode) => prefixLookup[mode]);
|
this.modes = modes.map((mode) => prefixLookup[mode]);
|
||||||
|
|
||||||
this.mode = this.modes[0] || "";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
User.prototype.toJSON = function () {
|
User.prototype.toJSON = function () {
|
||||||
return {
|
return {
|
||||||
nick: this.nick,
|
nick: this.nick,
|
||||||
mode: this.mode,
|
modes: this.modes,
|
||||||
lastMessage: this.lastMessage,
|
lastMessage: this.lastMessage,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -13,19 +13,6 @@ module.exports = (app) => {
|
|||||||
"webpack-hot-middleware/client?path=storage/__webpack_hmr"
|
"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);
|
const compiler = webpack(webpackConfig);
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
|
@ -48,7 +48,7 @@ exports.input = function (network, chan, cmd, args) {
|
|||||||
});
|
});
|
||||||
this.save();
|
this.save();
|
||||||
} else {
|
} else {
|
||||||
const partMessage = args.join(" ") || Helper.config.leaveMessage;
|
const partMessage = args.join(" ") || network.leaveMessage || Helper.config.leaveMessage;
|
||||||
network.irc.part(target.name, partMessage);
|
network.irc.part(target.name, partMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,14 +178,9 @@ module.exports = function (irc, network) {
|
|||||||
network.channels[0].pushMessage(
|
network.channels[0].pushMessage(
|
||||||
client,
|
client,
|
||||||
new Msg({
|
new Msg({
|
||||||
text:
|
text: `Disconnected from the network. Reconnecting in ${Math.round(
|
||||||
"Disconnected from the network. Reconnecting in " +
|
data.wait / 1000
|
||||||
Math.round(data.wait / 1000) +
|
)} seconds… (Attempt ${data.attempt})`,
|
||||||
" seconds… (Attempt " +
|
|
||||||
data.attempt +
|
|
||||||
" of " +
|
|
||||||
data.max_retries +
|
|
||||||
")",
|
|
||||||
}),
|
}),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,7 @@ const pkg = require("../../../package.json");
|
|||||||
|
|
||||||
const ctcpResponses = {
|
const ctcpResponses = {
|
||||||
CLIENTINFO: () =>
|
CLIENTINFO: () =>
|
||||||
Object.getOwnPropertyNames(ctcpResponses) // TODO: This is currently handled by irc-framework
|
Object.getOwnPropertyNames(ctcpResponses)
|
||||||
.filter((key) => key !== "CLIENTINFO" && typeof ctcpResponses[key] === "function")
|
.filter((key) => key !== "CLIENTINFO" && typeof ctcpResponses[key] === "function")
|
||||||
.join(" "),
|
.join(" "),
|
||||||
PING: ({message}) => message.substring(5),
|
PING: ({message}) => message.substring(5),
|
||||||
@ -67,17 +67,18 @@ module.exports = function (irc, network) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const target = data.from_server ? data.hostname : data.nick;
|
||||||
const response = ctcpResponses[data.type];
|
const response = ctcpResponses[data.type];
|
||||||
|
|
||||||
if (response) {
|
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
|
// Let user know someone is making a CTCP request against their nick
|
||||||
const msg = new Msg({
|
const msg = new Msg({
|
||||||
type: Msg.Type.CTCP_REQUEST,
|
type: Msg.Type.CTCP_REQUEST,
|
||||||
time: data.time,
|
time: data.time,
|
||||||
from: new User({nick: data.nick}),
|
from: new User({nick: target}),
|
||||||
hostmask: data.ident + "@" + data.hostname,
|
hostmask: data.ident + "@" + data.hostname,
|
||||||
ctcpMessage: data.message,
|
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,
|
time: data.time,
|
||||||
from: user,
|
from: user,
|
||||||
hostmask: data.ident + "@" + data.hostname,
|
hostmask: data.ident + "@" + data.hostname,
|
||||||
|
gecos: data.gecos,
|
||||||
|
account: data.account,
|
||||||
type: Msg.Type.JOIN,
|
type: Msg.Type.JOIN,
|
||||||
self: data.nick === irc.user.nick,
|
self: data.nick === irc.user.nick,
|
||||||
});
|
});
|
||||||
|
@ -5,22 +5,18 @@ const got = require("got");
|
|||||||
const URL = require("url").URL;
|
const URL = require("url").URL;
|
||||||
const mime = require("mime-types");
|
const mime = require("mime-types");
|
||||||
const Helper = require("../../helper");
|
const Helper = require("../../helper");
|
||||||
const cleanIrcMessage = require("../../../client/js/helpers/ircmessageparser/cleanIrcMessage");
|
const {findLinksWithSchema} = require("../../../client/js/helpers/ircmessageparser/findLinks");
|
||||||
const findLinks = require("../../../client/js/helpers/ircmessageparser/findLinks");
|
|
||||||
const storage = require("../storage");
|
const storage = require("../storage");
|
||||||
const currentFetchPromises = new Map();
|
const currentFetchPromises = new Map();
|
||||||
const imageTypeRegex = /^image\/.+/;
|
const imageTypeRegex = /^image\/.+/;
|
||||||
const mediaTypeRegex = /^(audio|video)\/.+/;
|
const mediaTypeRegex = /^(audio|video)\/.+/;
|
||||||
|
|
||||||
module.exports = function (client, chan, msg) {
|
module.exports = function (client, chan, msg, cleanText) {
|
||||||
if (!Helper.config.prefetch) {
|
if (!Helper.config.prefetch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove all IRC formatting characters before searching for links
|
msg.previews = findLinksWithSchema(cleanText).reduce((cleanLinks, link) => {
|
||||||
const cleanText = cleanIrcMessage(msg.text);
|
|
||||||
|
|
||||||
msg.previews = findLinks(cleanText).reduce((cleanLinks, link) => {
|
|
||||||
const url = normalizeURL(link.link);
|
const url = normalizeURL(link.link);
|
||||||
|
|
||||||
// If the URL is invalid and cannot be normalized, don't fetch it
|
// 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[property="og:description"]').attr("content") ||
|
||||||
$('meta[name="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) {
|
if (preview.head.length) {
|
||||||
preview.head = preview.head.substr(0, 100);
|
preview.head = preview.head.substr(0, 100);
|
||||||
@ -98,6 +89,17 @@ function parseHtml(preview, res, client) {
|
|||||||
preview.body = preview.body.substr(0, 300);
|
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
|
// Make sure thumbnail is a valid and absolute url
|
||||||
if (thumb.length) {
|
if (thumb.length) {
|
||||||
thumb = normalizeURL(thumb, preview.link) || "";
|
thumb = normalizeURL(thumb, preview.link) || "";
|
||||||
@ -127,7 +129,25 @@ function parseHtml(preview, res, client) {
|
|||||||
|
|
||||||
function parseHtmlMedia($, preview, client) {
|
function parseHtmlMedia($, preview, client) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
if (Helper.config.disableMediaPreview) {
|
||||||
|
reject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let foundMedia = false;
|
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) => {
|
["video", "audio"].forEach((type) => {
|
||||||
if (foundMedia) {
|
if (foundMedia) {
|
||||||
@ -203,6 +223,11 @@ function parse(msg, chan, preview, res, client) {
|
|||||||
case "image/jpg":
|
case "image/jpg":
|
||||||
case "image/jpeg":
|
case "image/jpeg":
|
||||||
case "image/webp":
|
case "image/webp":
|
||||||
|
case "image/avif":
|
||||||
|
if (!Helper.config.prefetchStorage && Helper.config.disableMediaPreview) {
|
||||||
|
return removePreview(msg, preview);
|
||||||
|
}
|
||||||
|
|
||||||
if (res.size > Helper.config.prefetchMaxImageSize * 1024) {
|
if (res.size > Helper.config.prefetchMaxImageSize * 1024) {
|
||||||
preview.type = "error";
|
preview.type = "error";
|
||||||
preview.error = "image-too-big";
|
preview.error = "image-too-big";
|
||||||
@ -228,6 +253,10 @@ function parse(msg, chan, preview, res, client) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Helper.config.disableMediaPreview) {
|
||||||
|
return removePreview(msg, preview);
|
||||||
|
}
|
||||||
|
|
||||||
preview.type = "audio";
|
preview.type = "audio";
|
||||||
preview.media = preview.link;
|
preview.media = preview.link;
|
||||||
preview.mediaType = res.type;
|
preview.mediaType = res.type;
|
||||||
@ -241,6 +270,10 @@ function parse(msg, chan, preview, res, client) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Helper.config.disableMediaPreview) {
|
||||||
|
return removePreview(msg, preview);
|
||||||
|
}
|
||||||
|
|
||||||
preview.type = "video";
|
preview.type = "video";
|
||||||
preview.media = preview.link;
|
preview.media = preview.link;
|
||||||
preview.mediaType = res.type;
|
preview.mediaType = res.type;
|
||||||
@ -354,7 +387,9 @@ function fetch(uri, headers) {
|
|||||||
retry: 0,
|
retry: 0,
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
headers: getRequestHeaders(headers),
|
headers: getRequestHeaders(headers),
|
||||||
rejectUnauthorized: false,
|
https: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
gotStream
|
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
|
// We don't need to download the file any further after we received content-type header
|
||||||
gotStream.destroy();
|
gotStream.destroy();
|
||||||
} else {
|
} else {
|
||||||
// if not image, limit download to 50kb, since we need only meta tags
|
// 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
|
// twitter.com sends opengraph meta tags within ~20kb of data for individual tweets, the default is set to 50.
|
||||||
limit = 1024 * 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))
|
.on("error", (e) => reject(e))
|
||||||
|
@ -115,6 +115,9 @@ module.exports = function (irc, network) {
|
|||||||
msg.showInActive = true;
|
msg.showInActive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove IRC formatting for custom highlight testing
|
||||||
|
const cleanMessage = cleanIrcMessage(data.message);
|
||||||
|
|
||||||
// Self messages in channels are never highlighted
|
// Self messages in channels are never highlighted
|
||||||
// Non-self messages are highlighted as soon as the nick is detected
|
// Non-self messages are highlighted as soon as the nick is detected
|
||||||
if (!msg.highlight && !msg.self) {
|
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 we still don't have a highlight, test against custom highlights if there's any
|
||||||
if (!msg.highlight && client.highlightRegex) {
|
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;
|
let match;
|
||||||
|
|
||||||
while ((match = nickRegExp.exec(data.message))) {
|
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
|
// No prefetch URLs unless are simple MESSAGE or ACTION types
|
||||||
if ([Msg.Type.MESSAGE, Msg.Type.ACTION].includes(data.type)) {
|
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);
|
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)
|
// Do not send notifications for messages older than 15 minutes (znc buffer for example)
|
||||||
if (msg.highlight && (!data.time || data.time > Date.now() - 900000)) {
|
if (msg.highlight && (!data.time || data.time > Date.now() - 900000)) {
|
||||||
let title = chan.name;
|
let title = chan.name;
|
||||||
let body = cleanIrcMessage(data.message);
|
let body = cleanMessage;
|
||||||
|
|
||||||
if (msg.type === Msg.Type.ACTION) {
|
if (msg.type === Msg.Type.ACTION) {
|
||||||
// For actions, do not include colon in the message
|
// For actions, do not include colon in the message
|
||||||
|
@ -60,16 +60,16 @@ module.exports = function (irc, network) {
|
|||||||
self: data.nick === irc.user.nick,
|
self: data.nick === irc.user.nick,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const param of data.raw_params) {
|
const users = [];
|
||||||
const users = [];
|
|
||||||
|
|
||||||
|
for (const param of data.raw_params) {
|
||||||
if (targetChan.findUser(param)) {
|
if (targetChan.findUser(param)) {
|
||||||
users.push(param);
|
users.push(param);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (users.length > 0) {
|
if (users.length > 0) {
|
||||||
msg.users = users;
|
msg.users = users;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
targetChan.pushMessage(client, msg);
|
targetChan.pushMessage(client, msg);
|
||||||
@ -117,9 +117,6 @@ module.exports = function (irc, network) {
|
|||||||
return userModeSortPriority[a] - userModeSortPriority[b];
|
return userModeSortPriority[a] - userModeSortPriority[b];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove in future
|
|
||||||
user.mode = (user.modes && user.modes[0]) || "";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!usersUpdated) {
|
if (!usersUpdated) {
|
||||||
|
@ -10,7 +10,8 @@ module.exports = function (irc, network) {
|
|||||||
|
|
||||||
if (data.motd) {
|
if (data.motd) {
|
||||||
const msg = new Msg({
|
const msg = new Msg({
|
||||||
type: Msg.Type.MOTD,
|
type: Msg.Type.MONOSPACE_BLOCK,
|
||||||
|
command: "motd",
|
||||||
text: data.motd,
|
text: data.motd,
|
||||||
});
|
});
|
||||||
lobby.pushMessage(client, msg);
|
lobby.pushMessage(client, msg);
|
||||||
@ -18,7 +19,8 @@ module.exports = function (irc, network) {
|
|||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
const msg = new Msg({
|
const msg = new Msg({
|
||||||
type: Msg.Type.MOTD,
|
type: Msg.Type.MONOSPACE_BLOCK,
|
||||||
|
command: "motd",
|
||||||
text: data.error,
|
text: data.error,
|
||||||
});
|
});
|
||||||
lobby.pushMessage(client, msg);
|
lobby.pushMessage(client, msg);
|
||||||
|
@ -183,18 +183,16 @@ class MessageStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
rows
|
rows.reverse().map((row) => {
|
||||||
.map((row) => {
|
const msg = JSON.parse(row.msg);
|
||||||
const msg = JSON.parse(row.msg);
|
msg.time = row.time;
|
||||||
msg.time = row.time;
|
msg.type = row.type;
|
||||||
msg.type = row.type;
|
|
||||||
|
|
||||||
const newMsg = new Msg(msg);
|
const newMsg = new Msg(msg);
|
||||||
newMsg.id = this.client.idMsg++;
|
newMsg.id = this.client.idMsg++;
|
||||||
|
|
||||||
return newMsg;
|
return newMsg;
|
||||||
})
|
})
|
||||||
.reverse()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -4,6 +4,7 @@ const _ = require("lodash");
|
|||||||
const log = require("../../log");
|
const log = require("../../log");
|
||||||
const colors = require("chalk");
|
const colors = require("chalk");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const semver = require("semver");
|
||||||
const Helper = require("../../helper");
|
const Helper = require("../../helper");
|
||||||
const themes = require("./themes");
|
const themes = require("./themes");
|
||||||
const packageMap = new Map();
|
const packageMap = new Map();
|
||||||
@ -93,6 +94,13 @@ function loadPackage(packageName) {
|
|||||||
throw "'thelounge' is not present in package.json";
|
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);
|
packageFile = require(packagePath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error(`Package ${colors.bold(packageName)} could not be loaded: ${colors.red(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 crypto = require("crypto");
|
||||||
const isUtf8 = require("is-utf8");
|
const isUtf8 = require("is-utf8");
|
||||||
const log = require("../log");
|
const log = require("../log");
|
||||||
|
const contentDisposition = require("content-disposition");
|
||||||
|
|
||||||
const whitelist = [
|
// Map of allowed mime types to their respecive default filenames
|
||||||
"application/ogg",
|
// that will be rendered in browser without forcing them to be downloaded
|
||||||
"audio/midi",
|
const inlineContentDispositionTypes = {
|
||||||
"audio/mpeg",
|
"application/ogg": "media.ogx",
|
||||||
"audio/ogg",
|
"audio/midi": "audio.midi",
|
||||||
"audio/vnd.wave",
|
"audio/mpeg": "audio.mp3",
|
||||||
"image/bmp",
|
"audio/ogg": "audio.ogg",
|
||||||
"image/gif",
|
"audio/vnd.wave": "audio.wav",
|
||||||
"image/jpeg",
|
"audio/flac": "audio.flac",
|
||||||
"image/png",
|
"image/bmp": "image.bmp",
|
||||||
"image/webp",
|
"image/gif": "image.gif",
|
||||||
"text/plain",
|
"image/jpeg": "image.jpg",
|
||||||
"video/mp4",
|
"image/png": "image.png",
|
||||||
"video/ogg",
|
"image/webp": "image.webp",
|
||||||
"video/webm",
|
"image/avif": "image.avif",
|
||||||
];
|
"text/plain": "text.txt",
|
||||||
|
"video/mp4": "video.mp4",
|
||||||
|
"video/ogg": "video.ogv",
|
||||||
|
"video/webm": "video.webm",
|
||||||
|
};
|
||||||
|
|
||||||
const uploadTokens = new Map();
|
const uploadTokens = new Map();
|
||||||
|
|
||||||
@ -35,17 +40,33 @@ class Uploader {
|
|||||||
socket.on("upload:auth", () => {
|
socket.on("upload:auth", () => {
|
||||||
const token = uuidv4();
|
const token = uuidv4();
|
||||||
|
|
||||||
uploadTokens.set(token, true);
|
|
||||||
|
|
||||||
socket.emit("upload:auth", token);
|
socket.emit("upload:auth", token);
|
||||||
|
|
||||||
// Invalidate the token in one minute
|
// 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) {
|
static createTokenTimeout(token) {
|
||||||
return whitelist.includes(mimeType);
|
return setTimeout(() => uploadTokens.delete(token), 60 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
static router(express) {
|
static router(express) {
|
||||||
@ -72,8 +93,21 @@ class Uploader {
|
|||||||
return res.status(404).send("Not found");
|
return res.status(404).send("Not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force a download in the browser if it's not a whitelisted type (binary or otherwise unknown)
|
// Force a download in the browser if it's not an allowed type (binary or otherwise unknown)
|
||||||
const contentDisposition = Uploader.isValidType(detectedMimeType) ? "inline" : "attachment";
|
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") {
|
if (detectedMimeType === "audio/vnd.wave") {
|
||||||
// Send a more common mime type for wave audio files
|
// Send a more common mime type for wave audio files
|
||||||
@ -81,7 +115,7 @@ class Uploader {
|
|||||||
detectedMimeType = "audio/wav";
|
detectedMimeType = "audio/wav";
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader("Content-Disposition", contentDisposition);
|
res.setHeader("Content-Disposition", disposition);
|
||||||
res.setHeader("Cache-Control", "max-age=86400");
|
res.setHeader("Cache-Control", "max-age=86400");
|
||||||
res.contentType(detectedMimeType);
|
res.contentType(detectedMimeType);
|
||||||
|
|
||||||
|
@ -167,6 +167,7 @@ module.exports = function (options = {}) {
|
|||||||
cookie: false,
|
cookie: false,
|
||||||
serveClient: false,
|
serveClient: false,
|
||||||
transports: Helper.config.transports,
|
transports: Helper.config.transports,
|
||||||
|
pingTimeout: 60000,
|
||||||
});
|
});
|
||||||
|
|
||||||
sockets.on("connect", (socket) => {
|
sockets.on("connect", (socket) => {
|
||||||
@ -363,13 +364,13 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("input", (data) => {
|
socket.on("input", (data) => {
|
||||||
if (typeof data === "object") {
|
if (_.isPlainObject(data)) {
|
||||||
client.input(data);
|
client.input(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("more", (data) => {
|
socket.on("more", (data) => {
|
||||||
if (typeof data === "object") {
|
if (_.isPlainObject(data)) {
|
||||||
const history = client.more(data);
|
const history = client.more(data);
|
||||||
|
|
||||||
if (history !== null) {
|
if (history !== null) {
|
||||||
@ -379,7 +380,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("network:new", (data) => {
|
socket.on("network:new", (data) => {
|
||||||
if (typeof data === "object") {
|
if (_.isPlainObject(data)) {
|
||||||
// prevent people from overriding webirc settings
|
// prevent people from overriding webirc settings
|
||||||
data.uuid = null;
|
data.uuid = null;
|
||||||
data.commands = null;
|
data.commands = null;
|
||||||
@ -404,7 +405,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("network:edit", (data) => {
|
socket.on("network:edit", (data) => {
|
||||||
if (typeof data !== "object") {
|
if (!_.isPlainObject(data)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,14 +419,14 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("history:clear", (data) => {
|
socket.on("history:clear", (data) => {
|
||||||
if (typeof data === "object") {
|
if (_.isPlainObject(data)) {
|
||||||
client.clearHistory(data);
|
client.clearHistory(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!Helper.config.public && !Helper.config.ldap.enable) {
|
if (!Helper.config.public && !Helper.config.ldap.enable) {
|
||||||
socket.on("change-password", (data) => {
|
socket.on("change-password", (data) => {
|
||||||
if (typeof data === "object") {
|
if (_.isPlainObject(data)) {
|
||||||
const old = data.old_password;
|
const old = data.old_password;
|
||||||
const p1 = data.new_password;
|
const p1 = data.new_password;
|
||||||
const p2 = data.verify_password;
|
const p2 = data.verify_password;
|
||||||
@ -475,13 +476,13 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("sort", (data) => {
|
socket.on("sort", (data) => {
|
||||||
if (typeof data === "object") {
|
if (_.isPlainObject(data)) {
|
||||||
client.sort(data);
|
client.sort(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("names", (data) => {
|
socket.on("names", (data) => {
|
||||||
if (typeof data === "object") {
|
if (_.isPlainObject(data)) {
|
||||||
client.names(data);
|
client.names(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -496,7 +497,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("msg:preview:toggle", (data) => {
|
socket.on("msg:preview:toggle", (data) => {
|
||||||
if (typeof data !== "object") {
|
if (!_.isPlainObject(data)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -546,7 +547,10 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||||||
client.mentions.findIndex((m) => m.msgId === msgId),
|
client.mentions.findIndex((m) => m.msgId === msgId),
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
// TODO: emit to other clients?
|
});
|
||||||
|
|
||||||
|
socket.on("mentions:hide_all", () => {
|
||||||
|
client.mentions = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!Helper.config.public) {
|
if (!Helper.config.public) {
|
||||||
@ -594,7 +598,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||||||
|
|
||||||
if (!Helper.config.public) {
|
if (!Helper.config.public) {
|
||||||
socket.on("setting:set", (newSetting) => {
|
socket.on("setting:set", (newSetting) => {
|
||||||
if (!newSetting || typeof newSetting !== "object") {
|
if (!_.isPlainObject(newSetting)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -618,7 +622,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||||||
|
|
||||||
client.save();
|
client.save();
|
||||||
|
|
||||||
if (newSetting.name === "highlights") {
|
if (newSetting.name === "highlights" || newSetting.name === "highlightExceptions") {
|
||||||
client.compileCustomHighlights();
|
client.compileCustomHighlights();
|
||||||
} else if (newSetting.name === "awayMessage") {
|
} else if (newSetting.name === "awayMessage") {
|
||||||
if (typeof newSetting.value !== "string") {
|
if (typeof newSetting.value !== "string") {
|
||||||
@ -649,7 +653,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||||||
|
|
||||||
socket.on("sign-out", (tokenToSignOut) => {
|
socket.on("sign-out", (tokenToSignOut) => {
|
||||||
// If no token provided, sign same client out
|
// If no token provided, sign same client out
|
||||||
if (!tokenToSignOut) {
|
if (!tokenToSignOut || typeof tokenToSignOut !== "string") {
|
||||||
tokenToSignOut = token;
|
tokenToSignOut = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -666,7 +670,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const socketToRemove = manager.sockets.of("/").connected[socketId];
|
const socketToRemove = manager.sockets.of("/").sockets.get(socketId);
|
||||||
|
|
||||||
socketToRemove.emit("sign-out");
|
socketToRemove.emit("sign-out");
|
||||||
socketToRemove.disconnect();
|
socketToRemove.disconnect();
|
||||||
@ -755,7 +759,7 @@ function getServerConfiguration() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function performAuthentication(data) {
|
function performAuthentication(data) {
|
||||||
if (typeof data !== "object") {
|
if (!_.isPlainObject(data)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -810,6 +814,10 @@ function performAuthentication(data) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof data.user !== "string") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const authCallback = (success) => {
|
const authCallback = (success) => {
|
||||||
// Authorization failed
|
// Authorization failed
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const expect = require("chai").expect;
|
const expect = require("chai").expect;
|
||||||
const findLinks = require("../../../../../client/js/helpers/ircmessageparser/findLinks");
|
const {
|
||||||
|
findLinks,
|
||||||
|
findLinksWithSchema,
|
||||||
|
} = require("../../../../../client/js/helpers/ircmessageparser/findLinks");
|
||||||
|
|
||||||
describe("findLinks", () => {
|
describe("findLinks", () => {
|
||||||
it("should find url", () => {
|
it("should find url", () => {
|
||||||
@ -354,4 +357,24 @@ describe("findLinks", () => {
|
|||||||
|
|
||||||
expect(actual).to.deep.equal(expected);
|
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: "",
|
password: "",
|
||||||
username: "",
|
username: "",
|
||||||
realname: "",
|
realname: "",
|
||||||
|
leaveMessage: "",
|
||||||
sasl: "plain",
|
sasl: "plain",
|
||||||
saslAccount: "testaccount",
|
saslAccount: "testaccount",
|
||||||
saslPassword: "testpassword",
|
saslPassword: "testpassword",
|
||||||
@ -109,10 +110,18 @@ describe("Network", function () {
|
|||||||
|
|
||||||
it("editing a network should enforce correct types", function () {
|
it("editing a network should enforce correct types", function () {
|
||||||
let saveCalled = false;
|
let saveCalled = false;
|
||||||
|
let nameEmitCalled = false;
|
||||||
|
|
||||||
const network = new Network();
|
const network = new Network();
|
||||||
network.edit(
|
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() {
|
save() {
|
||||||
saveCalled = true;
|
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",
|
commands: "/command 1 2 3\r\n/ping HELLO\r\r\r\r/whois test\r\n\r\n",
|
||||||
ip: "newIp",
|
ip: "newIp",
|
||||||
hostname: "newHostname",
|
hostname: "newHostname",
|
||||||
guid: "newGuid",
|
uuid: "newuuid",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(saveCalled).to.be.true;
|
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.ip).to.be.undefined;
|
||||||
expect(network.hostname).to.be.undefined;
|
expect(network.hostname).to.be.undefined;
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
|||||||
text: url,
|
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([
|
expect(message.previews).to.deep.equal([
|
||||||
{
|
{
|
||||||
@ -86,7 +86,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
|||||||
text: url,
|
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([
|
expect(message.previews).to.deep.equal([
|
||||||
{
|
{
|
||||||
@ -122,7 +122,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
|||||||
text: url,
|
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) {
|
app.get("/truncate", function (req, res) {
|
||||||
res.send(
|
res.send(
|
||||||
@ -146,7 +146,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
|||||||
text: "http://localhost:" + this.port + "/basic-og",
|
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) {
|
app.get("/basic-og", function (req, res) {
|
||||||
res.send("<title>test</title><meta property='og:title' content='opengraph test'>");
|
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",
|
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) {
|
app.get("/duplicate-tags", function (req, res) {
|
||||||
res.send(
|
res.send(
|
||||||
@ -183,7 +183,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
|||||||
text: "http://localhost:" + this.port + "/description-og",
|
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) {
|
app.get("/description-og", function (req, res) {
|
||||||
res.send(
|
res.send(
|
||||||
@ -203,7 +203,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
|||||||
text: "http://localhost:" + this.port + "/thumb",
|
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) {
|
app.get("/thumb", function (req, res) {
|
||||||
res.send(
|
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) {
|
it("should find image_src", function (done) {
|
||||||
const port = this.port;
|
const port = this.port;
|
||||||
const message = this.irc.createMessage({
|
const message = this.irc.createMessage({
|
||||||
text: "http://localhost:" + this.port + "/thumb-image-src",
|
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) {
|
app.get("/thumb-image-src", function (req, res) {
|
||||||
res.send(
|
res.send(
|
||||||
@ -250,7 +314,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
|||||||
text: "http://localhost:" + this.port + "/thumb-image-src",
|
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) {
|
app.get("/thumb-image-src", function (req, res) {
|
||||||
res.send("<link rel='image_src' href='//localhost:" + port + "/real-test-image.png'>");
|
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",
|
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) {
|
app.get("/relative-thumb", function (req, res) {
|
||||||
res.send(
|
res.send(
|
||||||
@ -294,7 +358,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
|||||||
text: "http://localhost:" + this.port + "/thumb-no-title",
|
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) {
|
app.get("/thumb-no-title", function (req, res) {
|
||||||
res.send(
|
res.send(
|
||||||
@ -320,7 +384,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
|||||||
text: "http://localhost:" + this.port + "/body-no-title",
|
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) {
|
app.get("/body-no-title", function (req, res) {
|
||||||
res.send("<meta name='description' content='hello world'>");
|
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",
|
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) {
|
app.get("/thumb-404", function (req, res) {
|
||||||
res.send(
|
res.send(
|
||||||
@ -365,7 +429,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
|||||||
text: "http://localhost:" + port + "/real-test-image.png",
|
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) {
|
this.irc.once("msg:preview", function (data) {
|
||||||
expect(data.preview.type).to.equal("image");
|
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",
|
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([
|
expect(message.previews).to.eql([
|
||||||
{
|
{
|
||||||
@ -447,7 +511,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
|||||||
text: "http://localhost:" + this.port + "/language-check",
|
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) {
|
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",
|
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) {
|
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",
|
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) {
|
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 + "",
|
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) {
|
this.irc.once("msg:preview", function (data) {
|
||||||
expect(data.preview.link).to.equal("http://localhost:" + port + "");
|
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",
|
"/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) {
|
app.get("/unicode/:q", function (req, res) {
|
||||||
res.send(`<title>${req.params.q}</title>`);
|
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 port = this.port;
|
||||||
const message = this.irc.createMessage({
|
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(message.previews).to.be.empty;
|
||||||
expect(data.preview.link).to.equal("http://localhost:" + port + "");
|
|
||||||
expect(data.preview.type).to.equal("error");
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should de-duplicate links", function (done) {
|
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([
|
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";
|
this.irc.config.browser.language = "very nice language";
|
||||||
|
|
||||||
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);
|
link(this.irc, this.network.channels[0], message, message.text);
|
||||||
process.nextTick(() => link(this.irc, this.network.channels[0], message));
|
process.nextTick(() => link(this.irc, this.network.channels[0], message, message.text));
|
||||||
|
|
||||||
app.get("/basic-og-once", function (req, res) {
|
app.get("/basic-og-once", function (req, res) {
|
||||||
requests++;
|
requests++;
|
||||||
@ -674,11 +734,11 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
|
|||||||
let responses = 0;
|
let responses = 0;
|
||||||
|
|
||||||
this.irc.config.browser.language = "first language";
|
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(() => {
|
setTimeout(() => {
|
||||||
this.irc.config.browser.language = "second language";
|
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);
|
}, 100);
|
||||||
|
|
||||||
app.get("/basic-og-once-lang", function (req, res) {
|
app.get("/basic-og-once-lang", function (req, res) {
|
||||||
|
@ -76,7 +76,7 @@ describe("Image storage", function () {
|
|||||||
text: "http://localhost:" + port + "/thumb",
|
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) {
|
this.app.get("/thumb", function (req, res) {
|
||||||
res.send(
|
res.send(
|
||||||
@ -100,7 +100,7 @@ describe("Image storage", function () {
|
|||||||
text: "http://localhost:" + port + "/real-test-image.png",
|
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) {
|
this.irc.once("msg:preview", function (data) {
|
||||||
expect(data.preview.type).to.equal("image");
|
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) {
|
this.irc.once("msg:preview", function (data) {
|
||||||
expect(data.preview.type).to.equal("link");
|
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;
|
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 () {
|
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"))).to.be.false;
|
||||||
expect(fs.existsSync(path.join(publicFolder, "index.html.tpl"))).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 () {
|
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"))).to.be.true;
|
||||||
expect(fs.existsSync(path.join(publicFolder, "css", "style.css.map"))).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) {
|
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
|
expect(fs.existsSync(path.join(publicFolder, "js", "loading-error-handlers.js"))).to.be
|
||||||
.true;
|
.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",
|
"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();
|
client.compileCustomHighlights();
|
||||||
expect(client.highlightRegex).to.be.null;
|
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,
|
loader: MiniCssExtractPlugin.loader,
|
||||||
options: {
|
options: {
|
||||||
hmr: false,
|
esModule: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -91,42 +91,50 @@ const config = {
|
|||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: "css/style.css",
|
filename: "css/style.css",
|
||||||
}),
|
}),
|
||||||
new CopyPlugin([
|
new CopyPlugin({
|
||||||
{
|
patterns: [
|
||||||
from: "./node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff*",
|
{
|
||||||
to: "fonts/[name].[ext]",
|
from:
|
||||||
},
|
"./node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff*",
|
||||||
{
|
to: "fonts/[name].[ext]",
|
||||||
from: "./client/js/loading-error-handlers.js",
|
|
||||||
to: "js/[name].[ext]",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: "./client/*",
|
|
||||||
to: "[name].[ext]",
|
|
||||||
ignore: ["index.html.tpl", "service-worker.js"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: "./client/service-worker.js",
|
|
||||||
to: "[name].[ext]",
|
|
||||||
transform(content) {
|
|
||||||
return content
|
|
||||||
.toString()
|
|
||||||
.replace("__HASH__", isProduction ? Helper.getVersionCacheBust() : "dev");
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
from: "./client/js/loading-error-handlers.js",
|
||||||
from: "./client/audio/*",
|
to: "js/[name].[ext]",
|
||||||
to: "audio/[name].[ext]",
|
},
|
||||||
},
|
{
|
||||||
{
|
from: "./client/*",
|
||||||
from: "./client/img/*",
|
to: "[name].[ext]",
|
||||||
to: "img/[name].[ext]",
|
globOptions: {
|
||||||
},
|
ignore: ["**/index.html.tpl", "**/service-worker.js"],
|
||||||
{
|
},
|
||||||
from: "./client/themes/*",
|
},
|
||||||
to: "themes/[name].[ext]",
|
{
|
||||||
},
|
from: "./client/service-worker.js",
|
||||||
]),
|
to: "[name].[ext]",
|
||||||
|
transform(content) {
|
||||||
|
return content
|
||||||
|
.toString()
|
||||||
|
.replace(
|
||||||
|
"__HASH__",
|
||||||
|
isProduction ? Helper.getVersionCacheBust() : "dev"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: "./client/audio/*",
|
||||||
|
to: "audio/[name].[ext]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: "./client/img/*",
|
||||||
|
to: "img/[name].[ext]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: "./client/themes/*",
|
||||||
|
to: "themes/[name].[ext]",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
// socket.io uses debug, we don't need it
|
// socket.io uses debug, we don't need it
|
||||||
new webpack.NormalModuleReplacementPlugin(
|
new webpack.NormalModuleReplacementPlugin(
|
||||||
/debug/,
|
/debug/,
|
||||||
|
Loading…
Reference in New Issue
Block a user