mirror of
https://github.com/thelounge/thelounge.git
synced 2024-06-08 00:32:19 +02:00
Compare commits
293 commits
v4.4.0-pre
...
master
Author | SHA1 | Date | |
---|---|---|---|
0d9c184f19 | |||
4de413070d | |||
0955d9df06 | |||
cb4aaf6a97 | |||
45c2fc87ee | |||
29fcc2da05 | |||
12679081c8 | |||
0e48014d5a | |||
4819406af5 | |||
74563effa7 | |||
cbab10f416 | |||
4dfeb899b4 | |||
3259ac596d | |||
3fbbc39cd6 | |||
9ae9482223 | |||
a3953405ed | |||
9086bc648d | |||
da2572fe25 | |||
d9977df315 | |||
cc0aa5e8e5 | |||
02df78b0f2 | |||
18b0e06855 | |||
d5db9c653b | |||
f7926267d9 | |||
8eb398c5cc | |||
36cb75ee99 | |||
1ec67a6605 | |||
8372c5a57e | |||
5567f07a7c | |||
a200bab8bd | |||
91ac363cc6 | |||
6c9d2c36a1 | |||
6241eed8f4 | |||
03151e0ab1 | |||
7f5e0f3ebf | |||
5e444be37b | |||
c8664301ba | |||
1edb5a72c1 | |||
31d987283a | |||
4ceafb653f | |||
f25fee4c6c | |||
96848c1c1b | |||
4b07e05491 | |||
fc9805545b | |||
82e4150cc8 | |||
e61e356f1e | |||
5001d607b1 | |||
8c41356ae9 | |||
e2b56cf16b | |||
92a0affba1 | |||
edb96f683b | |||
5c8951ffc3 | |||
c3fc54e158 | |||
917fdb2a0a | |||
b8400a3a46 | |||
071a5afda6 | |||
5274fdc21a | |||
b8a9fe08ab | |||
a4afa08add | |||
4614c35486 | |||
540144c417 | |||
bb7c3925c6 | |||
9898f38de6 | |||
9f2c82e152 | |||
17ba07db3b | |||
0311e5f836 | |||
4d0474b897 | |||
14b9169899 | |||
50037644c0 | |||
7287c6bcaa | |||
bfca0ca612 | |||
300bd4c84c | |||
42ea66c343 | |||
1565eb8d05 | |||
29750a3e51 | |||
3ea5170e6a | |||
fe4f497fad | |||
c20cd6bda1 | |||
1c4ce5d4a5 | |||
9c4d24d1f7 | |||
35e38d13c4 | |||
bf7eb0e727 | |||
5ee9c2b338 | |||
e15b121080 | |||
98452ccc18 | |||
a8e7022d04 | |||
60486bf5e3 | |||
46f3fd9682 | |||
56215382a3 | |||
9ab9ad0f56 | |||
0660a8772c | |||
f5c691f37b | |||
0067c30273 | |||
843db1727b | |||
e9ef59b641 | |||
fceffd42b9 | |||
b89b0cad53 | |||
c869ea9a73 | |||
9aee3e3e98 | |||
636b5c5b04 | |||
6984e8f25a | |||
e43cbb139c | |||
e57e547b74 | |||
3217536245 | |||
194b4e1a2f | |||
88c8830a17 | |||
7073584f1c | |||
8e6920af1d | |||
7bc184b252 | |||
4d237600d5 | |||
383907c2b8 | |||
f0ee3be6fb | |||
12a0b0b6f9 | |||
d716402da2 | |||
d0b71aba32 | |||
3f0ee6a961 | |||
b67e4699f5 | |||
68ba13ca12 | |||
3eb19135f5 | |||
549c445853 | |||
2466c1b1e4 | |||
f5867c3643 | |||
231c498def | |||
eeaec413d6 | |||
515f894c13 | |||
e8f6ba5b08 | |||
07276bbde4 | |||
9ad92e1860 | |||
7923d4a2cd | |||
9248358169 | |||
6ab52bc9a9 | |||
48213955b9 | |||
682b3b91aa | |||
be3e27aa19 | |||
c09f751552 | |||
fb5864ee00 | |||
3bd5b704c7 | |||
139ce47b73 | |||
45563d9a59 | |||
e2fda1fb84 | |||
a77fbb894f | |||
fe50a90235 | |||
a8be84028c | |||
25e55ce75c | |||
113e9bd2fb | |||
2b146ba3e6 | |||
f95dd29a0d | |||
91dc719c93 | |||
5af893db3a | |||
daabb76781 | |||
393d0a63b7 | |||
037fc479b8 | |||
646bafab99 | |||
d4c77c74f6 | |||
eeefeb229c | |||
29c5323bfd | |||
a12ddc75d8 | |||
dd24cb1300 | |||
ae6bae69ac | |||
b5372e3ed7 | |||
d15998d919 | |||
436bf6a180 | |||
1d2fdd95b0 | |||
eaa70caad7 | |||
aa95032760 | |||
e636121d7a | |||
083abae750 | |||
01cfe3d19d | |||
7f0b721790 | |||
edb1226b47 | |||
b0ca8e51fb | |||
21b1152f53 | |||
74aff7ee5a | |||
14d9ff247d | |||
aec8d0b033 | |||
60ddf17124 | |||
20227b174c | |||
d18182da8b | |||
ea35040b42 | |||
97f553eea8 | |||
6603c1a6e6 | |||
73a529acea | |||
2f40d9dbcc | |||
d1561f8ebc | |||
ec75ff00cb | |||
884a92c74b | |||
77b64c546b | |||
cc59e6b578 | |||
fb1d79f5fa | |||
100ff3c198 | |||
d893feff1c | |||
88a5fef4ea | |||
5b64ecbe68 | |||
5024acd7dc | |||
bbfada251c | |||
8cec292f2c | |||
22ae594cc3 | |||
1c6bec2323 | |||
9105fbc23a | |||
8c54cd50d8 | |||
59de6afd3f | |||
b506966b08 | |||
785ec0a0e2 | |||
250433c875 | |||
d4d5a8e386 | |||
bcca111a4d | |||
b686059c6b | |||
ff77a33663 | |||
d308e74183 | |||
f999db99c7 | |||
76c896aea2 | |||
08413c7b6b | |||
48301b1ca3 | |||
03795a2718 | |||
2985727996 | |||
9f05a75c39 | |||
c0b38d4762 | |||
2878f87879 | |||
447a237fc6 | |||
430a865e9f | |||
816b7686e3 | |||
2e019a2fdb | |||
4f9ca3e192 | |||
57c4d5513c | |||
3e21bfcbea | |||
607b9fc96a | |||
1a1153aed6 | |||
54ff563247 | |||
ed0a47fe2c | |||
3af4ad1076 | |||
79fae26f39 | |||
c6b1913b91 | |||
071ad96d9b | |||
2ef8b37009 | |||
8aa5e33b1d | |||
43a2b397a2 | |||
c43a47afc1 | |||
14575c94cf | |||
303f53fe72 | |||
06f1387f7b | |||
c5326e8795 | |||
355c5d6fa4 | |||
7ac2a6fd77 | |||
c4879fdbba | |||
4255c1cdec | |||
ae9d312b2a | |||
f7c6ba5eb1 | |||
4d60d9c282 | |||
af49ef21ea | |||
7a9ddc01e1 | |||
8f08cf3d0b | |||
e05871fd2f | |||
ede48ab034 | |||
4c6fa550aa | |||
9388960497 | |||
7bce779254 | |||
a7b85db990 | |||
f4ef11de3f | |||
3a63484762 | |||
04b2bf036b | |||
3066f48a69 | |||
ed40c83a2b | |||
f21f665384 | |||
12d9ef34f0 | |||
9ee1cf13a8 | |||
ba1a4206a6 | |||
59cf29ef4a | |||
8e43d8083d | |||
3cd0a75ac2 | |||
21d1dbaad6 | |||
90ad06a29a | |||
0c7cc85184 | |||
3be805bd38 | |||
e25c296901 | |||
4babd17383 | |||
b408843ff1 | |||
0f3487c533 | |||
21ada132b1 | |||
2f162daee1 | |||
3ac9c36d95 | |||
c30da27f95 | |||
edc6f77c64 | |||
0dd74a93bf | |||
7f6059d5b7 | |||
8ca9ee873b | |||
dfed1dd757 | |||
d67277d996 | |||
95aaba43fa | |||
3e7255ff20 | |||
86e376fc03 | |||
899762cddd | |||
063aca948c | |||
25642fbe98 |
|
@ -93,6 +93,7 @@ const tsRules = defineConfig({
|
|||
// note you must disable the base rule as it can report incorrect errors
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": ["error"],
|
||||
"@typescript-eslint/no-redundant-type-constituents": "off",
|
||||
},
|
||||
}).rules;
|
||||
|
||||
|
|
27
.github/workflows/build.yml
vendored
27
.github/workflows/build.yml
vendored
|
@ -1,5 +1,8 @@
|
|||
name: Build
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
|
@ -9,21 +12,19 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# EOL: April 2023
|
||||
- os: ubuntu-latest
|
||||
node_version: 14.x
|
||||
|
||||
# EOL: April 2024
|
||||
- os: ubuntu-latest
|
||||
node_version: 16.x
|
||||
- os: macOS-latest
|
||||
node_version: 16.x
|
||||
- os: windows-latest
|
||||
node_version: 16.x
|
||||
|
||||
# EOL: April 2025
|
||||
- os: macOS-latest
|
||||
node_version: 18.x
|
||||
- os: windows-latest
|
||||
node_version: 18.x
|
||||
- os: ubuntu-latest
|
||||
node_version: 18.x
|
||||
# EOL: April 2026
|
||||
- os: ubuntu-latest
|
||||
node_version: 20.x
|
||||
# EOL: April June 2024
|
||||
- os: ubuntu-latest
|
||||
node_version: 21.x
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
|
@ -31,7 +32,7 @@ jobs:
|
|||
- uses: actions/checkout@master
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node_version }}
|
||||
|
||||
|
|
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
|
@ -1,5 +1,9 @@
|
|||
name: Release
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: v*
|
||||
|
@ -14,8 +18,9 @@ jobs:
|
|||
- uses: actions/checkout@master
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "latest"
|
||||
registry-url: "https://registry.npmjs.org/"
|
||||
|
||||
- name: Install
|
||||
|
@ -31,13 +36,13 @@ jobs:
|
|||
|
||||
- name: Publish latest
|
||||
if: "!contains(github.ref, '-')"
|
||||
run: npm publish --tag latest
|
||||
run: npm publish --tag latest --provenance
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
|
||||
|
||||
- name: Publish next
|
||||
if: contains(github.ref, '-')
|
||||
run: npm publish --tag next
|
||||
run: npm publish --tag next --provenance
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
|
||||
|
||||
|
|
386
CHANGELOG.md
386
CHANGELOG.md
|
@ -4,6 +4,392 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
<!-- New entries go after this line -->
|
||||
|
||||
## v4.4.3 - 2024-04-01
|
||||
|
||||
The Lounge finally gains the ability to automatically clean up sqlite databases.
|
||||
Note that cleaning existing, large databases can take a significant amount of time
|
||||
and running a database `VACUUM` will use up ~2x the current DB disc space for a short period.
|
||||
|
||||
If you enable the storagePolicy, stop the running instance and run `thelounge storage clean`.
|
||||
This will force a full cleanup once, rather than doing so incrementally and will release all the
|
||||
disc space back to the OS.
|
||||
|
||||
As usual, we follow the Node.js release schedule, so the minimum Node.js version required is now 18.
|
||||
|
||||
Many thanks to all the contributors to this release, be that documentation, code or maintaining the packages.
|
||||
Your help is greatly appreciated!
|
||||
|
||||
For more details, [see the full changelog](https://github.com/thelounge/thelounge/compare/v4.4.1...v4.4.3)
|
||||
|
||||
### Added
|
||||
|
||||
- Sign in: use v-model ([`c5326e8`](https://github.com/thelounge/thelounge/commit/c5326e87958b1e99ca9405da5c8d17e3f45c983c) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Add comments explaining behavior when echo-message is not available ([`43a2b39`](https://github.com/thelounge/thelounge/commit/43a2b397a2efc65c7214893846831376bb880138) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Fix semver for prerelease versions #4744 ([`8aa5e33`](https://github.com/thelounge/thelounge/commit/8aa5e33b1d9e0a56e51481c227bf7d61fdd7b21f) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: add migrations support and introduce primary key ([`2ef8b37`](https://github.com/thelounge/thelounge/commit/2ef8b3700945deb9a113ddf4e3010ad36556deef) by [@brunnre8](https://github.com/brunnre8))
|
||||
- test/link: use helper for url creation ([`c6b1913`](https://github.com/thelounge/thelounge/commit/c6b1913b919421ab2b70093218422a390d822c75) by [@brunnre8](https://github.com/brunnre8))
|
||||
- test/storage: use helper for url creation ([`79fae26`](https://github.com/thelounge/thelounge/commit/79fae26f396081b6f557ae7b4f0c8fd4649b6a74) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Respect bind setting for all outgoing requests ([`3af4ad1`](https://github.com/thelounge/thelounge/commit/3af4ad1076330428da41f4205bb069d714b2a4e2) by [@brunnre8](https://github.com/brunnre8))
|
||||
- bump emoji-regex to latest ([`ed0a47f`](https://github.com/thelounge/thelounge/commit/ed0a47fe2c10a2512832c9365a863967f9fc1ee0) by [@brunnre8](https://github.com/brunnre8))
|
||||
- use shebang for generate-emoji script ([`1a1153a`](https://github.com/thelounge/thelounge/commit/1a1153aed638de0e5e2ca4089cb7656bbfa4394a) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Respect bind setting for all outgoing requests ([`2878f87`](https://github.com/thelounge/thelounge/commit/2878f87879cab30eabedbe2376507dae33295f22) by [@brunnre8](https://github.com/brunnre8))
|
||||
- store: use return type over a type cast ([#4770](https://github.com/thelounge/thelounge/pull/4770) by [@brunnre8](https://github.com/brunnre8))
|
||||
- don't crash on rDNS failure ([`8c54cd5`](https://github.com/thelounge/thelounge/commit/8c54cd50d8431481a70dec26a66a5343f2bbbd2c) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: fix typo fetch_rollbacks ([`884a92c`](https://github.com/thelounge/thelounge/commit/884a92c74bb669ff9a94c5a1c164912a9bd9891b) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: don't modify global array during tests ([`ec75ff0`](https://github.com/thelounge/thelounge/commit/ec75ff00cb8fdcef1857749ce6d033860e1ca157) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: return new version in downgrade() ([`d1561f8`](https://github.com/thelounge/thelounge/commit/d1561f8ebccacd0277d185626f3737bfd23bc99e) by [@brunnre8](https://github.com/brunnre8))
|
||||
- cli: don't fail if stderr is not in json format ([`97f553e`](https://github.com/thelounge/thelounge/commit/97f553eea8ed4a57f6d760a767425159f6451e08) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: use variadic function for serialize_run ([`60ddf17`](https://github.com/thelounge/thelounge/commit/60ddf17124af8e451412b14a11910ded894979d8) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: accept db connection string ([`aec8d0b`](https://github.com/thelounge/thelounge/commit/aec8d0b03341691a0211d172538afc61560a919c) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: implement deleteMessages ([`14d9ff2`](https://github.com/thelounge/thelounge/commit/14d9ff247d51e77640bc0f37464804eadc822dd7) by [@brunnre8](https://github.com/brunnre8))
|
||||
- introduce storage cleaner ([`74aff7e`](https://github.com/thelounge/thelounge/commit/74aff7ee5a9440a653859879390191031f81153e) by [@brunnre8](https://github.com/brunnre8))
|
||||
- cleaner: expose cli task to do cleaning + vacuum ([`21b1152`](https://github.com/thelounge/thelounge/commit/21b1152f5357f47586456949cadfb9876a0613da) by [@brunnre8](https://github.com/brunnre8))
|
||||
- wire up storage cleaner upon server start ([`b0ca8e5`](https://github.com/thelounge/thelounge/commit/b0ca8e51fb21b23859f95406f41dfe1ce273f419) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: add msg type index to speed up cleaner ([`edb1226`](https://github.com/thelounge/thelounge/commit/edb1226b474e9dc74d096201220d8e675821ac21) by [@brunnre8](https://github.com/brunnre8))
|
||||
- add storage cleaner ([`7f0b721`](https://github.com/thelounge/thelounge/commit/7f0b7217906abf90343f5b91dc7ceaa650dd058f) by [@brunnre8](https://github.com/brunnre8))
|
||||
- scripts: fix generate-config-doc, handle usage errors ([#4807](https://github.com/thelounge/thelounge/pull/4807) by [@flotwig](https://github.com/flotwig))
|
||||
- router: don't use next() in router guards ([#4783](https://github.com/thelounge/thelounge/pull/4783) by [@brunnre8](https://github.com/brunnre8))
|
||||
- linkify: Add web+ schema support ([`ae6bae6`](https://github.com/thelounge/thelounge/commit/ae6bae69ac2c915c3dcac4262168da46f8eddf39) by [@SoniEx2](https://github.com/SoniEx2))
|
||||
- linkify: simplify noscheme detection logic ([`dd24cb1`](https://github.com/thelounge/thelounge/commit/dd24cb13002b76ba0a67abfa11faedaa455df828) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Add shortcut to navigate between channels with undread msgs ([`daabb76`](https://github.com/thelounge/thelounge/commit/daabb7678172fc6b6d7c6eebc6fad40b6f84ea39) by [@Nachtalb](https://github.com/Nachtalb))
|
||||
- Remove husky, add githooks-install ([#4826](https://github.com/thelounge/thelounge/pull/4826) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Testing setup ([#4825](https://github.com/thelounge/thelounge/pull/4825) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Remove Node.js 16 from package.json and testing matrix ([`113e9bd`](https://github.com/thelounge/thelounge/commit/113e9bd2fb9a5154c048234d8ebbd8c0a61070d1) by [@MaxLeiter](https://github.com/MaxLeiter))
|
||||
- server: remove version from CTCP response ([`45563d9`](https://github.com/thelounge/thelounge/commit/45563d9a5938ae4fa46da8a2d6c51fc829ebb910) by [@flotwig](https://github.com/flotwig))
|
||||
|
||||
### Documentation
|
||||
|
||||
On the [website repository](https://github.com/thelounge/thelounge.github.io):
|
||||
|
||||
- Merge branch 'localInstall' ([`8c0d5a5`](https://github.com/thelounge/thelounge.github.io/commit/8c0d5a58075fc1035f5c71675847823751e1f98d) by [@brunnre8](https://github.com/brunnre8))
|
||||
- docs: update docker image to point to the new ghcr.io repository ([`5d7c993`](https://github.com/thelounge/thelounge.github.io/commit/5d7c993b9e26050b482550cb3f16aa11e0b99d9e) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Add "Hide all chat messages containing a link in a specific channel" … ([`993cf8b`](https://github.com/thelounge/thelounge.github.io/commit/993cf8b00e35ffeff1c20d122defc32d09e236b3) by [@zDEFz](https://github.com/zDEFz))
|
||||
- ctcp: remove stale link to code (#273) ([`379c34d`](https://github.com/thelounge/thelounge.github.io/commit/379c34d88aa73dd86078af7757a4536bb9958e02) by [@brunnre8](https://github.com/brunnre8))
|
||||
- docs: sync config.js.md (add prefetchTimeout, update ldap) (#275) ([`51dfc80`](https://github.com/thelounge/thelounge.github.io/commit/51dfc803415946e985c36317ea362ba625c67a3c) by [@flotwig](https://github.com/flotwig))
|
||||
- Removing #thelounge-scandinavia due to inactivity (#278) ([`403cc6a`](https://github.com/thelounge/thelounge.github.io/commit/403cc6aa05cd30a0f9a86b81369ec0c9f1ffd24f) by [@fnutt](https://github.com/fnutt))
|
||||
- Nodejs documentation link update (#277) ([`06e4725`](https://github.com/thelounge/thelounge.github.io/commit/06e47254cc6b98eabe4d527b1ce6be6f7ea7b9eb) by [@xfisbest](https://github.com/xfisbest))
|
||||
- Add installation instructions for Gentoo (#276) ([`52be432`](https://github.com/thelounge/thelounge.github.io/commit/52be432b36cabc7a9d393a07e7702e3aebff8075) by [@rahilarious](https://github.com/rahilarious))
|
||||
|
||||
### Dependency updates
|
||||
|
||||
- chore(deps): update dependency webpack-hot-middleware to v2.25.4 ([`06f1387`](https://github.com/thelounge/thelounge/commit/06f1387f7b5ff374b52bc4aeac06d6e936bc00f4) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @vue/test-utils to v2.4.0 ([`303f53f`](https://github.com/thelounge/thelounge/commit/303f53fe72a6cde53410821b2d59c81db90d308a) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency postcss to v8.4.26 ([`54ff563`](https://github.com/thelounge/thelounge/commit/54ff56324714bd5c6221250d02491f20b7ede6df) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/linkify-it to v3.0.3 ([`2985727`](https://github.com/thelounge/thelounge/commit/2985727996c1e84fefce06e5c2a0da02a8b6ccb6) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/bcryptjs to v2.4.4 ([`48301b1`](https://github.com/thelounge/thelounge/commit/48301b1ca31f0eb145695f320c81d0047e6883e6) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- build(deps): bump word-wrap from 1.2.3 to 1.2.5 ([`08413c7`](https://github.com/thelounge/thelounge/commit/08413c7b6b78f460bdee31239a87e6f86e14dda2) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
|
||||
- chore(deps): update dependency postcss to v8.4.31 [security] ([`ff77a33`](https://github.com/thelounge/thelounge/commit/ff77a3366305c23180e6e509f5f39d285edca8d1) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/cheerio to v0.22.33 ([`b686059`](https://github.com/thelounge/thelounge/commit/b686059c6bf2f2014497d7dceb093422c5fb8fc2) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/content-disposition to v0.5.7 ([`bcca111`](https://github.com/thelounge/thelounge/commit/bcca111a4dd42e8b648acee1da9548a0c677d056) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/lodash to v4.14.200 ([`d4d5a8e`](https://github.com/thelounge/thelounge/commit/d4d5a8e386df60c69826fb9b1c63c138a1503640) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/mousetrap to v1.6.13 ([`250433c`](https://github.com/thelounge/thelounge/commit/250433c87549b59f34cd4d3933364a3766cf587e) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update actions/setup-node action to v4 ([`785ec0a`](https://github.com/thelounge/thelounge/commit/785ec0a0e26f2233ddea6f51ef16cd5cc5e14e40) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/bcryptjs to v2.4.5 ([`b506966`](https://github.com/thelounge/thelounge/commit/b506966b08fba11ab9b8b88268c9371dac78c314) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/is-utf8 to v0.2.2 ([`59de6af`](https://github.com/thelounge/thelounge/commit/59de6afd3fdbeb894e8cf39321c786220bbcf66b) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/bcryptjs to v2.4.6 ([`2f40d9d`](https://github.com/thelounge/thelounge/commit/2f40d9dbcca6fff43f1a66a2e0efb826e22cd4b4) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/cheerio to v0.22.35 ([`73a529a`](https://github.com/thelounge/thelounge/commit/73a529acea765705c1903762106d8f8f3221e6fc) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/content-disposition to v0.5.8 ([`aa95032`](https://github.com/thelounge/thelounge/commit/aa95032760761cc7e28d802ed9bec93d4a807335) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/is-utf8 to v0.2.3 ([`eaa70ca`](https://github.com/thelounge/thelounge/commit/eaa70caad7e578af4bf5f1603c5008b9159a04e6) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/linkify-it to v3.0.5 ([`1d2fdd9`](https://github.com/thelounge/thelounge/commit/1d2fdd95b0ee698bbdc85eb70fd02f47d46e86da) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/lodash to v4.14.202 ([`fe50a90`](https://github.com/thelounge/thelounge/commit/fe50a9023509412b8c6d981053b469e27b5a49c0) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/mousetrap to v1.6.15 ([`a77fbb8`](https://github.com/thelounge/thelounge/commit/a77fbb894ff550cabf7d6f54e06296babdeb2b67) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/node to v17.0.45 ([`e2fda1f`](https://github.com/thelounge/thelounge/commit/e2fda1fb84da9cdbb445d6ebfe0f9795cb83633d) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- build(deps): bump semver from 7.3.5 to 7.5.2 ([`447a237`](https://github.com/thelounge/thelounge/commit/447a237fc6d54e59e563e982a406e16011c57b7a) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
|
||||
- build(deps): bump get-func-name from 2.0.0 to 2.0.2 ([`d308e74`](https://github.com/thelounge/thelounge/commit/d308e7418367e880f1b5454ade8267f5996bd035) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
|
||||
- build(deps): bump @babel/traverse from 7.18.9 to 7.23.6 ([`20227b1`](https://github.com/thelounge/thelounge/commit/20227b174c4bf375af1168c60ef57e6124c199f4) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
|
||||
- update emoji ([`607b9fc`](https://github.com/thelounge/thelounge/commit/607b9fc96a9ca933154dcc082fb2bb6dd545a2db) by [@brunnre8](https://github.com/brunnre8))
|
||||
- update dependency cheerio to v1.0.0-rc.12 ([`3e21bfc`](https://github.com/thelounge/thelounge/commit/3e21bfcbea579c08f0c02d692e59242653b553b3) by [@brunnre8](https://github.com/brunnre8))
|
||||
- update dependency webpack-hot-middleware to v2.25.4 ([`57c4d55`](https://github.com/thelounge/thelounge/commit/57c4d5513cfe6f0770a89330932dc07623c35e26) by [@brunnre8](https://github.com/brunnre8))
|
||||
- update dependency @vue/test-utils to v2.4.0 ([`4f9ca3e`](https://github.com/thelounge/thelounge/commit/4f9ca3e1923837f2886a58df4605255229b200b2) by [@brunnre8](https://github.com/brunnre8))
|
||||
- update dependency @types/lodash to v4.14.195 ([`2e019a2`](https://github.com/thelounge/thelounge/commit/2e019a2fdba684ad4cef15f55e514ae7a1bc8edf) by [@brunnre8](https://github.com/brunnre8))
|
||||
- update dependency @types/chai to v4.3.5 ([`816b768`](https://github.com/thelounge/thelounge/commit/816b7686e36aaac36371a5bfbcd2648443bc4e48) by [@brunnre8](https://github.com/brunnre8))
|
||||
- update dependency postcss to v8.4.26 ([`430a865`](https://github.com/thelounge/thelounge/commit/430a865e9fd7218ac8b0deaa6fc0841341b823ab) by [@brunnre8](https://github.com/brunnre8))
|
||||
- update @types/mousetrap ([`139ce47`](https://github.com/thelounge/thelounge/commit/139ce47b73a4907da0e2737dbb245bc686330ec1) by [@brunnre8](https://github.com/brunnre8))
|
||||
- bump caniuse-lite ([`22ae594`](https://github.com/thelounge/thelounge/commit/22ae594cc3d6905c82aa2238f4cd68506acf79a3) by [@brunnre8](https://github.com/brunnre8))
|
||||
|
||||
## v4.4.2-rc.1 - 2024-02-19 [Pre-release]
|
||||
|
||||
The Lounge finally gains the ability to automatically clean up sqlite databases.
|
||||
Note that cleaning existing, large databases can take a significant amount of time
|
||||
and running a database `VACUUM` will use up ~2x the current DB disc space for a short period.
|
||||
If you enable the storagePolicy, stop the running instance and run `thelounge storage clean`.
|
||||
This will force a full cleanup once, rather than doing so incrementally and will release all the
|
||||
disc space back to the OS.
|
||||
|
||||
As usual, we follow the Node.js release schedule, so the minimum Node.js version required is now 18.
|
||||
|
||||
[See the full changelog](https://github.com/thelounge/thelounge/compare/v4.4.1...v4.4.2-rc.1)
|
||||
|
||||
This is a release candidate (RC) for v4.4.2 to ensure maximum stability for public release.
|
||||
Bugs may be fixed, but no further features will be added until the next stable version.
|
||||
|
||||
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.4.1 - 2023-06-13
|
||||
|
||||
Small bug fix release that addresses the bugs reported since v4.4.0
|
||||
|
||||
- fixes the image preview buttons disappearing.
|
||||
- Restores the ability to change the password via the user interface.
|
||||
|
||||
Following the [Node.js maintenance schedule](https://nodejs.dev/en/about/releases/), The Lounge now needs at least Node.js 16 to run.
|
||||
|
||||
For more details, [see the full changelog](https://github.com/thelounge/thelounge/compare/v4.4.0...v4.4.1) and [milestone](https://github.com/thelounge/thelounge/milestone/43?closed=1).
|
||||
|
||||
### Fixed
|
||||
|
||||
- linkPreview: Pass channel prop ([`9388960`](https://github.com/thelounge/thelounge/commit/93889604973eeefb3a875e3ad5c9de737638888c) by [@brunnre8](https://github.com/brunnre8))
|
||||
- client: fix password change input ([`8f08cf3`](https://github.com/thelounge/thelounge/commit/8f08cf3d0bd5b839016000afca1c700c74193f39) by [@brunnre8](https://github.com/brunnre8))
|
||||
|
||||
### Documentation
|
||||
|
||||
On the [website repository](https://github.com/thelounge/thelounge.github.io):
|
||||
|
||||
- Document local installation of packages ([`c72092e`](https://github.com/thelounge/thelounge.github.io/commit/c72092e2f8feab66f912b2c63c5a0572b123ea29) by [@brunnre8](https://github.com/brunnre8))
|
||||
- docs: update docker image to point to the new ghcr.io repository ([`b43d002`](https://github.com/thelounge/thelounge.github.io/commit/b43d002584757709fff19dfdcf558c9d378f3d61) by [@williamboman](https://github.com/williamboman))
|
||||
- Fix deb link ([`485570d`](https://github.com/thelounge/thelounge.github.io/commit/485570d4c4027296c546c2773272e4b44b0db06a) by [@brunnre8](https://github.com/brunnre8))
|
||||
- deb: directly link to latest ([`c9a8ad9`](https://github.com/thelounge/thelounge.github.io/commit/c9a8ad95bbfc62f9ef704581fc742b069ff605fe) by [@brunnre8](https://github.com/brunnre8))
|
||||
|
||||
### Internals
|
||||
|
||||
- Remove unused code ([`7bce779`](https://github.com/thelounge/thelounge/commit/7bce77925449e2bcfa2db5d66dc5f808e04058c7) by [@brunnre8](https://github.com/brunnre8))
|
||||
- settings: make missing_field msg descriptive ([`7a9ddc0`](https://github.com/thelounge/thelounge/commit/7a9ddc01e1819da8d28860548a82736f35283ab0) by [@brunnre8](https://github.com/brunnre8))
|
||||
|
||||
### Dependency updates
|
||||
|
||||
- build(deps): bump socket.io-parser from 4.2.1 to 4.2.3 ([`af49ef2`](https://github.com/thelounge/thelounge/commit/af49ef21ea3fed54c0807a4d87f9c0f9f70017c3) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
|
||||
- bump socket.io-parser from 4.2.1 to 4.2.3 ([`4d60d9c`](https://github.com/thelounge/thelounge/commit/4d60d9c282490ad63a1ff61e57e9a6c7a5fb9684) by [@brunnre8](https://github.com/brunnre8))
|
||||
|
||||
## v4.4.1-rc.2 - 2023-05-27 [Pre-release]
|
||||
|
||||
Restore the ability to change the password via the user interface.
|
||||
|
||||
[See the full changelog](https://github.com/thelounge/thelounge/compare/v4.4.0-rc.1...v4.4.1-rc.2)
|
||||
|
||||
This is a release candidate (RC) for v4.4.1 to ensure maximum stability for public release.
|
||||
Bugs may be fixed, but no further features will be added until the next stable version.
|
||||
|
||||
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.
|
||||
|
||||
## v4.4.1-rc.1 - 2023-05-20 [Pre-release]
|
||||
|
||||
Small bug fix release that addresses the image preview buttons disappearing.
|
||||
|
||||
Following the [Node.js maintenance schedule](https://nodejs.dev/en/about/releases/), The Lounge now needs at least Node.js 16 to run.
|
||||
|
||||
[See the full changelog](https://github.com/thelounge/thelounge/compare/v4.4.0...v4.4.1-rc.1)
|
||||
|
||||
This is a release candidate (RC) for v4.4.1 to ensure maximum stability for public release.
|
||||
Bugs may be fixed, but no further features will be added until the next stable version.
|
||||
|
||||
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.4.0 - 2023-04-22
|
||||
|
||||
For more details, [see the full changelog](https://github.com/thelounge/thelounge/compare/v4.3.1...v4.4.0) and [milestone](https://github.com/thelounge/thelounge/milestone/42?closed=1).
|
||||
|
||||
This is mostly a developer focused release. Max, Eric and others rewrote the whole thing in TypeScript / Vue 3,
|
||||
which should make it much easier to add features and find bugs in the future. So huge kudos from the rest of the team!
|
||||
|
||||
Additionally, there's the obvious grab bag of fixes, dependency updates and improvements.
|
||||
Settings are now grouped and easier to navigate for new users.
|
||||
|
||||
Following the Node.js maintenance schedule, The Lounge now needs at least Node.js 14 to run.
|
||||
|
||||
A big thanks to everyone who contributed in any way to this release, your help is much appreciated.
|
||||
|
||||
Considering that a bunch of our dependencies had security issues assigned to them, all users are advised to update to the new version.
|
||||
|
||||
Packagers: Considering the switch to TypeScript, the server build now emits to the dist/ folder.
|
||||
You might need to adapt your build scripts.
|
||||
|
||||
### Added
|
||||
|
||||
- Add prefetchTimeout ([`aa7db1e`](https://github.com/thelounge/thelounge/commit/aa7db1e7f787350f4102f98b85a2e8173173f92a) by [@brunnre8](https://github.com/brunnre8))
|
||||
|
||||
### Changed
|
||||
|
||||
- Refactor settings to their own tabs and routes ([#4489](https://github.com/thelounge/thelounge/pull/4489) by [@MaxLeiter](https://github.com/MaxLeiter))
|
||||
- allow away and back to be collapsed ([#4669](https://github.com/thelounge/thelounge/pull/4669) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Kill TL when ident can't start up (#4512) ([`37d7de7`](https://github.com/thelounge/thelounge/commit/37d7de7671cf07f8a7fb3a8b3ea32122a738b646) by [@brunnre8](https://github.com/brunnre8))
|
||||
- enable znc/playback even without message storage ([`c8115e2`](https://github.com/thelounge/thelounge/commit/c8115e22acf4a6e34a1546fd2fc273c76cbb7e86) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Remove node 12, add node 18. Bump minimum node version 14 (#4552) ([`9dbb6e5`](https://github.com/thelounge/thelounge/commit/9dbb6e5e1923dc1a2d3d69b0eac2778ff8cf5d3b) by [@xPaw](https://github.com/xPaw))
|
||||
- linkPreviews: Enforce TLS validity ([`621fa92`](https://github.com/thelounge/thelounge/commit/621fa92036d59aa6558df828a1ff48136eed19ce) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Use nick as a realname fallback ([`30e9f45`](https://github.com/thelounge/thelounge/commit/30e9f45fac5b675ddadf5f904f0d0f05a7cdb5f9) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Plugins: include pre-releases in compatibility lookup (#4506) ([`e4840b4`](https://github.com/thelounge/thelounge/commit/e4840b4d75ff4dc79083955ebd9dfbdd7dd7ea8a) by [@brunnre8](https://github.com/brunnre8))
|
||||
- install: expand ~ for local paths ([`e221e70`](https://github.com/thelounge/thelounge/commit/e221e708c1237eaa3088d97aebf8bf4869843dc6) by [@brunnre8](https://github.com/brunnre8))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix the alignment of the header buttons ([#4539](https://github.com/thelounge/thelounge/pull/4539) by [@ronilaukkarinen](https://github.com/ronilaukkarinen))
|
||||
- Fix user commands not working ([#4594](https://github.com/thelounge/thelounge/pull/4594) by [@xPaw](https://github.com/xPaw))
|
||||
- Don't crash on oidentd socket race condition ([#4695](https://github.com/thelounge/thelounge/pull/4695) by [@maxpoulin64](https://github.com/maxpoulin64))
|
||||
- cli: don't error if the user folder doesn't exist (#4508) ([`8153198`](https://github.com/thelounge/thelounge/commit/815319810c28ffe17119a5dc62f7eac33eba12f5) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Fix user file permissions on create (#4507) ([`d7bba32`](https://github.com/thelounge/thelounge/commit/d7bba325a73b1898edfa4299c4525749e174bbac) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: Escape '%' and '\_' in search queries. (#4487) ([`20ed3e6`](https://github.com/thelounge/thelounge/commit/20ed3e6dc5cf482e38d537444163e98b2bae0879) by [@progval](https://github.com/progval))
|
||||
- set 'video/quicktime' to 'video/mp4' (#4495) ([`57b1e51`](https://github.com/thelounge/thelounge/commit/57b1e51e9f0f65e0866f5a809b12efaaf277536a) by [@xnaas](https://github.com/xnaas))
|
||||
- Preserve client certificate ([`c9c8cad`](https://github.com/thelounge/thelounge/commit/c9c8cadb1a00f01d00920792cc129077aa6934fd) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Remove uploading event listeners on ChatInput unmount (#4600) ([`80f65c5`](https://github.com/thelounge/thelounge/commit/80f65c5b7276c466d2032fb3a7822fa39df3c685) by [@MaxLeiter](https://github.com/MaxLeiter))
|
||||
- Potentially fix saving new networks (#4599) ([`d72d869`](https://github.com/thelounge/thelounge/commit/d72d8694bbea9fde7bf86275fb77b4c4c8a168ec) by [@MaxLeiter](https://github.com/MaxLeiter))
|
||||
- Fix regex escape for prefix patterns ([`d6e1af0`](https://github.com/thelounge/thelounge/commit/d6e1af0e7dedb34dcd9932105ee4f2ddbe98e221) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Fix ctcp request message (#4603) ([`c8cd405`](https://github.com/thelounge/thelounge/commit/c8cd4057bc4ef19271720fc6b893b9c74e690457) by [@brunnre8](https://github.com/brunnre8))
|
||||
- connect: Trim white space from user input fields (#4623) ([`0fa2035`](https://github.com/thelounge/thelounge/commit/0fa203569a62ee6bc6062b781729c7d801ccb8ba) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Search: Clear earlier searches when a new one is executed ([`83e11b0`](https://github.com/thelounge/thelounge/commit/83e11b0143e599a40924cab856636beeca6df27c) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Fix previous-source calculation (#4656) ([`073a38e`](https://github.com/thelounge/thelounge/commit/073a38ef1ef3c46740a028d4cbe7ebe4c7a08526) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Fix sidebar swipe flicker after letting go ([`502780c`](https://github.com/thelounge/thelounge/commit/502780c5a3e3455d977d8873506f1be51946fa68) by [@xPaw](https://github.com/xPaw))
|
||||
- search: ignore searchResults if it isn't the active query ([`0ebc3a5`](https://github.com/thelounge/thelounge/commit/0ebc3a574c42185c818ca8795a56d8eb58a20f4e) by [@brunnre8](https://github.com/brunnre8))
|
||||
- fix motd display to match settings ([#4726])(https://github.com/thelounge/thelounge/pull/4726) by [@SpaceLenore](https://github.com/SpaceLenore))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix misleading LDAP filiter in default config ([`f785acb`](https://github.com/thelounge/thelounge/commit/f785acb07d78ae791a24a39821a93afb81616934) by [@goodspeed34](https://github.com/goodspeed34))
|
||||
- Use correct option name (filter instead of ldapFilter) in config.js c… ([`4af5fc6`](https://github.com/thelounge/thelounge/commit/4af5fc6f33b43d64adcebcbf5aa8c4dceaad493f) by [@murph](https://github.com/murph))
|
||||
- Add password param to /join docs ([`8b1a4f7`](https://github.com/thelounge/thelounge/commit/8b1a4f72fa79e12b43ff3073f0d48b13d93008e7) by [@aab12345](https://github.com/aab12345))
|
||||
- install: Document file: prefix in cli help ([`31739b8`](https://github.com/thelounge/thelounge/commit/31739b8ac9ff95a03c374b32cc9bce2163d05d1e) by [@brunnre8](https://github.com/brunnre8))
|
||||
|
||||
On the [website repository](https://github.com/thelounge/thelounge.github.io):
|
||||
|
||||
- Link directly to themes on npm (#261) ([`410f5d0`](https://github.com/thelounge/thelounge.github.io/commit/410f5d077676cf597397b01acdc81414cc3dbc01) by [@jeremiah-rs](https://github.com/jeremiah-rs))
|
||||
- Don't use yarn link for source installs ([#262](https://github.com/thelounge/thelounge.github.io/pull/262) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Add Insecure Warning CSS (#264) ([`95efa48`](https://github.com/thelounge/thelounge.github.io/commit/95efa482668af7997c7058cf01dff611efdea644) by [@aab12345](https://github.com/aab12345))
|
||||
- Add custom nick colors section to custom css guide (#265) ([`63847c3`](https://github.com/thelounge/thelounge.github.io/commit/63847c346b6e49ddcdb34f5b733b57e3db8cc2df) by [@xnaas](https://github.com/xnaas))
|
||||
- Fix Apache configuration syntax ([`41cb84e`](https://github.com/thelounge/thelounge.github.io/commit/41cb84ee70f5dc4a6920dfd1916fdf5eb00f190c) by [@lucaswerkmeister](https://github.com/lucaswerkmeister))
|
||||
- Be more explicit about needing Yarn 1 (Classic) (#268) ([`1eff267`](https://github.com/thelounge/thelounge.github.io/commit/1eff26768a437e2bac1b62982da5ae02fdbda950) by [@SyntaxColoring](https://github.com/SyntaxColoring))
|
||||
- Don't mention `npm` command for installation ([`7e936c2`](https://github.com/thelounge/thelounge.github.io/commit/7e936c2814b2902855570e928e0f13a40e17fce7) by [@SyntaxColoring](https://github.com/SyntaxColoring))
|
||||
- Update reverse-proxies.md ([`afc7e29`](https://github.com/thelounge/thelounge.github.io/commit/afc7e2957211f0fa9a4f986fb4a0a03547384a6d) by [@PeGaSuS-Coder](https://github.com/PeGaSuS-Coder))
|
||||
|
||||
### Internals
|
||||
|
||||
- Decouple server ([#4686](https://github.com/thelounge/thelounge/pull/4686) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Tests/server: Tear down test fixtures in the order they were setup ([#4715](https://github.com/thelounge/thelounge/pull/4715) by [@progval](https://github.com/progval))
|
||||
- Refactor config out of Helper (#4558) ([`d4cc2dd`](https://github.com/thelounge/thelounge/commit/d4cc2dd361bd2f166924dd18efdc57634d67bc19) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Convert configs to cjs, move babel to own file, combine webpack confi… ([`c205b89`](https://github.com/thelounge/thelounge/commit/c205b895233f5d7c58ef44bad31ccee777f3b95d) by [@nemchik](https://github.com/nemchik))
|
||||
- Fix yarn dev (#4574) ([`2e3d9a6`](https://github.com/thelounge/thelounge/commit/2e3d9a6265d4c0d0168729a60b319bea236e098b) by [@nemchik](https://github.com/nemchik))
|
||||
- TypeScript and Vue 3 (#4559) ([`dd05ee3`](https://github.com/thelounge/thelounge/commit/dd05ee3a656cb5eb5d0ab7620dbc7a1cfa4102ab) by [@MaxLeiter](https://github.com/MaxLeiter))
|
||||
- Added client type checking to webpack (#4619) ([`117c5fa`](https://github.com/thelounge/thelounge/commit/117c5fa3fdbd2787bc1df521627b7b07fc1522c6) by [@antoniomika](https://github.com/antoniomika))
|
||||
- don't call search on a disabled msg provider ([`bea4545`](https://github.com/thelounge/thelounge/commit/bea4545abffe738dfeb025b36817490c1b5fa61d) by [@brunnre8](https://github.com/brunnre8))
|
||||
- extract migrations ([`f04a066`](https://github.com/thelounge/thelounge/commit/f04a06682d3690b571dc0b9720baa79b687b9465) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: error if sqlite isn't enabled but search() is called ([`cebc6d0`](https://github.com/thelounge/thelounge/commit/cebc6d069fa609de918881854414768fadc87fed) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: move export to bottom of the file ([`f6b2921`](https://github.com/thelounge/thelounge/commit/f6b292107ee4e627562d170babcb272cfa102a1e) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: fix docstring ([`e62b169`](https://github.com/thelounge/thelounge/commit/e62b169a6abab4b2a0df34a5da21c92136ba3790) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: add run helper function ([`89ee537`](https://github.com/thelounge/thelounge/commit/89ee5373643d1c5cb664401de745109bf7bcb77c) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: create serialize_fetchall helper function ([`cc3302e`](https://github.com/thelounge/thelounge/commit/cc3302e8743633b3b87e15fb54a964510b2466d1) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: use serialize_fetchall in getMessages ([`ee8223c`](https://github.com/thelounge/thelounge/commit/ee8223c2006ad31fc746824b495125b321da4bf8) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: use serialize_fetchall in search ([`5e1cbe3`](https://github.com/thelounge/thelounge/commit/5e1cbe32f95aca776fe4dff550a0c8c369460417) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: add serialize_get ([`bbe81bb`](https://github.com/thelounge/thelounge/commit/bbe81bb2fa9001762df90c1a267afa0239ebb7c7) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: convert migrations to async ([`f068fd4`](https://github.com/thelounge/thelounge/commit/f068fd429012c47648faf8c4d751f972062709bd) by [@brunnre8](https://github.com/brunnre8))
|
||||
- messageStorage: convert to async ([`d62dd3e`](https://github.com/thelounge/thelounge/commit/d62dd3e62d106009cbded2fd9af13fe9fae35ae5) by [@brunnre8](https://github.com/brunnre8))
|
||||
- SearchResults: remove computed search prop ([`6b617f8`](https://github.com/thelounge/thelounge/commit/6b617f893d73fb9e8304d228336cf574c29992a3) by [@brunnre8](https://github.com/brunnre8))
|
||||
- SearchResults: Fix search progess upon search ([`dca2024`](https://github.com/thelounge/thelounge/commit/dca202427aa543d43d18fb72ae10ffa51b3b6c60) by [@brunnre8](https://github.com/brunnre8))
|
||||
- SearchResults: remove dead code (#4639) ([`53f6041`](https://github.com/thelounge/thelounge/commit/53f6041f42ac36b5d69fc05cc66618ea0fe67a88) by [@brunnre8](https://github.com/brunnre8))
|
||||
- SearchQuery: offset is always a number ([`8095d9e`](https://github.com/thelounge/thelounge/commit/8095d9e88a0018d2ac559ab01488d2736b4fe5e6) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Search: fix off by one offset error ([`51c9ce0`](https://github.com/thelounge/thelounge/commit/51c9ce078d15efafd677cff525b681dcec51fdd5) by [@brunnre8](https://github.com/brunnre8))
|
||||
- keybinds: Fix invalid return ([`0765d20`](https://github.com/thelounge/thelounge/commit/0765d209f2ce204e2a3e86c56a7c2108a0487a6f) by [@brunnre8](https://github.com/brunnre8))
|
||||
- server: the http{,s} server can't be null ([`1597c2c`](https://github.com/thelounge/thelounge/commit/1597c2c56ec932859ebc77e31eda8c164f196388) by [@brunnre8](https://github.com/brunnre8))
|
||||
- make getClientConfiguration type safe ([`fd14b4a`](https://github.com/thelounge/thelounge/commit/fd14b4a17203bc043b8c9c1f371c2c5ced96eef7) by [@brunnre8](https://github.com/brunnre8))
|
||||
- remove VueApp from router ([`dfb4217`](https://github.com/thelounge/thelounge/commit/dfb4217167bd20232bf2bdc443454a7ea9cc1094) by [@brunnre8](https://github.com/brunnre8))
|
||||
- search: fix order of result merging ([`8204c34`](https://github.com/thelounge/thelounge/commit/8204c3481ad1e5eb3f59cabdb5c3c52936094b48) by [@brunnre8](https://github.com/brunnre8))
|
||||
- store: addMessageSearchResults shouldn't accept null ([`982816f`](https://github.com/thelounge/thelounge/commit/982816ff2015077fe2903180df6420005c73b33e) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: synchronize enable() internally ([`2d4143b`](https://github.com/thelounge/thelounge/commit/2d4143b7798c9cf0600280a5a79cb9061585be0e) by [@brunnre8](https://github.com/brunnre8))
|
||||
- messagestorage: remove implementation details from interface ([`661d5cb`](https://github.com/thelounge/thelounge/commit/661d5cb5b0d6c3aebb9a83ac4c5115d0411b3f39) by [@brunnre8](https://github.com/brunnre8))
|
||||
- textStorage: rip out client instance ([`52b8a2a`](https://github.com/thelounge/thelounge/commit/52b8a2a78e62dfdcdd2313e8c7e81a7b07f383e2) by [@brunnre8](https://github.com/brunnre8))
|
||||
- sqlite: Remove client from sqlitestorage ([`958a948`](https://github.com/thelounge/thelounge/commit/958a948456d1a0c3c97bb60e8759e8f9f5578ac8) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Fix uploader mount/unmount lifecycle ([`2ce374f`](https://github.com/thelounge/thelounge/commit/2ce374fe858992c5c930b0c49bf40cba2928f839) by [@maxpoulin64](https://github.com/maxpoulin64))
|
||||
- Fix git commit not being available in dist build ([`2f04150`](https://github.com/thelounge/thelounge/commit/2f04150461fbd538b09e58d8c1beb33ee0db18ce) by [@xPaw](https://github.com/xPaw))
|
||||
- network: add getLobby accessor ([`fade6a8`](https://github.com/thelounge/thelounge/commit/fade6a8d2ec5d621d761e2f6a716c5e59f4a9770) by [@brunnre8](https://github.com/brunnre8))
|
||||
- pluginCommand: type it and guard against bad input ([`4023323`](https://github.com/thelounge/thelounge/commit/402332340b727d7f4087b1f24dcd4eecf16b0891) by [@brunnre8](https://github.com/brunnre8))
|
||||
- packaging: Use an include list in package.json ([`efd24fd`](https://github.com/thelounge/thelounge/commit/efd24fd12cad9192d6f333c5a3c01c33ad23b0c6) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Fix incorrect typing of dehydrated networks and channels ([`76098d7`](https://github.com/thelounge/thelounge/commit/76098d7e766ad074eb6278ee487410f1f02817c3) [@progval](https://github.com/progval))
|
||||
- Client: move socket connection out of the constructor ([`a049a01`](https://github.com/thelounge/thelounge/commit/a049a01aeb2b09edaaf46411bb764c14a607b343) [@progval](https://github.com/progval))
|
||||
- Fix test wording ([`d58fb84`](https://github.com/thelounge/thelounge/commit/d58fb845651fe2859313c05a80cdcdebc27a8c68) [@progval](https://github.com/progval))
|
||||
- Remove override of UserConfig ([`320075e`](https://github.com/thelounge/thelounge/commit/320075e376eecc0843f57b2f9b3207f8f245930e) [@progval](https://github.com/progval))
|
||||
- Fix sqlite query invocation in test ([`845daba`](https://github.com/thelounge/thelounge/commit/845dabad53c4a47b6c39f7529ad02ec810c5ed48) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Fix config typing and make Client easier to test ([`eb509f7`](https://github.com/thelounge/thelounge/commit/eb509f7100869427d3f8b4dbd54692bf12630e67) by [@brunnre8](https://github.com/brunnre8))
|
||||
- server/client: refactor command input ([`4e954b9`](https://github.com/thelounge/thelounge/commit/4e954b919c86ad17f6c7f934de4aa8d6fe5b9b1d) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Clean up command input code ([`e8b6434`](https://github.com/thelounge/thelounge/commit/e8b6434144998693532ce2853c049e878f158d63) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Inline logger into changelog script ([#4717](https://github.com/thelounge/thelounge/pull/4717) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Fix load of channels from user config ([`0c7cc85`](https://github.com/thelounge/thelounge/commit/0c7cc85184d9f90987000ffcddfa2b9581bb96cb) Val Lorentz)
|
||||
- style: Put user colors into the smallest possible scope ([`f55f772`](https://github.com/thelounge/thelounge/commit/f55f772659a505ceb8751d8728c22c810afed018) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Fix Morning theme nick colors ([#4690](https://github.com/thelounge/thelounge/pull/4690) by [@maxpoulin64](https://github.com/maxpoulin64))
|
||||
- Publish to npm with provenance ([#4724])(https://github.com/thelounge/thelounge/pull/4724) by [@xPaw](https://github.com/xPaw))
|
||||
|
||||
### Dependency updates
|
||||
|
||||
_Aka the boring bits... It's the last section too, so feel free to gloss over it_
|
||||
|
||||
- fix(deps): update dependency got to v11.8.5 [security] ([#4596](https://github.com/thelounge/thelounge/pull/4596) by [@renovate](https://github.com/apps/renovate))
|
||||
- `sqlite3` ([#4541](https://github.com/thelounge/thelounge/pull/4541))
|
||||
- chore(deps): update dependency sqlite3 to v5.0.6 ([`da02350`](https://github.com/thelounge/thelounge/commit/da02350725291be79c0d6c5d15261a2e0ef72313) by [@renovate-bot](https://github.com/renovate-bot))
|
||||
- chore(deps): update dependency @textcomplete/core to v0.1.11 (#4555) ([`99c48db`](https://github.com/thelounge/thelounge/commit/99c48dbcea2ebe08d64a38946d81301fbfe66ee2) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update babel monorepo (#4554) ([`38f1352`](https://github.com/thelounge/thelounge/commit/38f13525e6104ee332c64d2df20bfe2694bc7fe5) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency mocha to v9.2.2 (#4581) ([`194b85b`](https://github.com/thelounge/thelounge/commit/194b85be4d93813f763b06264124d5545ba8aa27) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency sqlite3 to v5.0.8 (#4564) ([`ddcee53`](https://github.com/thelounge/thelounge/commit/ddcee5371acfe960c53e85e97405d005953dec3c) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @textcomplete/textarea to v0.1.12 ([`e972165`](https://github.com/thelounge/thelounge/commit/e97216518adb9ac7d6ef458c362a591a0f56ed14) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/content-disposition to v0.5.5 ([`740618c`](https://github.com/thelounge/thelounge/commit/740618ca499aeb2efb8ffd4f0363b5cf841a49dc) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @textcomplete/core to v0.1.12 ([`0cb4791`](https://github.com/thelounge/thelounge/commit/0cb4791cd02c0fd2e578edc1366124117529ac10) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency sqlite3 to v5.0.10 ([`520646a`](https://github.com/thelounge/thelounge/commit/520646a212e08f971c870e6f464712a90e198d66) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- fix(deps): update dependency file-type to v16.5.4 [security] ([`0495761`](https://github.com/thelounge/thelounge/commit/0495761c4485ac86b43ced638a361b905e7ddc60) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): lock file maintenance ([`57ed37c`](https://github.com/thelounge/thelounge/commit/57ed37c1fda4024ae655de2defdf4af68ade69fe) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- Revert "chore(deps): update dependency @textcomplete/core to v0.1.12" ([`3240997`](https://github.com/thelounge/thelounge/commit/32409973478ecb88290447faa7f2639a6d5c4d1f) by [@brunnre8](https://github.com/brunnre8))
|
||||
- chore(deps): update dependency sqlite3 to v5.1.2 ([`5a803cc`](https://github.com/thelounge/thelounge/commit/5a803ccd239e42fe8853b4c615e82ef2c64bbc14) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @vue/test-utils to v2.2.1 ([`cb17f8d`](https://github.com/thelounge/thelounge/commit/cb17f8d87f9eac3b3449455d47c5ddaec09c0c5d) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency postcss to v8.4.18 ([`5a4a39b`](https://github.com/thelounge/thelounge/commit/5a4a39b9d1f4a49ddc2f9c5551f9fd28d0307a4b) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency chai to v4.3.7 ([`0ad033f`](https://github.com/thelounge/thelounge/commit/0ad033fe0aac01e0f4512428fda0e93ddefdcfb6) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/is-utf8 to v0.2.1 ([`b5ea7cc`](https://github.com/thelounge/thelounge/commit/b5ea7cceb3ff6a13f0ee20f4ed1c017b983d7d8c) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/lodash to v4.14.188 ([`dfe288e`](https://github.com/thelounge/thelounge/commit/dfe288ef166a0ac07f538ee5a07c2f7b65ee15f9) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/chai to v4.3.4 ([`19307d0`](https://github.com/thelounge/thelounge/commit/19307d05e70f8b7ed9ab3d6177c7c9ae6c93a438) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency postcss to v8.4.19 ([`2218841`](https://github.com/thelounge/thelounge/commit/221884166df61feb43513205c982b271b299f074) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/lodash to v4.14.191 ([`d61ab7e`](https://github.com/thelounge/thelounge/commit/d61ab7e7a084018d68444c4b0ef8d14702142d84) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency sqlite3 to v5.1.4 ([`c854d27`](https://github.com/thelounge/thelounge/commit/c854d27d3d8451ea25051dc356dc8f101542f9a1) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/ws to v8.5.4 ([`502fb7a`](https://github.com/thelounge/thelounge/commit/502fb7a7050edbecd8e34b6c30664e0bdcfc4a6c) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @vue/test-utils to v2.2.7 ([`6b23b87`](https://github.com/thelounge/thelounge/commit/6b23b87063c893ce588321929598e579401e16ee) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency sinon to v13.0.2 ([`90d17ca`](https://github.com/thelounge/thelounge/commit/90d17cacc155a3a6bafd76411b2e00997347a24b) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency vue-loader to v17.0.1 ([`2f8dc01`](https://github.com/thelounge/thelounge/commit/2f8dc01930f921f4de23dff29abfc703fdbefdbc) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency webpack-dev-middleware to v5.3.3 ([`4742a07`](https://github.com/thelounge/thelounge/commit/4742a077211229191867033320c0efc876a9404c) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @vue/test-utils to v2.3.1 ([`50e8d2a`](https://github.com/thelounge/thelounge/commit/50e8d2a8903b1c1c826208850f46a5d98dbf6458) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency postcss to v8.4.21 ([`8e249d4`](https://github.com/thelounge/thelounge/commit/8e249d46afb234a4a1def2cbcc0204c4edd52bdc) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency sqlite3 to v5.1.5 [security] ([`bc4c308`](https://github.com/thelounge/thelounge/commit/bc4c3082b852e175e55003c8b91b2a69a7d8283f) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency webpack to v5.76.0 [security] ([`a67cee1`](https://github.com/thelounge/thelounge/commit/a67cee1ee43da01afd8c7584b44d46e6e8dc990d) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency sqlite3 to v5.1.6 ([`34a01c2`](https://github.com/thelounge/thelounge/commit/34a01c2dd164b60d7470b588f7c0e0ed3d3b7647) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- chore(deps): update dependency @types/mousetrap to v1.6.11 ([`5037383`](https://github.com/thelounge/thelounge/commit/5037383c4c9a87a53eaa358ffbe7492ab6ad6365) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- Autocomplete: update to @textcomplete package and close on blur (#4493) ([`bdd6e71`](https://github.com/thelounge/thelounge/commit/bdd6e71049a4ddc65eca8d6acc52ce5c7eb3f6fd) by [@MaxLeiter](https://github.com/MaxLeiter))
|
||||
- Update sqlite3 to 5.0.3 ([`7db0d46`](https://github.com/thelounge/thelounge/commit/7db0d4619d98ad473eff7a1dbdf41c8b0167d0dd) by [@xPaw](https://github.com/xPaw))
|
||||
- Merge sqlite3 upgrade to v5.0.6 ([`abf8906`](https://github.com/thelounge/thelounge/commit/abf89067575810339fa3c723af54a7ea670fe4e5) by [@brunnre8](https://github.com/brunnre8))
|
||||
- bump socket.io to 4.5.2 ([`d4bbd91`](https://github.com/thelounge/thelounge/commit/d4bbd9191cd78f065386fe25c7e8e90b1171a159) by [@brunnre8](https://github.com/brunnre8))
|
||||
- bump socket.io-client to 4.5.0 ([`4c7337b`](https://github.com/thelounge/thelounge/commit/4c7337b6257af2428e6e9f8af570126da094d266) by [@brunnre8](https://github.com/brunnre8))
|
||||
- Bump engine.io from 6.2.0 to 6.2.1 ([`f8eb0eb`](https://github.com/thelounge/thelounge/commit/f8eb0ebafdf8824bfe316fd2ad8adb3b8beda2d2) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
|
||||
- Bump loader-utils from 2.0.2 to 2.0.4 ([`8924545`](https://github.com/thelounge/thelounge/commit/89245455ceceba157821437a3f8f4e80f3b03268) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
|
||||
- Bump loader-utils from 2.0.2 to 2.0.4 ([`21c8b0d`](https://github.com/thelounge/thelounge/commit/21c8b0d17fc7e09d1cad77990fa833fdcad62927) by [@brunnre8](https://github.com/brunnre8))
|
||||
- update dependency @types/mousetrap to v1.6.11 ([`7ee4b80`](https://github.com/thelounge/thelounge/commit/7ee4b80a6e744b09385fc686cdca1fbf0e7784ac) by [@brunnre8](https://github.com/brunnre8))
|
||||
- update dependency @types/lodash to v4.14.191 ([`c67df36`](https://github.com/thelounge/thelounge/commit/c67df36a29a04bacc9e3197a32368493ae0a2ae9) by [@brunnre8](https://github.com/brunnre8))
|
||||
- caniuse-lite: update db ([`efd3b64`](https://github.com/thelounge/thelounge/commit/efd3b645642ff75639ecb27a8ff9d6f6e1c0ccab) by [@brunnre8](https://github.com/brunnre8))
|
||||
- build(deps): bump json5 from 2.2.1 to 2.2.3 ([`ce3ad56`](https://github.com/thelounge/thelounge/commit/ce3ad56ced3b498def5bb65065b4185a46a20995) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
|
||||
- fix(deps): update dependency ua-parser-js to v1.0.33 [security] ([`bde5c3d`](https://github.com/thelounge/thelounge/commit/bde5c3d443dc1e965bdd2641abb94b526600ddec) by [@renovate[bot]](https://github.com/renovate%5Bbot%5D))
|
||||
- build(deps): bump http-cache-semantics from 4.1.0 to 4.1.1 ([`7304acd`](https://github.com/thelounge/thelounge/commit/7304acd8e072af33dfdd1ea2f108b91a6e449f65) by [@dependabot[bot]](https://github.com/dependabot%5Bbot%5D))
|
||||
- update dependency postcss to v8.4.21 ([`95e5630`](https://github.com/thelounge/thelounge/commit/95e56300db48bbb75b3463267eb0809ee9739686) by [@brunnre8](https://github.com/brunnre8))
|
||||
- update dependency sinon to v13.0.2 ([`0183d89`](https://github.com/thelounge/thelounge/commit/0183d89384405ad944863ecffd783c99f0c36517) by [@brunnre8](https://github.com/brunnre8))
|
||||
- update dependency vue-loader to v17.0.1 ([`eddcbcc`](https://github.com/thelounge/thelounge/commit/eddcbcc7660e5f51d9b794ab0302abb9790c6b3c) by [@brunnre8](https://github.com/brunnre8))
|
||||
- update dependency webpack-dev-middleware to v5.3.3 ([`4831c20`](https://github.com/thelounge/thelounge/commit/4831c2080415a72492e97d55be8512c86c4324b3) by [@brunnre8](https://github.com/brunnre8))
|
||||
- update dependency webpack to v5.76.0 ([`6b00ccf`](https://github.com/thelounge/thelounge/commit/6b00ccf82b60503b31e4fee1e32f2765c234d8cc) by [@brunnre8](https://github.com/brunnre8))
|
||||
|
||||
## v4.4.0-pre.2 - 2023-03-19 [Pre-release]
|
||||
|
||||
[See the full changelog](https://github.com/thelounge/thelounge/compare/v4.4.0-pre.1...v4.4.0-pre.2)
|
||||
|
||||
Hot fix for a bug that lead to channel loss upon restart of TL.
|
||||
|
||||
## v4.4.0-pre.1 - 2023-03-19 [Pre-release]
|
||||
|
||||
[See the full changelog](https://github.com/thelounge/thelounge/compare/v4.3.1...v4.4.0-pre.1)
|
||||
|
|
|
@ -90,3 +90,6 @@ Before submitting any change, make sure to:
|
|||
- Run `yarn build:server` if you change anything in `server/`
|
||||
- The built files will be output to `dist/` by tsc
|
||||
- `yarn dev` can be used to start The Lounge with hot module reloading
|
||||
|
||||
To ensure that you don't commit files that fail the linting, you can install a pre-commit git hook.
|
||||
Execute `yarn githooks-install` to do so.
|
||||
|
|
|
@ -38,7 +38,6 @@ import {
|
|||
ref,
|
||||
Ref,
|
||||
InjectionKey,
|
||||
inject,
|
||||
} from "vue";
|
||||
import {useStore} from "../js/store";
|
||||
import type {DebouncedFunc} from "lodash";
|
||||
|
@ -47,10 +46,6 @@ export const imageViewerKey = Symbol() as InjectionKey<Ref<typeof ImageViewer |
|
|||
const contextMenuKey = Symbol() as InjectionKey<Ref<typeof ContextMenu | null>>;
|
||||
const confirmDialogKey = Symbol() as InjectionKey<Ref<typeof ConfirmDialog | null>>;
|
||||
|
||||
export const useImageViewer = () => {
|
||||
return inject(imageViewerKey) as Ref<typeof ImageViewer | null>;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "App",
|
||||
components: {
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
<div
|
||||
id="chat"
|
||||
:class="{
|
||||
'hide-motd': store.state.settings.motd,
|
||||
'hide-motd': !store.state.settings.motd,
|
||||
'time-seconds': store.state.settings.showSeconds,
|
||||
'time-12h': store.state.settings.use12hClock,
|
||||
'colored-nicks': true, // TODO temporarily fixes themes, to be removed in next major version
|
||||
}"
|
||||
>
|
||||
<div
|
||||
|
@ -135,6 +136,7 @@ import ListIgnored from "./Special/ListIgnored.vue";
|
|||
import {defineComponent, PropType, ref, computed, watch, nextTick, onMounted, Component} from "vue";
|
||||
import type {ClientNetwork, ClientChan} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
import {SpecialChanType, ChanType} from "../../shared/types/chan";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Chat",
|
||||
|
@ -160,13 +162,13 @@ export default defineComponent({
|
|||
|
||||
const specialComponent = computed(() => {
|
||||
switch (props.channel.special) {
|
||||
case "list_bans":
|
||||
case SpecialChanType.BANLIST:
|
||||
return ListBans as Component;
|
||||
case "list_invites":
|
||||
case SpecialChanType.INVITELIST:
|
||||
return ListInvites as Component;
|
||||
case "list_channels":
|
||||
case SpecialChanType.CHANNELLIST:
|
||||
return ListChannels as Component;
|
||||
case "list_ignored":
|
||||
case SpecialChanType.IGNORELIST:
|
||||
return ListIgnored as Component;
|
||||
}
|
||||
|
||||
|
@ -193,7 +195,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
const editTopic = () => {
|
||||
if (props.channel.type === "channel") {
|
||||
if (props.channel.type === ChanType.CHANNEL) {
|
||||
props.channel.editTopic = true;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -56,13 +56,14 @@
|
|||
import Mousetrap from "mousetrap";
|
||||
import {wrapCursor} from "undate";
|
||||
import autocompletion from "../js/autocompletion";
|
||||
import commands from "../js/commands/index";
|
||||
import {commands} from "../js/commands/index";
|
||||
import socket from "../js/socket";
|
||||
import upload from "../js/upload";
|
||||
import eventbus from "../js/eventbus";
|
||||
import {watch, defineComponent, nextTick, onMounted, PropType, ref, onUnmounted} from "vue";
|
||||
import type {ClientNetwork, ClientChan} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
|
||||
const formattingHotkeys = {
|
||||
"mod+k": "\x03",
|
||||
|
@ -130,7 +131,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
const getInputPlaceholder = (channel: ClientChan) => {
|
||||
if (channel.type === "channel" || channel.type === "query") {
|
||||
if (channel.type === ChanType.CHANNEL || channel.type === ChanType.QUERY) {
|
||||
return `Write to ${channel.name}`;
|
||||
}
|
||||
|
||||
|
@ -185,10 +186,7 @@ export default defineComponent({
|
|||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(commands, cmd) &&
|
||||
commands[cmd].input(args)
|
||||
) {
|
||||
if (Object.prototype.hasOwnProperty.call(commands, cmd) && commands[cmd](args)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
<script lang="ts">
|
||||
import {filter as fuzzyFilter} from "fuzzy";
|
||||
import {computed, defineComponent, nextTick, PropType, ref} from "vue";
|
||||
import type {UserInMessage} from "../../server/models/msg";
|
||||
import type {UserInMessage} from "../../shared/types/msg";
|
||||
import type {ClientChan, ClientUser} from "../js/types";
|
||||
import Username from "./Username.vue";
|
||||
|
||||
|
@ -104,7 +104,7 @@ export default defineComponent({
|
|||
const result = filteredUsers.value;
|
||||
|
||||
for (const user of result) {
|
||||
const mode = user.original.modes[0] || "";
|
||||
const mode: string = user.original.modes[0] || "";
|
||||
|
||||
if (!groups[mode]) {
|
||||
groups[mode] = [];
|
||||
|
|
|
@ -41,9 +41,9 @@
|
|||
<script lang="ts">
|
||||
import Mousetrap from "mousetrap";
|
||||
import {computed, defineComponent, ref, watch} from "vue";
|
||||
import {onBeforeRouteLeave, onBeforeRouteUpdate} from "vue-router";
|
||||
import eventbus from "../js/eventbus";
|
||||
import {ClientChan, ClientMessage, ClientLinkPreview} from "../js/types";
|
||||
import {ClientChan, ClientLinkPreview} from "../js/types";
|
||||
import {SharedMsg} from "../../shared/types/msg";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ImageViewer",
|
||||
|
@ -104,9 +104,9 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
const links = channel.value.messages
|
||||
.map((msg) => msg.previews)
|
||||
.map((msg: SharedMsg) => msg.previews)
|
||||
.flat()
|
||||
.filter((preview) => preview.thumb);
|
||||
.filter((preview) => preview && preview.thumb);
|
||||
|
||||
const currentIndex = links.indexOf(link.value);
|
||||
|
||||
|
@ -456,6 +456,7 @@ export default defineComponent({
|
|||
|
||||
return {
|
||||
link,
|
||||
channel,
|
||||
image,
|
||||
transform,
|
||||
closeViewer,
|
||||
|
|
|
@ -150,10 +150,14 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
const messageComponent = computed(() => {
|
||||
return "message-" + props.message.type;
|
||||
return "message-" + (props.message.type || "invalid"); // TODO: force existence of type in sharedmsg
|
||||
});
|
||||
|
||||
const isAction = () => {
|
||||
if (!props.message.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return typeof MessageTypes["message-" + props.message.type] !== "undefined";
|
||||
};
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType, ref} from "vue";
|
||||
import {condensedTypes} from "../../shared/irc";
|
||||
import {MessageType} from "../../shared/types/msg";
|
||||
import {ClientMessage, ClientNetwork} from "../js/types";
|
||||
import Message from "./Message.vue";
|
||||
|
||||
|
@ -57,16 +58,23 @@ export default defineComponent({
|
|||
|
||||
for (const message of props.messages) {
|
||||
// special case since one MODE message can change multiple modes
|
||||
if (message.type === "mode") {
|
||||
if (message.type === MessageType.MODE) {
|
||||
// syntax: +vv-t maybe-some targets
|
||||
// we want the number of mode changes in the message, so count the
|
||||
// number of chars other than + and - before the first space
|
||||
const modeChangesCount = message.text
|
||||
const text = message.text ? message.text : "";
|
||||
const modeChangesCount = text
|
||||
.split(" ")[0]
|
||||
.split("")
|
||||
.filter((char) => char !== "+" && char !== "-").length;
|
||||
obj[message.type] += modeChangesCount;
|
||||
} else {
|
||||
if (!message.type) {
|
||||
/* eslint-disable no-console */
|
||||
console.log(`empty message type, this should not happen: ${message.id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
obj[message.type]++;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,6 +59,8 @@
|
|||
|
||||
<script lang="ts">
|
||||
import {condensedTypes} from "../../shared/irc";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
import {MessageType, SharedMsg} from "../../shared/types/msg";
|
||||
import eventbus from "../js/eventbus";
|
||||
import clipboard from "../js/clipboard";
|
||||
import socket from "../js/socket";
|
||||
|
@ -79,7 +81,6 @@ import {
|
|||
} from "vue";
|
||||
import {useStore} from "../js/store";
|
||||
import {ClientChan, ClientMessage, ClientNetwork, ClientLinkPreview} from "../js/types";
|
||||
import Msg from "../../server/models/msg";
|
||||
|
||||
type CondensedMessageContainer = {
|
||||
type: "condensed";
|
||||
|
@ -103,7 +104,7 @@ export default defineComponent({
|
|||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
focused: Number,
|
||||
},
|
||||
setup(props, {emit}) {
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
|
||||
const chat = ref<HTMLDivElement | null>(null);
|
||||
|
@ -177,14 +178,14 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
const condensedMessages = computed(() => {
|
||||
if (props.channel.type !== "channel" && props.channel.type !== "query") {
|
||||
if (props.channel.type !== ChanType.CHANNEL && props.channel.type !== ChanType.QUERY) {
|
||||
return props.channel.messages;
|
||||
}
|
||||
|
||||
// If actions are hidden, just return a message list with them excluded
|
||||
if (store.state.settings.statusMessages === "hidden") {
|
||||
return props.channel.messages.filter(
|
||||
(message) => !condensedTypes.has(message.type)
|
||||
(message) => !condensedTypes.has(message.type || "")
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -200,7 +201,7 @@ export default defineComponent({
|
|||
for (const message of props.channel.messages) {
|
||||
// If this message is not condensable, or its an action affecting our user,
|
||||
// then just append the message to container and be done with it
|
||||
if (message.self || message.highlight || !condensedTypes.has(message.type)) {
|
||||
if (message.self || message.highlight || !condensedTypes.has(message.type || "")) {
|
||||
lastCondensedContainer = null;
|
||||
|
||||
condensed.push(message);
|
||||
|
@ -242,7 +243,7 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
const shouldDisplayDateMarker = (
|
||||
message: Msg | ClientMessage | CondensedMessageContainer,
|
||||
message: SharedMsg | CondensedMessageContainer,
|
||||
id: number
|
||||
) => {
|
||||
const previousMessage = condensedMessages.value[id - 1];
|
||||
|
@ -270,12 +271,13 @@ export default defineComponent({
|
|||
return false;
|
||||
};
|
||||
|
||||
const isPreviousSource = (currentMessage: ClientMessage | Msg, id: number) => {
|
||||
const isPreviousSource = (currentMessage: ClientMessage, id: number) => {
|
||||
const previousMessage = condensedMessages.value[id - 1];
|
||||
return !!(
|
||||
return (
|
||||
previousMessage &&
|
||||
currentMessage.type === "message" &&
|
||||
previousMessage.type === "message" &&
|
||||
currentMessage.type === MessageType.MESSAGE &&
|
||||
previousMessage.type === MessageType.MESSAGE &&
|
||||
currentMessage.from &&
|
||||
previousMessage.from &&
|
||||
currentMessage.from.nick === previousMessage.from.nick
|
||||
);
|
||||
|
|
|
@ -26,36 +26,43 @@ export default defineComponent({
|
|||
},
|
||||
setup(props) {
|
||||
const errorMessage = computed(() => {
|
||||
// TODO: enforce chan and nick fields so that we can get rid of that
|
||||
const chan = props.message.channel || "!UNKNOWN_CHAN";
|
||||
const nick = props.message.nick || "!UNKNOWN_NICK";
|
||||
|
||||
switch (props.message.error) {
|
||||
case "bad_channel_key":
|
||||
return `Cannot join ${props.message.channel} - Bad channel key.`;
|
||||
return `Cannot join ${chan} - Bad channel key.`;
|
||||
case "banned_from_channel":
|
||||
return `Cannot join ${props.message.channel} - You have been banned from the channel.`;
|
||||
return `Cannot join ${chan} - You have been banned from the channel.`;
|
||||
case "cannot_send_to_channel":
|
||||
return `Cannot send to channel ${props.message.channel}`;
|
||||
return `Cannot send to channel ${chan}`;
|
||||
case "channel_is_full":
|
||||
return `Cannot join ${props.message.channel} - Channel is full.`;
|
||||
return `Cannot join ${chan} - Channel is full.`;
|
||||
case "chanop_privs_needed":
|
||||
return "Cannot perform action: You're not a channel operator.";
|
||||
case "invite_only_channel":
|
||||
return `Cannot join ${props.message.channel} - Channel is invite only.`;
|
||||
return `Cannot join ${chan} - Channel is invite only.`;
|
||||
case "no_such_nick":
|
||||
return `User ${props.message.nick} hasn't logged in or does not exist.`;
|
||||
return `User ${nick} hasn't logged in or does not exist.`;
|
||||
case "not_on_channel":
|
||||
return "Cannot perform action: You're not on the channel.";
|
||||
case "password_mismatch":
|
||||
return "Password mismatch.";
|
||||
case "too_many_channels":
|
||||
return `Cannot join ${props.message.channel} - You've already reached the maximum number of channels allowed.`;
|
||||
return `Cannot join ${chan} - You've already reached the maximum number of channels allowed.`;
|
||||
case "unknown_command":
|
||||
return `Unknown command: ${props.message.command}`;
|
||||
// TODO: not having message.command should never happen, so force existence
|
||||
return `Unknown command: ${props.message.command || "!UNDEFINED_COMMAND_BUG"}`;
|
||||
case "user_not_in_channel":
|
||||
return `User ${props.message.nick} is not on the channel.`;
|
||||
return `User ${nick} is not on the channel.`;
|
||||
case "user_on_channel":
|
||||
return `User ${props.message.nick} is already on the channel.`;
|
||||
return `User ${nick} is already on the channel.`;
|
||||
default:
|
||||
if (props.message.reason) {
|
||||
return `${props.message.reason} (${props.message.error})`;
|
||||
return `${props.message.reason} (${
|
||||
props.message.error || "!UNDEFINED_ERR"
|
||||
})`;
|
||||
}
|
||||
|
||||
return props.message.error;
|
||||
|
|
|
@ -498,6 +498,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
watch(
|
||||
// eslint-disable-next-line
|
||||
() => props.defaults?.commands,
|
||||
() => {
|
||||
void nextTick(() => {
|
||||
|
@ -507,6 +508,7 @@ export default defineComponent({
|
|||
);
|
||||
|
||||
watch(
|
||||
// eslint-disable-next-line
|
||||
() => props.defaults?.tls,
|
||||
(isSecureChecked) => {
|
||||
const ports = [6667, 6697];
|
||||
|
|
|
@ -309,8 +309,7 @@ export default defineComponent({
|
|||
|
||||
moveItemInArray(store.state.networks, oldIndex, newIndex);
|
||||
|
||||
socket.emit("sort", {
|
||||
type: "networks",
|
||||
socket.emit("sort:networks", {
|
||||
order: store.state.networks.map((n) => n.uuid),
|
||||
});
|
||||
};
|
||||
|
@ -341,9 +340,8 @@ export default defineComponent({
|
|||
|
||||
moveItemInArray(netChan.network.channels, oldIndex, newIndex);
|
||||
|
||||
socket.emit("sort", {
|
||||
type: "channels",
|
||||
target: netChan.network.uuid,
|
||||
socket.emit("sort:channel", {
|
||||
network: netChan.network.uuid,
|
||||
order: netChan.network.channels.map((c) => c.id),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
<RevealPassword v-slot:default="slotProps">
|
||||
<input
|
||||
id="current-password"
|
||||
v-model="old_password"
|
||||
autocomplete="current-password"
|
||||
:type="slotProps.isVisible ? 'text' : 'password'"
|
||||
name="old_password"
|
||||
|
@ -28,6 +29,7 @@
|
|||
<RevealPassword v-slot:default="slotProps">
|
||||
<input
|
||||
id="new-password"
|
||||
v-model="new_password"
|
||||
:type="slotProps.isVisible ? 'text' : 'password'"
|
||||
name="new_password"
|
||||
autocomplete="new-password"
|
||||
|
@ -41,6 +43,7 @@
|
|||
<RevealPassword v-slot:default="slotProps">
|
||||
<input
|
||||
id="new-password-verify"
|
||||
v-model="verify_password"
|
||||
:type="slotProps.isVisible ? 'text' : 'password'"
|
||||
name="verify_password"
|
||||
autocomplete="new-password"
|
||||
|
@ -111,17 +114,11 @@ export default defineComponent({
|
|||
RevealPassword,
|
||||
Session,
|
||||
},
|
||||
props: {
|
||||
settingsForm: {
|
||||
type: Object as PropType<HTMLFormElement>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
setup() {
|
||||
const store = useStore();
|
||||
|
||||
const passwordErrors = {
|
||||
missing_fields: "Please enter a new password",
|
||||
missing_fields: "Please fill in all fields",
|
||||
password_mismatch: "Both new password fields must match",
|
||||
password_incorrect: "The current password field does not match your account password",
|
||||
update_failed: "Failed to update your password",
|
||||
|
@ -132,6 +129,10 @@ export default defineComponent({
|
|||
error: keyof typeof passwordErrors;
|
||||
}>();
|
||||
|
||||
const old_password = ref("");
|
||||
const new_password = ref("");
|
||||
const verify_password = ref("");
|
||||
|
||||
const currentSession = computed(() => {
|
||||
return store.state.sessions.find((item) => item.current);
|
||||
});
|
||||
|
@ -149,12 +150,10 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
const changePassword = () => {
|
||||
const allFields = new FormData(props.settingsForm);
|
||||
|
||||
const data = {
|
||||
old_password: allFields.get("old_password"),
|
||||
new_password: allFields.get("new_password"),
|
||||
verify_password: allFields.get("verify_password"),
|
||||
old_password: old_password.value,
|
||||
new_password: new_password.value,
|
||||
verify_password: verify_password.value,
|
||||
};
|
||||
|
||||
if (!data.old_password || !data.new_password || !data.verify_password) {
|
||||
|
@ -189,6 +188,9 @@ export default defineComponent({
|
|||
activeSessions,
|
||||
otherSessions,
|
||||
changePassword,
|
||||
old_password,
|
||||
new_password,
|
||||
verify_password,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -28,17 +28,6 @@
|
|||
<div v-if="store.state.pushNotificationState === 'unsupported'" class="error">
|
||||
<strong>Warning</strong>:
|
||||
<span>Push notifications are not supported by your browser.</span>
|
||||
|
||||
<div v-if="isIOS" class="apple-push-unsupported">
|
||||
Safari does
|
||||
<a
|
||||
href="https://bugs.webkit.org/show_bug.cgi?id=182566"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>not support the web push notification specification</a
|
||||
>, and because all browsers on iOS use Safari under the hood, The Lounge is
|
||||
unable to provide push notifications on iOS devices.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType} from "vue";
|
||||
import {UserInMessage} from "../../server/models/msg";
|
||||
import {UserInMessage} from "../../shared/types/msg";
|
||||
import eventbus from "../js/eventbus";
|
||||
import colorClass from "../js/helpers/colorClass";
|
||||
import type {ClientChan, ClientNetwork, ClientUser} from "../js/types";
|
||||
import type {ClientChan, ClientNetwork} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
|
||||
type UsernameUser = Partial<UserInMessage> & {
|
||||
|
|
|
@ -179,6 +179,26 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>Ctrl</kbd> <kbd>↓</kbd></span>
|
||||
<span v-else><kbd>⌥</kbd> <kbd>⌘</kbd> <kbd>↓</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Switch to the next window with unread messages in the channel list.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>Ctrl</kbd> <kbd>↑</kbd></span>
|
||||
<span v-else><kbd>⌥</kbd> <kbd>⌘</kbd> <kbd>↑</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Switch to the previous window with unread messages in the channel list.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>A</kbd></span>
|
||||
|
|
|
@ -48,7 +48,7 @@ export default defineComponent({
|
|||
|
||||
watch(
|
||||
() => route.params.uuid,
|
||||
(newValue) => {
|
||||
() => {
|
||||
setNetworkData();
|
||||
}
|
||||
);
|
||||
|
|
|
@ -106,7 +106,7 @@ import type {ClientMessage} from "../../js/types";
|
|||
import {useStore} from "../../js/store";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {switchToChannel} from "../../js/router";
|
||||
import {SearchQuery} from "../../../server/plugins/messageStorage/types";
|
||||
import {SearchQuery} from "../../../shared/types/storage";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SearchResults",
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
|
||||
<div class="container">
|
||||
<form ref="settingsForm" autocomplete="off" @change="onChange" @submit.prevent>
|
||||
<router-view :settings-form="settingsForm"></router-view>
|
||||
<router-view></router-view>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, ref} from "vue";
|
||||
import {defineComponent} from "vue";
|
||||
import SidebarToggle from "../SidebarToggle.vue";
|
||||
import Navigation from "../Settings/Navigation.vue";
|
||||
import {useStore} from "../../js/store";
|
||||
|
@ -27,7 +27,6 @@ export default defineComponent({
|
|||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const settingsForm = ref<HTMLFormElement>();
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
const ignore = ["old_password", "new_password", "verify_password"];
|
||||
|
@ -51,7 +50,6 @@ export default defineComponent({
|
|||
|
||||
return {
|
||||
onChange,
|
||||
settingsForm,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -19,14 +19,13 @@
|
|||
<label for="signin-username">Username</label>
|
||||
<input
|
||||
id="signin-username"
|
||||
ref="username"
|
||||
v-model="username"
|
||||
class="input"
|
||||
type="text"
|
||||
name="username"
|
||||
autocapitalize="none"
|
||||
autocorrect="off"
|
||||
autocomplete="username"
|
||||
:value="getStoredUser()"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
|
@ -36,9 +35,8 @@
|
|||
<RevealPassword v-slot:default="slotProps">
|
||||
<input
|
||||
id="signin-password"
|
||||
ref="password"
|
||||
v-model="password"
|
||||
:type="slotProps.isVisible ? 'text' : 'password'"
|
||||
name="password"
|
||||
class="input"
|
||||
autocapitalize="none"
|
||||
autocorrect="off"
|
||||
|
@ -70,8 +68,8 @@ export default defineComponent({
|
|||
const inFlight = ref(false);
|
||||
const errorShown = ref(false);
|
||||
|
||||
const username = ref<HTMLInputElement | null>(null);
|
||||
const password = ref<HTMLInputElement | null>(null);
|
||||
const username = ref(storage.get("user") || "");
|
||||
const password = ref("");
|
||||
|
||||
const onAuthFailed = () => {
|
||||
inFlight.value = false;
|
||||
|
@ -89,8 +87,8 @@ export default defineComponent({
|
|||
errorShown.value = false;
|
||||
|
||||
const values = {
|
||||
user: username.value?.value,
|
||||
password: password.value?.value,
|
||||
user: username.value,
|
||||
password: password.value,
|
||||
};
|
||||
|
||||
storage.set("user", values.user);
|
||||
|
@ -98,10 +96,6 @@ export default defineComponent({
|
|||
socket.emit("auth:perform", values);
|
||||
};
|
||||
|
||||
const getStoredUser = () => {
|
||||
return storage.get("user");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
socket.on("auth:failed", onAuthFailed);
|
||||
});
|
||||
|
@ -116,7 +110,6 @@ export default defineComponent({
|
|||
username,
|
||||
password,
|
||||
onSubmit,
|
||||
getStoredUser,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1928,7 +1928,6 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
#settings .apple-push-unsupported,
|
||||
#settings .settings-sync-panel {
|
||||
padding: 10px;
|
||||
margin-bottom: 16px;
|
||||
|
@ -1958,11 +1957,6 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
|||
box-shadow: 0 0 0 3px rgb(0 123 255 / 50%);
|
||||
}
|
||||
|
||||
#settings .apple-push-unsupported a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#settings .opt {
|
||||
display: block;
|
||||
padding: 5px 0 5px 1px;
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import constants from "./constants";
|
||||
|
||||
import Mousetrap from "mousetrap";
|
||||
import {Strategy, Textcomplete, StrategyProps} from "@textcomplete/core";
|
||||
import {Textcomplete, StrategyProps} from "@textcomplete/core";
|
||||
import {TextareaEditor} from "@textcomplete/textarea";
|
||||
|
||||
import fuzzy from "fuzzy";
|
||||
|
||||
import emojiMap from "./helpers/simplemap.json";
|
||||
import {store} from "./store";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
|
||||
export default enableAutocomplete;
|
||||
|
||||
|
@ -38,7 +39,6 @@ const nicksStrategy: StrategyProps = {
|
|||
|
||||
if (term[0] === "@") {
|
||||
// TODO: type
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
callback(completeNicks(term.slice(1), true).map((val) => ["@" + val[0], "@" + val[1]]));
|
||||
} else {
|
||||
callback(completeNicks(term, true));
|
||||
|
@ -292,7 +292,7 @@ function rawNicks() {
|
|||
const otherUser = store.state.activeChannel.channel.name;
|
||||
|
||||
// If this is a query, add their name to autocomplete
|
||||
if (me !== otherUser && store.state.activeChannel.channel.type === "query") {
|
||||
if (me !== otherUser && store.state.activeChannel.channel.type === ChanType.QUERY) {
|
||||
return [otherUser, me];
|
||||
}
|
||||
|
||||
|
@ -332,7 +332,7 @@ function completeChans(word: string) {
|
|||
if (store.state.activeChannel) {
|
||||
for (const channel of store.state.activeChannel.network.channels) {
|
||||
// Push all channels that start with the same CHANTYPE
|
||||
if (channel.type === "channel" && channel.name[0] === word[0]) {
|
||||
if (channel.type === ChanType.CHANNEL && channel.name[0] === word[0]) {
|
||||
words.push(channel.name);
|
||||
}
|
||||
}
|
||||
|
|
36
client/js/chan.ts
Normal file
36
client/js/chan.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import {ClientChan, ClientMessage} from "./types";
|
||||
import {SharedNetworkChan} from "../../shared/types/network";
|
||||
import {SharedMsg, MessageType} from "../../shared/types/msg";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
|
||||
export function toClientChan(shared: SharedNetworkChan): ClientChan {
|
||||
const history: string[] = [""].concat(
|
||||
shared.messages
|
||||
.filter((m) => m.self && m.text && m.type === MessageType.MESSAGE)
|
||||
// TS is too stupid to see the nil guard on filter... so we monkey patch it
|
||||
.map((m): string => (m.text ? m.text : ""))
|
||||
.reverse()
|
||||
.slice(0, 99)
|
||||
);
|
||||
// filter the unused vars
|
||||
const {messages, totalMessages: _, ...props} = shared;
|
||||
const channel: ClientChan = {
|
||||
...props,
|
||||
editTopic: false,
|
||||
pendingMessage: "",
|
||||
inputHistoryPosition: 0,
|
||||
historyLoading: false,
|
||||
scrolledToBottom: true,
|
||||
users: [],
|
||||
usersOutdated: shared.type === ChanType.CHANNEL ? true : false,
|
||||
moreHistoryAvailable: shared.totalMessages > shared.messages.length,
|
||||
inputHistory: history,
|
||||
messages: sharedMsgToClientMsg(messages),
|
||||
};
|
||||
return channel;
|
||||
}
|
||||
|
||||
function sharedMsgToClientMsg(shared: SharedMsg[]): ClientMessage[] {
|
||||
// TODO: this is a stub for now, we will want to populate client specific stuff here
|
||||
return shared;
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
|
||||
function input() {
|
||||
export function input(): boolean {
|
||||
if (!store.state.activeChannel) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const messageIds: number[] = [];
|
||||
|
@ -11,7 +11,7 @@ function input() {
|
|||
for (const message of store.state.activeChannel.channel.messages) {
|
||||
let toggled = false;
|
||||
|
||||
for (const preview of message.previews) {
|
||||
for (const preview of message.previews || []) {
|
||||
if (preview.shown) {
|
||||
preview.shown = false;
|
||||
toggled = true;
|
||||
|
@ -34,5 +34,3 @@ function input() {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {input};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
|
||||
function input() {
|
||||
export function input(): boolean {
|
||||
if (!store.state.activeChannel) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const messageIds: number[] = [];
|
||||
|
@ -11,7 +11,7 @@ function input() {
|
|||
for (const message of store.state.activeChannel.channel.messages) {
|
||||
let toggled = false;
|
||||
|
||||
for (const preview of message.previews) {
|
||||
for (const preview of message.previews || []) {
|
||||
if (!preview.shown) {
|
||||
preview.shown = true;
|
||||
toggled = true;
|
||||
|
@ -34,5 +34,3 @@ function input() {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {input};
|
||||
|
|
|
@ -1,19 +1,11 @@
|
|||
// Taken from views/index.js
|
||||
import {input as collapse} from "./collapse";
|
||||
import {input as expand} from "./expand";
|
||||
import {input as join} from "./join";
|
||||
import {input as search} from "./search";
|
||||
|
||||
// This creates a version of `require()` in the context of the current
|
||||
// directory, so we iterate over its content, which is a map statically built by
|
||||
// Webpack.
|
||||
// Second argument says it's recursive, third makes sure we only load javascript.
|
||||
const commands = require.context("./", true, /\.ts$/);
|
||||
|
||||
export default commands.keys().reduce<Record<string, unknown>>((acc, path) => {
|
||||
const command = path.substring(2, path.length - 3);
|
||||
|
||||
if (command === "index") {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc[command] = commands(path).default;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
export const commands = {
|
||||
collapse: collapse,
|
||||
expand: expand,
|
||||
join: join,
|
||||
search: search,
|
||||
};
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {switchToChannel} from "../router";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
function input(args: string[]) {
|
||||
export function input(args: string[]): boolean {
|
||||
if (args.length > 0) {
|
||||
let channels = args[0];
|
||||
|
||||
|
@ -35,7 +36,7 @@ function input(args: string[]) {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
} else if (store.state.activeChannel?.channel.type === "channel") {
|
||||
} else if (store.state.activeChannel?.channel.type === ChanType.CHANNEL) {
|
||||
// If `/join` command is used without any arguments, re-join current channel
|
||||
socket.emit("input", {
|
||||
target: store.state.activeChannel.channel.id,
|
||||
|
@ -44,6 +45,6 @@ function input(args: string[]) {
|
|||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default {input};
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {store} from "../store";
|
||||
import {router} from "../router";
|
||||
|
||||
function input(args: string[]) {
|
||||
export function input(args: string[]): boolean {
|
||||
if (!store.state.settings.searchEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
@ -23,5 +23,3 @@ function input(args: string[]) {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {input};
|
||||
|
|
|
@ -4,6 +4,7 @@ import type {ClientChan, ClientNetwork, ClientUser} from "../types";
|
|||
import {switchToChannel} from "../router";
|
||||
import {TypedStore} from "../store";
|
||||
import useCloseChannel from "../hooks/use-close-channel";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
type BaseContextMenuItem = {
|
||||
label: string;
|
||||
|
@ -61,7 +62,7 @@ export function generateChannelContextMenu(
|
|||
];
|
||||
|
||||
// Add menu items for lobbies
|
||||
if (channel.type === "lobby") {
|
||||
if (channel.type === ChanType.LOBBY) {
|
||||
items = [
|
||||
...items,
|
||||
{
|
||||
|
@ -121,7 +122,7 @@ export function generateChannelContextMenu(
|
|||
}
|
||||
|
||||
// Add menu items for channels
|
||||
if (channel.type === "channel") {
|
||||
if (channel.type === ChanType.CHANNEL) {
|
||||
items.push({
|
||||
label: "Edit topic",
|
||||
type: "item",
|
||||
|
@ -145,7 +146,7 @@ export function generateChannelContextMenu(
|
|||
}
|
||||
|
||||
// Add menu items for queries
|
||||
if (channel.type === "query") {
|
||||
if (channel.type === ChanType.QUERY) {
|
||||
items.push(
|
||||
{
|
||||
label: "User information",
|
||||
|
@ -173,7 +174,7 @@ export function generateChannelContextMenu(
|
|||
);
|
||||
}
|
||||
|
||||
if (channel.type === "channel" || channel.type === "query") {
|
||||
if (channel.type === ChanType.CHANNEL || channel.type === ChanType.QUERY) {
|
||||
items.push({
|
||||
label: "Clear history",
|
||||
type: "item",
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"😂": "face with tears of joy",
|
||||
"🙂": "slightly smiling face",
|
||||
"🙃": "upside-down face",
|
||||
"🫠": "melting face",
|
||||
"😉": "winking face",
|
||||
"😊": "smiling face with smiling eyes",
|
||||
"😇": "smiling face with halo",
|
||||
|
@ -27,20 +28,27 @@
|
|||
"🤪": "zany face",
|
||||
"😝": "squinting face with tongue",
|
||||
"🤑": "money-mouth face",
|
||||
"🤗": "hugging face",
|
||||
"🤗": "smiling face with open hands",
|
||||
"🤭": "face with hand over mouth",
|
||||
"🫢": "face with open eyes and hand over mouth",
|
||||
"🫣": "face with peeking eye",
|
||||
"🤫": "shushing face",
|
||||
"🤔": "thinking face",
|
||||
"🫡": "saluting face",
|
||||
"🤐": "zipper-mouth face",
|
||||
"🤨": "face with raised eyebrow",
|
||||
"😐": "neutral face",
|
||||
"😑": "expressionless face",
|
||||
"😶": "face without mouth",
|
||||
"🫥": "dotted line face",
|
||||
"😶🌫": "face in clouds",
|
||||
"😏": "smirking face",
|
||||
"😒": "unamused face",
|
||||
"🙄": "face with rolling eyes",
|
||||
"😬": "grimacing face",
|
||||
"😮💨": "face exhaling",
|
||||
"🤥": "lying face",
|
||||
"🫨": "shaking face",
|
||||
"😌": "relieved face",
|
||||
"😔": "pensive face",
|
||||
"😪": "sleepy face",
|
||||
|
@ -55,7 +63,8 @@
|
|||
"🥵": "hot face",
|
||||
"🥶": "cold face",
|
||||
"🥴": "woozy face",
|
||||
"😵": "dizzy face",
|
||||
"😵": "face with crossed-out eyes",
|
||||
"😵💫": "face with spiral eyes",
|
||||
"🤯": "exploding head",
|
||||
"🤠": "cowboy hat face",
|
||||
"🥳": "partying face",
|
||||
|
@ -64,6 +73,7 @@
|
|||
"🤓": "nerd face",
|
||||
"🧐": "face with monocle",
|
||||
"😕": "confused face",
|
||||
"🫤": "face with diagonal mouth",
|
||||
"😟": "worried face",
|
||||
"🙁": "slightly frowning face",
|
||||
"☹": "frowning face",
|
||||
|
@ -72,6 +82,7 @@
|
|||
"😲": "astonished face",
|
||||
"😳": "flushed face",
|
||||
"🥺": "pleading face",
|
||||
"🥹": "face holding back tears",
|
||||
"😦": "frowning face with open mouth",
|
||||
"😧": "anguished face",
|
||||
"😨": "fearful face",
|
||||
|
@ -88,7 +99,7 @@
|
|||
"😫": "tired face",
|
||||
"🥱": "yawning face",
|
||||
"😤": "face with steam from nose",
|
||||
"😡": "pouting face",
|
||||
"😡": "enraged face",
|
||||
"😠": "angry face",
|
||||
"🤬": "face with symbols on mouth",
|
||||
"😈": "smiling face with horns",
|
||||
|
@ -115,7 +126,6 @@
|
|||
"🙈": "see-no-evil monkey",
|
||||
"🙉": "hear-no-evil monkey",
|
||||
"🙊": "speak-no-evil monkey",
|
||||
"💋": "kiss mark",
|
||||
"💌": "love letter",
|
||||
"💘": "heart with arrow",
|
||||
"💝": "heart with ribbon",
|
||||
|
@ -127,15 +137,21 @@
|
|||
"💟": "heart decoration",
|
||||
"❣": "heart exclamation",
|
||||
"💔": "broken heart",
|
||||
"❤🔥": "heart on fire",
|
||||
"❤🩹": "mending heart",
|
||||
"❤": "red heart",
|
||||
"🩷": "pink heart",
|
||||
"🧡": "orange heart",
|
||||
"💛": "yellow heart",
|
||||
"💚": "green heart",
|
||||
"💙": "blue heart",
|
||||
"🩵": "light blue heart",
|
||||
"💜": "purple heart",
|
||||
"🤎": "brown heart",
|
||||
"🖤": "black heart",
|
||||
"🩶": "grey heart",
|
||||
"🤍": "white heart",
|
||||
"💋": "kiss mark",
|
||||
"💯": "hundred points",
|
||||
"💢": "anger symbol",
|
||||
"💥": "collision",
|
||||
|
@ -143,23 +159,29 @@
|
|||
"💦": "sweat droplets",
|
||||
"💨": "dashing away",
|
||||
"🕳": "hole",
|
||||
"💣": "bomb",
|
||||
"💬": "speech balloon",
|
||||
"👁🗨": "eye in speech bubble",
|
||||
"🗨": "left speech bubble",
|
||||
"🗯": "right anger bubble",
|
||||
"💭": "thought balloon",
|
||||
"💤": "zzz",
|
||||
"💤": "ZZZ",
|
||||
"👋": "waving hand",
|
||||
"🤚": "raised back of hand",
|
||||
"🖐": "hand with fingers splayed",
|
||||
"✋": "raised hand",
|
||||
"🖖": "vulcan salute",
|
||||
"🫱": "rightwards hand",
|
||||
"🫲": "leftwards hand",
|
||||
"🫳": "palm down hand",
|
||||
"🫴": "palm up hand",
|
||||
"🫷": "leftwards pushing hand",
|
||||
"🫸": "rightwards pushing hand",
|
||||
"👌": "OK hand",
|
||||
"🤌": "pinched fingers",
|
||||
"🤏": "pinching hand",
|
||||
"✌": "victory hand",
|
||||
"🤞": "crossed fingers",
|
||||
"🫰": "hand with index finger and thumb crossed",
|
||||
"🤟": "love-you gesture",
|
||||
"🤘": "sign of the horns",
|
||||
"🤙": "call me hand",
|
||||
|
@ -169,6 +191,7 @@
|
|||
"🖕": "middle finger",
|
||||
"👇": "backhand index pointing down",
|
||||
"☝": "index pointing up",
|
||||
"🫵": "index pointing at the viewer",
|
||||
"👍": "thumbs up",
|
||||
"👎": "thumbs down",
|
||||
"✊": "raised fist",
|
||||
|
@ -177,6 +200,7 @@
|
|||
"🤜": "right-facing fist",
|
||||
"👏": "clapping hands",
|
||||
"🙌": "raising hands",
|
||||
"🫶": "heart hands",
|
||||
"👐": "open hands",
|
||||
"🤲": "palms up together",
|
||||
"🤝": "handshake",
|
||||
|
@ -201,6 +225,7 @@
|
|||
"👁": "eye",
|
||||
"👅": "tongue",
|
||||
"👄": "mouth",
|
||||
"🫦": "biting lip",
|
||||
"👶": "baby",
|
||||
"🧒": "child",
|
||||
"👦": "boy",
|
||||
|
@ -208,7 +233,9 @@
|
|||
"🧑": "person",
|
||||
"👱": "person: blond hair",
|
||||
"👨": "man",
|
||||
"🧔": "man: beard",
|
||||
"🧔": "person: beard",
|
||||
"🧔♂": "man: beard",
|
||||
"🧔♀": "woman: beard",
|
||||
"👨🦰": "man: red hair",
|
||||
"👨🦱": "man: curly hair",
|
||||
"👨🦳": "man: white hair",
|
||||
|
@ -318,6 +345,7 @@
|
|||
"👷": "construction worker",
|
||||
"👷♂": "man construction worker",
|
||||
"👷♀": "woman construction worker",
|
||||
"🫅": "person with crown",
|
||||
"🤴": "prince",
|
||||
"👸": "princess",
|
||||
"👳": "person wearing turban",
|
||||
|
@ -332,6 +360,8 @@
|
|||
"👰♂": "man with veil",
|
||||
"👰♀": "woman with veil",
|
||||
"🤰": "pregnant woman",
|
||||
"🫃": "pregnant man",
|
||||
"🫄": "pregnant person",
|
||||
"🤱": "breast-feeding",
|
||||
"👩🍼": "woman feeding baby",
|
||||
"👨🍼": "man feeding baby",
|
||||
|
@ -367,6 +397,7 @@
|
|||
"🧟": "zombie",
|
||||
"🧟♂": "man zombie",
|
||||
"🧟♀": "woman zombie",
|
||||
"🧌": "troll",
|
||||
"💆": "person getting massage",
|
||||
"💆♂": "man getting massage",
|
||||
"💆♀": "woman getting massage",
|
||||
|
@ -517,6 +548,8 @@
|
|||
"🐅": "tiger",
|
||||
"🐆": "leopard",
|
||||
"🐴": "horse face",
|
||||
"🫎": "moose",
|
||||
"🫏": "donkey",
|
||||
"🐎": "horse",
|
||||
"🦄": "unicorn",
|
||||
"🦓": "zebra",
|
||||
|
@ -579,6 +612,9 @@
|
|||
"🦩": "flamingo",
|
||||
"🦚": "peacock",
|
||||
"🦜": "parrot",
|
||||
"🪽": "wing",
|
||||
"🐦⬛": "black bird",
|
||||
"🪿": "goose",
|
||||
"🐸": "frog",
|
||||
"🐊": "crocodile",
|
||||
"🐢": "turtle",
|
||||
|
@ -598,6 +634,8 @@
|
|||
"🦈": "shark",
|
||||
"🐙": "octopus",
|
||||
"🐚": "spiral shell",
|
||||
"🪸": "coral",
|
||||
"🪼": "jellyfish",
|
||||
"🐌": "snail",
|
||||
"🦋": "butterfly",
|
||||
"🐛": "bug",
|
||||
|
@ -617,6 +655,7 @@
|
|||
"💐": "bouquet",
|
||||
"🌸": "cherry blossom",
|
||||
"💮": "white flower",
|
||||
"🪷": "lotus",
|
||||
"🏵": "rosette",
|
||||
"🌹": "rose",
|
||||
"🥀": "wilted flower",
|
||||
|
@ -624,6 +663,7 @@
|
|||
"🌻": "sunflower",
|
||||
"🌼": "blossom",
|
||||
"🌷": "tulip",
|
||||
"🪻": "hyacinth",
|
||||
"🌱": "seedling",
|
||||
"🪴": "potted plant",
|
||||
"🌲": "evergreen tree",
|
||||
|
@ -637,6 +677,9 @@
|
|||
"🍁": "maple leaf",
|
||||
"🍂": "fallen leaf",
|
||||
"🍃": "leaf fluttering in wind",
|
||||
"🪹": "empty nest",
|
||||
"🪺": "nest with eggs",
|
||||
"🍄": "mushroom",
|
||||
"🍇": "grapes",
|
||||
"🍈": "melon",
|
||||
"🍉": "watermelon",
|
||||
|
@ -668,9 +711,11 @@
|
|||
"🥦": "broccoli",
|
||||
"🧄": "garlic",
|
||||
"🧅": "onion",
|
||||
"🍄": "mushroom",
|
||||
"🥜": "peanuts",
|
||||
"🫘": "beans",
|
||||
"🌰": "chestnut",
|
||||
"🫚": "ginger root",
|
||||
"🫛": "pea pod",
|
||||
"🍞": "bread",
|
||||
"🥐": "croissant",
|
||||
"🥖": "baguette bread",
|
||||
|
@ -755,6 +800,7 @@
|
|||
"🍻": "clinking beer mugs",
|
||||
"🥂": "clinking glasses",
|
||||
"🥃": "tumbler glass",
|
||||
"🫗": "pouring liquid",
|
||||
"🥤": "cup with straw",
|
||||
"🧋": "bubble tea",
|
||||
"🧃": "beverage box",
|
||||
|
@ -765,6 +811,7 @@
|
|||
"🍴": "fork and knife",
|
||||
"🥄": "spoon",
|
||||
"🔪": "kitchen knife",
|
||||
"🫙": "jar",
|
||||
"🏺": "amphora",
|
||||
"🌍": "globe showing Europe-Africa",
|
||||
"🌎": "globe showing Americas",
|
||||
|
@ -827,6 +874,7 @@
|
|||
"🌉": "bridge at night",
|
||||
"♨": "hot springs",
|
||||
"🎠": "carousel horse",
|
||||
"🛝": "playground slide",
|
||||
"🎡": "ferris wheel",
|
||||
"🎢": "roller coaster",
|
||||
"💈": "barber pole",
|
||||
|
@ -875,12 +923,14 @@
|
|||
"🛤": "railway track",
|
||||
"🛢": "oil drum",
|
||||
"⛽": "fuel pump",
|
||||
"🛞": "wheel",
|
||||
"🚨": "police car light",
|
||||
"🚥": "horizontal traffic light",
|
||||
"🚦": "vertical traffic light",
|
||||
"🛑": "stop sign",
|
||||
"🚧": "construction",
|
||||
"⚓": "anchor",
|
||||
"🛟": "ring buoy",
|
||||
"⛵": "sailboat",
|
||||
"🛶": "canoe",
|
||||
"🚤": "speedboat",
|
||||
|
@ -1035,13 +1085,13 @@
|
|||
"🎿": "skis",
|
||||
"🛷": "sled",
|
||||
"🥌": "curling stone",
|
||||
"🎯": "direct hit",
|
||||
"🎯": "bullseye",
|
||||
"🪀": "yo-yo",
|
||||
"🪁": "kite",
|
||||
"🔫": "water pistol",
|
||||
"🎱": "pool 8 ball",
|
||||
"🔮": "crystal ball",
|
||||
"🪄": "magic wand",
|
||||
"🧿": "nazar amulet",
|
||||
"🎮": "video game",
|
||||
"🕹": "joystick",
|
||||
"🎰": "slot machine",
|
||||
|
@ -1049,6 +1099,7 @@
|
|||
"🧩": "puzzle piece",
|
||||
"🧸": "teddy bear",
|
||||
"🪅": "piñata",
|
||||
"🪩": "mirror ball",
|
||||
"🪆": "nesting dolls",
|
||||
"♠": "spade suit",
|
||||
"♥": "heart suit",
|
||||
|
@ -1085,6 +1136,7 @@
|
|||
"🩳": "shorts",
|
||||
"👙": "bikini",
|
||||
"👚": "woman’s clothes",
|
||||
"🪭": "folding hand fan",
|
||||
"👛": "purse",
|
||||
"👜": "handbag",
|
||||
"👝": "clutch bag",
|
||||
|
@ -1099,6 +1151,7 @@
|
|||
"👡": "woman’s sandal",
|
||||
"🩰": "ballet shoes",
|
||||
"👢": "woman’s boot",
|
||||
"🪮": "hair pick",
|
||||
"👑": "crown",
|
||||
"👒": "woman’s hat",
|
||||
"🎩": "top hat",
|
||||
|
@ -1137,6 +1190,8 @@
|
|||
"🪕": "banjo",
|
||||
"🥁": "drum",
|
||||
"🪘": "long drum",
|
||||
"🪇": "maracas",
|
||||
"🪈": "flute",
|
||||
"📱": "mobile phone",
|
||||
"📲": "mobile phone with arrow",
|
||||
"☎": "telephone",
|
||||
|
@ -1144,6 +1199,7 @@
|
|||
"📟": "pager",
|
||||
"📠": "fax machine",
|
||||
"🔋": "battery",
|
||||
"🪫": "low battery",
|
||||
"🔌": "electric plug",
|
||||
"💻": "laptop",
|
||||
"🖥": "desktop computer",
|
||||
|
@ -1255,7 +1311,7 @@
|
|||
"🛠": "hammer and wrench",
|
||||
"🗡": "dagger",
|
||||
"⚔": "crossed swords",
|
||||
"🔫": "pistol",
|
||||
"💣": "bomb",
|
||||
"🪃": "boomerang",
|
||||
"🏹": "bow and arrow",
|
||||
"🛡": "shield",
|
||||
|
@ -1284,7 +1340,9 @@
|
|||
"🩸": "drop of blood",
|
||||
"💊": "pill",
|
||||
"🩹": "adhesive bandage",
|
||||
"🩼": "crutch",
|
||||
"🩺": "stethoscope",
|
||||
"🩻": "x-ray",
|
||||
"🚪": "door",
|
||||
"🛗": "elevator",
|
||||
"🪞": "mirror",
|
||||
|
@ -1305,6 +1363,7 @@
|
|||
"🧻": "roll of paper",
|
||||
"🪣": "bucket",
|
||||
"🧼": "soap",
|
||||
"🫧": "bubbles",
|
||||
"🪥": "toothbrush",
|
||||
"🧽": "sponge",
|
||||
"🧯": "fire extinguisher",
|
||||
|
@ -1313,8 +1372,11 @@
|
|||
"⚰": "coffin",
|
||||
"🪦": "headstone",
|
||||
"⚱": "funeral urn",
|
||||
"🧿": "nazar amulet",
|
||||
"🪬": "hamsa",
|
||||
"🗿": "moai",
|
||||
"🪧": "placard",
|
||||
"🪪": "identification card",
|
||||
"🏧": "ATM sign",
|
||||
"🚮": "litter in bin sign",
|
||||
"🚰": "potable water",
|
||||
|
@ -1374,6 +1436,7 @@
|
|||
"☮": "peace symbol",
|
||||
"🕎": "menorah",
|
||||
"🔯": "dotted six-pointed star",
|
||||
"🪯": "khanda",
|
||||
"♈": "Aries",
|
||||
"♉": "Taurus",
|
||||
"♊": "Gemini",
|
||||
|
@ -1409,6 +1472,7 @@
|
|||
"🔅": "dim button",
|
||||
"🔆": "bright button",
|
||||
"📶": "antenna bars",
|
||||
"🛜": "wireless",
|
||||
"📳": "vibration mode",
|
||||
"📴": "mobile phone off",
|
||||
"♀": "female sign",
|
||||
|
@ -1418,13 +1482,14 @@
|
|||
"➕": "plus",
|
||||
"➖": "minus",
|
||||
"➗": "divide",
|
||||
"🟰": "heavy equals sign",
|
||||
"♾": "infinity",
|
||||
"‼": "double exclamation mark",
|
||||
"⁉": "exclamation question mark",
|
||||
"❓": "question mark",
|
||||
"❓": "red question mark",
|
||||
"❔": "white question mark",
|
||||
"❕": "white exclamation mark",
|
||||
"❗": "exclamation mark",
|
||||
"❗": "red exclamation mark",
|
||||
"〰": "wavy dash",
|
||||
"💱": "currency exchange",
|
||||
"💲": "heavy dollar sign",
|
||||
|
|
|
@ -185,8 +185,7 @@ function parse(text: string, message?: ClientMessage, network?: ClientNetwork) {
|
|||
} else if (textPart.emoji) {
|
||||
const emojiWithoutModifiers = textPart.emoji.replace(emojiModifiersRegex, "");
|
||||
const title = emojiMap[emojiWithoutModifiers]
|
||||
? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
`Emoji: ${emojiMap[emojiWithoutModifiers]}`
|
||||
? `Emoji: ${emojiMap[emojiWithoutModifiers]}`
|
||||
: null;
|
||||
|
||||
return createElement(
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"joy": "😂",
|
||||
"slightly_smiling_face": "🙂",
|
||||
"upside_down_face": "🙃",
|
||||
"melting_face": "🫠",
|
||||
"wink": "😉",
|
||||
"blush": "😊",
|
||||
"innocent": "😇",
|
||||
|
@ -32,18 +33,25 @@
|
|||
"money_mouth_face": "🤑",
|
||||
"hugs": "🤗",
|
||||
"hand_over_mouth": "🤭",
|
||||
"face_with_open_eyes_and_hand_over_mouth": "🫢",
|
||||
"face_with_peeking_eye": "🫣",
|
||||
"shushing_face": "🤫",
|
||||
"thinking": "🤔",
|
||||
"saluting_face": "🫡",
|
||||
"zipper_mouth_face": "🤐",
|
||||
"raised_eyebrow": "🤨",
|
||||
"neutral_face": "😐",
|
||||
"expressionless": "😑",
|
||||
"no_mouth": "😶",
|
||||
"dotted_line_face": "🫥",
|
||||
"face_in_clouds": "😶🌫️",
|
||||
"smirk": "😏",
|
||||
"unamused": "😒",
|
||||
"roll_eyes": "🙄",
|
||||
"grimacing": "😬",
|
||||
"face_exhaling": "😮💨",
|
||||
"lying_face": "🤥",
|
||||
"shaking_face": "🫨",
|
||||
"relieved": "😌",
|
||||
"pensive": "😔",
|
||||
"sleepy": "😪",
|
||||
|
@ -59,6 +67,7 @@
|
|||
"cold_face": "🥶",
|
||||
"woozy_face": "🥴",
|
||||
"dizzy_face": "😵",
|
||||
"face_with_spiral_eyes": "😵💫",
|
||||
"exploding_head": "🤯",
|
||||
"cowboy_hat_face": "🤠",
|
||||
"partying_face": "🥳",
|
||||
|
@ -67,6 +76,7 @@
|
|||
"nerd_face": "🤓",
|
||||
"monocle_face": "🧐",
|
||||
"confused": "😕",
|
||||
"face_with_diagonal_mouth": "🫤",
|
||||
"worried": "😟",
|
||||
"slightly_frowning_face": "🙁",
|
||||
"frowning_face": "☹️",
|
||||
|
@ -75,6 +85,7 @@
|
|||
"astonished": "😲",
|
||||
"flushed": "😳",
|
||||
"pleading_face": "🥺",
|
||||
"face_holding_back_tears": "🥹",
|
||||
"frowning": "😦",
|
||||
"anguished": "😧",
|
||||
"fearful": "😨",
|
||||
|
@ -121,7 +132,6 @@
|
|||
"see_no_evil": "🙈",
|
||||
"hear_no_evil": "🙉",
|
||||
"speak_no_evil": "🙊",
|
||||
"kiss": "💋",
|
||||
"love_letter": "💌",
|
||||
"cupid": "💘",
|
||||
"gift_heart": "💝",
|
||||
|
@ -133,15 +143,21 @@
|
|||
"heart_decoration": "💟",
|
||||
"heavy_heart_exclamation": "❣️",
|
||||
"broken_heart": "💔",
|
||||
"heart_on_fire": "❤️🔥",
|
||||
"mending_heart": "❤️🩹",
|
||||
"heart": "❤️",
|
||||
"pink_heart": "🩷",
|
||||
"orange_heart": "🧡",
|
||||
"yellow_heart": "💛",
|
||||
"green_heart": "💚",
|
||||
"blue_heart": "💙",
|
||||
"light_blue_heart": "🩵",
|
||||
"purple_heart": "💜",
|
||||
"brown_heart": "🤎",
|
||||
"black_heart": "🖤",
|
||||
"grey_heart": "🩶",
|
||||
"white_heart": "🤍",
|
||||
"kiss": "💋",
|
||||
"anger": "💢",
|
||||
"boom": "💥",
|
||||
"collision": "💥",
|
||||
|
@ -149,7 +165,6 @@
|
|||
"sweat_drops": "💦",
|
||||
"dash": "💨",
|
||||
"hole": "🕳️",
|
||||
"bomb": "💣",
|
||||
"speech_balloon": "💬",
|
||||
"eye_speech_bubble": "👁️🗨️",
|
||||
"left_speech_bubble": "🗨️",
|
||||
|
@ -162,11 +177,18 @@
|
|||
"hand": "✋",
|
||||
"raised_hand": "✋",
|
||||
"vulcan_salute": "🖖",
|
||||
"rightwards_hand": "🫱",
|
||||
"leftwards_hand": "🫲",
|
||||
"palm_down_hand": "🫳",
|
||||
"palm_up_hand": "🫴",
|
||||
"leftwards_pushing_hand": "🫷",
|
||||
"rightwards_pushing_hand": "🫸",
|
||||
"ok_hand": "👌",
|
||||
"pinched_fingers": "🤌",
|
||||
"pinching_hand": "🤏",
|
||||
"v": "✌️",
|
||||
"crossed_fingers": "🤞",
|
||||
"hand_with_index_finger_and_thumb_crossed": "🫰",
|
||||
"love_you_gesture": "🤟",
|
||||
"metal": "🤘",
|
||||
"call_me_hand": "🤙",
|
||||
|
@ -177,6 +199,7 @@
|
|||
"fu": "🖕",
|
||||
"point_down": "👇",
|
||||
"point_up": "☝️",
|
||||
"index_pointing_at_the_viewer": "🫵",
|
||||
"+1": "👍",
|
||||
"thumbsup": "👍",
|
||||
"-1": "👎",
|
||||
|
@ -190,6 +213,7 @@
|
|||
"fist_right": "🤜",
|
||||
"clap": "👏",
|
||||
"raised_hands": "🙌",
|
||||
"heart_hands": "🫶",
|
||||
"open_hands": "👐",
|
||||
"palms_up_together": "🤲",
|
||||
"handshake": "🤝",
|
||||
|
@ -214,6 +238,7 @@
|
|||
"eye": "👁️",
|
||||
"tongue": "👅",
|
||||
"lips": "👄",
|
||||
"biting_lip": "🫦",
|
||||
"baby": "👶",
|
||||
"child": "🧒",
|
||||
"boy": "👦",
|
||||
|
@ -222,6 +247,8 @@
|
|||
"blond_haired_person": "👱",
|
||||
"man": "👨",
|
||||
"bearded_person": "🧔",
|
||||
"man_beard": "🧔♂️",
|
||||
"woman_beard": "🧔♀️",
|
||||
"red_haired_man": "👨🦰",
|
||||
"curly_haired_man": "👨🦱",
|
||||
"white_haired_man": "👨🦳",
|
||||
|
@ -338,6 +365,7 @@
|
|||
"construction_worker": "👷",
|
||||
"construction_worker_man": "👷♂️",
|
||||
"construction_worker_woman": "👷♀️",
|
||||
"person_with_crown": "🫅",
|
||||
"prince": "🤴",
|
||||
"princess": "👸",
|
||||
"person_with_turban": "👳",
|
||||
|
@ -353,6 +381,8 @@
|
|||
"woman_with_veil": "👰♀️",
|
||||
"bride_with_veil": "👰♀️",
|
||||
"pregnant_woman": "🤰",
|
||||
"pregnant_man": "🫃",
|
||||
"pregnant_person": "🫄",
|
||||
"breast_feeding": "🤱",
|
||||
"woman_feeding_baby": "👩🍼",
|
||||
"man_feeding_baby": "👨🍼",
|
||||
|
@ -388,6 +418,7 @@
|
|||
"zombie": "🧟",
|
||||
"zombie_man": "🧟♂️",
|
||||
"zombie_woman": "🧟♀️",
|
||||
"troll": "🧌",
|
||||
"massage": "💆",
|
||||
"massage_man": "💆♂️",
|
||||
"massage_woman": "💆♀️",
|
||||
|
@ -542,6 +573,8 @@
|
|||
"tiger2": "🐅",
|
||||
"leopard": "🐆",
|
||||
"horse": "🐴",
|
||||
"moose": "🫎",
|
||||
"donkey": "🫏",
|
||||
"racehorse": "🐎",
|
||||
"unicorn": "🦄",
|
||||
"zebra": "🦓",
|
||||
|
@ -605,6 +638,9 @@
|
|||
"flamingo": "🦩",
|
||||
"peacock": "🦚",
|
||||
"parrot": "🦜",
|
||||
"wing": "🪽",
|
||||
"black_bird": "🐦⬛",
|
||||
"goose": "🪿",
|
||||
"frog": "🐸",
|
||||
"crocodile": "🐊",
|
||||
"turtle": "🐢",
|
||||
|
@ -625,6 +661,8 @@
|
|||
"shark": "🦈",
|
||||
"octopus": "🐙",
|
||||
"shell": "🐚",
|
||||
"coral": "🪸",
|
||||
"jellyfish": "🪼",
|
||||
"snail": "🐌",
|
||||
"butterfly": "🦋",
|
||||
"bug": "🐛",
|
||||
|
@ -645,6 +683,7 @@
|
|||
"bouquet": "💐",
|
||||
"cherry_blossom": "🌸",
|
||||
"white_flower": "💮",
|
||||
"lotus": "🪷",
|
||||
"rosette": "🏵️",
|
||||
"rose": "🌹",
|
||||
"wilted_flower": "🥀",
|
||||
|
@ -652,6 +691,7 @@
|
|||
"sunflower": "🌻",
|
||||
"blossom": "🌼",
|
||||
"tulip": "🌷",
|
||||
"hyacinth": "🪻",
|
||||
"seedling": "🌱",
|
||||
"potted_plant": "🪴",
|
||||
"evergreen_tree": "🌲",
|
||||
|
@ -665,6 +705,9 @@
|
|||
"maple_leaf": "🍁",
|
||||
"fallen_leaf": "🍂",
|
||||
"leaves": "🍃",
|
||||
"empty_nest": "🪹",
|
||||
"nest_with_eggs": "🪺",
|
||||
"mushroom": "🍄",
|
||||
"grapes": "🍇",
|
||||
"melon": "🍈",
|
||||
"watermelon": "🍉",
|
||||
|
@ -698,9 +741,11 @@
|
|||
"broccoli": "🥦",
|
||||
"garlic": "🧄",
|
||||
"onion": "🧅",
|
||||
"mushroom": "🍄",
|
||||
"peanuts": "🥜",
|
||||
"beans": "🫘",
|
||||
"chestnut": "🌰",
|
||||
"ginger_root": "🫚",
|
||||
"pea_pod": "🫛",
|
||||
"bread": "🍞",
|
||||
"croissant": "🥐",
|
||||
"baguette_bread": "🥖",
|
||||
|
@ -785,6 +830,7 @@
|
|||
"beers": "🍻",
|
||||
"clinking_glasses": "🥂",
|
||||
"tumbler_glass": "🥃",
|
||||
"pouring_liquid": "🫗",
|
||||
"cup_with_straw": "🥤",
|
||||
"bubble_tea": "🧋",
|
||||
"beverage_box": "🧃",
|
||||
|
@ -796,6 +842,7 @@
|
|||
"spoon": "🥄",
|
||||
"hocho": "🔪",
|
||||
"knife": "🔪",
|
||||
"jar": "🫙",
|
||||
"amphora": "🏺",
|
||||
"earth_africa": "🌍",
|
||||
"earth_americas": "🌎",
|
||||
|
@ -858,6 +905,7 @@
|
|||
"bridge_at_night": "🌉",
|
||||
"hotsprings": "♨️",
|
||||
"carousel_horse": "🎠",
|
||||
"playground_slide": "🛝",
|
||||
"ferris_wheel": "🎡",
|
||||
"roller_coaster": "🎢",
|
||||
"barber": "💈",
|
||||
|
@ -907,12 +955,14 @@
|
|||
"railway_track": "🛤️",
|
||||
"oil_drum": "🛢️",
|
||||
"fuelpump": "⛽",
|
||||
"wheel": "🛞",
|
||||
"rotating_light": "🚨",
|
||||
"traffic_light": "🚥",
|
||||
"vertical_traffic_light": "🚦",
|
||||
"stop_sign": "🛑",
|
||||
"construction": "🚧",
|
||||
"anchor": "⚓",
|
||||
"ring_buoy": "🛟",
|
||||
"boat": "⛵",
|
||||
"sailboat": "⛵",
|
||||
"canoe": "🛶",
|
||||
|
@ -1072,10 +1122,10 @@
|
|||
"dart": "🎯",
|
||||
"yo_yo": "🪀",
|
||||
"kite": "🪁",
|
||||
"gun": "🔫",
|
||||
"8ball": "🎱",
|
||||
"crystal_ball": "🔮",
|
||||
"magic_wand": "🪄",
|
||||
"nazar_amulet": "🧿",
|
||||
"video_game": "🎮",
|
||||
"joystick": "🕹️",
|
||||
"slot_machine": "🎰",
|
||||
|
@ -1083,6 +1133,7 @@
|
|||
"jigsaw": "🧩",
|
||||
"teddy_bear": "🧸",
|
||||
"pinata": "🪅",
|
||||
"mirror_ball": "🪩",
|
||||
"nesting_dolls": "🪆",
|
||||
"spades": "♠️",
|
||||
"hearts": "♥️",
|
||||
|
@ -1120,6 +1171,7 @@
|
|||
"shorts": "🩳",
|
||||
"bikini": "👙",
|
||||
"womans_clothes": "👚",
|
||||
"folding_hand_fan": "🪭",
|
||||
"purse": "👛",
|
||||
"handbag": "👜",
|
||||
"pouch": "👝",
|
||||
|
@ -1135,6 +1187,7 @@
|
|||
"sandal": "👡",
|
||||
"ballet_shoes": "🩰",
|
||||
"boot": "👢",
|
||||
"hair_pick": "🪮",
|
||||
"crown": "👑",
|
||||
"womans_hat": "👒",
|
||||
"tophat": "🎩",
|
||||
|
@ -1173,6 +1226,8 @@
|
|||
"banjo": "🪕",
|
||||
"drum": "🥁",
|
||||
"long_drum": "🪘",
|
||||
"maracas": "🪇",
|
||||
"flute": "🪈",
|
||||
"iphone": "📱",
|
||||
"calling": "📲",
|
||||
"phone": "☎️",
|
||||
|
@ -1181,6 +1236,7 @@
|
|||
"pager": "📟",
|
||||
"fax": "📠",
|
||||
"battery": "🔋",
|
||||
"low_battery": "🪫",
|
||||
"electric_plug": "🔌",
|
||||
"computer": "💻",
|
||||
"desktop_computer": "🖥️",
|
||||
|
@ -1296,7 +1352,7 @@
|
|||
"hammer_and_wrench": "🛠️",
|
||||
"dagger": "🗡️",
|
||||
"crossed_swords": "⚔️",
|
||||
"gun": "🔫",
|
||||
"bomb": "💣",
|
||||
"boomerang": "🪃",
|
||||
"bow_and_arrow": "🏹",
|
||||
"shield": "🛡️",
|
||||
|
@ -1325,7 +1381,9 @@
|
|||
"drop_of_blood": "🩸",
|
||||
"pill": "💊",
|
||||
"adhesive_bandage": "🩹",
|
||||
"crutch": "🩼",
|
||||
"stethoscope": "🩺",
|
||||
"x_ray": "🩻",
|
||||
"door": "🚪",
|
||||
"elevator": "🛗",
|
||||
"mirror": "🪞",
|
||||
|
@ -1346,6 +1404,7 @@
|
|||
"roll_of_paper": "🧻",
|
||||
"bucket": "🪣",
|
||||
"soap": "🧼",
|
||||
"bubbles": "🫧",
|
||||
"toothbrush": "🪥",
|
||||
"sponge": "🧽",
|
||||
"fire_extinguisher": "🧯",
|
||||
|
@ -1354,8 +1413,11 @@
|
|||
"coffin": "⚰️",
|
||||
"headstone": "🪦",
|
||||
"funeral_urn": "⚱️",
|
||||
"nazar_amulet": "🧿",
|
||||
"hamsa": "🪬",
|
||||
"moyai": "🗿",
|
||||
"placard": "🪧",
|
||||
"identification_card": "🪪",
|
||||
"atm": "🏧",
|
||||
"put_litter_in_its_place": "🚮",
|
||||
"potable_water": "🚰",
|
||||
|
@ -1415,6 +1477,7 @@
|
|||
"peace_symbol": "☮️",
|
||||
"menorah": "🕎",
|
||||
"six_pointed_star": "🔯",
|
||||
"khanda": "🪯",
|
||||
"aries": "♈",
|
||||
"taurus": "♉",
|
||||
"gemini": "♊",
|
||||
|
@ -1450,6 +1513,7 @@
|
|||
"low_brightness": "🔅",
|
||||
"high_brightness": "🔆",
|
||||
"signal_strength": "📶",
|
||||
"wireless": "🛜",
|
||||
"vibration_mode": "📳",
|
||||
"mobile_phone_off": "📴",
|
||||
"female_sign": "♀️",
|
||||
|
@ -1459,6 +1523,7 @@
|
|||
"heavy_plus_sign": "➕",
|
||||
"heavy_minus_sign": "➖",
|
||||
"heavy_division_sign": "➗",
|
||||
"heavy_equals_sign": "🟰",
|
||||
"infinity": "♾️",
|
||||
"bangbang": "‼️",
|
||||
"interrobang": "⁉️",
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import eventbus from "../eventbus";
|
||||
import socket from "../socket";
|
||||
import {ClientChan} from "../types";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
export default function useCloseChannel(channel: ClientChan) {
|
||||
return () => {
|
||||
if (channel.type === "lobby") {
|
||||
if (channel.type === ChanType.LOBBY) {
|
||||
eventbus.emit(
|
||||
"confirm-dialog",
|
||||
{
|
||||
|
|
|
@ -6,6 +6,7 @@ import isChannelCollapsed from "./helpers/isChannelCollapsed";
|
|||
import isIgnoredKeybind from "./helpers/isIgnoredKeybind";
|
||||
import listenForTwoFingerSwipes from "./helpers/listenForTwoFingerSwipes";
|
||||
import {ClientChan} from "./types";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
|
||||
// Switch to the next/previous window in the channel list.
|
||||
Mousetrap.bind(["alt+up", "alt+down"], function (e, keys) {
|
||||
|
@ -73,7 +74,7 @@ Mousetrap.bind(["alt+shift+up", "alt+shift+down"], function (e, keys) {
|
|||
index = store.state.networks.findIndex((n) => n === store.state.activeChannel?.network);
|
||||
|
||||
// If we're in a channel, and it's not the lobby, jump to lobby of this network when going up
|
||||
if (direction !== -1 || store.state.activeChannel?.channel.type === "lobby") {
|
||||
if (direction !== -1 || store.state.activeChannel?.channel.type === ChanType.LOBBY) {
|
||||
index = (((index + direction) % length) + length) % length;
|
||||
}
|
||||
}
|
||||
|
@ -83,6 +84,35 @@ Mousetrap.bind(["alt+shift+up", "alt+shift+down"], function (e, keys) {
|
|||
return false;
|
||||
});
|
||||
|
||||
// Switch to the next/previous unread chat
|
||||
Mousetrap.bind(["alt+mod+up", "alt+mod+down"], function (e, keys) {
|
||||
if (isIgnoredKeybind(e)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const channels = store.state.networks
|
||||
.map((net) =>
|
||||
net.channels.filter(
|
||||
(chan) => chan.unread || chan === store.state.activeChannel?.channel
|
||||
)
|
||||
)
|
||||
.flat();
|
||||
|
||||
if (channels.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let index = channels.findIndex((chan) => chan === store.state.activeChannel?.channel);
|
||||
|
||||
const length = channels.length;
|
||||
const direction = keys.split("+").pop() === "up" ? -1 : 1;
|
||||
index = (((index + direction) % length) + length) % length;
|
||||
|
||||
jumpToChannel(channels[index]);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// Jump to the first window with a highlight in it, or the first with unread
|
||||
// activity if there are none with highlights.
|
||||
Mousetrap.bind(["alt+a"], function (e) {
|
||||
|
|
|
@ -110,26 +110,23 @@ router.beforeEach((to, from, next) => {
|
|||
next();
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
router.beforeEach((to, from) => {
|
||||
// Disallow navigating to non-existing routes
|
||||
if (!to.matched.length) {
|
||||
next(false);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Disallow navigating to invalid channels
|
||||
if (to.name === "RoutedChat" && !store.getters.findChannel(Number(to.params.id))) {
|
||||
next(false);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Disallow navigating to invalid networks
|
||||
if (to.name === "NetworkEdit" && !store.getters.findNetwork(String(to.params.uuid))) {
|
||||
next(false);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
next();
|
||||
return true;
|
||||
});
|
||||
|
||||
router.afterEach((to) => {
|
||||
|
@ -165,7 +162,6 @@ async function navigate(routeName: string, params: any = {}) {
|
|||
// If current route is null, replace the history entry
|
||||
// This prevents invalid entries from lingering in history,
|
||||
// and then the route guard preventing proper navigation
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
await router.replace({name: routeName, params}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import socket from "./socket";
|
|||
import type {TypedStore} from "./store";
|
||||
|
||||
const defaultSettingConfig = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
apply() {},
|
||||
default: null,
|
||||
sync: null,
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import {nextTick} from "vue";
|
||||
import socket from "../socket";
|
||||
import storage from "../localStorage";
|
||||
import {toClientChan} from "../chan";
|
||||
import {router, switchToChannel, navigate} from "../router";
|
||||
import {store} from "../store";
|
||||
import parseIrcUri from "../helpers/parseIrcUri";
|
||||
import {ClientNetwork, InitClientChan} from "../types";
|
||||
import {ClientNetwork, ClientChan} from "../types";
|
||||
import {SharedNetwork, SharedNetworkChan} from "../../../shared/types/network";
|
||||
|
||||
socket.on("init", async function (data) {
|
||||
store.commit("networks", mergeNetworkData(data.networks));
|
||||
|
@ -31,54 +32,54 @@ socket.on("init", async function (data) {
|
|||
window.g_TheLoungeRemoveLoading();
|
||||
}
|
||||
|
||||
const handledQuery = await handleQueryParams();
|
||||
if (await handleQueryParams()) {
|
||||
// If we handled query parameters like irc:// links or just general
|
||||
// connect parameters in public mode, then nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
// If we handled query parameters like irc:// links or just general
|
||||
// connect parameters in public mode, then nothing to do here
|
||||
if (!handledQuery) {
|
||||
// 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?.value?.name ||
|
||||
router.currentRoute?.value?.name === "SignIn"
|
||||
) {
|
||||
const channel = store.getters.findChannel(data.active);
|
||||
// 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?.value?.name || router.currentRoute?.value?.name === "SignIn") {
|
||||
const channel = store.getters.findChannel(data.active);
|
||||
|
||||
if (channel) {
|
||||
switchToChannel(channel.channel);
|
||||
} else if (store.state.networks.length > 0) {
|
||||
// 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
|
||||
switchToChannel(store.state.networks[0].channels[0]);
|
||||
} else {
|
||||
await navigate("Connect");
|
||||
}
|
||||
if (channel) {
|
||||
switchToChannel(channel.channel);
|
||||
} else if (store.state.networks.length > 0) {
|
||||
// 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
|
||||
switchToChannel(store.state.networks[0].channels[0]);
|
||||
} else {
|
||||
await navigate("Connect");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function mergeNetworkData(newNetworks: ClientNetwork[]) {
|
||||
function mergeNetworkData(newNetworks: SharedNetwork[]): ClientNetwork[] {
|
||||
const stored = storage.get("thelounge.networks.collapsed");
|
||||
const collapsedNetworks = stored ? new Set(JSON.parse(stored)) : new Set();
|
||||
const result: ReturnType<typeof mergeNetworkData> = [];
|
||||
|
||||
for (let n = 0; n < newNetworks.length; n++) {
|
||||
const network = newNetworks[n];
|
||||
const currentNetwork = store.getters.findNetwork(network.uuid);
|
||||
for (const sharedNet of newNetworks) {
|
||||
const currentNetwork = store.getters.findNetwork(sharedNet.uuid);
|
||||
|
||||
// If this network is new, set some default variables and initalize channel variables
|
||||
if (!currentNetwork) {
|
||||
network.isJoinChannelShown = false;
|
||||
network.isCollapsed = collapsedNetworks.has(network.uuid);
|
||||
network.channels.forEach(store.getters.initChannel);
|
||||
|
||||
const newNet: ClientNetwork = {
|
||||
...sharedNet,
|
||||
channels: sharedNet.channels.map(toClientChan),
|
||||
isJoinChannelShown: false,
|
||||
isCollapsed: collapsedNetworks.has(sharedNet.uuid),
|
||||
};
|
||||
result.push(newNet);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Merge received network object into existing network object on the client
|
||||
// so the object reference stays the same (e.g. for currentChannel state)
|
||||
for (const key in network) {
|
||||
if (!Object.prototype.hasOwnProperty.call(network, key)) {
|
||||
for (const key in sharedNet) {
|
||||
if (!Object.prototype.hasOwnProperty.call(sharedNet, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -86,81 +87,82 @@ function mergeNetworkData(newNetworks: ClientNetwork[]) {
|
|||
if (key === "channels") {
|
||||
currentNetwork.channels = mergeChannelData(
|
||||
currentNetwork.channels,
|
||||
network.channels as InitClientChan[]
|
||||
sharedNet.channels
|
||||
);
|
||||
} else {
|
||||
currentNetwork[key] = network[key];
|
||||
currentNetwork[key] = sharedNet[key];
|
||||
}
|
||||
}
|
||||
|
||||
newNetworks[n] = currentNetwork;
|
||||
result.push(currentNetwork);
|
||||
}
|
||||
|
||||
return newNetworks;
|
||||
return result;
|
||||
}
|
||||
|
||||
function mergeChannelData(oldChannels: InitClientChan[], newChannels: InitClientChan[]) {
|
||||
for (let c = 0; c < newChannels.length; c++) {
|
||||
const channel = newChannels[c];
|
||||
const currentChannel = oldChannels.find((chan) => chan.id === channel.id);
|
||||
function mergeChannelData(
|
||||
oldChannels: ClientChan[],
|
||||
newChannels: SharedNetworkChan[]
|
||||
): ClientChan[] {
|
||||
const result: ReturnType<typeof mergeChannelData> = [];
|
||||
|
||||
for (const newChannel of newChannels) {
|
||||
const currentChannel = oldChannels.find((chan) => chan.id === newChannel.id);
|
||||
|
||||
// This is a new channel that was joined while client was disconnected, initialize it
|
||||
if (!currentChannel) {
|
||||
store.getters.initChannel(channel);
|
||||
|
||||
// This is a new channel that was joined while client was disconnected, initialize it
|
||||
const current = toClientChan(newChannel);
|
||||
result.push(current);
|
||||
emitNamesOrMarkUsersOudated(current); // TODO: this should not carry logic like that
|
||||
continue;
|
||||
}
|
||||
|
||||
// Merge received channel object into existing currentChannel
|
||||
// so the object references are exactly the same (e.g. in store.state.activeChannel)
|
||||
for (const key in channel) {
|
||||
if (!Object.prototype.hasOwnProperty.call(channel, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Server sends an empty users array, client requests it whenever needed
|
||||
if (key === "users") {
|
||||
if (channel.type === "channel") {
|
||||
if (
|
||||
store.state.activeChannel &&
|
||||
store.state.activeChannel.channel === currentChannel
|
||||
) {
|
||||
// For currently open channel, request the user list straight away
|
||||
socket.emit("names", {
|
||||
target: channel.id,
|
||||
});
|
||||
} else {
|
||||
// For all other channels, mark the user list as outdated
|
||||
// so an update will be requested whenever user switches to these channels
|
||||
currentChannel.usersOutdated = true;
|
||||
}
|
||||
}
|
||||
emitNamesOrMarkUsersOudated(currentChannel); // TODO: this should not carry logic like that
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Server sends total count of messages in memory, we compare it to amount of messages
|
||||
// on the client, and decide whether theres more messages to load from server
|
||||
if (key === "totalMessages") {
|
||||
currentChannel.moreHistoryAvailable =
|
||||
channel.totalMessages! > currentChannel.messages.length;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reconnection only sends new messages, so merge it on the client
|
||||
// Only concat if server sent us less than 100 messages so we don't introduce gaps
|
||||
if (key === "messages" && currentChannel.messages && channel.messages.length < 100) {
|
||||
currentChannel.messages = currentChannel.messages.concat(channel.messages);
|
||||
} else {
|
||||
currentChannel[key] = channel[key];
|
||||
}
|
||||
// Reconnection only sends new messages, so merge it on the client
|
||||
// Only concat if server sent us less than 100 messages so we don't introduce gaps
|
||||
if (currentChannel.messages && newChannel.messages.length < 100) {
|
||||
currentChannel.messages = currentChannel.messages.concat(newChannel.messages);
|
||||
} else {
|
||||
currentChannel.messages = newChannel.messages;
|
||||
}
|
||||
|
||||
newChannels[c] = currentChannel;
|
||||
// TODO: this is copies more than what the compiler knows about
|
||||
for (const key in newChannel) {
|
||||
if (!Object.hasOwn(currentChannel, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === "messages") {
|
||||
// already handled
|
||||
continue;
|
||||
}
|
||||
|
||||
currentChannel[key] = newChannel[key];
|
||||
}
|
||||
|
||||
result.push(currentChannel);
|
||||
}
|
||||
|
||||
return newChannels;
|
||||
return result;
|
||||
}
|
||||
|
||||
function emitNamesOrMarkUsersOudated(chan: ClientChan) {
|
||||
if (store.state.activeChannel && store.state.activeChannel.channel === chan) {
|
||||
// For currently open channel, request the user list straight away
|
||||
socket.emit("names", {
|
||||
target: chan.id,
|
||||
});
|
||||
chan.usersOutdated = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// For all other channels, mark the user list as outdated
|
||||
// so an update will be requested whenever user switches to these channels
|
||||
chan.usersOutdated = true;
|
||||
}
|
||||
|
||||
async function handleQueryParams() {
|
||||
|
@ -170,30 +172,28 @@ async function handleQueryParams() {
|
|||
|
||||
const params = new URLSearchParams(document.location.search);
|
||||
|
||||
const cleanParams = () => {
|
||||
// Remove query parameters from url without reloading the page
|
||||
const cleanUri = window.location.origin + window.location.pathname + window.location.hash;
|
||||
window.history.replaceState({}, document.title, cleanUri);
|
||||
};
|
||||
|
||||
if (params.has("uri")) {
|
||||
// Set default connection settings from IRC protocol links
|
||||
const uri = params.get("uri");
|
||||
const queryParams = parseIrcUri(String(uri));
|
||||
|
||||
cleanParams();
|
||||
removeQueryParams();
|
||||
await router.push({name: "Connect", query: queryParams});
|
||||
|
||||
return true;
|
||||
} else if (document.body.classList.contains("public") && document.location.search) {
|
||||
}
|
||||
|
||||
if (document.body.classList.contains("public") && document.location.search) {
|
||||
// Set default connection settings from url params
|
||||
const queryParams = Object.fromEntries(params.entries());
|
||||
|
||||
cleanParams();
|
||||
removeQueryParams();
|
||||
await router.push({name: "Connect", query: queryParams});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove query parameters from url without reloading the page
|
||||
function removeQueryParams() {
|
||||
const cleanUri = window.location.origin + window.location.pathname + window.location.hash;
|
||||
window.history.replaceState(null, "", cleanUri);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {switchToChannel} from "../router";
|
||||
import {ClientChan} from "../types";
|
||||
import {toClientChan} from "../chan";
|
||||
|
||||
socket.on("join", function (data) {
|
||||
store.getters.initChannel(data.chan);
|
||||
|
||||
const network = store.getters.findNetwork(data.network);
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.splice(data.index || -1, 0, data.chan);
|
||||
const clientChan: ClientChan = toClientChan(data.chan);
|
||||
network.channels.splice(data.index || -1, 0, clientChan);
|
||||
|
||||
// Queries do not automatically focus, unless the user did a whois
|
||||
if (data.chan.type === "query" && !data.shouldOpen) {
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {ClientMention} from "../types";
|
||||
import {SharedMention} from "../../../shared/types/mention";
|
||||
|
||||
socket.on("mentions:list", function (data) {
|
||||
store.commit("mentions", data as ClientMention[]);
|
||||
store.commit("mentions", data.map(sharedToClientMention));
|
||||
});
|
||||
|
||||
function sharedToClientMention(shared: SharedMention): ClientMention {
|
||||
const mention: ClientMention = {
|
||||
...shared,
|
||||
localetime: "", // TODO: can't be right
|
||||
channel: null,
|
||||
};
|
||||
return mention;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import {nextTick} from "vue";
|
|||
|
||||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {ClientMessage} from "../types";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
socket.on("more", async (data) => {
|
||||
const channel = store.getters.findChannel(data.chan)?.channel;
|
||||
|
@ -13,14 +13,16 @@ socket.on("more", async (data) => {
|
|||
|
||||
channel.inputHistory = channel.inputHistory.concat(
|
||||
data.messages
|
||||
.filter((m) => m.self && m.text && m.type === "message")
|
||||
.map((m) => m.text)
|
||||
.filter((m) => m.self && m.text && m.type === MessageType.MESSAGE)
|
||||
// TS is too stupid to see the guard in .filter(), so we monkey patch it
|
||||
// to please the compiler
|
||||
.map((m) => (m.text ? m.text : ""))
|
||||
.reverse()
|
||||
.slice(0, 100 - channel.inputHistory.length)
|
||||
);
|
||||
channel.moreHistoryAvailable =
|
||||
data.totalMessages > channel.messages.length + data.messages.length;
|
||||
channel.messages.unshift(...(data.messages as ClientMessage[]));
|
||||
channel.messages.unshift(...data.messages);
|
||||
|
||||
await nextTick();
|
||||
channel.historyLoading = false;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
/* eslint-disable @typescript-eslint/restrict-plus-operands */
|
||||
import socket from "../socket";
|
||||
import {cleanIrcMessage} from "../../../shared/irc";
|
||||
import {store} from "../store";
|
||||
import {switchToChannel} from "../router";
|
||||
import {ClientChan, ClientMention, ClientMessage, NetChan} from "../types";
|
||||
import {ClientChan, NetChan, ClientMessage} from "../types";
|
||||
import {SharedMsg, MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
let pop;
|
||||
|
||||
|
@ -12,7 +13,6 @@ try {
|
|||
pop.src = "audio/pop.wav";
|
||||
} catch (e) {
|
||||
pop = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
play() {},
|
||||
};
|
||||
}
|
||||
|
@ -90,11 +90,19 @@ socket.on("msg", function (data) {
|
|||
channel.moreHistoryAvailable = true;
|
||||
}
|
||||
|
||||
if (channel.type === "channel") {
|
||||
if (channel.type === ChanType.CHANNEL) {
|
||||
updateUserList(channel, data.msg);
|
||||
}
|
||||
});
|
||||
|
||||
declare global {
|
||||
// this extends the interface from lib.dom with additional stuff which is not
|
||||
// exactly standard but implemented in some browsers
|
||||
interface NotificationOptions {
|
||||
timestamp?: number; // chrome has it, other browsers ignore it
|
||||
}
|
||||
}
|
||||
|
||||
function notifyMessage(
|
||||
targetId: number,
|
||||
channel: ClientChan,
|
||||
|
@ -105,7 +113,10 @@ function notifyMessage(
|
|||
return;
|
||||
}
|
||||
|
||||
if (msg.highlight || (store.state.settings.notifyAllMessages && msg.type === "message")) {
|
||||
if (
|
||||
msg.highlight ||
|
||||
(store.state.settings.notifyAllMessages && msg.type === MessageType.MESSAGE)
|
||||
) {
|
||||
if (!document.hasFocus() || !activeChannel || activeChannel.channel !== channel) {
|
||||
if (store.state.settings.notification) {
|
||||
try {
|
||||
|
@ -122,22 +133,25 @@ function notifyMessage(
|
|||
) {
|
||||
let title: string;
|
||||
let body: string;
|
||||
// TODO: fix msg type and get rid of that conditional
|
||||
const nick = msg.from && msg.from.nick ? msg.from.nick : "unkonown";
|
||||
|
||||
if (msg.type === "invite") {
|
||||
if (msg.type === MessageType.INVITE) {
|
||||
title = "New channel invite:";
|
||||
body = msg.from.nick + " invited you to " + msg.channel;
|
||||
body = nick + " invited you to " + msg.channel;
|
||||
} else {
|
||||
title = String(msg.from.nick);
|
||||
title = nick;
|
||||
|
||||
if (channel.type !== "query") {
|
||||
if (channel.type !== ChanType.QUERY) {
|
||||
title += ` (${channel.name})`;
|
||||
}
|
||||
|
||||
if (msg.type === "message") {
|
||||
if (msg.type === MessageType.MESSAGE) {
|
||||
title += " says:";
|
||||
}
|
||||
|
||||
body = cleanIrcMessage(msg.text);
|
||||
// TODO: fix msg type and get rid of that conditional
|
||||
body = cleanIrcMessage(msg.text ? msg.text : "");
|
||||
}
|
||||
|
||||
const timestamp = Date.parse(String(msg.time));
|
||||
|
@ -184,24 +198,40 @@ function notifyMessage(
|
|||
}
|
||||
}
|
||||
|
||||
function updateUserList(channel, msg) {
|
||||
if (msg.type === "message" || msg.type === "action") {
|
||||
const user = channel.users.find((u) => u.nick === msg.from.nick);
|
||||
function updateUserList(channel: ClientChan, msg: SharedMsg) {
|
||||
switch (msg.type) {
|
||||
case MessageType.MESSAGE: // fallthrough
|
||||
|
||||
if (user) {
|
||||
user.lastMessage = new Date(msg.time).getTime() || Date.now();
|
||||
case MessageType.ACTION: {
|
||||
const user = channel.users.find((u) => u.nick === msg.from?.nick);
|
||||
|
||||
if (user) {
|
||||
user.lastMessage = new Date(msg.time).getTime() || Date.now();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
} else if (msg.type === "quit" || msg.type === "part") {
|
||||
const idx = channel.users.findIndex((u) => u.nick === msg.from.nick);
|
||||
|
||||
if (idx > -1) {
|
||||
channel.users.splice(idx, 1);
|
||||
case MessageType.QUIT: // fallthrough
|
||||
|
||||
case MessageType.PART: {
|
||||
const idx = channel.users.findIndex((u) => u.nick === msg.from?.nick);
|
||||
|
||||
if (idx > -1) {
|
||||
channel.users.splice(idx, 1);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
} else if (msg.type === "kick") {
|
||||
const idx = channel.users.findIndex((u) => u.nick === msg.target.nick);
|
||||
|
||||
if (idx > -1) {
|
||||
channel.users.splice(idx, 1);
|
||||
case MessageType.KICK: {
|
||||
const idx = channel.users.findIndex((u) => u.nick === msg.target?.nick);
|
||||
|
||||
if (idx > -1) {
|
||||
channel.users.splice(idx, 1);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ socket.on("msg:preview", function (data) {
|
|||
const netChan = store.getters.findChannel(data.chan);
|
||||
const message = netChan?.channel.messages.find((m) => m.id === data.id);
|
||||
|
||||
if (!message) {
|
||||
if (!message || !message.previews) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
socket.on("mute:changed", (response) => {
|
||||
const {target, status} = response;
|
||||
|
||||
const netChan = store.getters.findChannel(target);
|
||||
|
||||
if (netChan?.channel.type === "lobby") {
|
||||
if (netChan?.channel.type === ChanType.LOBBY) {
|
||||
for (const chan of netChan.network.channels) {
|
||||
if (chan.type !== "special") {
|
||||
if (chan.type !== ChanType.SPECIAL) {
|
||||
chan.muted = status;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
import {switchToChannel} from "../router";
|
||||
import {toClientChan} from "../chan";
|
||||
import {ClientNetwork} from "../types";
|
||||
import {ChanState} from "../../../shared/types/chan";
|
||||
|
||||
socket.on("network", function (data) {
|
||||
const network = data.networks[0];
|
||||
|
||||
network.isJoinChannelShown = false;
|
||||
network.isCollapsed = false;
|
||||
network.channels.forEach(store.getters.initChannel);
|
||||
const network: ClientNetwork = {
|
||||
...data.network,
|
||||
channels: data.network.channels.map(toClientChan),
|
||||
isJoinChannelShown: false,
|
||||
isCollapsed: false,
|
||||
};
|
||||
|
||||
store.commit("networks", [...store.state.networks, network]);
|
||||
|
||||
|
@ -19,7 +23,7 @@ socket.on("network:options", function (data) {
|
|||
const network = store.getters.findNetwork(data.network);
|
||||
|
||||
if (network) {
|
||||
network.serverOptions = data.serverOptions as typeof network.serverOptions;
|
||||
network.serverOptions = data.serverOptions;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -35,8 +39,8 @@ socket.on("network:status", function (data) {
|
|||
|
||||
if (!data.connected) {
|
||||
network.channels.forEach((channel) => {
|
||||
channel.users = [];
|
||||
channel.state = 0;
|
||||
channel.users = []; // TODO: untangle this
|
||||
channel.state = ChanState.PARTED;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,30 +1,16 @@
|
|||
import socket from "../socket";
|
||||
import {store} from "../store";
|
||||
|
||||
socket.on("sync_sort", function (data) {
|
||||
const order = data.order;
|
||||
|
||||
switch (data.type) {
|
||||
case "networks":
|
||||
store.commit(
|
||||
"sortNetworks",
|
||||
(a, b) => (order as string[]).indexOf(a.uuid) - (order as string[]).indexOf(b.uuid)
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case "channels": {
|
||||
const network = store.getters.findNetwork(data.target);
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.sort(
|
||||
(a, b) => (order as number[]).indexOf(a.id) - (order as number[]).indexOf(b.id)
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
socket.on("sync_sort:networks", function (data) {
|
||||
store.commit("sortNetworks", (a, b) => data.order.indexOf(a.uuid) - data.order.indexOf(b.uuid));
|
||||
});
|
||||
|
||||
socket.on("sync_sort:channels", function (data) {
|
||||
const network = store.getters.findNetwork(data.network);
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.sort((a, b) => data.order.indexOf(a.id) - data.order.indexOf(b.id));
|
||||
});
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import io, {Socket} from "socket.io-client";
|
||||
import type {ServerToClientEvents, ClientToServerEvents} from "../../server/types/socket-events";
|
||||
import io, {Socket as rawSocket} from "socket.io-client";
|
||||
import type {ServerToClientEvents, ClientToServerEvents} from "../../shared/types/socket-events";
|
||||
|
||||
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io({
|
||||
type Socket = rawSocket<ServerToClientEvents, ClientToServerEvents>;
|
||||
|
||||
const socket: Socket = io({
|
||||
transports: JSON.parse(document.body.dataset.transports || "['polling', 'websocket']"),
|
||||
path: window.location.pathname + "socket.io/",
|
||||
autoConnect: false,
|
||||
|
|
|
@ -3,19 +3,12 @@
|
|||
import {ActionContext, createStore, Store, useStore as baseUseStore} from "vuex";
|
||||
import {createSettingsStore} from "./store-settings";
|
||||
import storage from "./localStorage";
|
||||
import type {
|
||||
ClientChan,
|
||||
ClientConfiguration,
|
||||
ClientNetwork,
|
||||
InitClientChan,
|
||||
NetChan,
|
||||
ClientMessage,
|
||||
ClientMention,
|
||||
} from "./types";
|
||||
import type {ClientChan, ClientNetwork, NetChan, ClientMention, ClientMessage} from "./types";
|
||||
import type {InjectionKey} from "vue";
|
||||
|
||||
import {SettingsState} from "./settings";
|
||||
import {SearchQuery} from "../../server/plugins/messageStorage/types";
|
||||
import {SearchQuery} from "../../shared/types/storage";
|
||||
import {SharedConfiguration, LockedSharedConfiguration} from "../../shared/types/config";
|
||||
|
||||
const appName = document.title;
|
||||
|
||||
|
@ -59,7 +52,7 @@ export type State = {
|
|||
mentions: ClientMention[];
|
||||
hasServiceWorker: boolean;
|
||||
pushNotificationState: string;
|
||||
serverConfiguration: ClientConfiguration | null;
|
||||
serverConfiguration: SharedConfiguration | LockedSharedConfiguration | null;
|
||||
sessions: ClientSession[];
|
||||
sidebarOpen: boolean;
|
||||
sidebarDragging: boolean;
|
||||
|
@ -90,31 +83,30 @@ export type State = {
|
|||
searchEnabled: boolean;
|
||||
};
|
||||
|
||||
const state = () =>
|
||||
({
|
||||
appLoaded: false,
|
||||
activeChannel: undefined,
|
||||
currentUserVisibleError: null,
|
||||
desktopNotificationState: detectDesktopNotificationState(),
|
||||
isAutoCompleting: false,
|
||||
isConnected: false,
|
||||
networks: [],
|
||||
mentions: [],
|
||||
hasServiceWorker: false,
|
||||
pushNotificationState: "unsupported",
|
||||
serverConfiguration: null,
|
||||
sessions: [],
|
||||
sidebarOpen: false,
|
||||
sidebarDragging: false,
|
||||
userlistOpen: storage.get("thelounge.state.userlist") !== "false",
|
||||
versionData: null,
|
||||
versionStatus: "loading",
|
||||
versionDataExpired: false,
|
||||
serverHasSettings: false,
|
||||
messageSearchResults: null,
|
||||
messageSearchPendingQuery: null,
|
||||
searchEnabled: false,
|
||||
} as State);
|
||||
const state = (): State => ({
|
||||
appLoaded: false,
|
||||
activeChannel: undefined,
|
||||
currentUserVisibleError: null,
|
||||
desktopNotificationState: detectDesktopNotificationState(),
|
||||
isAutoCompleting: false,
|
||||
isConnected: false,
|
||||
networks: [],
|
||||
mentions: [],
|
||||
hasServiceWorker: false,
|
||||
pushNotificationState: "unsupported",
|
||||
serverConfiguration: null,
|
||||
sessions: [],
|
||||
sidebarOpen: false,
|
||||
sidebarDragging: false,
|
||||
userlistOpen: storage.get("thelounge.state.userlist") !== "false",
|
||||
versionData: null,
|
||||
versionStatus: "loading",
|
||||
versionDataExpired: false,
|
||||
serverHasSettings: false,
|
||||
messageSearchResults: null,
|
||||
messageSearchPendingQuery: null,
|
||||
searchEnabled: false,
|
||||
});
|
||||
|
||||
type Getters = {
|
||||
findChannelOnCurrentNetwork: (state: State) => (name: string) => ClientChan | undefined;
|
||||
|
@ -132,7 +124,6 @@ type Getters = {
|
|||
findNetwork: (state: State) => (uuid: string) => ClientNetwork | null;
|
||||
highlightCount(state: State): number;
|
||||
title(state: State, getters: Omit<Getters, "title">): string;
|
||||
initChannel: () => (channel: InitClientChan) => ClientChan;
|
||||
};
|
||||
|
||||
// getters without the state argument
|
||||
|
@ -203,31 +194,6 @@ const getters: Getters = {
|
|||
|
||||
return alertEventCount + channelname + appName;
|
||||
},
|
||||
initChannel: () => (channel: InitClientChan) => {
|
||||
// TODO: This should be a mutation
|
||||
channel.pendingMessage = "";
|
||||
channel.inputHistoryPosition = 0;
|
||||
|
||||
channel.inputHistory = [""].concat(
|
||||
channel.messages
|
||||
.filter((m) => m.self && m.text && m.type === "message")
|
||||
.map((m) => m.text)
|
||||
.reverse()
|
||||
.slice(0, 99)
|
||||
);
|
||||
channel.historyLoading = false;
|
||||
channel.scrolledToBottom = true;
|
||||
channel.editTopic = false;
|
||||
|
||||
channel.moreHistoryAvailable = channel.totalMessages! > channel.messages.length;
|
||||
delete channel.totalMessages;
|
||||
|
||||
if (channel.type === "channel") {
|
||||
channel.usersOutdated = true;
|
||||
}
|
||||
|
||||
return channel as ClientChan;
|
||||
},
|
||||
};
|
||||
|
||||
type Mutations = {
|
||||
|
|
40
client/js/types.d.ts
vendored
40
client/js/types.d.ts
vendored
|
@ -1,34 +1,25 @@
|
|||
import {defineComponent} from "vue";
|
||||
|
||||
import Chan from "../../server/models/chan";
|
||||
import Network from "../../server/models/network";
|
||||
import User from "../../server/models/user";
|
||||
import Message from "../../server/models/msg";
|
||||
import {Mention} from "../../server/client";
|
||||
import {ClientConfiguration} from "../../server/server";
|
||||
import {LinkPreview} from "../../server/plugins/irc-events/link";
|
||||
import {SharedChan} from "../../shared/types/chan";
|
||||
import {SharedNetwork} from "../../shared/types/network";
|
||||
import {SharedUser} from "../../shared/types/user";
|
||||
import {SharedMention} from "../../shared/types/mention";
|
||||
import {SharedConfiguration, LockedSharedConfiguration} from "../../shared/types/config";
|
||||
import {LinkPreview, SharedMsg} from "../../shared/types/msg";
|
||||
|
||||
interface LoungeWindow extends Window {
|
||||
g_TheLoungeRemoveLoading?: () => void;
|
||||
navigator: Window["navigator"] & {
|
||||
setAppBadge?: (highlightCount: number) => void;
|
||||
clearAppBadge?: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
type ClientUser = User & {
|
||||
//
|
||||
};
|
||||
type ClientUser = SharedUser;
|
||||
|
||||
type ClientMessage = Omit<Message, "users"> & {
|
||||
time: number;
|
||||
users: string[];
|
||||
};
|
||||
// we will eventually need to put client specific fields here
|
||||
// which are not shared with the server
|
||||
export type ClientMessage = SharedMsg;
|
||||
|
||||
type ClientChan = Omit<Chan, "users" | "messages"> & {
|
||||
type ClientChan = Omit<SharedChan, "messages"> & {
|
||||
moreHistoryAvailable: boolean;
|
||||
editTopic: boolean;
|
||||
users: ClientUser[];
|
||||
messages: ClientMessage[];
|
||||
|
||||
// these are added in store/initChannel
|
||||
|
@ -38,6 +29,8 @@ type ClientChan = Omit<Chan, "users" | "messages"> & {
|
|||
historyLoading: boolean;
|
||||
scrolledToBottom: boolean;
|
||||
usersOutdated: boolean;
|
||||
|
||||
users: ClientUser[];
|
||||
};
|
||||
|
||||
type InitClientChan = ClientChan & {
|
||||
|
@ -46,7 +39,7 @@ type InitClientChan = ClientChan & {
|
|||
};
|
||||
|
||||
// We omit channels so we can use ClientChan[] instead of Chan[]
|
||||
type ClientNetwork = Omit<Network, "channels"> & {
|
||||
type ClientNetwork = Omit<SharedNetwork, "channels"> & {
|
||||
isJoinChannelShown: boolean;
|
||||
isCollapsed: boolean;
|
||||
channels: ClientChan[];
|
||||
|
@ -57,9 +50,8 @@ type NetChan = {
|
|||
network: ClientNetwork;
|
||||
};
|
||||
|
||||
type ClientConfiguration = ClientConfiguration;
|
||||
type ClientMention = Mention & {
|
||||
localetime: string;
|
||||
type ClientMention = SharedMention & {
|
||||
localetime: string; // TODO: this needs to go the way of the dodo, nothing but a single component uses it
|
||||
channel: NetChan | null;
|
||||
};
|
||||
|
||||
|
|
|
@ -7,9 +7,9 @@ import App from "../components/App.vue";
|
|||
import storage from "./localStorage";
|
||||
import {router} from "./router";
|
||||
import socket from "./socket";
|
||||
import "./socket-events"; // this sets up all socket event listeners, do not remove
|
||||
import eventbus from "./eventbus";
|
||||
|
||||
import "./socket-events";
|
||||
import "./webpush";
|
||||
import "./keybinds";
|
||||
import {LoungeWindow} from "./types";
|
||||
|
@ -61,10 +61,10 @@ store.watch(
|
|||
|
||||
if (nav.setAppBadge) {
|
||||
if (highlightCount > 0) {
|
||||
nav.setAppBadge(highlightCount);
|
||||
nav.setAppBadge(highlightCount).catch(() => {});
|
||||
} else {
|
||||
if (nav.clearAppBadge) {
|
||||
nav.clearAppBadge();
|
||||
nav.clearAppBadge().catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,39 +6,8 @@
|
|||
] /* Specifies a list of glob patterns that match files to be included in compilation. If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. Requires TypeScript version 2.0 or later. */,
|
||||
"files": [
|
||||
"../package.json",
|
||||
"../server/types/socket-events.d.ts",
|
||||
"../server/helper.ts",
|
||||
"../server/log.ts",
|
||||
"../server/config.ts",
|
||||
"../server/client.ts",
|
||||
"../server/clientManager.ts",
|
||||
"../server/identification.ts",
|
||||
"../server/plugins/changelog.ts",
|
||||
"../server/plugins/uploader.ts",
|
||||
"../server/plugins/storage.ts",
|
||||
"../server/plugins/inputs/index.ts",
|
||||
"../server/plugins/messageStorage/sqlite.ts",
|
||||
"../server/plugins/messageStorage/text.ts",
|
||||
"../server/plugins/packages/index.ts",
|
||||
"../server/plugins/packages/publicClient.ts",
|
||||
"../server/plugins/packages/themes.ts",
|
||||
"../server/plugins/dev-server.ts",
|
||||
"../server/plugins/webpush.ts",
|
||||
"../server/plugins/sts.ts",
|
||||
"../server/plugins/clientCertificate.ts",
|
||||
"../server/plugins/auth.ts",
|
||||
"../server/plugins/auth/local.ts",
|
||||
"../server/plugins/auth/ldap.ts",
|
||||
"../server/plugins/irc-events/link.ts",
|
||||
"../server/command-line/utils.ts",
|
||||
"../server/models/network.ts",
|
||||
"../server/models/user.ts",
|
||||
"../server/models/msg.ts",
|
||||
"../server/models/prefix.ts",
|
||||
"./js/helpers/fullnamemap.json",
|
||||
"./js/helpers/simplemap.json",
|
||||
"../webpack.config.ts",
|
||||
"../babel.config.cjs"
|
||||
"./js/helpers/simplemap.json"
|
||||
] /* If no 'files' or 'include' property is present in a tsconfig.json, the compiler defaults to including all files in the containing directory and subdirectories except those specified by 'exclude'. When a 'files' property is specified, only those files and those specified by 'include' are included. */,
|
||||
// "exclude": [],
|
||||
"compilerOptions": {
|
||||
|
|
|
@ -304,6 +304,26 @@ module.exports = {
|
|||
// This value is set to `["sqlite", "text"]` by default.
|
||||
messageStorage: ["sqlite", "text"],
|
||||
|
||||
// ### `storagePolicy`
|
||||
|
||||
// When the sqlite storage is in use, control the maximum storage duration.
|
||||
// A background task will periodically clean up messages older than the limit.
|
||||
|
||||
// The available keys for the `storagePolicy` object are:
|
||||
//
|
||||
// - `enabled`: If this is false, the cleaning task is not running.
|
||||
// - `maxAgeDays`: Maximum age of an entry in days.
|
||||
// - `deletionPolicy`: Controls what types of messages are being deleted.
|
||||
// Valid options are:
|
||||
// - `statusOnly`: Only delete message types which are status related (e.g. away, back, join, parts, mode, ctcp...)
|
||||
// but keep actual messages from nicks. This keeps the DB size down while retaining "precious" messages.
|
||||
// - `everything`: Delete everything, including messages from irc nicks
|
||||
storagePolicy: {
|
||||
enabled: false,
|
||||
maxAgeDays: 7,
|
||||
deletionPolicy: "statusOnly",
|
||||
},
|
||||
|
||||
// ### `useHexIp`
|
||||
//
|
||||
// When set to `true`, users' IP addresses will be encoded as hex.
|
||||
|
|
83
package.json
83
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "thelounge",
|
||||
"description": "The self-hosted Web IRC client",
|
||||
"version": "4.4.0-pre.1",
|
||||
"version": "4.4.3",
|
||||
"preferGlobal": true,
|
||||
"bin": {
|
||||
"thelounge": "index.js"
|
||||
|
@ -18,15 +18,18 @@
|
|||
"coverage": "run-s test:* && nyc --nycrc-path=test/.nycrc-report.json report",
|
||||
"dev": "cross-env NODE_ENV=development ts-node --project server/tsconfig.json server/index.ts start --dev",
|
||||
"format:prettier": "prettier --write \"**/*.*\"",
|
||||
"generate:config:doc": "ts-node scripts/generate-config-doc.js",
|
||||
"lint:check-eslint": "eslint-config-prettier .eslintrc.cjs",
|
||||
"lint:eslint": "eslint . --report-unused-disable-directives --color",
|
||||
"lint:prettier": "prettier --list-different \"**/*.*\"",
|
||||
"lint:stylelint": "stylelint --color \"client/**/*.css\"",
|
||||
"lint": "run-p --aggregate-output --continue-on-error lint:*",
|
||||
"start": "node index start",
|
||||
"test": "run-p --aggregate-output --continue-on-error lint:* test:*",
|
||||
"test:mocha": "cross-env NODE_ENV=test webpack --mode=development && cross-env NODE_ENV=test TS_NODE_PROJECT='./test/tsconfig.json' nyc --nycrc-path=test/.nycrc-mocha.json mocha --require ts-node/register --colors --config=test/.mocharc.yml",
|
||||
"watch": "webpack --watch"
|
||||
"test": "run-p --aggregate-output --continue-on-error lint:* test:mocha",
|
||||
"test:mocha": "webpack --mode=development && cross-env NODE_ENV=test TS_NODE_PROJECT='./test/tsconfig.json' mocha --config=test/.mocharc.yml 'test/**/*.ts'",
|
||||
"test:nospec": "webpack --mode=development && cross-env NODE_ENV=test TS_NODE_PROJECT='./test/tsconfig.json' mocha --config=test/.mocharc.yml",
|
||||
"watch": "webpack --watch",
|
||||
"githooks-install": "git config core.hooksPath scripts/git-hooks"
|
||||
},
|
||||
"keywords": [
|
||||
"lounge",
|
||||
|
@ -40,7 +43,7 @@
|
|||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"files": [
|
||||
"./.thelounge_home",
|
||||
|
@ -55,10 +58,10 @@
|
|||
"@fastify/busboy": "1.0.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"chalk": "4.1.2",
|
||||
"cheerio": "1.0.0-rc.10",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"commander": "9.0.0",
|
||||
"content-disposition": "0.5.4",
|
||||
"express": "4.17.3",
|
||||
"express": "4.19.2",
|
||||
"file-type": "16.5.4",
|
||||
"filenamify": "4.3.0",
|
||||
"got": "11.8.5",
|
||||
|
@ -72,8 +75,8 @@
|
|||
"package-json": "7.0.0",
|
||||
"read": "1.0.7",
|
||||
"read-chunk": "3.2.0",
|
||||
"semver": "7.3.5",
|
||||
"socket.io": "4.5.2",
|
||||
"semver": "7.5.2",
|
||||
"socket.io": "4.6.1",
|
||||
"tlds": "1.228.0",
|
||||
"ua-parser-js": "1.0.33",
|
||||
"uuid": "8.3.2",
|
||||
|
@ -81,7 +84,7 @@
|
|||
"yarn": "1.22.17"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sqlite3": "5.1.6"
|
||||
"sqlite3": "5.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.17.10",
|
||||
|
@ -90,34 +93,34 @@
|
|||
"@fortawesome/fontawesome-free": "5.15.4",
|
||||
"@istanbuljs/nyc-config-typescript": "1.0.2",
|
||||
"@textcomplete/core": "0.1.10",
|
||||
"@textcomplete/textarea": "0.1.12",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/chai": "4.3.4",
|
||||
"@types/cheerio": "0.22.31",
|
||||
"@types/content-disposition": "0.5.5",
|
||||
"@types/express": "4.17.13",
|
||||
"@types/is-utf8": "0.2.1",
|
||||
"@textcomplete/textarea": "0.1.13",
|
||||
"@types/bcryptjs": "2.4.6",
|
||||
"@types/chai": "4.3.5",
|
||||
"@types/cheerio": "0.22.35",
|
||||
"@types/content-disposition": "0.5.8",
|
||||
"@types/express": "4.17.21",
|
||||
"@types/is-utf8": "0.2.3",
|
||||
"@types/ldapjs": "2.2.2",
|
||||
"@types/linkify-it": "3.0.2",
|
||||
"@types/lodash": "4.14.191",
|
||||
"@types/linkify-it": "3.0.5",
|
||||
"@types/lodash": "4.14.202",
|
||||
"@types/mime-types": "2.1.1",
|
||||
"@types/mocha": "9.1.1",
|
||||
"@types/mousetrap": "1.6.11",
|
||||
"@types/node": "17.0.31",
|
||||
"@types/read": "0.0.29",
|
||||
"@types/mousetrap": "1.6.15",
|
||||
"@types/node": "17.0.45",
|
||||
"@types/read": "0.0.32",
|
||||
"@types/semver": "7.3.9",
|
||||
"@types/sortablejs": "1.13.0",
|
||||
"@types/sqlite3": "3.1.8",
|
||||
"@types/ua-parser-js": "0.7.36",
|
||||
"@types/sqlite3": "3.1.11",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/webpack-env": "1.16.4",
|
||||
"@types/webpack-hot-middleware": "2.25.6",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.22.0",
|
||||
"@typescript-eslint/parser": "5.22.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.8.0",
|
||||
"@typescript-eslint/parser": "7.8.0",
|
||||
"@vue/runtime-dom": "3.2.33",
|
||||
"@vue/test-utils": "2.3.1",
|
||||
"@vue/test-utils": "2.4.0",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"babel-preset-typescript-vue3": "2.0.17",
|
||||
|
@ -127,21 +130,20 @@
|
|||
"css-loader": "6.5.1",
|
||||
"cssnano": "5.0.17",
|
||||
"dayjs": "1.10.8",
|
||||
"emoji-regex": "9.2.2",
|
||||
"eslint": "8.16.0",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint-define-config": "1.5.1",
|
||||
"eslint-plugin-vue": "9.0.1",
|
||||
"emoji-regex": "10.2.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-define-config": "2.1.0",
|
||||
"eslint-plugin-vue": "9.25.0",
|
||||
"fork-ts-checker-webpack-plugin": "7.2.13",
|
||||
"fuzzy": "0.1.3",
|
||||
"husky": "4.3.8",
|
||||
"mini-css-extract-plugin": "2.5.3",
|
||||
"mocha": "9.2.2",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"npm-run-all": "4.1.5",
|
||||
"nyc": "15.1.0",
|
||||
"postcss": "8.4.21",
|
||||
"postcss": "8.4.31",
|
||||
"postcss-import": "14.0.2",
|
||||
"postcss-loader": "6.2.1",
|
||||
"postcss-preset-env": "7.3.0",
|
||||
|
@ -156,21 +158,16 @@
|
|||
"ts-loader": "9.3.0",
|
||||
"ts-node": "10.7.0",
|
||||
"ts-sinon": "2.0.2",
|
||||
"typescript": "4.7.2",
|
||||
"typescript": "5.4.5",
|
||||
"undate": "0.3.0",
|
||||
"vue": "3.2.35",
|
||||
"vue-eslint-parser": "8.3.0",
|
||||
"vue-eslint-parser": "9.4.2",
|
||||
"vue-loader": "17.0.1",
|
||||
"vue-router": "4.0.15",
|
||||
"vuex": "4.0.2",
|
||||
"webpack": "5.76.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-dev-middleware": "5.3.3",
|
||||
"webpack-hot-middleware": "2.25.1"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "pretty-quick --staged"
|
||||
}
|
||||
"webpack-dev-middleware": "5.3.4",
|
||||
"webpack-hot-middleware": "2.25.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,13 +53,38 @@ const got = require("got");
|
|||
const dayjs = require("dayjs");
|
||||
const semver = require("semver");
|
||||
const util = require("util");
|
||||
const log = require("../server/log");
|
||||
const packageJson = require("../package.json");
|
||||
let token = process.env.CHANGELOG_TOKEN;
|
||||
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
|
||||
function timestamp() {
|
||||
const datetime = new Date().toISOString().split(".")[0].replace("T", " ");
|
||||
|
||||
return colors.dim(datetime);
|
||||
}
|
||||
|
||||
const log = {
|
||||
/* eslint-disable no-console */
|
||||
error(...args) {
|
||||
console.error(timestamp(), colors.red("[ERROR]"), ...args);
|
||||
},
|
||||
warn(...args) {
|
||||
console.error(timestamp(), colors.yellow("[WARN]"), ...args);
|
||||
},
|
||||
info(...args) {
|
||||
console.log(timestamp(), colors.blue("[INFO]"), ...args);
|
||||
},
|
||||
debug(...args) {
|
||||
console.log(timestamp(), colors.green("[DEBUG]"), ...args);
|
||||
},
|
||||
raw(...args) {
|
||||
console.log(...args);
|
||||
},
|
||||
/* eslint-enable no-console */
|
||||
};
|
||||
|
||||
const changelogPath = path.resolve(__dirname, "..", "CHANGELOG.md");
|
||||
|
||||
// CLI argument validations
|
||||
|
@ -529,9 +554,8 @@ function printPullRequest(pullRequest) {
|
|||
|
||||
// Builds a Markdown list item for a commit made directly in `master`
|
||||
function printCommit(commit) {
|
||||
return `- ${commit.messageHeadline} (${printEntryLink(commit)} ${printAuthorLink(
|
||||
commit.author.user
|
||||
)})`;
|
||||
const author_link = commit.author.user ? printAuthorLink(commit.author.user) : "unknown author";
|
||||
return `- ${commit.messageHeadline} (${printEntryLink(commit)} ${author_link})`;
|
||||
}
|
||||
|
||||
// Builds a Markdown list of all given items
|
||||
|
@ -787,15 +811,31 @@ function dedupeEntries(changelog, items) {
|
|||
// (with format `@username`) of everyone who contributed to this version.
|
||||
function extractContributors(entries) {
|
||||
const set = Object.values(entries).reduce((memo, {__typename, author}) => {
|
||||
if (__typename === "PullRequest" && author.__typename !== "Bot") {
|
||||
memo.add("@" + author.login);
|
||||
// Commit authors are *always* of type "User", so have to discriminate some
|
||||
// other way. Making the assumption of a suffix for now, see how that goes.
|
||||
} else if (__typename === "Commit" && !author.user.login.endsWith("-bot")) {
|
||||
memo.add("@" + author.user.login);
|
||||
if (!author) {
|
||||
// author can be null if GH doesn't recognize them
|
||||
return memo;
|
||||
}
|
||||
|
||||
return memo;
|
||||
switch (__typename) {
|
||||
case "PullRequest":
|
||||
if (author.__typename !== "Bot") {
|
||||
memo.add("@" + author.login);
|
||||
}
|
||||
|
||||
return memo;
|
||||
case "Commit":
|
||||
// Commit authors are *always* of type "User", so have to discriminate some
|
||||
// other way. Making the assumption of a suffix for now, see how that goes.
|
||||
// author.user can be nil if GH doesn't recognize the email
|
||||
if (!author.user || author.user.login.endsWith("-bot")) {
|
||||
return memo;
|
||||
}
|
||||
|
||||
memo.add("@" + author.user.login);
|
||||
return memo;
|
||||
default:
|
||||
throw new Error(`got an unexpected type for extractContributors: ${__typename}`);
|
||||
}
|
||||
}, new Set());
|
||||
|
||||
return Array.from(set).sort((a, b) => a.localeCompare(b, "en", {sensitivity: "base"}));
|
||||
|
|
|
@ -1,27 +1,62 @@
|
|||
"use strict";
|
||||
|
||||
// Usage: `node generate-config-doc.js DOC_REPO_PATH`
|
||||
// Usage: `npm run generate:config:doc DOC_REPO_PATH`
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ```sh
|
||||
// node scripts/generate-config-doc.js ../thelounge.github.io/
|
||||
// npm run generate:config:doc ../thelounge.github.io/
|
||||
// ```
|
||||
|
||||
const {readFileSync, writeFileSync} = require("fs");
|
||||
const colors = require("chalk");
|
||||
const log = require("../server/log");
|
||||
const {join} = require("path");
|
||||
const {spawnSync} = require("child_process");
|
||||
|
||||
function timestamp() {
|
||||
const datetime = new Date().toISOString().split(".")[0].replace("T", " ");
|
||||
|
||||
return colors.dim(datetime);
|
||||
}
|
||||
|
||||
const log = {
|
||||
/* eslint-disable no-console */
|
||||
error(...args) {
|
||||
console.error(timestamp(), colors.red("[ERROR]"), ...args);
|
||||
},
|
||||
warn(...args) {
|
||||
console.error(timestamp(), colors.yellow("[WARN]"), ...args);
|
||||
},
|
||||
info(...args) {
|
||||
console.log(timestamp(), colors.blue("[INFO]"), ...args);
|
||||
},
|
||||
debug(...args) {
|
||||
console.log(timestamp(), colors.green("[DEBUG]"), ...args);
|
||||
},
|
||||
raw(...args) {
|
||||
console.log(...args);
|
||||
},
|
||||
/* eslint-enable no-console */
|
||||
};
|
||||
|
||||
function getGitUsername() {
|
||||
return spawnSync("git", ["config", "user.name"], {encoding: "utf8"}).stdout.trim();
|
||||
}
|
||||
|
||||
const configContent = readFileSync(join(__dirname, "..", "defaults", "config.js"), "utf8");
|
||||
|
||||
const docRoot = process.argv[2];
|
||||
|
||||
if (!docRoot) {
|
||||
log.error("Missing DOC_REPO_PATH. Pass the path to the cloned `thelounge.github.io` repo.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const docPath = join(process.argv[2], "_includes", "config.js.md");
|
||||
|
||||
/** @type {string[]} */
|
||||
const acc = [];
|
||||
|
||||
const extractedDoc = configContent
|
||||
.replace(/https:\/\/thelounge\.chat\/docs/g, "/docs") // make links relative
|
||||
.split("\n")
|
||||
|
@ -37,7 +72,7 @@ const extractedDoc = configContent
|
|||
}
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
}, acc)
|
||||
.join("\n");
|
||||
|
||||
const infoBlockHeader = `<!--
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const got = require("got");
|
||||
|
|
1
scripts/git-hooks/pre-commit
Executable file
1
scripts/git-hooks/pre-commit
Executable file
|
@ -0,0 +1 @@
|
|||
yarn pretty-quick --staged
|
139
server/client.ts
139
server/client.ts
|
@ -6,10 +6,12 @@ import crypto from "crypto";
|
|||
import colors from "chalk";
|
||||
|
||||
import log from "./log";
|
||||
import Chan, {Channel, ChanType} from "./models/chan";
|
||||
import Msg, {MessageType, UserInMessage} from "./models/msg";
|
||||
import Chan, {ChanConfig} from "./models/chan";
|
||||
import Msg from "./models/msg";
|
||||
import Config from "./config";
|
||||
import {condensedTypes} from "../shared/irc";
|
||||
import {MessageType} from "../shared/types/msg";
|
||||
import {SharedMention} from "../shared/types/mention";
|
||||
|
||||
import inputs from "./plugins/inputs";
|
||||
import PublicClient from "./plugins/packages/publicClient";
|
||||
|
@ -17,10 +19,12 @@ import SqliteMessageStorage from "./plugins/messageStorage/sqlite";
|
|||
import TextFileMessageStorage from "./plugins/messageStorage/text";
|
||||
import Network, {IgnoreListItem, NetworkConfig, NetworkWithIrcFramework} from "./models/network";
|
||||
import ClientManager from "./clientManager";
|
||||
import {MessageStorage, SearchQuery, SearchResponse} from "./plugins/messageStorage/types";
|
||||
|
||||
type OrderItem = Chan["id"] | Network["uuid"];
|
||||
type Order = OrderItem[];
|
||||
import {MessageStorage} from "./plugins/messageStorage/types";
|
||||
import {StorageCleaner} from "./storageCleaner";
|
||||
import {SearchQuery, SearchResponse} from "../shared/types/storage";
|
||||
import {SharedChan, ChanType} from "../shared/types/chan";
|
||||
import {SharedNetwork} from "../shared/types/network";
|
||||
import {ServerToClientEvents} from "../shared/types/socket-events";
|
||||
|
||||
const events = [
|
||||
"away",
|
||||
|
@ -81,15 +85,6 @@ export type UserConfig = {
|
|||
networks?: NetworkConfig[];
|
||||
};
|
||||
|
||||
export type Mention = {
|
||||
chanId: number;
|
||||
msgId: number;
|
||||
type: MessageType;
|
||||
time: Date;
|
||||
text: string;
|
||||
from: UserInMessage;
|
||||
};
|
||||
|
||||
class Client {
|
||||
awayMessage!: string;
|
||||
lastActiveChannel!: number;
|
||||
|
@ -97,12 +92,12 @@ class Client {
|
|||
[socketId: string]: {token: string; openChannel: number};
|
||||
};
|
||||
config!: UserConfig;
|
||||
id!: number;
|
||||
id: string;
|
||||
idMsg!: number;
|
||||
idChan!: number;
|
||||
name!: string;
|
||||
networks!: Network[];
|
||||
mentions!: Mention[];
|
||||
mentions!: SharedMention[];
|
||||
manager!: ClientManager;
|
||||
messageStorage!: MessageStorage[];
|
||||
highlightRegex!: RegExp | null;
|
||||
|
@ -112,12 +107,12 @@ class Client {
|
|||
fileHash!: string;
|
||||
|
||||
constructor(manager: ClientManager, name?: string, config = {} as UserConfig) {
|
||||
this.id = uuidv4();
|
||||
_.merge(this, {
|
||||
awayMessage: "",
|
||||
lastActiveChannel: -1,
|
||||
attachedClients: {},
|
||||
config: config,
|
||||
id: uuidv4(),
|
||||
idChan: 1,
|
||||
idMsg: 1,
|
||||
name: name,
|
||||
|
@ -138,6 +133,15 @@ class Client {
|
|||
if (!Config.values.public && client.config.log) {
|
||||
if (Config.values.messageStorage.includes("sqlite")) {
|
||||
client.messageProvider = new SqliteMessageStorage(client.name);
|
||||
|
||||
if (Config.values.storagePolicy.enabled) {
|
||||
log.info(
|
||||
`Activating storage cleaner. Policy: ${Config.values.storagePolicy.deletionPolicy}. MaxAge: ${Config.values.storagePolicy.maxAgeDays} days`
|
||||
);
|
||||
const cleaner = new StorageCleaner(client.messageProvider);
|
||||
cleaner.start();
|
||||
}
|
||||
|
||||
client.messageStorage.push(client.messageProvider);
|
||||
}
|
||||
|
||||
|
@ -219,9 +223,12 @@ class Client {
|
|||
return chan;
|
||||
}
|
||||
|
||||
emit(event: string, data?: any) {
|
||||
emit<Ev extends keyof ServerToClientEvents>(
|
||||
event: Ev,
|
||||
...args: Parameters<ServerToClientEvents[Ev]>
|
||||
) {
|
||||
if (this.manager !== null) {
|
||||
this.manager.sockets.in(this.id.toString()).emit(event, data);
|
||||
this.manager.sockets.in(this.id).emit(event, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -251,11 +258,13 @@ class Client {
|
|||
let channels: Chan[] = [];
|
||||
|
||||
if (Array.isArray(args.channels)) {
|
||||
let badName = false;
|
||||
let badChanConf = false;
|
||||
|
||||
args.channels.forEach((chan: Chan) => {
|
||||
if (!chan.name) {
|
||||
badName = true;
|
||||
args.channels.forEach((chan: ChanConfig) => {
|
||||
const type = ChanType[(chan.type || "channel").toUpperCase()];
|
||||
|
||||
if (!chan.name || !type) {
|
||||
badChanConf = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -263,13 +272,13 @@ class Client {
|
|||
client.createChannel({
|
||||
name: chan.name,
|
||||
key: chan.key || "",
|
||||
type: chan.type,
|
||||
type: type,
|
||||
muted: chan.muted,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (badName && client.name) {
|
||||
if (badChanConf && client.name) {
|
||||
log.warn(
|
||||
"User '" +
|
||||
client.name +
|
||||
|
@ -339,7 +348,7 @@ class Client {
|
|||
|
||||
client.networks.push(network);
|
||||
client.emit("network", {
|
||||
networks: [network.getFilteredClone(this.lastActiveChannel, -1)],
|
||||
network: network.getFilteredClone(this.lastActiveChannel, -1),
|
||||
});
|
||||
|
||||
if (!network.validate(client)) {
|
||||
|
@ -685,56 +694,39 @@ class Client {
|
|||
this.emit("open", targetNetChan.chan.id);
|
||||
}
|
||||
|
||||
sort(data: {order: Order; type: "networks" | "channels"; target: string}) {
|
||||
const order = data.order;
|
||||
sortChannels(netid: SharedNetwork["uuid"], order: SharedChan["id"][]) {
|
||||
const network = _.find(this.networks, {uuid: netid});
|
||||
|
||||
if (!_.isArray(order)) {
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case "networks":
|
||||
this.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
|
||||
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort", {
|
||||
order: this.networks.map((obj) => obj.uuid),
|
||||
type: data.type,
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case "channels": {
|
||||
const network = _.find(this.networks, {uuid: data.target});
|
||||
|
||||
if (!network) {
|
||||
return;
|
||||
}
|
||||
|
||||
network.channels.sort((a, b) => {
|
||||
// Always sort lobby to the top regardless of what the client has sent
|
||||
// Because there's a lot of code that presumes channels[0] is the lobby
|
||||
if (a.type === ChanType.LOBBY) {
|
||||
return -1;
|
||||
} else if (b.type === ChanType.LOBBY) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return order.indexOf(a.id) - order.indexOf(b.id);
|
||||
});
|
||||
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort", {
|
||||
order: network.channels.map((obj) => obj.id),
|
||||
type: data.type,
|
||||
target: network.uuid,
|
||||
});
|
||||
|
||||
break;
|
||||
network.channels.sort((a, b) => {
|
||||
// Always sort lobby to the top regardless of what the client has sent
|
||||
// Because there's a lot of code that presumes channels[0] is the lobby
|
||||
if (a.type === ChanType.LOBBY) {
|
||||
return -1;
|
||||
} else if (b.type === ChanType.LOBBY) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return order.indexOf(a.id) - order.indexOf(b.id);
|
||||
});
|
||||
this.save();
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort:channels", {
|
||||
network: network.uuid,
|
||||
order: network.channels.map((obj) => obj.id),
|
||||
});
|
||||
}
|
||||
|
||||
sortNetworks(order: SharedNetwork["uuid"][]) {
|
||||
this.networks.sort((a, b) => order.indexOf(a.uuid) - order.indexOf(b.uuid));
|
||||
this.save();
|
||||
// Sync order to connected clients
|
||||
this.emit("sync_sort:networks", {
|
||||
order: this.networks.map((obj) => obj.uuid),
|
||||
});
|
||||
}
|
||||
|
||||
names(data: {target: number}) {
|
||||
|
@ -764,7 +756,7 @@ class Client {
|
|||
|
||||
quit(signOut?: boolean) {
|
||||
const sockets = this.manager.sockets.sockets;
|
||||
const room = sockets.adapter.rooms.get(this.id.toString());
|
||||
const room = sockets.adapter.rooms.get(this.id);
|
||||
|
||||
if (room) {
|
||||
for (const user of room) {
|
||||
|
@ -824,12 +816,13 @@ class Client {
|
|||
}
|
||||
|
||||
// TODO: type session to this.attachedClients
|
||||
registerPushSubscription(session: any, subscription: ClientPushSubscription, noSave = false) {
|
||||
registerPushSubscription(session: any, subscription: PushSubscriptionJSON, noSave = false) {
|
||||
if (
|
||||
!_.isPlainObject(subscription) ||
|
||||
!_.isPlainObject(subscription.keys) ||
|
||||
typeof subscription.endpoint !== "string" ||
|
||||
!/^https?:\/\//.test(subscription.endpoint) ||
|
||||
!_.isPlainObject(subscription.keys) ||
|
||||
!subscription.keys || // TS compiler doesn't understand isPlainObject
|
||||
typeof subscription.keys.p256dh !== "string" ||
|
||||
typeof subscription.keys.auth !== "string"
|
||||
) {
|
||||
|
|
|
@ -7,10 +7,9 @@ import path from "path";
|
|||
import Auth from "./plugins/auth";
|
||||
import Client, {UserConfig} from "./client";
|
||||
import Config from "./config";
|
||||
import {NetworkConfig} from "./models/network";
|
||||
import WebPush from "./plugins/webpush";
|
||||
import log from "./log";
|
||||
import {Server} from "socket.io";
|
||||
import {Server} from "./server";
|
||||
|
||||
class ClientManager {
|
||||
clients: Client[];
|
||||
|
@ -185,7 +184,6 @@ class ClientManager {
|
|||
mode: 0o600,
|
||||
});
|
||||
} catch (e: any) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
log.error(`Failed to create user ${colors.green(name)} (${e})`);
|
||||
throw e;
|
||||
}
|
||||
|
@ -253,7 +251,6 @@ class ClientManager {
|
|||
|
||||
return callback ? callback() : true;
|
||||
} catch (e: any) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
log.error(`Failed to update user ${colors.green(client.name)} (${e})`);
|
||||
|
||||
if (callback) {
|
||||
|
@ -275,7 +272,7 @@ class ClientManager {
|
|||
return true;
|
||||
}
|
||||
|
||||
private readUserConfig(name: string) {
|
||||
readUserConfig(name: string) {
|
||||
const userPath = Config.getUserConfigPath(name);
|
||||
|
||||
if (!fs.existsSync(userPath)) {
|
||||
|
@ -287,7 +284,6 @@ class ClientManager {
|
|||
const data = fs.readFileSync(userPath, "utf-8");
|
||||
return JSON.parse(data) as UserConfig;
|
||||
} catch (e: any) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
log.error(`Failed to read user ${colors.bold(name)}: ${e}`);
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ program.addCommand(require("./install").default);
|
|||
program.addCommand(require("./uninstall").default);
|
||||
program.addCommand(require("./upgrade").default);
|
||||
program.addCommand(require("./outdated").default);
|
||||
program.addCommand(require("./storage").default);
|
||||
|
||||
if (!Config.values.public) {
|
||||
require("./users").default.forEach((command: Command) => {
|
||||
|
|
|
@ -74,7 +74,9 @@ program
|
|||
|
||||
if (
|
||||
json.thelounge.supports &&
|
||||
!semver.satisfies(Helper.getVersionNumber(), json.thelounge.supports)
|
||||
!semver.satisfies(Helper.getVersionNumber(), json.thelounge.supports, {
|
||||
includePrerelease: true,
|
||||
})
|
||||
) {
|
||||
log.error(
|
||||
`${colors.red(
|
||||
|
|
123
server/command-line/storage.ts
Normal file
123
server/command-line/storage.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
import log from "../log";
|
||||
import {Command} from "commander";
|
||||
import ClientManager from "../clientManager";
|
||||
import Utils from "./utils";
|
||||
import SqliteMessageStorage from "../plugins/messageStorage/sqlite";
|
||||
import {StorageCleaner} from "../storageCleaner";
|
||||
|
||||
const program = new Command("storage").description(
|
||||
"various utilities related to the message storage"
|
||||
);
|
||||
|
||||
program
|
||||
.command("migrate")
|
||||
.argument("[username]", "migrate a specific user only, all if not provided")
|
||||
.description("Migrate message storage where needed")
|
||||
.on("--help", Utils.extraHelp)
|
||||
.action(function (user) {
|
||||
runMigrations(user).catch((err) => {
|
||||
log.error(err.toString());
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command("clean")
|
||||
.argument("[user]", "clean messages for a specific user only, all if not provided")
|
||||
.description("Delete messages from the DB based on the storage policy")
|
||||
.on("--help", Utils.extraHelp)
|
||||
.action(function (user) {
|
||||
runCleaning(user).catch((err) => {
|
||||
log.error(err.toString());
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
async function runMigrations(user?: string) {
|
||||
const manager = new ClientManager();
|
||||
const users = manager.getUsers();
|
||||
|
||||
if (user) {
|
||||
if (!users.includes(user)) {
|
||||
throw new Error(`invalid user ${user}`);
|
||||
}
|
||||
|
||||
return migrateUser(manager, user);
|
||||
}
|
||||
|
||||
for (const name of users) {
|
||||
await migrateUser(manager, name);
|
||||
// if any migration fails we blow up,
|
||||
// chances are the rest won't complete either
|
||||
}
|
||||
}
|
||||
|
||||
// runs sqlite migrations for a user, which must exist
|
||||
async function migrateUser(manager: ClientManager, user: string) {
|
||||
log.info("handling user", user);
|
||||
|
||||
if (!isUserLogEnabled(manager, user)) {
|
||||
log.info("logging disabled for user", user, ". Skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
const sqlite = new SqliteMessageStorage(user);
|
||||
await sqlite.enable(); // enable runs migrations
|
||||
await sqlite.close();
|
||||
log.info("user", user, "migrated successfully");
|
||||
}
|
||||
|
||||
function isUserLogEnabled(manager: ClientManager, user: string): boolean {
|
||||
const conf = manager.readUserConfig(user);
|
||||
|
||||
if (!conf) {
|
||||
log.error("Could not open user configuration of", user);
|
||||
return false;
|
||||
}
|
||||
|
||||
return conf.log;
|
||||
}
|
||||
|
||||
async function runCleaning(user: string) {
|
||||
const manager = new ClientManager();
|
||||
const users = manager.getUsers();
|
||||
|
||||
if (user) {
|
||||
if (!users.includes(user)) {
|
||||
throw new Error(`invalid user ${user}`);
|
||||
}
|
||||
|
||||
return cleanUser(manager, user);
|
||||
}
|
||||
|
||||
for (const name of users) {
|
||||
await cleanUser(manager, name);
|
||||
// if any migration fails we blow up,
|
||||
// chances are the rest won't complete either
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanUser(manager: ClientManager, user: string) {
|
||||
log.info("handling user", user);
|
||||
|
||||
if (!isUserLogEnabled(manager, user)) {
|
||||
log.info("logging disabled for user", user, ". Skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
const sqlite = new SqliteMessageStorage(user);
|
||||
await sqlite.enable();
|
||||
const cleaner = new StorageCleaner(sqlite);
|
||||
const num_deleted = await cleaner.runDeletesNoLimit();
|
||||
log.info(`deleted ${num_deleted} messages`);
|
||||
log.info("running a vacuum now, this might take a while");
|
||||
|
||||
if (num_deleted > 0) {
|
||||
await sqlite.vacuum();
|
||||
}
|
||||
|
||||
await sqlite.close();
|
||||
log.info(`cleaning messages for ${user} has been successful`);
|
||||
}
|
||||
|
||||
export default program;
|
|
@ -143,11 +143,11 @@ class Utils {
|
|||
data.toString()
|
||||
.trim()
|
||||
.split("\n")
|
||||
.forEach((line) => {
|
||||
.forEach((line: string) => {
|
||||
try {
|
||||
line = JSON.parse(line);
|
||||
const json = JSON.parse(line);
|
||||
|
||||
if (line.type === "success") {
|
||||
if (json.type === "success") {
|
||||
success = true;
|
||||
}
|
||||
} catch (e: any) {
|
||||
|
@ -163,11 +163,26 @@ class Utils {
|
|||
.trim()
|
||||
.split("\n")
|
||||
.forEach((line: string) => {
|
||||
const json = JSON.parse(line);
|
||||
try {
|
||||
const json = JSON.parse(line);
|
||||
|
||||
if (json.type === "error") {
|
||||
log.error(json.data);
|
||||
switch (json.type) {
|
||||
case "error":
|
||||
log.error(json.data);
|
||||
break;
|
||||
case "warning":
|
||||
// this includes pointless things like "ignored scripts due to flag"
|
||||
// so let's hide it
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (e: any) {
|
||||
// we simply fall through and log at debug... chances are there's nothing the user can do about it
|
||||
// as it includes things like deprecation warnings, but we might want to know as developers
|
||||
}
|
||||
|
||||
log.debug(line);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import fs, {Stats} from "fs";
|
|||
import os from "os";
|
||||
import _ from "lodash";
|
||||
import colors from "chalk";
|
||||
import {SearchOptions} from "ldapjs";
|
||||
|
||||
import log from "./log";
|
||||
import Helper from "./helper";
|
||||
|
@ -44,7 +45,7 @@ export type Defaults = Pick<
|
|||
| "saslAccount"
|
||||
| "saslPassword"
|
||||
> & {
|
||||
join?: string;
|
||||
join: string;
|
||||
};
|
||||
|
||||
type Identd = {
|
||||
|
@ -57,7 +58,7 @@ type SearchDN = {
|
|||
rootPassword: string;
|
||||
filter: string;
|
||||
base: string;
|
||||
scope: string;
|
||||
scope: SearchOptions["scope"];
|
||||
};
|
||||
|
||||
type Ldap = {
|
||||
|
@ -76,6 +77,12 @@ type Debug = {
|
|||
raw: boolean;
|
||||
};
|
||||
|
||||
type StoragePolicy = {
|
||||
enabled: boolean;
|
||||
maxAgeDays: number;
|
||||
deletionPolicy: "statusOnly" | "everything";
|
||||
};
|
||||
|
||||
export type ConfigType = {
|
||||
public: boolean;
|
||||
host: string | undefined;
|
||||
|
@ -97,6 +104,7 @@ export type ConfigType = {
|
|||
defaults: Defaults;
|
||||
lockNetwork: boolean;
|
||||
messageStorage: string[];
|
||||
storagePolicy: StoragePolicy;
|
||||
useHexIp: boolean;
|
||||
webirc?: WebIRC;
|
||||
identd: Identd;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import log from "./log";
|
||||
import fs from "fs";
|
||||
import net, {Socket} from "net";
|
||||
import colors from "chalk";
|
||||
import Helper from "./helper";
|
||||
import Config from "./config";
|
||||
import log from "./log";
|
||||
|
||||
type Connection = {
|
||||
socket: Socket;
|
||||
|
@ -66,31 +66,56 @@ class Identification {
|
|||
|
||||
serverConnection(socket: Socket) {
|
||||
socket.on("error", (err: string) => log.error(`Identd socket error: ${err}`));
|
||||
socket.on("data", (data) => {
|
||||
socket.setTimeout(5000, () => {
|
||||
log.warn(
|
||||
`identd: no data received, closing connection to ${
|
||||
socket.remoteAddress || "undefined"
|
||||
}`
|
||||
);
|
||||
socket.destroy();
|
||||
});
|
||||
socket.once("data", (data) => {
|
||||
this.respondToIdent(socket, data);
|
||||
socket.end();
|
||||
});
|
||||
}
|
||||
|
||||
respondToIdent(socket: Socket, buffer: Buffer) {
|
||||
if (!socket.remoteAddress) {
|
||||
log.warn("identd: no remote address");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = buffer.toString().split(",");
|
||||
|
||||
const lport = parseInt(data[0], 10) || 0;
|
||||
const fport = parseInt(data[1], 10) || 0;
|
||||
|
||||
if (lport < 1 || fport < 1 || lport > 65535 || fport > 65535) {
|
||||
log.warn(`identd: bogus request from ${socket.remoteAddress}`);
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`identd: remote ${socket.remoteAddress} query ${lport}, ${fport}`);
|
||||
|
||||
for (const connection of this.connections.values()) {
|
||||
if (connection.socket.remotePort === fport && connection.socket.localPort === lport) {
|
||||
return socket.write(
|
||||
`${lport}, ${fport} : USERID : TheLounge : ${connection.user}\r\n`
|
||||
);
|
||||
// we only want to respond if all the ip,port tuples match, to avoid user enumeration
|
||||
if (
|
||||
connection.socket.remotePort === fport &&
|
||||
connection.socket.localPort === lport &&
|
||||
socket.remoteAddress === connection.socket.remoteAddress &&
|
||||
socket.localAddress === connection.socket.localAddress
|
||||
) {
|
||||
const reply = `${lport}, ${fport} : USERID : TheLounge : ${connection.user}\r\n`;
|
||||
log.debug(`identd: reply is ${reply.trimEnd()}`);
|
||||
socket.write(reply);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
socket.write(`${lport}, ${fport} : ERROR : NO-USER\r\n`);
|
||||
const reply = `${lport}, ${fport} : ERROR : NO-USER\r\n`;
|
||||
log.debug(`identd: reply is ${reply.trimEnd()}`);
|
||||
socket.write(reply);
|
||||
}
|
||||
|
||||
addSocket(socket: Socket, user: string) {
|
||||
|
@ -127,8 +152,21 @@ class Identification {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!connection.socket.remoteAddress) {
|
||||
log.warn(`oidentd: socket has no remote address, will not respond to queries`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connection.socket.localAddress) {
|
||||
log.warn(`oidentd: socket has no local address, will not respond to queries`);
|
||||
return;
|
||||
}
|
||||
|
||||
// we only want to respond if all the ip,port tuples match, to avoid user enumeration
|
||||
file +=
|
||||
`fport ${connection.socket.remotePort}` +
|
||||
`to ${connection.socket.remoteAddress}` +
|
||||
` fport ${connection.socket.remotePort}` +
|
||||
` from ${connection.socket.localAddress}` +
|
||||
` lport ${connection.socket.localPort}` +
|
||||
` { reply "${connection.user}" }\n`;
|
||||
});
|
||||
|
|
1
server/index.d.ts
vendored
1
server/index.d.ts
vendored
|
@ -1 +0,0 @@
|
|||
import "./types";
|
|
@ -2,36 +2,14 @@ import _ from "lodash";
|
|||
import log from "../log";
|
||||
import Config from "../config";
|
||||
import User from "./user";
|
||||
import Msg, {MessageType} from "./msg";
|
||||
import Msg from "./msg";
|
||||
import storage from "../plugins/storage";
|
||||
import Client from "../client";
|
||||
import Network from "./network";
|
||||
import Prefix from "./prefix";
|
||||
|
||||
export enum ChanType {
|
||||
CHANNEL = "channel",
|
||||
LOBBY = "lobby",
|
||||
QUERY = "query",
|
||||
SPECIAL = "special",
|
||||
}
|
||||
|
||||
export enum SpecialChanType {
|
||||
BANLIST = "list_bans",
|
||||
INVITELIST = "list_invites",
|
||||
CHANNELLIST = "list_channels",
|
||||
IGNORELIST = "list_ignored",
|
||||
}
|
||||
|
||||
export enum ChanState {
|
||||
PARTED = 0,
|
||||
JOINED = 1,
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export type FilteredChannel = Chan & {
|
||||
users: [];
|
||||
totalMessages: number;
|
||||
};
|
||||
import {MessageType, SharedMsg} from "../../shared/types/msg";
|
||||
import {ChanType, SpecialChanType, ChanState} from "../../shared/types/chan";
|
||||
import {SharedNetworkChan} from "../../shared/types/network";
|
||||
|
||||
export type ChanConfig = {
|
||||
name: string;
|
||||
|
@ -41,43 +19,41 @@ export type ChanConfig = {
|
|||
};
|
||||
|
||||
class Chan {
|
||||
id: number;
|
||||
messages: Msg[];
|
||||
name: string;
|
||||
key: string;
|
||||
topic: string;
|
||||
firstUnread: number;
|
||||
unread: number;
|
||||
highlight: number;
|
||||
users: Map<string, User>;
|
||||
muted: boolean;
|
||||
type: ChanType;
|
||||
state: ChanState;
|
||||
// TODO: don't force existence, figure out how to make TS infer it.
|
||||
id!: number;
|
||||
messages!: Msg[];
|
||||
name!: string;
|
||||
key!: string;
|
||||
topic!: string;
|
||||
firstUnread!: number;
|
||||
unread!: number;
|
||||
highlight!: number;
|
||||
users!: Map<string, User>;
|
||||
muted!: boolean;
|
||||
type!: ChanType;
|
||||
state!: ChanState;
|
||||
|
||||
userAway?: boolean;
|
||||
special?: SpecialChanType;
|
||||
data?: any;
|
||||
closed?: boolean;
|
||||
num_users?: number;
|
||||
static optionalProperties = ["userAway", "special", "data", "closed", "num_users"];
|
||||
|
||||
constructor(attr?: Partial<Chan>) {
|
||||
this.id = 0;
|
||||
this.messages = [];
|
||||
this.name = "";
|
||||
this.key = "";
|
||||
this.topic = "";
|
||||
this.type = ChanType.CHANNEL;
|
||||
this.state = ChanState.PARTED;
|
||||
this.firstUnread = 0;
|
||||
this.unread = 0;
|
||||
this.highlight = 0;
|
||||
this.users = new Map();
|
||||
this.muted = false;
|
||||
|
||||
if (attr) {
|
||||
Object.assign(this, attr);
|
||||
}
|
||||
_.defaults(this, attr, {
|
||||
id: 0,
|
||||
messages: [],
|
||||
name: "",
|
||||
key: "",
|
||||
topic: "",
|
||||
type: ChanType.CHANNEL,
|
||||
state: ChanState.PARTED,
|
||||
firstUnread: 0,
|
||||
unread: 0,
|
||||
highlight: 0,
|
||||
users: new Map(),
|
||||
muted: false,
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
@ -85,18 +61,11 @@ class Chan {
|
|||
}
|
||||
|
||||
pushMessage(client: Client, msg: Msg, increasesUnread = false) {
|
||||
const chan = this.id;
|
||||
const obj = {chan, msg} as {
|
||||
chan: number;
|
||||
msg: Msg;
|
||||
unread?: number;
|
||||
highlight?: number;
|
||||
};
|
||||
|
||||
const chanId = this.id;
|
||||
msg.id = client.idMsg++;
|
||||
|
||||
// If this channel is open in any of the clients, do not increase unread counter
|
||||
const isOpen = _.find(client.attachedClients, {openChannel: chan}) !== undefined;
|
||||
const isOpen = _.find(client.attachedClients, {openChannel: chanId}) !== undefined;
|
||||
|
||||
if (msg.self) {
|
||||
// reset counters/markers when receiving self-/echo-message
|
||||
|
@ -109,15 +78,15 @@ class Chan {
|
|||
}
|
||||
|
||||
if (increasesUnread || msg.highlight) {
|
||||
obj.unread = ++this.unread;
|
||||
this.unread++;
|
||||
}
|
||||
|
||||
if (msg.highlight) {
|
||||
obj.highlight = ++this.highlight;
|
||||
this.highlight++;
|
||||
}
|
||||
}
|
||||
|
||||
client.emit("msg", obj);
|
||||
client.emit("msg", {chan: chanId, msg, unread: this.unread, highlight: this.highlight});
|
||||
|
||||
// Never store messages in public mode as the session
|
||||
// is completely destroyed when the page gets closed
|
||||
|
@ -145,7 +114,8 @@ class Chan {
|
|||
}
|
||||
}
|
||||
}
|
||||
dereferencePreviews(messages) {
|
||||
|
||||
dereferencePreviews(messages: Msg[]) {
|
||||
if (!Config.values.prefetch || !Config.values.prefetchStorage) {
|
||||
return;
|
||||
}
|
||||
|
@ -161,6 +131,7 @@ class Chan {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
getSortedUsers(irc?: Network["irc"]) {
|
||||
const users = Array.from(this.users.values());
|
||||
|
||||
|
@ -183,21 +154,27 @@ class Chan {
|
|||
return userModeSortPriority[a.mode] - userModeSortPriority[b.mode];
|
||||
});
|
||||
}
|
||||
|
||||
findMessage(msgId: number) {
|
||||
return this.messages.find((message) => message.id === msgId);
|
||||
}
|
||||
|
||||
findUser(nick: string) {
|
||||
return this.users.get(nick.toLowerCase());
|
||||
}
|
||||
|
||||
getUser(nick: string) {
|
||||
return this.findUser(nick) || new User({nick}, new Prefix([]));
|
||||
}
|
||||
|
||||
setUser(user: User) {
|
||||
this.users.set(user.nick.toLowerCase(), user);
|
||||
}
|
||||
|
||||
removeUser(user: User) {
|
||||
this.users.delete(user.nick.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a clean clone of this channel that will be sent to the client.
|
||||
* This function performs manual cloning of channel object for
|
||||
|
@ -207,38 +184,54 @@ class Chan {
|
|||
* If true, channel is assumed active.
|
||||
* @param {int} lastMessage - Last message id seen by active client to avoid sending duplicates.
|
||||
*/
|
||||
getFilteredClone(lastActiveChannel?: number | boolean, lastMessage?: number): FilteredChannel {
|
||||
return Object.keys(this).reduce((newChannel, prop) => {
|
||||
if (Chan.optionalProperties.includes(prop)) {
|
||||
if (this[prop] !== undefined || (Array.isArray(this[prop]) && this[prop].length)) {
|
||||
newChannel[prop] = this[prop];
|
||||
}
|
||||
} else if (prop === "users") {
|
||||
// Do not send users, client requests updated user list whenever needed
|
||||
newChannel[prop] = [];
|
||||
} else if (prop === "messages") {
|
||||
// If client is reconnecting, only send new messages that client has not seen yet
|
||||
if (lastMessage && lastMessage > -1) {
|
||||
// When reconnecting, always send up to 100 messages to prevent message gaps on the client
|
||||
// See https://github.com/thelounge/thelounge/issues/1883
|
||||
newChannel[prop] = this[prop].filter((m) => m.id > lastMessage).slice(-100);
|
||||
} else {
|
||||
// If channel is active, send up to 100 last messages, for all others send just 1
|
||||
// Client will automatically load more messages whenever needed based on last seen messages
|
||||
const messagesToSend =
|
||||
lastActiveChannel === true || this.id === lastActiveChannel ? 100 : 1;
|
||||
getFilteredClone(
|
||||
lastActiveChannel?: number | boolean,
|
||||
lastMessage?: number
|
||||
): SharedNetworkChan {
|
||||
let msgs: SharedMsg[];
|
||||
|
||||
newChannel[prop] = this[prop].slice(-messagesToSend);
|
||||
}
|
||||
// If client is reconnecting, only send new messages that client has not seen yet
|
||||
if (lastMessage && lastMessage > -1) {
|
||||
// When reconnecting, always send up to 100 messages to prevent message gaps on the client
|
||||
// See https://github.com/thelounge/thelounge/issues/1883
|
||||
msgs = this.messages.filter((m) => m.id > lastMessage).slice(-100);
|
||||
} else {
|
||||
// If channel is active, send up to 100 last messages, for all others send just 1
|
||||
// Client will automatically load more messages whenever needed based on last seen messages
|
||||
const messagesToSend =
|
||||
lastActiveChannel === true || this.id === lastActiveChannel ? 100 : 1;
|
||||
msgs = this.messages.slice(-messagesToSend);
|
||||
}
|
||||
|
||||
(newChannel as FilteredChannel).totalMessages = this[prop].length;
|
||||
} else {
|
||||
newChannel[prop] = this[prop];
|
||||
}
|
||||
return {
|
||||
id: this.id,
|
||||
messages: msgs,
|
||||
totalMessages: this.messages.length,
|
||||
name: this.name,
|
||||
key: this.key,
|
||||
topic: this.topic,
|
||||
firstUnread: this.firstUnread,
|
||||
unread: this.unread,
|
||||
highlight: this.highlight,
|
||||
muted: this.muted,
|
||||
type: this.type,
|
||||
state: this.state,
|
||||
|
||||
return newChannel;
|
||||
}, {}) as FilteredChannel;
|
||||
special: this.special,
|
||||
data: this.data,
|
||||
closed: this.closed,
|
||||
num_users: this.num_users,
|
||||
};
|
||||
// TODO: funny array mutation below might need to be reproduced
|
||||
// static optionalProperties = ["userAway", "special", "data", "closed", "num_users"];
|
||||
// return Object.keys(this).reduce((newChannel, prop) => {
|
||||
// if (Chan.optionalProperties.includes(prop)) {
|
||||
// if (this[prop] !== undefined || (Array.isArray(this[prop]) && this[prop].length)) {
|
||||
// newChannel[prop] = this[prop];
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
writeUserLog(client: Client, msg: Msg) {
|
||||
this.messages.push(msg);
|
||||
|
||||
|
@ -271,6 +264,7 @@ class Chan {
|
|||
messageStorage.index(target.network, targetChannel, msg).catch((e) => log.error(e));
|
||||
}
|
||||
}
|
||||
|
||||
loadMessages(client: Client, network: Network) {
|
||||
if (!this.isLoggable()) {
|
||||
return;
|
||||
|
@ -327,15 +321,23 @@ class Chan {
|
|||
log.error(`Failed to load messages for ${client.name}: ${err.toString()}`)
|
||||
);
|
||||
}
|
||||
|
||||
isLoggable() {
|
||||
return this.type === ChanType.CHANNEL || this.type === ChanType.QUERY;
|
||||
}
|
||||
|
||||
setMuteStatus(muted: boolean) {
|
||||
this.muted = !!muted;
|
||||
}
|
||||
}
|
||||
|
||||
function requestZncPlayback(channel, network, from) {
|
||||
function requestZncPlayback(channel: Chan, network: Network, from: number) {
|
||||
if (!network.irc) {
|
||||
throw new Error(
|
||||
`requestZncPlayback: no irc field on network "${network.name}", this is a bug`
|
||||
);
|
||||
}
|
||||
|
||||
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,41 +1,5 @@
|
|||
import _ from "lodash";
|
||||
import {LinkPreview} from "../plugins/irc-events/link";
|
||||
import User from "./user";
|
||||
|
||||
export type UserInMessage = Partial<User> & {
|
||||
mode: string;
|
||||
};
|
||||
|
||||
export enum MessageType {
|
||||
UNHANDLED = "unhandled",
|
||||
ACTION = "action",
|
||||
AWAY = "away",
|
||||
BACK = "back",
|
||||
ERROR = "error",
|
||||
INVITE = "invite",
|
||||
JOIN = "join",
|
||||
KICK = "kick",
|
||||
LOGIN = "login",
|
||||
LOGOUT = "logout",
|
||||
MESSAGE = "message",
|
||||
MODE = "mode",
|
||||
MODE_CHANNEL = "mode_channel",
|
||||
MODE_USER = "mode_user", // RPL_UMODEIS
|
||||
MONOSPACE_BLOCK = "monospace_block",
|
||||
NICK = "nick",
|
||||
NOTICE = "notice",
|
||||
PART = "part",
|
||||
QUIT = "quit",
|
||||
CTCP = "ctcp",
|
||||
CTCP_REQUEST = "ctcp_request",
|
||||
CHGHOST = "chghost",
|
||||
TOPIC = "topic",
|
||||
TOPIC_SET_BY = "topic_set_by",
|
||||
WHOIS = "whois",
|
||||
RAW = "raw",
|
||||
PLUGIN = "plugin",
|
||||
WALLOPS = "wallops",
|
||||
}
|
||||
import {MessageType, LinkPreview, UserInMessage} from "../../shared/types/msg";
|
||||
|
||||
class Msg {
|
||||
from!: UserInMessage;
|
||||
|
@ -70,7 +34,7 @@ class Msg {
|
|||
raw_modes!: any;
|
||||
when!: Date;
|
||||
whois!: any;
|
||||
users!: UserInMessage[] | string[];
|
||||
users!: string[];
|
||||
statusmsgGroup!: string;
|
||||
params!: string[];
|
||||
|
||||
|
|
|
@ -1,24 +1,17 @@
|
|||
import _ from "lodash";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import IrcFramework, {Client as IRCClient} from "irc-framework";
|
||||
import Chan, {ChanConfig, Channel, ChanType} from "./chan";
|
||||
import Msg, {MessageType} from "./msg";
|
||||
import Chan, {ChanConfig, Channel} from "./chan";
|
||||
import Msg from "./msg";
|
||||
import Prefix from "./prefix";
|
||||
import Helper, {Hostmask} from "../helper";
|
||||
import Config, {WebIRC} from "../config";
|
||||
import STSPolicies from "../plugins/sts";
|
||||
import ClientCertificate, {ClientCertificateType} from "../plugins/clientCertificate";
|
||||
import Client from "../client";
|
||||
|
||||
/**
|
||||
* List of keys which should be sent to the client by default.
|
||||
*/
|
||||
const fieldsForClient = {
|
||||
uuid: true,
|
||||
name: true,
|
||||
nick: true,
|
||||
serverOptions: true,
|
||||
};
|
||||
import {MessageType} from "../../shared/types/msg";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
import {SharedNetwork} from "../../shared/types/network";
|
||||
|
||||
type NetworkIrcOptions = {
|
||||
host: string;
|
||||
|
@ -52,7 +45,7 @@ type NetworkStatus = {
|
|||
};
|
||||
|
||||
export type IgnoreListItem = Hostmask & {
|
||||
when?: number;
|
||||
when: number;
|
||||
};
|
||||
|
||||
type IgnoreList = IgnoreListItem[];
|
||||
|
@ -96,96 +89,93 @@ export type NetworkConfig = {
|
|||
};
|
||||
|
||||
class Network {
|
||||
nick: string;
|
||||
name: string;
|
||||
host: string;
|
||||
port: number;
|
||||
tls: boolean;
|
||||
userDisconnected: boolean;
|
||||
rejectUnauthorized: boolean;
|
||||
password: string;
|
||||
awayMessage: string;
|
||||
commands: any[];
|
||||
username: string;
|
||||
realname: string;
|
||||
leaveMessage: string;
|
||||
sasl: string;
|
||||
saslAccount: string;
|
||||
saslPassword: string;
|
||||
channels: Chan[];
|
||||
uuid: string;
|
||||
proxyHost: string;
|
||||
proxyPort: number;
|
||||
proxyUsername: string;
|
||||
proxyPassword: string;
|
||||
proxyEnabled: boolean;
|
||||
nick!: string;
|
||||
name!: string;
|
||||
host!: string;
|
||||
port!: number;
|
||||
tls!: boolean;
|
||||
userDisconnected!: boolean;
|
||||
rejectUnauthorized!: boolean;
|
||||
password!: string;
|
||||
awayMessage!: string;
|
||||
commands!: any[];
|
||||
username!: string;
|
||||
realname!: string;
|
||||
leaveMessage!: string;
|
||||
sasl!: string;
|
||||
saslAccount!: string;
|
||||
saslPassword!: string;
|
||||
channels!: Chan[];
|
||||
uuid!: string;
|
||||
proxyHost!: string;
|
||||
proxyPort!: number;
|
||||
proxyUsername!: string;
|
||||
proxyPassword!: string;
|
||||
proxyEnabled!: boolean;
|
||||
highlightRegex?: RegExp;
|
||||
|
||||
irc?: IrcFramework.Client & {
|
||||
options?: NetworkIrcOptions;
|
||||
};
|
||||
|
||||
chanCache: Chan[];
|
||||
ignoreList: IgnoreList;
|
||||
keepNick: string | null;
|
||||
chanCache!: Chan[];
|
||||
ignoreList!: IgnoreList;
|
||||
keepNick!: string | null;
|
||||
|
||||
serverOptions: {
|
||||
status!: NetworkStatus;
|
||||
|
||||
serverOptions!: {
|
||||
CHANTYPES: string[];
|
||||
PREFIX: Prefix;
|
||||
NETWORK: string;
|
||||
};
|
||||
|
||||
// TODO: this is only available on export
|
||||
hasSTSPolicy: boolean;
|
||||
status: NetworkStatus;
|
||||
hasSTSPolicy!: boolean;
|
||||
|
||||
constructor(attr?: Partial<Network>) {
|
||||
this.name = "";
|
||||
this.nick = "";
|
||||
this.host = "";
|
||||
this.port = 6667;
|
||||
this.tls = false;
|
||||
this.userDisconnected = false;
|
||||
this.rejectUnauthorized = false;
|
||||
this.password = "";
|
||||
this.awayMessage = "";
|
||||
this.commands = [];
|
||||
this.username = "";
|
||||
this.realname = "";
|
||||
this.leaveMessage = "";
|
||||
this.sasl = "";
|
||||
this.saslAccount = "";
|
||||
this.saslPassword = "";
|
||||
this.channels = [];
|
||||
this.serverOptions = {
|
||||
CHANTYPES: ["#", "&"],
|
||||
PREFIX: new Prefix([
|
||||
{symbol: "!", mode: "Y"},
|
||||
{symbol: "@", mode: "o"},
|
||||
{symbol: "%", mode: "h"},
|
||||
{symbol: "+", mode: "v"},
|
||||
]),
|
||||
NETWORK: "",
|
||||
};
|
||||
this.proxyHost = "";
|
||||
this.proxyPort = 1080;
|
||||
this.proxyUsername = "";
|
||||
this.proxyPassword = "";
|
||||
this.proxyEnabled = false;
|
||||
_.defaults(this, attr, {
|
||||
name: "",
|
||||
nick: "",
|
||||
host: "",
|
||||
port: 6667,
|
||||
tls: false,
|
||||
userDisconnected: false,
|
||||
rejectUnauthorized: false,
|
||||
password: "",
|
||||
awayMessage: "",
|
||||
commands: [],
|
||||
username: "",
|
||||
realname: "",
|
||||
leaveMessage: "",
|
||||
sasl: "",
|
||||
saslAccount: "",
|
||||
saslPassword: "",
|
||||
channels: [],
|
||||
irc: null,
|
||||
serverOptions: {
|
||||
CHANTYPES: ["#", "&"],
|
||||
PREFIX: new Prefix([
|
||||
{symbol: "!", mode: "Y"},
|
||||
{symbol: "@", mode: "o"},
|
||||
{symbol: "%", mode: "h"},
|
||||
{symbol: "+", mode: "v"},
|
||||
]),
|
||||
NETWORK: "",
|
||||
},
|
||||
|
||||
this.chanCache = [];
|
||||
this.ignoreList = [];
|
||||
this.keepNick = null;
|
||||
this.hasSTSPolicy = false;
|
||||
this.uuid = "invalid"; // sentinel value that makes us generate a new one
|
||||
proxyHost: "",
|
||||
proxyPort: 1080,
|
||||
proxyUsername: "",
|
||||
proxyPassword: "",
|
||||
proxyEnabled: false,
|
||||
|
||||
this.status = {connected: false, secure: false};
|
||||
chanCache: [],
|
||||
ignoreList: [],
|
||||
keepNick: null,
|
||||
});
|
||||
|
||||
if (attr) {
|
||||
Object.assign(this, attr);
|
||||
}
|
||||
|
||||
if (this.uuid === "invalid" || !this.uuid) {
|
||||
if (!this.uuid) {
|
||||
this.uuid = uuidv4();
|
||||
}
|
||||
|
||||
|
@ -444,10 +434,8 @@ class Network {
|
|||
}
|
||||
|
||||
if (this.irc) {
|
||||
const connected = this.irc.connection && this.irc.connection.connected;
|
||||
|
||||
if (this.nick !== oldNick) {
|
||||
if (connected) {
|
||||
if (this.irc.connected) {
|
||||
// Send new nick straight away
|
||||
this.irc.changeNick(this.nick);
|
||||
} else {
|
||||
|
@ -462,7 +450,7 @@ class Network {
|
|||
}
|
||||
|
||||
if (
|
||||
connected &&
|
||||
this.irc.connected &&
|
||||
this.realname !== oldRealname &&
|
||||
this.irc.network.cap.isEnabled("setname")
|
||||
) {
|
||||
|
@ -510,24 +498,17 @@ class Network {
|
|||
}
|
||||
}
|
||||
|
||||
getFilteredClone(lastActiveChannel?: number, lastMessage?: number) {
|
||||
const filteredNetwork = Object.keys(this).reduce((newNetwork, prop) => {
|
||||
if (prop === "channels") {
|
||||
// Channels objects perform their own cloning
|
||||
newNetwork[prop] = this[prop].map((channel) =>
|
||||
channel.getFilteredClone(lastActiveChannel, lastMessage)
|
||||
);
|
||||
} else if (fieldsForClient[prop]) {
|
||||
// Some properties that are not useful for the client are skipped
|
||||
newNetwork[prop] = this[prop];
|
||||
}
|
||||
|
||||
return newNetwork;
|
||||
}, {}) as Network;
|
||||
|
||||
filteredNetwork.status = this.getNetworkStatus();
|
||||
|
||||
return filteredNetwork;
|
||||
getFilteredClone(lastActiveChannel?: number, lastMessage?: number): SharedNetwork {
|
||||
return {
|
||||
uuid: this.uuid,
|
||||
name: this.name,
|
||||
nick: this.nick,
|
||||
serverOptions: this.serverOptions,
|
||||
status: this.getNetworkStatus(),
|
||||
channels: this.channels.map((channel) =>
|
||||
channel.getFilteredClone(lastActiveChannel, lastMessage)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
getNetworkStatus() {
|
||||
|
|
|
@ -1,29 +1,32 @@
|
|||
import _ from "lodash";
|
||||
import Prefix from "./prefix";
|
||||
|
||||
class User {
|
||||
modes: string[];
|
||||
modes!: string[];
|
||||
// Users in the channel have only one mode assigned
|
||||
away: string;
|
||||
nick: string;
|
||||
lastMessage: number;
|
||||
mode!: string;
|
||||
away!: string;
|
||||
nick!: string;
|
||||
lastMessage!: number;
|
||||
|
||||
constructor(attr: Partial<User>, prefix?: Prefix) {
|
||||
this.modes = [];
|
||||
this.away = "";
|
||||
this.nick = "";
|
||||
this.lastMessage = 0;
|
||||
_.defaults(this, attr, {
|
||||
modes: [],
|
||||
away: "",
|
||||
nick: "",
|
||||
lastMessage: 0,
|
||||
});
|
||||
|
||||
if (attr) {
|
||||
Object.assign(this, attr);
|
||||
}
|
||||
Object.defineProperty(this, "mode", {
|
||||
get() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return this.modes[0] || "";
|
||||
},
|
||||
});
|
||||
|
||||
this.setModes(this.modes, prefix || new Prefix([]));
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this.modes[0] || "";
|
||||
}
|
||||
|
||||
setModes(modes: string[], prefix: Prefix) {
|
||||
// irc-framework sets character mode, but The Lounge works with symbols
|
||||
this.modes = modes.map((mode) => prefix.modeToSymbol[mode]);
|
||||
|
|
|
@ -67,11 +67,11 @@ function advancedLdapAuth(user: string, password: string, callback: (success: bo
|
|||
});
|
||||
|
||||
const base = config.ldap.searchDN.base;
|
||||
const searchOptions = {
|
||||
const searchOptions: SearchOptions = {
|
||||
scope: config.ldap.searchDN.scope,
|
||||
filter: `(&(${config.ldap.primaryKey}=${userDN})${config.ldap.searchDN.filter})`,
|
||||
attributes: ["dn"],
|
||||
} as SearchOptions;
|
||||
};
|
||||
|
||||
ldapclient.on("error", function (err: Error) {
|
||||
log.error(`Unable to connect to LDAP server: ${err.toString()}`);
|
||||
|
@ -178,12 +178,12 @@ function advancedLdapLoadUsers(users: string[], callbackLoadUser) {
|
|||
|
||||
const remainingUsers = new Set(users);
|
||||
|
||||
const searchOptions = {
|
||||
const searchOptions: SearchOptions = {
|
||||
scope: config.ldap.searchDN.scope,
|
||||
filter: `${config.ldap.searchDN.filter}`,
|
||||
attributes: [config.ldap.primaryKey],
|
||||
paged: true,
|
||||
} as SearchOptions;
|
||||
};
|
||||
|
||||
ldapclient.search(base, searchOptions, function (err2, res) {
|
||||
if (err2) {
|
||||
|
|
|
@ -3,7 +3,7 @@ import log from "../../log";
|
|||
import Helper from "../../helper";
|
||||
import type {AuthHandler} from "../auth";
|
||||
|
||||
const localAuth: AuthHandler = (manager, client, user, password, callback) => {
|
||||
const localAuth: AuthHandler = (_manager, client, user, password, callback) => {
|
||||
// If no user is found, or if the client has not provided a password,
|
||||
// fail the authentication straight away
|
||||
if (!client || !password) {
|
||||
|
@ -40,7 +40,6 @@ const localAuth: AuthHandler = (manager, client, user, password, callback) => {
|
|||
callback(matching);
|
||||
})
|
||||
.catch((error) => {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
log.error(`Error while checking users password. Error: ${error}`);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -3,6 +3,8 @@ import colors from "chalk";
|
|||
import log from "../log";
|
||||
import pkg from "../../package.json";
|
||||
import ClientManager from "../clientManager";
|
||||
import Config from "../config";
|
||||
import {SharedChangelogData} from "../../shared/types/changelog";
|
||||
|
||||
const TIME_TO_LIVE = 15 * 60 * 1000; // 15 minutes, in milliseconds
|
||||
|
||||
|
@ -11,31 +13,17 @@ export default {
|
|||
fetch,
|
||||
checkForUpdates,
|
||||
};
|
||||
export type ChangelogData = {
|
||||
current: {
|
||||
prerelease: boolean;
|
||||
version: string;
|
||||
changelog?: string;
|
||||
url: string;
|
||||
};
|
||||
expiresAt: number;
|
||||
latest?: {
|
||||
prerelease: boolean;
|
||||
version: string;
|
||||
url: string;
|
||||
};
|
||||
packages?: boolean;
|
||||
};
|
||||
|
||||
const versions = {
|
||||
const versions: SharedChangelogData = {
|
||||
current: {
|
||||
prerelease: false,
|
||||
version: `v${pkg.version}`,
|
||||
changelog: undefined,
|
||||
url: "", // TODO: properly init
|
||||
},
|
||||
expiresAt: -1,
|
||||
latest: undefined,
|
||||
packages: undefined,
|
||||
} as ChangelogData;
|
||||
};
|
||||
|
||||
async function fetch() {
|
||||
const time = Date.now();
|
||||
|
@ -51,6 +39,7 @@ async function fetch() {
|
|||
Accept: "application/vnd.github.v3.html", // Request rendered markdown
|
||||
"User-Agent": pkg.name + "; +" + pkg.repository.url, // Identify the client
|
||||
},
|
||||
localAddress: Config.values.bind,
|
||||
});
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
|
|
|
@ -31,7 +31,7 @@ function get(uuid: string): ClientCertificateType | null {
|
|||
return {
|
||||
private_key: fs.readFileSync(paths.privateKeyPath, "utf-8"),
|
||||
certificate: fs.readFileSync(paths.certificatePath, "utf-8"),
|
||||
} as ClientCertificateType;
|
||||
};
|
||||
} catch (e: any) {
|
||||
log.error("Unable to get certificate", e);
|
||||
}
|
||||
|
@ -95,7 +95,6 @@ function generate() {
|
|||
// Set notAfter 100 years into the future just in case
|
||||
// the server actually validates this field
|
||||
cert.validity.notAfter = new Date();
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 100);
|
||||
|
||||
const attrs = [
|
||||
|
@ -122,10 +121,10 @@ function generate() {
|
|||
// Sign this certificate with a SHA256 signature
|
||||
cert.sign(keys.privateKey, md.sha256.create());
|
||||
|
||||
const pem = {
|
||||
const pem: ClientCertificateType = {
|
||||
private_key: pki.privateKeyToPem(keys.privateKey),
|
||||
certificate: pki.certificateToPem(cert),
|
||||
} as ClientCertificateType;
|
||||
};
|
||||
|
||||
return pem;
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ export default (app: express.Application) => {
|
|||
const compiler = webpack(webpackConfig);
|
||||
|
||||
app.use(
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
webpackDevMiddleware(compiler, {
|
||||
index: "/",
|
||||
publicPath: webpackConfig.output?.publicPath,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["slap", "me"];
|
||||
|
||||
|
@ -32,6 +33,8 @@ const input: PluginInputHandler = function ({irc}, chan, cmd, args) {
|
|||
|
||||
irc.action(chan.name, text);
|
||||
|
||||
// If the IRCd does not support echo-message, simulate the message
|
||||
// being sent back to us.
|
||||
if (!irc.network.cap.isEnabled("echo-message")) {
|
||||
irc.emit("action", {
|
||||
nick: irc.user.nick,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {ChanType} from "../../models/chan";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {PluginInputHandler} from "./index";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["ban", "unban", "banlist", "kickban"];
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {PluginInputHandler} from "./index";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["connect", "server"];
|
||||
const allowDisconnected = true;
|
||||
|
@ -15,7 +16,7 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (irc.connection && irc.connection.connected) {
|
||||
if (irc.connected) {
|
||||
chan.pushMessage(
|
||||
this,
|
||||
new Msg({
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {PluginInputHandler} from "./index";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["ctcp"];
|
||||
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import Helper from "../../helper";
|
||||
import {PluginInputHandler} from "./index";
|
||||
import {IgnoreListItem} from "../../models/network";
|
||||
import {ChanType, SpecialChanType} from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["ignore", "unignore", "ignorelist"];
|
||||
const commands = ["ignore", "unignore"];
|
||||
|
||||
const input: PluginInputHandler = function (network, chan, cmd, args) {
|
||||
const client = this;
|
||||
let target: string;
|
||||
// let hostmask: cmd === "ignoreList" ? string : undefined;
|
||||
let hostmask: IgnoreListItem | undefined;
|
||||
|
||||
if (cmd !== "ignorelist" && (args.length === 0 || args[0].trim().length === 0)) {
|
||||
if (args.length === 0 || args[0].trim().length === 0) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
@ -24,16 +20,13 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (cmd !== "ignorelist") {
|
||||
// Trim to remove any spaces from the hostmask
|
||||
target = args[0].trim();
|
||||
hostmask = Helper.parseHostmask(target) as IgnoreListItem;
|
||||
}
|
||||
const target = args[0].trim();
|
||||
const hostmask = Helper.parseHostmask(target);
|
||||
|
||||
switch (cmd) {
|
||||
case "ignore": {
|
||||
// IRC nicks are case insensitive
|
||||
if (hostmask!.nick.toLowerCase() === network.nick.toLowerCase()) {
|
||||
if (hostmask.nick.toLowerCase() === network.nick.toLowerCase()) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
@ -41,25 +34,14 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
text: "You can't ignore yourself",
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
!network.ignoreList.some(function (entry) {
|
||||
return Helper.compareHostmask(entry, hostmask!);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
network.ignoreList.some(function (entry) {
|
||||
return Helper.compareHostmask(entry, hostmask);
|
||||
})
|
||||
) {
|
||||
hostmask!.when = Date.now();
|
||||
network.ignoreList.push(hostmask!);
|
||||
|
||||
client.save();
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: `\u0002${hostmask!.nick}!${hostmask!.ident}@${
|
||||
hostmask!.hostname
|
||||
}\u000f added to ignorelist`,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
@ -67,32 +49,31 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
text: "The specified user/hostmask is already ignored",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
network.ignoreList.push({
|
||||
...hostmask,
|
||||
when: Date.now(),
|
||||
});
|
||||
|
||||
client.save();
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR, // TODO: Successfully added via type.Error 🤔 ?
|
||||
text: `\u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f added to ignorelist`,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
case "unignore": {
|
||||
const idx = network.ignoreList.findIndex(function (entry) {
|
||||
return Helper.compareHostmask(entry, hostmask!);
|
||||
return Helper.compareHostmask(entry, hostmask);
|
||||
});
|
||||
|
||||
// Check if the entry exists before removing it, otherwise
|
||||
// let the user know.
|
||||
if (idx !== -1) {
|
||||
network.ignoreList.splice(idx, 1);
|
||||
client.save();
|
||||
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: `Successfully removed \u0002${hostmask!.nick}!${hostmask!.ident}@${
|
||||
hostmask!.hostname
|
||||
}\u000f from ignorelist`,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
if (idx === -1) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
|
@ -100,52 +81,20 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
text: "The specified user/hostmask is not ignored",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
network.ignoreList.splice(idx, 1);
|
||||
client.save();
|
||||
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR, // TODO: Successfully removed via type.Error 🤔 ?
|
||||
text: `Successfully removed \u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f from ignorelist`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
case "ignorelist":
|
||||
if (network.ignoreList.length === 0) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: "Ignorelist is empty",
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const chanName = "Ignored users";
|
||||
const ignored = network.ignoreList.map((data) => ({
|
||||
hostmask: `${data.nick}!${data.ident}@${data.hostname}`,
|
||||
when: data.when,
|
||||
}));
|
||||
let newChan = network.getChannel(chanName);
|
||||
|
||||
if (typeof newChan === "undefined") {
|
||||
newChan = client.createChannel({
|
||||
type: ChanType.SPECIAL,
|
||||
special: SpecialChanType.IGNORELIST,
|
||||
name: chanName,
|
||||
data: ignored,
|
||||
});
|
||||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: newChan.getFilteredClone(true),
|
||||
index: network.addChannel(newChan),
|
||||
});
|
||||
} else {
|
||||
// TODO: add type for this chan/event
|
||||
newChan.data = ignored;
|
||||
|
||||
client.emit("msg:special", {
|
||||
chan: newChan.id,
|
||||
data: ignored,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
57
server/plugins/inputs/ignorelist.ts
Normal file
57
server/plugins/inputs/ignorelist.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg from "../../models/msg";
|
||||
import {ChanType, SpecialChanType} from "../../../shared/types/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["ignorelist"];
|
||||
|
||||
const input: PluginInputHandler = function (network, chan, _cmd, _args) {
|
||||
const client = this;
|
||||
|
||||
if (network.ignoreList.length === 0) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: MessageType.ERROR,
|
||||
text: "Ignorelist is empty",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const chanName = "Ignored users";
|
||||
const ignored = network.ignoreList.map((data) => ({
|
||||
hostmask: `${data.nick}!${data.ident}@${data.hostname}`,
|
||||
when: data.when,
|
||||
}));
|
||||
let newChan = network.getChannel(chanName);
|
||||
|
||||
if (typeof newChan === "undefined") {
|
||||
newChan = client.createChannel({
|
||||
type: ChanType.SPECIAL,
|
||||
special: SpecialChanType.IGNORELIST,
|
||||
name: chanName,
|
||||
data: ignored,
|
||||
});
|
||||
client.emit("join", {
|
||||
network: network.uuid,
|
||||
chan: newChan.getFilteredClone(true),
|
||||
shouldOpen: false,
|
||||
index: network.addChannel(newChan),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: add type for this chan/event
|
||||
newChan.data = ignored;
|
||||
|
||||
client.emit("msg:special", {
|
||||
chan: newChan.id,
|
||||
data: ignored,
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
commands,
|
||||
input,
|
||||
};
|
|
@ -54,6 +54,7 @@ const builtInInputs = [
|
|||
"ctcp",
|
||||
"disconnect",
|
||||
"ignore",
|
||||
"ignorelist",
|
||||
"invite",
|
||||
"kick",
|
||||
"kill",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["invite", "invitelist"];
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["kick"];
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["mode", "umode", "op", "deop", "hop", "dehop", "voice", "devoice"];
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Chan, {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import Chan from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["query", "msg", "say"];
|
||||
|
||||
|
@ -93,12 +95,14 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
|
||||
network.irc.say(targetName, msg);
|
||||
|
||||
// If the IRCd does not support echo-message, simulate the message
|
||||
// being sent back to us.
|
||||
if (!network.irc.network.cap.isEnabled("echo-message")) {
|
||||
const parsedTarget = network.irc.network.extractTargetGroup(targetName);
|
||||
let targetGroup;
|
||||
let targetGroup: string | undefined = undefined;
|
||||
|
||||
if (parsedTarget) {
|
||||
targetName = parsedTarget.target as string;
|
||||
targetName = parsedTarget.target;
|
||||
targetGroup = parsedTarget.target_group;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,11 @@ import Chan from "../../models/chan";
|
|||
import Network from "../../models/network";
|
||||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
|
||||
import Client from "../../client";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["mute", "unmute"];
|
||||
const allowDisconnected = true;
|
||||
|
@ -24,7 +26,7 @@ function args_to_channels(network: Network, args: string[]) {
|
|||
}
|
||||
|
||||
function change_mute_state(client: Client, target: Chan, valueToSet: boolean) {
|
||||
if (target.type === "special") {
|
||||
if (target.type === ChanType.SPECIAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
const commands = ["nick"];
|
||||
const allowDisconnected = true;
|
||||
|
@ -47,7 +48,7 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
// If connected to IRC, send to server and wait for ACK
|
||||
// otherwise update the nick and UI straight away
|
||||
if (network.irc) {
|
||||
if (network.irc.connection && network.irc.connection.connected) {
|
||||
if (network.irc.connected) {
|
||||
network.irc.changeNick(newNick);
|
||||
|
||||
return;
|
||||
|
|
|
@ -12,6 +12,8 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
|
||||
network.irc.notice(targetName, message);
|
||||
|
||||
// If the IRCd does not support echo-message, simulate the message
|
||||
// being sent back to us.
|
||||
if (!network.irc.network.cap.isEnabled("echo-message")) {
|
||||
let targetGroup;
|
||||
const parsedTarget = network.irc.network.extractTargetGroup(targetName);
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import Config from "../../config";
|
||||
import {ChanType, ChanState} from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType, ChanState} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["close", "leave", "part"];
|
||||
const allowDisconnected = true;
|
||||
|
@ -36,9 +37,7 @@ const input: PluginInputHandler = function (network, chan, cmd, args) {
|
|||
if (
|
||||
target.type !== ChanType.CHANNEL ||
|
||||
target.state === ChanState.PARTED ||
|
||||
!network.irc ||
|
||||
!network.irc.connection ||
|
||||
!network.irc.connection.connected
|
||||
!network.irc.connected
|
||||
) {
|
||||
this.part(network, target);
|
||||
} else {
|
||||
|
|
|
@ -4,7 +4,7 @@ const commands = ["raw", "send", "quote"];
|
|||
|
||||
const input: PluginInputHandler = function ({irc}, chan, cmd, args) {
|
||||
if (args.length !== 0) {
|
||||
irc.connection.write(args.join(" "));
|
||||
irc.raw(...args);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["cycle", "rejoin"];
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {PluginInputHandler} from "./index";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
const commands = ["topic"];
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
import {ChanType} from "../../models/chan";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -14,7 +14,7 @@ export default <IrcEventHandler>function (irc, network) {
|
|||
handleSTS(data, false);
|
||||
});
|
||||
|
||||
function handleSTS(data, shouldReconnect) {
|
||||
function handleSTS(data, shouldReconnect: boolean) {
|
||||
if (!Object.prototype.hasOwnProperty.call(data.capabilities, "sts")) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/* eslint-disable @typescript-eslint/restrict-plus-operands */
|
||||
import _ from "lodash";
|
||||
import {IrcEventHandler} from "../../client";
|
||||
|
||||
import log from "../../log";
|
||||
import Msg, {MessageType} from "../../models/msg";
|
||||
import Msg from "../../models/msg";
|
||||
import Helper from "../../helper";
|
||||
import Config from "../../config";
|
||||
import {ChanType, ChanState} from "../../models/chan";
|
||||
import {MessageType} from "../../../shared/types/msg";
|
||||
import {ChanType, ChanState} from "../../../shared/types/chan";
|
||||
|
||||
export default <IrcEventHandler>function (irc, network) {
|
||||
const client = this;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue