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
@ -33,7 +33,7 @@ jobs:
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: '0.132.2'
hugo-version: '0.80.0'
extended: true
- 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

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
@ -19,10 +19,10 @@ builddoctranslations:
pages:
stage: deploy
image: registry.gitlab.com/pages/hugo/hugo_extended:0.132.2
image: registry.gitlab.com/pages/hugo/hugo_extended:latest
variables:
GIT_SUBMODULE_STRATEGY: recursive
GIT_SUBMODULE_PATHS: support/documentation/themes/hugo-theme-relearn
GIT_SUBMODULE_PATHS: support/documentation/themes/hugo-theme-learn
script:
# 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/'

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
[submodule "support/documentation/themes/hugo-theme-relearn"]
path = support/documentation/themes/hugo-theme-relearn
url = https://github.com/McShelby/hugo-theme-relearn.git
[submodule "documentation/themes/hugo-theme-learn"]
path = support/documentation/themes/hugo-theme-learn
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

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

View File

@ -10,29 +10,25 @@ Source: https://github.com/JohnXLivingston/peertube-plugin-livechat/
# License: ...
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
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
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
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
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
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
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
'use strict'
'use strict';
module.exports = {
extends: [
@ -14,7 +14,7 @@ module.exports = {
// extending the kebab-case to accept ConverseJS class names.
'^([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
## 13.0.0
**Important note**: if you got an error on updating the plugin, please try to restart Peertube and install it again.
### Security Fix
Severity: low.
[Radically Open Security](radicallyopensecurity.com) reported a security vulnerability: a malicious user can forge a malicious Regular Expression to cause a [ReDOS](https://en.wikipedia.org/wiki/ReDoS) on the Chat Bot.
Such attack would only make the bot unresponsive, and won't affect the Peertube server or the XMPP server.
This version mitigates the attack by using the [RE2](https://github.com/google/re2) regular expression library.
Thanks [NlNet](https://nlnet.nl/) for funding the security audit.
### Breaking changes
#### Bot timers
There was a regression some months ago in the "bot timer" functionnality.
In the channels settings, the delay between two quotes is supposed to be in minutes, but in fact we applied seconds.
We don't have any way to detect if the user meant seconds or minutes when they configured their channels (it depends if it was before or after the regression).
So we encourage all streamers to go through their channel settings, check the frequency of their bot timers (if enabled), set them to the correct value, and save the form.
Users must save the form to be sure to apply the correct value.
#### Bot forbidden words
When using regular expressions for the forbidden words, the chat bot now uses the [RE2](https://github.com/google/re2) regular expression library.
This library does not support all character classes, and all regular expressions that were previously possible (with the Javascript RegExp class).
For more information about the accepted regular expression, please refer to the [documentation](https://livingston.frama.io/peertube-plugin-livechat/documentation/user/streamers/bot/forbidden_words/#consider-as-regular-expressions).
If you configured non-compatible regular expressions, the bot will just ignore them, and log an error.
When saving channel's preference, if non-compatible regular expression is used, an error will be shown.
### Minor changes and fixes
* Translations updates.
* Dependencies updates.
* Fix #329: auto focus message field after anonymous user has entered nickname (Thanks [Axolotle](https://github.com/axolotle).
* Fix #392: add draggable items touch screen handling (Thanks [Axolotle](https://github.com/axolotle).
* Fix #506: hide offline users by default in occupant list (Thanks [Axolotle](https://github.com/axolotle).
* Fix #547: add button to go to the end of the chat (Thanks [Axolotle](https://github.com/axolotle).
* Fix #503: set custom emojis max height to text height + bigger when posted alone (Thanks [Axolotle](https://github.com/axolotle).
* Fix: Converse bottom panel messages not visible on new Peertube v7 theme (for example for muted users).
* Fix #75: New short video urls makes it difficult to use the settings «Activate chat for these videos».
* Fix moderation notes: fix filter button wrongly displayed on notes without associated occupant.
* Fix tasks: checkbox state does not change when clicked.
* Fix: bot timer can't be negative or null.
* Fix #626: Bot timer was buggy, using seconds as delay instead of minutes.
* Fix: message deletions were not properly anonymized when using "Anonymize moderation actions" option.
## 12.0.4
### Minor changes and fixes
* Fix #660: don't send headers twice on emoji router errors.
* Fix shebangs (for NixOS compatibility).
* Translations updates.
* Updating various dependencies.
* Adding a warning in settings if theme is not set to Peertube or if autocolors are disabled.
## 12.0.3
### Minor changes and fixes
* Translations updates.
* Slovak translation integration.
* Differenciate pt-PT and pt-BR translations.
* Fix styling for "configure mod_firewall" button + Peertube v7.0.0 compatibility.
* Fix #648: workaround for a regression in Firefox that breaks the scrollbar (Thanks [Raph](https://github.com/raphgilles) for the workaround!).
## 12.0.2
### Minor changes and fixes
* Fix task list label styling.
* Translations updates.
## 12.0.1
### Minor changes and fixes
* Fix custom emojis vs upper/lower case.
## 12.0.0
### Importante Notes
This version requires Peertube 5.2.0 or superior.
It also requires NodeJS 16 or superior (same as Peertube 5.2.0.).
If you use the "system Prosody", you should update to Prosody 0.12.4, and Lua 5.4.
### New features
* #131: Emoji only mode.
* #516: new option for the moderation bot: forbid duplicate messages.
* #517: new option for the moderation bot: forbid messages with too many special characters.
* #518: moderators can send announcements and highlighted messages.
* #610: compatibility with PeerTube v7
### Minor changes and fixes
* Updating ConverseJS (v11 WIP) with latest fixes.
* Updating Prosody AppImage to Prosody 0.12.4 + Lua 5.4.
* Various translation updates.
* Using Typescript 5.5.4, and Eslint 8.57.0 (with new ruleset).
* Fix race condition in bot/ctl.
* Various type improvements.
* Update dependencies.
* Fix emoji picker colors and size.
* Fix: moderation delay max value was not correctly handled.
## 11.0.1
### Minor changes and fixes
* Fix "send message" button that was sending the message twice.
## 11.0.0
### Importante Notes
With the new [mod_firewall](https://livingston.frama.io/peertube-plugin-livechat/documentation/admin/mod_firewall/) feature, Peertube admins can write firewall rules for the Prosody server. These rules could be used to run arbitrary code on the server. If you are a hosting provider, and you don't want to allow Peertube admins to write such rules, you can disable the online editing by creating a `disable_mod_firewall_editing` file in the plugin directory. Check the documentation for more information. This is opt-out, as Peertube admins can already run arbitrary code just by installing any plugin.
The concord theme was removed from ConverseJS. If you had it set in the plugin settings, it will fallback to the Peertube theme.
### New features
* Updating ConverseJS, to use upstream (v11 WIP). This comes with many improvements and new features.
* #146: copy message button for moderators.
* #137: option to hide moderator name who made actions (kick, ban, message moderation, ...).
* #144: [moderator notes](https://livingston.frama.io/peertube-plugin-livechat/documentation/user/streamers/moderation_notes/).
* #145: action for moderators to find all messages from a given participant.
* #97: option to use and configure [mod_firewall](https://livingston.frama.io/peertube-plugin-livechat/documentation/admin/mod_firewall/) at the server level.
### Minor changes and fixes
* #118: improved accessibility.
* Avatar set for anonymous users: new 'none' choice (that will fallback to Converse new colorized avatars).
* New translation: Albanian.
* Translation updates: Crotian, Japanese, traditional Chinese, Arabic, Galician.
* Updated mod_muc_moderation to upstream.
* Fix new task ordering.
* Fix: clicking on the current user nickname in message history was failing to open the profile modal.
* Fix: increase chat height on small screens, try to better detect the device viewport size and orientation.
* Converse theme: removed concord, added cyberpunk.
* Fixed Converse theme settings localization.
* Fix: improved minimum chat width.
## 10.3.3
### Minor changes and fixes

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
-->

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
-->

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
-->

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
-->

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
-->

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

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

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

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

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

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

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

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

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

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
*/

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
*/

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
*/
@ -21,7 +21,7 @@ $small-view: 800px;
/* See Peertube sub-menu-h1 mixin */
font-size: 1.3rem;
border-bottom: 2px solid var(--bg-secondary-400, var(--greyBackgroundColor));
border-bottom: 2px solid var(--greyBackgroundColor);
padding-bottom: 15px;
}
@ -29,7 +29,7 @@ $small-view: 800px;
&.peertube-plugin-livechat-configuration-channel {
.peertube-plugin-livechat-configuration-channel-info {
/* stylelint-disable-next-line value-keyword-case */
color: var(--fg, var(--mainForegroundColor));
color: var(--mainForegroundColor);
span:first-child {
/* See Peertube .video-channel-display-name */
@ -48,7 +48,7 @@ $small-view: 800px;
h2 {
// See Peertube settings-big-title mixin
text-transform: uppercase;
color: var(--primary, var(--mainColor));
color: var(--mainColor);
font-weight: variables.$font-bold;
font-size: 1rem;
margin-bottom: 10px;
@ -82,35 +82,35 @@ $small-view: 800px;
&:active,
&:focus {
color: #fff;
background-color: var(--primary, var(--mainColor));
background-color: var(--mainColor);
}
&:hover {
color: #fff;
background-color: var(--fg-400, var(--mainHoverColor));
background-color: var(--mainHoverColor);
}
&[disabled],
&.disabled {
cursor: default;
color: #fff;
background-color: var(--input-border-color, var(--inputBorderColor));
background-color: var(--inputBorderColor);
}
}
input[type="reset"],
button[type="reset"] {
// Peertube grey-button mixin
background-color: var(--bg-secondary-400, var(--greyBackgroundColor));
color: var(--fg-400, var(--greyForegroundColor));
background-color: var(--greyBackgroundColor);
color: var(--greyForegroundColor);
&:hover,
&:active,
&:focus,
&[disabled],
&.disabled {
color: var(--fg-400, var(--greyForegroundColor));
background-color: var(--bg-secondary-300, var(--greySecondaryBackgroundColor));
color: var(--greyForegroundColor);
background-color: var(--greySecondaryBackgroundColor);
}
&[disabled],
@ -174,12 +174,6 @@ $small-view: 800px;
a {
/* 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,
&:focus,
&:active {
@ -190,6 +184,12 @@ $small-view: 800px;
outline: none !important;
}
width: fit-content;
display: flex;
align-items: baseline;
/* stylelint-disable-next-line value-keyword-case */
color: var(--mainForegroundColor);
div:first-child {
/* See Peertube .video-channel-display-name */
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
*/

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
*/

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
*/

View File

@ -1,6 +1,6 @@
/*
* 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
*/

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
*/

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
*/

View File

@ -1,6 +1,6 @@
/*
* 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
*/

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
*/

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
*/
@ -56,7 +56,7 @@ livechat-share-chat {
&.livechat-shareurl-suboptions-disabled {
label {
/* 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
*/
@ -15,9 +15,9 @@ livechat-spinner,
height: 48px;
margin: 20px;
/* 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 */
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%;
display: inline-block;
box-sizing: border-box;

View File

@ -1,6 +1,6 @@
/*
* 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
*/
@ -51,14 +51,14 @@ livechat-tags-input {
transition-duration: 0.3s;
@supports (scrollbar-width: auto) {
scrollbar-color: var(--fg-400, var(--greyForegroundColor)) transparent;
scrollbar-color: var(--greyForegroundColor) transparent;
scrollbar-width: thin;
}
}
.livechat-tags-container,
.livechat-tags-searched {
border-bottom: 1px dashed var(--fg-400, var(--greyForegroundColor));
border-bottom: 1px dashed var(--greyForegroundColor);
&.livechat-empty {
height: 0;
@ -104,7 +104,7 @@ livechat-tags-input {
text-align: center;
font-size: 10px;
margin-left: var(--tag-padding-horizontal);
color: var(--primary, var(--mainColor));
color: var(--mainColor);
border-radius: 50%;
background: #fff;
cursor: pointer;
@ -118,19 +118,19 @@ livechat-tags-input {
&:active,
&:focus {
color: #fff;
background-color: var(--primary, var(--mainColor));
background-color: var(--mainColor);
.livechat-tag-close {
color: var(--primary, var(--mainColor));
color: var(--mainColor);
}
}
&:hover {
color: #fff;
background-color: var(--fg-400, var(--mainHoverColor));
background-color: var(--mainHoverColor);
.livechat-tag-close {
color: var(--fg-400, var(--mainHoverColor));
color: var(--mainHoverColor);
}
}
@ -138,10 +138,10 @@ livechat-tags-input {
&.disabled {
cursor: default;
color: #fff;
background-color: var(--input-border-color, var(--inputBorderColor));
background-color: var(--inputBorderColor);
.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
*/
@ -9,10 +9,10 @@
livechat-token-list {
table {
width: 100%;
@include tables.data-table;
width: 100%;
tr th:first-child,
tr th:last-child {
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
*/
@ -19,7 +19,7 @@ table.peertube-plugin-livechat-prosody-list-rooms tr:nth-child(even) {
table.peertube-plugin-livechat-prosody-list-rooms th {
/* stylelint-disable-next-line custom-property-pattern */
background-color: var(--fg-400, var(--mainHoverColor));
background-color: var(--mainHoverColor);
border: 1px solid black;
/* stylelint-disable-next-line custom-property-pattern */
color: var(--mainBackgroundColor);

View File

@ -1,6 +1,6 @@
/*
* 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
*/
@ -54,7 +54,7 @@ $bs-green: #39cc0b;
&.disabled {
cursor: default;
color: #fff;
background-color: var(--input-border-color, var(--inputBorderColor));
background-color: var(--inputBorderColor);
}
}
@ -67,7 +67,7 @@ $bs-green: #39cc0b;
&:active,
&:focus {
color: #fff;
background-color: var(--primary, var(--mainColor));
background-color: var(--mainColor);
}
&:focus,
@ -77,13 +77,13 @@ $bs-green: #39cc0b;
&:hover {
color: #fff;
background-color: var(--fg-400, var(--mainHoverColor));
background-color: var(--mainHoverColor);
}
&[disabled],
&.disabled {
cursor: default;
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-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
*/
@ -13,7 +13,7 @@
text-align: center;
tr {
border: 1px var(--bg-secondary-400, var(--greyBackgroundColor)) solid;
border: 1px var(--greyBackgroundColor) solid;
}
td,
@ -34,6 +34,6 @@
}
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
*/
@ -9,5 +9,4 @@
@use "elements/index";
@use "video";
@use "configuration/configuration";
@use "admin/firewall/firewall";
@use "list-rooms/list-rooms";
@use "list-rooms/list-rooms.scss";

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
*/

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
*/
@ -18,31 +18,17 @@
/* Note: livechat-viewer-mode-content (the form where anonymous users can
choose nickname or log in with external account), can be something like
~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.
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 {
display: block;
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%;
min-width: min(400px, 25vw);
converse-muc {
min-height: max(30vh, 300px);
}
@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);
}
min-height: max(59vh, 400px);
}
}

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
*/

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: 2025 Mehdi Benadel <https://mehdibenadel.com>
// 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
@ -15,7 +15,7 @@ const clientFiles = [
'admin-plugin-client-plugin'
]
function loadLocs(globalFile) {
function loadLocs() {
// Loading english strings, so we can inject them as constants.
const refFile = path.resolve(__dirname, 'dist', 'languages', 'en.reference.json')
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.
const r = {}
const globalFile = path.resolve(__dirname, 'client', '@types', 'global.d.ts')
const globalFileContent = '' + fs.readFileSync(globalFile)
const matches = globalFileContent.matchAll(/^declare const LOC_(\w+)\b/gm)
for (const match of matches) {
@ -40,7 +41,7 @@ function loadLocs(globalFile) {
const define = Object.assign({
PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name),
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 => ({
entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ],
@ -58,14 +59,8 @@ const configs = clientFiles.map(f => ({
outfile: path.resolve(__dirname, 'dist/client', f + '.js'),
}))
const defineBuiltin = Object.assign(
{},
loadLocs(path.resolve(__dirname, 'conversejs', 'lib', '@types', 'global.d.ts'))
)
configs.push({
entryPoints: ["./conversejs/builtin.ts"],
define: defineBuiltin,
bundle: true,
minify: true,
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

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: 2025 Mehdi Benadel <https://mehdibenadel.com>
# SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
#
# 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.
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_sha256sum='83a583ac7036387514bed17afab257dab4161ccdd0ab7453818c78b51f830357'
x86_64_sha256sum='f4af9bfefa2f804ad7e8b03a68f04194abb801f070ae620b3d4bcedb144e8523'
aarch64_filename='prosody-aarch64.AppImage'
aarch64_sha256sum='7b7e6bf30d4498fc99a40022232c3065707ee4f4df24dc17947b007621634304'
aarch64_sha256sum='878c5be719e1e36a84d637fd2bd44e3059aa91ddb6906ad05f1dd0334078df09'
download_dir="$(pwd)/vendor/prosody-appimage"
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
@ -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.
// See the online documentation: https://livingston.frama.io/peertube-plugin-livechat/contributing/translate/
declare const LOC_ONLINE_HELP: string
declare const LOC_CHAT: string
declare const LOC_OPEN_CHAT: string
declare const LOC_OPEN_CHAT_NEW_WINDOW: 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_FORBIDDEN_WORDS_LABEL: 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_RETRACTATION_REASON_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2: 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_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL: 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_DESC: 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_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
@ -143,7 +143,7 @@ function register (clientOptions: RegisterClientOptions): void {
lastActivityEl.textContent = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
}
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.onclick = async () => {
await fetch(
@ -243,10 +243,7 @@ function register (clientOptions: RegisterClientOptions): void {
}
} catch (error: any) {
console.error(error)
peertubeHelpers.notifier.error(
(error as Error).toString(),
await peertubeHelpers.translate(LOC_LOADING_ERROR)
)
peertubeHelpers.notifier.error(error.toString(), await peertubeHelpers.translate(LOC_LOADING_ERROR))
}
}
})
@ -269,15 +266,10 @@ function register (clientOptions: RegisterClientOptions): void {
return options.formValues['prosody-components'] !== true
case 'converse-autocolors':
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':
return !(options.formValues['chat-all-lives'] === true && options.formValues['chat-per-live-video'] === true)
case 'auto-ban-anonymous-ip':
return options.formValues['chat-no-anonymous'] !== false
case 'prosody-firewall-configure-button':
return options.formValues['prosody-firewall-enabled'] !== true
}
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
@ -8,7 +8,6 @@ import { registerConfiguration } from './common/configuration/register'
import { registerVideoWatch } from './common/videowatch/register'
import { registerRoom } from './common/room/register'
import { initPtContext } from './common/lib/contexts/peertube'
import { registerAdminFirewall } from './common/admin/firewall/register'
import './common/lib/elements' // Import shared elements.
async function register (clientOptions: RegisterClientOptions): Promise<void> {
@ -46,7 +45,7 @@ async function register (clientOptions: RegisterClientOptions): Promise<void> {
])
const webchatFieldOptions: RegisterClientFormFieldOptions = {
name: 'livechat-active',
label,
label: label,
descriptionHTML: description,
type: 'input-checkbox',
default: true,
@ -70,8 +69,7 @@ async function register (clientOptions: RegisterClientOptions): Promise<void> {
await Promise.all([
registerVideoWatch(),
registerRoom(clientOptions),
registerConfiguration(clientOptions),
registerAdminFirewall(clientOptions)
registerConfiguration(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-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
@ -32,7 +32,7 @@ export class ChannelConfigurationElement extends LivechatElement {
public validationError?: ValidationError
@state()
public actionDisabled = false
public actionDisabled: boolean = false
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 =
this.validationError?.properties[propertyName]
this.validationError?.properties[`${propertyName}`]
return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {}
}
@ -123,7 +123,7 @@ export class ChannelConfigurationElement extends LivechatElement {
propertyName: string): TemplateResult | typeof nothing => {
const errorMessages: TemplateResult[] = []
const validationErrorTypes: ValidationErrorType[] | undefined =
this.validationError?.properties[propertyName] ?? undefined
this.validationError?.properties[`${propertyName}`] ?? undefined
// FIXME: this code is duplicated in dymamic table form
if (validationErrorTypes && validationErrorTypes.length !== 0) {

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
@ -30,7 +30,7 @@ export class ChannelEmojisElement extends LivechatElement {
public validationError?: ValidationError
@state()
public actionDisabled = false
public actionDisabled: boolean = false
private _asyncTaskRender: Task
@ -192,7 +192,7 @@ export class ChannelEmojisElement extends LivechatElement {
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 item: ChannelEmojisConfiguration['emojis']['customEmojis'][0] = {
@ -211,7 +211,7 @@ export class ChannelEmojisElement extends LivechatElement {
await this.ptTranslate(LOC_ACTION_IMPORT_EMOJIS_INFO)
)
} 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 {
this.actionDisabled = false
}
@ -250,27 +250,12 @@ export class ChannelEmojisElement extends LivechatElement {
a.remove()
} catch (err: any) {
this.logger.error(err)
this.ptNotifier.error((err as Error).toString())
this.ptNotifier.error(err.toString())
} finally {
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.
* @param url the url

View File

@ -2,9 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { html } from 'lit'
import { customElement, state } from 'lit/decorators.js'
import { ptTr } from '../../lib/directives/translation'
@ -53,7 +50,7 @@ export class ChannelHomeElement extends LivechatElement {
<ul class="peertube-plugin-livechat-configuration-home-channels">
${this._channels?.map((channel) => html`
<li>
<a href="${channel.livechatConfigurationUri}" aria-hidden="true">
<a href="${channel.livechatConfigurationUri}">
${channel.avatar
? html`<img class="avatar channel" src="${channel.avatar.path}">`
: 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
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { LivechatElement } from '../../lib/elements/livechat'
import { ptTr } from '../../lib/directives/translation'
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-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
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import type { ChannelConfigurationElement } from '../channel-configuration'
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
import { ptTr } from '../../../lib/directives/translation'
import { html, TemplateResult } from 'lit'
import { classMap } from 'lit/directives/class-map.js'
import { noDuplicateMaxDelay, forbidSpecialCharsMaxTolerance } from 'shared/lib/constants'
export function tplChannelConfiguration (el: ChannelConfigurationElement): TemplateResult {
const tableHeaderList: Record<string, DynamicFormHeader> = {
const tableHeaderList: {[key: string]: DynamicFormHeader} = {
forbiddenWords: {
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: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC)
},
applyToModerators: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC)
},
label: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC)
},
reason: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC)
},
comments: {
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: {
entries: {
inputType: 'tags',
@ -96,8 +93,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
},
delay: {
inputType: 'number',
default: 10,
min: 1
default: 10
}
},
commands: {
@ -139,7 +135,6 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
</livechat-configuration-section-header>
<div class="form-group">
<textarea
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TERMS_LABEL) as any}
name="terms"
id="peertube-livechat-terms"
.value=${el.channelConfiguration?.configuration.terms ?? ''}
@ -172,7 +167,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
<label>
<input
type="checkbox"
name="mute_anonymous"
name="bot"
id="peertube-livechat-mute-anonymous"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
@ -259,32 +254,6 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
${el.renderFeedback('peertube-livechat-moderation-delay-feedback', 'moderation.delay')}
</div>
<livechat-configuration-section-header
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_DESC, true)}
.helpPage=${'documentation/user/streamers/moderation'}>
</livechat-configuration-section-header>
<div class="form-group">
<label>
<input
type="checkbox"
name="anonymize-moderation"
id="peertube-livechat-anonymize-moderation"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.moderation.anonymize =
(event.target as HTMLInputElement).checked
}
el.requestUpdate('channelConfiguration')
}
}
value="1"
?checked=${el.channelConfiguration?.configuration.moderation.anonymize}
/>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL)}
</label>
</div>
<livechat-configuration-section-header
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
.description=${''}
@ -341,246 +310,6 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
${el.renderFeedback('peertube-livechat-bot-nickname-feedback', 'bot.nickname')}
</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
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
.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
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import type { ChannelEmojisElement } from '../channel-emojis'
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
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>
<h2>${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_TITLE)}</h2>
<p>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_DESC)}
<livechat-help-button .page=${'documentation/user/streamers/emojis'}>
</livechat-help-button>
</p>
<form role="form" @submit=${el.saveEmojis} @change=${el.resetValidation}>
<div class="peertube-plugin-livechat-configuration-actions">
${
@ -90,7 +86,7 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
.maxLines=${maxEmojisPerChannel}
.validation=${el.validationError?.properties}
.validationPrefix=${'emojis'}
.rows=${el.channelEmojisConfiguration?.emojis.customEmojis ?? []}
.rows=${el.channelEmojisConfiguration?.emojis.customEmojis}
@update=${(e: CustomEvent) => {
el.resetValidation(e)
if (el.channelEmojisConfiguration) {
@ -110,23 +106,5 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
</button>
</div>
</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>`
}

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-License-Identifier: AGPL-3.0-only
@ -66,16 +66,15 @@ async function registerConfiguration (clientOptions: RegisterClientOptions): Pro
for (const link of links) {
if (typeof link !== 'object') { continue }
if (!('key' in link)) { continue }
if (link.key === 'in-my-library' || link.key === 'my-video-space') {
myLibraryLinks = link
break
}
if (link.key !== 'in-my-library') { continue }
myLibraryLinks = link
break
}
if (!myLibraryLinks) { return links }
if (!Array.isArray(myLibraryLinks.links)) { return links }
const label = await peertubeHelpers.translate(LOC_MENU_CONFIGURATION_LABEL)
myLibraryLinks.links.unshift({
myLibraryLinks.links.push({
label,
shortLabel: label,
path: '/p/livechat/configuration',

View File

@ -1,5 +1,5 @@
// 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
@ -11,7 +11,7 @@ import type {
import { ValidationError, ValidationErrorType } from '../../lib/models/validation'
import { getBaseRoute } from '../../../utils/uri'
import { maxEmojisPerChannel } from 'shared/lib/emojis'
import { channelTermsMaxLength, noDuplicateMaxDelay, forbidSpecialCharsMaxTolerance } from 'shared/lib/constants'
import { channelTermsMaxLength } from 'shared/lib/constants'
export class ChannelDetailsService {
public _registerClientOptions: RegisterClientOptions
@ -67,43 +67,11 @@ export class ChannelDetailsService {
// The backend will ignore those values.
if (botConf.enabled) {
propertiesError['bot.nickname'] = []
propertiesError['bot.forbidSpecialChars.tolerance'] = []
propertiesError['bot.noDuplicate.delay'] = []
if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) {
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 v of fw.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()) {
propertiesError[`bot.commands.${i}.command`] = []
@ -151,29 +110,6 @@ export class ChannelDetailsService {
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,
channelConfigurationOptions: ChannelConfigurationOptions): Promise<Response> => {
if (!await this.validateOptions(channelConfigurationOptions)) {
@ -185,21 +121,11 @@ export class ChannelDetailsService {
{
method: 'POST',
headers: this._headers,
body: JSON.stringify(
this.frontToBack(channelConfigurationOptions)
)
body: JSON.stringify(channelConfigurationOptions)
}
)
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.')
}
@ -220,8 +146,7 @@ export class ChannelDetailsService {
}
for (const channel of channels.data) {
channel.livechatConfigurationUri =
'/p/livechat/configuration/channel?channelId=' + encodeURIComponent(channel.id as string | number)
channel.livechatConfigurationUri = '/p/livechat/configuration/channel?channelId=' + encodeURIComponent(channel.id)
// 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.
@ -251,15 +176,14 @@ export class ChannelDetailsService {
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> {
const url = getBaseRoute(this._registerClientOptions) +
'/api/configuration/channel/emojis/' +
encodeURIComponent(channelId)
const response = await fetch(
url,
getBaseRoute(this._registerClientOptions) +
'/api/configuration/channel/emojis/' +
encodeURIComponent(channelId),
{
method: 'GET',
headers: this._headers
@ -371,11 +295,10 @@ export class ChannelDetailsService {
channelId: number,
channelEmojis: ChannelEmojis
): Promise<ChannelEmojisConfiguration> {
const url = getBaseRoute(this._registerClientOptions) +
'/api/configuration/channel/emojis/' +
encodeURIComponent(channelId)
const response = await fetch(
url,
getBaseRoute(this._registerClientOptions) +
'/api/configuration/channel/emojis/' +
encodeURIComponent(channelId),
{
method: 'POST',
headers: this._headers,
@ -389,24 +312,4 @@ export class ChannelDetailsService {
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-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
// 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"
aria-hidden="true"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-plus-square">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
@ -14,9 +13,8 @@ export const AddSVG =
</svg>`
// 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"
aria-hidden="true"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-x-square">
<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.
*/
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
}
}

View File

@ -13,8 +13,8 @@ import { getPtContext } from '../contexts/peertube'
export class TranslationDirective extends AsyncDirective {
private readonly _peertubeHelpers: RegisterClientHelpers
private _translatedValue = ''
private _localizationId = ''
private _translatedValue: string = ''
private _localizationId: string = ''
private _allowUnsafeHTML = false
@ -25,7 +25,7 @@ export class TranslationDirective extends AsyncDirective {
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._allowUnsafeHTML = allowHTML

View File

@ -1,11 +1,8 @@
// 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
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { ptTr } from '../directives/translation'
import { html } from 'lit'
import { customElement, property } from 'lit/decorators.js'

View File

@ -1,11 +1,8 @@
// 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
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import type { TagsInputElement } from './tags-input'
import type { DirectiveResult } from 'lit/directive'
import { ValidationErrorType } from '../models/validation'
@ -23,26 +20,26 @@ import { AddSVG, RemoveSVG } from '../buttons'
type DynamicTableAcceptedTypes = number | string | boolean | Date | Array<number | string>
type DynamicTableAcceptedInputTypes = 'textarea'
| 'select'
| 'checkbox'
| 'range'
| 'color'
| 'date'
| 'datetime'
| 'datetime-local'
| 'email'
| 'file'
| 'image'
| 'month'
| 'number'
| 'password'
| 'tel'
| 'text'
| 'time'
| 'url'
| 'week'
| 'tags'
| 'image-file'
| 'select'
| 'checkbox'
| 'range'
| 'color'
| 'date'
| 'datetime'
| 'datetime-local'
| 'email'
| 'file'
| 'image'
| 'month'
| 'number'
| 'password'
| 'tel'
| 'text'
| 'time'
| 'url'
| 'week'
| 'tags'
| 'image-file'
interface CellDataSchema {
min?: number
@ -50,11 +47,11 @@ interface CellDataSchema {
minlength?: number
maxlength?: number
size?: number
options?: Record<string, string>
label?: TemplateResult | string
options?: { [key: string]: string }
datalist?: DynamicTableAcceptedTypes[]
separator?: string
inputType?: DynamicTableAcceptedInputTypes
inputTitle?: string
default?: DynamicTableAcceptedTypes
colClassList?: string[] // CSS classes to add to the <td> element.
}
@ -62,17 +59,19 @@ interface CellDataSchema {
interface DynamicTableRowData {
_id: number
_originalIndex: number
row: Record<string, DynamicTableAcceptedTypes>
row: { [key: string]: DynamicTableAcceptedTypes }
}
interface DynamicFormHeaderCellData {
colName: TemplateResult | DirectiveResult
description?: TemplateResult | DirectiveResult
description: TemplateResult | DirectiveResult
headerClassList?: string[]
}
export type DynamicFormHeader = Record<string, DynamicFormHeaderCellData>
export type DynamicFormSchema = Record<string, CellDataSchema>
export interface DynamicFormHeader {
[key: string]: DynamicFormHeaderCellData
}
export interface DynamicFormSchema { [key: string]: CellDataSchema }
@customElement('livechat-dynamic-table-form')
export class DynamicTableFormElement extends LivechatElement {
@ -86,19 +85,19 @@ export class DynamicTableFormElement extends LivechatElement {
public maxLines?: number = undefined
@property()
public validation?: Record<string, ValidationErrorType[]>
public validation?: {[key: string]: ValidationErrorType[] }
@property({ attribute: false })
public validationPrefix = ''
public validationPrefix: string = ''
@property({ attribute: false })
public rows: Array<Record<string, DynamicTableAcceptedTypes>> = []
public rows: Array<{ [key: string]: DynamicTableAcceptedTypes }> = []
@state()
public _rowsById: DynamicTableRowData[] = []
@property({ attribute: false })
public formName = ''
public formName: string = ''
@state()
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()
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)
}
return html`<th scope="col" class=${classList.join(' ')}>
${headerCellData.description ?? ''}
${headerCellData.description}
</th>`
}
@ -246,11 +245,11 @@ export class DynamicTableFormElement extends LivechatElement {
return html`<tr id=${inputId}>
${Object.keys(this.header)
.sort((k1, k2) => this.columnOrder.indexOf(k1) - this.columnOrder.indexOf(k2))
.map(key => this.renderDataCell(key,
rowData.row[key] ?? this.schema[key].default,
rowData._id,
rowData._originalIndex))}
.sort((k1, k2) => this.columnOrder.indexOf(k1) - this.columnOrder.indexOf(k2))
.map(key => this.renderDataCell(key,
rowData.row[key] ?? this.schema[key].default,
rowData._id,
rowData._originalIndex))}
<td class="form-group">
<button type="button"
class="dynamic-table-remove-row"
@ -296,7 +295,6 @@ export class DynamicTableFormElement extends LivechatElement {
const inputId =
`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)
switch (propertySchema.default?.constructor) {
@ -322,7 +320,6 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderInput(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
propertyValue as string,
@ -335,7 +332,6 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderTextarea(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
propertyValue as string,
@ -348,7 +344,6 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderSelect(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
propertyValue as string,
@ -361,7 +356,6 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderImageFileInput(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
propertyValue?.toString(),
@ -382,7 +376,6 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderInput(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
(propertyValue as Date).toISOString(),
@ -401,7 +394,6 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderInput(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
propertyValue as string,
@ -419,7 +411,6 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderCheckbox(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
propertyValue as boolean,
@ -455,10 +446,10 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderInput(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
(propertyValue)?.join(propertySchema.separator ?? ',') ?? propertyValue ?? propertySchema.default ?? '',
(propertyValue)?.join(propertySchema.separator ?? ',') ??
propertyValue ?? propertySchema.default ?? '',
originalIndex)}
${feedback}
`
@ -470,10 +461,10 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderTextarea(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
(propertyValue)?.join(propertySchema.separator ?? ',') ?? propertyValue ?? propertySchema.default ?? '',
(propertyValue)?.join(propertySchema.separator ?? ',') ??
propertyValue ?? propertySchema.default ?? '',
originalIndex)}
${feedback}
`
@ -485,7 +476,6 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderTagsInput(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
propertyValue,
@ -497,10 +487,8 @@ export class DynamicTableFormElement extends LivechatElement {
}
if (!formElement) {
this.logger.warn(
`value type '${(propertyValue.constructor.toString())}' is incompatible` +
`with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`
)
this.logger.warn(`value type '${(propertyValue.constructor.toString())}' is incompatible` +
`with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`)
}
const classList = ['form-group']
@ -513,7 +501,6 @@ export class DynamicTableFormElement extends LivechatElement {
_renderInput = (rowId: number,
inputId: string,
inputName: string,
inputTitle: string | DirectiveResult | undefined,
propertyName: string,
propertySchema: CellDataSchema,
propertyValue: string,
@ -528,7 +515,6 @@ export class DynamicTableFormElement extends LivechatElement {
)
)}
id=${inputId}
title=${ifDefined(inputTitle)}
aria-describedby="${inputId}-feedback"
list=${ifDefined(propertySchema.datalist ? inputId + '-datalist' : undefined)}
min=${ifDefined(propertySchema.min)}
@ -548,7 +534,6 @@ export class DynamicTableFormElement extends LivechatElement {
_renderTagsInput = (rowId: number,
inputId: string,
inputName: string,
inputTitle: string | DirectiveResult | undefined,
propertyName: string,
propertySchema: CellDataSchema,
propertyValue: Array<string | number>,
@ -562,7 +547,7 @@ export class DynamicTableFormElement extends LivechatElement {
)
)}
id=${inputId}
.inputTitle=${inputTitle as any}
.inputPlaceholder=${propertySchema.label as any}
aria-describedby="${inputId}-feedback"
.min=${propertySchema.min}
.max=${propertySchema.max}
@ -578,7 +563,6 @@ export class DynamicTableFormElement extends LivechatElement {
_renderTextarea = (rowId: number,
inputId: string,
inputName: string,
inputTitle: string | DirectiveResult | undefined,
propertyName: string,
propertySchema: CellDataSchema,
propertyValue: string,
@ -592,7 +576,6 @@ export class DynamicTableFormElement extends LivechatElement {
)
)}
id=${inputId}
title=${ifDefined(inputTitle)}
aria-describedby="${inputId}-feedback"
min=${ifDefined(propertySchema.min)}
max=${ifDefined(propertySchema.max)}
@ -605,7 +588,6 @@ export class DynamicTableFormElement extends LivechatElement {
_renderCheckbox = (rowId: number,
inputId: string,
inputName: string,
inputTitle: string | DirectiveResult | undefined,
propertyName: string,
propertySchema: CellDataSchema,
propertyValue: boolean,
@ -620,7 +602,6 @@ export class DynamicTableFormElement extends LivechatElement {
)
)}
id=${inputId}
title=${ifDefined(inputTitle)}
aria-describedby="${inputId}-feedback"
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
value="1"
@ -630,7 +611,6 @@ export class DynamicTableFormElement extends LivechatElement {
_renderSelect = (rowId: number,
inputId: string,
inputName: string,
inputTitle: string | DirectiveResult | undefined,
propertyName: string,
propertySchema: CellDataSchema,
propertyValue: string,
@ -643,12 +623,11 @@ export class DynamicTableFormElement extends LivechatElement {
)
)}
id=${inputId}
title=${ifDefined(inputTitle)}
aria-describedby="${inputId}-feedback"
aria-label=${inputName}
@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 ?? {})
?.map(([value, name]) =>
html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`
@ -659,7 +638,6 @@ export class DynamicTableFormElement extends LivechatElement {
_renderImageFileInput = (rowId: number,
inputId: string,
inputName: string,
inputTitle: string | DirectiveResult | undefined,
propertyName: string,
propertySchema: CellDataSchema,
propertyValue: string,
@ -669,7 +647,6 @@ export class DynamicTableFormElement extends LivechatElement {
.name=${inputName}
class=${classMap(this._getInputValidationClass(propertyName, originalIndex))}
id=${inputId}
.inputTitle=${inputTitle as any}
aria-describedby="${inputId}-feedback"
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
.value=${propertyValue}
@ -679,7 +656,7 @@ export class DynamicTableFormElement extends LivechatElement {
}
_getInputValidationClass = (propertyName: string,
originalIndex: number): Record<string, boolean> => {
originalIndex: number): { [key: string]: boolean } => {
const validationErrorTypes: ValidationErrorType[] | undefined =
this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`]
@ -753,9 +730,6 @@ export class DynamicTableFormElement extends LivechatElement {
.split(propertySchema.separator)
}
break
case Number:
rowById.row[propertyName] = Number(value)
break
default:
rowById.row[propertyName] = value
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

View File

@ -18,7 +18,7 @@ export class HelpButtonElement extends LivechatElement {
public buttonTitle: string | DirectiveResult = ptTr(LOC_ONLINE_HELP)
@property({ attribute: false })
public page = ''
public page: string = ''
@state()
public url: URL = new URL('https://lmddgtfy.net/')
@ -38,7 +38,7 @@ export class HelpButtonElement extends LivechatElement {
href="${this.url.href}"
target=_blank
title="${this.buttonTitle}"
class="primary-button orange-button peertube-button-link"
class="orange-button peertube-button-link"
>${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
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { LivechatElement } from './livechat'
import { html } from 'lit'
import type { DirectiveResult } from 'lit/directive'
import { customElement, property } from 'lit/decorators.js'
import { ifDefined } from 'lit/directives/if-defined.js'
/**
* Special element to upload image files.
* If no current value, displays an input type="file" field.
@ -33,16 +29,13 @@ export class ImageFileInputElement extends LivechatElement {
@property({ attribute: false })
public maxSize?: number
@property({ attribute: false })
public inputTitle?: string | DirectiveResult
@property({ attribute: false })
public accept: string[] = ['image/jpg', 'image/png', 'image/gif']
protected override render = (): unknown => {
return html`
${this.value
? html`<img src=${this.value} alt=${ifDefined(this.inputTitle)} @click=${(ev: Event) => {
? html`<img src=${this.value} @click=${(ev: Event) => {
ev.preventDefault()
const upload: HTMLInputElement | null | undefined = this.parentElement?.querySelector('input[type="file"]')
upload?.click()
@ -51,7 +44,6 @@ export class ImageFileInputElement extends LivechatElement {
}
<input
type="file"
title=${ifDefined(this.inputTitle)}
accept="${this.accept.join(',')}"
class="form-control"
style=${this.value ? 'display: none;' : ''}

View File

@ -1,5 +1,5 @@
// 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

View File

@ -1,5 +1,5 @@
// 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
@ -26,7 +26,7 @@ export class LivechatElement extends LitElement {
this.logger = this.ptContext.logger.createLogger(this.tagName.toLowerCase())
}
protected override createRenderRoot = (): HTMLElement | DocumentFragment => {
protected override createRenderRoot = (): Element | ShadowRoot => {
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

View File

@ -1,11 +1,8 @@
// 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
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { LivechatElement } from './livechat'
import { ptTr } from '../directives/translation'
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 { animate, fadeOut, fadeIn } from '@lit-labs/motion'
import { repeat } from 'lit/directives/repeat.js'
import type { DirectiveResult } from 'lit/directive'
// FIXME: find a better way to store this image.
// 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»
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">` +
// 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)"/>' +
// 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)"/>' +
// eslint-disable-next-line @stylistic/indent-binary-ops
`</g>
</svg>`
@ -53,7 +48,7 @@ export class TagsInputElement extends LivechatElement {
private _inputValue?: string = ''
@property({ attribute: false })
public inputTitle?: string | DirectiveResult = ''
public inputPlaceholder?: string = ''
@property({ attribute: false })
public datalist?: string[]
@ -68,10 +63,10 @@ export class TagsInputElement extends LivechatElement {
private readonly _isPressingKey: string[] = []
@property({ attribute: false })
public separator = '\n'
public separator: string = '\n'
@property({ attribute: false })
public animDuration = 200
public animDuration: number = 200
/**
* Overloading the standard focus method.
@ -171,7 +166,7 @@ export class TagsInputElement extends LivechatElement {
@input=${(e: InputEvent) => this._handleInputEvent(e)}
@change=${(e: Event) => e.stopPropagation()}
.value=${this._inputValue ?? ''}
title=${ifDefined(this.inputTitle)} />
placeholder=${ifDefined(this.inputPlaceholder)} />
${(this.datalist)
? html`<datalist id="${this.id ?? 'tags-input'}-datalist">
${(this.datalist ?? []).map((value) => html`<option value=${value}>`)}
@ -249,9 +244,8 @@ export class TagsInputElement extends LivechatElement {
if (!this._isPressingKey.includes(e.key)) {
this._isPressingKey.push(e.key)
if (
(target.selectionStart === target.selectionEnd) && target.selectionStart === 0
) {
if ((target.selectionStart === target.selectionEnd) &&
target.selectionStart === 0) {
this._handleDeleteTag((this._searchedTagsIndex.length)
? this._searchedTagsIndex.slice(-1)[0]
: (this.value.length - 1))
@ -264,9 +258,8 @@ export class TagsInputElement extends LivechatElement {
if (!this._isPressingKey.includes(e.key)) {
this._isPressingKey.push(e.key)
if (
(target.selectionStart === target.selectionEnd) && target.selectionStart === target.value.length
) {
if ((target.selectionStart === target.selectionEnd) &&
target.selectionStart === target.value.length) {
this._handleDeleteTag((this._searchedTagsIndex.length)
? this._searchedTagsIndex[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
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import type { LivechatTokenListElement } from '../token-list'
import { html, TemplateResult } from 'lit'
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
@ -26,11 +23,11 @@ export function tplTokenList (el: LivechatTokenListElement): TemplateResult {
<tbody>
${
repeat(el.tokenList ?? [], (token) => token.id, (token) => {
let dateStr = ''
let dateStr: string = ''
try {
const date = new Date(token.date)
dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
} catch (_err) {}
} catch (err) {}
return html`<tr>
<td>${
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
@ -27,7 +27,7 @@ export class LivechatTokenListElement extends LivechatElement {
public currentSelectedToken?: LivechatToken
@property({ attribute: false })
public actionDisabled = false
public actionDisabled: boolean = false
private readonly _tokenListService: TokenListService
private readonly _asyncTaskRender: Task
@ -83,7 +83,7 @@ export class LivechatTokenListElement extends LivechatElement {
this.dispatchEvent(new CustomEvent('update', {}))
} catch (err: any) {
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 {
this.actionDisabled = false
}
@ -102,7 +102,7 @@ export class LivechatTokenListElement extends LivechatElement {
this.dispatchEvent(new CustomEvent('update', {}))
} catch (err: any) {
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 {
this.actionDisabled = false
}

View File

@ -12,7 +12,7 @@ export enum ValidationErrorType {
}
export class ValidationError extends Error {
properties: Record<string, ValidationErrorType[]> = {}
properties: {[key: string]: ValidationErrorType[] } = {}
constructor (name: string, message: string | undefined, properties: ValidationError['properties']) {
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

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

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
@ -27,7 +27,7 @@ type displayButtonOptions = displayButtonOptionsCallback | displayButtonOptionsH
function displayButton (dbo: displayButtonOptions): void {
const button = document.createElement('a')
button.classList.add(
'primary-button', 'orange-button', 'peertube-button-link',
'orange-button', 'peertube-button-link',
'peertube-plugin-livechat-button',
'peertube-plugin-livechat-button-' + dbo.name
)
@ -42,23 +42,6 @@ function displayButton (dbo: displayButtonOptions): void {
if ('href' in dbo) {
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) {
button.target = '_blank'
}
@ -69,10 +52,6 @@ function displayButton (dbo: displayButtonOptions): void {
tmp.innerHTML = svg.trim()
const svgDom = tmp.firstChild
if (svgDom) {
if ('ariaHidden' in (svgDom as HTMLElement)) {
// Icon must be hidden for screen readers.
(svgDom as HTMLElement).ariaHidden = 'true'
}
button.prepend(svgDom)
}
} 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

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
@ -16,6 +16,8 @@ import { localizedHelpUrl } from '../../utils/help'
import { getBaseRoute } from '../../utils/uri'
import { displayConverseJS } from '../../utils/conversejs'
let savedMyPluginFlexGrow: string | undefined
/**
* Initialize the chat for the current video
* @param video the video
@ -23,6 +25,7 @@ import { displayConverseJS } from '../../utils/conversejs'
async function initChat (video: Video): Promise<void> {
const ptContext = getPtContext()
const logger = ptContext.logger
savedMyPluginFlexGrow = undefined
if (!video) {
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('peertube-plugin-livechat-state', 'initializing')
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)
try {
@ -60,8 +61,8 @@ async function initChat (video: Video): Promise<void> {
return
}
let showShareUrlButton = false
let showPromote = false
let showShareUrlButton: boolean = false
let showPromote: boolean = false
if (video.isLocal) { // No need for shareButton on remote chats.
const chatShareUrl = settings['chat-share-url'] ?? ''
if (chatShareUrl === 'everyone') {
@ -187,10 +188,9 @@ async function _insertChatDom (
callback: async () => {
try {
// 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(
url,
getBaseRoute(ptContext.ptOptions) + '/api/configuration/room/' +
encodeURIComponent(video.uuid),
{
method: 'GET',
headers: peertubeHelpers.getAuthHeader()
@ -304,7 +304,7 @@ async function _openChat (video: Video): Promise<void | false> {
// Loading converseJS...
await displayConverseJS(ptContext.ptOptions, container, roomkey, 'peertube-video', false)
} catch (_err) {
} catch (err) {
// Displaying an error page.
if (container) {
const message = document.createElement('div')
@ -353,6 +353,19 @@ function _hackStyles (on: boolean): void {
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) {
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

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
@ -14,7 +14,7 @@ import { getIframeUri, getXMPPAddr, UriOptions } from '../uri'
import { isAnonymousUser } from '../../../utils/user'
// 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]
@ -61,49 +61,49 @@ export class ShareChatElement extends LivechatElement {
* Should we render the XMPP tab?
*/
@property({ attribute: false })
public xmppUriEnabled = false
public xmppUriEnabled: boolean = false
/**
* Should we render the Dock tab?
*/
@property({ attribute: false })
public dockEnabled = false
public dockEnabled: boolean = false
/**
* Can we use autocolors?
*/
@property({ attribute: false })
public autocolorsAvailable = false
public autocolorsAvailable: boolean = false
/**
* In the Embed tab, should we generated an iframe link.
*/
@property({ attribute: false })
public embedIFrame = false
public embedIFrame: boolean = false
/**
* In the Embed tab, should we generated a read-only chat link.
*/
@property({ attribute: false })
public embedReadOnly = false
public embedReadOnly: boolean = false
/**
* Read-only, with scrollbar?
*/
@property({ attribute: false })
public embedReadOnlyScrollbar = false
public embedReadOnlyScrollbar: boolean = false
/**
* Read-only, transparent background?
*/
@property({ attribute: false })
public embedReadOnlyTransparentBackground = false
public embedReadOnlyTransparentBackground: boolean = false
/**
* In the Embed tab, should we use current theme color?
*/
@property({ attribute: false })
public embedAutocolors = false
public embedAutocolors: boolean = false
protected override firstUpdated (changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties)
@ -156,7 +156,7 @@ export class ShareChatElement extends LivechatElement {
return
}
this.logger.log('Restoring previous state')
if (validTabNames.includes(v.currentTab as string)) {
if (validTabNames.includes(v.currentTab)) {
this.currentTab = v.currentTab
}
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
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import type { ShareChatElement } from '../share-chat'
import { html, TemplateResult } from 'lit'
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

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
@ -71,7 +71,8 @@ async function shareChatUrl (
addedNodes.forEach(node => {
if ((node as HTMLElement).localName === 'ngb-modal-window') {
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)) {
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
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import type { Video } from '@peertube/peertube-types'
import type { LiveChatSettings } from '../lib/contexts/peertube'
import { AutoColors, isAutoColorsAvailable } from 'shared/lib/autocolors'
import { getBaseRoute } from '../../utils/uri'
import { logger } from '../../utils/logger'
@ -18,7 +17,7 @@ interface UriOptions {
}
function getIframeUri (
registerOptions: RegisterClientOptions, settings: LiveChatSettings, video: Video, uriOptions: UriOptions = {}
registerOptions: RegisterClientOptions, settings: any, video: Video, uriOptions: UriOptions = {}
): string | null {
if (!settings) {
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