12 Commits

Author SHA1 Message Date
de14b95f9a better send button 2024-06-19 21:52:15 -04:00
4f80119c83 slightly edit send button style 2024-06-19 21:36:15 -04:00
80b2093202 livechat message form formatting 2024-06-19 21:00:18 -04:00
3d4afc4341 edit chat text area size 2024-06-19 20:39:46 -04:00
49a87237ec better sizing for mobile 2024-06-19 20:15:45 -04:00
0737e14472 better sizing for mobile 2024-06-19 19:52:55 -04:00
226ea38e4d better sizing for mobile 2024-06-19 19:34:40 -04:00
559fe731e0 update package json 2024-06-19 19:24:27 -04:00
e8eb56d0b7 fix dumb nigger shit 2024-06-19 19:02:36 -04:00
1b97366cd8 fix dumb nigger shit 2024-06-19 18:19:19 -04:00
1f3eee9889 better mobile device sizing 2024-06-19 17:56:09 -04:00
772c1c1d14 better sizing for mobile and desktop devices 2024-06-19 17:43:09 -04:00
569 changed files with 18316 additions and 72656 deletions

14
.eslintrc.json Normal file
View File

@ -0,0 +1,14 @@
{
"root": true,
"env": {},
"extends": [],
"globals": {},
"plugins": [],
"ignorePatterns": [
"node_modules/", "dist/", "webpack.config.js",
"build/",
"vendor/",
"support/documentation",
"build-*js"],
"rules": {}
}

3
.eslintrc.json.license Normal file
View File

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,49 +0,0 @@
# 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.

View File

@ -1,34 +0,0 @@
# 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

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> # SPDX-FileCopyrightText: 2024 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.132.2' hugo-version: '0.80.0'
extended: true extended: true
- name: Generate documentation translations - name: Generate documentation translations

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> # SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
# #
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> # SPDX-FileCopyrightText: 2024 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:0.132.2 image: registry.gitlab.com/pages/hugo/hugo_extended:latest
variables: variables:
GIT_SUBMODULE_STRATEGY: recursive GIT_SUBMODULE_STRATEGY: recursive
GIT_SUBMODULE_PATHS: support/documentation/themes/hugo-theme-relearn GIT_SUBMODULE_PATHS: support/documentation/themes/hugo-theme-learn
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
View File

@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> # SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
# #
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
[submodule "support/documentation/themes/hugo-theme-relearn"] [submodule "documentation/themes/hugo-theme-learn"]
path = support/documentation/themes/hugo-theme-relearn path = support/documentation/themes/hugo-theme-learn
url = https://github.com/McShelby/hugo-theme-relearn.git url = https://github.com/matcornic/hugo-theme-learn.git

View File

@ -1,3 +1,3 @@
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> # SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
# #
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only

View File

@ -10,29 +10,25 @@ Source: https://github.com/JohnXLivingston/peertube-plugin-livechat/
# License: ... # License: ...
Files: CHANGELOG.md Files: CHANGELOG.md
Copyright: 2024-2025 John Livingston <https://www.john-livingston.fr/> Copyright: 2024 John Livingston <https://www.john-livingston.fr/>
License: AGPL-3.0-only License: AGPL-3.0-only
Files: languages/* Files: languages/*
Copyright: 2024-2025 John Livingston <https://www.john-livingston.fr/> Copyright: 2024 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-2025 John Livingston <https://www.john-livingston.fr/> Copyright: 2024 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-2025 John Livingston <https://www.john-livingston.fr/> Copyright: 2024 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-2025 John Livingston <https://www.john-livingston.fr/> Copyright: 2024 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-2025 John Livingston <https://www.john-livingston.fr/> Copyright: 2024 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

View File

@ -1,8 +1,8 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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.',
} }
] ]
} }

View File

@ -1,211 +1,5 @@
# 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
@ -240,7 +34,7 @@ The concord theme was removed from ConverseJS. If you had it set in the plugin s
### 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/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/fr/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.
@ -931,7 +725,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, they can get previously send messages. * 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 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.
@ -968,7 +762,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, they should not be downgraded to admin. * Fix: if the video owner is already owner of the chatroom, he should not be downgraded to admin.
## v2.1.2 ## v2.1.2

View File

@ -1,5 +1,5 @@
<!-- <!--
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
--> -->

View File

@ -1,5 +1,5 @@
<!-- <!--
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
--> -->

View File

@ -1,5 +1,5 @@
<!-- <!--
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
--> -->

View File

@ -1,5 +1,5 @@
<!-- <!--
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
--> -->

View File

@ -1,5 +1,5 @@
<!-- <!--
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
--> -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

View File

@ -1,3 +1,3 @@
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,3 +1,3 @@
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,3 +1,3 @@
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,3 +1,3 @@
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,3 +1,3 @@
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,3 +1,3 @@
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,3 +1,3 @@
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,3 +1,3 @@
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,3 +1,3 @@
SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */

