Compare commits
643 Commits
de14b95f9a
...
main
Author | SHA1 | Date | |
---|---|---|---|
3503a63a10 | |||
e41529b61f | |||
1e7e851dc9 | |||
80a5b76172 | |||
6cd7f013f2 | |||
419355640a | |||
9c2d8cfbb6 | |||
06c274c4b5 | |||
bdf8817bf8 | |||
cd1fa7c4d4 | |||
d75e07e022 | |||
bebb7918bd | |||
14ff00c730 | |||
edd29ed1ed | |||
e24385c3f1 | |||
0be11fb2ae | |||
98dc729447 | |||
7dcfac00cb | |||
3b319927c6 | |||
ad1d7786af | |||
500624d0ff | |||
6dd4639727 | |||
c00f5d1593 | |||
ee1417774a | |||
dfd694782e | |||
8348011b34 | |||
b0c08e72e2 | |||
60ea2b4ed0 | |||
078515572e | |||
8a12ddabc2 | |||
1b8531a198 | |||
a5ede49402 | |||
4bebc18c67 | |||
562e6a16e2 | |||
1bb79054b4 | |||
8ec8eb63e4 | |||
688c4099ad | |||
238407c060 | |||
97587ab92f | |||
fdff085b37 | |||
743e794d5d | |||
9669861dec | |||
bb78c61b08 | |||
5b8ccdf5ae | |||
922e2fbdf4 | |||
42e589d397 | |||
02fff14351 | |||
3e70fd3892 | |||
dd040582d4 | |||
d79deef5c0 | |||
048c003ff0 | |||
ccbfe8e6d5 | |||
3bc8d2466b | |||
356b385fc9 | |||
b5c7fa4246 | |||
e7c0be98ee | |||
ca13d237a3 | |||
799043c88e | |||
f12d4cb13a | |||
c3142a3282 | |||
134d94d6fc | |||
6b30886bb6 | |||
090679d6c3 | |||
88376b82ee | |||
d841e88e00 | |||
aef508ef77 | |||
673c128c1b | |||
22668c5532 | |||
fb27214d36 | |||
8ff8b01148 | |||
0ffc652194 | |||
69c9c1f83f | |||
94753ab60b | |||
375e5cb405 | |||
38cfc05962 | |||
c0d2c0caae | |||
3af263fb12 | |||
d36fa2e241 | |||
59f56087c6 | |||
e47954f72b | |||
f831962868 | |||
984d5fc529 | |||
433588d6cd | |||
a40f23380f | |||
4d28df5687 | |||
df9b9359f7 | |||
c93e1e4fa3 | |||
fa79afa42c | |||
87431327e6 | |||
a129a9bc4c | |||
561bd50a42 | |||
8d9541ac9b | |||
ae012fa5b8 | |||
e8d45a4c61 | |||
56f263d755 | |||
1b686ca183 | |||
df4eb42769 | |||
dc2d59ca22 | |||
35236328b2 | |||
99a0ba6945 | |||
4ab4a0fa7f | |||
7fd895c82e | |||
5216076844 | |||
04ebfc9002 | |||
193f15dcfc | |||
98b89c19f0 | |||
802b3de2eb | |||
317e136f22 | |||
e12c63c552 | |||
5512d0f717 | |||
cf8c3d67e1 | |||
b9d0d0a5d2 | |||
40b09ec1bf | |||
c7be72d6a0 | |||
ea837273cd | |||
fa69be743a | |||
cde28daff9 | |||
51daea7942 | |||
d236b31630 | |||
1fffd32414 | |||
7555e1555a | |||
a1f0529ce9 | |||
f767287bd3 | |||
449849e9b5 | |||
433abc6e83 | |||
c1dba5d038 | |||
74adea194e | |||
c369a78d06 | |||
64cb5a0c22 | |||
531d462b3f | |||
166482c735 | |||
23a29b3b2d | |||
c042100189 | |||
e33731c4e1 | |||
c98f117617 | |||
d4001cb7c7 | |||
dd10174709 | |||
b98886d106 | |||
649ba3995f | |||
4c7a25da1e | |||
97bb68593a | |||
8e98b464f1 | |||
8a9d0ed446 | |||
e0e0712102 | |||
f739dcf0c7 | |||
f70705dc55 | |||
57e65cf6ac | |||
87f2a3dda6 | |||
0720a8550d | |||
568add06c7 | |||
330552ebb4 | |||
ae7ab16970 | |||
dd29b96f8f | |||
8f1bcb6f13 | |||
affe72e919 | |||
d11cbfa51b | |||
067d53957d | |||
ac9e5145e4 | |||
8f722f5560 | |||
3f256b1027 | |||
b54c17e497 | |||
238d62f17d | |||
373a3901f5 | |||
28be2a8bbb | |||
f9938b5f9d | |||
2975018f91 | |||
09ef387d71 | |||
ff8a148dbc | |||
d0797ba631 | |||
1bea08654f | |||
f35bb5c8ac | |||
6286ca90b9 | |||
a4bf37d534 | |||
a37438bdaf | |||
9b01d8ed09 | |||
5d3a0288d4 | |||
3a0138b70d | |||
0d3cb9957e | |||
60531dc502 | |||
1f7028703f | |||
35b802f580 | |||
d113b4cabe | |||
d2027b7fe2 | |||
e0d356eb35 | |||
437dcdb09c | |||
46acb4a186 | |||
d6b90b87c0 | |||
bf9c51d80e | |||
7c90d40f42 | |||
1459a7c51b | |||
ec8c66d490 | |||
6ebcacdd41 | |||
940d7b630d | |||
575ea7cdd3 | |||
629a01e55e | |||
195c56b5ec | |||
11ae5f8e5a | |||
4c23e17dfe | |||
c511484511 | |||
76a25326e8 | |||
c51245501c | |||
6156429e31 | |||
eb50ef5524 | |||
950643ac1d | |||
577eec6fa4 | |||
2e011a8022 | |||
7b91684e47 | |||
0477f7e656 | |||
b354ac2895 | |||
c6340a289c | |||
903c291e2c | |||
afbece60ab | |||
79893c5cea | |||
d217836bc8 | |||
fee56c383d | |||
b871d873c6 | |||
1e97193030 | |||
bb992e29a6 | |||
6836138fc1 | |||
77ec284ade | |||
3f0f6e4d3b | |||
8a481ecb0e | |||
e1195c18d8 | |||
ce8ec5cee4 | |||
1aa406cae8 | |||
1bb6f6fc41 | |||
b2f8595198 | |||
36934ed56f | |||
b5e18faaaa | |||
4a29b68d57 | |||
c94f23fbe5 | |||
abfdb0f08d | |||
84b21dde6d | |||
1b53a6ec2d | |||
583f581192 | |||
3da491ea49 | |||
04768347f2 | |||
261d7e0506 | |||
f98013f2be | |||
c426f0c8c3 | |||
a63240ca25 | |||
05b1f0f645 | |||
baf08ae321 | |||
149bab2fc5 | |||
80274ecee3 | |||
e08e59d625 | |||
98eb12104c | |||
3c93aad053 | |||
0cc87e95e9 | |||
75dcd3ab1c | |||
aaea13a2fc | |||
e940fdc2d3 | |||
d7e98642f5 | |||
ee37e8893b | |||
8fa17b050b | |||
b336dbbc78 | |||
a68a2d0e30 | |||
99371bcdec | |||
1bdd7df712 | |||
3ec4482043 | |||
9a30958979 | |||
538ec28da9 | |||
9824435b6e | |||
4e436c00f0 | |||
f0088671ea | |||
d92bf9073e | |||
8944bb95d8 | |||
b357619f7a | |||
5e754b0103 | |||
7e2f6ede65 | |||
91d6782a3c | |||
35c486035e | |||
b66b0aa315 | |||
0b196805b2 | |||
33be9b3fc5 | |||
1d0a7a97d2 | |||
89f1f42e7a | |||
c2430856b4 | |||
4c84146cff | |||
6f479d26c5 | |||
5225257bb5 | |||
651641f63c | |||
8dcd3ef488 | |||
2d5fec25c7 | |||
5566f4b6cf | |||
5018d04b78 | |||
c65995e5fa | |||
071ee9f6b4 | |||
3e23d2751f | |||
9731835c31 | |||
e6381c7bba | |||
ace8ad72f5 | |||
50309c6ba9 | |||
0c220b2fc4 | |||
1ce68eec7e | |||
5f4cc7c46e | |||
7330729ac1 | |||
d5a25af6c8 | |||
634e894522 | |||
4f2fbfc228 | |||
4b5f83c45f | |||
c561851bb6 | |||
edca1be70a | |||
fd27105c2c | |||
c010758164 | |||
7b3d93b290 | |||
15ea3c8306 | |||
bcabf148ad | |||
64a9c7be21 | |||
b5990f0c1d | |||
f15d3ed542 | |||
b6028ef740 | |||
e61db352c7 | |||
b3d74a3816 | |||
122ab9725b | |||
ac64c27c3e | |||
8ad762eaa2 | |||
acce1c5a5c | |||
e57d5b0f30 | |||
b45ab79c0e | |||
91cddfa8d8 | |||
08017ac2bb | |||
7e0cfee8f1 | |||
b115c28ee7 | |||
5db4f46421 | |||
1a75b30c50 | |||
2f78b901e3 | |||
575703a7e5 | |||
8e0f239993 | |||
8a1948520d | |||
0fe0ebfb3e | |||
9ee4476f4d | |||
0e98cbaba5 | |||
22dc4db61b | |||
42147148ea | |||
87e8f9fd39 | |||
033a86b3a6 | |||
8394e7222d | |||
d8fc90dd1f | |||
04db24b6af | |||
b0d65add1f | |||
6b69f0bf46 | |||
6373af32ba | |||
d87cdbb1ff | |||
5d87121631 | |||
c205acbb17 | |||
5462e3d52f | |||
ef5bc3cb8a | |||
d8da3ca3b8 | |||
3d8fbba767 | |||
8183dc82bb | |||
a799a9c07e | |||
6eeb19607f | |||
56547cc084 | |||
75925b1117 | |||
2824bd1e38 | |||
7293f4a6d5 | |||
f7ddd58a2c | |||
4a747d7314 | |||
f5074934e4 | |||
6c0b5e1c19 | |||
1b75f3d504 | |||
b673a49af6 | |||
944bdcebb7 | |||
c6d012cfb4 | |||
0732bd1de3 | |||
e65bd5c426 | |||
3177c31b08 | |||
cee42b4bcc | |||
9e252193d4 | |||
08eb466e27 | |||
0cf5647a89 | |||
4113259975 | |||
4bdf40e905 | |||
a385204256 | |||
e1d1dd94e6 | |||
940e8c9ac4 | |||
9a22ab7f18 | |||
8e99199f29 | |||
481f265a44 | |||
6fd8383439 | |||
54c31500a3 | |||
e08c413682 | |||
73845eb5d4 | |||
a47737967a | |||
67b89f1aef | |||
e1e91c2984 | |||
a813ceb723 | |||
cd0813fb14 | |||
62caa63dc5 | |||
ef1b49f291 | |||
b8db486410 | |||
df75659a05 | |||
a3555ed3cd | |||
fd7d24c121 | |||
a9ae96622a | |||
4afc0b6ab8 | |||
ad5397b3c7 | |||
49e11d2b6b | |||
df7981f896 | |||
003cb24dd8 | |||
33da4314af | |||
c2c1211b9a | |||
5d843ebf92 | |||
18fa3aec10 | |||
18a1c2f71c | |||
21ecdf77c6 | |||
cfe2ac0607 | |||
74566a895a | |||
587334a3e1 | |||
30cce2ec03 | |||
24f48788ab | |||
bed00aa4c5 | |||
fd16c95b8f | |||
39f6e4c637 | |||
f8c34213cb | |||
eb889711f5 | |||
8dd6ed7888 | |||
0cc38ac575 | |||
cde9b3f74a | |||
75245c0858 | |||
631d8c7a6b | |||
9746f3d86e | |||
d412f86577 | |||
e665823f5b | |||
966669ebbc | |||
be59329581 | |||
4181661faf | |||
dd03075831 | |||
a4497739fa | |||
cdbe97137e | |||
7892fd6c03 | |||
0543a720f2 | |||
af2941f4e0 | |||
3004105b5e | |||
bb2aca71c1 | |||
48763e6173 | |||
70f702f78e | |||
a46425d51f | |||
e81a7c90b8 | |||
9c2b84027a | |||
704e660f37 | |||
31c4e5a646 | |||
1c749f68bc | |||
fbc9a39485 | |||
eb76e7ebb9 | |||
20cb668e09 | |||
86cac34ef3 | |||
074e688ed8 | |||
34da786b65 | |||
a700263eda | |||
1b92e7287d | |||
bf8f3a08ec | |||
6d103af5c9 | |||
faef584f8b | |||
ce5114afc9 | |||
c5bcb9fc14 | |||
6e92882176 | |||
ebc8fc8797 | |||
38f2b2af57 | |||
b7c595214b | |||
58676a5508 | |||
12f11e4468 | |||
e1252709b3 | |||
80d458c445 | |||
f88520d925 | |||
dd4bca8c06 | |||
81632fa467 | |||
c6c365abf0 | |||
099ff28c76 | |||
e57d39c8ab | |||
b40b3a2716 | |||
731be16e53 | |||
a7bd0c1c3e | |||
e938f79182 | |||
36323569c0 | |||
e7b1376a43 | |||
9526a19aab | |||
1b4ccf6693 | |||
d4ecafb6de | |||
20550bc3d7 | |||
548b79a3a6 | |||
ed0b2eb913 | |||
bdc11cb92e | |||
db1993f97e | |||
8b0d72bf13 | |||
64f03e5454 | |||
522265db5c | |||
22daa45b92 | |||
76cd519c00 | |||
123f9a5a8a | |||
717a5c75de | |||
12a1300df6 | |||
5fb50b9221 | |||
e8dc9a01fe | |||
6b54580f91 | |||
cc51bb2c70 | |||
00960652fe | |||
712d2bcdcb | |||
897f111e9d | |||
2f69e45b26 | |||
b9473cada9 | |||
cbcf51d1eb | |||
1226162b50 | |||
f1ac80d468 | |||
e8f287b8a9 | |||
e97cd1d78e | |||
6218d65b72 | |||
d0ab3d94ae | |||
a0d5c4a368 | |||
51b603c894 | |||
9679aec739 | |||
31646beac3 | |||
977534721a | |||
8bed52df96 | |||
340b98e39c | |||
7ff5a8c005 | |||
25a8ca4cdc | |||
9c200a4e46 | |||
2f98dfa538 | |||
d5c0c474b3 | |||
4fcda5f62d | |||
49616d77a6 | |||
ac4234281c | |||
a5bcfe61b2 | |||
1989f84e94 | |||
44babd02c3 | |||
870ec9dac2 | |||
28871f3b66 | |||
4996335c7b | |||
a37b532793 | |||
7a54594967 | |||
00a0dca1f9 | |||
cf68414710 | |||
f870aa6cfb | |||
7606a0ec7e | |||
60385704be | |||
5be420ebd8 | |||
5eb0ae414d | |||
49e67a8006 | |||
f796585aa0 | |||
f14c91d3e3 | |||
cd9c3db0b3 | |||
80c18c25e0 | |||
4307a4513b | |||
e97c33cec5 | |||
9b1f24eafe | |||
914de79400 | |||
867c1debd6 | |||
a93cf8fc19 | |||
e839fea580 | |||
3d0c240c63 | |||
29ab4c4640 | |||
bcb11ecbd7 | |||
14ffa90208 | |||
6dda0cc44f | |||
0983c8ed57 | |||
6f7c5c50f7 | |||
3c45c2d1e6 | |||
3665096836 | |||
1249f0895d | |||
ffb8ac8ddc | |||
4168b2ce41 | |||
1c6434630e | |||
4591633400 | |||
3ef0541886 | |||
8e2a3335ab | |||
52391c922e | |||
c5b0176e95 | |||
5e6fd50c49 | |||
b741959312 | |||
212076c3a3 | |||
22076e9929 | |||
e779a669c8 | |||
300eb14ca4 | |||
a7250efd06 | |||
14e0576329 | |||
b792588364 | |||
241065e683 | |||
36ca665fdb | |||
7fef055c14 | |||
62ac787546 | |||
cad2eef7af | |||
eaf3d85974 | |||
2ee851bbe2 | |||
951e60d2a1 | |||
06971e2597 | |||
28355e4cda | |||
321551a361 | |||
f9fd8a9779 | |||
cf89db63bc | |||
36c4fb0897 | |||
0c1c6a352d | |||
4b1208f7d4 | |||
f0577984b3 | |||
8cb5de7d39 | |||
b65e1cb873 | |||
c588a73233 | |||
76cc4b4d07 | |||
c093b5e6f6 | |||
934c07e34e | |||
bd211d777e | |||
fa8aec49b7 | |||
d75f6d89ff | |||
c0c835df11 | |||
27c10a0324 | |||
e6721b1fa8 | |||
2316c6b1fe | |||
af04f70611 | |||
d14c2868fd | |||
a06ef00e2a | |||
b110456029 | |||
45a63eaecd | |||
4b49037f68 | |||
579c1fe8f8 | |||
a0430fdaa1 | |||
cc526c529f | |||
6fbb9a99bb | |||
861970c436 | |||
81b8e07169 | |||
ec0e500578 | |||
9c2ad4ac14 | |||
ebbe4e8241 | |||
14b2357b2d | |||
a72f989c31 | |||
a7356ff41d | |||
ec36240963 | |||
2d3b4f1a31 | |||
ad25398924 | |||
5a455fff93 | |||
1f1543bc97 | |||
1be95f01b1 | |||
4149207317 | |||
e177ca26b3 | |||
d19a84a645 | |||
ef998d49f8 | |||
e30ee7b40b | |||
c21b0c9bb0 | |||
b8eb6a8c7e | |||
abd9086ae3 | |||
48905dd70d | |||
a0b4b5f61a | |||
83dd3130a1 |
@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
"env": {},
|
|
||||||
"extends": [],
|
|
||||||
"globals": {},
|
|
||||||
"plugins": [],
|
|
||||||
"ignorePatterns": [
|
|
||||||
"node_modules/", "dist/", "webpack.config.js",
|
|
||||||
"build/",
|
|
||||||
"vendor/",
|
|
||||||
"support/documentation",
|
|
||||||
"build-*js"],
|
|
||||||
"rules": {}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
|
49
.github/dependabot.yml
vendored
Normal file
49
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
|
||||||
|
groups:
|
||||||
|
minor-and-patch:
|
||||||
|
applies-to: version-updates
|
||||||
|
update-types:
|
||||||
|
- "patch"
|
||||||
|
- "minor"
|
||||||
|
|
||||||
|
versioning-strategy: increase
|
||||||
|
|
||||||
|
ignore:
|
||||||
|
- dependency-name: typescript
|
||||||
|
versions:
|
||||||
|
- ">=5.6.0" # linting libs are not ready for 5.6
|
||||||
|
- dependency-name: "@types/node"
|
||||||
|
versions:
|
||||||
|
- ">=17.0.0" # must be set to the Peertube required version.
|
||||||
|
- dependency-name: "@peertube/peertube-types"
|
||||||
|
versions:
|
||||||
|
- ">5.2.0" # must be set to the Peertube required version.
|
||||||
|
- dependency-name: eslint
|
||||||
|
versions:
|
||||||
|
- ">=9.0.0" # not ready for v9, missing dependencies.
|
||||||
|
- dependency-name: "@stylistic/eslint-plugin"
|
||||||
|
versions:
|
||||||
|
- ">=4.0.0" # needs eslint >= 9.0.0
|
||||||
|
- dependency-name: got
|
||||||
|
versions:
|
||||||
|
- ">=12.0.0" # breaking changes, must adapt code.
|
||||||
|
- dependency-name: "@typescript-eslint/parser"
|
||||||
|
versions:
|
||||||
|
- ">=8.5.0" # for now 8.5.0 is broken because of the lack of ./tsconfig.json file. Must fix conf.
|
||||||
|
- dependency-name: "eslint-config-love"
|
||||||
|
versions:
|
||||||
|
- ">=85.0.0" # Versions goes up to 118 very quickly. Too much breaking changes. We must do this progressively.
|
||||||
|
- dependency-name: "openid-client"
|
||||||
|
versions:
|
||||||
|
- ">=6.0.0" # this is a complete rewrite. We have to check if it is compatible.
|
34
.github/workflows/gh-build.yml
vendored
Normal file
34
.github/workflows/gh-build.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
name: github build and lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
types: [assigned, opened, synchronize, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: false # Fetch Hugo themes (true OR recursive)
|
||||||
|
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: sudo apt update && sudo apt install wget reuse -y
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: npm run lint
|
4
.github/workflows/gh-pages.yml
vendored
4
.github/workflows/gh-pages.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
# SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
- name: Setup Hugo
|
- name: Setup Hugo
|
||||||
uses: peaceiris/actions-hugo@v2
|
uses: peaceiris/actions-hugo@v2
|
||||||
with:
|
with:
|
||||||
hugo-version: '0.80.0'
|
hugo-version: '0.132.2'
|
||||||
extended: true
|
extended: true
|
||||||
|
|
||||||
- name: Generate documentation translations
|
- name: Generate documentation translations
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
# SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
# SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@ -19,10 +19,10 @@ builddoctranslations:
|
|||||||
|
|
||||||
pages:
|
pages:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: registry.gitlab.com/pages/hugo/hugo_extended:latest
|
image: registry.gitlab.com/pages/hugo/hugo_extended:0.132.2
|
||||||
variables:
|
variables:
|
||||||
GIT_SUBMODULE_STRATEGY: recursive
|
GIT_SUBMODULE_STRATEGY: recursive
|
||||||
GIT_SUBMODULE_PATHS: support/documentation/themes/hugo-theme-learn
|
GIT_SUBMODULE_PATHS: support/documentation/themes/hugo-theme-relearn
|
||||||
script:
|
script:
|
||||||
# gitlab need the generated documentation to be in the /public dir.
|
# gitlab need the generated documentation to be in the /public dir.
|
||||||
- hugo -s support/documentation/ --minify -d ../../public/ --baseURL='https://livingston.frama.io/peertube-plugin-livechat/'
|
- hugo -s support/documentation/ --minify -d ../../public/ --baseURL='https://livingston.frama.io/peertube-plugin-livechat/'
|
||||||
|
8
.gitmodules
vendored
8
.gitmodules
vendored
@ -1,7 +1,7 @@
|
|||||||
# SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
# SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
[submodule "documentation/themes/hugo-theme-learn"]
|
[submodule "support/documentation/themes/hugo-theme-relearn"]
|
||||||
path = support/documentation/themes/hugo-theme-learn
|
path = support/documentation/themes/hugo-theme-relearn
|
||||||
url = https://github.com/matcornic/hugo-theme-learn.git
|
url = https://github.com/McShelby/hugo-theme-relearn.git
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
# SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
16
.reuse/dep5
16
.reuse/dep5
@ -10,25 +10,29 @@ Source: https://github.com/JohnXLivingston/peertube-plugin-livechat/
|
|||||||
# License: ...
|
# License: ...
|
||||||
|
|
||||||
Files: CHANGELOG.md
|
Files: CHANGELOG.md
|
||||||
Copyright: 2024 John Livingston <https://www.john-livingston.fr/>
|
Copyright: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
License: AGPL-3.0-only
|
License: AGPL-3.0-only
|
||||||
|
|
||||||
Files: languages/*
|
Files: languages/*
|
||||||
Copyright: 2024 John Livingston <https://www.john-livingston.fr/>
|
Copyright: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
License: AGPL-3.0-only
|
License: AGPL-3.0-only
|
||||||
|
|
||||||
Files: support/documentation/po/*
|
Files: support/documentation/po/*
|
||||||
Copyright: 2024 John Livingston <https://www.john-livingston.fr/>
|
Copyright: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
License: AGPL-3.0-only
|
License: AGPL-3.0-only
|
||||||
|
|
||||||
Files: support/documentation/content/en/*
|
Files: support/documentation/content/en/*
|
||||||
Copyright: 2024 John Livingston <https://www.john-livingston.fr/>
|
Copyright: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
License: AGPL-3.0-only
|
License: AGPL-3.0-only
|
||||||
|
|
||||||
Files: .github/ISSUE_TEMPLATE/*
|
Files: .github/ISSUE_TEMPLATE/*
|
||||||
Copyright: 2024 John Livingston <https://www.john-livingston.fr/>
|
Copyright: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
License: AGPL-3.0-only
|
License: AGPL-3.0-only
|
||||||
|
|
||||||
Files: .github/PULL_REQUEST_TEMPLATE.md
|
Files: .github/PULL_REQUEST_TEMPLATE.md
|
||||||
Copyright: 2024 John Livingston <https://www.john-livingston.fr/>
|
Copyright: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
License: AGPL-3.0-only
|
License: AGPL-3.0-only
|
||||||
|
|
||||||
|
Files: prosody-modules/mod_firewall/*
|
||||||
|
Copyright: Prosody Community Modules <https://modules.prosody.im/mod_firewall>
|
||||||
|
License: MIT
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
'use strict';
|
'use strict'
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [
|
extends: [
|
||||||
@ -14,7 +14,7 @@ module.exports = {
|
|||||||
// extending the kebab-case to accept ConverseJS class names.
|
// extending the kebab-case to accept ConverseJS class names.
|
||||||
'^([a-z][a-z0-9]*)(-[a-z0-9]+)*((__|--)[a-z]+(-[a-z0-9]+)*)?$',
|
'^([a-z][a-z0-9]*)(-[a-z0-9]+)*((__|--)[a-z]+(-[a-z0-9]+)*)?$',
|
||||||
{
|
{
|
||||||
message: 'Expected class selector to be kebab-case, or ConverseJS-style.',
|
message: 'Expected class selector to be kebab-case, or ConverseJS-style.'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
212
CHANGELOG.md
212
CHANGELOG.md
@ -1,5 +1,211 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 13.0.0
|
||||||
|
|
||||||
|
**Important note**: if you got an error on updating the plugin, please try to restart Peertube and install it again.
|
||||||
|
|
||||||
|
### Security Fix
|
||||||
|
|
||||||
|
Severity: low.
|
||||||
|
|
||||||
|
[Radically Open Security](radicallyopensecurity.com) reported a security vulnerability: a malicious user can forge a malicious Regular Expression to cause a [ReDOS](https://en.wikipedia.org/wiki/ReDoS) on the Chat Bot.
|
||||||
|
Such attack would only make the bot unresponsive, and won't affect the Peertube server or the XMPP server.
|
||||||
|
|
||||||
|
This version mitigates the attack by using the [RE2](https://github.com/google/re2) regular expression library.
|
||||||
|
|
||||||
|
Thanks [NlNet](https://nlnet.nl/) for funding the security audit.
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
#### Bot timers
|
||||||
|
|
||||||
|
There was a regression some months ago in the "bot timer" functionnality.
|
||||||
|
In the channels settings, the delay between two quotes is supposed to be in minutes, but in fact we applied seconds.
|
||||||
|
We don't have any way to detect if the user meant seconds or minutes when they configured their channels (it depends if it was before or after the regression).
|
||||||
|
So we encourage all streamers to go through their channel settings, check the frequency of their bot timers (if enabled), set them to the correct value, and save the form.
|
||||||
|
Users must save the form to be sure to apply the correct value.
|
||||||
|
|
||||||
|
#### Bot forbidden words
|
||||||
|
|
||||||
|
When using regular expressions for the forbidden words, the chat bot now uses the [RE2](https://github.com/google/re2) regular expression library.
|
||||||
|
This library does not support all character classes, and all regular expressions that were previously possible (with the Javascript RegExp class).
|
||||||
|
|
||||||
|
For more information about the accepted regular expression, please refer to the [documentation](https://livingston.frama.io/peertube-plugin-livechat/documentation/user/streamers/bot/forbidden_words/#consider-as-regular-expressions).
|
||||||
|
|
||||||
|
If you configured non-compatible regular expressions, the bot will just ignore them, and log an error.
|
||||||
|
When saving channel's preference, if non-compatible regular expression is used, an error will be shown.
|
||||||
|
|
||||||
|
### Minor changes and fixes
|
||||||
|
|
||||||
|
* Translations updates.
|
||||||
|
* Dependencies updates.
|
||||||
|
* Fix #329: auto focus message field after anonymous user has entered nickname (Thanks [Axolotle](https://github.com/axolotle).
|
||||||
|
* Fix #392: add draggable items touch screen handling (Thanks [Axolotle](https://github.com/axolotle).
|
||||||
|
* Fix #506: hide offline users by default in occupant list (Thanks [Axolotle](https://github.com/axolotle).
|
||||||
|
* Fix #547: add button to go to the end of the chat (Thanks [Axolotle](https://github.com/axolotle).
|
||||||
|
* Fix #503: set custom emojis max height to text height + bigger when posted alone (Thanks [Axolotle](https://github.com/axolotle).
|
||||||
|
* Fix: Converse bottom panel messages not visible on new Peertube v7 theme (for example for muted users).
|
||||||
|
* Fix #75: New short video urls makes it difficult to use the settings «Activate chat for these videos».
|
||||||
|
* Fix moderation notes: fix filter button wrongly displayed on notes without associated occupant.
|
||||||
|
* Fix tasks: checkbox state does not change when clicked.
|
||||||
|
* Fix: bot timer can't be negative or null.
|
||||||
|
* Fix #626: Bot timer was buggy, using seconds as delay instead of minutes.
|
||||||
|
* Fix: message deletions were not properly anonymized when using "Anonymize moderation actions" option.
|
||||||
|
|
||||||
|
## 12.0.4
|
||||||
|
|
||||||
|
### Minor changes and fixes
|
||||||
|
|
||||||
|
* Fix #660: don't send headers twice on emoji router errors.
|
||||||
|
* Fix shebangs (for NixOS compatibility).
|
||||||
|
* Translations updates.
|
||||||
|
* Updating various dependencies.
|
||||||
|
* Adding a warning in settings if theme is not set to Peertube or if autocolors are disabled.
|
||||||
|
|
||||||
|
## 12.0.3
|
||||||
|
|
||||||
|
### Minor changes and fixes
|
||||||
|
|
||||||
|
* Translations updates.
|
||||||
|
* Slovak translation integration.
|
||||||
|
* Differenciate pt-PT and pt-BR translations.
|
||||||
|
* Fix styling for "configure mod_firewall" button + Peertube v7.0.0 compatibility.
|
||||||
|
* Fix #648: workaround for a regression in Firefox that breaks the scrollbar (Thanks [Raph](https://github.com/raphgilles) for the workaround!).
|
||||||
|
|
||||||
|
## 12.0.2
|
||||||
|
|
||||||
|
### Minor changes and fixes
|
||||||
|
|
||||||
|
* Fix task list label styling.
|
||||||
|
* Translations updates.
|
||||||
|
|
||||||
|
## 12.0.1
|
||||||
|
|
||||||
|
### Minor changes and fixes
|
||||||
|
|
||||||
|
* Fix custom emojis vs upper/lower case.
|
||||||
|
|
||||||
|
## 12.0.0
|
||||||
|
|
||||||
|
### Importante Notes
|
||||||
|
|
||||||
|
This version requires Peertube 5.2.0 or superior.
|
||||||
|
It also requires NodeJS 16 or superior (same as Peertube 5.2.0.).
|
||||||
|
|
||||||
|
If you use the "system Prosody", you should update to Prosody 0.12.4, and Lua 5.4.
|
||||||
|
|
||||||
|
### New features
|
||||||
|
|
||||||
|
* #131: Emoji only mode.
|
||||||
|
* #516: new option for the moderation bot: forbid duplicate messages.
|
||||||
|
* #517: new option for the moderation bot: forbid messages with too many special characters.
|
||||||
|
* #518: moderators can send announcements and highlighted messages.
|
||||||
|
* #610: compatibility with PeerTube v7
|
||||||
|
|
||||||
|
### Minor changes and fixes
|
||||||
|
|
||||||
|
* Updating ConverseJS (v11 WIP) with latest fixes.
|
||||||
|
* Updating Prosody AppImage to Prosody 0.12.4 + Lua 5.4.
|
||||||
|
* Various translation updates.
|
||||||
|
* Using Typescript 5.5.4, and Eslint 8.57.0 (with new ruleset).
|
||||||
|
* Fix race condition in bot/ctl.
|
||||||
|
* Various type improvements.
|
||||||
|
* Update dependencies.
|
||||||
|
* Fix emoji picker colors and size.
|
||||||
|
* Fix: moderation delay max value was not correctly handled.
|
||||||
|
|
||||||
|
## 11.0.1
|
||||||
|
|
||||||
|
### Minor changes and fixes
|
||||||
|
|
||||||
|
* Fix "send message" button that was sending the message twice.
|
||||||
|
|
||||||
|
## 11.0.0
|
||||||
|
|
||||||
|
### Importante Notes
|
||||||
|
|
||||||
|
With the new [mod_firewall](https://livingston.frama.io/peertube-plugin-livechat/documentation/admin/mod_firewall/) feature, Peertube admins can write firewall rules for the Prosody server. These rules could be used to run arbitrary code on the server. If you are a hosting provider, and you don't want to allow Peertube admins to write such rules, you can disable the online editing by creating a `disable_mod_firewall_editing` file in the plugin directory. Check the documentation for more information. This is opt-out, as Peertube admins can already run arbitrary code just by installing any plugin.
|
||||||
|
|
||||||
|
The concord theme was removed from ConverseJS. If you had it set in the plugin settings, it will fallback to the Peertube theme.
|
||||||
|
|
||||||
|
### New features
|
||||||
|
|
||||||
|
* Updating ConverseJS, to use upstream (v11 WIP). This comes with many improvements and new features.
|
||||||
|
* #146: copy message button for moderators.
|
||||||
|
* #137: option to hide moderator name who made actions (kick, ban, message moderation, ...).
|
||||||
|
* #144: [moderator notes](https://livingston.frama.io/peertube-plugin-livechat/documentation/user/streamers/moderation_notes/).
|
||||||
|
* #145: action for moderators to find all messages from a given participant.
|
||||||
|
* #97: option to use and configure [mod_firewall](https://livingston.frama.io/peertube-plugin-livechat/documentation/admin/mod_firewall/) at the server level.
|
||||||
|
|
||||||
|
### Minor changes and fixes
|
||||||
|
|
||||||
|
* #118: improved accessibility.
|
||||||
|
* Avatar set for anonymous users: new 'none' choice (that will fallback to Converse new colorized avatars).
|
||||||
|
* New translation: Albanian.
|
||||||
|
* Translation updates: Crotian, Japanese, traditional Chinese, Arabic, Galician.
|
||||||
|
* Updated mod_muc_moderation to upstream.
|
||||||
|
* Fix new task ordering.
|
||||||
|
* Fix: clicking on the current user nickname in message history was failing to open the profile modal.
|
||||||
|
* Fix: increase chat height on small screens, try to better detect the device viewport size and orientation.
|
||||||
|
* Converse theme: removed concord, added cyberpunk.
|
||||||
|
* Fixed Converse theme settings localization.
|
||||||
|
* Fix: improved minimum chat width.
|
||||||
|
|
||||||
|
## 10.3.3
|
||||||
|
|
||||||
|
### Minor changes and fixes
|
||||||
|
|
||||||
|
* Fix #481: Moderation bot was not able to connect when remote chat was disabled.
|
||||||
|
* Some cleaning in code generating Prosody configuration file.
|
||||||
|
|
||||||
|
## 10.3.2
|
||||||
|
|
||||||
|
### Minor changes and fixes
|
||||||
|
|
||||||
|
* Fix #477: ended polls never disappear when archiving is disabled (and no more than 20 new messages).
|
||||||
|
|
||||||
|
## 10.3.1
|
||||||
|
|
||||||
|
### Minor changes and fixes
|
||||||
|
|
||||||
|
* Moderation delay: fix accessibility on the timer shown to moderators.
|
||||||
|
* Fix «create new poll» icon.
|
||||||
|
|
||||||
|
## 10.3.0
|
||||||
|
|
||||||
|
### New features
|
||||||
|
|
||||||
|
* #132: [moderation delay](https://livingston.frama.io/peertube-plugin-livechat/documentation/user/streamers/moderation_delay/).
|
||||||
|
|
||||||
|
### Minor changes and fixes
|
||||||
|
|
||||||
|
* Translations updates: german.
|
||||||
|
* Performance: don't send markers, even if requested by the sender.
|
||||||
|
|
||||||
|
## 10.2.0
|
||||||
|
|
||||||
|
### New features
|
||||||
|
|
||||||
|
* #231: [polls](https://livingston.frama.io/peertube-plugin-livechat/documentation/user/streamers/polls/).
|
||||||
|
* #233: new option to [mute anonymous users](https://livingston.frama.io/peertube-plugin-livechat/documentation/user/streamers/moderation/).
|
||||||
|
* #18: terms & conditions. You can configure terms&conditions on your instance that will be shown to each joining users. Streamers can also add [terms&conditions in their channels options](https://livingston.frama.io/peertube-plugin-livechat/documentation/user/streamers/terms/).
|
||||||
|
|
||||||
|
### Minor changes and fixes
|
||||||
|
|
||||||
|
* Fix #449: Remove the constraint for custom emojis shortnames to have ":" at the beginning and at the end.
|
||||||
|
* Translations updates: french, german, crotian, polish, slovak.
|
||||||
|
|
||||||
|
## 10.1.2
|
||||||
|
|
||||||
|
* Fix: clicking on the import custom emojis button, without selected any file, was resulting in a state with all action button disabled.
|
||||||
|
|
||||||
|
## 10.1.1
|
||||||
|
|
||||||
|
* Fix #436: Saving emojis per batch, to avoid hitting max payload limit.
|
||||||
|
* Fix: the emojis import function could add more entries than max allowed emoji count.
|
||||||
|
* Fix #437: removing last line if empty when importing emojis.
|
||||||
|
* Updated translations: de, sk.
|
||||||
|
|
||||||
## 10.1.0
|
## 10.1.0
|
||||||
|
|
||||||
### New features
|
### New features
|
||||||
@ -34,7 +240,7 @@
|
|||||||
|
|
||||||
### New features
|
### New features
|
||||||
|
|
||||||
* #177: streamer's task/to-do lists: streamers, and their room's moderators, can handle task lists directly. This can be used to handle viewers questions, moderation actions, ... More info in the [tasks documentation](https://livingston.frama.io/peertube-plugin-livechat/fr/documentation/user/streamers/tasks/).
|
* #177: streamer's task/to-do lists: streamers, and their room's moderators, can handle task lists directly. This can be used to handle viewers questions, moderation actions, ... More info in the [tasks documentation](https://livingston.frama.io/peertube-plugin-livechat/documentation/user/streamers/tasks/).
|
||||||
* #385: new way of managing chat access rights. Now streamers are owner of their chat rooms. Peertube admins/moderators are not by default, so that their identities are not leaking. But they have a button to promote as chat room owner, if they need to take action. Please note that there is a migration script that will remove all Peertube admins/moderators affiliations (unless they are video/channel's owner). They can get this access back using the button.
|
* #385: new way of managing chat access rights. Now streamers are owner of their chat rooms. Peertube admins/moderators are not by default, so that their identities are not leaking. But they have a button to promote as chat room owner, if they need to take action. Please note that there is a migration script that will remove all Peertube admins/moderators affiliations (unless they are video/channel's owner). They can get this access back using the button.
|
||||||
* #385: the slow mode duration on the channel option page is now a default value for new rooms. Streamers can change the value room per room in the room's configuration.
|
* #385: the slow mode duration on the channel option page is now a default value for new rooms. Streamers can change the value room per room in the room's configuration.
|
||||||
|
|
||||||
@ -725,7 +931,7 @@ Moreover, they don't seem to be used much.
|
|||||||
### Features
|
### Features
|
||||||
|
|
||||||
* Builtin prosody use a working dir provided by Peertube (needs Peertube >= 3.2.0)
|
* Builtin prosody use a working dir provided by Peertube (needs Peertube >= 3.2.0)
|
||||||
* Starting with Peertube 3.2.0, builtin prosody save room history on server. So when a user connects, he can get previously send messages.
|
* Starting with Peertube 3.2.0, builtin prosody save room history on server. So when a user connects, they can get previously send messages.
|
||||||
* Starting with Peertube 3.2.0, builtin prosody also activate mod_muc_moderation, enabling moderators to moderate messages.
|
* Starting with Peertube 3.2.0, builtin prosody also activate mod_muc_moderation, enabling moderators to moderate messages.
|
||||||
* Prosody log level will be the same as the Peertube's one.
|
* Prosody log level will be the same as the Peertube's one.
|
||||||
* Prosody log rotation every 24 hour.
|
* Prosody log rotation every 24 hour.
|
||||||
@ -762,7 +968,7 @@ Moreover, they don't seem to be used much.
|
|||||||
## v2.1.3
|
## v2.1.3
|
||||||
|
|
||||||
* Fix: 2.1.0 was in fact correct... Did not work on my preprod env because of... a Livebox bug...
|
* Fix: 2.1.0 was in fact correct... Did not work on my preprod env because of... a Livebox bug...
|
||||||
* Fix: if the video owner is already owner of the chatroom, he should not be downgraded to admin.
|
* Fix: if the video owner is already owner of the chatroom, they should not be downgraded to admin.
|
||||||
|
|
||||||
## v2.1.2
|
## v2.1.2
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!--
|
<!--
|
||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!--
|
<!--
|
||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!--
|
<!--
|
||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!--
|
<!--
|
||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!--
|
<!--
|
||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
BIN
assets/images/avatars/nctv/hat_01.png
Normal file
BIN
assets/images/avatars/nctv/hat_01.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 350 B |
BIN
assets/images/avatars/nctv/nigbot.png
Normal file
BIN
assets/images/avatars/nctv/nigbot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 61 KiB |
@ -1,3 +1,3 @@
|
|||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
98
assets/styles/admin/firewall/_firewall.scss
Normal file
98
assets/styles/admin/firewall/_firewall.scss
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* stylelint-disable custom-property-pattern */
|
||||||
|
|
||||||
|
@use "sass:color";
|
||||||
|
@use "../../variables";
|
||||||
|
|
||||||
|
.peertube-plugin-livechat-admin-firewall {
|
||||||
|
h1 {
|
||||||
|
padding-top: 40px;
|
||||||
|
|
||||||
|
/* See Peertube sub-menu-h1 mixin */
|
||||||
|
font-size: 1.3rem;
|
||||||
|
border-bottom: 2px solid var(--bg-secondary-400, var(--greyBackgroundColor));
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea[name^="_content_"] {
|
||||||
|
min-height: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"],
|
||||||
|
input[type="reset"],
|
||||||
|
button[type="submit"],
|
||||||
|
button[type="reset"] {
|
||||||
|
// Peertube rounded-line-height-1-5 mixins
|
||||||
|
line-height: variables.$button-calc-line-height;
|
||||||
|
|
||||||
|
// Peertube peertube-button mixin
|
||||||
|
padding: 4px 13px;
|
||||||
|
border: 0;
|
||||||
|
font-weight: variables.$font-semibold;
|
||||||
|
border-radius: 3px !important;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: variables.$button-font-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"],
|
||||||
|
button[type="submit"] {
|
||||||
|
&,
|
||||||
|
&:active,
|
||||||
|
&.active,
|
||||||
|
&:focus {
|
||||||
|
color: var(--on-primary, #fff);
|
||||||
|
background-color: var(--primary, var(--mainColor));
|
||||||
|
border: 1px solid var(--primary, var(--mainColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--on-primary, #fff);
|
||||||
|
background-color: var(--primary-400, var(--mainHoverColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="reset"],
|
||||||
|
button[type="reset"] {
|
||||||
|
color: var(--fg, var(--mainForegroundColor));
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid var(--bg-secondary-500, var(--inputBorderColor)) !important;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&.active,
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
color: var(--fg, var(--mainForegroundColor));
|
||||||
|
background-color: var(--bg-secondary-500, var(--inputBorderColor));
|
||||||
|
border-color: var(--bg-secondary-500, var(--inputBorderColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--fg, var(--mainForegroundColor));
|
||||||
|
background-color: var(--bg-secondary-450, var(--inputBorderColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.peertube-livechat-admin-firewall-col-name {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peertube-livechat-admin-firewall-col-content {
|
||||||
|
width: 65%;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
@ -21,7 +21,7 @@ $small-view: 800px;
|
|||||||
|
|
||||||
/* See Peertube sub-menu-h1 mixin */
|
/* See Peertube sub-menu-h1 mixin */
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
border-bottom: 2px solid var(--greyBackgroundColor);
|
border-bottom: 2px solid var(--bg-secondary-400, var(--greyBackgroundColor));
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ $small-view: 800px;
|
|||||||
&.peertube-plugin-livechat-configuration-channel {
|
&.peertube-plugin-livechat-configuration-channel {
|
||||||
.peertube-plugin-livechat-configuration-channel-info {
|
.peertube-plugin-livechat-configuration-channel-info {
|
||||||
/* stylelint-disable-next-line value-keyword-case */
|
/* stylelint-disable-next-line value-keyword-case */
|
||||||
color: var(--mainForegroundColor);
|
color: var(--fg, var(--mainForegroundColor));
|
||||||
|
|
||||||
span:first-child {
|
span:first-child {
|
||||||
/* See Peertube .video-channel-display-name */
|
/* See Peertube .video-channel-display-name */
|
||||||
@ -48,7 +48,7 @@ $small-view: 800px;
|
|||||||
h2 {
|
h2 {
|
||||||
// See Peertube settings-big-title mixin
|
// See Peertube settings-big-title mixin
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--mainColor);
|
color: var(--primary, var(--mainColor));
|
||||||
font-weight: variables.$font-bold;
|
font-weight: variables.$font-bold;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@ -82,35 +82,35 @@ $small-view: 800px;
|
|||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--mainColor);
|
background-color: var(--primary, var(--mainColor));
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--mainHoverColor);
|
background-color: var(--fg-400, var(--mainHoverColor));
|
||||||
}
|
}
|
||||||
|
|
||||||
&[disabled],
|
&[disabled],
|
||||||
&.disabled {
|
&.disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--inputBorderColor);
|
background-color: var(--input-border-color, var(--inputBorderColor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="reset"],
|
input[type="reset"],
|
||||||
button[type="reset"] {
|
button[type="reset"] {
|
||||||
// Peertube grey-button mixin
|
// Peertube grey-button mixin
|
||||||
background-color: var(--greyBackgroundColor);
|
background-color: var(--bg-secondary-400, var(--greyBackgroundColor));
|
||||||
color: var(--greyForegroundColor);
|
color: var(--fg-400, var(--greyForegroundColor));
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&[disabled],
|
&[disabled],
|
||||||
&.disabled {
|
&.disabled {
|
||||||
color: var(--greyForegroundColor);
|
color: var(--fg-400, var(--greyForegroundColor));
|
||||||
background-color: var(--greySecondaryBackgroundColor);
|
background-color: var(--bg-secondary-300, var(--greySecondaryBackgroundColor));
|
||||||
}
|
}
|
||||||
|
|
||||||
&[disabled],
|
&[disabled],
|
||||||
@ -174,6 +174,12 @@ $small-view: 800px;
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
/* See Peertube .video-channel-names */
|
/* See Peertube .video-channel-names */
|
||||||
|
width: fit-content;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
/* stylelint-disable-next-line value-keyword-case */
|
||||||
|
color: var(--fg, var(--mainForegroundColor));
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:active {
|
&:active {
|
||||||
@ -184,12 +190,6 @@ $small-view: 800px;
|
|||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
width: fit-content;
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
/* stylelint-disable-next-line value-keyword-case */
|
|
||||||
color: var(--mainForegroundColor);
|
|
||||||
|
|
||||||
div:first-child {
|
div:first-child {
|
||||||
/* See Peertube .video-channel-display-name */
|
/* See Peertube .video-channel-display-name */
|
||||||
font-weight: variables.$font-semibold;
|
font-weight: variables.$font-semibold;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
* SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
* SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
@ -56,7 +56,7 @@ livechat-share-chat {
|
|||||||
&.livechat-shareurl-suboptions-disabled {
|
&.livechat-shareurl-suboptions-disabled {
|
||||||
label {
|
label {
|
||||||
/* stylelint-disable-next-line custom-property-pattern */
|
/* stylelint-disable-next-line custom-property-pattern */
|
||||||
color: var(--greyForegroundColor);
|
color: var(--fg-400, var(--greyForegroundColor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
@ -15,9 +15,9 @@ livechat-spinner,
|
|||||||
height: 48px;
|
height: 48px;
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
/* stylelint-disable-next-line custom-property-pattern */
|
/* stylelint-disable-next-line custom-property-pattern */
|
||||||
border: 5px solid var(--greyBackgroundColor);
|
border: 5px solid var(--bg-secondary-400, var(--greyBackgroundColor)) !important; // !important is required for it to work in ConverseJS
|
||||||
/* stylelint-disable-next-line custom-property-pattern */
|
/* stylelint-disable-next-line custom-property-pattern */
|
||||||
border-bottom-color: var(--mainColor);
|
border-bottom-color: var(--primary, var(--mainColor)) !important; // !important is required for it to work in ConverseJS
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
* SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
@ -51,14 +51,14 @@ livechat-tags-input {
|
|||||||
transition-duration: 0.3s;
|
transition-duration: 0.3s;
|
||||||
|
|
||||||
@supports (scrollbar-width: auto) {
|
@supports (scrollbar-width: auto) {
|
||||||
scrollbar-color: var(--greyForegroundColor) transparent;
|
scrollbar-color: var(--fg-400, var(--greyForegroundColor)) transparent;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.livechat-tags-container,
|
.livechat-tags-container,
|
||||||
.livechat-tags-searched {
|
.livechat-tags-searched {
|
||||||
border-bottom: 1px dashed var(--greyForegroundColor);
|
border-bottom: 1px dashed var(--fg-400, var(--greyForegroundColor));
|
||||||
|
|
||||||
&.livechat-empty {
|
&.livechat-empty {
|
||||||
height: 0;
|
height: 0;
|
||||||
@ -104,7 +104,7 @@ livechat-tags-input {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
margin-left: var(--tag-padding-horizontal);
|
margin-left: var(--tag-padding-horizontal);
|
||||||
color: var(--mainColor);
|
color: var(--primary, var(--mainColor));
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -118,19 +118,19 @@ livechat-tags-input {
|
|||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--mainColor);
|
background-color: var(--primary, var(--mainColor));
|
||||||
|
|
||||||
.livechat-tag-close {
|
.livechat-tag-close {
|
||||||
color: var(--mainColor);
|
color: var(--primary, var(--mainColor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--mainHoverColor);
|
background-color: var(--fg-400, var(--mainHoverColor));
|
||||||
|
|
||||||
.livechat-tag-close {
|
.livechat-tag-close {
|
||||||
color: var(--mainHoverColor);
|
color: var(--fg-400, var(--mainHoverColor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,10 +138,10 @@ livechat-tags-input {
|
|||||||
&.disabled {
|
&.disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--inputBorderColor);
|
background-color: var(--input-border-color, var(--inputBorderColor));
|
||||||
|
|
||||||
.livechat-tag-close {
|
.livechat-tag-close {
|
||||||
color: var(--inputBorderColor);
|
color: var(--input-border-color, var(--inputBorderColor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
@ -9,10 +9,10 @@
|
|||||||
|
|
||||||
livechat-token-list {
|
livechat-token-list {
|
||||||
table {
|
table {
|
||||||
@include tables.data-table;
|
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@include tables.data-table;
|
||||||
|
|
||||||
tr th:first-child,
|
tr th:first-child,
|
||||||
tr th:last-child {
|
tr th:last-child {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
@ -19,7 +19,7 @@ table.peertube-plugin-livechat-prosody-list-rooms tr:nth-child(even) {
|
|||||||
|
|
||||||
table.peertube-plugin-livechat-prosody-list-rooms th {
|
table.peertube-plugin-livechat-prosody-list-rooms th {
|
||||||
/* stylelint-disable-next-line custom-property-pattern */
|
/* stylelint-disable-next-line custom-property-pattern */
|
||||||
background-color: var(--mainHoverColor);
|
background-color: var(--fg-400, var(--mainHoverColor));
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
/* stylelint-disable-next-line custom-property-pattern */
|
/* stylelint-disable-next-line custom-property-pattern */
|
||||||
color: var(--mainBackgroundColor);
|
color: var(--mainBackgroundColor);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
* SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
@ -54,7 +54,7 @@ $bs-green: #39cc0b;
|
|||||||
&.disabled {
|
&.disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--inputBorderColor);
|
background-color: var(--input-border-color, var(--inputBorderColor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ $bs-green: #39cc0b;
|
|||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--mainColor);
|
background-color: var(--primary, var(--mainColor));
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
@ -77,13 +77,13 @@ $bs-green: #39cc0b;
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--mainHoverColor);
|
background-color: var(--fg-400, var(--mainHoverColor));
|
||||||
}
|
}
|
||||||
|
|
||||||
&[disabled],
|
&[disabled],
|
||||||
&.disabled {
|
&.disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--inputBorderColor);
|
background-color: var(--input-border-color, var(--inputBorderColor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
* SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
@ -13,7 +13,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
border: 1px var(--greyBackgroundColor) solid;
|
border: 1px var(--bg-secondary-400, var(--greyBackgroundColor)) solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
td,
|
td,
|
||||||
@ -34,6 +34,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
tbody tr:nth-child(odd) {
|
tbody tr:nth-child(odd) {
|
||||||
background-color: var(--greySecondaryBackgroundColor);
|
background-color: var(--bg-secondary-300, var(--greySecondaryBackgroundColor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
@ -9,4 +9,5 @@
|
|||||||
@use "elements/index";
|
@use "elements/index";
|
||||||
@use "video";
|
@use "video";
|
||||||
@use "configuration/configuration";
|
@use "configuration/configuration";
|
||||||
@use "list-rooms/list-rooms.scss";
|
@use "admin/firewall/firewall";
|
||||||
|
@use "list-rooms/list-rooms";
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
@ -18,53 +18,81 @@
|
|||||||
/* Note: livechat-viewer-mode-content (the form where anonymous users can
|
/* Note: livechat-viewer-mode-content (the form where anonymous users can
|
||||||
choose nickname or log in with external account), can be something like
|
choose nickname or log in with external account), can be something like
|
||||||
~180px height (at time of writing).
|
~180px height (at time of writing).
|
||||||
We must ensure that the 200px limit for converse-muc and converse-root is
|
We must ensure that the px height limit for converse-muc and converse-root is
|
||||||
always higher than livechat-viewer-mode-content max size.
|
always higher than livechat-viewer-mode-content max size.
|
||||||
|
Note: We also must ensure that when the user has choosen its nickname, and there is an
|
||||||
|
ongoing poll, the user can see the chat when the poll is folded.
|
||||||
*/
|
*/
|
||||||
#peertube-plugin-livechat-container converse-root {
|
#peertube-plugin-livechat-container converse-root {
|
||||||
display: block;
|
display: block;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
min-height: max(30vh, 200px); // Always at least 200px, and ideally at least 30% of viewport.
|
min-height: max(30vh, 300px); // Always at least 200px, and ideally at least 30% of viewport.
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-width: min(400px, 25vw);
|
||||||
|
|
||||||
converse-muc {
|
converse-muc {
|
||||||
min-height: max(30vh, 200px);
|
min-height: max(30vh, 300px);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Media query for mobile devices */
|
@media screen and (orientation: portrait) and (width <= 767px) {
|
||||||
@media only screen and (max-width: 50vw) {
|
/* On small screen, and when portrait mode, we are giving the chat more vertical space.
|
||||||
#peertube-plugin-livechat-container converse-root {
|
It should go under the video.
|
||||||
|
*/
|
||||||
|
min-height: max(58vh, 300px);
|
||||||
|
|
||||||
converse-muc {
|
converse-muc {
|
||||||
min-height: 62vh;
|
min-height: max(58vh, 300px);
|
||||||
/* 100vh - 30vh for video = 70vh remaining */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Media query for tablets in portrait mode */
|
// /* Media query for mobile devices */
|
||||||
@media only screen and (min-width: 50vw) and (max-width: 75vw) {
|
// @media only screen and (max-width: 767px) {
|
||||||
|
// #peertube-plugin-livechat-container converse-root {
|
||||||
|
// converse-muc {
|
||||||
|
// min-height: 58vh;
|
||||||
|
// /* 100vh - 30vh for video = 70vh remaining */
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /* Media query for tablets in portrait mode */
|
||||||
|
// @media only screen and (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
// #peertube-plugin-livechat-container converse-root {
|
||||||
|
// converse-muc {
|
||||||
|
// min-height: 25vh;
|
||||||
|
// /* Slightly less to account for other elements */
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /* Media query for tablets in landscape mode */
|
||||||
|
// @media only screen and (min-width: 1024px) and (max-width: 1279px) {
|
||||||
|
// #peertube-plugin-livechat-container converse-root {
|
||||||
|
// converse-muc {
|
||||||
|
// min-height: 25vh;
|
||||||
|
// /* Assuming more height can be used */
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
/* Media query for desktops */
|
||||||
|
@media only screen and (min-width: 1280px) {
|
||||||
#peertube-plugin-livechat-container converse-root {
|
#peertube-plugin-livechat-container converse-root {
|
||||||
converse-muc {
|
converse-muc {
|
||||||
min-height: 62vh;
|
height: inherit;
|
||||||
/* Slightly less to account for other elements */
|
/* Full desktop experience */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Media query for tablets in landscape mode */
|
|
||||||
@media only screen and (min-width: 76vw) and (max-width: 100vw) {
|
|
||||||
#peertube-plugin-livechat-container converse-root {
|
|
||||||
converse-muc {
|
|
||||||
min-height: 62vh;
|
|
||||||
/* Assuming more height can be used */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* custom toolbar CSS */
|
/* custom toolbar CSS */
|
||||||
|
|
||||||
.send-button {
|
.send-button {
|
||||||
border-radius: 0.25rem !important;
|
border-radius: 0.25rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.send-button:hover {
|
||||||
|
background-color: #0067c1 !important;
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#!/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
// SPDX-FileCopyrightText: 2025 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@ -58,6 +59,9 @@ const avatarPartsDef = {
|
|||||||
fur: 10,
|
fur: 10,
|
||||||
eyes: 15,
|
eyes: 15,
|
||||||
mouth: 10
|
mouth: 10
|
||||||
|
},
|
||||||
|
'nctv': {
|
||||||
|
body: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,9 +173,26 @@ async function generateAvatars (part) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const generateNigbotAvatar = async () => {
|
||||||
|
console.log('Starting generating nigbot avatar');
|
||||||
|
|
||||||
|
const inputDir = './assets/images/avatars/nctv/';
|
||||||
|
const outputDir = './dist/server/avatars/nctv/';
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
const buff = await sharp(path.join(inputDir, 'nigbot.png')).toBuffer();
|
||||||
|
await sharp(buff)
|
||||||
|
.resize(60, 60)
|
||||||
|
.png({ palette: true })
|
||||||
|
.toFile(path.join(outputDir, '1.png'));
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function generateBotsAvatars () {
|
async function generateBotsAvatars () {
|
||||||
{
|
{
|
||||||
// Moderation bot avatar: choosing some parts, and turning it so he is facing left.
|
// Moderation bot avatar: choosing some parts, and turning it so it is facing left.
|
||||||
const inputDir = path.join('./assets/images/avatars/', 'sepia')
|
const inputDir = path.join('./assets/images/avatars/', 'sepia')
|
||||||
const botOutputDir = './dist/server/bot_avatars/sepia/'
|
const botOutputDir = './dist/server/bot_avatars/sepia/'
|
||||||
fs.mkdirSync(botOutputDir, { recursive: true })
|
fs.mkdirSync(botOutputDir, { recursive: true })
|
||||||
@ -196,7 +217,7 @@ async function generateBotsAvatars () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Moderation bot avatar: choosing some parts, and turning it so he is facing left.
|
// Moderation bot avatar: choosing some parts, and turning it so it is facing left.
|
||||||
const inputDir = path.join('./assets/images/avatars/', 'cat')
|
const inputDir = path.join('./assets/images/avatars/', 'cat')
|
||||||
const botOutputDir = './dist/server/bot_avatars/cat/'
|
const botOutputDir = './dist/server/bot_avatars/cat/'
|
||||||
fs.mkdirSync(botOutputDir, { recursive: true })
|
fs.mkdirSync(botOutputDir, { recursive: true })
|
||||||
@ -220,7 +241,7 @@ async function generateBotsAvatars () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Moderation bot avatar: choosing some parts, and turning it so he is facing left.
|
// Moderation bot avatar: choosing some parts, and turning it so it is facing left.
|
||||||
const inputDir = path.join('./assets/images/avatars/', 'bird')
|
const inputDir = path.join('./assets/images/avatars/', 'bird')
|
||||||
const botOutputDir = './dist/server/bot_avatars/bird/'
|
const botOutputDir = './dist/server/bot_avatars/bird/'
|
||||||
fs.mkdirSync(botOutputDir, { recursive: true })
|
fs.mkdirSync(botOutputDir, { recursive: true })
|
||||||
@ -246,7 +267,7 @@ async function generateBotsAvatars () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Moderation bot avatar: choosing some parts, and turning it so he is facing left.
|
// Moderation bot avatar: choosing some parts, and turning it so it is facing left.
|
||||||
const inputDir = './assets/images/avatars/fenec'
|
const inputDir = './assets/images/avatars/fenec'
|
||||||
const botOutputDir = './dist/server/bot_avatars/fenec/'
|
const botOutputDir = './dist/server/bot_avatars/fenec/'
|
||||||
fs.mkdirSync(botOutputDir, { recursive: true })
|
fs.mkdirSync(botOutputDir, { recursive: true })
|
||||||
@ -273,7 +294,7 @@ async function generateBotsAvatars () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Moderation bot avatar: choosing some parts, and turning it so he is facing left.
|
// Moderation bot avatar: choosing some parts, and turning it so it is facing left.
|
||||||
const inputDir = './assets/images/avatars/abstract'
|
const inputDir = './assets/images/avatars/abstract'
|
||||||
const botOutputDir = './dist/server/bot_avatars/abstract/'
|
const botOutputDir = './dist/server/bot_avatars/abstract/'
|
||||||
fs.mkdirSync(botOutputDir, { recursive: true })
|
fs.mkdirSync(botOutputDir, { recursive: true })
|
||||||
@ -294,6 +315,21 @@ async function generateBotsAvatars () {
|
|||||||
})
|
})
|
||||||
.toFile(path.join(botOutputDir, '1.png'))
|
.toFile(path.join(botOutputDir, '1.png'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Nigbot avatar for users
|
||||||
|
const inputDir = './assets/images/avatars/nctv'
|
||||||
|
const botOutputDir = './dist/server/bot_avatars/nctv/'
|
||||||
|
fs.mkdirSync(botOutputDir, { recursive: true })
|
||||||
|
const buff = await sharp(path.join(inputDir, 'nigbot.png'))
|
||||||
|
.toBuffer()
|
||||||
|
|
||||||
|
await sharp(buff)
|
||||||
|
// .resize(60, 60)
|
||||||
|
.png()
|
||||||
|
.toFile(path.join(botOutputDir, '1.png'))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMainThread) {
|
if (isMainThread) {
|
||||||
@ -337,6 +373,9 @@ if (isMainThread) {
|
|||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
} else if (part === 'nctv') {
|
||||||
|
generateNigbotAvatar();
|
||||||
|
parentPort.postMessage('done');
|
||||||
} else {
|
} else {
|
||||||
generateAvatars(part).then(
|
generateAvatars(part).then(
|
||||||
() => {
|
() => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ const clientFiles = [
|
|||||||
'admin-plugin-client-plugin'
|
'admin-plugin-client-plugin'
|
||||||
]
|
]
|
||||||
|
|
||||||
function loadLocs() {
|
function loadLocs(globalFile) {
|
||||||
// Loading english strings, so we can inject them as constants.
|
// Loading english strings, so we can inject them as constants.
|
||||||
const refFile = path.resolve(__dirname, 'dist', 'languages', 'en.reference.json')
|
const refFile = path.resolve(__dirname, 'dist', 'languages', 'en.reference.json')
|
||||||
if (!fs.existsSync(refFile)) {
|
if (!fs.existsSync(refFile)) {
|
||||||
@ -25,7 +25,6 @@ function loadLocs() {
|
|||||||
|
|
||||||
// Reading client/@types/global.d.ts, to have a list of needed localized strings.
|
// Reading client/@types/global.d.ts, to have a list of needed localized strings.
|
||||||
const r = {}
|
const r = {}
|
||||||
const globalFile = path.resolve(__dirname, 'client', '@types', 'global.d.ts')
|
|
||||||
const globalFileContent = '' + fs.readFileSync(globalFile)
|
const globalFileContent = '' + fs.readFileSync(globalFile)
|
||||||
const matches = globalFileContent.matchAll(/^declare const LOC_(\w+)\b/gm)
|
const matches = globalFileContent.matchAll(/^declare const LOC_(\w+)\b/gm)
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
@ -41,7 +40,7 @@ function loadLocs() {
|
|||||||
const define = Object.assign({
|
const define = Object.assign({
|
||||||
PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name),
|
PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name),
|
||||||
PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, ''))
|
PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, ''))
|
||||||
}, loadLocs())
|
}, loadLocs(path.resolve(__dirname, 'client', '@types', 'global.d.ts')))
|
||||||
|
|
||||||
const configs = clientFiles.map(f => ({
|
const configs = clientFiles.map(f => ({
|
||||||
entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ],
|
entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ],
|
||||||
@ -59,8 +58,14 @@ const configs = clientFiles.map(f => ({
|
|||||||
outfile: path.resolve(__dirname, 'dist/client', f + '.js'),
|
outfile: path.resolve(__dirname, 'dist/client', f + '.js'),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const defineBuiltin = Object.assign(
|
||||||
|
{},
|
||||||
|
loadLocs(path.resolve(__dirname, 'conversejs', 'lib', '@types', 'global.d.ts'))
|
||||||
|
)
|
||||||
|
|
||||||
configs.push({
|
configs.push({
|
||||||
entryPoints: ["./conversejs/builtin.ts"],
|
entryPoints: ["./conversejs/builtin.ts"],
|
||||||
|
define: defineBuiltin,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
minify: true,
|
minify: true,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
# SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
# SPDX-FileCopyrightText: 2025 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@ -10,12 +11,12 @@ set -euo pipefail
|
|||||||
# This script download the Prosody AppImage from the https://github.com/JohnXLivingston/prosody-appimage project.
|
# This script download the Prosody AppImage from the https://github.com/JohnXLivingston/prosody-appimage project.
|
||||||
|
|
||||||
repo_base_url='https://github.com/JohnXLivingston/prosody-appimage/releases/download'
|
repo_base_url='https://github.com/JohnXLivingston/prosody-appimage/releases/download'
|
||||||
wanted_release='v0.12.3-1'
|
wanted_release='v0.12.4-3'
|
||||||
|
|
||||||
x86_64_filename='prosody-x86_64.AppImage'
|
x86_64_filename='prosody-x86_64.AppImage'
|
||||||
x86_64_sha256sum='f4af9bfefa2f804ad7e8b03a68f04194abb801f070ae620b3d4bcedb144e8523'
|
x86_64_sha256sum='83a583ac7036387514bed17afab257dab4161ccdd0ab7453818c78b51f830357'
|
||||||
aarch64_filename='prosody-aarch64.AppImage'
|
aarch64_filename='prosody-aarch64.AppImage'
|
||||||
aarch64_sha256sum='878c5be719e1e36a84d637fd2bd44e3059aa91ddb6906ad05f1dd0334078df09'
|
aarch64_sha256sum='7b7e6bf30d4498fc99a40022232c3065707ee4f4df24dc17947b007621634304'
|
||||||
|
|
||||||
download_dir="$(pwd)/vendor/prosody-appimage"
|
download_dir="$(pwd)/vendor/prosody-appimage"
|
||||||
dist_dir="$(pwd)/dist/server/prosody"
|
dist_dir="$(pwd)/dist/server/prosody"
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"standard-with-typescript",
|
|
||||||
"plugin:lit/recommended"
|
|
||||||
],
|
|
||||||
"globals": {},
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 2018,
|
|
||||||
"project": [
|
|
||||||
"./client/tsconfig.json"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"@typescript-eslint"
|
|
||||||
],
|
|
||||||
"ignorePatterns": [],
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/no-unused-vars": [2, {"argsIgnorePattern": "^_"}],
|
|
||||||
"@typescript-eslint/no-floating-promises": "error",
|
|
||||||
"@typescript-eslint/no-misused-promises": "error",
|
|
||||||
"@typescript-eslint/no-var-requires": "off",
|
|
||||||
"@typescript-eslint/strict-boolean-expressions": "off",
|
|
||||||
"@typescript-eslint/return-await": [2, "in-try-catch"], // FIXME: correct?
|
|
||||||
"@typescript-eslint/no-invalid-void-type": "off",
|
|
||||||
"@typescript-eslint/triple-slash-reference": "off",
|
|
||||||
"max-len": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"code": 120,
|
|
||||||
"comments": 120
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-unused-vars": "off"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
|
49
client/@types/global.d.ts
vendored
49
client/@types/global.d.ts
vendored
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@ -12,6 +12,7 @@ declare const MUSTACHE_CONFIGURATION_CHANNEL: string
|
|||||||
// Constants that begins with "LOC_" are loaded by build-client.js, reading the english locale file.
|
// Constants that begins with "LOC_" are loaded by build-client.js, reading the english locale file.
|
||||||
// See the online documentation: https://livingston.frama.io/peertube-plugin-livechat/contributing/translate/
|
// See the online documentation: https://livingston.frama.io/peertube-plugin-livechat/contributing/translate/
|
||||||
declare const LOC_ONLINE_HELP: string
|
declare const LOC_ONLINE_HELP: string
|
||||||
|
declare const LOC_CHAT: string
|
||||||
declare const LOC_OPEN_CHAT: string
|
declare const LOC_OPEN_CHAT: string
|
||||||
declare const LOC_OPEN_CHAT_NEW_WINDOW: string
|
declare const LOC_OPEN_CHAT_NEW_WINDOW: string
|
||||||
declare const LOC_CLOSE_CHAT: string
|
declare const LOC_CLOSE_CHAT: string
|
||||||
@ -55,13 +56,12 @@ declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_ENABLE_BOT_LABEL: string
|
|||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC: string
|
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC: string
|
declare const LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL: string
|
||||||
@ -81,8 +81,13 @@ declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DELAY_DESC: string
|
|||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_BANNED_JIDS_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_BANNED_JIDS_LABEL: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_NICKNAME: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_NICKNAME: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FOR_MORE_INFO: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FOR_MORE_INFO: string
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_MUTE_ANONYMOUS_LABEL: string
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_MUTE_ANONYMOUS_DESC: string
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_TERMS_LABEL: string
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_TERMS_DESC: string
|
||||||
|
|
||||||
declare const LOC_VALIDATION_ERROR: string
|
declare const LOC_VALIDATION_ERROR: string
|
||||||
|
declare const LOC_TOO_MANY_ENTRIES: string
|
||||||
declare const LOC_INVALID_VALUE: string
|
declare const LOC_INVALID_VALUE: string
|
||||||
declare const LOC_INVALID_VALUE_MISSING: string
|
declare const LOC_INVALID_VALUE_MISSING: string
|
||||||
declare const LOC_INVALID_VALUE_WRONG_TYPE: string
|
declare const LOC_INVALID_VALUE_WRONG_TYPE: string
|
||||||
@ -90,6 +95,7 @@ declare const LOC_INVALID_VALUE_WRONG_FORMAT: string
|
|||||||
declare const LOC_INVALID_VALUE_NOT_IN_RANGE: string
|
declare const LOC_INVALID_VALUE_NOT_IN_RANGE: string
|
||||||
declare const LOC_INVALID_VALUE_FILE_TOO_BIG: string
|
declare const LOC_INVALID_VALUE_FILE_TOO_BIG: string
|
||||||
declare const LOC_INVALID_VALUE_DUPLICATE: string
|
declare const LOC_INVALID_VALUE_DUPLICATE: string
|
||||||
|
declare const LOC_INVALID_VALUE_TOO_LONG: string
|
||||||
|
|
||||||
declare const LOC_CHATROOM_NOT_ACCESSIBLE: string
|
declare const LOC_CHATROOM_NOT_ACCESSIBLE: string
|
||||||
|
|
||||||
@ -122,3 +128,34 @@ declare const LOC_TOKEN_ACTION_CREATE: string
|
|||||||
declare const LOC_TOKEN_ACTION_REVOKE: string
|
declare const LOC_TOKEN_ACTION_REVOKE: string
|
||||||
declare const LOC_TOKEN_DEFAULT_LABEL: string
|
declare const LOC_TOKEN_DEFAULT_LABEL: string
|
||||||
declare const LOC_TOKEN_ACTION_REVOKE_CONFIRM: string
|
declare const LOC_TOKEN_ACTION_REVOKE_CONFIRM: string
|
||||||
|
|
||||||
|
declare const LOC_POLL_VOTE_OK: string
|
||||||
|
|
||||||
|
declare const LOC_MODERATION_DELAY: string
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_MODERATION_DELAY_DESC: string
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL: string
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_DESC: string
|
||||||
|
|
||||||
|
declare const LOC_PROSODY_FIREWALL_CONFIGURATION: string
|
||||||
|
declare const LOC_PROSODY_FIREWALL_CONFIGURATION_HELP: string
|
||||||
|
declare const LOC_PROSODY_FIREWALL_DISABLED_WARNING: string
|
||||||
|
declare const LOC_PROSODY_FIREWALL_FILE_ENABLED: string
|
||||||
|
declare const LOC_PROSODY_FIREWALL_NAME: string
|
||||||
|
declare const LOC_PROSODY_FIREWALL_NAME_DESC: string
|
||||||
|
declare const LOC_PROSODY_FIREWALL_CONTENT: string
|
||||||
|
|
||||||
|
declare const LOC_EMOJI_ONLY_MODE_TITLE: string
|
||||||
|
declare const LOC_EMOJI_ONLY_MODE_DESC_1: string
|
||||||
|
declare const LOC_EMOJI_ONLY_MODE_DESC_2: string
|
||||||
|
declare const LOC_EMOJI_ONLY_MODE_DESC_3: string
|
||||||
|
declare const LOC_EMOJI_ONLY_ENABLE_ALL_ROOMS: string
|
||||||
|
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_LABEL: string
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_DESC: string
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_LABEL: string
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_DESC: string
|
||||||
|
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL: string
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DESC: string
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_LABEL: string
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_DESC: string
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ function register (clientOptions: RegisterClientOptions): void {
|
|||||||
lastActivityEl.textContent = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
lastActivityEl.textContent = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
||||||
}
|
}
|
||||||
const promoteButton = document.createElement('a')
|
const promoteButton = document.createElement('a')
|
||||||
promoteButton.classList.add('orange-button', 'peertube-button-link')
|
promoteButton.classList.add('primary-button', 'orange-button', 'peertube-button-link')
|
||||||
promoteButton.style.margin = '5px'
|
promoteButton.style.margin = '5px'
|
||||||
promoteButton.onclick = async () => {
|
promoteButton.onclick = async () => {
|
||||||
await fetch(
|
await fetch(
|
||||||
@ -243,7 +243,10 @@ function register (clientOptions: RegisterClientOptions): void {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
peertubeHelpers.notifier.error(error.toString(), await peertubeHelpers.translate(LOC_LOADING_ERROR))
|
peertubeHelpers.notifier.error(
|
||||||
|
(error as Error).toString(),
|
||||||
|
await peertubeHelpers.translate(LOC_LOADING_ERROR)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -266,10 +269,15 @@ function register (clientOptions: RegisterClientOptions): void {
|
|||||||
return options.formValues['prosody-components'] !== true
|
return options.formValues['prosody-components'] !== true
|
||||||
case 'converse-autocolors':
|
case 'converse-autocolors':
|
||||||
return options.formValues['converse-theme'] !== 'peertube'
|
return options.formValues['converse-theme'] !== 'peertube'
|
||||||
|
case 'converse-theme-warning':
|
||||||
|
return options.formValues['converse-theme'] === 'peertube' &&
|
||||||
|
options.formValues['converse-autocolors'] === true
|
||||||
case 'chat-per-live-video-warning':
|
case 'chat-per-live-video-warning':
|
||||||
return !(options.formValues['chat-all-lives'] === true && options.formValues['chat-per-live-video'] === true)
|
return !(options.formValues['chat-all-lives'] === true && options.formValues['chat-per-live-video'] === true)
|
||||||
case 'auto-ban-anonymous-ip':
|
case 'auto-ban-anonymous-ip':
|
||||||
return options.formValues['chat-no-anonymous'] !== false
|
return options.formValues['chat-no-anonymous'] !== false
|
||||||
|
case 'prosody-firewall-configure-button':
|
||||||
|
return options.formValues['prosody-firewall-enabled'] !== true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name?.startsWith('external-auth-')) {
|
if (name?.startsWith('external-auth-')) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@ -8,6 +8,7 @@ import { registerConfiguration } from './common/configuration/register'
|
|||||||
import { registerVideoWatch } from './common/videowatch/register'
|
import { registerVideoWatch } from './common/videowatch/register'
|
||||||
import { registerRoom } from './common/room/register'
|
import { registerRoom } from './common/room/register'
|
||||||
import { initPtContext } from './common/lib/contexts/peertube'
|
import { initPtContext } from './common/lib/contexts/peertube'
|
||||||
|
import { registerAdminFirewall } from './common/admin/firewall/register'
|
||||||
import './common/lib/elements' // Import shared elements.
|
import './common/lib/elements' // Import shared elements.
|
||||||
|
|
||||||
async function register (clientOptions: RegisterClientOptions): Promise<void> {
|
async function register (clientOptions: RegisterClientOptions): Promise<void> {
|
||||||
@ -45,7 +46,7 @@ async function register (clientOptions: RegisterClientOptions): Promise<void> {
|
|||||||
])
|
])
|
||||||
const webchatFieldOptions: RegisterClientFormFieldOptions = {
|
const webchatFieldOptions: RegisterClientFormFieldOptions = {
|
||||||
name: 'livechat-active',
|
name: 'livechat-active',
|
||||||
label: label,
|
label,
|
||||||
descriptionHTML: description,
|
descriptionHTML: description,
|
||||||
type: 'input-checkbox',
|
type: 'input-checkbox',
|
||||||
default: true,
|
default: true,
|
||||||
@ -69,7 +70,8 @@ async function register (clientOptions: RegisterClientOptions): Promise<void> {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
registerVideoWatch(),
|
registerVideoWatch(),
|
||||||
registerRoom(clientOptions),
|
registerRoom(clientOptions),
|
||||||
registerConfiguration(clientOptions)
|
registerConfiguration(clientOptions),
|
||||||
|
registerAdminFirewall(clientOptions)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
131
client/common/admin/firewall/elements/admin-firewall.ts
Normal file
131
client/common/admin/firewall/elements/admin-firewall.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { AdminFirewallConfiguration } from 'shared/lib/types'
|
||||||
|
import { AdminFirewallService } from '../services/admin-firewall'
|
||||||
|
import { LivechatElement } from '../../../lib/elements/livechat'
|
||||||
|
import { ValidationError, ValidationErrorType } from '../../../lib/models/validation'
|
||||||
|
import { tplAdminFirewall } from '../templates/admin-firewall'
|
||||||
|
import { TemplateResult, html, nothing } from 'lit'
|
||||||
|
import { customElement, state } from 'lit/decorators.js'
|
||||||
|
import { Task } from '@lit/task'
|
||||||
|
|
||||||
|
@customElement('livechat-admin-firewall')
|
||||||
|
export class AdminFirewallElement extends LivechatElement {
|
||||||
|
private _adminFirewallService?: AdminFirewallService
|
||||||
|
|
||||||
|
@state()
|
||||||
|
public firewallConfiguration?: AdminFirewallConfiguration
|
||||||
|
|
||||||
|
@state()
|
||||||
|
public validationError?: ValidationError
|
||||||
|
|
||||||
|
@state()
|
||||||
|
public actionDisabled = false
|
||||||
|
|
||||||
|
private _asyncTaskRender: Task
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
super()
|
||||||
|
this._asyncTaskRender = this._initTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _initTask (): Task {
|
||||||
|
return new Task(this, {
|
||||||
|
task: async () => {
|
||||||
|
this._adminFirewallService = new AdminFirewallService(this.ptOptions)
|
||||||
|
this.firewallConfiguration = await this._adminFirewallService.fetchConfiguration()
|
||||||
|
this.actionDisabled = false // in case of reset
|
||||||
|
},
|
||||||
|
args: () => []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the form by reloading data from backend.
|
||||||
|
*/
|
||||||
|
public async reset (event?: Event): Promise<void> {
|
||||||
|
event?.preventDefault()
|
||||||
|
this.actionDisabled = true
|
||||||
|
this._asyncTaskRender = this._initTask()
|
||||||
|
this.requestUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the validation errors.
|
||||||
|
* @param ev the vent
|
||||||
|
*/
|
||||||
|
public resetValidation (_ev?: Event): void {
|
||||||
|
if (this.validationError) {
|
||||||
|
this.validationError = undefined
|
||||||
|
this.requestUpdate('_validationError')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the configuration.
|
||||||
|
* @param event event
|
||||||
|
*/
|
||||||
|
public readonly saveConfig = async (event?: Event): Promise<void> => {
|
||||||
|
event?.preventDefault()
|
||||||
|
if (!this.firewallConfiguration || !this._adminFirewallService) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.actionDisabled = true
|
||||||
|
this._adminFirewallService.saveConfiguration(this.firewallConfiguration)
|
||||||
|
.then((result: AdminFirewallConfiguration) => {
|
||||||
|
this.validationError = undefined
|
||||||
|
this.ptTranslate(LOC_SUCCESSFULLY_SAVED).then((msg) => {
|
||||||
|
this.ptNotifier.info(msg)
|
||||||
|
}, () => {})
|
||||||
|
this.firewallConfiguration = result
|
||||||
|
this.requestUpdate('firewallConfiguration')
|
||||||
|
this.requestUpdate('_validationError')
|
||||||
|
})
|
||||||
|
.catch(async (error: Error) => {
|
||||||
|
this.validationError = undefined
|
||||||
|
if (error instanceof ValidationError) {
|
||||||
|
this.validationError = error
|
||||||
|
}
|
||||||
|
this.logger.warn(`A validation error occurred in saving configuration. ${error.name}: ${error.message}`)
|
||||||
|
this.ptNotifier.error(
|
||||||
|
error.message
|
||||||
|
? error.message
|
||||||
|
: await this.ptTranslate(LOC_ERROR)
|
||||||
|
)
|
||||||
|
this.requestUpdate('_validationError')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.actionDisabled = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly getInputValidationClass = (propertyName: string): Record<string, boolean> => {
|
||||||
|
const validationErrorTypes: ValidationErrorType[] | undefined =
|
||||||
|
this.validationError?.properties[propertyName]
|
||||||
|
return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly renderFeedback = (feedbackId: string,
|
||||||
|
propertyName: string): TemplateResult | typeof nothing => {
|
||||||
|
const errorMessages: TemplateResult[] = []
|
||||||
|
const validationErrorTypes: ValidationErrorType[] | undefined =
|
||||||
|
this.validationError?.properties[propertyName] ?? undefined
|
||||||
|
|
||||||
|
// FIXME: this code is duplicated in dymamic table form
|
||||||
|
if (validationErrorTypes && validationErrorTypes.length !== 0) {
|
||||||
|
return html`<div id=${feedbackId} class="invalid-feedback">${errorMessages}</div>`
|
||||||
|
} else {
|
||||||
|
return nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override render = (): unknown => {
|
||||||
|
return this._asyncTaskRender.render({
|
||||||
|
pending: () => html`<livechat-spinner></livechat-spinner>`,
|
||||||
|
error: () => html`<livechat-error></livechat-error>`,
|
||||||
|
complete: () => tplAdminFirewall(this)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
5
client/common/admin/firewall/elements/index.ts
Normal file
5
client/common/admin/firewall/elements/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import './admin-firewall'
|
26
client/common/admin/firewall/register.ts
Normal file
26
client/common/admin/firewall/register.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
||||||
|
import { html, render } from 'lit'
|
||||||
|
import './elements' // Import all needed elements.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers stuff related to mod_firewall configuration.
|
||||||
|
* @param clientOptions Peertube client options
|
||||||
|
*/
|
||||||
|
async function registerAdminFirewall (clientOptions: RegisterClientOptions): Promise<void> {
|
||||||
|
const { registerClientRoute } = clientOptions
|
||||||
|
|
||||||
|
registerClientRoute({
|
||||||
|
route: 'livechat/admin/firewall',
|
||||||
|
onMount: async ({ rootEl }) => {
|
||||||
|
render(html`<livechat-admin-firewall .registerClientOptions=${clientOptions}></livechat-admin-firewall>`, rootEl)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
registerAdminFirewall
|
||||||
|
}
|
108
client/common/admin/firewall/services/admin-firewall.ts
Normal file
108
client/common/admin/firewall/services/admin-firewall.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
||||||
|
import type { AdminFirewallConfiguration } from 'shared/lib/types'
|
||||||
|
import {
|
||||||
|
maxFirewallFileSize, maxFirewallNameLength, maxFirewallFiles, firewallNameRegexp
|
||||||
|
} from 'shared/lib/admin-firewall'
|
||||||
|
import { ValidationError, ValidationErrorType } from '../../../lib/models/validation'
|
||||||
|
import { getBaseRoute } from '../../../../utils/uri'
|
||||||
|
|
||||||
|
export class AdminFirewallService {
|
||||||
|
public _registerClientOptions: RegisterClientOptions
|
||||||
|
|
||||||
|
private readonly _headers: any = {}
|
||||||
|
|
||||||
|
constructor (registerClientOptions: RegisterClientOptions) {
|
||||||
|
this._registerClientOptions = registerClientOptions
|
||||||
|
|
||||||
|
this._headers = this._registerClientOptions.peertubeHelpers.getAuthHeader() ?? {}
|
||||||
|
this._headers['content-type'] = 'application/json;charset=UTF-8'
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateConfiguration (adminFirewallConfiguration: AdminFirewallConfiguration): Promise<boolean> {
|
||||||
|
const propertiesError: ValidationError['properties'] = {}
|
||||||
|
|
||||||
|
if (adminFirewallConfiguration.files.length > maxFirewallFiles) {
|
||||||
|
const validationError = new ValidationError(
|
||||||
|
'AdminFirewallConfigurationValidationError',
|
||||||
|
await this._registerClientOptions.peertubeHelpers.translate(LOC_TOO_MANY_ENTRIES),
|
||||||
|
propertiesError
|
||||||
|
)
|
||||||
|
throw validationError
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Map<string, true>()
|
||||||
|
for (const [i, e] of adminFirewallConfiguration.files.entries()) {
|
||||||
|
propertiesError[`files.${i}.name`] = []
|
||||||
|
if (e.name === '') {
|
||||||
|
propertiesError[`files.${i}.name`].push(ValidationErrorType.Missing)
|
||||||
|
} else if (e.name.length > maxFirewallNameLength) {
|
||||||
|
propertiesError[`files.${i}.name`].push(ValidationErrorType.TooLong)
|
||||||
|
} else if (!firewallNameRegexp.test(e.name)) {
|
||||||
|
propertiesError[`files.${i}.name`].push(ValidationErrorType.WrongFormat)
|
||||||
|
} else if (seen.has(e.name)) {
|
||||||
|
propertiesError[`files.${i}.name`].push(ValidationErrorType.Duplicate)
|
||||||
|
} else {
|
||||||
|
seen.set(e.name, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
propertiesError[`files.${i}.content`] = []
|
||||||
|
if (e.content.length > maxFirewallFileSize) {
|
||||||
|
propertiesError[`files.${i}.content`].push(ValidationErrorType.TooLong)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.values(propertiesError).find(e => e.length > 0)) {
|
||||||
|
const validationError = new ValidationError(
|
||||||
|
'AdminFirewallConfigurationValidationError',
|
||||||
|
await this._registerClientOptions.peertubeHelpers.translate(LOC_VALIDATION_ERROR),
|
||||||
|
propertiesError
|
||||||
|
)
|
||||||
|
throw validationError
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveConfiguration (
|
||||||
|
adminFirewallConfiguration: AdminFirewallConfiguration
|
||||||
|
): Promise<AdminFirewallConfiguration> {
|
||||||
|
if (!await this.validateConfiguration(adminFirewallConfiguration)) {
|
||||||
|
throw new Error('Invalid form data')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
getBaseRoute(this._registerClientOptions) + '/api/admin/firewall/',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: this._headers,
|
||||||
|
body: JSON.stringify(adminFirewallConfiguration)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to save configuration.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchConfiguration (): Promise<AdminFirewallConfiguration> {
|
||||||
|
const response = await fetch(
|
||||||
|
getBaseRoute(this._registerClientOptions) + '/api/admin/firewall/',
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: this._headers
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Can\'t get firewall configuration.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
}
|
91
client/common/admin/firewall/templates/admin-firewall.ts
Normal file
91
client/common/admin/firewall/templates/admin-firewall.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
|
import type { AdminFirewallElement } from '../elements/admin-firewall'
|
||||||
|
import type { TemplateResult } from 'lit'
|
||||||
|
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
|
||||||
|
import { maxFirewallFiles, maxFirewallNameLength, maxFirewallFileSize } from 'shared/lib/admin-firewall'
|
||||||
|
import { ptTr } from '../../../lib/directives/translation'
|
||||||
|
import { html } from 'lit'
|
||||||
|
|
||||||
|
export function tplAdminFirewall (el: AdminFirewallElement): TemplateResult {
|
||||||
|
const tableHeaderList: DynamicFormHeader = {
|
||||||
|
enabled: {
|
||||||
|
colName: ptTr(LOC_PROSODY_FIREWALL_FILE_ENABLED)
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
colName: ptTr(LOC_PROSODY_FIREWALL_NAME),
|
||||||
|
description: ptTr(LOC_PROSODY_FIREWALL_NAME_DESC),
|
||||||
|
headerClassList: ['peertube-livechat-admin-firewall-col-name']
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
colName: ptTr(LOC_PROSODY_FIREWALL_CONTENT),
|
||||||
|
headerClassList: ['peertube-livechat-admin-firewall-col-content']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const tableSchema: DynamicFormSchema = {
|
||||||
|
enabled: {
|
||||||
|
inputType: 'checkbox',
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
inputType: 'text',
|
||||||
|
default: '',
|
||||||
|
maxlength: maxFirewallNameLength
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
inputType: 'textarea',
|
||||||
|
default: '',
|
||||||
|
maxlength: maxFirewallFileSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="margin-content peertube-plugin-livechat-admin-firewall">
|
||||||
|
<h1>
|
||||||
|
${ptTr(LOC_PROSODY_FIREWALL_CONFIGURATION)}
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
${ptTr(LOC_PROSODY_FIREWALL_CONFIGURATION_HELP, true)}
|
||||||
|
<livechat-help-button .page=${'documentation/admin/mod_firewall'}>
|
||||||
|
</livechat-help-button>
|
||||||
|
</p>
|
||||||
|
${
|
||||||
|
el.firewallConfiguration?.enabled
|
||||||
|
? ''
|
||||||
|
: html`<p class="peertube-plugin-livechat-warning">${ptTr(LOC_PROSODY_FIREWALL_DISABLED_WARNING, true)}</p>`
|
||||||
|
}
|
||||||
|
|
||||||
|
<form role="form" @submit=${el.saveConfig} @change=${el.resetValidation}>
|
||||||
|
<livechat-dynamic-table-form
|
||||||
|
.header=${tableHeaderList}
|
||||||
|
.schema=${tableSchema}
|
||||||
|
.maxLines=${maxFirewallFiles}
|
||||||
|
.validation=${el.validationError?.properties}
|
||||||
|
.validationPrefix=${'files'}
|
||||||
|
.rows=${el.firewallConfiguration?.files ?? []}
|
||||||
|
@update=${(e: CustomEvent) => {
|
||||||
|
el.resetValidation(e)
|
||||||
|
if (el.firewallConfiguration) {
|
||||||
|
el.firewallConfiguration.files = e.detail
|
||||||
|
el.requestUpdate('firewallConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
></livechat-dynamic-table-form>
|
||||||
|
|
||||||
|
<div class="form-group mt-5">
|
||||||
|
<button type="reset" @click=${el.reset} ?disabled=${el.actionDisabled}>
|
||||||
|
${ptTr(LOC_CANCEL)}
|
||||||
|
</button>
|
||||||
|
<button type="submit" ?disabled=${el.actionDisabled}>
|
||||||
|
${ptTr(LOC_SAVE)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>`
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ import { customElement, property, state } from 'lit/decorators.js'
|
|||||||
import { ptTr } from '../../lib/directives/translation'
|
import { ptTr } from '../../lib/directives/translation'
|
||||||
import { Task } from '@lit/task'
|
import { Task } from '@lit/task'
|
||||||
import { provide } from '@lit/context'
|
import { provide } from '@lit/context'
|
||||||
|
import { channelTermsMaxLength } from 'shared/lib/constants'
|
||||||
|
|
||||||
@customElement('livechat-channel-configuration')
|
@customElement('livechat-channel-configuration')
|
||||||
export class ChannelConfigurationElement extends LivechatElement {
|
export class ChannelConfigurationElement extends LivechatElement {
|
||||||
@ -31,7 +32,7 @@ export class ChannelConfigurationElement extends LivechatElement {
|
|||||||
public validationError?: ValidationError
|
public validationError?: ValidationError
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
public actionDisabled: boolean = false
|
public actionDisabled = false
|
||||||
|
|
||||||
private _asyncTaskRender: Task
|
private _asyncTaskRender: Task
|
||||||
|
|
||||||
@ -51,6 +52,10 @@ export class ChannelConfigurationElement extends LivechatElement {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public termsMaxLength (): number {
|
||||||
|
return channelTermsMaxLength
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the form by reloading data from backend.
|
* Resets the form by reloading data from backend.
|
||||||
*/
|
*/
|
||||||
@ -108,9 +113,9 @@ export class ChannelConfigurationElement extends LivechatElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly getInputValidationClass = (propertyName: string): { [key: string]: boolean } => {
|
public readonly getInputValidationClass = (propertyName: string): Record<string, boolean> => {
|
||||||
const validationErrorTypes: ValidationErrorType[] | undefined =
|
const validationErrorTypes: ValidationErrorType[] | undefined =
|
||||||
this.validationError?.properties[`${propertyName}`]
|
this.validationError?.properties[propertyName]
|
||||||
return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {}
|
return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,9 +123,13 @@ export class ChannelConfigurationElement extends LivechatElement {
|
|||||||
propertyName: string): TemplateResult | typeof nothing => {
|
propertyName: string): TemplateResult | typeof nothing => {
|
||||||
const errorMessages: TemplateResult[] = []
|
const errorMessages: TemplateResult[] = []
|
||||||
const validationErrorTypes: ValidationErrorType[] | undefined =
|
const validationErrorTypes: ValidationErrorType[] | undefined =
|
||||||
this.validationError?.properties[`${propertyName}`] ?? undefined
|
this.validationError?.properties[propertyName] ?? undefined
|
||||||
|
|
||||||
|
// FIXME: this code is duplicated in dymamic table form
|
||||||
if (validationErrorTypes && validationErrorTypes.length !== 0) {
|
if (validationErrorTypes && validationErrorTypes.length !== 0) {
|
||||||
|
if (validationErrorTypes.includes(ValidationErrorType.Missing)) {
|
||||||
|
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_MISSING)}`)
|
||||||
|
}
|
||||||
if (validationErrorTypes.includes(ValidationErrorType.WrongType)) {
|
if (validationErrorTypes.includes(ValidationErrorType.WrongType)) {
|
||||||
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_WRONG_TYPE)}`)
|
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_WRONG_TYPE)}`)
|
||||||
}
|
}
|
||||||
@ -130,6 +139,9 @@ export class ChannelConfigurationElement extends LivechatElement {
|
|||||||
if (validationErrorTypes.includes(ValidationErrorType.NotInRange)) {
|
if (validationErrorTypes.includes(ValidationErrorType.NotInRange)) {
|
||||||
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_NOT_IN_RANGE)}`)
|
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_NOT_IN_RANGE)}`)
|
||||||
}
|
}
|
||||||
|
if (validationErrorTypes.includes(ValidationErrorType.TooLong)) {
|
||||||
|
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_TOO_LONG)}`)
|
||||||
|
}
|
||||||
|
|
||||||
return html`<div id=${feedbackId} class="invalid-feedback">${errorMessages}</div>`
|
return html`<div id=${feedbackId} class="invalid-feedback">${errorMessages}</div>`
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
public validationError?: ValidationError
|
public validationError?: ValidationError
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
public actionDisabled: boolean = false
|
public actionDisabled = false
|
||||||
|
|
||||||
private _asyncTaskRender: Task
|
private _asyncTaskRender: Task
|
||||||
|
|
||||||
@ -102,9 +102,13 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.actionDisabled = true
|
this.actionDisabled = true
|
||||||
await this._channelDetailsService.saveEmojisConfiguration(this.channelId, this.channelEmojisConfiguration.emojis)
|
this.channelEmojisConfiguration = await this._channelDetailsService.saveEmojisConfiguration(
|
||||||
|
this.channelId,
|
||||||
|
this.channelEmojisConfiguration.emojis
|
||||||
|
)
|
||||||
this.validationError = undefined
|
this.validationError = undefined
|
||||||
this.ptNotifier.info(await this.ptTranslate(LOC_SUCCESSFULLY_SAVED))
|
this.ptNotifier.info(await this.ptTranslate(LOC_SUCCESSFULLY_SAVED))
|
||||||
|
this.requestUpdate('channelEmojisConfiguration')
|
||||||
this.requestUpdate('_validationError')
|
this.requestUpdate('_validationError')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.validationError = undefined
|
this.validationError = undefined
|
||||||
@ -128,7 +132,6 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
*/
|
*/
|
||||||
public async importEmojis (ev: Event): Promise<void> {
|
public async importEmojis (ev: Event): Promise<void> {
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
this.actionDisabled = true
|
|
||||||
try {
|
try {
|
||||||
// download a json file:
|
// download a json file:
|
||||||
const file = await new Promise<File>((resolve, reject) => {
|
const file = await new Promise<File>((resolve, reject) => {
|
||||||
@ -149,6 +152,8 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
input.remove()
|
input.remove()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.actionDisabled = true
|
||||||
|
|
||||||
const content = await new Promise<string>((resolve, reject) => {
|
const content = await new Promise<string>((resolve, reject) => {
|
||||||
const fileReader = new FileReader()
|
const fileReader = new FileReader()
|
||||||
fileReader.onerror = reject
|
fileReader.onerror = reject
|
||||||
@ -170,6 +175,15 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
if (!Array.isArray(json)) {
|
if (!Array.isArray(json)) {
|
||||||
throw new Error('Invalid data, an array was expected')
|
throw new Error('Invalid data, an array was expected')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Before adding new entries, we check if the last current line is empty,
|
||||||
|
// and remove it in such case.
|
||||||
|
// See https://github.com/JohnXLivingston/peertube-plugin-livechat/issues/437
|
||||||
|
const last = this.channelEmojisConfiguration?.emojis.customEmojis.slice(-1)[0]
|
||||||
|
if (last && last.sn === '' && last.url === '') {
|
||||||
|
this.channelEmojisConfiguration?.emojis.customEmojis.pop()
|
||||||
|
}
|
||||||
|
|
||||||
for (const entry of json) {
|
for (const entry of json) {
|
||||||
if (typeof entry !== 'object') {
|
if (typeof entry !== 'object') {
|
||||||
throw new Error('Invalid data')
|
throw new Error('Invalid data')
|
||||||
@ -178,10 +192,8 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
throw new Error('Invalid data')
|
throw new Error('Invalid data')
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = await this._convertImageToDataUrl(entry.url)
|
const url = await this._convertImageToDataUrl(entry.url as string)
|
||||||
let sn = entry.sn as string
|
const sn = entry.sn as string
|
||||||
if (!sn.startsWith(':')) { sn = ':' + sn }
|
|
||||||
if (!sn.endsWith(':')) { sn += ':' }
|
|
||||||
|
|
||||||
const item: ChannelEmojisConfiguration['emojis']['customEmojis'][0] = {
|
const item: ChannelEmojisConfiguration['emojis']['customEmojis'][0] = {
|
||||||
sn,
|
sn,
|
||||||
@ -199,7 +211,7 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
await this.ptTranslate(LOC_ACTION_IMPORT_EMOJIS_INFO)
|
await this.ptTranslate(LOC_ACTION_IMPORT_EMOJIS_INFO)
|
||||||
)
|
)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR))
|
this.ptNotifier.error((err as Error).toString(), await this.ptTranslate(LOC_ERROR))
|
||||||
} finally {
|
} finally {
|
||||||
this.actionDisabled = false
|
this.actionDisabled = false
|
||||||
}
|
}
|
||||||
@ -238,12 +250,27 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
a.remove()
|
a.remove()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error(err)
|
this.logger.error(err)
|
||||||
this.ptNotifier.error(err.toString())
|
this.ptNotifier.error((err as Error).toString())
|
||||||
} finally {
|
} finally {
|
||||||
this.actionDisabled = false
|
this.actionDisabled = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async enableEmojisOnlyModeOnAllRooms (ev: Event): Promise<void> {
|
||||||
|
ev.preventDefault()
|
||||||
|
if (!this._channelDetailsService || !this.channelId) {
|
||||||
|
this.ptNotifier.error(await this.ptTranslate(LOC_ERROR))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this._channelDetailsService.enableEmojisOnlyModeOnAllRooms(this.channelId)
|
||||||
|
this.ptNotifier.info(await this.ptTranslate(LOC_SUCCESSFULLY_SAVED))
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
this.ptNotifier.error(await this.ptTranslate(LOC_ERROR))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes an url (or dataUrl), download the image, and converts to dataUrl.
|
* Takes an url (or dataUrl), download the image, and converts to dataUrl.
|
||||||
* @param url the url
|
* @param url the url
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
import { customElement, state } from 'lit/decorators.js'
|
import { customElement, state } from 'lit/decorators.js'
|
||||||
import { ptTr } from '../../lib/directives/translation'
|
import { ptTr } from '../../lib/directives/translation'
|
||||||
@ -50,7 +53,7 @@ export class ChannelHomeElement extends LivechatElement {
|
|||||||
<ul class="peertube-plugin-livechat-configuration-home-channels">
|
<ul class="peertube-plugin-livechat-configuration-home-channels">
|
||||||
${this._channels?.map((channel) => html`
|
${this._channels?.map((channel) => html`
|
||||||
<li>
|
<li>
|
||||||
<a href="${channel.livechatConfigurationUri}">
|
<a href="${channel.livechatConfigurationUri}" aria-hidden="true">
|
||||||
${channel.avatar
|
${channel.avatar
|
||||||
? html`<img class="avatar channel" src="${channel.avatar.path}">`
|
? html`<img class="avatar channel" src="${channel.avatar.path}">`
|
||||||
: html`<div class="avatar channel initial gray"></div>`
|
: html`<div class="avatar channel initial gray"></div>`
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import { LivechatElement } from '../../lib/elements/livechat'
|
import { LivechatElement } from '../../lib/elements/livechat'
|
||||||
import { ptTr } from '../../lib/directives/translation'
|
import { ptTr } from '../../lib/directives/translation'
|
||||||
import { html, TemplateResult } from 'lit'
|
import { html, TemplateResult } from 'lit'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
@ -1,35 +1,38 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import type { ChannelConfigurationElement } from '../channel-configuration'
|
import type { ChannelConfigurationElement } from '../channel-configuration'
|
||||||
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
|
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
|
||||||
import { ptTr } from '../../../lib/directives/translation'
|
import { ptTr } from '../../../lib/directives/translation'
|
||||||
import { html, TemplateResult } from 'lit'
|
import { html, TemplateResult } from 'lit'
|
||||||
import { classMap } from 'lit/directives/class-map.js'
|
import { classMap } from 'lit/directives/class-map.js'
|
||||||
|
import { noDuplicateMaxDelay, forbidSpecialCharsMaxTolerance } from 'shared/lib/constants'
|
||||||
|
|
||||||
export function tplChannelConfiguration (el: ChannelConfigurationElement): TemplateResult {
|
export function tplChannelConfiguration (el: ChannelConfigurationElement): TemplateResult {
|
||||||
const tableHeaderList: {[key: string]: DynamicFormHeader} = {
|
const tableHeaderList: Record<string, DynamicFormHeader> = {
|
||||||
forbiddenWords: {
|
forbiddenWords: {
|
||||||
entries: {
|
entries: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL),
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)
|
||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2)
|
|
||||||
},
|
},
|
||||||
regexp: {
|
regexp: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL),
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL),
|
||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC)
|
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC)
|
||||||
},
|
},
|
||||||
applyToModerators: {
|
applyToModerators: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL),
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL),
|
||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC)
|
description: ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL),
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL),
|
||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC)
|
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC)
|
||||||
},
|
},
|
||||||
reason: {
|
reason: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL),
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL),
|
||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC)
|
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)
|
||||||
},
|
},
|
||||||
comments: {
|
comments: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL),
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL),
|
||||||
@ -57,7 +60,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tableSchema: {[key: string]: DynamicFormSchema} = {
|
const tableSchema: Record<string, DynamicFormSchema> = {
|
||||||
forbiddenWords: {
|
forbiddenWords: {
|
||||||
entries: {
|
entries: {
|
||||||
inputType: 'tags',
|
inputType: 'tags',
|
||||||
@ -93,7 +96,8 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
},
|
},
|
||||||
delay: {
|
delay: {
|
||||||
inputType: 'number',
|
inputType: 'number',
|
||||||
default: 10
|
default: 10,
|
||||||
|
min: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
commands: {
|
commands: {
|
||||||
@ -127,6 +131,64 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form livechat-configuration-channel-options role="form" @submit=${el.saveConfig} @change=${el.resetValidation}>
|
<form livechat-configuration-channel-options role="form" @submit=${el.saveConfig} @change=${el.resetValidation}>
|
||||||
|
|
||||||
|
<livechat-configuration-section-header
|
||||||
|
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TERMS_LABEL)}
|
||||||
|
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TERMS_DESC, true)}
|
||||||
|
.helpPage=${'documentation/user/streamers/terms'}>
|
||||||
|
</livechat-configuration-section-header>
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea
|
||||||
|
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TERMS_LABEL) as any}
|
||||||
|
name="terms"
|
||||||
|
id="peertube-livechat-terms"
|
||||||
|
.value=${el.channelConfiguration?.configuration.terms ?? ''}
|
||||||
|
maxlength=${el.termsMaxLength()}
|
||||||
|
class=${classMap(
|
||||||
|
Object.assign(
|
||||||
|
{ 'form-control': true },
|
||||||
|
el.getInputValidationClass('terms')
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
@change=${(event: Event) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
let value: string | undefined = (event.target as HTMLTextAreaElement).value
|
||||||
|
if (value === '') { value = undefined }
|
||||||
|
el.channelConfiguration.configuration.terms = value
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
></textarea>
|
||||||
|
${el.renderFeedback('peertube-livechat-terms-feedback', 'terms')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<livechat-configuration-section-header
|
||||||
|
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_MUTE_ANONYMOUS_LABEL)}
|
||||||
|
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_MUTE_ANONYMOUS_DESC, true)}
|
||||||
|
.helpPage=${'documentation/user/streamers/moderation'}>
|
||||||
|
</livechat-configuration-section-header>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="mute_anonymous"
|
||||||
|
id="peertube-livechat-mute-anonymous"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.mute.anonymous =
|
||||||
|
(event.target as HTMLInputElement).checked
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="1"
|
||||||
|
?checked=${el.channelConfiguration?.configuration.mute.anonymous}
|
||||||
|
/>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_MUTE_ANONYMOUS_LABEL)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<livechat-configuration-section-header
|
<livechat-configuration-section-header
|
||||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_LABEL)}
|
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_LABEL)}
|
||||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_DESC, true)}
|
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_DESC, true)}
|
||||||
@ -162,6 +224,67 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
${el.renderFeedback('peertube-livechat-slowmode-duration-feedback', 'slowMode.duration')}
|
${el.renderFeedback('peertube-livechat-slowmode-duration-feedback', 'slowMode.duration')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<livechat-configuration-section-header
|
||||||
|
.label=${ptTr(LOC_MODERATION_DELAY)}
|
||||||
|
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_MODERATION_DELAY_DESC, true)}
|
||||||
|
.helpPage=${'documentation/user/streamers/moderation_delay'}>
|
||||||
|
</livechat-configuration-section-header>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
${ptTr(LOC_MODERATION_DELAY)}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="moderation_delay"
|
||||||
|
class=${classMap(
|
||||||
|
Object.assign(
|
||||||
|
{ 'form-control': true },
|
||||||
|
el.getInputValidationClass('moderation.delay')
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
min="0"
|
||||||
|
max="60"
|
||||||
|
id="peertube-livechat-moderation-delay"
|
||||||
|
aria-describedby="peertube-livechat-moderation-delay-feedback"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.moderation.delay =
|
||||||
|
Number((event.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="${el.channelConfiguration?.configuration.moderation.delay ?? ''}"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
${el.renderFeedback('peertube-livechat-moderation-delay-feedback', 'moderation.delay')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<livechat-configuration-section-header
|
||||||
|
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL)}
|
||||||
|
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_DESC, true)}
|
||||||
|
.helpPage=${'documentation/user/streamers/moderation'}>
|
||||||
|
</livechat-configuration-section-header>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="anonymize-moderation"
|
||||||
|
id="peertube-livechat-anonymize-moderation"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.moderation.anonymize =
|
||||||
|
(event.target as HTMLInputElement).checked
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="1"
|
||||||
|
?checked=${el.channelConfiguration?.configuration.moderation.anonymize}
|
||||||
|
/>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<livechat-configuration-section-header
|
<livechat-configuration-section-header
|
||||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
|
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
|
||||||
.description=${''}
|
.description=${''}
|
||||||
@ -218,6 +341,246 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
${el.renderFeedback('peertube-livechat-bot-nickname-feedback', 'bot.nickname')}
|
${el.renderFeedback('peertube-livechat-bot-nickname-feedback', 'bot.nickname')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<livechat-configuration-section-header
|
||||||
|
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_LABEL)}
|
||||||
|
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_DESC)}
|
||||||
|
.helpPage=${'documentation/user/streamers/bot/special_chars'}>
|
||||||
|
</livechat-configuration-section-header>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="forbid_special_chars"
|
||||||
|
id="peertube-livechat-forbid-special-chars"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.bot.forbidSpecialChars.enabled =
|
||||||
|
(event.target as HTMLInputElement).checked
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="1"
|
||||||
|
?checked=${el.channelConfiguration?.configuration.bot.forbidSpecialChars.enabled}
|
||||||
|
/>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_LABEL)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
${!el.channelConfiguration?.configuration.bot.forbidSpecialChars.enabled
|
||||||
|
? ''
|
||||||
|
: html`
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_LABEL)}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="special_chars_tolerance"
|
||||||
|
class=${classMap(
|
||||||
|
Object.assign(
|
||||||
|
{ 'form-control': true },
|
||||||
|
el.getInputValidationClass('bot.forbidSpecialChars.tolerance')
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
min="0"
|
||||||
|
max="${forbidSpecialCharsMaxTolerance}"
|
||||||
|
id="peertube-livechat-forbid-special-chars-tolerance"
|
||||||
|
aria-describedby="peertube-livechat-forbid-special-chars-tolerance-feedback"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.bot.forbidSpecialChars.tolerance =
|
||||||
|
Number((event.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="${el.channelConfiguration?.configuration.bot.forbidSpecialChars.tolerance ?? '0'}"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_DESC)}
|
||||||
|
</small>
|
||||||
|
${el.renderFeedback('peertube-livechat-forbid-special-chars-tolerance-feedback',
|
||||||
|
'bot.forbidSpecialChars.tolerance')
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL)}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="special_chars_reason"
|
||||||
|
class=${classMap(
|
||||||
|
Object.assign(
|
||||||
|
{ 'form-control': true },
|
||||||
|
el.getInputValidationClass('bot.forbidSpecialChars.reason')
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
id="peertube-livechat-forbid-special-chars-reason"
|
||||||
|
aria-describedby="peertube-livechat-forbid-special-chars-reason-feedback"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.bot.forbidSpecialChars.reason =
|
||||||
|
(event.target as HTMLInputElement).value
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="${el.channelConfiguration?.configuration.bot.forbidSpecialChars.reason ?? ''}"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)}
|
||||||
|
</small>
|
||||||
|
${el.renderFeedback('peertube-livechat-forbid-special-chars-reason-feedback',
|
||||||
|
'bot.forbidSpecialChars.reason')
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="forbid_special_chars_applyToModerators"
|
||||||
|
id="peertube-livechat-forbid-special-chars-applyToModerators"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.bot.forbidSpecialChars.applyToModerators =
|
||||||
|
(event.target as HTMLInputElement).checked
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="1"
|
||||||
|
?checked=${el.channelConfiguration?.configuration.bot.forbidSpecialChars.applyToModerators}
|
||||||
|
/>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL)}
|
||||||
|
</label>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
<livechat-configuration-section-header
|
||||||
|
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL)}
|
||||||
|
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DESC)}
|
||||||
|
.helpPage=${'documentation/user/streamers/bot/no_duplicate'}>
|
||||||
|
</livechat-configuration-section-header>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="no_duplicate"
|
||||||
|
id="peertube-livechat-no-duplicate"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.bot.noDuplicate.enabled =
|
||||||
|
(event.target as HTMLInputElement).checked
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="1"
|
||||||
|
?checked=${el.channelConfiguration?.configuration.bot.noDuplicate.enabled}
|
||||||
|
/>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
${!el.channelConfiguration?.configuration.bot.noDuplicate.enabled
|
||||||
|
? ''
|
||||||
|
: html`
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_LABEL)}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="no_duplicate_delay"
|
||||||
|
class=${classMap(
|
||||||
|
Object.assign(
|
||||||
|
{ 'form-control': true },
|
||||||
|
el.getInputValidationClass('bot.noDuplicate.delay')
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
min="0"
|
||||||
|
max="${noDuplicateMaxDelay.toString()}"
|
||||||
|
id="peertube-livechat-no-duplicate-delay"
|
||||||
|
aria-describedby="peertube-livechat-no-duplicate-delay-feedback"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.bot.noDuplicate.delay =
|
||||||
|
Number((event.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="${el.channelConfiguration?.configuration.bot.noDuplicate.delay ?? '0'}"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_DESC)}
|
||||||
|
</small>
|
||||||
|
${el.renderFeedback('peertube-livechat-no-duplicate-delay-feedback',
|
||||||
|
'bot.noDuplicate.delay')
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL)}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="no_duplicate_reason"
|
||||||
|
class=${classMap(
|
||||||
|
Object.assign(
|
||||||
|
{ 'form-control': true },
|
||||||
|
el.getInputValidationClass('bot.noDuplicate.reason')
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
id="peertube-livechat-no-duplicate-reason"
|
||||||
|
aria-describedby="peertube-livechat-no-duplicate-reason-feedback"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.bot.noDuplicate.reason =
|
||||||
|
(event.target as HTMLInputElement).value
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="${el.channelConfiguration?.configuration.bot.noDuplicate.reason ?? ''}"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)}
|
||||||
|
</small>
|
||||||
|
${el.renderFeedback('peertube-livechat-no-duplicate-reason-feedback',
|
||||||
|
'bot.noDuplicate.reason')
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="no_duplicate_applyToModerators"
|
||||||
|
id="peertube-livechat-no-duplicate-applyToModerators"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.bot.noDuplicate.applyToModerators =
|
||||||
|
(event.target as HTMLInputElement).checked
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="1"
|
||||||
|
?checked=${el.channelConfiguration?.configuration.bot.noDuplicate.applyToModerators}
|
||||||
|
/>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL)}
|
||||||
|
</label>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
<livechat-configuration-section-header
|
<livechat-configuration-section-header
|
||||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
|
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
|
||||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)}
|
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import type { ChannelEmojisElement } from '../channel-emojis'
|
import type { ChannelEmojisElement } from '../channel-emojis'
|
||||||
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
|
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
|
||||||
import { maxEmojisPerChannel } from 'shared/lib/emojis'
|
import { maxEmojisPerChannel } from 'shared/lib/emojis'
|
||||||
@ -45,13 +48,14 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
|
|||||||
|
|
||||||
<livechat-channel-tabs .active=${'emojis'} .channelId=${el.channelId}></livechat-channel-tabs>
|
<livechat-channel-tabs .active=${'emojis'} .channelId=${el.channelId}></livechat-channel-tabs>
|
||||||
|
|
||||||
|
<h2>${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_TITLE)}</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_DESC)}
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_DESC)}
|
||||||
<livechat-help-button .page=${'documentation/user/streamers/emojis'}>
|
<livechat-help-button .page=${'documentation/user/streamers/emojis'}>
|
||||||
</livechat-help-button>
|
</livechat-help-button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
<form role="form" @submit=${el.saveEmojis} @change=${el.resetValidation}>
|
<form role="form" @submit=${el.saveEmojis} @change=${el.resetValidation}>
|
||||||
<div class="peertube-plugin-livechat-configuration-actions">
|
<div class="peertube-plugin-livechat-configuration-actions">
|
||||||
${
|
${
|
||||||
@ -86,17 +90,11 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
|
|||||||
.maxLines=${maxEmojisPerChannel}
|
.maxLines=${maxEmojisPerChannel}
|
||||||
.validation=${el.validationError?.properties}
|
.validation=${el.validationError?.properties}
|
||||||
.validationPrefix=${'emojis'}
|
.validationPrefix=${'emojis'}
|
||||||
.rows=${el.channelEmojisConfiguration?.emojis.customEmojis}
|
.rows=${el.channelEmojisConfiguration?.emojis.customEmojis ?? []}
|
||||||
@update=${(e: CustomEvent) => {
|
@update=${(e: CustomEvent) => {
|
||||||
el.resetValidation(e)
|
el.resetValidation(e)
|
||||||
if (el.channelEmojisConfiguration) {
|
if (el.channelEmojisConfiguration) {
|
||||||
el.channelEmojisConfiguration.emojis.customEmojis = e.detail
|
el.channelEmojisConfiguration.emojis.customEmojis = e.detail
|
||||||
// Fixing missing ':' for shortnames:
|
|
||||||
for (const desc of el.channelEmojisConfiguration.emojis.customEmojis) {
|
|
||||||
if (desc.sn === '') { continue }
|
|
||||||
if (!desc.sn.startsWith(':')) { desc.sn = ':' + desc.sn }
|
|
||||||
if (!desc.sn.endsWith(':')) { desc.sn += ':' }
|
|
||||||
}
|
|
||||||
el.requestUpdate('channelEmojisConfiguration')
|
el.requestUpdate('channelEmojisConfiguration')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,5 +110,23 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<h2>${ptTr(LOC_EMOJI_ONLY_MODE_TITLE)}</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
${ptTr(LOC_EMOJI_ONLY_MODE_DESC_1, true)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
${ptTr(LOC_EMOJI_ONLY_MODE_DESC_2, true)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
${ptTr(LOC_EMOJI_ONLY_MODE_DESC_3, true)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="peertube-plugin-livechat-configuration-actions">
|
||||||
|
<button type="button" @click=${el.enableEmojisOnlyModeOnAllRooms}>
|
||||||
|
${ptTr(LOC_EMOJI_ONLY_ENABLE_ALL_ROOMS)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
@ -66,15 +66,16 @@ async function registerConfiguration (clientOptions: RegisterClientOptions): Pro
|
|||||||
for (const link of links) {
|
for (const link of links) {
|
||||||
if (typeof link !== 'object') { continue }
|
if (typeof link !== 'object') { continue }
|
||||||
if (!('key' in link)) { continue }
|
if (!('key' in link)) { continue }
|
||||||
if (link.key !== 'in-my-library') { continue }
|
if (link.key === 'in-my-library' || link.key === 'my-video-space') {
|
||||||
myLibraryLinks = link
|
myLibraryLinks = link
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!myLibraryLinks) { return links }
|
if (!myLibraryLinks) { return links }
|
||||||
if (!Array.isArray(myLibraryLinks.links)) { return links }
|
if (!Array.isArray(myLibraryLinks.links)) { return links }
|
||||||
|
|
||||||
const label = await peertubeHelpers.translate(LOC_MENU_CONFIGURATION_LABEL)
|
const label = await peertubeHelpers.translate(LOC_MENU_CONFIGURATION_LABEL)
|
||||||
myLibraryLinks.links.push({
|
myLibraryLinks.links.unshift({
|
||||||
label,
|
label,
|
||||||
shortLabel: label,
|
shortLabel: label,
|
||||||
path: '/p/livechat/configuration',
|
path: '/p/livechat/configuration',
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
||||||
import type {
|
import type {
|
||||||
ChannelLiveChatInfos, ChannelConfiguration, ChannelConfigurationOptions, ChannelEmojisConfiguration, ChannelEmojis
|
ChannelLiveChatInfos, ChannelConfiguration, ChannelConfigurationOptions, ChannelEmojisConfiguration, ChannelEmojis,
|
||||||
|
CustomEmojiDefinition
|
||||||
} from 'shared/lib/types'
|
} from 'shared/lib/types'
|
||||||
import { ValidationError, ValidationErrorType } from '../../lib/models/validation'
|
import { ValidationError, ValidationErrorType } from '../../lib/models/validation'
|
||||||
import { getBaseRoute } from '../../../utils/uri'
|
import { getBaseRoute } from '../../../utils/uri'
|
||||||
|
import { maxEmojisPerChannel } from 'shared/lib/emojis'
|
||||||
|
import { channelTermsMaxLength, noDuplicateMaxDelay, forbidSpecialCharsMaxTolerance } from 'shared/lib/constants'
|
||||||
|
|
||||||
export class ChannelDetailsService {
|
export class ChannelDetailsService {
|
||||||
public _registerClientOptions: RegisterClientOptions
|
public _registerClientOptions: RegisterClientOptions
|
||||||
@ -25,10 +28,16 @@ export class ChannelDetailsService {
|
|||||||
validateOptions = async (channelConfigurationOptions: ChannelConfigurationOptions): Promise<boolean> => {
|
validateOptions = async (channelConfigurationOptions: ChannelConfigurationOptions): Promise<boolean> => {
|
||||||
const propertiesError: ValidationError['properties'] = {}
|
const propertiesError: ValidationError['properties'] = {}
|
||||||
|
|
||||||
|
if (channelConfigurationOptions.terms && channelConfigurationOptions.terms.length > channelTermsMaxLength) {
|
||||||
|
propertiesError.terms = [ValidationErrorType.TooLong]
|
||||||
|
}
|
||||||
|
|
||||||
const botConf = channelConfigurationOptions.bot
|
const botConf = channelConfigurationOptions.bot
|
||||||
const slowModeDuration = channelConfigurationOptions.slowMode.duration
|
const slowModeDuration = channelConfigurationOptions.slowMode.duration
|
||||||
|
const moderationDelay = channelConfigurationOptions.moderation.delay
|
||||||
|
|
||||||
propertiesError['slowMode.duration'] = []
|
propertiesError['slowMode.duration'] = []
|
||||||
|
propertiesError['moderation.delay'] = []
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(typeof slowModeDuration !== 'number') ||
|
(typeof slowModeDuration !== 'number') ||
|
||||||
@ -42,15 +51,59 @@ export class ChannelDetailsService {
|
|||||||
propertiesError['slowMode.duration'].push(ValidationErrorType.NotInRange)
|
propertiesError['slowMode.duration'].push(ValidationErrorType.NotInRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(typeof moderationDelay !== 'number') ||
|
||||||
|
isNaN(moderationDelay)
|
||||||
|
) {
|
||||||
|
propertiesError['moderation.delay'].push(ValidationErrorType.WrongType)
|
||||||
|
} else if (
|
||||||
|
moderationDelay < 0 ||
|
||||||
|
moderationDelay > 60
|
||||||
|
) {
|
||||||
|
propertiesError['moderation.delay'].push(ValidationErrorType.NotInRange)
|
||||||
|
}
|
||||||
|
|
||||||
// If !bot.enabled, we don't have to validate these fields:
|
// If !bot.enabled, we don't have to validate these fields:
|
||||||
// The backend will ignore those values.
|
// The backend will ignore those values.
|
||||||
if (botConf.enabled) {
|
if (botConf.enabled) {
|
||||||
propertiesError['bot.nickname'] = []
|
propertiesError['bot.nickname'] = []
|
||||||
|
propertiesError['bot.forbidSpecialChars.tolerance'] = []
|
||||||
|
propertiesError['bot.noDuplicate.delay'] = []
|
||||||
|
|
||||||
if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) {
|
if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) {
|
||||||
propertiesError['bot.nickname'].push(ValidationErrorType.WrongFormat)
|
propertiesError['bot.nickname'].push(ValidationErrorType.WrongFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (botConf.forbidSpecialChars.enabled) {
|
||||||
|
const forbidSpecialCharsTolerance = channelConfigurationOptions.bot.forbidSpecialChars.tolerance
|
||||||
|
if (
|
||||||
|
(typeof forbidSpecialCharsTolerance !== 'number') ||
|
||||||
|
isNaN(forbidSpecialCharsTolerance)
|
||||||
|
) {
|
||||||
|
propertiesError['bot.forbidSpecialChars.tolerance'].push(ValidationErrorType.WrongType)
|
||||||
|
} else if (
|
||||||
|
forbidSpecialCharsTolerance < 0 ||
|
||||||
|
forbidSpecialCharsTolerance > forbidSpecialCharsMaxTolerance
|
||||||
|
) {
|
||||||
|
propertiesError['bot.forbidSpecialChars.tolerance'].push(ValidationErrorType.NotInRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (botConf.noDuplicate.enabled) {
|
||||||
|
const noDuplicateDelay = channelConfigurationOptions.bot.noDuplicate.delay
|
||||||
|
if (
|
||||||
|
(typeof noDuplicateDelay !== 'number') ||
|
||||||
|
isNaN(noDuplicateDelay)
|
||||||
|
) {
|
||||||
|
propertiesError['bot.noDuplicate.delay'].push(ValidationErrorType.WrongType)
|
||||||
|
} else if (
|
||||||
|
noDuplicateDelay < 0 ||
|
||||||
|
noDuplicateDelay > noDuplicateMaxDelay
|
||||||
|
) {
|
||||||
|
propertiesError['bot.noDuplicate.delay'].push(ValidationErrorType.NotInRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [i, fw] of botConf.forbiddenWords.entries()) {
|
for (const [i, fw] of botConf.forbiddenWords.entries()) {
|
||||||
for (const v of fw.entries) {
|
for (const v of fw.entries) {
|
||||||
propertiesError[`bot.forbiddenWords.${i}.entries`] = []
|
propertiesError[`bot.forbiddenWords.${i}.entries`] = []
|
||||||
@ -68,6 +121,15 @@ export class ChannelDetailsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const [i, q] of botConf.quotes.entries()) {
|
||||||
|
propertiesError[`bot.quotes.${i}.delay`] = []
|
||||||
|
if ((typeof q.delay !== 'number') || isNaN(q.delay)) {
|
||||||
|
propertiesError[`bot.quotes.${i}.delay`].push(ValidationErrorType.WrongFormat)
|
||||||
|
} else if (q.delay <= 0) {
|
||||||
|
propertiesError[`bot.quotes.${i}.delay`].push(ValidationErrorType.NotInRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [i, cd] of botConf.commands.entries()) {
|
for (const [i, cd] of botConf.commands.entries()) {
|
||||||
propertiesError[`bot.commands.${i}.command`] = []
|
propertiesError[`bot.commands.${i}.command`] = []
|
||||||
|
|
||||||
@ -89,6 +151,29 @@ export class ChannelDetailsService {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frontToBack = (channelConfigurationOptions: ChannelConfigurationOptions): ChannelConfigurationOptions => {
|
||||||
|
// // This is a dirty hack, because backend wants seconds for botConf.quotes.delay, and front wants minutes.
|
||||||
|
const c = JSON.parse(JSON.stringify(channelConfigurationOptions)) as ChannelConfigurationOptions // clone
|
||||||
|
c.bot?.quotes.forEach(q => {
|
||||||
|
if (typeof q.delay === 'number') {
|
||||||
|
q.delay = Math.round(q.delay * 60)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
backToFront = (channelConfiguration: any): ChannelConfiguration => {
|
||||||
|
// This is a dirty hack, because backend wants seconds for botConf.quotes.delay, and front wants minutes.
|
||||||
|
const c = JSON.parse(JSON.stringify(channelConfiguration)) as ChannelConfiguration // clone
|
||||||
|
c.configuration.bot?.quotes.forEach(q => {
|
||||||
|
if (typeof q.delay === 'number') {
|
||||||
|
q.delay = Math.round(q.delay / 60)
|
||||||
|
if (q.delay < 1) { q.delay = 1 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
saveOptions = async (channelId: number,
|
saveOptions = async (channelId: number,
|
||||||
channelConfigurationOptions: ChannelConfigurationOptions): Promise<Response> => {
|
channelConfigurationOptions: ChannelConfigurationOptions): Promise<Response> => {
|
||||||
if (!await this.validateOptions(channelConfigurationOptions)) {
|
if (!await this.validateOptions(channelConfigurationOptions)) {
|
||||||
@ -100,11 +185,21 @@ export class ChannelDetailsService {
|
|||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this._headers,
|
headers: this._headers,
|
||||||
body: JSON.stringify(channelConfigurationOptions)
|
body: JSON.stringify(
|
||||||
|
this.frontToBack(channelConfigurationOptions)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
let e
|
||||||
|
try {
|
||||||
|
// checking if there are some json data in the response, with custom error message.
|
||||||
|
e = await response.json()
|
||||||
|
} catch (_err) {}
|
||||||
|
if (e?.validationErrorMessage && (typeof e.validationErrorMessage === 'string')) {
|
||||||
|
throw new Error('Failed to save configuration options: ' + e.validationErrorMessage)
|
||||||
|
}
|
||||||
throw new Error('Failed to save configuration options.')
|
throw new Error('Failed to save configuration options.')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +220,8 @@ export class ChannelDetailsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const channel of channels.data) {
|
for (const channel of channels.data) {
|
||||||
channel.livechatConfigurationUri = '/p/livechat/configuration/channel?channelId=' + encodeURIComponent(channel.id)
|
channel.livechatConfigurationUri =
|
||||||
|
'/p/livechat/configuration/channel?channelId=' + encodeURIComponent(channel.id as string | number)
|
||||||
|
|
||||||
// Note: since Peertube v6.0.0, channel.avatar is dropped, and we have to use channel.avatars.
|
// Note: since Peertube v6.0.0, channel.avatar is dropped, and we have to use channel.avatars.
|
||||||
// So, if !channel.avatar, we will search a suitable one in channel.avatars, and fill channel.avatar.
|
// So, if !channel.avatar, we will search a suitable one in channel.avatars, and fill channel.avatar.
|
||||||
@ -155,14 +251,15 @@ export class ChannelDetailsService {
|
|||||||
throw new Error('Can\'t get channel configuration options.')
|
throw new Error('Can\'t get channel configuration options.')
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json()
|
return this.backToFront(await response.json())
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fetchEmojisConfiguration (channelId: number): Promise<ChannelEmojisConfiguration> {
|
public async fetchEmojisConfiguration (channelId: number): Promise<ChannelEmojisConfiguration> {
|
||||||
const response = await fetch(
|
const url = getBaseRoute(this._registerClientOptions) +
|
||||||
getBaseRoute(this._registerClientOptions) +
|
|
||||||
'/api/configuration/channel/emojis/' +
|
'/api/configuration/channel/emojis/' +
|
||||||
encodeURIComponent(channelId),
|
encodeURIComponent(channelId)
|
||||||
|
const response = await fetch(
|
||||||
|
url,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: this._headers
|
headers: this._headers
|
||||||
@ -179,12 +276,22 @@ export class ChannelDetailsService {
|
|||||||
public async validateEmojisConfiguration (channelEmojis: ChannelEmojis): Promise<boolean> {
|
public async validateEmojisConfiguration (channelEmojis: ChannelEmojis): Promise<boolean> {
|
||||||
const propertiesError: ValidationError['properties'] = {}
|
const propertiesError: ValidationError['properties'] = {}
|
||||||
|
|
||||||
|
if (channelEmojis.customEmojis.length > maxEmojisPerChannel) {
|
||||||
|
// This can happen when using the import function.
|
||||||
|
const validationError = new ValidationError(
|
||||||
|
'ChannelEmojisValidationError',
|
||||||
|
await this._registerClientOptions.peertubeHelpers.translate(LOC_TOO_MANY_ENTRIES),
|
||||||
|
propertiesError
|
||||||
|
)
|
||||||
|
throw validationError
|
||||||
|
}
|
||||||
|
|
||||||
const seen = new Map<string, true>()
|
const seen = new Map<string, true>()
|
||||||
for (const [i, e] of channelEmojis.customEmojis.entries()) {
|
for (const [i, e] of channelEmojis.customEmojis.entries()) {
|
||||||
propertiesError[`emojis.${i}.sn`] = []
|
propertiesError[`emojis.${i}.sn`] = []
|
||||||
if (e.sn === '') {
|
if (e.sn === '') {
|
||||||
propertiesError[`emojis.${i}.sn`].push(ValidationErrorType.Missing)
|
propertiesError[`emojis.${i}.sn`].push(ValidationErrorType.Missing)
|
||||||
} else if (!/^:[\w-]+:$/.test(e.sn)) {
|
} else if (!/^:?[\w-]+:?$/.test(e.sn)) { // optional ':' at the beggining and at the end
|
||||||
propertiesError[`emojis.${i}.sn`].push(ValidationErrorType.WrongFormat)
|
propertiesError[`emojis.${i}.sn`].push(ValidationErrorType.WrongFormat)
|
||||||
} else if (seen.has(e.sn)) {
|
} else if (seen.has(e.sn)) {
|
||||||
propertiesError[`emojis.${i}.sn`].push(ValidationErrorType.Duplicate)
|
propertiesError[`emojis.${i}.sn`].push(ValidationErrorType.Duplicate)
|
||||||
@ -213,15 +320,62 @@ export class ChannelDetailsService {
|
|||||||
public async saveEmojisConfiguration (
|
public async saveEmojisConfiguration (
|
||||||
channelId: number,
|
channelId: number,
|
||||||
channelEmojis: ChannelEmojis
|
channelEmojis: ChannelEmojis
|
||||||
): Promise<void> {
|
): Promise<ChannelEmojisConfiguration> {
|
||||||
if (!await this.validateEmojisConfiguration(channelEmojis)) {
|
if (!await this.validateEmojisConfiguration(channelEmojis)) {
|
||||||
throw new Error('Invalid form data')
|
throw new Error('Invalid form data')
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
// Note: API request body size is limited to 100Kb (expressjs body-parser defaut limit, and Peertube nginx config).
|
||||||
getBaseRoute(this._registerClientOptions) +
|
// So we must send new emojis 1 by 1, to be sure to not reach the limit.
|
||||||
|
if (!channelEmojis.customEmojis.find(e => e.url.startsWith('data:'))) {
|
||||||
|
// No new emojis, just saving.
|
||||||
|
return this._saveEmojisConfiguration(channelId, channelEmojis)
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastResult: ChannelEmojisConfiguration | undefined
|
||||||
|
let customEmojis: CustomEmojiDefinition[] = [...channelEmojis.customEmojis] // copy the original array
|
||||||
|
let i = customEmojis.findIndex(e => e.url.startsWith('data:'))
|
||||||
|
let watchDog = 0
|
||||||
|
while (i >= 0) {
|
||||||
|
watchDog++
|
||||||
|
if (watchDog > channelEmojis.customEmojis.length + 10) { // just to avoid infinite loop
|
||||||
|
throw new Error('Seems we have sent too many emojis, this was not expected')
|
||||||
|
}
|
||||||
|
const data: CustomEmojiDefinition[] = customEmojis.slice(0, i + 1) // all elements until first new file
|
||||||
|
data.push(
|
||||||
|
// all remaining elements that where already uploaded (to not loose them):
|
||||||
|
...customEmojis.slice(i + 1).filter((e) => !e.url.startsWith('data:'))
|
||||||
|
)
|
||||||
|
lastResult = await this._saveEmojisConfiguration(channelId, {
|
||||||
|
customEmojis: data
|
||||||
|
})
|
||||||
|
|
||||||
|
// Must inject the result in customEmojis
|
||||||
|
const temp = lastResult.emojis.customEmojis.slice(0, i + 1) // last element should have been replace by a http url
|
||||||
|
temp.push(
|
||||||
|
...customEmojis.slice(i + 1) // remaining elements in the previous array
|
||||||
|
)
|
||||||
|
customEmojis = temp
|
||||||
|
|
||||||
|
// and searching again next new emojis
|
||||||
|
i = customEmojis.findIndex(e => e.url.startsWith('data:'))
|
||||||
|
}
|
||||||
|
if (!lastResult) {
|
||||||
|
// This should not happen...
|
||||||
|
throw new Error('Unexpected: no last result')
|
||||||
|
}
|
||||||
|
return lastResult
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _saveEmojisConfiguration (
|
||||||
|
channelId: number,
|
||||||
|
channelEmojis: ChannelEmojis
|
||||||
|
): Promise<ChannelEmojisConfiguration> {
|
||||||
|
const url = getBaseRoute(this._registerClientOptions) +
|
||||||
'/api/configuration/channel/emojis/' +
|
'/api/configuration/channel/emojis/' +
|
||||||
encodeURIComponent(channelId),
|
encodeURIComponent(channelId)
|
||||||
|
const response = await fetch(
|
||||||
|
url,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this._headers,
|
headers: this._headers,
|
||||||
@ -230,10 +384,29 @@ export class ChannelDetailsService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 404) {
|
|
||||||
// File does not exist yet, that is a normal use case.
|
|
||||||
}
|
|
||||||
throw new Error('Can\'t get channel emojis options.')
|
throw new Error('Can\'t get channel emojis options.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
public async enableEmojisOnlyModeOnAllRooms (channelId: number): Promise<void> {
|
||||||
|
const url = getBaseRoute(this._registerClientOptions) +
|
||||||
|
'/api/configuration/channel/emojis/' +
|
||||||
|
encodeURIComponent(channelId) +
|
||||||
|
'/enable_emoji_only'
|
||||||
|
const response = await fetch(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: this._headers
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Can\'t enable Emojis Only Mode on all rooms.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
|
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
|
||||||
export const AddSVG: string =
|
export const AddSVG =
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
stroke-linejoin="round" class="feather feather-plus-square">
|
stroke-linejoin="round" class="feather feather-plus-square">
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
@ -13,8 +14,9 @@ export const AddSVG: string =
|
|||||||
</svg>`
|
</svg>`
|
||||||
|
|
||||||
// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/
|
// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/
|
||||||
export const RemoveSVG: string =
|
export const RemoveSVG =
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
stroke-linejoin="round" class="feather feather-x-square">
|
stroke-linejoin="round" class="feather feather-x-square">
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
|
@ -39,9 +39,7 @@ export class PtContext {
|
|||||||
* Keep them in cache after first request.
|
* Keep them in cache after first request.
|
||||||
*/
|
*/
|
||||||
public async getSettings (): Promise<LiveChatSettings> {
|
public async getSettings (): Promise<LiveChatSettings> {
|
||||||
if (!this._settings) {
|
this._settings ??= await this.ptOptions.peertubeHelpers.getSettings() as LiveChatSettings
|
||||||
this._settings = await this.ptOptions.peertubeHelpers.getSettings() as LiveChatSettings
|
|
||||||
}
|
|
||||||
return this._settings
|
return this._settings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@ import { getPtContext } from '../contexts/peertube'
|
|||||||
export class TranslationDirective extends AsyncDirective {
|
export class TranslationDirective extends AsyncDirective {
|
||||||
private readonly _peertubeHelpers: RegisterClientHelpers
|
private readonly _peertubeHelpers: RegisterClientHelpers
|
||||||
|
|
||||||
private _translatedValue: string = ''
|
private _translatedValue = ''
|
||||||
private _localizationId: string = ''
|
private _localizationId = ''
|
||||||
|
|
||||||
private _allowUnsafeHTML = false
|
private _allowUnsafeHTML = false
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ export class TranslationDirective extends AsyncDirective {
|
|||||||
this._asyncUpdateTranslation().then(() => {}, () => {})
|
this._asyncUpdateTranslation().then(() => {}, () => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
public override render = (locId: string, allowHTML: boolean = false): TemplateResult | string => {
|
public override render = (locId: string, allowHTML = false): TemplateResult | string => {
|
||||||
this._localizationId = locId // TODO Check current component for context (to infer the prefix)
|
this._localizationId = locId // TODO Check current component for context (to infer the prefix)
|
||||||
|
|
||||||
this._allowUnsafeHTML = allowHTML
|
this._allowUnsafeHTML = allowHTML
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import { ptTr } from '../directives/translation'
|
import { ptTr } from '../directives/translation'
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
import { customElement, property } from 'lit/decorators.js'
|
import { customElement, property } from 'lit/decorators.js'
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import type { TagsInputElement } from './tags-input'
|
import type { TagsInputElement } from './tags-input'
|
||||||
import type { DirectiveResult } from 'lit/directive'
|
import type { DirectiveResult } from 'lit/directive'
|
||||||
import { ValidationErrorType } from '../models/validation'
|
import { ValidationErrorType } from '../models/validation'
|
||||||
@ -47,11 +50,11 @@ interface CellDataSchema {
|
|||||||
minlength?: number
|
minlength?: number
|
||||||
maxlength?: number
|
maxlength?: number
|
||||||
size?: number
|
size?: number
|
||||||
label?: TemplateResult | string
|
options?: Record<string, string>
|
||||||
options?: { [key: string]: string }
|
|
||||||
datalist?: DynamicTableAcceptedTypes[]
|
datalist?: DynamicTableAcceptedTypes[]
|
||||||
separator?: string
|
separator?: string
|
||||||
inputType?: DynamicTableAcceptedInputTypes
|
inputType?: DynamicTableAcceptedInputTypes
|
||||||
|
inputTitle?: string
|
||||||
default?: DynamicTableAcceptedTypes
|
default?: DynamicTableAcceptedTypes
|
||||||
colClassList?: string[] // CSS classes to add to the <td> element.
|
colClassList?: string[] // CSS classes to add to the <td> element.
|
||||||
}
|
}
|
||||||
@ -59,19 +62,17 @@ interface CellDataSchema {
|
|||||||
interface DynamicTableRowData {
|
interface DynamicTableRowData {
|
||||||
_id: number
|
_id: number
|
||||||
_originalIndex: number
|
_originalIndex: number
|
||||||
row: { [key: string]: DynamicTableAcceptedTypes }
|
row: Record<string, DynamicTableAcceptedTypes>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DynamicFormHeaderCellData {
|
interface DynamicFormHeaderCellData {
|
||||||
colName: TemplateResult | DirectiveResult
|
colName: TemplateResult | DirectiveResult
|
||||||
description: TemplateResult | DirectiveResult
|
description?: TemplateResult | DirectiveResult
|
||||||
headerClassList?: string[]
|
headerClassList?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DynamicFormHeader {
|
export type DynamicFormHeader = Record<string, DynamicFormHeaderCellData>
|
||||||
[key: string]: DynamicFormHeaderCellData
|
export type DynamicFormSchema = Record<string, CellDataSchema>
|
||||||
}
|
|
||||||
export interface DynamicFormSchema { [key: string]: CellDataSchema }
|
|
||||||
|
|
||||||
@customElement('livechat-dynamic-table-form')
|
@customElement('livechat-dynamic-table-form')
|
||||||
export class DynamicTableFormElement extends LivechatElement {
|
export class DynamicTableFormElement extends LivechatElement {
|
||||||
@ -85,19 +86,19 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
public maxLines?: number = undefined
|
public maxLines?: number = undefined
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
public validation?: {[key: string]: ValidationErrorType[] }
|
public validation?: Record<string, ValidationErrorType[]>
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public validationPrefix: string = ''
|
public validationPrefix = ''
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public rows: Array<{ [key: string]: DynamicTableAcceptedTypes }> = []
|
public rows: Array<Record<string, DynamicTableAcceptedTypes>> = []
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
public _rowsById: DynamicTableRowData[] = []
|
public _rowsById: DynamicTableRowData[] = []
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public formName: string = ''
|
public formName = ''
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private _lastRowId = 1
|
private _lastRowId = 1
|
||||||
@ -112,7 +113,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly _getDefaultRow = (): { [key: string]: DynamicTableAcceptedTypes } => {
|
private readonly _getDefaultRow = (): Record<string, DynamicTableAcceptedTypes> => {
|
||||||
this._updateLastRowId()
|
this._updateLastRowId()
|
||||||
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? ''])])
|
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? ''])])
|
||||||
}
|
}
|
||||||
@ -236,7 +237,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
classList.push(...headerCellData.headerClassList)
|
classList.push(...headerCellData.headerClassList)
|
||||||
}
|
}
|
||||||
return html`<th scope="col" class=${classList.join(' ')}>
|
return html`<th scope="col" class=${classList.join(' ')}>
|
||||||
${headerCellData.description}
|
${headerCellData.description ?? ''}
|
||||||
</th>`
|
</th>`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,6 +296,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
const inputId =
|
const inputId =
|
||||||
`peertube-livechat-${this.formName.replace(/_/g, '-')}-${propertyName.toString().replace(/_/g, '-')}-${rowId}`
|
`peertube-livechat-${this.formName.replace(/_/g, '-')}-${propertyName.toString().replace(/_/g, '-')}-${rowId}`
|
||||||
|
|
||||||
|
const inputTitle: DirectiveResult | undefined = propertySchema.inputTitle ?? this.header[propertyName]?.colName
|
||||||
const feedback = this._renderFeedback(inputId, propertyName, originalIndex)
|
const feedback = this._renderFeedback(inputId, propertyName, originalIndex)
|
||||||
|
|
||||||
switch (propertySchema.default?.constructor) {
|
switch (propertySchema.default?.constructor) {
|
||||||
@ -320,6 +322,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderInput(rowId,
|
formElement = html`${this._renderInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue as string,
|
propertyValue as string,
|
||||||
@ -332,6 +335,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderTextarea(rowId,
|
formElement = html`${this._renderTextarea(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue as string,
|
propertyValue as string,
|
||||||
@ -344,6 +348,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderSelect(rowId,
|
formElement = html`${this._renderSelect(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue as string,
|
propertyValue as string,
|
||||||
@ -356,6 +361,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderImageFileInput(rowId,
|
formElement = html`${this._renderImageFileInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue?.toString(),
|
propertyValue?.toString(),
|
||||||
@ -376,6 +382,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderInput(rowId,
|
formElement = html`${this._renderInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
(propertyValue as Date).toISOString(),
|
(propertyValue as Date).toISOString(),
|
||||||
@ -394,6 +401,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderInput(rowId,
|
formElement = html`${this._renderInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue as string,
|
propertyValue as string,
|
||||||
@ -411,6 +419,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderCheckbox(rowId,
|
formElement = html`${this._renderCheckbox(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue as boolean,
|
propertyValue as boolean,
|
||||||
@ -446,10 +455,10 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderInput(rowId,
|
formElement = html`${this._renderInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
(propertyValue)?.join(propertySchema.separator ?? ',') ??
|
(propertyValue)?.join(propertySchema.separator ?? ',') ?? propertyValue ?? propertySchema.default ?? '',
|
||||||
propertyValue ?? propertySchema.default ?? '',
|
|
||||||
originalIndex)}
|
originalIndex)}
|
||||||
${feedback}
|
${feedback}
|
||||||
`
|
`
|
||||||
@ -461,10 +470,10 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderTextarea(rowId,
|
formElement = html`${this._renderTextarea(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
(propertyValue)?.join(propertySchema.separator ?? ',') ??
|
(propertyValue)?.join(propertySchema.separator ?? ',') ?? propertyValue ?? propertySchema.default ?? '',
|
||||||
propertyValue ?? propertySchema.default ?? '',
|
|
||||||
originalIndex)}
|
originalIndex)}
|
||||||
${feedback}
|
${feedback}
|
||||||
`
|
`
|
||||||
@ -476,6 +485,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderTagsInput(rowId,
|
formElement = html`${this._renderTagsInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue,
|
propertyValue,
|
||||||
@ -487,8 +497,10 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!formElement) {
|
if (!formElement) {
|
||||||
this.logger.warn(`value type '${(propertyValue.constructor.toString())}' is incompatible` +
|
this.logger.warn(
|
||||||
`with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`)
|
`value type '${(propertyValue.constructor.toString())}' is incompatible` +
|
||||||
|
`with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const classList = ['form-group']
|
const classList = ['form-group']
|
||||||
@ -501,6 +513,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderInput = (rowId: number,
|
_renderInput = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
|
inputTitle: string | DirectiveResult | undefined,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: string,
|
propertyValue: string,
|
||||||
@ -515,6 +528,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
|
title=${ifDefined(inputTitle)}
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
list=${ifDefined(propertySchema.datalist ? inputId + '-datalist' : undefined)}
|
list=${ifDefined(propertySchema.datalist ? inputId + '-datalist' : undefined)}
|
||||||
min=${ifDefined(propertySchema.min)}
|
min=${ifDefined(propertySchema.min)}
|
||||||
@ -534,6 +548,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderTagsInput = (rowId: number,
|
_renderTagsInput = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
|
inputTitle: string | DirectiveResult | undefined,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: Array<string | number>,
|
propertyValue: Array<string | number>,
|
||||||
@ -547,7 +562,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
.inputPlaceholder=${propertySchema.label as any}
|
.inputTitle=${inputTitle as any}
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
.min=${propertySchema.min}
|
.min=${propertySchema.min}
|
||||||
.max=${propertySchema.max}
|
.max=${propertySchema.max}
|
||||||
@ -563,6 +578,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderTextarea = (rowId: number,
|
_renderTextarea = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
|
inputTitle: string | DirectiveResult | undefined,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: string,
|
propertyValue: string,
|
||||||
@ -576,6 +592,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
|
title=${ifDefined(inputTitle)}
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
min=${ifDefined(propertySchema.min)}
|
min=${ifDefined(propertySchema.min)}
|
||||||
max=${ifDefined(propertySchema.max)}
|
max=${ifDefined(propertySchema.max)}
|
||||||
@ -588,6 +605,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderCheckbox = (rowId: number,
|
_renderCheckbox = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
|
inputTitle: string | DirectiveResult | undefined,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: boolean,
|
propertyValue: boolean,
|
||||||
@ -602,6 +620,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
|
title=${ifDefined(inputTitle)}
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
||||||
value="1"
|
value="1"
|
||||||
@ -611,6 +630,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderSelect = (rowId: number,
|
_renderSelect = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
|
inputTitle: string | DirectiveResult | undefined,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: string,
|
propertyValue: string,
|
||||||
@ -623,11 +643,12 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
|
title=${ifDefined(inputTitle)}
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
aria-label=${inputName}
|
aria-label=${inputName}
|
||||||
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
||||||
>
|
>
|
||||||
<option ?selected=${!propertyValue}>${propertySchema.label ?? 'Choose your option'}</option>
|
<option ?selected=${!propertyValue}>${inputTitle ?? ''}</option>
|
||||||
${Object.entries(propertySchema.options ?? {})
|
${Object.entries(propertySchema.options ?? {})
|
||||||
?.map(([value, name]) =>
|
?.map(([value, name]) =>
|
||||||
html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`
|
html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`
|
||||||
@ -638,6 +659,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderImageFileInput = (rowId: number,
|
_renderImageFileInput = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
|
inputTitle: string | DirectiveResult | undefined,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: string,
|
propertyValue: string,
|
||||||
@ -647,6 +669,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
.name=${inputName}
|
.name=${inputName}
|
||||||
class=${classMap(this._getInputValidationClass(propertyName, originalIndex))}
|
class=${classMap(this._getInputValidationClass(propertyName, originalIndex))}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
|
.inputTitle=${inputTitle as any}
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
||||||
.value=${propertyValue}
|
.value=${propertyValue}
|
||||||
@ -656,7 +679,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getInputValidationClass = (propertyName: string,
|
_getInputValidationClass = (propertyName: string,
|
||||||
originalIndex: number): { [key: string]: boolean } => {
|
originalIndex: number): Record<string, boolean> => {
|
||||||
const validationErrorTypes: ValidationErrorType[] | undefined =
|
const validationErrorTypes: ValidationErrorType[] | undefined =
|
||||||
this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`]
|
this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`]
|
||||||
|
|
||||||
@ -672,6 +695,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
const validationErrorTypes: ValidationErrorType[] | undefined =
|
const validationErrorTypes: ValidationErrorType[] | undefined =
|
||||||
this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`]
|
this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`]
|
||||||
|
|
||||||
|
// FIXME: this code is duplicated in channel-configuration
|
||||||
if (validationErrorTypes !== undefined && validationErrorTypes.length !== 0) {
|
if (validationErrorTypes !== undefined && validationErrorTypes.length !== 0) {
|
||||||
if (validationErrorTypes.includes(ValidationErrorType.Missing)) {
|
if (validationErrorTypes.includes(ValidationErrorType.Missing)) {
|
||||||
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_MISSING)}`)
|
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_MISSING)}`)
|
||||||
@ -688,6 +712,9 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
if (validationErrorTypes.includes(ValidationErrorType.Duplicate)) {
|
if (validationErrorTypes.includes(ValidationErrorType.Duplicate)) {
|
||||||
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_DUPLICATE)}`)
|
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_DUPLICATE)}`)
|
||||||
}
|
}
|
||||||
|
if (validationErrorTypes.includes(ValidationErrorType.TooLong)) {
|
||||||
|
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_TOO_LONG)}`)
|
||||||
|
}
|
||||||
|
|
||||||
return html`<div id="${inputId}-feedback" class="invalid-feedback">${errorMessages}</div>`
|
return html`<div id="${inputId}-feedback" class="invalid-feedback">${errorMessages}</div>`
|
||||||
} else {
|
} else {
|
||||||
@ -726,6 +753,9 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
.split(propertySchema.separator)
|
.split(propertySchema.separator)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case Number:
|
||||||
|
rowById.row[propertyName] = Number(value)
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
rowById.row[propertyName] = value
|
rowById.row[propertyName] = value
|
||||||
break
|
break
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ export class HelpButtonElement extends LivechatElement {
|
|||||||
public buttonTitle: string | DirectiveResult = ptTr(LOC_ONLINE_HELP)
|
public buttonTitle: string | DirectiveResult = ptTr(LOC_ONLINE_HELP)
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public page: string = ''
|
public page = ''
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
public url: URL = new URL('https://lmddgtfy.net/')
|
public url: URL = new URL('https://lmddgtfy.net/')
|
||||||
@ -38,7 +38,7 @@ export class HelpButtonElement extends LivechatElement {
|
|||||||
href="${this.url.href}"
|
href="${this.url.href}"
|
||||||
target=_blank
|
target=_blank
|
||||||
title="${this.buttonTitle}"
|
title="${this.buttonTitle}"
|
||||||
class="orange-button peertube-button-link"
|
class="primary-button orange-button peertube-button-link"
|
||||||
>${unsafeHTML(helpButtonSVG())}</a>`
|
>${unsafeHTML(helpButtonSVG())}</a>`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import { LivechatElement } from './livechat'
|
import { LivechatElement } from './livechat'
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
|
import type { DirectiveResult } from 'lit/directive'
|
||||||
import { customElement, property } from 'lit/decorators.js'
|
import { customElement, property } from 'lit/decorators.js'
|
||||||
|
import { ifDefined } from 'lit/directives/if-defined.js'
|
||||||
/**
|
/**
|
||||||
* Special element to upload image files.
|
* Special element to upload image files.
|
||||||
* If no current value, displays an input type="file" field.
|
* If no current value, displays an input type="file" field.
|
||||||
@ -29,13 +33,16 @@ export class ImageFileInputElement extends LivechatElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public maxSize?: number
|
public maxSize?: number
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public inputTitle?: string | DirectiveResult
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public accept: string[] = ['image/jpg', 'image/png', 'image/gif']
|
public accept: string[] = ['image/jpg', 'image/png', 'image/gif']
|
||||||
|
|
||||||
protected override render = (): unknown => {
|
protected override render = (): unknown => {
|
||||||
return html`
|
return html`
|
||||||
${this.value
|
${this.value
|
||||||
? html`<img src=${this.value} @click=${(ev: Event) => {
|
? html`<img src=${this.value} alt=${ifDefined(this.inputTitle)} @click=${(ev: Event) => {
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
const upload: HTMLInputElement | null | undefined = this.parentElement?.querySelector('input[type="file"]')
|
const upload: HTMLInputElement | null | undefined = this.parentElement?.querySelector('input[type="file"]')
|
||||||
upload?.click()
|
upload?.click()
|
||||||
@ -44,6 +51,7 @@ export class ImageFileInputElement extends LivechatElement {
|
|||||||
}
|
}
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
title=${ifDefined(this.inputTitle)}
|
||||||
accept="${this.accept.join(',')}"
|
accept="${this.accept.join(',')}"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
style=${this.value ? 'display: none;' : ''}
|
style=${this.value ? 'display: none;' : ''}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ export class LivechatElement extends LitElement {
|
|||||||
this.logger = this.ptContext.logger.createLogger(this.tagName.toLowerCase())
|
this.logger = this.ptContext.logger.createLogger(this.tagName.toLowerCase())
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override createRenderRoot = (): Element | ShadowRoot => {
|
protected override createRenderRoot = (): HTMLElement | DocumentFragment => {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import { LivechatElement } from './livechat'
|
import { LivechatElement } from './livechat'
|
||||||
import { ptTr } from '../directives/translation'
|
import { ptTr } from '../directives/translation'
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
@ -12,6 +15,7 @@ import { ifDefined } from 'lit/directives/if-defined.js'
|
|||||||
import { classMap } from 'lit/directives/class-map.js'
|
import { classMap } from 'lit/directives/class-map.js'
|
||||||
import { animate, fadeOut, fadeIn } from '@lit-labs/motion'
|
import { animate, fadeOut, fadeIn } from '@lit-labs/motion'
|
||||||
import { repeat } from 'lit/directives/repeat.js'
|
import { repeat } from 'lit/directives/repeat.js'
|
||||||
|
import type { DirectiveResult } from 'lit/directive'
|
||||||
|
|
||||||
// FIXME: find a better way to store this image.
|
// FIXME: find a better way to store this image.
|
||||||
// This content comes from the file assets/images/copy.svg, after svgo cleaning.
|
// This content comes from the file assets/images/copy.svg, after svgo cleaning.
|
||||||
@ -20,10 +24,11 @@ import { repeat } from 'lit/directives/repeat.js'
|
|||||||
// Then replace the main color by «currentColor»
|
// Then replace the main color by «currentColor»
|
||||||
const copySVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 4.233 4.233">
|
const copySVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 4.233 4.233">
|
||||||
<g style="stroke-width:1.00021;stroke-miterlimit:4;stroke-dasharray:none">` +
|
<g style="stroke-width:1.00021;stroke-miterlimit:4;stroke-dasharray:none">` +
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len, @stylistic/indent-binary-ops
|
||||||
'<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m4.084 4.046-.616.015-.645-.004a.942.942 0 0 1-.942-.942v-4.398a.94.94 0 0 1 .942-.943H7.22a.94.94 0 0 1 .942.943l-.006.334-.08.962" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' +
|
'<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m4.084 4.046-.616.015-.645-.004a.942.942 0 0 1-.942-.942v-4.398a.94.94 0 0 1 .942-.943H7.22a.94.94 0 0 1 .942.943l-.006.334-.08.962" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' +
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
'<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M8.434 5.85c-.422.009-1.338.009-1.76.01-.733.004-2.199 0-2.199 0a.94.94 0 0 1-.942-.941V.52a.94.94 0 0 1 .942-.942h4.398a.94.94 0 0 1 .943.942s.004 1.466 0 2.2c-.003.418-.019 1.251-.006 1.67.024.812-.382 1.439-1.376 1.46z" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' +
|
'<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M8.434 5.85c-.422.009-1.338.009-1.76.01-.733.004-2.199 0-2.199 0a.94.94 0 0 1-.942-.941V.52a.94.94 0 0 1 .942-.942h4.398a.94.94 0 0 1 .943.942s.004 1.466 0 2.2c-.003.418-.019 1.251-.006 1.67.024.812-.382 1.439-1.376 1.46z" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' +
|
||||||
|
// eslint-disable-next-line @stylistic/indent-binary-ops
|
||||||
`</g>
|
`</g>
|
||||||
</svg>`
|
</svg>`
|
||||||
|
|
||||||
@ -48,7 +53,7 @@ export class TagsInputElement extends LivechatElement {
|
|||||||
private _inputValue?: string = ''
|
private _inputValue?: string = ''
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public inputPlaceholder?: string = ''
|
public inputTitle?: string | DirectiveResult = ''
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public datalist?: string[]
|
public datalist?: string[]
|
||||||
@ -63,10 +68,10 @@ export class TagsInputElement extends LivechatElement {
|
|||||||
private readonly _isPressingKey: string[] = []
|
private readonly _isPressingKey: string[] = []
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public separator: string = '\n'
|
public separator = '\n'
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public animDuration: number = 200
|
public animDuration = 200
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overloading the standard focus method.
|
* Overloading the standard focus method.
|
||||||
@ -166,7 +171,7 @@ export class TagsInputElement extends LivechatElement {
|
|||||||
@input=${(e: InputEvent) => this._handleInputEvent(e)}
|
@input=${(e: InputEvent) => this._handleInputEvent(e)}
|
||||||
@change=${(e: Event) => e.stopPropagation()}
|
@change=${(e: Event) => e.stopPropagation()}
|
||||||
.value=${this._inputValue ?? ''}
|
.value=${this._inputValue ?? ''}
|
||||||
placeholder=${ifDefined(this.inputPlaceholder)} />
|
title=${ifDefined(this.inputTitle)} />
|
||||||
${(this.datalist)
|
${(this.datalist)
|
||||||
? html`<datalist id="${this.id ?? 'tags-input'}-datalist">
|
? html`<datalist id="${this.id ?? 'tags-input'}-datalist">
|
||||||
${(this.datalist ?? []).map((value) => html`<option value=${value}>`)}
|
${(this.datalist ?? []).map((value) => html`<option value=${value}>`)}
|
||||||
@ -244,8 +249,9 @@ export class TagsInputElement extends LivechatElement {
|
|||||||
if (!this._isPressingKey.includes(e.key)) {
|
if (!this._isPressingKey.includes(e.key)) {
|
||||||
this._isPressingKey.push(e.key)
|
this._isPressingKey.push(e.key)
|
||||||
|
|
||||||
if ((target.selectionStart === target.selectionEnd) &&
|
if (
|
||||||
target.selectionStart === 0) {
|
(target.selectionStart === target.selectionEnd) && target.selectionStart === 0
|
||||||
|
) {
|
||||||
this._handleDeleteTag((this._searchedTagsIndex.length)
|
this._handleDeleteTag((this._searchedTagsIndex.length)
|
||||||
? this._searchedTagsIndex.slice(-1)[0]
|
? this._searchedTagsIndex.slice(-1)[0]
|
||||||
: (this.value.length - 1))
|
: (this.value.length - 1))
|
||||||
@ -258,8 +264,9 @@ export class TagsInputElement extends LivechatElement {
|
|||||||
if (!this._isPressingKey.includes(e.key)) {
|
if (!this._isPressingKey.includes(e.key)) {
|
||||||
this._isPressingKey.push(e.key)
|
this._isPressingKey.push(e.key)
|
||||||
|
|
||||||
if ((target.selectionStart === target.selectionEnd) &&
|
if (
|
||||||
target.selectionStart === target.value.length) {
|
(target.selectionStart === target.selectionEnd) && target.selectionStart === target.value.length
|
||||||
|
) {
|
||||||
this._handleDeleteTag((this._searchedTagsIndex.length)
|
this._handleDeleteTag((this._searchedTagsIndex.length)
|
||||||
? this._searchedTagsIndex[0]
|
? this._searchedTagsIndex[0]
|
||||||
: 0)
|
: 0)
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import type { LivechatTokenListElement } from '../token-list'
|
import type { LivechatTokenListElement } from '../token-list'
|
||||||
import { html, TemplateResult } from 'lit'
|
import { html, TemplateResult } from 'lit'
|
||||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
|
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
|
||||||
@ -23,11 +26,11 @@ export function tplTokenList (el: LivechatTokenListElement): TemplateResult {
|
|||||||
<tbody>
|
<tbody>
|
||||||
${
|
${
|
||||||
repeat(el.tokenList ?? [], (token) => token.id, (token) => {
|
repeat(el.tokenList ?? [], (token) => token.id, (token) => {
|
||||||
let dateStr: string = ''
|
let dateStr = ''
|
||||||
try {
|
try {
|
||||||
const date = new Date(token.date)
|
const date = new Date(token.date)
|
||||||
dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
||||||
} catch (err) {}
|
} catch (_err) {}
|
||||||
return html`<tr>
|
return html`<tr>
|
||||||
<td>${
|
<td>${
|
||||||
el.mode === 'select'
|
el.mode === 'select'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ export class LivechatTokenListElement extends LivechatElement {
|
|||||||
public currentSelectedToken?: LivechatToken
|
public currentSelectedToken?: LivechatToken
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public actionDisabled: boolean = false
|
public actionDisabled = false
|
||||||
|
|
||||||
private readonly _tokenListService: TokenListService
|
private readonly _tokenListService: TokenListService
|
||||||
private readonly _asyncTaskRender: Task
|
private readonly _asyncTaskRender: Task
|
||||||
@ -83,7 +83,7 @@ export class LivechatTokenListElement extends LivechatElement {
|
|||||||
this.dispatchEvent(new CustomEvent('update', {}))
|
this.dispatchEvent(new CustomEvent('update', {}))
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error(err)
|
this.logger.error(err)
|
||||||
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR))
|
this.ptNotifier.error((err as Error).toString(), await this.ptTranslate(LOC_ERROR))
|
||||||
} finally {
|
} finally {
|
||||||
this.actionDisabled = false
|
this.actionDisabled = false
|
||||||
}
|
}
|
||||||
@ -102,7 +102,7 @@ export class LivechatTokenListElement extends LivechatElement {
|
|||||||
this.dispatchEvent(new CustomEvent('update', {}))
|
this.dispatchEvent(new CustomEvent('update', {}))
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error(err)
|
this.logger.error(err)
|
||||||
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR))
|
this.ptNotifier.error((err as Error).toString(), await this.ptTranslate(LOC_ERROR))
|
||||||
} finally {
|
} finally {
|
||||||
this.actionDisabled = false
|
this.actionDisabled = false
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,12 @@ export enum ValidationErrorType {
|
|||||||
WrongType,
|
WrongType,
|
||||||
WrongFormat,
|
WrongFormat,
|
||||||
NotInRange,
|
NotInRange,
|
||||||
Duplicate
|
Duplicate,
|
||||||
|
TooLong
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ValidationError extends Error {
|
export class ValidationError extends Error {
|
||||||
properties: {[key: string]: ValidationErrorType[] } = {}
|
properties: Record<string, ValidationErrorType[]> = {}
|
||||||
|
|
||||||
constructor (name: string, message: string | undefined, properties: ValidationError['properties']) {
|
constructor (name: string, message: string | undefined, properties: ValidationError['properties']) {
|
||||||
super(message)
|
super(message)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ type displayButtonOptions = displayButtonOptionsCallback | displayButtonOptionsH
|
|||||||
function displayButton (dbo: displayButtonOptions): void {
|
function displayButton (dbo: displayButtonOptions): void {
|
||||||
const button = document.createElement('a')
|
const button = document.createElement('a')
|
||||||
button.classList.add(
|
button.classList.add(
|
||||||
'orange-button', 'peertube-button-link',
|
'primary-button', 'orange-button', 'peertube-button-link',
|
||||||
'peertube-plugin-livechat-button',
|
'peertube-plugin-livechat-button',
|
||||||
'peertube-plugin-livechat-button-' + dbo.name
|
'peertube-plugin-livechat-button-' + dbo.name
|
||||||
)
|
)
|
||||||
@ -42,6 +42,23 @@ function displayButton (dbo: displayButtonOptions): void {
|
|||||||
if ('href' in dbo) {
|
if ('href' in dbo) {
|
||||||
button.href = dbo.href
|
button.href = dbo.href
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!button.href || button.href === '#') {
|
||||||
|
// No href => it is not a link.
|
||||||
|
button.role = 'button'
|
||||||
|
button.tabIndex = 0
|
||||||
|
|
||||||
|
// We must also ensure that the enter key is triggering the onclick
|
||||||
|
if (button.onclick) {
|
||||||
|
button.onkeydown = ev => {
|
||||||
|
if (ev.key === 'Enter') {
|
||||||
|
ev.preventDefault()
|
||||||
|
button.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (('targetBlank' in dbo) && dbo.targetBlank) {
|
if (('targetBlank' in dbo) && dbo.targetBlank) {
|
||||||
button.target = '_blank'
|
button.target = '_blank'
|
||||||
}
|
}
|
||||||
@ -52,6 +69,10 @@ function displayButton (dbo: displayButtonOptions): void {
|
|||||||
tmp.innerHTML = svg.trim()
|
tmp.innerHTML = svg.trim()
|
||||||
const svgDom = tmp.firstChild
|
const svgDom = tmp.firstChild
|
||||||
if (svgDom) {
|
if (svgDom) {
|
||||||
|
if ('ariaHidden' in (svgDom as HTMLElement)) {
|
||||||
|
// Icon must be hidden for screen readers.
|
||||||
|
(svgDom as HTMLElement).ariaHidden = 'true'
|
||||||
|
}
|
||||||
button.prepend(svgDom)
|
button.prepend(svgDom)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@ -16,8 +16,6 @@ import { localizedHelpUrl } from '../../utils/help'
|
|||||||
import { getBaseRoute } from '../../utils/uri'
|
import { getBaseRoute } from '../../utils/uri'
|
||||||
import { displayConverseJS } from '../../utils/conversejs'
|
import { displayConverseJS } from '../../utils/conversejs'
|
||||||
|
|
||||||
let savedMyPluginFlexGrow: string | undefined
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the chat for the current video
|
* Initialize the chat for the current video
|
||||||
* @param video the video
|
* @param video the video
|
||||||
@ -25,7 +23,6 @@ let savedMyPluginFlexGrow: string | undefined
|
|||||||
async function initChat (video: Video): Promise<void> {
|
async function initChat (video: Video): Promise<void> {
|
||||||
const ptContext = getPtContext()
|
const ptContext = getPtContext()
|
||||||
const logger = ptContext.logger
|
const logger = ptContext.logger
|
||||||
savedMyPluginFlexGrow = undefined
|
|
||||||
|
|
||||||
if (!video) {
|
if (!video) {
|
||||||
logger.error('No video provided')
|
logger.error('No video provided')
|
||||||
@ -46,6 +43,8 @@ async function initChat (video: Video): Promise<void> {
|
|||||||
container.setAttribute('id', 'peertube-plugin-livechat-container')
|
container.setAttribute('id', 'peertube-plugin-livechat-container')
|
||||||
container.setAttribute('peertube-plugin-livechat-state', 'initializing')
|
container.setAttribute('peertube-plugin-livechat-state', 'initializing')
|
||||||
container.setAttribute('peertube-plugin-livechat-current-url', window.location.href)
|
container.setAttribute('peertube-plugin-livechat-current-url', window.location.href)
|
||||||
|
container.role = 'region'
|
||||||
|
container.ariaLabel = await ptContext.ptOptions.peertubeHelpers.translate(LOC_CHAT)
|
||||||
placeholder.append(container)
|
placeholder.append(container)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -61,8 +60,8 @@ async function initChat (video: Video): Promise<void> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let showShareUrlButton: boolean = false
|
let showShareUrlButton = false
|
||||||
let showPromote: boolean = false
|
let showPromote = false
|
||||||
if (video.isLocal) { // No need for shareButton on remote chats.
|
if (video.isLocal) { // No need for shareButton on remote chats.
|
||||||
const chatShareUrl = settings['chat-share-url'] ?? ''
|
const chatShareUrl = settings['chat-share-url'] ?? ''
|
||||||
if (chatShareUrl === 'everyone') {
|
if (chatShareUrl === 'everyone') {
|
||||||
@ -188,9 +187,10 @@ async function _insertChatDom (
|
|||||||
callback: async () => {
|
callback: async () => {
|
||||||
try {
|
try {
|
||||||
// First we must get the room JID (can be video.uuid@ or channel.id@)
|
// First we must get the room JID (can be video.uuid@ or channel.id@)
|
||||||
|
const url = getBaseRoute(ptContext.ptOptions) + '/api/configuration/room/' +
|
||||||
|
encodeURIComponent(video.uuid)
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
getBaseRoute(ptContext.ptOptions) + '/api/configuration/room/' +
|
url,
|
||||||
encodeURIComponent(video.uuid),
|
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: peertubeHelpers.getAuthHeader()
|
headers: peertubeHelpers.getAuthHeader()
|
||||||
@ -304,7 +304,7 @@ async function _openChat (video: Video): Promise<void | false> {
|
|||||||
|
|
||||||
// Loading converseJS...
|
// Loading converseJS...
|
||||||
await displayConverseJS(ptContext.ptOptions, container, roomkey, 'peertube-video', false)
|
await displayConverseJS(ptContext.ptOptions, container, roomkey, 'peertube-video', false)
|
||||||
} catch (err) {
|
} catch (_err) {
|
||||||
// Displaying an error page.
|
// Displaying an error page.
|
||||||
if (container) {
|
if (container) {
|
||||||
const message = document.createElement('div')
|
const message = document.createElement('div')
|
||||||
@ -353,19 +353,6 @@ function _hackStyles (on: boolean): void {
|
|||||||
buttons.classList.remove('peertube-plugin-livechat-buttons-open')
|
buttons.classList.remove('peertube-plugin-livechat-buttons-open')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const myPluginPlaceholder: HTMLElement | null = document.querySelector('my-plugin-placeholder')
|
|
||||||
if (on) {
|
|
||||||
// Saving current style attributes and maximazing space for the chat
|
|
||||||
if (myPluginPlaceholder) {
|
|
||||||
savedMyPluginFlexGrow = myPluginPlaceholder.style.flexGrow // Should be "", but can be anything else.
|
|
||||||
myPluginPlaceholder.style.flexGrow = '1'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// restoring values...
|
|
||||||
if (savedMyPluginFlexGrow !== undefined && myPluginPlaceholder) {
|
|
||||||
myPluginPlaceholder.style.flexGrow = savedMyPluginFlexGrow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
getPtContext().logger.error(`Failed hacking styles: '${err as string}'`)
|
getPtContext().logger.error(`Failed hacking styles: '${err as string}'`)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ import { getIframeUri, getXMPPAddr, UriOptions } from '../uri'
|
|||||||
import { isAnonymousUser } from '../../../utils/user'
|
import { isAnonymousUser } from '../../../utils/user'
|
||||||
|
|
||||||
// First is default tab.
|
// First is default tab.
|
||||||
const validTabNames = ['embed', 'dock', 'peertube', 'xmpp'] as const
|
const validTabNames: string[] = ['embed', 'dock', 'peertube', 'xmpp'] as const
|
||||||
|
|
||||||
type ValidTabNames = typeof validTabNames[number]
|
type ValidTabNames = typeof validTabNames[number]
|
||||||
|
|
||||||
@ -61,49 +61,49 @@ export class ShareChatElement extends LivechatElement {
|
|||||||
* Should we render the XMPP tab?
|
* Should we render the XMPP tab?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public xmppUriEnabled: boolean = false
|
public xmppUriEnabled = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should we render the Dock tab?
|
* Should we render the Dock tab?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public dockEnabled: boolean = false
|
public dockEnabled = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can we use autocolors?
|
* Can we use autocolors?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public autocolorsAvailable: boolean = false
|
public autocolorsAvailable = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In the Embed tab, should we generated an iframe link.
|
* In the Embed tab, should we generated an iframe link.
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public embedIFrame: boolean = false
|
public embedIFrame = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In the Embed tab, should we generated a read-only chat link.
|
* In the Embed tab, should we generated a read-only chat link.
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public embedReadOnly: boolean = false
|
public embedReadOnly = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read-only, with scrollbar?
|
* Read-only, with scrollbar?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public embedReadOnlyScrollbar: boolean = false
|
public embedReadOnlyScrollbar = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read-only, transparent background?
|
* Read-only, transparent background?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public embedReadOnlyTransparentBackground: boolean = false
|
public embedReadOnlyTransparentBackground = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In the Embed tab, should we use current theme color?
|
* In the Embed tab, should we use current theme color?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public embedAutocolors: boolean = false
|
public embedAutocolors = false
|
||||||
|
|
||||||
protected override firstUpdated (changedProperties: PropertyValues): void {
|
protected override firstUpdated (changedProperties: PropertyValues): void {
|
||||||
super.firstUpdated(changedProperties)
|
super.firstUpdated(changedProperties)
|
||||||
@ -156,7 +156,7 @@ export class ShareChatElement extends LivechatElement {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.logger.log('Restoring previous state')
|
this.logger.log('Restoring previous state')
|
||||||
if (validTabNames.includes(v.currentTab)) {
|
if (validTabNames.includes(v.currentTab as string)) {
|
||||||
this.currentTab = v.currentTab
|
this.currentTab = v.currentTab
|
||||||
}
|
}
|
||||||
this.embedIFrame = !!v.embedIFrame
|
this.embedIFrame = !!v.embedIFrame
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import type { ShareChatElement } from '../share-chat'
|
import type { ShareChatElement } from '../share-chat'
|
||||||
import { html, TemplateResult } from 'lit'
|
import { html, TemplateResult } from 'lit'
|
||||||
import { ptTr } from '../../../lib/directives/translation'
|
import { ptTr } from '../../../lib/directives/translation'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@ -71,8 +71,7 @@ async function shareChatUrl (
|
|||||||
addedNodes.forEach(node => {
|
addedNodes.forEach(node => {
|
||||||
if ((node as HTMLElement).localName === 'ngb-modal-window') {
|
if ((node as HTMLElement).localName === 'ngb-modal-window') {
|
||||||
logger.info('Detecting a new modal, checking if this is the good one...')
|
logger.info('Detecting a new modal, checking if this is the good one...')
|
||||||
if (!(node as HTMLElement).querySelector) { return }
|
const title = (node as HTMLElement).querySelector?.('.modal-title')
|
||||||
const title = (node as HTMLElement).querySelector('.modal-title')
|
|
||||||
if (!(title?.textContent === labelShare)) {
|
if (!(title?.textContent === labelShare)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user