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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,10 +9,10 @@
livechat-token-list {
table {
@include tables.data-table;
width: 100%;
@include tables.data-table;
tr th:first-child,
tr th:last-child {
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 {
/* stylelint-disable-next-line custom-property-pattern */
background-color: var(--mainHoverColor);
background-color: var(--fg-400, var(--mainHoverColor));
border: 1px solid black;
/* stylelint-disable-next-line custom-property-pattern */
color: var(--mainBackgroundColor);

View File

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

View File

@ -13,7 +13,7 @@
text-align: center;
tr {
border: 1px var(--greyBackgroundColor) solid;
border: 1px var(--bg-secondary-400, var(--greyBackgroundColor)) solid;
}
td,
@ -34,6 +34,6 @@
}
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 "configuration/configuration";
@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);
}
@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.
It should go under the video.
*/

View File

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

View File

@ -10,12 +10,12 @@ set -euo pipefail
# This script download the Prosody AppImage from the https://github.com/JohnXLivingston/prosody-appimage project.
repo_base_url='https://github.com/JohnXLivingston/prosody-appimage/releases/download'
wanted_release='v0.12.3-1'
wanted_release='v0.12.4-3'
x86_64_filename='prosody-x86_64.AppImage'
x86_64_sha256sum='f4af9bfefa2f804ad7e8b03a68f04194abb801f070ae620b3d4bcedb144e8523'
x86_64_sha256sum='83a583ac7036387514bed17afab257dab4161ccdd0ab7453818c78b51f830357'
aarch64_filename='prosody-aarch64.AppImage'
aarch64_sha256sum='878c5be719e1e36a84d637fd2bd44e3059aa91ddb6906ad05f1dd0334078df09'
aarch64_sha256sum='7b7e6bf30d4498fc99a40022232c3065707ee4f4df24dc17947b007621634304'
download_dir="$(pwd)/vendor/prosody-appimage"
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_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL: 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_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL: 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_DESC: 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_DESC: string
declare const LOC_PROSODY_FIREWALL_CONTENT: string
declare const LOC_EMOJI_ONLY_MODE_TITLE: string
declare const LOC_EMOJI_ONLY_MODE_DESC_1: string
declare const LOC_EMOJI_ONLY_MODE_DESC_2: string
declare const LOC_EMOJI_ONLY_MODE_DESC_3: string
declare const LOC_EMOJI_ONLY_ENABLE_ALL_ROOMS: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_DESC: string

View File

@ -143,7 +143,7 @@ function register (clientOptions: RegisterClientOptions): void {
lastActivityEl.textContent = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
}
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.onclick = async () => {
await fetch(
@ -243,7 +243,10 @@ function register (clientOptions: RegisterClientOptions): void {
}
} catch (error: any) {
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 = {
name: 'livechat-active',
label: label,
label,
descriptionHTML: description,
type: 'input-checkbox',
default: true,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
export const AddSVG: string =
export const AddSVG =
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
aria-hidden="true"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
@ -14,7 +14,7 @@ export const AddSVG: string =
</svg>`
// 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"
aria-hidden="true"
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 {
private readonly _peertubeHelpers: RegisterClientHelpers
private _translatedValue: string = ''
private _localizationId: string = ''
private _translatedValue = ''
private _localizationId = ''
private _allowUnsafeHTML = false
@ -25,7 +25,7 @@ export class TranslationDirective extends AsyncDirective {
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._allowUnsafeHTML = allowHTML

View File

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

View File

@ -3,6 +3,9 @@
//
// SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import type { TagsInputElement } from './tags-input'
import type { DirectiveResult } from 'lit/directive'
import { ValidationErrorType } from '../models/validation'
@ -47,7 +50,7 @@ interface CellDataSchema {
minlength?: number
maxlength?: number
size?: number
options?: { [key: string]: string }
options?: Record<string, string>
datalist?: DynamicTableAcceptedTypes[]
separator?: string
inputType?: DynamicTableAcceptedInputTypes
@ -59,7 +62,7 @@ interface CellDataSchema {
interface DynamicTableRowData {
_id: number
_originalIndex: number
row: { [key: string]: DynamicTableAcceptedTypes }
row: Record<string, DynamicTableAcceptedTypes>
}
interface DynamicFormHeaderCellData {
@ -68,10 +71,8 @@ interface DynamicFormHeaderCellData {
headerClassList?: string[]
}
export interface DynamicFormHeader {
[key: string]: DynamicFormHeaderCellData
}
export interface DynamicFormSchema { [key: string]: CellDataSchema }
export type DynamicFormHeader = Record<string, DynamicFormHeaderCellData>
export type DynamicFormSchema = Record<string, CellDataSchema>
@customElement('livechat-dynamic-table-form')
export class DynamicTableFormElement extends LivechatElement {
@ -85,19 +86,19 @@ export class DynamicTableFormElement extends LivechatElement {
public maxLines?: number = undefined
@property()
public validation?: {[key: string]: ValidationErrorType[] }
public validation?: Record<string, ValidationErrorType[]>
@property({ attribute: false })
public validationPrefix: string = ''
public validationPrefix = ''
@property({ attribute: false })
public rows: Array<{ [key: string]: DynamicTableAcceptedTypes }> = []
public rows: Array<Record<string, DynamicTableAcceptedTypes>> = []
@state()
public _rowsById: DynamicTableRowData[] = []
@property({ attribute: false })
public formName: string = ''
public formName = ''
@state()
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()
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? ''])])
}
@ -457,8 +458,7 @@ export class DynamicTableFormElement extends LivechatElement {
inputTitle,
propertyName,
propertySchema,
(propertyValue)?.join(propertySchema.separator ?? ',') ??
propertyValue ?? propertySchema.default ?? '',
(propertyValue)?.join(propertySchema.separator ?? ',') ?? propertyValue ?? propertySchema.default ?? '',
originalIndex)}
${feedback}
`
@ -473,8 +473,7 @@ export class DynamicTableFormElement extends LivechatElement {
inputTitle,
propertyName,
propertySchema,
(propertyValue)?.join(propertySchema.separator ?? ',') ??
propertyValue ?? propertySchema.default ?? '',
(propertyValue)?.join(propertySchema.separator ?? ',') ?? propertyValue ?? propertySchema.default ?? '',
originalIndex)}
${feedback}
`
@ -498,8 +497,10 @@ export class DynamicTableFormElement extends LivechatElement {
}
if (!formElement) {
this.logger.warn(`value type '${(propertyValue.constructor.toString())}' is incompatible` +
`with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`)
this.logger.warn(
`value type '${(propertyValue.constructor.toString())}' is incompatible` +
`with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`
)
}
const classList = ['form-group']
@ -678,7 +679,7 @@ export class DynamicTableFormElement extends LivechatElement {
}
_getInputValidationClass = (propertyName: string,
originalIndex: number): { [key: string]: boolean } => {
originalIndex: number): Record<string, boolean> => {
const validationErrorTypes: ValidationErrorType[] | undefined =
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)
@property({ attribute: false })
public page: string = ''
public page = ''
@state()
public url: URL = new URL('https://lmddgtfy.net/')
@ -38,7 +38,7 @@ export class HelpButtonElement extends LivechatElement {
href="${this.url.href}"
target=_blank
title="${this.buttonTitle}"
class="orange-button peertube-button-link"
class="primary-button orange-button peertube-button-link"
>${unsafeHTML(helpButtonSVG())}</a>`
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -71,8 +71,7 @@ async function shareChatUrl (
addedNodes.forEach(node => {
if ((node as HTMLElement).localName === 'ngb-modal-window') {
logger.info('Detecting a new modal, checking if this is the good one...')
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)) {
return
}

View File

@ -4,6 +4,7 @@
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import type { Video } from '@peertube/peertube-types'
import type { LiveChatSettings } from '../lib/contexts/peertube'
import { AutoColors, isAutoColorsAvailable } from 'shared/lib/autocolors'
import { getBaseRoute } from '../../utils/uri'
import { logger } from '../../utils/logger'
@ -17,7 +18,7 @@ interface UriOptions {
}
function getIframeUri (
registerOptions: RegisterClientOptions, settings: any, video: Video, uriOptions: UriOptions = {}
registerOptions: RegisterClientOptions, settings: LiveChatSettings, video: Video, uriOptions: UriOptions = {}
): string | null {
if (!settings) {
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'
}),
body: JSON.stringify({
test: test
test
})
})
if (!response.ok) {
return {
test: test,
test,
messages: [response.statusText ?? 'Unknown error'],
ok: false
}
@ -169,7 +169,7 @@ function launchTests (): void {
const data = await response.json()
if ((typeof data) !== 'object') {
return {
test: test,
test,
messages: ['Incorrect reponse type: ' + (typeof data)],
ok: false
}
@ -190,6 +190,7 @@ function launchTests (): void {
waiting.innerHTML = '<i>Testing...</i>'
ul.append(waiting)
if ((typeof result.next) === 'function') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const r: Result = (result.next as Function)()
waiting.remove()
await machine(r)

View File

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

View File

@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// 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 { InitConverseJSParams, ChatPeertubeIncludeMode } from 'shared/lib/types'
@ -17,7 +18,7 @@ declare global {
}
}
let pollListenerInitiliazed: boolean = false
let pollListenerInitiliazed = false
/**
* load the ConverseJS CSS.
@ -152,10 +153,11 @@ async function displayConverseJS (
const authHeader = peertubeHelpers.getAuthHeader()
const response = await fetch(
getBaseRoute(clientOptions) + '/api/configuration/room/' +
const url = getBaseRoute(clientOptions) + '/api/configuration/room/' +
encodeURIComponent(roomKey) +
(forceType ? '?forcetype=1' : ''),
(forceType ? '?forcetype=1' : '')
const response = await fetch(
url,
{
method: 'GET',
headers: authHeader

View File

@ -4,7 +4,7 @@
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) {
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
/* eslint-disable @typescript-eslint/no-require-imports */
const fs = require('node:fs')
const path = require('node:path')
const YAML = require('yaml')

View File

@ -18,8 +18,10 @@ set -x
CONVERSE_VERSION="v11.0.0"
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.
# 2024-09-02: using Converse upstream (v11 WIP).
CONVERSE_COMMIT="9952046d580bc2930e29833f4c9987a3d4c95bc2"
# 2024-09-17: using Converse upstream (v11 WIP).
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):
# CONVERSE_VERSION="livechat"
@ -29,8 +31,8 @@ CONVERSE_COMMIT="9952046d580bc2930e29833f4c9987a3d4c95bc2"
# 2024-09-03: include badges short label and quick fix for sendMessage button
CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js"
CONVERSE_VERSION="livechat-11.0.1"
CONVERSE_COMMIT=""
CONVERSE_VERSION="livechat-12.0.0"
# CONVERSE_COMMIT=""
rootdir="$(pwd)"
src_dir="$rootdir/conversejs"
@ -40,6 +42,7 @@ if [ -n "$CONVERSE_COMMIT" ]; then
fi
converse_build_dir="$rootdir/build/conversejs"
converse_destination_dir="$rootdir/dist/client/conversejs"
converse_emoji_destination="$rootdir/dist/converse-emoji.json"
if [[ ! -d $src_dir ]]; then
echo "$0 must be called from the plugin livechat root dir."
@ -119,6 +122,9 @@ cd $rootdir
echo "Copying ConverseJS dist files..."
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."
exit 0

View File

@ -2,6 +2,8 @@
//
// 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 { inIframe } from './lib/utils'
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 { livechatEmojisPlugin } from './lib/plugins/livechat-emojis'
import { moderationDelayPlugin } from './lib/plugins/moderation-delay'
import { livechatAnnouncementsPlugin } from './lib/plugins/livechat-announcements'
declare global {
interface Window {
@ -35,11 +38,17 @@ declare global {
html: Function
sizzle: Function
dayjs: Function
__: Function
u: {
hasClass: Function
addClass: Function
removeClass: Function
}
}
}
initConversePlugins: typeof initConversePlugins
initConverse: typeof initConverse
reconnectConverse?: (room: string) => void
reconnectConverse?: (params: any) => void
externalAuthGetResult?: (data: ExternalAuthResult) => void
}
}
@ -74,6 +83,8 @@ function initConversePlugins (peertubeEmbedded: boolean): void {
converse.plugins.add('livechatViewerModePlugin', livechatViewerModePlugin)
converse.plugins.add('converse-moderation-delay', moderationDelayPlugin)
converse.plugins.add('livechatAnnouncementsPlugin', livechatAnnouncementsPlugin)
}
window.initConversePlugins = initConversePlugins
@ -86,7 +97,7 @@ window.initConversePlugins = initConversePlugins
async function initConverse (
initConverseParams: InitConverseJSParams,
chatIncludeMode: ChatIncludeMode = 'chat-only',
peertubeAuthHeader?: { [header: string]: string } | null
peertubeAuthHeader?: Record<string, string> | null
): Promise<void> {
// First, fixing relative websocket urls.
if (initConverseParams.localWebsocketServiceUrl?.startsWith('/')) {
@ -121,9 +132,9 @@ async function initConverse (
params.view_mode = chatIncludeMode === 'chat-only' ? 'fullscreen' : 'embedded'
params.allow_url_history_change = chatIncludeMode === 'chat-only'
let isAuthenticated: boolean = false
let isAuthenticatedWithExternalAccount: boolean = false
let isRemoteWithNicknameSet: boolean = false
let isAuthenticated = false
let isAuthenticatedWithExternalAccount = false
let isRemoteWithNicknameSet = false
// OIDC (OpenID Connect):
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
// (see headless/plugins/muc, getDiscoInfoFeatures, which loops on this const)
ROOM_FEATURES.push('x_peertubelivechat_mute_anonymous')
ROOM_FEATURES.push('x_peertubelivechat_emoji_only_mode')
_converse.exports.CustomElement = CustomElement

View File

@ -2,6 +2,9 @@
//
// 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 { tplMUCApp } from '../../../shared/components/muc-app/templates/muc-app.js'
import { html } from 'lit'

View File

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

View File

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

View File

@ -2,6 +2,9 @@
//
// 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 { tplMUCApp } from '../../../shared/components/muc-app/templates/muc-app.js'
import { html } from 'lit'

View File

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

View File

@ -2,6 +2,9 @@
//
// 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 { html } from 'lit'
import { __ } from 'i18n'

View File

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

View File

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

View File

@ -2,6 +2,9 @@
//
// 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 { html } from 'lit'
import { __ } from 'i18n'

View File

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

View File

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

View File

@ -2,6 +2,9 @@
//
// 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 { tplMUCApp } from '../../../shared/components/muc-app/templates/muc-app.js'
import { html } from 'lit'

View File

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

View File

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

View File

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

View File

@ -2,6 +2,9 @@
//
// 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 { api } from '@converse/headless'
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 {
border-top: none !important; // removing border, to avoid confusing the toolbar with an input field.
color: var(--peertube-main-foreground);
@ -43,6 +51,10 @@
// Fixing emoji colors for some emoji like «motorcycle»
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 {
.insert-emoji {
a {
@ -52,13 +64,17 @@
}
.emoji-picker__header {
color: var(--peertube-main-background);
background-color: var(--peertube-main-foreground);
background-color: var(--peertube-main-background);
color: var(--peertube-main-foreground);
.emoji-search {
color: currentcolor;
}
ul {
.emoji-category {
color: var(--peertube-main-background);
background-color: var(--peertube-main-foreground);
background-color: var(--peertube-main-background);
color: var(--peertube-main-foreground);
a {
color: currentcolor;
@ -190,7 +206,7 @@
}
// 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 {
min-width: 50%;
}

View File

@ -7,6 +7,7 @@
@import "./variables";
@import "shared/styles/index";
@import "./peertubetheme";
@import "./announcements";
body.livechat-iframe {
#conversejs .chat-head {
@ -21,7 +22,7 @@ body.livechat-iframe {
}
#conversejs .livechat-mini-muc-bar-buttons {
a.orange-button {
a.primary-button {
// force these colors...
color: var(--peertube-button-foreground);
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.
margin-left: auto !important;
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
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { _converse, api } from '@converse/headless'
import { __ } from 'i18n'
import { html } from 'lit'
@ -28,8 +31,9 @@ function externalLoginClickHandler (ev, el, externalAuthOIDCUrl) {
console.log('Received an external authentication result...', data)
if (!data.ok) {
el.external_auth_oidc_alert_message =
// eslint-disable-next-line no-undef
el.external_auth_oidc_alert_message = __(LOC_login_external_auth_alert_message) +
__(LOC_login_external_auth_alert_message) +
(data.message ? ` (${data.message})` : '')
return
}

View File

@ -2,6 +2,9 @@
//
// SPDX-License-Identifier: AGPL-3.0-only
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { __ } from 'i18n'
import { _converse, api } from '@converse/headless'
import { html } from 'lit'
@ -83,6 +86,20 @@ const tplSlowMode = (o) => {
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) => {
if (!api.settings.get('livechat_enable_viewer_mode')) {
return html``
@ -145,6 +162,7 @@ export default (o) => {
return html`
${tplViewerMode(o)}
${tplSlowMode(o)}
${tplEmojiOnly(o)}
${
mutedAnonymousMessage
? 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
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
import { api } from '@converse/headless'
import tplMUCChatarea from '../../src/plugins/muc-views/templates/muc-chatarea.js'
import { html } from 'lit'

View File

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

View File

@ -4,6 +4,9 @@
// SPDX-License-Identifier: MPL-2.0
// 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.
import '../../src/plugins/muc-views/templates/muc.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'
interface AuthHeader { [key: string]: string }
type AuthHeader = Record<string, string>
async function getLocalAuthentInfos (
authenticationUrl: string,
@ -66,7 +66,7 @@ async function getLocalAuthentInfos (
{
'content-type': 'application/json;charset=UTF-8'
}
)
) as HeadersInit
)
})
@ -104,7 +104,7 @@ async function getLocalAuthentInfos (
function getLivechatTokenAuthInfos (): ProsodyAuthentInfos | undefined {
try {
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.
const u = new URL('http://localhost' + hash.substring(1))
const jid = u.searchParams.get('j')

View File

@ -86,7 +86,8 @@ function defaultConverseParams (
'livechatDisconnectOnUnloadPlugin',
'converse-slow-mode',
'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?)
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
})
_converse.api.listen.on('loadEmojis', async (_context: Object, json: any) => {
const url = _converse.api.settings.get('livechat_custom_emojis_url')
_converse.api.listen.on('loadEmojis', async (_context: object, json: Record<string, Record<string, unknown>>) => {
const url = _converse.api.settings.get('livechat_custom_emojis_url') as string | undefined
if (!url) {
return json
}
let customs
let customs: CustomEmojiDefinition[] | undefined
try {
customs = await loadCustomEmojis(url)
} catch (err) {
console.error(err)
}
if (customs === undefined || !customs?.length) {
if (!customs?.length) {
return json
}
@ -51,7 +51,7 @@ export const livechatEmojisPlugin = {
// We must also remove any existing emojis in category other than custom
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)) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete v[def.sn]

View File

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

View File

@ -2,7 +2,8 @@
//
// 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 {
/* 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. */

View File

@ -2,7 +2,8 @@
//
// 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 {
getActionInfoMessage: function getActionInfoMessage (this: any, code: string, nick: string, actor: any): any {
if (code === '303') {
@ -27,8 +28,9 @@ export function chatRoomOverrides (): {[key: string]: Function} {
initOccupants: function initOccupants (this: any) {
const r = this.__super__.initOccupants()
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
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.
const nick1: string = occupant1.getDisplayName()
const nick2: string = occupant2.getDisplayName()

View File

@ -19,7 +19,7 @@ export function customizeMessageAction (plugin: any): void {
try {
txt += this.model.getDisplayName() as string
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 += '\n'
} 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
// FIXME: @stylistic/indent is buggy with strings literrals.
/* eslint-disable @stylistic/indent */
/**
* Do some customization on the toolbar:
* * change the appearance of the toggle occupants button

View File

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

View File

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

View File

@ -21,7 +21,13 @@ export const slowModePlugin = {
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.
return
}

View File

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

View File

@ -62,7 +62,13 @@ const locKeys = [
'moderator_note_original_nick',
'search_occupant_message',
'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

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