View File

@ -1,98 +0,0 @@
/*
* 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%;
}
}

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 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(--bg-secondary-400, var(--greyBackgroundColor)); border-bottom: 2px solid 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(--fg, var(--mainForegroundColor)); color: 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(--primary, var(--mainColor)); color: 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(--primary, var(--mainColor)); background-color: var(--mainColor);
} }
&:hover { &:hover {
color: #fff; color: #fff;
background-color: var(--fg-400, var(--mainHoverColor)); background-color: var(--mainHoverColor);
} }
&[disabled], &[disabled],
&.disabled { &.disabled {
cursor: default; cursor: default;
color: #fff; color: #fff;
background-color: var(--input-border-color, var(--inputBorderColor)); background-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(--bg-secondary-400, var(--greyBackgroundColor)); background-color: var(--greyBackgroundColor);
color: var(--fg-400, var(--greyForegroundColor)); color: var(--greyForegroundColor);
&:hover, &:hover,
&:active, &:active,
&:focus, &:focus,
&[disabled], &[disabled],
&.disabled { &.disabled {
color: var(--fg-400, var(--greyForegroundColor)); color: var(--greyForegroundColor);
background-color: var(--bg-secondary-300, var(--greySecondaryBackgroundColor)); background-color: var(--greySecondaryBackgroundColor);
} }
&[disabled], &[disabled],
@ -174,12 +174,6 @@ $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 {
@ -190,6 +184,12 @@ $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;

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */

View File

@ -1,6 +1,6 @@
/* /*
* SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com> * SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */

View File

@ -1,6 +1,6 @@
/* /*
* SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com> * SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 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(--fg-400, var(--greyForegroundColor)); color: var(--greyForegroundColor);
} }
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 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(--bg-secondary-400, var(--greyBackgroundColor)) !important; // !important is required for it to work in ConverseJS border: 5px solid var(--greyBackgroundColor);
/* stylelint-disable-next-line custom-property-pattern */ /* stylelint-disable-next-line custom-property-pattern */
border-bottom-color: var(--primary, var(--mainColor)) !important; // !important is required for it to work in ConverseJS border-bottom-color: var(--mainColor);
border-radius: 50%; border-radius: 50%;
display: inline-block; display: inline-block;
box-sizing: border-box; box-sizing: border-box;

View File

@ -1,6 +1,6 @@
/* /*
* SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com> * SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 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(--fg-400, var(--greyForegroundColor)) transparent; scrollbar-color: 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(--fg-400, var(--greyForegroundColor)); border-bottom: 1px dashed 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(--primary, var(--mainColor)); color: 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(--primary, var(--mainColor)); background-color: var(--mainColor);
.livechat-tag-close { .livechat-tag-close {
color: var(--primary, var(--mainColor)); color: var(--mainColor);
} }
} }
&:hover { &:hover {
color: #fff; color: #fff;
background-color: var(--fg-400, var(--mainHoverColor)); background-color: var(--mainHoverColor);
.livechat-tag-close { .livechat-tag-close {
color: var(--fg-400, var(--mainHoverColor)); color: var(--mainHoverColor);
} }
} }
@ -138,10 +138,10 @@ livechat-tags-input {
&.disabled { &.disabled {
cursor: default; cursor: default;
color: #fff; color: #fff;
background-color: var(--input-border-color, var(--inputBorderColor)); background-color: var(--inputBorderColor);
.livechat-tag-close { .livechat-tag-close {
color: var(--input-border-color, var(--inputBorderColor)); color: var(--inputBorderColor);
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 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 {
width: 100%;
@include tables.data-table; @include tables.data-table;
width: 100%;
tr th:first-child, tr th:first-child,
tr th:last-child { tr th:last-child {
width: 50px; width: 50px;

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 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(--fg-400, var(--mainHoverColor)); background-color: 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);

View File

@ -1,6 +1,6 @@
/* /*
* SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com> * SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 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(--input-border-color, var(--inputBorderColor)); background-color: var(--inputBorderColor);
} }
} }
@ -67,7 +67,7 @@ $bs-green: #39cc0b;
&:active, &:active,
&:focus { &:focus {
color: #fff; color: #fff;
background-color: var(--primary, var(--mainColor)); background-color: var(--mainColor);
} }
&:focus, &:focus,
@ -77,13 +77,13 @@ $bs-green: #39cc0b;
&:hover { &:hover {
color: #fff; color: #fff;
background-color: var(--fg-400, var(--mainHoverColor)); background-color: var(--mainHoverColor);
} }
&[disabled], &[disabled],
&.disabled { &.disabled {
cursor: default; cursor: default;
color: #fff; color: #fff;
background-color: var(--input-border-color, var(--inputBorderColor)); background-color: var(--inputBorderColor);
} }
} }

View File

@ -1,6 +1,6 @@
/* /*
* SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com> * SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 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(--bg-secondary-400, var(--greyBackgroundColor)) solid; border: 1px var(--greyBackgroundColor) solid;
} }
td, td,
@ -34,6 +34,6 @@
} }
tbody tr:nth-child(odd) { tbody tr:nth-child(odd) {
background-color: var(--bg-secondary-300, var(--greySecondaryBackgroundColor)); background-color: var(--greySecondaryBackgroundColor);
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -9,5 +9,4 @@
@use "elements/index"; @use "elements/index";
@use "video"; @use "video";
@use "configuration/configuration"; @use "configuration/configuration";
@use "admin/firewall/firewall"; @use "list-rooms/list-rooms.scss";
@use "list-rooms/list-rooms";

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */

