Compare commits

..

No commits in common. "main" and "nctv-avatar" have entirely different histories.

553 changed files with 17651 additions and 53948 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,156 +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 ## 10.3.3
### Minor changes and fixes ### Minor changes and fixes

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: 61 KiB

After

Width:  |  Height:  |  Size: 65 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,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
*/ */
@ -18,31 +18,17 @@
/* 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(59vh, 400px);
}
@media screen and (orientation: portrait) and (width <= 767px) {
/* On small screen, and when portrait mode, we are giving the chat more vertical space.
It should go under the video.
*/
min-height: max(58vh, 300px);
converse-muc {
min-height: max(58vh, 300px);
}
} }
} }

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

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
@ -133,29 +133,3 @@ declare const LOC_POLL_VOTE_OK: string
declare const LOC_MODERATION_DELAY: string declare const LOC_MODERATION_DELAY: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_MODERATION_DELAY_DESC: 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
@ -32,7 +32,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
@ -113,9 +113,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,7 +123,7 @@ 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 // FIXME: this code is duplicated in dymamic table form
if (validationErrorTypes && validationErrorTypes.length !== 0) { if (validationErrorTypes && validationErrorTypes.length !== 0) {

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
@ -192,7 +192,7 @@ 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 const sn = entry.sn as string
const item: ChannelEmojisConfiguration['emojis']['customEmojis'][0] = { const item: ChannelEmojisConfiguration['emojis']['customEmojis'][0] = {
@ -211,7 +211,7 @@ export class ChannelEmojisElement extends LivechatElement {
await this.ptTranslate(LOC_ACTION_IMPORT_EMOJIS_INFO) await this.ptTranslate(LOC_ACTION_IMPORT_EMOJIS_INFO)
) )
} catch (err: any) { } catch (err: any) {
this.ptNotifier.error((err 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 +250,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: {
@ -139,7 +135,6 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
</livechat-configuration-section-header> </livechat-configuration-section-header>
<div class="form-group"> <div class="form-group">
<textarea <textarea
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TERMS_LABEL) as any}
name="terms" name="terms"
id="peertube-livechat-terms" id="peertube-livechat-terms"
.value=${el.channelConfiguration?.configuration.terms ?? ''} .value=${el.channelConfiguration?.configuration.terms ?? ''}
@ -172,7 +167,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
<label> <label>
<input <input
type="checkbox" type="checkbox"
name="mute_anonymous" name="bot"
id="peertube-livechat-mute-anonymous" id="peertube-livechat-mute-anonymous"
@input=${(event: InputEvent) => { @input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) { if (event?.target && el.channelConfiguration) {
@ -259,32 +254,6 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
${el.renderFeedback('peertube-livechat-moderation-delay-feedback', 'moderation.delay')} ${el.renderFeedback('peertube-livechat-moderation-delay-feedback', 'moderation.delay')}
</div> </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 +310,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,7 +86,7 @@ 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) {
@ -110,23 +106,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,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
@ -11,7 +11,7 @@ import type {
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 { maxEmojisPerChannel } from 'shared/lib/emojis'
import { channelTermsMaxLength, noDuplicateMaxDelay, forbidSpecialCharsMaxTolerance } from 'shared/lib/constants' import { channelTermsMaxLength } from 'shared/lib/constants'
export class ChannelDetailsService { export class ChannelDetailsService {
public _registerClientOptions: RegisterClientOptions public _registerClientOptions: RegisterClientOptions
@ -67,43 +67,11 @@ export class ChannelDetailsService {
// 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 +89,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 +110,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 +121,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 +146,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 +176,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
@ -371,11 +295,10 @@ export class ChannelDetailsService {
channelId: number, channelId: number,
channelEmojis: ChannelEmojis channelEmojis: ChannelEmojis
): Promise<ChannelEmojisConfiguration> { ): 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,
@ -389,24 +312,4 @@ export class ChannelDetailsService {
return response.json() 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'
@ -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>`
} }
@ -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}`]
@ -753,9 +730,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

@ -12,7 +12,7 @@ export enum ValidationErrorType {
} }
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
} }

View File

@ -1,10 +1,9 @@
// 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 { Video } from '@peertube/peertube-types' import type { Video } from '@peertube/peertube-types'
import type { LiveChatSettings } from '../lib/contexts/peertube'
import { AutoColors, isAutoColorsAvailable } from 'shared/lib/autocolors' import { AutoColors, isAutoColorsAvailable } from 'shared/lib/autocolors'
import { getBaseRoute } from '../../utils/uri' import { getBaseRoute } from '../../utils/uri'
import { logger } from '../../utils/logger' import { logger } from '../../utils/logger'
@ -18,7 +17,7 @@ interface UriOptions {
} }
function getIframeUri ( function getIframeUri (
registerOptions: RegisterClientOptions, settings: LiveChatSettings, video: Video, uriOptions: UriOptions = {} registerOptions: RegisterClientOptions, settings: any, video: Video, uriOptions: UriOptions = {}
): string | null { ): string | null {
if (!settings) { if (!settings) {
logger.error('Settings are not initialized, too soon to compute the iframeUri') logger.error('Settings are not initialized, too soon to compute the iframeUri')

Some files were not shown because too many files have changed in this diff Show More