This commit is contained in:
matty 2024-12-03 17:03:42 -05:00
commit a4bf37d534
231 changed files with 18277 additions and 11174 deletions

View File

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

40
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,40 @@
# SPDX-FileCopyrightText: 2024 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: 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.

34
.github/workflows/gh-build.yml vendored Normal file
View File

@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: 2024 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

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
'use strict'; 'use strict'
module.exports = { module.exports = {
extends: [ extends: [
@ -14,7 +14,7 @@ module.exports = {
// extending the kebab-case to accept ConverseJS class names. // extending the kebab-case to accept ConverseJS class names.
'^([a-z][a-z0-9]*)(-[a-z0-9]+)*((__|--)[a-z]+(-[a-z0-9]+)*)?$', '^([a-z][a-z0-9]*)(-[a-z0-9]+)*((__|--)[a-z]+(-[a-z0-9]+)*)?$',
{ {
message: 'Expected class selector to be kebab-case, or ConverseJS-style.', message: 'Expected class selector to be kebab-case, or ConverseJS-style.'
} }
] ]
} }

View File

@ -1,5 +1,34 @@
# Changelog # Changelog
## 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 ## 11.0.1
### Minor changes and fixes ### Minor changes and fixes

View File

@ -15,7 +15,7 @@
/* See Peertube sub-menu-h1 mixin */ /* See Peertube sub-menu-h1 mixin */
font-size: 1.3rem; font-size: 1.3rem;
border-bottom: 2px solid var(--greyBackgroundColor); border-bottom: 2px solid var(--bg-secondary-400, var(--greyBackgroundColor));
padding-bottom: 15px; padding-bottom: 15px;
} }
@ -42,45 +42,49 @@
input[type="submit"], input[type="submit"],
button[type="submit"] { button[type="submit"] {
// Peertube orange-button mixin
&, &,
&:active, &:active,
&.active,
&:focus { &:focus {
color: #fff; color: var(--on-primary, #fff);
background-color: var(--mainColor); background-color: var(--primary, var(--mainColor));
border: 1px solid var(--primary, var(--mainColor));
} }
&:hover { &:hover {
color: #fff; color: var(--on-primary, #fff);
background-color: var(--mainHoverColor); background-color: var(--primary-400, var(--mainHoverColor));
} }
&[disabled], &[disabled] {
&.disabled { pointer-events: none;
cursor: default; opacity: 0.6;
color: #fff;
background-color: var(--inputBorderColor);
} }
} }
input[type="reset"], input[type="reset"],
button[type="reset"] { button[type="reset"] {
// Peertube grey-button mixin color: var(--fg, var(--mainForegroundColor));
background-color: var(--greyBackgroundColor); background-color: transparent;
color: var(--greyForegroundColor); border: 1px solid var(--bg-secondary-500, var(--inputBorderColor)) !important;
&:hover,
&:active, &:active,
&.active,
&:focus, &:focus,
&[disabled], &:focus-visible {
&.disabled { color: var(--fg, var(--mainForegroundColor));
color: var(--greyForegroundColor); background-color: var(--bg-secondary-500, var(--inputBorderColor));
background-color: var(--greySecondaryBackgroundColor); border-color: var(--bg-secondary-500, var(--inputBorderColor));
} }
&[disabled], &:hover {
&.disabled { color: var(--fg, var(--mainForegroundColor));
cursor: default; background-color: var(--bg-secondary-450, var(--inputBorderColor));
}
&[disabled] {
pointer-events: none;
opacity: 0.8;
} }
} }

View File

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

View File

@ -56,7 +56,7 @@ livechat-share-chat {
&.livechat-shareurl-suboptions-disabled { &.livechat-shareurl-suboptions-disabled {
label { label {
/* stylelint-disable-next-line custom-property-pattern */ /* stylelint-disable-next-line custom-property-pattern */
color: var(--greyForegroundColor); color: var(--fg-400, var(--greyForegroundColor));
} }
} }
} }

View File

@ -15,9 +15,9 @@ livechat-spinner,
height: 48px; height: 48px;
margin: 20px; margin: 20px;
/* stylelint-disable-next-line custom-property-pattern */ /* stylelint-disable-next-line custom-property-pattern */
border: 5px solid var(--greyBackgroundColor) !important; // !important is required for it to work in ConverseJS border: 5px solid var(--bg-secondary-400, var(--greyBackgroundColor)) !important; // !important is required for it to work in ConverseJS
/* stylelint-disable-next-line custom-property-pattern */ /* stylelint-disable-next-line custom-property-pattern */
border-bottom-color: var(--mainColor) !important; // !important is required for it to work in ConverseJS border-bottom-color: var(--primary, var(--mainColor)) !important; // !important is required for it to work in ConverseJS
border-radius: 50%; border-radius: 50%;
display: inline-block; display: inline-block;
box-sizing: border-box; box-sizing: border-box;

View File