View File

@ -1,10 +1,10 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
#peertube-plugin-livechat-container { #peertube-plugin-livechat-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
@ -18,81 +18,53 @@
/* 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 px height limit for converse-muc and converse-root is We must ensure that the 200px 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, 300px); // Always at least 200px, and ideally at least 30% of viewport. min-height: max(30vh, 200px); // 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, 300px); min-height: max(30vh, 200px);
} }
}
@media screen and (orientation: portrait) and (width <= 767px) { /* Media query for mobile devices */
/* On small screen, and when portrait mode, we are giving the chat more vertical space. @media only screen and (max-width: 50vw) {
It should go under the video. #peertube-plugin-livechat-container converse-root {
*/
min-height: max(58vh, 300px);
converse-muc { converse-muc {
min-height: max(58vh, 300px); min-height: 62vh;
/* 100vh - 30vh for video = 70vh remaining */
} }
} }
} }
// /* Media query for mobile devices */ /* Media query for tablets in portrait mode */
// @media only screen and (max-width: 767px) { @media only screen and (min-width: 50vw) and (max-width: 75vw) {
// #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 {
height: inherit; min-height: 62vh;
/* Full desktop experience */ /* Slightly less to account for other elements */
} }
} }
} }
/* 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;
}

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> * SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */

View File

@ -1,7 +1,6 @@
#!/usr/bin/env node #!/bin/env node
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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
@ -59,9 +58,6 @@ const avatarPartsDef = {
fur: 10, fur: 10,
eyes: 15, eyes: 15,
mouth: 10 mouth: 10
},
'nctv': {
body: null,
} }
} }
@ -173,26 +169,9 @@ 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 it is facing left. // Moderation bot avatar: choosing some parts, and turning it so he 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 })
@ -217,7 +196,7 @@ async function generateBotsAvatars () {
} }
{ {
// Moderation bot avatar: choosing some parts, and turning it so it is facing left. // Moderation bot avatar: choosing some parts, and turning it so he 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 })
@ -241,7 +220,7 @@ async function generateBotsAvatars () {
} }
{ {
// Moderation bot avatar: choosing some parts, and turning it so it is facing left. // Moderation bot avatar: choosing some parts, and turning it so he 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 })
@ -267,7 +246,7 @@ async function generateBotsAvatars () {
} }
{ {
// Moderation bot avatar: choosing some parts, and turning it so it is facing left. // Moderation bot avatar: choosing some parts, and turning it so he 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 })
@ -294,7 +273,7 @@ async function generateBotsAvatars () {
} }
{ {
// Moderation bot avatar: choosing some parts, and turning it so it is facing left. // Moderation bot avatar: choosing some parts, and turning it so he 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 })
@ -315,21 +294,6 @@ 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) {
@ -373,9 +337,6 @@ if (isMainThread) {
throw err throw err
} }
) )
} else if (part === 'nctv') {
generateNigbotAvatar();
parentPort.postMessage('done');
} else { } else {
generateAvatars(part).then( generateAvatars(part).then(
() => { () => {

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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(globalFile) { function loadLocs() {
// 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,6 +25,7 @@ function loadLocs(globalFile) {
// 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) {
@ -40,7 +41,7 @@ function loadLocs(globalFile) {
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(path.resolve(__dirname, 'client', '@types', 'global.d.ts'))) }, loadLocs())
const configs = clientFiles.map(f => ({ const configs = clientFiles.map(f => ({
entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ], entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ],
@ -58,14 +59,8 @@ 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,

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,7 +1,6 @@
#!/usr/bin/env bash #!/bin/bash
# SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> # SPDX-FileCopyrightText: 2024 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
@ -11,12 +10,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.4-3' wanted_release='v0.12.3-1'
x86_64_filename='prosody-x86_64.AppImage' x86_64_filename='prosody-x86_64.AppImage'
x86_64_sha256sum='83a583ac7036387514bed17afab257dab4161ccdd0ab7453818c78b51f830357' x86_64_sha256sum='f4af9bfefa2f804ad7e8b03a68f04194abb801f070ae620b3d4bcedb144e8523'
aarch64_filename='prosody-aarch64.AppImage' aarch64_filename='prosody-aarch64.AppImage'
aarch64_sha256sum='7b7e6bf30d4498fc99a40022232c3065707ee4f4df24dc17947b007621634304' aarch64_sha256sum='878c5be719e1e36a84d637fd2bd44e3059aa91ddb6906ad05f1dd0334078df09'
download_dir="$(pwd)/vendor/prosody-appimage" download_dir="$(pwd)/vendor/prosody-appimage"
dist_dir="$(pwd)/dist/server/prosody" dist_dir="$(pwd)/dist/server/prosody"

41
client/.eslintrc.json Normal file
View File

@ -0,0 +1,41 @@
{
"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"
}
}

View File

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
@ -12,7 +12,6 @@ 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
@ -56,12 +55,13 @@ 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_RETRACTATION_REASON_LABEL: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL: 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_APPLYTOMODERATORS_LABEL: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_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,13 +81,8 @@ 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
@ -95,7 +90,6 @@ 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
@ -128,34 +122,3 @@ 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

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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('primary-button', 'orange-button', 'peertube-button-link') promoteButton.classList.add('orange-button', 'peertube-button-link')
promoteButton.style.margin = '5px' promoteButton.style.margin = '5px'
promoteButton.onclick = async () => { promoteButton.onclick = async () => {
await fetch( await fetch(
@ -243,10 +243,7 @@ function register (clientOptions: RegisterClientOptions): void {
} }
} catch (error: any) { } catch (error: any) {
console.error(error) console.error(error)
peertubeHelpers.notifier.error( peertubeHelpers.notifier.error(error.toString(), await peertubeHelpers.translate(LOC_LOADING_ERROR))
(error as Error).toString(),
await peertubeHelpers.translate(LOC_LOADING_ERROR)
)
} }
} }
}) })
@ -269,15 +266,10 @@ 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-')) {

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
@ -8,7 +8,6 @@ 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> {
@ -46,7 +45,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,
@ -70,8 +69,7 @@ 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)
]) ])
} }

View File

@ -1,131 +0,0 @@
// 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)
})
}
}

View File

@ -1,5 +0,0 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import './admin-firewall'

View File

@ -1,26 +0,0 @@
// 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
}

View File

@ -1,108 +0,0 @@
// 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()
}
}

View File

@ -1,91 +0,0 @@
// 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>`
}

View File

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com> // SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
@ -14,7 +14,6 @@ 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 {
@ -32,7 +31,7 @@ export class ChannelConfigurationElement extends LivechatElement {
public validationError?: ValidationError public validationError?: ValidationError
@state() @state()
public actionDisabled = false public actionDisabled: boolean = false
private _asyncTaskRender: Task private _asyncTaskRender: Task
@ -52,10 +51,6 @@ 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.
*/ */
@ -113,9 +108,9 @@ export class ChannelConfigurationElement extends LivechatElement {
} }
} }
public readonly getInputValidationClass = (propertyName: string): Record<string, boolean> => { public readonly getInputValidationClass = (propertyName: string): { [key: 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 }) : {}
} }
@ -123,13 +118,9 @@ 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)}`)
} }
@ -139,9 +130,6 @@ 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 {

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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 = false public actionDisabled: boolean = false
private _asyncTaskRender: Task private _asyncTaskRender: Task
@ -102,13 +102,9 @@ export class ChannelEmojisElement extends LivechatElement {
try { try {
this.actionDisabled = true this.actionDisabled = true
this.channelEmojisConfiguration = await this._channelDetailsService.saveEmojisConfiguration( await this._channelDetailsService.saveEmojisConfiguration(this.channelId, this.channelEmojisConfiguration.emojis)
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
@ -132,6 +128,7 @@ 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) => {
@ -152,8 +149,6 @@ 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
@ -175,15 +170,6 @@ 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')
@ -192,8 +178,10 @@ export class ChannelEmojisElement extends LivechatElement {
throw new Error('Invalid data') throw new Error('Invalid data')
} }
const url = await this._convertImageToDataUrl(entry.url as string) const url = await this._convertImageToDataUrl(entry.url)
const sn = entry.sn as string let 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,
@ -211,7 +199,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 as Error).toString(), await this.ptTranslate(LOC_ERROR)) this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR))
} finally { } finally {
this.actionDisabled = false this.actionDisabled = false
} }
@ -250,27 +238,12 @@ 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 as Error).toString()) this.ptNotifier.error(err.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

View File

@ -2,9 +2,6 @@
// //
// 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'
@ -53,7 +50,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}" aria-hidden="true"> <a href="${channel.livechatConfigurationUri}">
${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>`