@ -51,14 +51,14 @@ livechat-tags-input {
transition-duration: 0.3s; transition-duration: 0.3s;
@supports (scrollbar-width: auto) { @supports (scrollbar-width: auto) {
scrollbar-color: var(--greyForegroundColor) transparent; scrollbar-color: var(--fg-400, var(--greyForegroundColor)) transparent;
scrollbar-width: thin; scrollbar-width: thin;
} }
} }
.livechat-tags-container, .livechat-tags-container,
.livechat-tags-searched { .livechat-tags-searched {
border-bottom: 1px dashed var(--greyForegroundColor); border-bottom: 1px dashed var(--fg-400, var(--greyForegroundColor));
&.livechat-empty { &.livechat-empty {
height: 0; height: 0;
@ -104,7 +104,7 @@ livechat-tags-input {
text-align: center; text-align: center;
font-size: 10px; font-size: 10px;
margin-left: var(--tag-padding-horizontal); margin-left: var(--tag-padding-horizontal);
color: var(--mainColor); color: var(--primary, var(--mainColor));
border-radius: 50%; border-radius: 50%;
background: #fff; background: #fff;
cursor: pointer; cursor: pointer;
@ -118,19 +118,19 @@ livechat-tags-input {
&:active, &:active,
&:focus { &:focus {
color: #fff; color: #fff;
background-color: var(--mainColor); background-color: var(--primary, var(--mainColor));
.livechat-tag-close { .livechat-tag-close {
color: var(--mainColor); color: var(--primary, var(--mainColor));
} }
} }
&:hover { &:hover {
color: #fff; color: #fff;
background-color: var(--mainHoverColor); background-color: var(--fg-400, var(--mainHoverColor));
.livechat-tag-close { .livechat-tag-close {
color: var(--mainHoverColor); color: var(--fg-400, var(--mainHoverColor));
} }
} }
@ -138,10 +138,10 @@ livechat-tags-input {
&.disabled { &.disabled {
cursor: default; cursor: default;
color: #fff; color: #fff;
background-color: var(--inputBorderColor); background-color: var(--input-border-color, var(--inputBorderColor));
.livechat-tag-close { .livechat-tag-close {
color: var(--inputBorderColor); color: var(--input-border-color, var(--inputBorderColor));
} }
} }

View File

@ -9,10 +9,10 @@
livechat-token-list { livechat-token-list {
table { table {
@include tables.data-table;
width: 100%; width: 100%;
@include tables.data-table;
tr th:first-child, tr th:first-child,
tr th:last-child { tr th:last-child {
width: 50px; width: 50px;

View File

@ -19,7 +19,7 @@ table.peertube-plugin-livechat-prosody-list-rooms tr:nth-child(even) {
table.peertube-plugin-livechat-prosody-list-rooms th { table.peertube-plugin-livechat-prosody-list-rooms th {
/* stylelint-disable-next-line custom-property-pattern */ /* stylelint-disable-next-line custom-property-pattern */
background-color: var(--mainHoverColor); background-color: var(--fg-400, var(--mainHoverColor));
border: 1px solid black; border: 1px solid black;
/* stylelint-disable-next-line custom-property-pattern */ /* stylelint-disable-next-line custom-property-pattern */
color: var(--mainBackgroundColor); color: var(--mainBackgroundColor);

View File

@ -54,7 +54,7 @@ $bs-green: #39cc0b;
&.disabled { &.disabled {
cursor: default; cursor: default;
color: #fff; color: #fff;
background-color: var(--inputBorderColor); background-color: var(--input-border-color, var(--inputBorderColor));
} }
} }
@ -67,7 +67,7 @@ $bs-green: #39cc0b;
&:active, &:active,
&:focus { &:focus {
color: #fff; color: #fff;
background-color: var(--mainColor); background-color: var(--primary, var(--mainColor));
} }
&:focus, &:focus,
@ -77,13 +77,13 @@ $bs-green: #39cc0b;
&:hover { &:hover {
color: #fff; color: #fff;
background-color: var(--mainHoverColor); background-color: var(--fg-400, var(--mainHoverColor));
} }
&[disabled], &[disabled],
&.disabled { &.disabled {
cursor: default; cursor: default;
color: #fff; color: #fff;
background-color: var(--inputBorderColor); background-color: var(--input-border-color, var(--inputBorderColor));
} }
} }

View File

@ -13,7 +13,7 @@
text-align: center; text-align: center;
tr { tr {
border: 1px var(--greyBackgroundColor) solid; border: 1px var(--bg-secondary-400, var(--greyBackgroundColor)) solid;
} }
td, td,
@ -34,6 +34,6 @@
} }
tbody tr:nth-child(odd) { tbody tr:nth-child(odd) {
background-color: var(--greySecondaryBackgroundColor); background-color: var(--bg-secondary-300, var(--greySecondaryBackgroundColor));
} }
} }

View File

@ -10,4 +10,4 @@
@use "video"; @use "video";
@use "configuration/configuration"; @use "configuration/configuration";
@use "admin/firewall/firewall"; @use "admin/firewall/firewall";
@use "list-rooms/list-rooms.scss"; @use "list-rooms/list-rooms";

View File

@ -34,7 +34,7 @@
min-height: max(30vh, 300px); min-height: max(30vh, 300px);
} }
@media screen and (orientation: portrait) and (max-width: 767px) { @media screen and (orientation: portrait) and (width <= 767px) {
/* On small screen, and when portrait mode, we are giving the chat more vertical space. /* On small screen, and when portrait mode, we are giving the chat more vertical space.
It should go under the video. It should go under the video.
*/ */

View File

@ -15,7 +15,7 @@ const clientFiles = [
'admin-plugin-client-plugin' 'admin-plugin-client-plugin'
] ]
function loadLocs() { function loadLocs(globalFile) {
// Loading english strings, so we can inject them as constants. // Loading english strings, so we can inject them as constants.
const refFile = path.resolve(__dirname, 'dist', 'languages', 'en.reference.json') const refFile = path.resolve(__dirname, 'dist', 'languages', 'en.reference.json')
if (!fs.existsSync(refFile)) { if (!fs.existsSync(refFile)) {
@ -25,7 +25,6 @@ function loadLocs() {
// Reading client/@types/global.d.ts, to have a list of needed localized strings. // Reading client/@types/global.d.ts, to have a list of needed localized strings.
const r = {} const r = {}
const globalFile = path.resolve(__dirname, 'client', '@types', 'global.d.ts')
const globalFileContent = '' + fs.readFileSync(globalFile) const globalFileContent = '' + fs.readFileSync(globalFile)
const matches = globalFileContent.matchAll(/^declare const LOC_(\w+)\b/gm) const matches = globalFileContent.matchAll(/^declare const LOC_(\w+)\b/gm)
for (const match of matches) { for (const match of matches) {
@ -41,7 +40,7 @@ function loadLocs() {
const define = Object.assign({ const define = Object.assign({
PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name), PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name),
PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, '')) PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, ''))
}, loadLocs()) }, loadLocs(path.resolve(__dirname, 'client', '@types', 'global.d.ts')))
const configs = clientFiles.map(f => ({ const configs = clientFiles.map(f => ({
entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ], entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ],
@ -59,8 +58,14 @@ const configs = clientFiles.map(f => ({
outfile: path.resolve(__dirname, 'dist/client', f + '.js'), outfile: path.resolve(__dirname, 'dist/client', f + '.js'),
})) }))
const defineBuiltin = Object.assign(
{},
loadLocs(path.resolve(__dirname, 'conversejs', 'lib', '@types', 'global.d.ts'))
)
configs.push({ configs.push({
entryPoints: ["./conversejs/builtin.ts"], entryPoints: ["./conversejs/builtin.ts"],
define: defineBuiltin,
bundle: true, bundle: true,
minify: true, minify: true,
sourcemap, sourcemap,

View File

@ -10,12 +10,12 @@ set -euo pipefail
# This script download the Prosody AppImage from the https://github.com/JohnXLivingston/prosody-appimage project. # This script download the Prosody AppImage from the https://github.com/JohnXLivingston/prosody-appimage project.
repo_base_url='https://github.com/JohnXLivingston/prosody-appimage/releases/download' repo_base_url='https://github.com/JohnXLivingston/prosody-appimage/releases/download'
wanted_release='v0.12.3-1' wanted_release='v0.12.4-3'
x86_64_filename='prosody-x86_64.AppImage' x86_64_filename='prosody-x86_64.AppImage'
x86_64_sha256sum='f4af9bfefa2f804ad7e8b03a68f04194abb801f070ae620b3d4bcedb144e8523' x86_64_sha256sum='83a583ac7036387514bed17afab257dab4161ccdd0ab7453818c78b51f830357'
aarch64_filename='prosody-aarch64.AppImage' aarch64_filename='prosody-aarch64.AppImage'
aarch64_sha256sum='878c5be719e1e36a84d637fd2bd44e3059aa91ddb6906ad05f1dd0334078df09' aarch64_sha256sum='7b7e6bf30d4498fc99a40022232c3065707ee4f4df24dc17947b007621634304'
download_dir="$(pwd)/vendor/prosody-appimage" download_dir="$(pwd)/vendor/prosody-appimage"
dist_dir="$(pwd)/dist/server/prosody" dist_dir="$(pwd)/dist/server/prosody"

View File

@ -1,41 +0,0 @@
{
"root": true,
"env": {
"browser": true,
"es6": true
},
"extends": [
"standard-with-typescript",
"plugin:lit/recommended"
],
"globals": {},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"project": [
"./client/tsconfig.json"
]
},
"plugins": [
"@typescript-eslint"
],
"ignorePatterns": [],
"rules": {
"@typescript-eslint/no-unused-vars": [2, {"argsIgnorePattern": "^_"}],
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/return-await": [2, "in-try-catch"], // FIXME: correct?
"@typescript-eslint/no-invalid-void-type": "off",
"@typescript-eslint/triple-slash-reference": "off",
"max-len": [
"error",
{
"code": 120,
"comments": 120
}
],
"no-unused-vars": "off"
}
}

View File

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

View File

@ -57,12 +57,12 @@ declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL: string declare const LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC: string declare const LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL: string
@ -144,3 +144,19 @@ declare const LOC_PROSODY_FIREWALL_FILE_ENABLED: string
declare const LOC_PROSODY_FIREWALL_NAME: string declare const LOC_PROSODY_FIREWALL_NAME: string
declare const LOC_PROSODY_FIREWALL_NAME_DESC: string declare const LOC_PROSODY_FIREWALL_NAME_DESC: string
declare const LOC_PROSODY_FIREWALL_CONTENT: 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

@ -143,7 +143,7 @@ function register (clientOptions: RegisterClientOptions): void {
lastActivityEl.textContent = date.toLocaleDateString() + ' ' + date.toLocaleTimeString() lastActivityEl.textContent = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
} }
const promoteButton = document.createElement('a') const promoteButton = document.createElement('a')
promoteButton.classList.add('orange-button', 'peertube-button-link') promoteButton.classList.add('primary-button', 'orange-button', 'peertube-button-link')
promoteButton.style.margin = '5px' promoteButton.style.margin = '5px'
promoteButton.onclick = async () => { promoteButton.onclick = async () => {
await fetch( await fetch(
@ -243,7 +243,10 @@ function register (clientOptions: RegisterClientOptions): void {
} }
} catch (error: any) { } catch (error: any) {
console.error(error) console.error(error)
peertubeHelpers.notifier.error(error.toString(), await peertubeHelpers.translate(LOC_LOADING_ERROR)) peertubeHelpers.notifier.error(
(error as Error).toString(),
await peertubeHelpers.translate(LOC_LOADING_ERROR)
)
} }
} }
}) })

View File

@ -46,7 +46,7 @@ async function register (clientOptions: RegisterClientOptions): Promise<void> {
]) ])
const webchatFieldOptions: RegisterClientFormFieldOptions = { const webchatFieldOptions: RegisterClientFormFieldOptions = {
name: 'livechat-active', name: 'livechat-active',
label: label, label,
descriptionHTML: description, descriptionHTML: description,
type: 'input-checkbox', type: 'input-checkbox',
default: true, default: true,

View File

@ -22,7 +22,7 @@ export class AdminFirewallElement extends LivechatElement {
public validationError?: ValidationError public validationError?: ValidationError
@state() @state()
public actionDisabled: boolean = false public actionDisabled = false
private _asyncTaskRender: Task private _asyncTaskRender: Task
@ -101,9 +101,9 @@ export class AdminFirewallElement extends LivechatElement {
}) })
} }
public readonly getInputValidationClass = (propertyName: string): { [key: string]: boolean } => { public readonly getInputValidationClass = (propertyName: string): Record<string, boolean> => {
const validationErrorTypes: ValidationErrorType[] | undefined = const validationErrorTypes: ValidationErrorType[] | undefined =
this.validationError?.properties[`${propertyName}`] this.validationError?.properties[propertyName]
return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {} return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {}
} }
@ -111,7 +111,7 @@ export class AdminFirewallElement extends LivechatElement {
propertyName: string): TemplateResult | typeof nothing => { propertyName: string): TemplateResult | typeof nothing => {
const errorMessages: TemplateResult[] = [] const errorMessages: TemplateResult[] = []
const validationErrorTypes: ValidationErrorType[] | undefined = const validationErrorTypes: ValidationErrorType[] | undefined =
this.validationError?.properties[`${propertyName}`] ?? undefined this.validationError?.properties[propertyName] ?? undefined
// FIXME: this code is duplicated in dymamic table form // FIXME: this code is duplicated in dymamic table form
if (validationErrorTypes && validationErrorTypes.length !== 0) { if (validationErrorTypes && validationErrorTypes.length !== 0) {

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import type { AdminFirewallElement } from '../elements/admin-firewall' import type { AdminFirewallElement } from '../elements/admin-firewall'
import type { TemplateResult } from 'lit' import type { TemplateResult } from 'lit'
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form' import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
@ -64,7 +67,7 @@ export function tplAdminFirewall (el: AdminFirewallElement): TemplateResult {
.maxLines=${maxFirewallFiles} .maxLines=${maxFirewallFiles}
.validation=${el.validationError?.properties} .validation=${el.validationError?.properties}
.validationPrefix=${'files'} .validationPrefix=${'files'}
.rows=${el.firewallConfiguration?.files} .rows=${el.firewallConfiguration?.files ?? []}
@update=${(e: CustomEvent) => { @update=${(e: CustomEvent) => {
el.resetValidation(e) el.resetValidation(e)
if (el.firewallConfiguration) { if (el.firewallConfiguration) {

View File

@ -32,7 +32,7 @@ export class ChannelConfigurationElement extends LivechatElement {
public validationError?: ValidationError public validationError?: ValidationError
@state() @state()
public actionDisabled: boolean = false public actionDisabled = false
private _asyncTaskRender: Task private _asyncTaskRender: Task
@ -113,9 +113,9 @@ export class ChannelConfigurationElement extends LivechatElement {
} }
} }
public readonly getInputValidationClass = (propertyName: string): { [key: string]: boolean } => { public readonly getInputValidationClass = (propertyName: string): Record<string, boolean> => {
const validationErrorTypes: ValidationErrorType[] | undefined = const validationErrorTypes: ValidationErrorType[] | undefined =
this.validationError?.properties[`${propertyName}`] this.validationError?.properties[propertyName]
return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {} return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {}
} }
@ -123,7 +123,7 @@ export class ChannelConfigurationElement extends LivechatElement {
propertyName: string): TemplateResult | typeof nothing => { propertyName: string): TemplateResult | typeof nothing => {
const errorMessages: TemplateResult[] = [] const errorMessages: TemplateResult[] = []
const validationErrorTypes: ValidationErrorType[] | undefined = const validationErrorTypes: ValidationErrorType[] | undefined =
this.validationError?.properties[`${propertyName}`] ?? undefined this.validationError?.properties[propertyName] ?? undefined
// FIXME: this code is duplicated in dymamic table form // FIXME: this code is duplicated in dymamic table form
if (validationErrorTypes && validationErrorTypes.length !== 0) { if (validationErrorTypes && validationErrorTypes.length !== 0) {

View File

@ -30,7 +30,7 @@ export class ChannelEmojisElement extends LivechatElement {
public validationError?: ValidationError public validationError?: ValidationError
@state() @state()
public actionDisabled: boolean = false public actionDisabled = false
private _asyncTaskRender: Task private _asyncTaskRender: Task
@ -192,7 +192,7 @@ export class ChannelEmojisElement extends LivechatElement {
throw new Error('Invalid data') throw new Error('Invalid data')
} }
const url = await this._convertImageToDataUrl(entry.url) const url = await this._convertImageToDataUrl(entry.url as string)
const sn = entry.sn as string const sn = entry.sn as string
const item: ChannelEmojisConfiguration['emojis']['customEmojis'][0] = { const item: ChannelEmojisConfiguration['emojis']['customEmojis'][0] = {
@ -211,7 +211,7 @@ export class ChannelEmojisElement extends LivechatElement {
await this.ptTranslate(LOC_ACTION_IMPORT_EMOJIS_INFO) await this.ptTranslate(LOC_ACTION_IMPORT_EMOJIS_INFO)
) )
} catch (err: any) { } catch (err: any) {
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR)) this.ptNotifier.error((err as Error).toString(), await this.ptTranslate(LOC_ERROR))
} finally { } finally {
this.actionDisabled = false this.actionDisabled = false
} }
@ -250,12 +250,27 @@ export class ChannelEmojisElement extends LivechatElement {
a.remove() a.remove()
} catch (err: any) { } catch (err: any) {
this.logger.error(err) this.logger.error(err)
this.ptNotifier.error(err.toString()) this.ptNotifier.error((err as Error).toString())
} finally { } finally {
this.actionDisabled = false this.actionDisabled = false
} }
} }
public async enableEmojisOnlyModeOnAllRooms (ev: Event): Promise<void> {
ev.preventDefault()
if (!this._channelDetailsService || !this.channelId) {
this.ptNotifier.error(await this.ptTranslate(LOC_ERROR))
return
}
try {
await this._channelDetailsService.enableEmojisOnlyModeOnAllRooms(this.channelId)
this.ptNotifier.info(await this.ptTranslate(LOC_SUCCESSFULLY_SAVED))
} catch (err) {
console.error(err)
this.ptNotifier.error(await this.ptTranslate(LOC_ERROR))
}
}
/** /**
* Takes an url (or dataUrl), download the image, and converts to dataUrl. * Takes an url (or dataUrl), download the image, and converts to dataUrl.
* @param url the url * @param url the url

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { html } from 'lit' import { html } from 'lit'
import { customElement, state } from 'lit/decorators.js' import { customElement, state } from 'lit/decorators.js'
import { ptTr } from '../../lib/directives/translation' import { ptTr } from '../../lib/directives/translation'

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { LivechatElement } from '../../lib/elements/livechat' import { LivechatElement } from '../../lib/elements/livechat'
import { ptTr } from '../../lib/directives/translation' import { ptTr } from '../../lib/directives/translation'
import { html, TemplateResult } from 'lit' import { html, TemplateResult } from 'lit'

View File

@ -2,14 +2,18 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import type { ChannelConfigurationElement } from '../channel-configuration' import type { ChannelConfigurationElement } from '../channel-configuration'
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form' import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
import { ptTr } from '../../../lib/directives/translation' import { ptTr } from '../../../lib/directives/translation'
import { html, TemplateResult } from 'lit' import { html, TemplateResult } from 'lit'
import { classMap } from 'lit/directives/class-map.js' import { classMap } from 'lit/directives/class-map.js'
import { noDuplicateMaxDelay, forbidSpecialCharsMaxTolerance } from 'shared/lib/constants'
export function tplChannelConfiguration (el: ChannelConfigurationElement): TemplateResult { export function tplChannelConfiguration (el: ChannelConfigurationElement): TemplateResult {
const tableHeaderList: {[key: string]: DynamicFormHeader} = { const tableHeaderList: Record<string, DynamicFormHeader> = {
forbiddenWords: { forbiddenWords: {
entries: { entries: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL), colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL),
@ -20,16 +24,16 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC) description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC)
}, },
applyToModerators: { applyToModerators: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL), colName: ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC) description: ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)
}, },
label: { label: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL), colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC) description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC)
}, },
reason: { reason: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL), colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC) description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)
}, },
comments: { comments: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL), colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL),
@ -57,7 +61,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
} }
} }
} }
const tableSchema: {[key: string]: DynamicFormSchema} = { const tableSchema: Record<string, DynamicFormSchema> = {
forbiddenWords: { forbiddenWords: {
entries: { entries: {
inputType: 'tags', inputType: 'tags',
@ -337,6 +341,246 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
${el.renderFeedback('peertube-livechat-bot-nickname-feedback', 'bot.nickname')} ${el.renderFeedback('peertube-livechat-bot-nickname-feedback', 'bot.nickname')}
</div> </div>
<livechat-configuration-section-header
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_DESC)}
.helpPage=${'documentation/user/streamers/bot/special_chars'}>
</livechat-configuration-section-header>
<div class="form-group">
<label>
<input
type="checkbox"
name="forbid_special_chars"
id="peertube-livechat-forbid-special-chars"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.bot.forbidSpecialChars.enabled =
(event.target as HTMLInputElement).checked
}
el.requestUpdate('channelConfiguration')
}
}
value="1"
?checked=${el.channelConfiguration?.configuration.bot.forbidSpecialChars.enabled}
/>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_LABEL)}
</label>
</div>
${!el.channelConfiguration?.configuration.bot.forbidSpecialChars.enabled
? ''
: html`
<div class="form-group">
<label>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_LABEL)}
<input
type="number"
name="special_chars_tolerance"
class=${classMap(
Object.assign(
{ 'form-control': true },
el.getInputValidationClass('bot.forbidSpecialChars.tolerance')
)
)}
min="0"
max="${forbidSpecialCharsMaxTolerance}"
id="peertube-livechat-forbid-special-chars-tolerance"
aria-describedby="peertube-livechat-forbid-special-chars-tolerance-feedback"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.bot.forbidSpecialChars.tolerance =
Number((event.target as HTMLInputElement).value)
}
el.requestUpdate('channelConfiguration')
}
}
value="${el.channelConfiguration?.configuration.bot.forbidSpecialChars.tolerance ?? '0'}"
/>
</label>
<small class="form-text text-muted">
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_DESC)}
</small>
${el.renderFeedback('peertube-livechat-forbid-special-chars-tolerance-feedback',
'bot.forbidSpecialChars.tolerance')
}
</div>
<div class="form-group">
<label>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL)}
<input
type="text"
name="special_chars_reason"
class=${classMap(
Object.assign(
{ 'form-control': true },
el.getInputValidationClass('bot.forbidSpecialChars.reason')
)
)}
id="peertube-livechat-forbid-special-chars-reason"
aria-describedby="peertube-livechat-forbid-special-chars-reason-feedback"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.bot.forbidSpecialChars.reason =
(event.target as HTMLInputElement).value
}
el.requestUpdate('channelConfiguration')
}
}
value="${el.channelConfiguration?.configuration.bot.forbidSpecialChars.reason ?? ''}"
/>
</label>
<small class="form-text text-muted">
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)}
</small>
${el.renderFeedback('peertube-livechat-forbid-special-chars-reason-feedback',
'bot.forbidSpecialChars.reason')
}
</div>
<div class="form-group">
<label>
<input
type="checkbox"
name="forbid_special_chars_applyToModerators"
id="peertube-livechat-forbid-special-chars-applyToModerators"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.bot.forbidSpecialChars.applyToModerators =
(event.target as HTMLInputElement).checked
}
el.requestUpdate('channelConfiguration')
}
}
value="1"
?checked=${el.channelConfiguration?.configuration.bot.forbidSpecialChars.applyToModerators}
/>
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL)}
</label>
<small class="form-text text-muted">
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)}
</small>
</div>
`
}
<livechat-configuration-section-header
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DESC)}
.helpPage=${'documentation/user/streamers/bot/no_duplicate'}>
</livechat-configuration-section-header>
<div class="form-group">
<label>
<input
type="checkbox"
name="no_duplicate"
id="peertube-livechat-no-duplicate"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.bot.noDuplicate.enabled =
(event.target as HTMLInputElement).checked
}
el.requestUpdate('channelConfiguration')
}
}
value="1"
?checked=${el.channelConfiguration?.configuration.bot.noDuplicate.enabled}
/>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL)}
</label>
</div>
${!el.channelConfiguration?.configuration.bot.noDuplicate.enabled
? ''
: html`
<div class="form-group">
<label>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_LABEL)}
<input
type="number"
name="no_duplicate_delay"
class=${classMap(
Object.assign(
{ 'form-control': true },
el.getInputValidationClass('bot.noDuplicate.delay')
)
)}
min="0"
max="${noDuplicateMaxDelay.toString()}"
id="peertube-livechat-no-duplicate-delay"
aria-describedby="peertube-livechat-no-duplicate-delay-feedback"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.bot.noDuplicate.delay =
Number((event.target as HTMLInputElement).value)
}
el.requestUpdate('channelConfiguration')
}
}
value="${el.channelConfiguration?.configuration.bot.noDuplicate.delay ?? '0'}"
/>
</label>
<small class="form-text text-muted">
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_DESC)}
</small>
${el.renderFeedback('peertube-livechat-no-duplicate-delay-feedback',
'bot.noDuplicate.delay')
}
</div>
<div class="form-group">
<label>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL)}
<input
type="text"
name="no_duplicate_reason"
class=${classMap(
Object.assign(
{ 'form-control': true },
el.getInputValidationClass('bot.noDuplicate.reason')
)
)}
id="peertube-livechat-no-duplicate-reason"
aria-describedby="peertube-livechat-no-duplicate-reason-feedback"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.bot.noDuplicate.reason =
(event.target as HTMLInputElement).value
}
el.requestUpdate('channelConfiguration')
}
}
value="${el.channelConfiguration?.configuration.bot.noDuplicate.reason ?? ''}"
/>
</label>
<small class="form-text text-muted">
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)}
</small>
${el.renderFeedback('peertube-livechat-no-duplicate-reason-feedback',
'bot.noDuplicate.reason')
}
</div>
<div class="form-group">
<label>
<input
type="checkbox"
name="no_duplicate_applyToModerators"
id="peertube-livechat-no-duplicate-applyToModerators"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.bot.noDuplicate.applyToModerators =
(event.target as HTMLInputElement).checked
}
el.requestUpdate('channelConfiguration')
}
}
value="1"
?checked=${el.channelConfiguration?.configuration.bot.noDuplicate.applyToModerators}
/>
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL)}
</label>
<small class="form-text text-muted">
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)}
</small>
</div>
`
}
<livechat-configuration-section-header <livechat-configuration-section-header
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)} .label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)} .description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)}

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import type { ChannelEmojisElement } from '../channel-emojis' import type { ChannelEmojisElement } from '../channel-emojis'
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form' import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
import { maxEmojisPerChannel } from 'shared/lib/emojis' import { maxEmojisPerChannel } from 'shared/lib/emojis'
@ -45,13 +48,14 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
<livechat-channel-tabs .active=${'emojis'} .channelId=${el.channelId}></livechat-channel-tabs> <livechat-channel-tabs .active=${'emojis'} .channelId=${el.channelId}></livechat-channel-tabs>
<h2>${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_TITLE)}</h2>
<p> <p>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_DESC)} ${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_DESC)}
<livechat-help-button .page=${'documentation/user/streamers/emojis'}> <livechat-help-button .page=${'documentation/user/streamers/emojis'}>
</livechat-help-button> </livechat-help-button>
</p> </p>
<form role="form" @submit=${el.saveEmojis} @change=${el.resetValidation}> <form role="form" @submit=${el.saveEmojis} @change=${el.resetValidation}>
<div class="peertube-plugin-livechat-configuration-actions"> <div class="peertube-plugin-livechat-configuration-actions">
${ ${
@ -86,7 +90,7 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
.maxLines=${maxEmojisPerChannel} .maxLines=${maxEmojisPerChannel}
.validation=${el.validationError?.properties} .validation=${el.validationError?.properties}
.validationPrefix=${'emojis'} .validationPrefix=${'emojis'}
.rows=${el.channelEmojisConfiguration?.emojis.customEmojis} .rows=${el.channelEmojisConfiguration?.emojis.customEmojis ?? []}
@update=${(e: CustomEvent) => { @update=${(e: CustomEvent) => {
el.resetValidation(e) el.resetValidation(e)
if (el.channelEmojisConfiguration) { if (el.channelEmojisConfiguration) {
@ -106,5 +110,23 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
</button> </button>
</div> </div>
</form> </form>
<h2>${ptTr(LOC_EMOJI_ONLY_MODE_TITLE)}</h2>
<p>
${ptTr(LOC_EMOJI_ONLY_MODE_DESC_1, true)}
</p>
<p>
${ptTr(LOC_EMOJI_ONLY_MODE_DESC_2, true)}
</p>
<p>
${ptTr(LOC_EMOJI_ONLY_MODE_DESC_3, true)}
</p>
<div class="peertube-plugin-livechat-configuration-actions">
<button type="button" @click=${el.enableEmojisOnlyModeOnAllRooms}>
${ptTr(LOC_EMOJI_ONLY_ENABLE_ALL_ROOMS)}
</button>
</div>
</div>` </div>`
} }

View File

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

View File

@ -11,7 +11,7 @@ import type {
import { ValidationError, ValidationErrorType } from '../../lib/models/validation' import { ValidationError, ValidationErrorType } from '../../lib/models/validation'
import { getBaseRoute } from '../../../utils/uri' import { getBaseRoute } from '../../../utils/uri'
import { maxEmojisPerChannel } from 'shared/lib/emojis' import { maxEmojisPerChannel } from 'shared/lib/emojis'
import { channelTermsMaxLength } from 'shared/lib/constants' import { channelTermsMaxLength, noDuplicateMaxDelay, forbidSpecialCharsMaxTolerance } from 'shared/lib/constants'
export class ChannelDetailsService { export class ChannelDetailsService {
public _registerClientOptions: RegisterClientOptions public _registerClientOptions: RegisterClientOptions
@ -67,11 +67,43 @@ export class ChannelDetailsService {
// The backend will ignore those values. // The backend will ignore those values.
if (botConf.enabled) { if (botConf.enabled) {
propertiesError['bot.nickname'] = [] propertiesError['bot.nickname'] = []
propertiesError['bot.forbidSpecialChars.tolerance'] = []
propertiesError['bot.noDuplicate.delay'] = []
if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) { if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) {
propertiesError['bot.nickname'].push(ValidationErrorType.WrongFormat) propertiesError['bot.nickname'].push(ValidationErrorType.WrongFormat)
} }
if (botConf.forbidSpecialChars.enabled) {
const forbidSpecialCharsTolerance = channelConfigurationOptions.bot.forbidSpecialChars.tolerance
if (
(typeof forbidSpecialCharsTolerance !== 'number') ||
isNaN(forbidSpecialCharsTolerance)
) {
propertiesError['bot.forbidSpecialChars.tolerance'].push(ValidationErrorType.WrongType)
} else if (
forbidSpecialCharsTolerance < 0 ||
forbidSpecialCharsTolerance > forbidSpecialCharsMaxTolerance
) {
propertiesError['bot.forbidSpecialChars.tolerance'].push(ValidationErrorType.NotInRange)
}
}
if (botConf.noDuplicate.enabled) {
const noDuplicateDelay = channelConfigurationOptions.bot.noDuplicate.delay
if (
(typeof noDuplicateDelay !== 'number') ||
isNaN(noDuplicateDelay)
) {
propertiesError['bot.noDuplicate.delay'].push(ValidationErrorType.WrongType)
} else if (
noDuplicateDelay < 0 ||
noDuplicateDelay > noDuplicateMaxDelay
) {
propertiesError['bot.noDuplicate.delay'].push(ValidationErrorType.NotInRange)
}
}
for (const [i, fw] of botConf.forbiddenWords.entries()) { for (const [i, fw] of botConf.forbiddenWords.entries()) {
for (const v of fw.entries) { for (const v of fw.entries) {
propertiesError[`bot.forbiddenWords.${i}.entries`] = [] propertiesError[`bot.forbiddenWords.${i}.entries`] = []
@ -146,7 +178,8 @@ export class ChannelDetailsService {
} }
for (const channel of channels.data) { for (const channel of channels.data) {
channel.livechatConfigurationUri = '/p/livechat/configuration/channel?channelId=' + encodeURIComponent(channel.id) channel.livechatConfigurationUri =
'/p/livechat/configuration/channel?channelId=' + encodeURIComponent(channel.id as string | number)
// Note: since Peertube v6.0.0, channel.avatar is dropped, and we have to use channel.avatars. // Note: since Peertube v6.0.0, channel.avatar is dropped, and we have to use channel.avatars.
// So, if !channel.avatar, we will search a suitable one in channel.avatars, and fill channel.avatar. // So, if !channel.avatar, we will search a suitable one in channel.avatars, and fill channel.avatar.
@ -180,10 +213,11 @@ export class ChannelDetailsService {
} }
public async fetchEmojisConfiguration (channelId: number): Promise<ChannelEmojisConfiguration> { public async fetchEmojisConfiguration (channelId: number): Promise<ChannelEmojisConfiguration> {
const url = getBaseRoute(this._registerClientOptions) +
'/api/configuration/channel/emojis/' +
encodeURIComponent(channelId)
const response = await fetch( const response = await fetch(
getBaseRoute(this._registerClientOptions) + url,
'/api/configuration/channel/emojis/' +
encodeURIComponent(channelId),
{ {
method: 'GET', method: 'GET',
headers: this._headers headers: this._headers
@ -295,10 +329,11 @@ export class ChannelDetailsService {
channelId: number, channelId: number,
channelEmojis: ChannelEmojis channelEmojis: ChannelEmojis
): Promise<ChannelEmojisConfiguration> { ): Promise<ChannelEmojisConfiguration> {
const url = getBaseRoute(this._registerClientOptions) +
'/api/configuration/channel/emojis/' +
encodeURIComponent(channelId)
const response = await fetch( const response = await fetch(
getBaseRoute(this._registerClientOptions) + url,
'/api/configuration/channel/emojis/' +
encodeURIComponent(channelId),
{ {
method: 'POST', method: 'POST',
headers: this._headers, headers: this._headers,
@ -312,4 +347,24 @@ export class ChannelDetailsService {
return response.json() return response.json()
} }
public async enableEmojisOnlyModeOnAllRooms (channelId: number): Promise<void> {
const url = getBaseRoute(this._registerClientOptions) +
'/api/configuration/channel/emojis/' +
encodeURIComponent(channelId) +
'/enable_emoji_only'
const response = await fetch(
url,
{
method: 'POST',
headers: this._headers
}
)
if (!response.ok) {
throw new Error('Can\'t enable Emojis Only Mode on all rooms.')
}
return response.json()
}
} }

View File

@ -4,7 +4,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/ // This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
export const AddSVG: string = export const AddSVG =
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
aria-hidden="true" aria-hidden="true"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
@ -14,7 +14,7 @@ export const AddSVG: string =
</svg>` </svg>`
// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/ // This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/
export const RemoveSVG: string = export const RemoveSVG =
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
aria-hidden="true" aria-hidden="true"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"

View File

@ -13,8 +13,8 @@ import { getPtContext } from '../contexts/peertube'
export class TranslationDirective extends AsyncDirective { export class TranslationDirective extends AsyncDirective {
private readonly _peertubeHelpers: RegisterClientHelpers private readonly _peertubeHelpers: RegisterClientHelpers
private _translatedValue: string = '' private _translatedValue = ''
private _localizationId: string = '' private _localizationId = ''
private _allowUnsafeHTML = false private _allowUnsafeHTML = false
@ -25,7 +25,7 @@ export class TranslationDirective extends AsyncDirective {
this._asyncUpdateTranslation().then(() => {}, () => {}) this._asyncUpdateTranslation().then(() => {}, () => {})
} }
public override render = (locId: string, allowHTML: boolean = false): TemplateResult | string => { public override render = (locId: string, allowHTML = false): TemplateResult | string => {
this._localizationId = locId // TODO Check current component for context (to infer the prefix) this._localizationId = locId // TODO Check current component for context (to infer the prefix)
this._allowUnsafeHTML = allowHTML this._allowUnsafeHTML = allowHTML

View File

@ -3,6 +3,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { ptTr } from '../directives/translation' import { ptTr } from '../directives/translation'
import { html } from 'lit' import { html } from 'lit'
import { customElement, property } from 'lit/decorators.js' import { customElement, property } from 'lit/decorators.js'

View File

@ -3,6 +3,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import type { TagsInputElement } from './tags-input' import type { TagsInputElement } from './tags-input'
import type { DirectiveResult } from 'lit/directive' import type { DirectiveResult } from 'lit/directive'
import { ValidationErrorType } from '../models/validation' import { ValidationErrorType } from '../models/validation'
@ -20,26 +23,26 @@ import { AddSVG, RemoveSVG } from '../buttons'
type DynamicTableAcceptedTypes = number | string | boolean | Date | Array<number | string> type DynamicTableAcceptedTypes = number | string | boolean | Date | Array<number | string>
type DynamicTableAcceptedInputTypes = 'textarea' type DynamicTableAcceptedInputTypes = 'textarea'
| 'select' | 'select'
| 'checkbox' | 'checkbox'
| 'range' | 'range'
| 'color' | 'color'
| 'date' | 'date'
| 'datetime' | 'datetime'
| 'datetime-local' | 'datetime-local'
| 'email' | 'email'
| 'file' | 'file'
| 'image' | 'image'
| 'month' | 'month'
| 'number' | 'number'
| 'password' | 'password'
| 'tel' | 'tel'
| 'text' | 'text'
| 'time' | 'time'
| 'url' | 'url'
| 'week' | 'week'
| 'tags' | 'tags'
| 'image-file' | 'image-file'
interface CellDataSchema { interface CellDataSchema {
min?: number min?: number
@ -47,7 +50,7 @@ interface CellDataSchema {
minlength?: number minlength?: number
maxlength?: number maxlength?: number
size?: number size?: number
options?: { [key: string]: string } options?: Record<string, string>
datalist?: DynamicTableAcceptedTypes[] datalist?: DynamicTableAcceptedTypes[]
separator?: string separator?: string
inputType?: DynamicTableAcceptedInputTypes inputType?: DynamicTableAcceptedInputTypes
@ -59,7 +62,7 @@ interface CellDataSchema {
interface DynamicTableRowData { interface DynamicTableRowData {
_id: number _id: number
_originalIndex: number _originalIndex: number
row: { [key: string]: DynamicTableAcceptedTypes } row: Record<string, DynamicTableAcceptedTypes>
} }
interface DynamicFormHeaderCellData { interface DynamicFormHeaderCellData {
@ -68,10 +71,8 @@ interface DynamicFormHeaderCellData {
headerClassList?: string[] headerClassList?: string[]
} }
export interface DynamicFormHeader { export type DynamicFormHeader = Record<string, DynamicFormHeaderCellData>
[key: string]: DynamicFormHeaderCellData export type DynamicFormSchema = Record<string, CellDataSchema>
}
export interface DynamicFormSchema { [key: string]: CellDataSchema }
@customElement('livechat-dynamic-table-form') @customElement('livechat-dynamic-table-form')
export class DynamicTableFormElement extends LivechatElement { export class DynamicTableFormElement extends LivechatElement {
@ -85,19 +86,19 @@ export class DynamicTableFormElement extends LivechatElement {
public maxLines?: number = undefined public maxLines?: number = undefined
@property() @property()
public validation?: {[key: string]: ValidationErrorType[] } public validation?: Record<string, ValidationErrorType[]>
@property({ attribute: false }) @property({ attribute: false })
public validationPrefix: string = '' public validationPrefix = ''
@property({ attribute: false }) @property({ attribute: false })
public rows: Array<{ [key: string]: DynamicTableAcceptedTypes }> = [] public rows: Array<Record<string, DynamicTableAcceptedTypes>> = []
@state() @state()
public _rowsById: DynamicTableRowData[] = [] public _rowsById: DynamicTableRowData[] = []
@property({ attribute: false }) @property({ attribute: false })
public formName: string = '' public formName = ''
@state() @state()
private _lastRowId = 1 private _lastRowId = 1
@ -112,7 +113,7 @@ export class DynamicTableFormElement extends LivechatElement {
} }
} }
private readonly _getDefaultRow = (): { [key: string]: DynamicTableAcceptedTypes } => { private readonly _getDefaultRow = (): Record<string, DynamicTableAcceptedTypes> => {
this._updateLastRowId() this._updateLastRowId()
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? ''])]) return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? ''])])
} }
@ -245,11 +246,11 @@ export class DynamicTableFormElement extends LivechatElement {
return html`<tr id=${inputId}> return html`<tr id=${inputId}>
${Object.keys(this.header) ${Object.keys(this.header)
.sort((k1, k2) => this.columnOrder.indexOf(k1) - this.columnOrder.indexOf(k2)) .sort((k1, k2) => this.columnOrder.indexOf(k1) - this.columnOrder.indexOf(k2))
.map(key => this.renderDataCell(key, .map(key => this.renderDataCell(key,
rowData.row[key] ?? this.schema[key].default, rowData.row[key] ?? this.schema[key].default,
rowData._id, rowData._id,
rowData._originalIndex))} rowData._originalIndex))}
<td class="form-group"> <td class="form-group">
<button type="button" <button type="button"
class="dynamic-table-remove-row" class="dynamic-table-remove-row"
@ -457,8 +458,7 @@ export class DynamicTableFormElement extends LivechatElement {
inputTitle, inputTitle,
propertyName, propertyName,
propertySchema, propertySchema,
(propertyValue)?.join(propertySchema.separator ?? ',') ?? (propertyValue)?.join(propertySchema.separator ?? ',') ?? propertyValue ?? propertySchema.default ?? '',
propertyValue ?? propertySchema.default ?? '',
originalIndex)} originalIndex)}
${feedback} ${feedback}
` `
@ -473,8 +473,7 @@ export class DynamicTableFormElement extends LivechatElement {
inputTitle, inputTitle,
propertyName, propertyName,
propertySchema, propertySchema,
(propertyValue)?.join(propertySchema.separator ?? ',') ?? (propertyValue)?.join(propertySchema.separator ?? ',') ?? propertyValue ?? propertySchema.default ?? '',
propertyValue ?? propertySchema.default ?? '',
originalIndex)} originalIndex)}
${feedback} ${feedback}
` `
@ -498,8 +497,10 @@ export class DynamicTableFormElement extends LivechatElement {
} }
if (!formElement) { if (!formElement) {
this.logger.warn(`value type '${(propertyValue.constructor.toString())}' is incompatible` + this.logger.warn(
`with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`) `value type '${(propertyValue.constructor.toString())}' is incompatible` +
`with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`
)
} }
const classList = ['form-group'] const classList = ['form-group']
@ -678,7 +679,7 @@ export class DynamicTableFormElement extends LivechatElement {
} }
_getInputValidationClass = (propertyName: string, _getInputValidationClass = (propertyName: string,
originalIndex: number): { [key: string]: boolean } => { originalIndex: number): Record<string, boolean> => {
const validationErrorTypes: ValidationErrorType[] | undefined = const validationErrorTypes: ValidationErrorType[] | undefined =
this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`] this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`]

View File

@ -18,7 +18,7 @@ export class HelpButtonElement extends LivechatElement {
public buttonTitle: string | DirectiveResult = ptTr(LOC_ONLINE_HELP) public buttonTitle: string | DirectiveResult = ptTr(LOC_ONLINE_HELP)
@property({ attribute: false }) @property({ attribute: false })
public page: string = '' public page = ''
@state() @state()
public url: URL = new URL('https://lmddgtfy.net/') public url: URL = new URL('https://lmddgtfy.net/')
@ -38,7 +38,7 @@ export class HelpButtonElement extends LivechatElement {
href="${this.url.href}" href="${this.url.href}"
target=_blank target=_blank
title="${this.buttonTitle}" title="${this.buttonTitle}"
class="orange-button peertube-button-link" class="primary-button orange-button peertube-button-link"
>${unsafeHTML(helpButtonSVG())}</a>` >${unsafeHTML(helpButtonSVG())}</a>`
}) })
} }

View File

@ -1,6 +1,10 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { LivechatElement } from './livechat' import { LivechatElement } from './livechat'
import { html } from 'lit' import { html } from 'lit'
import type { DirectiveResult } from 'lit/directive' import type { DirectiveResult } from 'lit/directive'

View File

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

View File

@ -3,6 +3,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { LivechatElement } from './livechat' import { LivechatElement } from './livechat'
import { ptTr } from '../directives/translation' import { ptTr } from '../directives/translation'
import { html } from 'lit' import { html } from 'lit'
@ -21,10 +24,11 @@ import type { DirectiveResult } from 'lit/directive'
// Then replace the main color by «currentColor» // Then replace the main color by «currentColor»
const copySVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 4.233 4.233"> const copySVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 4.233 4.233">
<g style="stroke-width:1.00021;stroke-miterlimit:4;stroke-dasharray:none">` + <g style="stroke-width:1.00021;stroke-miterlimit:4;stroke-dasharray:none">` +
// eslint-disable-next-line max-len // eslint-disable-next-line max-len, @stylistic/indent-binary-ops
'<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m4.084 4.046-.616.015-.645-.004a.942.942 0 0 1-.942-.942v-4.398a.94.94 0 0 1 .942-.943H7.22a.94.94 0 0 1 .942.943l-.006.334-.08.962" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' + '<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m4.084 4.046-.616.015-.645-.004a.942.942 0 0 1-.942-.942v-4.398a.94.94 0 0 1 .942-.943H7.22a.94.94 0 0 1 .942.943l-.006.334-.08.962" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' +
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
'<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M8.434 5.85c-.422.009-1.338.009-1.76.01-.733.004-2.199 0-2.199 0a.94.94 0 0 1-.942-.941V.52a.94.94 0 0 1 .942-.942h4.398a.94.94 0 0 1 .943.942s.004 1.466 0 2.2c-.003.418-.019 1.251-.006 1.67.024.812-.382 1.439-1.376 1.46z" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' + '<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M8.434 5.85c-.422.009-1.338.009-1.76.01-.733.004-2.199 0-2.199 0a.94.94 0 0 1-.942-.941V.52a.94.94 0 0 1 .942-.942h4.398a.94.94 0 0 1 .943.942s.004 1.466 0 2.2c-.003.418-.019 1.251-.006 1.67.024.812-.382 1.439-1.376 1.46z" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' +
// eslint-disable-next-line @stylistic/indent-binary-ops
`</g> `</g>
</svg>` </svg>`
@ -64,10 +68,10 @@ export class TagsInputElement extends LivechatElement {
private readonly _isPressingKey: string[] = [] private readonly _isPressingKey: string[] = []
@property({ attribute: false }) @property({ attribute: false })
public separator: string = '\n' public separator = '\n'
@property({ attribute: false }) @property({ attribute: false })
public animDuration: number = 200 public animDuration = 200
/** /**
* Overloading the standard focus method. * Overloading the standard focus method.
@ -245,8 +249,9 @@ export class TagsInputElement extends LivechatElement {
if (!this._isPressingKey.includes(e.key)) { if (!this._isPressingKey.includes(e.key)) {
this._isPressingKey.push(e.key) this._isPressingKey.push(e.key)
if ((target.selectionStart === target.selectionEnd) && if (
target.selectionStart === 0) { (target.selectionStart === target.selectionEnd) && target.selectionStart === 0
) {
this._handleDeleteTag((this._searchedTagsIndex.length) this._handleDeleteTag((this._searchedTagsIndex.length)
? this._searchedTagsIndex.slice(-1)[0] ? this._searchedTagsIndex.slice(-1)[0]
: (this.value.length - 1)) : (this.value.length - 1))
@ -259,8 +264,9 @@ export class TagsInputElement extends LivechatElement {
if (!this._isPressingKey.includes(e.key)) { if (!this._isPressingKey.includes(e.key)) {
this._isPressingKey.push(e.key) this._isPressingKey.push(e.key)
if ((target.selectionStart === target.selectionEnd) && if (
target.selectionStart === target.value.length) { (target.selectionStart === target.selectionEnd) && target.selectionStart === target.value.length
) {
this._handleDeleteTag((this._searchedTagsIndex.length) this._handleDeleteTag((this._searchedTagsIndex.length)
? this._searchedTagsIndex[0] ? this._searchedTagsIndex[0]
: 0) : 0)

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import type { LivechatTokenListElement } from '../token-list' import type { LivechatTokenListElement } from '../token-list'
import { html, TemplateResult } from 'lit' import { html, TemplateResult } from 'lit'
import { unsafeHTML } from 'lit/directives/unsafe-html.js' import { unsafeHTML } from 'lit/directives/unsafe-html.js'
@ -23,11 +26,11 @@ export function tplTokenList (el: LivechatTokenListElement): TemplateResult {
<tbody> <tbody>
${ ${
repeat(el.tokenList ?? [], (token) => token.id, (token) => { repeat(el.tokenList ?? [], (token) => token.id, (token) => {
let dateStr: string = '' let dateStr = ''
try { try {
const date = new Date(token.date) const date = new Date(token.date)
dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString() dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
} catch (err) {} } catch (_err) {}
return html`<tr> return html`<tr>
<td>${ <td>${
el.mode === 'select' el.mode === 'select'

View File

@ -27,7 +27,7 @@ export class LivechatTokenListElement extends LivechatElement {
public currentSelectedToken?: LivechatToken public currentSelectedToken?: LivechatToken
@property({ attribute: false }) @property({ attribute: false })
public actionDisabled: boolean = false public actionDisabled = false
private readonly _tokenListService: TokenListService private readonly _tokenListService: TokenListService
private readonly _asyncTaskRender: Task private readonly _asyncTaskRender: Task
@ -83,7 +83,7 @@ export class LivechatTokenListElement extends LivechatElement {
this.dispatchEvent(new CustomEvent('update', {})) this.dispatchEvent(new CustomEvent('update', {}))
} catch (err: any) { } catch (err: any) {
this.logger.error(err) this.logger.error(err)
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR)) this.ptNotifier.error((err as Error).toString(), await this.ptTranslate(LOC_ERROR))
} finally { } finally {
this.actionDisabled = false this.actionDisabled = false
} }
@ -102,7 +102,7 @@ export class LivechatTokenListElement extends LivechatElement {
this.dispatchEvent(new CustomEvent('update', {})) this.dispatchEvent(new CustomEvent('update', {}))
} catch (err: any) { } catch (err: any) {
this.logger.error(err) this.logger.error(err)
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR)) this.ptNotifier.error((err as Error).toString(), await this.ptTranslate(LOC_ERROR))
} finally { } finally {
this.actionDisabled = false this.actionDisabled = false
} }

View File

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

View File

@ -27,7 +27,7 @@ type displayButtonOptions = displayButtonOptionsCallback | displayButtonOptionsH
function displayButton (dbo: displayButtonOptions): void { function displayButton (dbo: displayButtonOptions): void {
const button = document.createElement('a') const button = document.createElement('a')
button.classList.add( button.classList.add(
'orange-button', 'peertube-button-link', 'primary-button', 'orange-button', 'peertube-button-link',
'peertube-plugin-livechat-button', 'peertube-plugin-livechat-button',
'peertube-plugin-livechat-button-' + dbo.name 'peertube-plugin-livechat-button-' + dbo.name
) )

View File

@ -60,8 +60,8 @@ async function initChat (video: Video): Promise<void> {
return return
} }
let showShareUrlButton: boolean = false let showShareUrlButton = false
let showPromote: boolean = false let showPromote = false
if (video.isLocal) { // No need for shareButton on remote chats. if (video.isLocal) { // No need for shareButton on remote chats.
const chatShareUrl = settings['chat-share-url'] ?? '' const chatShareUrl = settings['chat-share-url'] ?? ''
if (chatShareUrl === 'everyone') { if (chatShareUrl === 'everyone') {
@ -187,9 +187,10 @@ async function _insertChatDom (
callback: async () => { callback: async () => {
try { try {
// First we must get the room JID (can be video.uuid@ or channel.id@) // First we must get the room JID (can be video.uuid@ or channel.id@)
const url = getBaseRoute(ptContext.ptOptions) + '/api/configuration/room/' +
encodeURIComponent(video.uuid)
const response = await fetch( const response = await fetch(
getBaseRoute(ptContext.ptOptions) + '/api/configuration/room/' + url,
encodeURIComponent(video.uuid),
{ {
method: 'GET', method: 'GET',
headers: peertubeHelpers.getAuthHeader() headers: peertubeHelpers.getAuthHeader()
@ -303,7 +304,7 @@ async function _openChat (video: Video): Promise<void | false> {
// Loading converseJS... // Loading converseJS...
await displayConverseJS(ptContext.ptOptions, container, roomkey, 'peertube-video', false) await displayConverseJS(ptContext.ptOptions, container, roomkey, 'peertube-video', false)
} catch (err) { } catch (_err) {
// Displaying an error page. // Displaying an error page.
if (container) { if (container) {
const message = document.createElement('div') const message = document.createElement('div')

View File

@ -14,7 +14,7 @@ import { getIframeUri, getXMPPAddr, UriOptions } from '../uri'
import { isAnonymousUser } from '../../../utils/user' import { isAnonymousUser } from '../../../utils/user'
// First is default tab. // First is default tab.
const validTabNames = ['embed', 'dock', 'peertube', 'xmpp'] as const const validTabNames: string[] = ['embed', 'dock', 'peertube', 'xmpp'] as const
type ValidTabNames = typeof validTabNames[number] type ValidTabNames = typeof validTabNames[number]
@ -61,49 +61,49 @@ export class ShareChatElement extends LivechatElement {
* Should we render the XMPP tab? * Should we render the XMPP tab?
*/ */
@property({ attribute: false }) @property({ attribute: false })
public xmppUriEnabled: boolean = false public xmppUriEnabled = false
/** /**
* Should we render the Dock tab? * Should we render the Dock tab?
*/ */
@property({ attribute: false }) @property({ attribute: false })
public dockEnabled: boolean = false public dockEnabled = false
/** /**
* Can we use autocolors? * Can we use autocolors?
*/ */
@property({ attribute: false }) @property({ attribute: false })
public autocolorsAvailable: boolean = false public autocolorsAvailable = false
/** /**
* In the Embed tab, should we generated an iframe link. * In the Embed tab, should we generated an iframe link.
*/ */
@property({ attribute: false }) @property({ attribute: false })
public embedIFrame: boolean = false public embedIFrame = false
/** /**
* In the Embed tab, should we generated a read-only chat link. * In the Embed tab, should we generated a read-only chat link.
*/ */
@property({ attribute: false }) @property({ attribute: false })
public embedReadOnly: boolean = false public embedReadOnly = false
/** /**
* Read-only, with scrollbar? * Read-only, with scrollbar?
*/ */
@property({ attribute: false }) @property({ attribute: false })
public embedReadOnlyScrollbar: boolean = false public embedReadOnlyScrollbar = false
/** /**
* Read-only, transparent background? * Read-only, transparent background?
*/ */
@property({ attribute: false }) @property({ attribute: false })
public embedReadOnlyTransparentBackground: boolean = false public embedReadOnlyTransparentBackground = false
/** /**
* In the Embed tab, should we use current theme color? * In the Embed tab, should we use current theme color?
*/ */
@property({ attribute: false }) @property({ attribute: false })
public embedAutocolors: boolean = false public embedAutocolors = false
protected override firstUpdated (changedProperties: PropertyValues): void { protected override firstUpdated (changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties) super.firstUpdated(changedProperties)
@ -156,7 +156,7 @@ export class ShareChatElement extends LivechatElement {
return return
} }
this.logger.log('Restoring previous state') this.logger.log('Restoring previous state')
if (validTabNames.includes(v.currentTab)) { if (validTabNames.includes(v.currentTab as string)) {
this.currentTab = v.currentTab this.currentTab = v.currentTab
} }
this.embedIFrame = !!v.embedIFrame this.embedIFrame = !!v.embedIFrame

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import type { ShareChatElement } from '../share-chat' import type { ShareChatElement } from '../share-chat'
import { html, TemplateResult } from 'lit' import { html, TemplateResult } from 'lit'
import { ptTr } from '../../../lib/directives/translation' import { ptTr } from '../../../lib/directives/translation'

View File

@ -71,8 +71,7 @@ async function shareChatUrl (
addedNodes.forEach(node => { addedNodes.forEach(node => {
if ((node as HTMLElement).localName === 'ngb-modal-window') { if ((node as HTMLElement).localName === 'ngb-modal-window') {
logger.info('Detecting a new modal, checking if this is the good one...') logger.info('Detecting a new modal, checking if this is the good one...')
if (!(node as HTMLElement).querySelector) { return } const title = (node as HTMLElement).querySelector?.('.modal-title')
const title = (node as HTMLElement).querySelector('.modal-title')
if (!(title?.textContent === labelShare)) { if (!(title?.textContent === labelShare)) {
return return
} }

View File

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

View File

@ -156,12 +156,12 @@ function launchTests (): void {
'content-type': 'application/json;charset=UTF-8' 'content-type': 'application/json;charset=UTF-8'
}), }),
body: JSON.stringify({ body: JSON.stringify({
test: test test
}) })
}) })
if (!response.ok) { if (!response.ok) {
return { return {
test: test, test,
messages: [response.statusText ?? 'Unknown error'], messages: [response.statusText ?? 'Unknown error'],
ok: false ok: false
} }
@ -169,7 +169,7 @@ function launchTests (): void {
const data = await response.json() const data = await response.json()
if ((typeof data) !== 'object') { if ((typeof data) !== 'object') {
return { return {
test: test, test,
messages: ['Incorrect reponse type: ' + (typeof data)], messages: ['Incorrect reponse type: ' + (typeof data)],
ok: false ok: false
} }
@ -190,6 +190,7 @@ function launchTests (): void {
waiting.innerHTML = '<i>Testing...</i>' waiting.innerHTML = '<i>Testing...</i>'
ul.append(waiting) ul.append(waiting)
if ((typeof result.next) === 'function') { if ((typeof result.next) === 'function') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const r: Result = (result.next as Function)() const r: Result = (result.next as Function)()
waiting.remove() waiting.remove()
await machine(r) await machine(r)

View File

@ -24,18 +24,41 @@ function computeAutoColors (): AutoColors | null {
const buttonStyles = window.getComputedStyle(button) const buttonStyles = window.getComputedStyle(button)
const autocolors: AutoColors = { const autocolors: AutoColors = {
mainForeground: styles.getPropertyValue('--mainForegroundColor').trim(), mainForeground: styles.getPropertyValue('--fg').trim() ||
mainBackground: styles.getPropertyValue('--mainBackgroundColor').trim(), styles.getPropertyValue('--mainForegroundColor').trim(),
greyForeground: styles.getPropertyValue('--greyForegroundColor').trim(),
greyBackground: styles.getPropertyValue('--greyBackgroundColor').trim(), mainBackground: styles.getPropertyValue('--bg').trim() ||
menuForeground: styles.getPropertyValue('--menuForegroundColor').trim(), styles.getPropertyValue('--mainBackgroundColor').trim(),
menuBackground: styles.getPropertyValue('--menuBackgroundColor').trim(),
inputForeground: styles.getPropertyValue('--inputForegroundColor').trim(), greyForeground: styles.getPropertyValue('--fg-300').trim() ||
inputBackground: styles.getPropertyValue('--inputBackgroundColor').trim(), styles.getPropertyValue('--greyForegroundColor').trim(),
buttonForeground: buttonStyles.color.trim(),
buttonBackground: styles.getPropertyValue('--mainColor').trim(), greyBackground: styles.getPropertyValue('--bg-secondary-300').trim() ||
link: styles.getPropertyValue('--mainForegroundColor').trim(), styles.getPropertyValue('--greyBackgroundColor').trim(),
linkHover: styles.getPropertyValue('--mainForegroundColor').trim()
menuForeground: styles.getPropertyValue('--fg').trim() ||
styles.getPropertyValue('--menuForegroundColor').trim(),
menuBackground: styles.getPropertyValue('--bg-secondary-400').trim() ||
styles.getPropertyValue('--menuBackgroundColor').trim(),
inputForeground: styles.getPropertyValue('--input-fg').trim() ||
styles.getPropertyValue('--inputForegroundColor').trim(),
inputBackground: styles.getPropertyValue('--input-bg').trim() ||
styles.getPropertyValue('--inputBackgroundColor').trim(),
buttonForeground: styles.getPropertyValue('--on-primary').trim() ||
buttonStyles.color.trim(),
buttonBackground: styles.getPropertyValue('--primary').trim() ||
styles.getPropertyValue('--mainColor').trim(),
link: styles.getPropertyValue('--fg').trim() ||
styles.getPropertyValue('--mainForegroundColor').trim(),
linkHover: styles.getPropertyValue('--fg-400').trim() ||
styles.getPropertyValue('--mainForegroundColor').trim()
} }
const autoColorsTest = areAutoColorsValid(autocolors) const autoColorsTest = areAutoColorsValid(autocolors)
if (autoColorsTest !== true) { if (autoColorsTest !== true) {

View File

@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/> // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
import type { RegisterClientOptions } from '@peertube/peertube-types/client' import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import type { InitConverseJSParams, ChatPeertubeIncludeMode } from 'shared/lib/types' import type { InitConverseJSParams, ChatPeertubeIncludeMode } from 'shared/lib/types'
@ -17,7 +18,7 @@ declare global {
} }
} }
let pollListenerInitiliazed: boolean = false let pollListenerInitiliazed = false
/** /**
* load the ConverseJS CSS. * load the ConverseJS CSS.
@ -152,10 +153,11 @@ async function displayConverseJS (
const authHeader = peertubeHelpers.getAuthHeader() const authHeader = peertubeHelpers.getAuthHeader()
const url = getBaseRoute(clientOptions) + '/api/configuration/room/' +
encodeURIComponent(roomKey) +
(forceType ? '?forcetype=1' : '')
const response = await fetch( const response = await fetch(
getBaseRoute(clientOptions) + '/api/configuration/room/' + url,
encodeURIComponent(roomKey) +
(forceType ? '?forcetype=1' : ''),
{ {
method: 'GET', method: 'GET',
headers: authHeader headers: authHeader

View File

@ -4,7 +4,7 @@
import type { RegisterClientOptions } from '@peertube/peertube-types/client' import type { RegisterClientOptions } from '@peertube/peertube-types/client'
function getBaseRoute ({ peertubeHelpers }: RegisterClientOptions, permanent: boolean = false): string { function getBaseRoute ({ peertubeHelpers }: RegisterClientOptions, permanent = false): string {
if (permanent) { if (permanent) {
return '/plugins/livechat/router' return '/plugins/livechat/router'
} }

View File

@ -1,40 +0,0 @@
{
"root": true,
"env": {
"browser": true,
"es6": true
},
"extends": [
"standard-with-typescript"
],
"globals": {},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"project": [
"./conversejs/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

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

View File

@ -4,6 +4,7 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable @typescript-eslint/no-require-imports */
const fs = require('node:fs') const fs = require('node:fs')
const path = require('node:path') const path = require('node:path')
const YAML = require('yaml') const YAML = require('yaml')

View File

@ -18,8 +18,10 @@ set -x
CONVERSE_VERSION="v11.0.0" CONVERSE_VERSION="v11.0.0"
CONVERSE_REPO="https://github.com/conversejs/converse.js.git" CONVERSE_REPO="https://github.com/conversejs/converse.js.git"
# You can eventually set CONVERSE_COMMIT to a specific commit ID, if you want to apply some patches. # You can eventually set CONVERSE_COMMIT to a specific commit ID, if you want to apply some patches.
# 2024-09-02: using Converse upstream (v11 WIP). # 2024-09-17: using Converse upstream (v11 WIP).
CONVERSE_COMMIT="9952046d580bc2930e29833f4c9987a3d4c95bc2" CONVERSE_COMMIT="07dc6f4f5da5890b02a46a8a2f2d0498649786bc"
# 2024-12-03: using Converse upstream (v11 WIP).
CONVERSE_COMMIT="8f32df723e3aa392db02326dc6a3279c9497b6fb"
# It is possible to use another repository, if we want some customization that are not upstream (yet): # It is possible to use another repository, if we want some customization that are not upstream (yet):
# CONVERSE_VERSION="livechat" # CONVERSE_VERSION="livechat"
@ -29,8 +31,8 @@ CONVERSE_COMMIT="9952046d580bc2930e29833f4c9987a3d4c95bc2"
# 2024-09-03: include badges short label and quick fix for sendMessage button # 2024-09-03: include badges short label and quick fix for sendMessage button
CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js" CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js"
CONVERSE_VERSION="livechat-11.0.1" CONVERSE_VERSION="livechat-12.0.0"
CONVERSE_COMMIT="" # CONVERSE_COMMIT=""
rootdir="$(pwd)" rootdir="$(pwd)"
src_dir="$rootdir/conversejs" src_dir="$rootdir/conversejs"
@ -40,6 +42,7 @@ if [ -n "$CONVERSE_COMMIT" ]; then
fi fi
converse_build_dir="$rootdir/build/conversejs" converse_build_dir="$rootdir/build/conversejs"
converse_destination_dir="$rootdir/dist/client/conversejs" converse_destination_dir="$rootdir/dist/client/conversejs"
converse_emoji_destination="$rootdir/dist/converse-emoji.json"
if [[ ! -d $src_dir ]]; then if [[ ! -d $src_dir ]]; then
echo "$0 must be called from the plugin livechat root dir." echo "$0 must be called from the plugin livechat root dir."
@ -119,6 +122,9 @@ cd $rootdir
echo "Copying ConverseJS dist files..." echo "Copying ConverseJS dist files..."
mkdir -p "$converse_destination_dir" && cp -r $converse_build_dir/dist/* "$converse_destination_dir/" mkdir -p "$converse_destination_dir" && cp -r $converse_build_dir/dist/* "$converse_destination_dir/"
echo "Copying ConverseJS original emoji.json file..." # this is needed for some backend code.
cp "$converse_build_dir/src/headless/plugins/emoji/emoji.json" "$converse_emoji_destination"
echo "ConverseJS OK." echo "ConverseJS OK."
exit 0 exit 0

View File

@ -2,6 +2,8 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
import type { InitConverseJSParams, ChatIncludeMode, ExternalAuthResult } from 'shared/lib/types' import type { InitConverseJSParams, ChatIncludeMode, ExternalAuthResult } from 'shared/lib/types'
import { inIframe } from './lib/utils' import { inIframe } from './lib/utils'
import { initDom } from './lib/dom' import { initDom } from './lib/dom'
@ -21,6 +23,7 @@ import { livechatViewerModePlugin } from './lib/plugins/livechat-viewer-mode'
import { livechatMiniMucHeadPlugin } from './lib/plugins/livechat-mini-muc-head' import { livechatMiniMucHeadPlugin } from './lib/plugins/livechat-mini-muc-head'
import { livechatEmojisPlugin } from './lib/plugins/livechat-emojis' import { livechatEmojisPlugin } from './lib/plugins/livechat-emojis'
import { moderationDelayPlugin } from './lib/plugins/moderation-delay' import { moderationDelayPlugin } from './lib/plugins/moderation-delay'
import { livechatAnnouncementsPlugin } from './lib/plugins/livechat-announcements'
declare global { declare global {
interface Window { interface Window {
@ -35,11 +38,17 @@ declare global {
html: Function html: Function
sizzle: Function sizzle: Function
dayjs: Function dayjs: Function
__: Function
u: {
hasClass: Function
addClass: Function
removeClass: Function
}
} }
} }
initConversePlugins: typeof initConversePlugins initConversePlugins: typeof initConversePlugins
initConverse: typeof initConverse initConverse: typeof initConverse
reconnectConverse?: (room: string) => void reconnectConverse?: (params: any) => void
externalAuthGetResult?: (data: ExternalAuthResult) => void externalAuthGetResult?: (data: ExternalAuthResult) => void
} }
} }
@ -74,6 +83,8 @@ function initConversePlugins (peertubeEmbedded: boolean): void {
converse.plugins.add('livechatViewerModePlugin', livechatViewerModePlugin) converse.plugins.add('livechatViewerModePlugin', livechatViewerModePlugin)
converse.plugins.add('converse-moderation-delay', moderationDelayPlugin) converse.plugins.add('converse-moderation-delay', moderationDelayPlugin)
converse.plugins.add('livechatAnnouncementsPlugin', livechatAnnouncementsPlugin)
} }
window.initConversePlugins = initConversePlugins window.initConversePlugins = initConversePlugins
@ -86,7 +97,7 @@ window.initConversePlugins = initConversePlugins
async function initConverse ( async function initConverse (
initConverseParams: InitConverseJSParams, initConverseParams: InitConverseJSParams,
chatIncludeMode: ChatIncludeMode = 'chat-only', chatIncludeMode: ChatIncludeMode = 'chat-only',
peertubeAuthHeader?: { [header: string]: string } | null peertubeAuthHeader?: Record<string, string> | null
): Promise<void> { ): Promise<void> {
// First, fixing relative websocket urls. // First, fixing relative websocket urls.
if (initConverseParams.localWebsocketServiceUrl?.startsWith('/')) { if (initConverseParams.localWebsocketServiceUrl?.startsWith('/')) {
@ -121,9 +132,9 @@ async function initConverse (
params.view_mode = chatIncludeMode === 'chat-only' ? 'fullscreen' : 'embedded' params.view_mode = chatIncludeMode === 'chat-only' ? 'fullscreen' : 'embedded'
params.allow_url_history_change = chatIncludeMode === 'chat-only' params.allow_url_history_change = chatIncludeMode === 'chat-only'
let isAuthenticated: boolean = false let isAuthenticated = false
let isAuthenticatedWithExternalAccount: boolean = false let isAuthenticatedWithExternalAccount = false
let isRemoteWithNicknameSet: boolean = false let isRemoteWithNicknameSet = false
// OIDC (OpenID Connect): // OIDC (OpenID Connect):
const tryOIDC = (initConverseParams.externalAuthOIDC?.length ?? 0) > 0 const tryOIDC = (initConverseParams.externalAuthOIDC?.length ?? 0) > 0

View File

@ -66,6 +66,7 @@ CORE_PLUGINS.push('livechat-converse-mam-search')
// We must also add our custom ROOM_FEATURES, so that they correctly resets // We must also add our custom ROOM_FEATURES, so that they correctly resets
// (see headless/plugins/muc, getDiscoInfoFeatures, which loops on this const) // (see headless/plugins/muc, getDiscoInfoFeatures, which loops on this const)
ROOM_FEATURES.push('x_peertubelivechat_mute_anonymous') ROOM_FEATURES.push('x_peertubelivechat_mute_anonymous')
ROOM_FEATURES.push('x_peertubelivechat_emoji_only_mode')
_converse.exports.CustomElement = CustomElement _converse.exports.CustomElement = CustomElement

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { converseLocalizedHelpUrl } from '../../../shared/lib/help' import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
import { tplMUCApp } from '../../../shared/components/muc-app/templates/muc-app.js' import { tplMUCApp } from '../../../shared/components/muc-app/templates/muc-app.js'
import { html } from 'lit' import { html } from 'lit'

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { html } from 'lit' import { html } from 'lit'
/** /**

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { html } from 'lit' import { html } from 'lit'
import { api } from '@converse/headless' import { api } from '@converse/headless'
import { getAuthorStyle } from '../../../../src/utils/color.js' import { getAuthorStyle } from '../../../../src/utils/color.js'

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { converseLocalizedHelpUrl } from '../../../shared/lib/help' import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
import { tplMUCApp } from '../../../shared/components/muc-app/templates/muc-app.js' import { tplMUCApp } from '../../../shared/components/muc-app/templates/muc-app.js'
import { html } from 'lit' import { html } from 'lit'

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { html } from 'lit' import { html } from 'lit'
import { api } from '@converse/headless' import { api } from '@converse/headless'
import { getAuthorStyle } from '../../../../src/utils/color.js' import { getAuthorStyle } from '../../../../src/utils/color.js'

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { api } from '@converse/headless' import { api } from '@converse/headless'
import { html } from 'lit' import { html } from 'lit'
import { __ } from 'i18n' import { __ } from 'i18n'

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { html } from 'lit' import { html } from 'lit'
import { repeat } from 'lit/directives/repeat.js' import { repeat } from 'lit/directives/repeat.js'
import { __ } from 'i18n' import { __ } from 'i18n'

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { __ } from 'i18n' import { __ } from 'i18n'
import BaseModal from 'plugins/modal/modal.js' import BaseModal from 'plugins/modal/modal.js'
import { api } from '@converse/headless' import { api } from '@converse/headless'

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { converseLocalizedHelpUrl } from '../../../shared/lib/help' import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
import { html } from 'lit' import { html } from 'lit'
import { __ } from 'i18n' import { __ } from 'i18n'

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { html } from 'lit' import { html } from 'lit'
import { repeat } from 'lit/directives/repeat.js' import { repeat } from 'lit/directives/repeat.js'
import { __ } from 'i18n' import { __ } from 'i18n'

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { html } from 'lit' import { html } from 'lit'
import { repeat } from 'lit/directives/repeat.js' import { repeat } from 'lit/directives/repeat.js'
import { __ } from 'i18n' import { __ } from 'i18n'

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { converseLocalizedHelpUrl } from '../../../shared/lib/help' import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
import { tplMUCApp } from '../../../shared/components/muc-app/templates/muc-app.js' import { tplMUCApp } from '../../../shared/components/muc-app/templates/muc-app.js'
import { html } from 'lit' import { html } from 'lit'

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { html } from 'lit' import { html } from 'lit'
import { repeat } from 'lit/directives/repeat.js' import { repeat } from 'lit/directives/repeat.js'
import { __ } from 'i18n' import { __ } from 'i18n'

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { html } from 'lit' import { html } from 'lit'
import { repeat } from 'lit/directives/repeat.js' import { repeat } from 'lit/directives/repeat.js'
import { __ } from 'i18n' import { __ } from 'i18n'

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { html } from 'lit' import { html } from 'lit'
import { __ } from 'i18n' import { __ } from 'i18n'
@ -20,7 +23,8 @@ export function tplMucTask (el, task) {
type="checkbox" type="checkbox"
class="form-check-input" class="form-check-input"
.checked=${done === true} .checked=${done === true}
@click=${(_ev) => { @click=${(ev) => {
ev?.preventDefault()
task.set('done', !done) task.set('done', !done)
task.saveItem() task.saveItem()
}} }}

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { CustomElement } from 'shared/components/element.js' import { CustomElement } from 'shared/components/element.js'
import { api } from '@converse/headless' import { api } from '@converse/headless'
import { html } from 'lit' import { html } from 'lit'

View File

@ -0,0 +1,59 @@
/*
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
// FIXME: this should be with the livechat-announcement plugin.
// But for now, there is no way to build scss from there.
#conversejs {
.message.chat-msg {
&.livechat-announcement {
--livechat-announcement-color: #000;
--livechat-announcement-background-color: #dbf2d8;
--livechat-announcement-border-color: #2ab218;
}
&.livechat-highlight {
--livechat-announcement-color: #000;
--livechat-announcement-background-color: #dce8fa;
--livechat-announcement-border-color: #3075e5;
}
&.livechat-warning {
--livechat-announcement-color: #000;
--livechat-announcement-background-color: #fadede;
--livechat-announcement-border-color: #e03e3e;
}
&.livechat-announcement,
&.livechat-highlight,
&.livechat-warning {
converse-chat-message-body {
border: 2px solid var(--livechat-announcement-border-color);
color: var(--livechat-announcement-color);
background-color: var(--livechat-announcement-background-color);
min-width: 50%;
padding: 1em;
}
}
}
.livechat-announcements-form {
label {
// only for screen readers
border: 0 !important;
clip: rect(1px, 1px, 1px, 1px) !important;
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-clip-path: inset(50%) !important;
clip-path: inset(50%) !important;
height: 1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
width: 1px !important;
white-space: nowrap !important;
}
}
}

View File

@ -35,6 +35,14 @@
} }
} }
// Emoji only info box
.livechat-emoji-only-info-box {
border: 1px dashed var(--peertube-menu-background);
color: var(--peertube-main-foreground);
background-color: var(--peertube-main-background);
margin: 0 5px;
}
converse-chat-toolbar { converse-chat-toolbar {
border-top: none !important; // removing border, to avoid confusing the toolbar with an input field. border-top: none !important; // removing border, to avoid confusing the toolbar with an input field.
color: var(--peertube-main-foreground); color: var(--peertube-main-foreground);
@ -43,6 +51,10 @@
// Fixing emoji colors for some emoji like «motorcycle» // Fixing emoji colors for some emoji like «motorcycle»
converse-emoji-picker { converse-emoji-picker {
// Must set display block. Without this, Converse defined max-width will not apply.
// Don't really know why it is working in pure ConverseJs and not in livechat.
display: block;
.emoji-picker { .emoji-picker {
.insert-emoji { .insert-emoji {
a { a {
@ -52,13 +64,17 @@
} }
.emoji-picker__header { .emoji-picker__header {
color: var(--peertube-main-background); background-color: var(--peertube-main-background);
background-color: var(--peertube-main-foreground); color: var(--peertube-main-foreground);
.emoji-search {
color: currentcolor;
}
ul { ul {
.emoji-category { .emoji-category {
color: var(--peertube-main-background); background-color: var(--peertube-main-background);
background-color: var(--peertube-main-foreground); color: var(--peertube-main-foreground);
a { a {
color: currentcolor; color: currentcolor;
@ -190,7 +206,7 @@
} }
// Bigger occupants sidebar when width is not big enough. // Bigger occupants sidebar when width is not big enough.
@media screen and (max-width: 576px) { @media screen and (width <= 576px) {
.chatroom .box-flyout .chatroom-body .occupants { .chatroom .box-flyout .chatroom-body .occupants {
min-width: 50%; min-width: 50%;
} }

View File

@ -7,6 +7,7 @@
@import "./variables"; @import "./variables";
@import "shared/styles/index"; @import "shared/styles/index";
@import "./peertubetheme"; @import "./peertubetheme";
@import "./announcements";
body.livechat-iframe { body.livechat-iframe {
#conversejs .chat-head { #conversejs .chat-head {
@ -21,7 +22,7 @@ body.livechat-iframe {
} }
#conversejs .livechat-mini-muc-bar-buttons { #conversejs .livechat-mini-muc-bar-buttons {
a.orange-button { a.primary-button {
// force these colors... // force these colors...
color: var(--peertube-button-foreground); color: var(--peertube-button-foreground);
background-color: var(--peertube-button-background); background-color: var(--peertube-button-background);
@ -202,6 +203,7 @@ body.converse-embedded {
// This margin-left trick is to align the button on the right. // This margin-left trick is to align the button on the right.
margin-left: auto !important; margin-left: auto !important;
order: 99; order: 99;
white-space: nowrap;
} }
} }
} }
@ -229,3 +231,27 @@ body.converse-embedded {
} }
} }
} }
// When livechat has not many height, must reduce the emoji picker height.
/* stylelint-disable-next-line no-duplicate-selectors */
#conversejs {
&[livechat-converse-root-height="small"] {
converse-emoji-picker {
converse-emoji-picker-content {
.emoji-picker__lists {
height: 2em;
}
}
}
}
&[livechat-converse-root-height="medium"] {
converse-emoji-picker {
converse-emoji-picker-content {
.emoji-picker__lists {
height: 4em;
}
}
}
}
}

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { _converse, api } from '@converse/headless' import { _converse, api } from '@converse/headless'
import { __ } from 'i18n' import { __ } from 'i18n'
import { html } from 'lit' import { html } from 'lit'
@ -28,8 +31,9 @@ function externalLoginClickHandler (ev, el, externalAuthOIDCUrl) {
console.log('Received an external authentication result...', data) console.log('Received an external authentication result...', data)
if (!data.ok) { if (!data.ok) {
// eslint-disable-next-line no-undef el.external_auth_oidc_alert_message =
el.external_auth_oidc_alert_message = __(LOC_login_external_auth_alert_message) + // eslint-disable-next-line no-undef
__(LOC_login_external_auth_alert_message) +
(data.message ? ` (${data.message})` : '') (data.message ? ` (${data.message})` : '')
return return
} }

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { __ } from 'i18n' import { __ } from 'i18n'
import { _converse, api } from '@converse/headless' import { _converse, api } from '@converse/headless'
import { html } from 'lit' import { html } from 'lit'
@ -83,6 +86,20 @@ const tplSlowMode = (o) => {
return html`<livechat-slow-mode jid=${o.model.get('jid')}>` return html`<livechat-slow-mode jid=${o.model.get('jid')}>`
} }
const tplEmojiOnly = (o) => {
if (!o.can_post) { return html`` }
if (!o.model.features?.get?.('x_peertubelivechat_emoji_only_mode')) {
return ''
}
return html`<div class="livechat-emoji-only-info-box">
<converse-icon class="fa fa-info-circle" size="1.2em"></converse-icon>
${
// eslint-disable-next-line no-undef
__(LOC_emoji_only_info)
}
</div>`
}
const tplViewerMode = (o) => { const tplViewerMode = (o) => {
if (!api.settings.get('livechat_enable_viewer_mode')) { if (!api.settings.get('livechat_enable_viewer_mode')) {
return html`` return html``
@ -145,6 +162,7 @@ export default (o) => {
return html` return html`
${tplViewerMode(o)} ${tplViewerMode(o)}
${tplSlowMode(o)} ${tplSlowMode(o)}
${tplEmojiOnly(o)}
${ ${
mutedAnonymousMessage mutedAnonymousMessage
? html`<span class="muc-bottom-panel muc-bottom-panel--muted">${mutedAnonymousMessage}</span>` ? html`<span class="muc-bottom-panel muc-bottom-panel--muted">${mutedAnonymousMessage}</span>`

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { api } from '@converse/headless' import { api } from '@converse/headless'
import tplMUCChatarea from '../../src/plugins/muc-views/templates/muc-chatarea.js' import tplMUCChatarea from '../../src/plugins/muc-views/templates/muc-chatarea.js'
import { html } from 'lit' import { html } from 'lit'

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { html } from 'lit' import { html } from 'lit'
import { api } from '@converse/headless' import { api } from '@converse/headless'
import { until } from 'lit/directives/until.js' import { until } from 'lit/directives/until.js'

View File

@ -4,6 +4,9 @@
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
// Must import the original muc.js, because it imports some custom elements files. // Must import the original muc.js, because it imports some custom elements files.
import '../../src/plugins/muc-views/templates/muc.js' import '../../src/plugins/muc-views/templates/muc.js'
import { getChatRoomBodyTemplate } from '../../src/plugins/muc-views/utils.js' import { getChatRoomBodyTemplate } from '../../src/plugins/muc-views/utils.js'

11
conversejs/lib/@types/global.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
// Important note: loc segments that are declared here must also be in loc.keys.js (for now).
declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE: string
declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE_STANDARD: string
declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE_ANNOUNCEMENT: string
declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE_HIGHLIGHT: string
declare const LOC_ANNOUNCEMENTS_MESSAGE_TYPE_WARNING: string

View File

@ -4,7 +4,7 @@
import type { ProsodyAuthentInfos } from 'shared/lib/types' import type { ProsodyAuthentInfos } from 'shared/lib/types'
interface AuthHeader { [key: string]: string } type AuthHeader = Record<string, string>
async function getLocalAuthentInfos ( async function getLocalAuthentInfos (
authenticationUrl: string, authenticationUrl: string,
@ -66,7 +66,7 @@ async function getLocalAuthentInfos (
{ {
'content-type': 'application/json;charset=UTF-8' 'content-type': 'application/json;charset=UTF-8'
} }
) ) as HeadersInit
) )
}) })
@ -104,7 +104,7 @@ async function getLocalAuthentInfos (
function getLivechatTokenAuthInfos (): ProsodyAuthentInfos | undefined { function getLivechatTokenAuthInfos (): ProsodyAuthentInfos | undefined {
try { try {
const hash = window.location.hash const hash = window.location.hash
if (!hash || !hash.startsWith('#?')) { return undefined } if (!hash?.startsWith('#?')) { return undefined }
// We try to read the hash as a queryString. // We try to read the hash as a queryString.
const u = new URL('http://localhost' + hash.substring(1)) const u = new URL('http://localhost' + hash.substring(1))
const jid = u.searchParams.get('j') const jid = u.searchParams.get('j')

View File

@ -86,7 +86,8 @@ function defaultConverseParams (
'livechatDisconnectOnUnloadPlugin', 'livechatDisconnectOnUnloadPlugin',
'converse-slow-mode', 'converse-slow-mode',
'livechatEmojis', 'livechatEmojis',
'converse-moderation-delay' 'converse-moderation-delay',
'livechatAnnouncementsPlugin'
], ],
show_retraction_warning: false, // No need to use this warning (except if we open to external clients?) show_retraction_warning: false, // No need to use this warning (except if we open to external clients?)
muc_show_info_messages: mucShowInfoMessages, muc_show_info_messages: mucShowInfoMessages,

View File

@ -0,0 +1,249 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
interface Current {
announcementType: string | undefined
}
/**
* livechat announcements ConverseJS plugin:
* with this plugin, moderators can send highlighted/announcements messages.
*
* Moderators will have a special select field in the chat toolbar, so that they can choose a messaging style.
* These special messages will have a first line with a generated title (for XMPP compatibility).
* They will also have a special attribute on the body tag.
* This attribute will be used to apply some CSS with this plugin.
*/
export const livechatAnnouncementsPlugin = {
dependencies: ['converse-muc', 'converse-muc-views'],
initialize: function (this: any) {
const _converse = this._converse
// This is a closure variable, to get the current form status when sending a message.
const current: Current = {
announcementType: undefined
}
overrideMUCMessageForm(_converse, current)
_converse.api.listen.on('getToolbarButtons', getToolbarButtons.bind(this))
_converse.api.listen.on('chatRoomInitialized', (muc: any) => onAffiliationChange(_converse, muc))
_converse.api.listen.on('getOutgoingMessageAttributes', (chatbox: any, attrs: any) => {
return onGetOutgoingMessageAttributes(current, _converse, chatbox, attrs)
})
_converse.api.listen.on('createMessageStanza', createMessageStanza)
_converse.api.listen.on('parseMUCMessage', parseMUCMessage)
overrideMessage(_converse)
}
}
/**
* Overloads the MUCMessageForm to handle the announcement type field (if present) when sending a message.
*
* Also hides the announcement type field if we are correcting a previous message.
*/
function overrideMUCMessageForm (_converse: any, current: Current): void {
const MUCMessageForm = _converse.api.elements.registry['converse-muc-message-form']
if (MUCMessageForm) {
class MUCMessageFormloaded extends MUCMessageForm {
async onFormSubmitted (ev?: Event): Promise<void> {
const announcementSelect = this.querySelector('[name=livechat-announcements]')
current.announcementType = announcementSelect?.selectedOptions?.[0]?.value || undefined
try {
await super.onFormSubmitted(ev)
if (announcementSelect) { announcementSelect.selectedIndex = 0 } // set back to default
} catch (err) {
console.log(err)
}
current.announcementType = undefined
}
insertIntoTextArea (...args: any[]): void {
super.insertIntoTextArea(...args)
try {
// FIXME: doing this here is not very clean.
// But that's how ConverseJS adds or removes the 'correction' class to the textarea.
const textarea = this.querySelector('.chat-textarea')
if (!textarea) { return }
const correcting = window.converse.env.u.hasClass('correcting', textarea)
const announcementForm = this.querySelector('.livechat-announcements-form')
const announcementSelect = this.querySelector('[name=livechat-announcements]')
if (correcting) {
if (announcementSelect) { announcementSelect.selectedIndex = 0 }
if (announcementForm) { announcementForm.style.display = 'none' }
} else {
if (announcementForm) { announcementForm.style.display = 'block' }
}
} catch (err) {
console.error(err)
}
}
}
_converse.api.elements.define('converse-muc-message-form', MUCMessageFormloaded)
}
}
/**
* Adds the announcement selector in the toolbar for owner/admin.
* @param this the plugin
* @param toolbarEl the toolbar element
* @param buttons the button list
* @returns the updated "button" list
*/
function getToolbarButtons (this: any, toolbarEl: any, buttons: any[]): Parameters<typeof getToolbarButtons>[1] {
const _converse = this._converse
const mucModel = toolbarEl.model
if (!toolbarEl.is_groupchat) {
return buttons
}
const myself = mucModel.getOwnOccupant()
if (!myself || !['admin', 'owner'].includes(myself.get('affiliation') as string)) {
return buttons
}
const { __ } = _converse
const { html } = window.converse.env
const i18n = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE)
const i18nStandard = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_STANDARD)
const i18nAnnouncement = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_ANNOUNCEMENT)
const i18nHighlight = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_HIGHLIGHT)
const i18nWarning = __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_WARNING)
const select = html`<span class="livechat-announcements-form form-inline">
<label for="livechat-announcements-select">${i18n}</label>
<select
name="livechat-announcements"
id="livechat-announcements-select"
class="form-control form-control-sm"
title=${i18n}
>
<option value="">${i18nStandard}</option>
<option value="highlight">${i18nHighlight}</option>
<option value="announcement">${i18nAnnouncement}</option>
<option value="warning">${i18nWarning}</option>
</select>
</span>`
if (_converse.api.settings.get('visible_toolbar_buttons').emoji) {
// Emojis should be the first entry, so adding select in second place.
buttons = [
buttons.shift(),
select,
...buttons
]
} else {
// Adding the select in first place.
buttons.unshift(select)
}
return buttons
}
/**
* Refreshed the toolbar when current user affiliation changes.
* @param _converse _converse object
* @param muc the current muc
*/
function onAffiliationChange (_converse: any, muc: any): void {
muc.occupants.on('change:affiliation', (occupant: any) => {
if (occupant.get('jid') !== _converse.bare_jid) { // only for myself
return
}
document.querySelectorAll('converse-chat-toolbar').forEach(e => (e as any).requestUpdate?.())
})
}
/**
* For outgoing message, adding the announcement type if there is a current one.
* @param current current object
* @param _converse _converse object
* @param chatbox the chatbox
* @param attrs message attributes
* @returns
*/
function onGetOutgoingMessageAttributes (
current: Current,
_converse: any,
chatbox: any,
attrs: any
): Parameters<typeof onGetOutgoingMessageAttributes>[3] {
if (!current.announcementType) { return attrs }
const { __ } = _converse
attrs.livechat_announcement_type = current.announcementType
if (current.announcementType === 'announcement') {
attrs.body = '* ' + __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_ANNOUNCEMENT) + ' * \n' + attrs.body
} else if (current.announcementType === 'warning') {
attrs.body = '* ' + __(LOC_ANNOUNCEMENTS_MESSAGE_TYPE_WARNING) + ' *\n' + attrs.body
}
return attrs
}
/**
* Outgoing messages: adding an attribute on body for announcements.
* @param chat
* @param data
*/
async function createMessageStanza (
chat: any,
data: any
): Promise<Parameters<typeof createMessageStanza>[1]> {
const { message, stanza } = data
const announcementType = message.get('livechat_announcement_type')
if (!announcementType) {
return data
}
stanza.tree().querySelector('message body')?.setAttribute('x-livechat-announcement-type', announcementType)
return data
}
/**
* Incoming messages: checking if there is an announcement attribute, and adding it in computed attributes.
* @param stanza
* @param attrs
*/
function parseMUCMessage (stanza: any, attrs: any): Parameters<typeof parseMUCMessage>[1] {
const { sizzle } = window.converse.env
const body = sizzle('message body', stanza)?.[0]
if (!body) { return attrs }
const announcementType = body.getAttribute('x-livechat-announcement-type')
if (!announcementType) { return attrs }
// Note: we don't check the value here. Will be done in getExtraMessageClasses.
// Moreover, the backend server will ensure that only admins/owners can send this attribute.
attrs.livechat_announcement_type = announcementType
return attrs
}
/**
* Overloading the Message class to add CSS for announcements.
* @param _converse
*/
function overrideMessage (_converse: any): void {
const Message = _converse.api.elements.registry['converse-chat-message']
if (Message) {
class MessageOverloaded extends Message {
getExtraMessageClasses (this: any): string {
// Adding CSS class if the message is an announcement.
let extraClasses = super.getExtraMessageClasses() ?? ''
const announcementType: string | undefined = this.model.get('livechat_announcement_type')
if (!announcementType) {
return extraClasses
}
if (['announcement', 'highlight', 'warning'].includes(announcementType)) {
extraClasses += ' livechat-' + announcementType
}
return extraClasses
}
}
_converse.api.elements.define('converse-chat-message', MessageOverloaded)
}
}

View File

@ -17,19 +17,19 @@ export const livechatEmojisPlugin = {
livechat_custom_emojis_url: false livechat_custom_emojis_url: false
}) })
_converse.api.listen.on('loadEmojis', async (_context: Object, json: any) => { _converse.api.listen.on('loadEmojis', async (_context: object, json: Record<string, Record<string, unknown>>) => {
const url = _converse.api.settings.get('livechat_custom_emojis_url') const url = _converse.api.settings.get('livechat_custom_emojis_url') as string | undefined
if (!url) { if (!url) {
return json return json
} }
let customs let customs: CustomEmojiDefinition[] | undefined
try { try {
customs = await loadCustomEmojis(url) customs = await loadCustomEmojis(url)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
if (customs === undefined || !customs?.length) { if (!customs?.length) {
return json return json
} }
@ -51,7 +51,7 @@ export const livechatEmojisPlugin = {
// We must also remove any existing emojis in category other than custom // We must also remove any existing emojis in category other than custom
for (const type of Object.keys(json)) { for (const type of Object.keys(json)) {
const v: {[key: string]: any} = json[type] const v: Record<string, any> = json[type]
if (type !== 'custom' && type !== 'modifiers' && (def.sn in v)) { if (type !== 'custom' && type !== 'modifiers' && (def.sn in v)) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete v[def.sn] delete v[def.sn]

View File

@ -9,6 +9,7 @@ import { chatRoomOverrides } from './livechat-specific/chatroom'
import { chatRoomMessageOverrides } from './livechat-specific/chatroom-message' import { chatRoomMessageOverrides } from './livechat-specific/chatroom-message'
import { customizeMessageAction } from './livechat-specific/message-action' import { customizeMessageAction } from './livechat-specific/message-action'
import { customizeProfileModal } from './livechat-specific/profile' import { customizeProfileModal } from './livechat-specific/profile'
import { customizeMUCBottomPanel } from './livechat-specific/muc-bottom-panel'
export const livechatSpecificsPlugin = { export const livechatSpecificsPlugin = {
dependencies: ['converse-muc', 'converse-muc-views'], dependencies: ['converse-muc', 'converse-muc-views'],
@ -26,6 +27,7 @@ export const livechatSpecificsPlugin = {
customizeToolbar(this) customizeToolbar(this)
customizeMessageAction(this) customizeMessageAction(this)
customizeProfileModal(this) customizeProfileModal(this)
customizeMUCBottomPanel(this)
_converse.api.listen.on('chatRoomViewInitialized', function (this: any, _model: any): void { _converse.api.listen.on('chatRoomViewInitialized', function (this: any, _model: any): void {
// Remove the spinner if present... // Remove the spinner if present...

View File

@ -2,7 +2,8 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
export function chatRoomMessageOverrides (): {[key: string]: Function} { // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
export function chatRoomMessageOverrides (): Record<string, Function> {
return { return {
/* By default, ConverseJS groups messages from the same users for a 10 minutes period. /* By default, ConverseJS groups messages from the same users for a 10 minutes period.
* This make no sense in a livechat room. So we override isFollowup to ignore. */ * This make no sense in a livechat room. So we override isFollowup to ignore. */

View File

@ -2,7 +2,8 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
export function chatRoomOverrides (): {[key: string]: Function} { // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
export function chatRoomOverrides (): Record<string, Function> {
return { return {
getActionInfoMessage: function getActionInfoMessage (this: any, code: string, nick: string, actor: any): any { getActionInfoMessage: function getActionInfoMessage (this: any, code: string, nick: string, actor: any): any {
if (code === '303') { if (code === '303') {
@ -27,8 +28,9 @@ export function chatRoomOverrides (): {[key: string]: Function} {
initOccupants: function initOccupants (this: any) { initOccupants: function initOccupants (this: any) {
const r = this.__super__.initOccupants() const r = this.__super__.initOccupants()
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const originalComparatorFunction: Function = this.occupants.comparator const originalComparatorFunction: Function = this.occupants.comparator
this.occupants.comparator = function (this: any, occupant1: any, occupant2: any): Number { this.occupants.comparator = function (this: any, occupant1: any, occupant2: any): number {
// Overriding Occupants comparators, to display anonymous users at the end of the list. // Overriding Occupants comparators, to display anonymous users at the end of the list.
const nick1: string = occupant1.getDisplayName() const nick1: string = occupant1.getDisplayName()
const nick2: string = occupant2.getDisplayName() const nick2: string = occupant2.getDisplayName()

View File

@ -19,7 +19,7 @@ export function customizeMessageAction (plugin: any): void {
try { try {
txt += this.model.getDisplayName() as string txt += this.model.getDisplayName() as string
txt += ' - ' txt += ' - '
const date = new Date(this.model.get('edited') || this.model.get('time')) const date = new Date((this.model.get('edited') || this.model.get('time')) as string)
txt += date.toLocaleDateString() + ' ' + date.toLocaleTimeString() txt += date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
txt += '\n' txt += '\n'
} catch {} } catch {}

View File

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
/**
* Override the MUCBottomPanel custom element
*/
export function customizeMUCBottomPanel (plugin: any): void {
const _converse = plugin._converse
const MUCBottomPanel = _converse.api.elements.registry['converse-muc-bottom-panel']
if (MUCBottomPanel) {
class MUCBottomPanelOverloaded extends MUCBottomPanel {
async initialize (): Promise<any> {
await super.initialize()
// We must refresh the bottom panel when these features changes (to display the infobox)
// FIXME: the custom muc-bottom-panel template should be used here, in an overloaded render method, instead
// of using webpack to overload the original file.
this.listenTo(this.model.features, 'change:x_peertubelivechat_emoji_only_mode', () => this.requestUpdate())
}
}
_converse.api.elements.define('converse-muc-bottom-panel', MUCBottomPanelOverloaded)
}
}

View File

@ -2,6 +2,9 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
/** /**
* Do some customization on the toolbar: * Do some customization on the toolbar:
* * change the appearance of the toggle occupants button * * change the appearance of the toggle occupants button

View File

@ -25,7 +25,7 @@ export function getOpenPromise (): any {
promise.isResolved = false promise.isResolved = false
promise.isPending = false promise.isPending = false
promise.isRejected = true promise.isRejected = true
throw (e) throw (e as Error)
} }
) )
return promise return promise

View File

@ -22,11 +22,11 @@ export const livechatViewerModePlugin = {
console.error('[livechatViewerModePlugin] getDefaultMUCNickname is not initialized.') console.error('[livechatViewerModePlugin] getDefaultMUCNickname is not initialized.')
} else { } else {
Object.assign(_converse.exports, { Object.assign(_converse.exports, {
getDefaultMUCNickname: function (this: any): any { getDefaultMUCNickname: function (this: any, ...args: any[]): any {
if (!_converse.api.settings.get('livechat_enable_viewer_mode')) { if (!_converse.api.settings.get('livechat_enable_viewer_mode')) {
return originalGetDefaultMUCNickname.apply(this, arguments) return originalGetDefaultMUCNickname.apply(this, args)
} }
return originalGetDefaultMUCNickname.apply(this, arguments) ?? return originalGetDefaultMUCNickname.apply(this, args) ??
getPreviousAnonymousNick() ?? getPreviousAnonymousNick() ??
randomNick('Anonymous') randomNick('Anonymous')
} }
@ -72,8 +72,8 @@ export const livechatViewerModePlugin = {
// Note: when previousNickname is set, model.get('nick') has not the nick yet... // Note: when previousNickname is set, model.get('nick') has not the nick yet...
// It will only come after receiving a presence stanza. // It will only come after receiving a presence stanza.
// So we use previousNickname before trying to read the model. // So we use previousNickname before trying to read the model.
const nick = getPreviousAnonymousNick() ?? (model?.get ? model.get('nick') : '') const nick = getPreviousAnonymousNick() ?? (model?.get ? model.get('nick') as string : '')
refreshViewerMode(nick && !/^Anonymous /.test(nick)) refreshViewerMode(!!nick && !/^Anonymous /.test(nick))
}) })
} }
} }

View File

@ -21,7 +21,13 @@ export const slowModePlugin = {
return return
} }
const slowModeDuration = parseInt(chatbox?.config?.get('slow_mode_duration')) const slowModeDurationRaw = chatbox?.config?.get('slow_mode_duration') ?? NaN
const slowModeDuration =
typeof slowModeDurationRaw === 'string'
? parseInt(slowModeDurationRaw)
: typeof slowModeDurationRaw === 'number'
? Math.trunc(slowModeDurationRaw)
: NaN
if (!(slowModeDuration > 0)) { // undefined, NaN, ... are not considered > 0. if (!(slowModeDuration > 0)) { // undefined, NaN, ... are not considered > 0.
return return
} }

View File

@ -5,7 +5,7 @@
function inIframe (): boolean { function inIframe (): boolean {
try { try {
return window.self !== window.top return window.self !== window.top
} catch (e) { } catch (_err) {
return true return true
} }
} }

View File

@ -62,7 +62,13 @@ const locKeys = [
'moderator_note_original_nick', 'moderator_note_original_nick',
'search_occupant_message', 'search_occupant_message',
'message_search', 'message_search',
'message_search_original_nick' 'message_search_original_nick',
'emoji_only_info',
'announcements_message_type',
'announcements_message_type_standard',
'announcements_message_type_announcement',
'announcements_message_type_highlight',
'announcements_message_type_warning'
] ]
module.exports = locKeys module.exports = locKeys

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