View File

@ -1,10 +1,7 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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'

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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

View File

@ -1,38 +1,35 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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: Record<string, DynamicFormHeader> = { const tableHeaderList: {[key: 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_APPLYTOMODERATORS_LABEL), colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC) description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_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_RETRACTATION_REASON_LABEL), colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC) description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC)
}, },
comments: { comments: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL), colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL),
@ -60,7 +57,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
} }
} }
} }
const tableSchema: Record<string, DynamicFormSchema> = { const tableSchema: {[key: string]: DynamicFormSchema} = {
forbiddenWords: { forbiddenWords: {
entries: { entries: {
inputType: 'tags', inputType: 'tags',
@ -96,8 +93,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
}, },
delay: { delay: {
inputType: 'number', inputType: 'number',
default: 10, default: 10
min: 1
} }
}, },
commands: { commands: {
@ -131,64 +127,6 @@ 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)}
@ -224,67 +162,6 @@ 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=${''}
@ -341,246 +218,6 @@ 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)}

View File

@ -1,10 +1,7 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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'
@ -48,14 +45,13 @@ 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">
${ ${
@ -90,11 +86,17 @@ 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')
} }
} }
@ -110,23 +112,5 @@ 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>`
} }

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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,16 +66,15 @@ 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' || link.key === 'my-video-space') { if (link.key !== 'in-my-library') { continue }
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.unshift({ myLibraryLinks.links.push({
label, label,
shortLabel: label, shortLabel: label,
path: '/p/livechat/configuration', path: '/p/livechat/configuration',

View File

@ -1,17 +1,14 @@
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com> // SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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
@ -28,16 +25,10 @@ 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') ||
@ -51,59 +42,15 @@ 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`] = []
@ -121,15 +68,6 @@ 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`] = []
@ -151,29 +89,6 @@ 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)) {
@ -185,21 +100,11 @@ export class ChannelDetailsService {
{ {
method: 'POST', method: 'POST',
headers: this._headers, headers: this._headers,
body: JSON.stringify( body: JSON.stringify(channelConfigurationOptions)
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.')
} }
@ -220,8 +125,7 @@ export class ChannelDetailsService {
} }
for (const channel of channels.data) { for (const channel of channels.data) {
channel.livechatConfigurationUri = channel.livechatConfigurationUri = '/p/livechat/configuration/channel?channelId=' + encodeURIComponent(channel.id)
'/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.
@ -251,15 +155,14 @@ export class ChannelDetailsService {
throw new Error('Can\'t get channel configuration options.') throw new Error('Can\'t get channel configuration options.')
} }
return this.backToFront(await response.json()) return response.json()
} }
public async fetchEmojisConfiguration (channelId: number): Promise<ChannelEmojisConfiguration> { public async fetchEmojisConfiguration (channelId: number): Promise<ChannelEmojisConfiguration> {
const url = getBaseRoute(this._registerClientOptions) +
'/api/configuration/channel/emojis/' +
encodeURIComponent(channelId)
const response = await fetch( const response = await fetch(
url, getBaseRoute(this._registerClientOptions) +
'/api/configuration/channel/emojis/' +
encodeURIComponent(channelId),
{ {
method: 'GET', method: 'GET',
headers: this._headers headers: this._headers
@ -276,22 +179,12 @@ 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)) { // optional ':' at the beggining and at the end } else if (!/^:[\w-]+:$/.test(e.sn)) {
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)
@ -320,62 +213,15 @@ export class ChannelDetailsService {
public async saveEmojisConfiguration ( public async saveEmojisConfiguration (
channelId: number, channelId: number,
channelEmojis: ChannelEmojis channelEmojis: ChannelEmojis
): Promise<ChannelEmojisConfiguration> { ): Promise<void> {
if (!await this.validateEmojisConfiguration(channelEmojis)) { if (!await this.validateEmojisConfiguration(channelEmojis)) {
throw new Error('Invalid form data') throw new Error('Invalid form data')
} }
// Note: API request body size is limited to 100Kb (expressjs body-parser defaut limit, and Peertube nginx config).
// 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/' +
encodeURIComponent(channelId)
const response = await fetch( const response = await fetch(
url, getBaseRoute(this._registerClientOptions) +
'/api/configuration/channel/emojis/' +
encodeURIComponent(channelId),
{ {
method: 'POST', method: 'POST',
headers: this._headers, headers: this._headers,
@ -384,29 +230,10 @@ 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()
} }
} }

View File

@ -1,12 +1,11 @@
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com> // SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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 = export const AddSVG: string =
`<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>
@ -14,9 +13,8 @@ export const AddSVG =
</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 = export const RemoveSVG: string =
`<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>

View File

@ -39,7 +39,9 @@ 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> {
this._settings ??= await this.ptOptions.peertubeHelpers.getSettings() as LiveChatSettings if (!this._settings) {
this._settings = await this.ptOptions.peertubeHelpers.getSettings() as LiveChatSettings
}
return this._settings return this._settings
} }
} }

View File

@ -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 = '' private _translatedValue: string = ''
private _localizationId = '' private _localizationId: string = ''
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 = false): TemplateResult | string => { public override render = (locId: string, allowHTML: boolean = 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

View File

@ -1,11 +1,8 @@
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com> // SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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'

View File

@ -1,11 +1,8 @@
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com> // SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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'
@ -23,26 +20,26 @@ import { AddSVG, RemoveSVG } from '../buttons'
type DynamicTableAcceptedTypes = number | string | boolean | Date | Array<number | string> type DynamicTableAcceptedTypes = number | string | boolean | Date | Array<number | string>
type DynamicTableAcceptedInputTypes = 'textarea' type DynamicTableAcceptedInputTypes = 'textarea'
| 'select' | 'select'
| 'checkbox' | 'checkbox'
| 'range' | 'range'
| 'color' | 'color'
| 'date' | 'date'
| 'datetime' | 'datetime'
| 'datetime-local' | 'datetime-local'
| 'email' | 'email'
| 'file' | 'file'
| 'image' | 'image'
| 'month' | 'month'
| 'number' | 'number'
| 'password' | 'password'
| 'tel' | 'tel'
| 'text' | 'text'
| 'time' | 'time'
| 'url' | 'url'
| 'week' | 'week'
| 'tags' | 'tags'
| 'image-file' | 'image-file'
interface CellDataSchema { interface CellDataSchema {
min?: number min?: number
@ -50,11 +47,11 @@ interface CellDataSchema {
minlength?: number minlength?: number
maxlength?: number maxlength?: number
size?: number size?: number
options?: Record<string, string> label?: TemplateResult | 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.
} }
@ -62,17 +59,19 @@ interface CellDataSchema {
interface DynamicTableRowData { interface DynamicTableRowData {
_id: number _id: number
_originalIndex: number _originalIndex: number
row: Record<string, DynamicTableAcceptedTypes> row: { [key: string]: DynamicTableAcceptedTypes }
} }
interface DynamicFormHeaderCellData { interface DynamicFormHeaderCellData {
colName: TemplateResult | DirectiveResult colName: TemplateResult | DirectiveResult
description?: TemplateResult | DirectiveResult description: TemplateResult | DirectiveResult
headerClassList?: string[] headerClassList?: string[]
} }
export type DynamicFormHeader = Record<string, DynamicFormHeaderCellData> export interface DynamicFormHeader {
export type DynamicFormSchema = Record<string, CellDataSchema> [key: string]: DynamicFormHeaderCellData
}
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 {
@ -86,19 +85,19 @@ export class DynamicTableFormElement extends LivechatElement {
public maxLines?: number = undefined public maxLines?: number = undefined
@property() @property()
public validation?: Record<string, ValidationErrorType[]> public validation?: {[key: string]: ValidationErrorType[] }
@property({ attribute: false }) @property({ attribute: false })
public validationPrefix = '' public validationPrefix: string = ''
@property({ attribute: false }) @property({ attribute: false })
public rows: Array<Record<string, DynamicTableAcceptedTypes>> = [] public rows: Array<{ [key: string]: DynamicTableAcceptedTypes }> = []
@state() @state()
public _rowsById: DynamicTableRowData[] = [] public _rowsById: DynamicTableRowData[] = []
@property({ attribute: false }) @property({ attribute: false })
public formName = '' public formName: string = ''
@state() @state()
private _lastRowId = 1 private _lastRowId = 1
@ -113,7 +112,7 @@ export class DynamicTableFormElement extends LivechatElement {
} }
} }
private readonly _getDefaultRow = (): Record<string, DynamicTableAcceptedTypes> => { private readonly _getDefaultRow = (): { [key: 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 ?? ''])])
} }
@ -237,7 +236,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>`
} }
@ -246,11 +245,11 @@ export class DynamicTableFormElement extends LivechatElement {
return html`<tr id=${inputId}> return html`<tr id=${inputId}>
${Object.keys(this.header) ${Object.keys(this.header)
.sort((k1, k2) => this.columnOrder.indexOf(k1) - this.columnOrder.indexOf(k2)) .sort((k1, k2) => this.columnOrder.indexOf(k1) - this.columnOrder.indexOf(k2))
.map(key => this.renderDataCell(key, .map(key => this.renderDataCell(key,
rowData.row[key] ?? this.schema[key].default, rowData.row[key] ?? this.schema[key].default,
rowData._id, rowData._id,
rowData._originalIndex))} rowData._originalIndex))}
<td class="form-group"> <td class="form-group">
<button type="button" <button type="button"
class="dynamic-table-remove-row" class="dynamic-table-remove-row"
@ -296,7 +295,6 @@ 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) {
@ -322,7 +320,6 @@ 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,
@ -335,7 +332,6 @@ 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,
@ -348,7 +344,6 @@ 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,
@ -361,7 +356,6 @@ 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(),
@ -382,7 +376,6 @@ 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(),
@ -401,7 +394,6 @@ 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,
@ -419,7 +411,6 @@ 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,
@ -455,10 +446,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 ?? propertySchema.default ?? '', (propertyValue)?.join(propertySchema.separator ?? ',') ??
propertyValue ?? propertySchema.default ?? '',
originalIndex)} originalIndex)}
${feedback} ${feedback}
` `
@ -470,10 +461,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 ?? propertySchema.default ?? '', (propertyValue)?.join(propertySchema.separator ?? ',') ??
propertyValue ?? propertySchema.default ?? '',
originalIndex)} originalIndex)}
${feedback} ${feedback}
` `
@ -485,7 +476,6 @@ 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,
@ -497,10 +487,8 @@ export class DynamicTableFormElement extends LivechatElement {
} }
if (!formElement) { if (!formElement) {
this.logger.warn( this.logger.warn(`value type '${(propertyValue.constructor.toString())}' is incompatible` +
`value type '${(propertyValue.constructor.toString())}' is incompatible` + `with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`)
`with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`
)
} }
const classList = ['form-group'] const classList = ['form-group']
@ -513,7 +501,6 @@ 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,
@ -528,7 +515,6 @@ 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)}
@ -548,7 +534,6 @@ 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>,
@ -562,7 +547,7 @@ export class DynamicTableFormElement extends LivechatElement {
) )
)} )}
id=${inputId} id=${inputId}
.inputTitle=${inputTitle as any} .inputPlaceholder=${propertySchema.label as any}
aria-describedby="${inputId}-feedback" aria-describedby="${inputId}-feedback"
.min=${propertySchema.min} .min=${propertySchema.min}
.max=${propertySchema.max} .max=${propertySchema.max}
@ -578,7 +563,6 @@ 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,
@ -592,7 +576,6 @@ 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)}
@ -605,7 +588,6 @@ 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,
@ -620,7 +602,6 @@ 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"
@ -630,7 +611,6 @@ 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,
@ -643,12 +623,11 @@ 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}>${inputTitle ?? ''}</option> <option ?selected=${!propertyValue}>${propertySchema.label ?? 'Choose your option'}</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>`
@ -659,7 +638,6 @@ 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,
@ -669,7 +647,6 @@ 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}
@ -679,7 +656,7 @@ export class DynamicTableFormElement extends LivechatElement {
} }
_getInputValidationClass = (propertyName: string, _getInputValidationClass = (propertyName: string,
originalIndex: number): Record<string, boolean> => { originalIndex: number): { [key: string]: boolean } => {
const validationErrorTypes: ValidationErrorType[] | undefined = const validationErrorTypes: ValidationErrorType[] | undefined =
this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`] this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`]
@ -695,7 +672,6 @@ 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)}`)
@ -712,9 +688,6 @@ 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 {
@ -753,9 +726,6 @@ 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

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only

View File

@ -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 = '' public page: string = ''
@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="primary-button orange-button peertube-button-link" class="orange-button peertube-button-link"
>${unsafeHTML(helpButtonSVG())}</a>` >${unsafeHTML(helpButtonSVG())}</a>`
}) })
} }

View File

@ -1,15 +1,11 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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.
@ -33,16 +29,13 @@ 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} alt=${ifDefined(this.inputTitle)} @click=${(ev: Event) => { ? html`<img src=${this.value} @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()
@ -51,7 +44,6 @@ 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;' : ''}

View File

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com> // SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com> // SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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 = (): HTMLElement | DocumentFragment => { protected override createRenderRoot = (): Element | ShadowRoot => {
return this return this
} }
} }

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,11 +1,8 @@
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com> // SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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'
@ -15,7 +12,6 @@ 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.
@ -24,11 +20,10 @@ import type { DirectiveResult } from 'lit/directive'
// 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, @stylistic/indent-binary-ops // 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="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>`
@ -53,7 +48,7 @@ export class TagsInputElement extends LivechatElement {
private _inputValue?: string = '' private _inputValue?: string = ''
@property({ attribute: false }) @property({ attribute: false })
public inputTitle?: string | DirectiveResult = '' public inputPlaceholder?: string = ''
@property({ attribute: false }) @property({ attribute: false })
public datalist?: string[] public datalist?: string[]
@ -68,10 +63,10 @@ export class TagsInputElement extends LivechatElement {
private readonly _isPressingKey: string[] = [] private readonly _isPressingKey: string[] = []
@property({ attribute: false }) @property({ attribute: false })
public separator = '\n' public separator: string = '\n'
@property({ attribute: false }) @property({ attribute: false })
public animDuration = 200 public animDuration: number = 200
/** /**
* Overloading the standard focus method. * Overloading the standard focus method.
@ -171,7 +166,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 ?? ''}
title=${ifDefined(this.inputTitle)} /> placeholder=${ifDefined(this.inputPlaceholder)} />
${(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}>`)}
@ -249,9 +244,8 @@ 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 ( if ((target.selectionStart === target.selectionEnd) &&
(target.selectionStart === target.selectionEnd) && target.selectionStart === 0 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))
@ -264,9 +258,8 @@ 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 ( if ((target.selectionStart === target.selectionEnd) &&
(target.selectionStart === target.selectionEnd) && target.selectionStart === target.value.length target.selectionStart === target.value.length) {
) {
this._handleDeleteTag((this._searchedTagsIndex.length) this._handleDeleteTag((this._searchedTagsIndex.length)
? this._searchedTagsIndex[0] ? this._searchedTagsIndex[0]
: 0) : 0)

View File

@ -1,10 +1,7 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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'
@ -26,11 +23,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 = '' let dateStr: string = ''
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'

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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 = false public actionDisabled: boolean = 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 as Error).toString(), await this.ptTranslate(LOC_ERROR)) this.ptNotifier.error(err.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 as Error).toString(), await this.ptTranslate(LOC_ERROR)) this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR))
} finally { } finally {
this.actionDisabled = false this.actionDisabled = false
} }

View File

@ -7,12 +7,11 @@ export enum ValidationErrorType {
WrongType, WrongType,
WrongFormat, WrongFormat,
NotInRange, NotInRange,
Duplicate, Duplicate
TooLong
} }
export class ValidationError extends Error { export class ValidationError extends Error {
properties: Record<string, ValidationErrorType[]> = {} properties: {[key: string]: ValidationErrorType[] } = {}
constructor (name: string, message: string | undefined, properties: ValidationError['properties']) { constructor (name: string, message: string | undefined, properties: ValidationError['properties']) {
super(message) super(message)

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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(
'primary-button', 'orange-button', 'peertube-button-link', '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,23 +42,6 @@ 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'
} }
@ -69,10 +52,6 @@ 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) {

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
@ -16,6 +16,8 @@ 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
@ -23,6 +25,7 @@ import { displayConverseJS } from '../../utils/conversejs'
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')
@ -43,8 +46,6 @@ 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 {
@ -60,8 +61,8 @@ async function initChat (video: Video): Promise<void> {
return return
} }
let showShareUrlButton = false let showShareUrlButton: boolean = false
let showPromote = false let showPromote: boolean = 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') {
@ -187,10 +188,9 @@ 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(
url, getBaseRoute(ptContext.ptOptions) + '/api/configuration/room/' +
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,6 +353,19 @@ 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}'`)
} }

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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: string[] = ['embed', 'dock', 'peertube', 'xmpp'] as const const validTabNames = ['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 = false public xmppUriEnabled: boolean = false
/** /**
* Should we render the Dock tab? * Should we render the Dock tab?
*/ */
@property({ attribute: false }) @property({ attribute: false })
public dockEnabled = false public dockEnabled: boolean = false
/** /**
* Can we use autocolors? * Can we use autocolors?
*/ */
@property({ attribute: false }) @property({ attribute: false })
public autocolorsAvailable = false public autocolorsAvailable: boolean = 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 = false public embedIFrame: boolean = 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 = false public embedReadOnly: boolean = false
/** /**
* Read-only, with scrollbar? * Read-only, with scrollbar?
*/ */
@property({ attribute: false }) @property({ attribute: false })
public embedReadOnlyScrollbar = false public embedReadOnlyScrollbar: boolean = false
/** /**
* Read-only, transparent background? * Read-only, transparent background?
*/ */
@property({ attribute: false }) @property({ attribute: false })
public embedReadOnlyTransparentBackground = false public embedReadOnlyTransparentBackground: boolean = 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 = false public embedAutocolors: boolean = 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 as string)) { if (validTabNames.includes(v.currentTab)) {
this.currentTab = v.currentTab this.currentTab = v.currentTab
} }
this.embedIFrame = !!v.embedIFrame this.embedIFrame = !!v.embedIFrame

View File

@ -1,10 +1,7 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 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'

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024-2025 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
@ -71,7 +71,8 @@ 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...')
const title = (node as HTMLElement).querySelector?.('.modal-title') if (!(node as HTMLElement).querySelector) { return }
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