Compare commits
No commits in common. "main" and "nctv-avatar" have entirely different histories.
main
...
nctv-avata
14
.eslintrc.json
Normal file
14
.eslintrc.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"env": {},
|
||||||
|
"extends": [],
|
||||||
|
"globals": {},
|
||||||
|
"plugins": [],
|
||||||
|
"ignorePatterns": [
|
||||||
|
"node_modules/", "dist/", "webpack.config.js",
|
||||||
|
"build/",
|
||||||
|
"vendor/",
|
||||||
|
"support/documentation",
|
||||||
|
"build-*js"],
|
||||||
|
"rules": {}
|
||||||
|
}
|
40
.github/dependabot.yml
vendored
40
.github/dependabot.yml
vendored
@ -1,40 +0,0 @@
|
|||||||
# 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
34
.github/workflows/gh-build.yml
vendored
@ -1,34 +0,0 @@
|
|||||||
# 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
|
|
2
.github/workflows/gh-pages.yml
vendored
2
.github/workflows/gh-pages.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
|||||||
- name: Setup Hugo
|
- name: Setup Hugo
|
||||||
uses: peaceiris/actions-hugo@v2
|
uses: peaceiris/actions-hugo@v2
|
||||||
with:
|
with:
|
||||||
hugo-version: '0.132.2'
|
hugo-version: '0.80.0'
|
||||||
extended: true
|
extended: true
|
||||||
|
|
||||||
- name: Generate documentation translations
|
- name: Generate documentation translations
|
||||||
|
@ -19,10 +19,10 @@ builddoctranslations:
|
|||||||
|
|
||||||
pages:
|
pages:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: registry.gitlab.com/pages/hugo/hugo_extended:0.132.2
|
image: registry.gitlab.com/pages/hugo/hugo_extended:latest
|
||||||
variables:
|
variables:
|
||||||
GIT_SUBMODULE_STRATEGY: recursive
|
GIT_SUBMODULE_STRATEGY: recursive
|
||||||
GIT_SUBMODULE_PATHS: support/documentation/themes/hugo-theme-relearn
|
GIT_SUBMODULE_PATHS: support/documentation/themes/hugo-theme-learn
|
||||||
script:
|
script:
|
||||||
# gitlab need the generated documentation to be in the /public dir.
|
# gitlab need the generated documentation to be in the /public dir.
|
||||||
- hugo -s support/documentation/ --minify -d ../../public/ --baseURL='https://livingston.frama.io/peertube-plugin-livechat/'
|
- hugo -s support/documentation/ --minify -d ../../public/ --baseURL='https://livingston.frama.io/peertube-plugin-livechat/'
|
||||||
|
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -2,6 +2,6 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
[submodule "support/documentation/themes/hugo-theme-relearn"]
|
[submodule "documentation/themes/hugo-theme-learn"]
|
||||||
path = support/documentation/themes/hugo-theme-relearn
|
path = support/documentation/themes/hugo-theme-learn
|
||||||
url = https://github.com/McShelby/hugo-theme-relearn.git
|
url = https://github.com/matcornic/hugo-theme-learn.git
|
||||||
|
@ -32,7 +32,3 @@ License: AGPL-3.0-only
|
|||||||
Files: .github/PULL_REQUEST_TEMPLATE.md
|
Files: .github/PULL_REQUEST_TEMPLATE.md
|
||||||
Copyright: 2024 John Livingston <https://www.john-livingston.fr/>
|
Copyright: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
License: AGPL-3.0-only
|
License: AGPL-3.0-only
|
||||||
|
|
||||||
Files: prosody-modules/mod_firewall/*
|
|
||||||
Copyright: Prosody Community Modules <https://modules.prosody.im/mod_firewall>
|
|
||||||
License: MIT
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
'use strict'
|
'use strict';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [
|
extends: [
|
||||||
@ -14,7 +14,7 @@ module.exports = {
|
|||||||
// extending the kebab-case to accept ConverseJS class names.
|
// extending the kebab-case to accept ConverseJS class names.
|
||||||
'^([a-z][a-z0-9]*)(-[a-z0-9]+)*((__|--)[a-z]+(-[a-z0-9]+)*)?$',
|
'^([a-z][a-z0-9]*)(-[a-z0-9]+)*((__|--)[a-z]+(-[a-z0-9]+)*)?$',
|
||||||
{
|
{
|
||||||
message: 'Expected class selector to be kebab-case, or ConverseJS-style.'
|
message: 'Expected class selector to be kebab-case, or ConverseJS-style.',
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
89
CHANGELOG.md
89
CHANGELOG.md
@ -1,94 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 12.0.3
|
|
||||||
|
|
||||||
### Minor changes and fixes
|
|
||||||
|
|
||||||
* Translations updates.
|
|
||||||
* Slovak translation integration.
|
|
||||||
* Differenciate pt-PT and pt-BR translations.
|
|
||||||
* Fix styling for "configure mod_firewall" button + Peertube v7.0.0 compatibility.
|
|
||||||
* Fix #648: workaround for a regression in Firefox that breaks the scrollbar (Thanks [Raph](https://github.com/raphgilles) for the workaround!).
|
|
||||||
|
|
||||||
## 12.0.2
|
|
||||||
|
|
||||||
### Minor changes and fixes
|
|
||||||
|
|
||||||
* Fix task list label styling.
|
|
||||||
* Translations updates.
|
|
||||||
|
|
||||||
## 12.0.1
|
|
||||||
|
|
||||||
### Minor changes and fixes
|
|
||||||
|
|
||||||
* Fix custom emojis vs upper/lower case.
|
|
||||||
|
|
||||||
## 12.0.0
|
|
||||||
|
|
||||||
### Importante Notes
|
|
||||||
|
|
||||||
This version requires Peertube 5.2.0 or superior.
|
|
||||||
It also requires NodeJS 16 or superior (same as Peertube 5.2.0.).
|
|
||||||
|
|
||||||
If you use the "system Prosody", you should update to Prosody 0.12.4, and Lua 5.4.
|
|
||||||
|
|
||||||
### New features
|
|
||||||
|
|
||||||
* #131: Emoji only mode.
|
|
||||||
* #516: new option for the moderation bot: forbid duplicate messages.
|
|
||||||
* #517: new option for the moderation bot: forbid messages with too many special characters.
|
|
||||||
* #518: moderators can send announcements and highlighted messages.
|
|
||||||
* #610: compatibility with PeerTube v7
|
|
||||||
|
|
||||||
### Minor changes and fixes
|
|
||||||
|
|
||||||
* Updating ConverseJS (v11 WIP) with latest fixes.
|
|
||||||
* Updating Prosody AppImage to Prosody 0.12.4 + Lua 5.4.
|
|
||||||
* Various translation updates.
|
|
||||||
* Using Typescript 5.5.4, and Eslint 8.57.0 (with new ruleset).
|
|
||||||
* Fix race condition in bot/ctl.
|
|
||||||
* Various type improvements.
|
|
||||||
* Update dependencies.
|
|
||||||
* Fix emoji picker colors and size.
|
|
||||||
* Fix: moderation delay max value was not correctly handled.
|
|
||||||
|
|
||||||
## 11.0.1
|
|
||||||
|
|
||||||
### Minor changes and fixes
|
|
||||||
|
|
||||||
* Fix "send message" button that was sending the message twice.
|
|
||||||
|
|
||||||
## 11.0.0
|
|
||||||
|
|
||||||
### Importante Notes
|
|
||||||
|
|
||||||
With the new [mod_firewall](https://livingston.frama.io/peertube-plugin-livechat/documentation/admin/mod_firewall/) feature, Peertube admins can write firewall rules for the Prosody server. These rules could be used to run arbitrary code on the server. If you are a hosting provider, and you don't want to allow Peertube admins to write such rules, you can disable the online editing by creating a `disable_mod_firewall_editing` file in the plugin directory. Check the documentation for more information. This is opt-out, as Peertube admins can already run arbitrary code just by installing any plugin.
|
|
||||||
|
|
||||||
The concord theme was removed from ConverseJS. If you had it set in the plugin settings, it will fallback to the Peertube theme.
|
|
||||||
|
|
||||||
### New features
|
|
||||||
|
|
||||||
* Updating ConverseJS, to use upstream (v11 WIP). This comes with many improvements and new features.
|
|
||||||
* #146: copy message button for moderators.
|
|
||||||
* #137: option to hide moderator name who made actions (kick, ban, message moderation, ...).
|
|
||||||
* #144: [moderator notes](https://livingston.frama.io/peertube-plugin-livechat/documentation/user/streamers/moderation_notes/).
|
|
||||||
* #145: action for moderators to find all messages from a given participant.
|
|
||||||
* #97: option to use and configure [mod_firewall](https://livingston.frama.io/peertube-plugin-livechat/documentation/admin/mod_firewall/) at the server level.
|
|
||||||
|
|
||||||
### Minor changes and fixes
|
|
||||||
|
|
||||||
* #118: improved accessibility.
|
|
||||||
* Avatar set for anonymous users: new 'none' choice (that will fallback to Converse new colorized avatars).
|
|
||||||
* New translation: Albanian.
|
|
||||||
* Translation updates: Crotian, Japanese, traditional Chinese, Arabic, Galician.
|
|
||||||
* Updated mod_muc_moderation to upstream.
|
|
||||||
* Fix new task ordering.
|
|
||||||
* Fix: clicking on the current user nickname in message history was failing to open the profile modal.
|
|
||||||
* Fix: increase chat height on small screens, try to better detect the device viewport size and orientation.
|
|
||||||
* Converse theme: removed concord, added cyberpunk.
|
|
||||||
* Fixed Converse theme settings localization.
|
|
||||||
* Fix: improved minimum chat width.
|
|
||||||
|
|
||||||
## 10.3.3
|
## 10.3.3
|
||||||
|
|
||||||
### Minor changes and fixes
|
### Minor changes and fixes
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 65 KiB |
@ -1,98 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* stylelint-disable custom-property-pattern */
|
|
||||||
|
|
||||||
@use "sass:color";
|
|
||||||
@use "../../variables";
|
|
||||||
|
|
||||||
.peertube-plugin-livechat-admin-firewall {
|
|
||||||
h1 {
|
|
||||||
padding-top: 40px;
|
|
||||||
|
|
||||||
/* See Peertube sub-menu-h1 mixin */
|
|
||||||
font-size: 1.3rem;
|
|
||||||
border-bottom: 2px solid var(--bg-secondary-400, var(--greyBackgroundColor));
|
|
||||||
padding-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea[name^="_content_"] {
|
|
||||||
min-height: 10rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="submit"],
|
|
||||||
input[type="reset"],
|
|
||||||
button[type="submit"],
|
|
||||||
button[type="reset"] {
|
|
||||||
// Peertube rounded-line-height-1-5 mixins
|
|
||||||
line-height: variables.$button-calc-line-height;
|
|
||||||
|
|
||||||
// Peertube peertube-button mixin
|
|
||||||
padding: 4px 13px;
|
|
||||||
border: 0;
|
|
||||||
font-weight: variables.$font-semibold;
|
|
||||||
border-radius: 3px !important;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: variables.$button-font-size;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="submit"],
|
|
||||||
button[type="submit"] {
|
|
||||||
&,
|
|
||||||
&:active,
|
|
||||||
&.active,
|
|
||||||
&:focus {
|
|
||||||
color: var(--on-primary, #fff);
|
|
||||||
background-color: var(--primary, var(--mainColor));
|
|
||||||
border: 1px solid var(--primary, var(--mainColor));
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--on-primary, #fff);
|
|
||||||
background-color: var(--primary-400, var(--mainHoverColor));
|
|
||||||
}
|
|
||||||
|
|
||||||
&[disabled] {
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="reset"],
|
|
||||||
button[type="reset"] {
|
|
||||||
color: var(--fg, var(--mainForegroundColor));
|
|
||||||
background-color: transparent;
|
|
||||||
border: 1px solid var(--bg-secondary-500, var(--inputBorderColor)) !important;
|
|
||||||
|
|
||||||
&:active,
|
|
||||||
&.active,
|
|
||||||
&:focus,
|
|
||||||
&:focus-visible {
|
|
||||||
color: var(--fg, var(--mainForegroundColor));
|
|
||||||
background-color: var(--bg-secondary-500, var(--inputBorderColor));
|
|
||||||
border-color: var(--bg-secondary-500, var(--inputBorderColor));
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--fg, var(--mainForegroundColor));
|
|
||||||
background-color: var(--bg-secondary-450, var(--inputBorderColor));
|
|
||||||
}
|
|
||||||
|
|
||||||
&[disabled] {
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.peertube-livechat-admin-firewall-col-name {
|
|
||||||
width: 25%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.peertube-livechat-admin-firewall-col-content {
|
|
||||||
width: 65%;
|
|
||||||
}
|
|
||||||
}
|
|
@ -21,7 +21,7 @@ $small-view: 800px;
|
|||||||
|
|
||||||
/* See Peertube sub-menu-h1 mixin */
|
/* See Peertube sub-menu-h1 mixin */
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
border-bottom: 2px solid var(--bg-secondary-400, var(--greyBackgroundColor));
|
border-bottom: 2px solid var(--greyBackgroundColor);
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ $small-view: 800px;
|
|||||||
&.peertube-plugin-livechat-configuration-channel {
|
&.peertube-plugin-livechat-configuration-channel {
|
||||||
.peertube-plugin-livechat-configuration-channel-info {
|
.peertube-plugin-livechat-configuration-channel-info {
|
||||||
/* stylelint-disable-next-line value-keyword-case */
|
/* stylelint-disable-next-line value-keyword-case */
|
||||||
color: var(--fg, var(--mainForegroundColor));
|
color: var(--mainForegroundColor);
|
||||||
|
|
||||||
span:first-child {
|
span:first-child {
|
||||||
/* See Peertube .video-channel-display-name */
|
/* See Peertube .video-channel-display-name */
|
||||||
@ -48,7 +48,7 @@ $small-view: 800px;
|
|||||||
h2 {
|
h2 {
|
||||||
// See Peertube settings-big-title mixin
|
// See Peertube settings-big-title mixin
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--primary, var(--mainColor));
|
color: var(--mainColor);
|
||||||
font-weight: variables.$font-bold;
|
font-weight: variables.$font-bold;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@ -82,35 +82,35 @@ $small-view: 800px;
|
|||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--primary, var(--mainColor));
|
background-color: var(--mainColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--fg-400, var(--mainHoverColor));
|
background-color: var(--mainHoverColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[disabled],
|
&[disabled],
|
||||||
&.disabled {
|
&.disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--input-border-color, var(--inputBorderColor));
|
background-color: var(--inputBorderColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="reset"],
|
input[type="reset"],
|
||||||
button[type="reset"] {
|
button[type="reset"] {
|
||||||
// Peertube grey-button mixin
|
// Peertube grey-button mixin
|
||||||
background-color: var(--bg-secondary-400, var(--greyBackgroundColor));
|
background-color: var(--greyBackgroundColor);
|
||||||
color: var(--fg-400, var(--greyForegroundColor));
|
color: var(--greyForegroundColor);
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&[disabled],
|
&[disabled],
|
||||||
&.disabled {
|
&.disabled {
|
||||||
color: var(--fg-400, var(--greyForegroundColor));
|
color: var(--greyForegroundColor);
|
||||||
background-color: var(--bg-secondary-300, var(--greySecondaryBackgroundColor));
|
background-color: var(--greySecondaryBackgroundColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[disabled],
|
&[disabled],
|
||||||
@ -174,12 +174,6 @@ $small-view: 800px;
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
/* See Peertube .video-channel-names */
|
/* See Peertube .video-channel-names */
|
||||||
width: fit-content;
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
/* stylelint-disable-next-line value-keyword-case */
|
|
||||||
color: var(--fg, var(--mainForegroundColor));
|
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:active {
|
&:active {
|
||||||
@ -190,6 +184,12 @@ $small-view: 800px;
|
|||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
/* stylelint-disable-next-line value-keyword-case */
|
||||||
|
color: var(--mainForegroundColor);
|
||||||
|
|
||||||
div:first-child {
|
div:first-child {
|
||||||
/* See Peertube .video-channel-display-name */
|
/* See Peertube .video-channel-display-name */
|
||||||
font-weight: variables.$font-semibold;
|
font-weight: variables.$font-semibold;
|
||||||
|
@ -56,7 +56,7 @@ livechat-share-chat {
|
|||||||
&.livechat-shareurl-suboptions-disabled {
|
&.livechat-shareurl-suboptions-disabled {
|
||||||
label {
|
label {
|
||||||
/* stylelint-disable-next-line custom-property-pattern */
|
/* stylelint-disable-next-line custom-property-pattern */
|
||||||
color: var(--fg-400, var(--greyForegroundColor));
|
color: var(--greyForegroundColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,9 @@ livechat-spinner,
|
|||||||
height: 48px;
|
height: 48px;
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
/* stylelint-disable-next-line custom-property-pattern */
|
/* stylelint-disable-next-line custom-property-pattern */
|
||||||
border: 5px solid var(--bg-secondary-400, var(--greyBackgroundColor)) !important; // !important is required for it to work in ConverseJS
|
border: 5px solid var(--greyBackgroundColor);
|
||||||
/* stylelint-disable-next-line custom-property-pattern */
|
/* stylelint-disable-next-line custom-property-pattern */
|
||||||
border-bottom-color: var(--primary, var(--mainColor)) !important; // !important is required for it to work in ConverseJS
|
border-bottom-color: var(--mainColor);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -51,14 +51,14 @@ livechat-tags-input {
|
|||||||
transition-duration: 0.3s;
|
transition-duration: 0.3s;
|
||||||
|
|
||||||
@supports (scrollbar-width: auto) {
|
@supports (scrollbar-width: auto) {
|
||||||
scrollbar-color: var(--fg-400, var(--greyForegroundColor)) transparent;
|
scrollbar-color: var(--greyForegroundColor) transparent;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.livechat-tags-container,
|
.livechat-tags-container,
|
||||||
.livechat-tags-searched {
|
.livechat-tags-searched {
|
||||||
border-bottom: 1px dashed var(--fg-400, var(--greyForegroundColor));
|
border-bottom: 1px dashed var(--greyForegroundColor);
|
||||||
|
|
||||||
&.livechat-empty {
|
&.livechat-empty {
|
||||||
height: 0;
|
height: 0;
|
||||||
@ -104,7 +104,7 @@ livechat-tags-input {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
margin-left: var(--tag-padding-horizontal);
|
margin-left: var(--tag-padding-horizontal);
|
||||||
color: var(--primary, var(--mainColor));
|
color: var(--mainColor);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -118,19 +118,19 @@ livechat-tags-input {
|
|||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--primary, var(--mainColor));
|
background-color: var(--mainColor);
|
||||||
|
|
||||||
.livechat-tag-close {
|
.livechat-tag-close {
|
||||||
color: var(--primary, var(--mainColor));
|
color: var(--mainColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--fg-400, var(--mainHoverColor));
|
background-color: var(--mainHoverColor);
|
||||||
|
|
||||||
.livechat-tag-close {
|
.livechat-tag-close {
|
||||||
color: var(--fg-400, var(--mainHoverColor));
|
color: var(--mainHoverColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,10 +138,10 @@ livechat-tags-input {
|
|||||||
&.disabled {
|
&.disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--input-border-color, var(--inputBorderColor));
|
background-color: var(--inputBorderColor);
|
||||||
|
|
||||||
.livechat-tag-close {
|
.livechat-tag-close {
|
||||||
color: var(--input-border-color, var(--inputBorderColor));
|
color: var(--inputBorderColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,10 +9,10 @@
|
|||||||
|
|
||||||
livechat-token-list {
|
livechat-token-list {
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
@include tables.data-table;
|
@include tables.data-table;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
tr th:first-child,
|
tr th:first-child,
|
||||||
tr th:last-child {
|
tr th:last-child {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
|
@ -19,7 +19,7 @@ table.peertube-plugin-livechat-prosody-list-rooms tr:nth-child(even) {
|
|||||||
|
|
||||||
table.peertube-plugin-livechat-prosody-list-rooms th {
|
table.peertube-plugin-livechat-prosody-list-rooms th {
|
||||||
/* stylelint-disable-next-line custom-property-pattern */
|
/* stylelint-disable-next-line custom-property-pattern */
|
||||||
background-color: var(--fg-400, var(--mainHoverColor));
|
background-color: var(--mainHoverColor);
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
/* stylelint-disable-next-line custom-property-pattern */
|
/* stylelint-disable-next-line custom-property-pattern */
|
||||||
color: var(--mainBackgroundColor);
|
color: var(--mainBackgroundColor);
|
||||||
|
@ -54,7 +54,7 @@ $bs-green: #39cc0b;
|
|||||||
&.disabled {
|
&.disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--input-border-color, var(--inputBorderColor));
|
background-color: var(--inputBorderColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ $bs-green: #39cc0b;
|
|||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--primary, var(--mainColor));
|
background-color: var(--mainColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
@ -77,13 +77,13 @@ $bs-green: #39cc0b;
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--fg-400, var(--mainHoverColor));
|
background-color: var(--mainHoverColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[disabled],
|
&[disabled],
|
||||||
&.disabled {
|
&.disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: var(--input-border-color, var(--inputBorderColor));
|
background-color: var(--inputBorderColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
border: 1px var(--bg-secondary-400, var(--greyBackgroundColor)) solid;
|
border: 1px var(--greyBackgroundColor) solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
td,
|
td,
|
||||||
@ -34,6 +34,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
tbody tr:nth-child(odd) {
|
tbody tr:nth-child(odd) {
|
||||||
background-color: var(--bg-secondary-300, var(--greySecondaryBackgroundColor));
|
background-color: var(--greySecondaryBackgroundColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,5 +9,4 @@
|
|||||||
@use "elements/index";
|
@use "elements/index";
|
||||||
@use "video";
|
@use "video";
|
||||||
@use "configuration/configuration";
|
@use "configuration/configuration";
|
||||||
@use "admin/firewall/firewall";
|
@use "list-rooms/list-rooms.scss";
|
||||||
@use "list-rooms/list-rooms";
|
|
||||||
|
@ -18,31 +18,17 @@
|
|||||||
/* Note: livechat-viewer-mode-content (the form where anonymous users can
|
/* Note: livechat-viewer-mode-content (the form where anonymous users can
|
||||||
choose nickname or log in with external account), can be something like
|
choose nickname or log in with external account), can be something like
|
||||||
~180px height (at time of writing).
|
~180px height (at time of writing).
|
||||||
We must ensure that the px height limit for converse-muc and converse-root is
|
We must ensure that the 200px limit for converse-muc and converse-root is
|
||||||
always higher than livechat-viewer-mode-content max size.
|
always higher than livechat-viewer-mode-content max size.
|
||||||
Note: We also must ensure that when the user has choosen its nickname, and there is an
|
|
||||||
ongoing poll, the user can see the chat when the poll is folded.
|
|
||||||
*/
|
*/
|
||||||
#peertube-plugin-livechat-container converse-root {
|
#peertube-plugin-livechat-container converse-root {
|
||||||
display: block;
|
display: block;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
min-height: max(30vh, 300px); // Always at least 200px, and ideally at least 30% of viewport.
|
min-height: max(30vh, 200px); // Always at least 200px, and ideally at least 30% of viewport.
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: min(400px, 25vw);
|
|
||||||
|
|
||||||
converse-muc {
|
converse-muc {
|
||||||
min-height: max(30vh, 300px);
|
min-height: max(59vh, 400px);
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (orientation: portrait) and (width <= 767px) {
|
|
||||||
/* On small screen, and when portrait mode, we are giving the chat more vertical space.
|
|
||||||
It should go under the video.
|
|
||||||
*/
|
|
||||||
min-height: max(58vh, 300px);
|
|
||||||
|
|
||||||
converse-muc {
|
|
||||||
min-height: max(58vh, 300px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ const clientFiles = [
|
|||||||
'admin-plugin-client-plugin'
|
'admin-plugin-client-plugin'
|
||||||
]
|
]
|
||||||
|
|
||||||
function loadLocs(globalFile) {
|
function loadLocs() {
|
||||||
// Loading english strings, so we can inject them as constants.
|
// Loading english strings, so we can inject them as constants.
|
||||||
const refFile = path.resolve(__dirname, 'dist', 'languages', 'en.reference.json')
|
const refFile = path.resolve(__dirname, 'dist', 'languages', 'en.reference.json')
|
||||||
if (!fs.existsSync(refFile)) {
|
if (!fs.existsSync(refFile)) {
|
||||||
@ -25,6 +25,7 @@ function loadLocs(globalFile) {
|
|||||||
|
|
||||||
// Reading client/@types/global.d.ts, to have a list of needed localized strings.
|
// Reading client/@types/global.d.ts, to have a list of needed localized strings.
|
||||||
const r = {}
|
const r = {}
|
||||||
|
const globalFile = path.resolve(__dirname, 'client', '@types', 'global.d.ts')
|
||||||
const globalFileContent = '' + fs.readFileSync(globalFile)
|
const globalFileContent = '' + fs.readFileSync(globalFile)
|
||||||
const matches = globalFileContent.matchAll(/^declare const LOC_(\w+)\b/gm)
|
const matches = globalFileContent.matchAll(/^declare const LOC_(\w+)\b/gm)
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
@ -40,7 +41,7 @@ function loadLocs(globalFile) {
|
|||||||
const define = Object.assign({
|
const define = Object.assign({
|
||||||
PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name),
|
PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name),
|
||||||
PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, ''))
|
PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, ''))
|
||||||
}, loadLocs(path.resolve(__dirname, 'client', '@types', 'global.d.ts')))
|
}, loadLocs())
|
||||||
|
|
||||||
const configs = clientFiles.map(f => ({
|
const configs = clientFiles.map(f => ({
|
||||||
entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ],
|
entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ],
|
||||||
@ -58,14 +59,8 @@ const configs = clientFiles.map(f => ({
|
|||||||
outfile: path.resolve(__dirname, 'dist/client', f + '.js'),
|
outfile: path.resolve(__dirname, 'dist/client', f + '.js'),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const defineBuiltin = Object.assign(
|
|
||||||
{},
|
|
||||||
loadLocs(path.resolve(__dirname, 'conversejs', 'lib', '@types', 'global.d.ts'))
|
|
||||||
)
|
|
||||||
|
|
||||||
configs.push({
|
configs.push({
|
||||||
entryPoints: ["./conversejs/builtin.ts"],
|
entryPoints: ["./conversejs/builtin.ts"],
|
||||||
define: defineBuiltin,
|
|
||||||
bundle: true,
|
bundle: true,
|
||||||
minify: true,
|
minify: true,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
|
@ -10,12 +10,12 @@ set -euo pipefail
|
|||||||
# This script download the Prosody AppImage from the https://github.com/JohnXLivingston/prosody-appimage project.
|
# This script download the Prosody AppImage from the https://github.com/JohnXLivingston/prosody-appimage project.
|
||||||
|
|
||||||
repo_base_url='https://github.com/JohnXLivingston/prosody-appimage/releases/download'
|
repo_base_url='https://github.com/JohnXLivingston/prosody-appimage/releases/download'
|
||||||
wanted_release='v0.12.4-3'
|
wanted_release='v0.12.3-1'
|
||||||
|
|
||||||
x86_64_filename='prosody-x86_64.AppImage'
|
x86_64_filename='prosody-x86_64.AppImage'
|
||||||
x86_64_sha256sum='83a583ac7036387514bed17afab257dab4161ccdd0ab7453818c78b51f830357'
|
x86_64_sha256sum='f4af9bfefa2f804ad7e8b03a68f04194abb801f070ae620b3d4bcedb144e8523'
|
||||||
aarch64_filename='prosody-aarch64.AppImage'
|
aarch64_filename='prosody-aarch64.AppImage'
|
||||||
aarch64_sha256sum='7b7e6bf30d4498fc99a40022232c3065707ee4f4df24dc17947b007621634304'
|
aarch64_sha256sum='878c5be719e1e36a84d637fd2bd44e3059aa91ddb6906ad05f1dd0334078df09'
|
||||||
|
|
||||||
download_dir="$(pwd)/vendor/prosody-appimage"
|
download_dir="$(pwd)/vendor/prosody-appimage"
|
||||||
dist_dir="$(pwd)/dist/server/prosody"
|
dist_dir="$(pwd)/dist/server/prosody"
|
||||||
|
41
client/.eslintrc.json
Normal file
41
client/.eslintrc.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"standard-with-typescript",
|
||||||
|
"plugin:lit/recommended"
|
||||||
|
],
|
||||||
|
"globals": {},
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2018,
|
||||||
|
"project": [
|
||||||
|
"./client/tsconfig.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"ignorePatterns": [],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-unused-vars": [2, {"argsIgnorePattern": "^_"}],
|
||||||
|
"@typescript-eslint/no-floating-promises": "error",
|
||||||
|
"@typescript-eslint/no-misused-promises": "error",
|
||||||
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
|
"@typescript-eslint/strict-boolean-expressions": "off",
|
||||||
|
"@typescript-eslint/return-await": [2, "in-try-catch"], // FIXME: correct?
|
||||||
|
"@typescript-eslint/no-invalid-void-type": "off",
|
||||||
|
"@typescript-eslint/triple-slash-reference": "off",
|
||||||
|
"max-len": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"code": 120,
|
||||||
|
"comments": 120
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-unused-vars": "off"
|
||||||
|
}
|
||||||
|
}
|
35
client/@types/global.d.ts
vendored
35
client/@types/global.d.ts
vendored
@ -12,7 +12,6 @@ declare const MUSTACHE_CONFIGURATION_CHANNEL: string
|
|||||||
// Constants that begins with "LOC_" are loaded by build-client.js, reading the english locale file.
|
// Constants that begins with "LOC_" are loaded by build-client.js, reading the english locale file.
|
||||||
// See the online documentation: https://livingston.frama.io/peertube-plugin-livechat/contributing/translate/
|
// See the online documentation: https://livingston.frama.io/peertube-plugin-livechat/contributing/translate/
|
||||||
declare const LOC_ONLINE_HELP: string
|
declare const LOC_ONLINE_HELP: string
|
||||||
declare const LOC_CHAT: string
|
|
||||||
declare const LOC_OPEN_CHAT: string
|
declare const LOC_OPEN_CHAT: string
|
||||||
declare const LOC_OPEN_CHAT_NEW_WINDOW: string
|
declare const LOC_OPEN_CHAT_NEW_WINDOW: string
|
||||||
declare const LOC_CLOSE_CHAT: string
|
declare const LOC_CLOSE_CHAT: string
|
||||||
@ -57,12 +56,12 @@ declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE: string
|
|||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL: string
|
||||||
@ -134,29 +133,3 @@ declare const LOC_POLL_VOTE_OK: string
|
|||||||
|
|
||||||
declare const LOC_MODERATION_DELAY: string
|
declare const LOC_MODERATION_DELAY: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_MODERATION_DELAY_DESC: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_MODERATION_DELAY_DESC: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL: string
|
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_DESC: string
|
|
||||||
|
|
||||||
declare const LOC_PROSODY_FIREWALL_CONFIGURATION: string
|
|
||||||
declare const LOC_PROSODY_FIREWALL_CONFIGURATION_HELP: string
|
|
||||||
declare const LOC_PROSODY_FIREWALL_DISABLED_WARNING: string
|
|
||||||
declare const LOC_PROSODY_FIREWALL_FILE_ENABLED: string
|
|
||||||
declare const LOC_PROSODY_FIREWALL_NAME: string
|
|
||||||
declare const LOC_PROSODY_FIREWALL_NAME_DESC: string
|
|
||||||
declare const LOC_PROSODY_FIREWALL_CONTENT: string
|
|
||||||
|
|
||||||
declare const LOC_EMOJI_ONLY_MODE_TITLE: string
|
|
||||||
declare const LOC_EMOJI_ONLY_MODE_DESC_1: string
|
|
||||||
declare const LOC_EMOJI_ONLY_MODE_DESC_2: string
|
|
||||||
declare const LOC_EMOJI_ONLY_MODE_DESC_3: string
|
|
||||||
declare const LOC_EMOJI_ONLY_ENABLE_ALL_ROOMS: string
|
|
||||||
|
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_LABEL: string
|
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_DESC: string
|
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_LABEL: string
|
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_DESC: string
|
|
||||||
|
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL: string
|
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DESC: string
|
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_LABEL: string
|
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_DESC: string
|
|
||||||
|
@ -143,7 +143,7 @@ function register (clientOptions: RegisterClientOptions): void {
|
|||||||
lastActivityEl.textContent = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
lastActivityEl.textContent = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
||||||
}
|
}
|
||||||
const promoteButton = document.createElement('a')
|
const promoteButton = document.createElement('a')
|
||||||
promoteButton.classList.add('primary-button', 'orange-button', 'peertube-button-link')
|
promoteButton.classList.add('orange-button', 'peertube-button-link')
|
||||||
promoteButton.style.margin = '5px'
|
promoteButton.style.margin = '5px'
|
||||||
promoteButton.onclick = async () => {
|
promoteButton.onclick = async () => {
|
||||||
await fetch(
|
await fetch(
|
||||||
@ -243,10 +243,7 @@ function register (clientOptions: RegisterClientOptions): void {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
peertubeHelpers.notifier.error(
|
peertubeHelpers.notifier.error(error.toString(), await peertubeHelpers.translate(LOC_LOADING_ERROR))
|
||||||
(error as Error).toString(),
|
|
||||||
await peertubeHelpers.translate(LOC_LOADING_ERROR)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -273,8 +270,6 @@ function register (clientOptions: RegisterClientOptions): void {
|
|||||||
return !(options.formValues['chat-all-lives'] === true && options.formValues['chat-per-live-video'] === true)
|
return !(options.formValues['chat-all-lives'] === true && options.formValues['chat-per-live-video'] === true)
|
||||||
case 'auto-ban-anonymous-ip':
|
case 'auto-ban-anonymous-ip':
|
||||||
return options.formValues['chat-no-anonymous'] !== false
|
return options.formValues['chat-no-anonymous'] !== false
|
||||||
case 'prosody-firewall-configure-button':
|
|
||||||
return options.formValues['prosody-firewall-enabled'] !== true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name?.startsWith('external-auth-')) {
|
if (name?.startsWith('external-auth-')) {
|
||||||
|
@ -8,7 +8,6 @@ import { registerConfiguration } from './common/configuration/register'
|
|||||||
import { registerVideoWatch } from './common/videowatch/register'
|
import { registerVideoWatch } from './common/videowatch/register'
|
||||||
import { registerRoom } from './common/room/register'
|
import { registerRoom } from './common/room/register'
|
||||||
import { initPtContext } from './common/lib/contexts/peertube'
|
import { initPtContext } from './common/lib/contexts/peertube'
|
||||||
import { registerAdminFirewall } from './common/admin/firewall/register'
|
|
||||||
import './common/lib/elements' // Import shared elements.
|
import './common/lib/elements' // Import shared elements.
|
||||||
|
|
||||||
async function register (clientOptions: RegisterClientOptions): Promise<void> {
|
async function register (clientOptions: RegisterClientOptions): Promise<void> {
|
||||||
@ -46,7 +45,7 @@ async function register (clientOptions: RegisterClientOptions): Promise<void> {
|
|||||||
])
|
])
|
||||||
const webchatFieldOptions: RegisterClientFormFieldOptions = {
|
const webchatFieldOptions: RegisterClientFormFieldOptions = {
|
||||||
name: 'livechat-active',
|
name: 'livechat-active',
|
||||||
label,
|
label: label,
|
||||||
descriptionHTML: description,
|
descriptionHTML: description,
|
||||||
type: 'input-checkbox',
|
type: 'input-checkbox',
|
||||||
default: true,
|
default: true,
|
||||||
@ -70,8 +69,7 @@ async function register (clientOptions: RegisterClientOptions): Promise<void> {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
registerVideoWatch(),
|
registerVideoWatch(),
|
||||||
registerRoom(clientOptions),
|
registerRoom(clientOptions),
|
||||||
registerConfiguration(clientOptions),
|
registerConfiguration(clientOptions)
|
||||||
registerAdminFirewall(clientOptions)
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,131 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import type { AdminFirewallConfiguration } from 'shared/lib/types'
|
|
||||||
import { AdminFirewallService } from '../services/admin-firewall'
|
|
||||||
import { LivechatElement } from '../../../lib/elements/livechat'
|
|
||||||
import { ValidationError, ValidationErrorType } from '../../../lib/models/validation'
|
|
||||||
import { tplAdminFirewall } from '../templates/admin-firewall'
|
|
||||||
import { TemplateResult, html, nothing } from 'lit'
|
|
||||||
import { customElement, state } from 'lit/decorators.js'
|
|
||||||
import { Task } from '@lit/task'
|
|
||||||
|
|
||||||
@customElement('livechat-admin-firewall')
|
|
||||||
export class AdminFirewallElement extends LivechatElement {
|
|
||||||
private _adminFirewallService?: AdminFirewallService
|
|
||||||
|
|
||||||
@state()
|
|
||||||
public firewallConfiguration?: AdminFirewallConfiguration
|
|
||||||
|
|
||||||
@state()
|
|
||||||
public validationError?: ValidationError
|
|
||||||
|
|
||||||
@state()
|
|
||||||
public actionDisabled = false
|
|
||||||
|
|
||||||
private _asyncTaskRender: Task
|
|
||||||
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
this._asyncTaskRender = this._initTask()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _initTask (): Task {
|
|
||||||
return new Task(this, {
|
|
||||||
task: async () => {
|
|
||||||
this._adminFirewallService = new AdminFirewallService(this.ptOptions)
|
|
||||||
this.firewallConfiguration = await this._adminFirewallService.fetchConfiguration()
|
|
||||||
this.actionDisabled = false // in case of reset
|
|
||||||
},
|
|
||||||
args: () => []
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the form by reloading data from backend.
|
|
||||||
*/
|
|
||||||
public async reset (event?: Event): Promise<void> {
|
|
||||||
event?.preventDefault()
|
|
||||||
this.actionDisabled = true
|
|
||||||
this._asyncTaskRender = this._initTask()
|
|
||||||
this.requestUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the validation errors.
|
|
||||||
* @param ev the vent
|
|
||||||
*/
|
|
||||||
public resetValidation (_ev?: Event): void {
|
|
||||||
if (this.validationError) {
|
|
||||||
this.validationError = undefined
|
|
||||||
this.requestUpdate('_validationError')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the configuration.
|
|
||||||
* @param event event
|
|
||||||
*/
|
|
||||||
public readonly saveConfig = async (event?: Event): Promise<void> => {
|
|
||||||
event?.preventDefault()
|
|
||||||
if (!this.firewallConfiguration || !this._adminFirewallService) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.actionDisabled = true
|
|
||||||
this._adminFirewallService.saveConfiguration(this.firewallConfiguration)
|
|
||||||
.then((result: AdminFirewallConfiguration) => {
|
|
||||||
this.validationError = undefined
|
|
||||||
this.ptTranslate(LOC_SUCCESSFULLY_SAVED).then((msg) => {
|
|
||||||
this.ptNotifier.info(msg)
|
|
||||||
}, () => {})
|
|
||||||
this.firewallConfiguration = result
|
|
||||||
this.requestUpdate('firewallConfiguration')
|
|
||||||
this.requestUpdate('_validationError')
|
|
||||||
})
|
|
||||||
.catch(async (error: Error) => {
|
|
||||||
this.validationError = undefined
|
|
||||||
if (error instanceof ValidationError) {
|
|
||||||
this.validationError = error
|
|
||||||
}
|
|
||||||
this.logger.warn(`A validation error occurred in saving configuration. ${error.name}: ${error.message}`)
|
|
||||||
this.ptNotifier.error(
|
|
||||||
error.message
|
|
||||||
? error.message
|
|
||||||
: await this.ptTranslate(LOC_ERROR)
|
|
||||||
)
|
|
||||||
this.requestUpdate('_validationError')
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.actionDisabled = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly getInputValidationClass = (propertyName: string): Record<string, boolean> => {
|
|
||||||
const validationErrorTypes: ValidationErrorType[] | undefined =
|
|
||||||
this.validationError?.properties[propertyName]
|
|
||||||
return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {}
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly renderFeedback = (feedbackId: string,
|
|
||||||
propertyName: string): TemplateResult | typeof nothing => {
|
|
||||||
const errorMessages: TemplateResult[] = []
|
|
||||||
const validationErrorTypes: ValidationErrorType[] | undefined =
|
|
||||||
this.validationError?.properties[propertyName] ?? undefined
|
|
||||||
|
|
||||||
// FIXME: this code is duplicated in dymamic table form
|
|
||||||
if (validationErrorTypes && validationErrorTypes.length !== 0) {
|
|
||||||
return html`<div id=${feedbackId} class="invalid-feedback">${errorMessages}</div>`
|
|
||||||
} else {
|
|
||||||
return nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override render = (): unknown => {
|
|
||||||
return this._asyncTaskRender.render({
|
|
||||||
pending: () => html`<livechat-spinner></livechat-spinner>`,
|
|
||||||
error: () => html`<livechat-error></livechat-error>`,
|
|
||||||
complete: () => tplAdminFirewall(this)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import './admin-firewall'
|
|
@ -1,26 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
|
||||||
import { html, render } from 'lit'
|
|
||||||
import './elements' // Import all needed elements.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers stuff related to mod_firewall configuration.
|
|
||||||
* @param clientOptions Peertube client options
|
|
||||||
*/
|
|
||||||
async function registerAdminFirewall (clientOptions: RegisterClientOptions): Promise<void> {
|
|
||||||
const { registerClientRoute } = clientOptions
|
|
||||||
|
|
||||||
registerClientRoute({
|
|
||||||
route: 'livechat/admin/firewall',
|
|
||||||
onMount: async ({ rootEl }) => {
|
|
||||||
render(html`<livechat-admin-firewall .registerClientOptions=${clientOptions}></livechat-admin-firewall>`, rootEl)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
registerAdminFirewall
|
|
||||||
}
|
|
@ -1,108 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
|
||||||
import type { AdminFirewallConfiguration } from 'shared/lib/types'
|
|
||||||
import {
|
|
||||||
maxFirewallFileSize, maxFirewallNameLength, maxFirewallFiles, firewallNameRegexp
|
|
||||||
} from 'shared/lib/admin-firewall'
|
|
||||||
import { ValidationError, ValidationErrorType } from '../../../lib/models/validation'
|
|
||||||
import { getBaseRoute } from '../../../../utils/uri'
|
|
||||||
|
|
||||||
export class AdminFirewallService {
|
|
||||||
public _registerClientOptions: RegisterClientOptions
|
|
||||||
|
|
||||||
private readonly _headers: any = {}
|
|
||||||
|
|
||||||
constructor (registerClientOptions: RegisterClientOptions) {
|
|
||||||
this._registerClientOptions = registerClientOptions
|
|
||||||
|
|
||||||
this._headers = this._registerClientOptions.peertubeHelpers.getAuthHeader() ?? {}
|
|
||||||
this._headers['content-type'] = 'application/json;charset=UTF-8'
|
|
||||||
}
|
|
||||||
|
|
||||||
async validateConfiguration (adminFirewallConfiguration: AdminFirewallConfiguration): Promise<boolean> {
|
|
||||||
const propertiesError: ValidationError['properties'] = {}
|
|
||||||
|
|
||||||
if (adminFirewallConfiguration.files.length > maxFirewallFiles) {
|
|
||||||
const validationError = new ValidationError(
|
|
||||||
'AdminFirewallConfigurationValidationError',
|
|
||||||
await this._registerClientOptions.peertubeHelpers.translate(LOC_TOO_MANY_ENTRIES),
|
|
||||||
propertiesError
|
|
||||||
)
|
|
||||||
throw validationError
|
|
||||||
}
|
|
||||||
|
|
||||||
const seen = new Map<string, true>()
|
|
||||||
for (const [i, e] of adminFirewallConfiguration.files.entries()) {
|
|
||||||
propertiesError[`files.${i}.name`] = []
|
|
||||||
if (e.name === '') {
|
|
||||||
propertiesError[`files.${i}.name`].push(ValidationErrorType.Missing)
|
|
||||||
} else if (e.name.length > maxFirewallNameLength) {
|
|
||||||
propertiesError[`files.${i}.name`].push(ValidationErrorType.TooLong)
|
|
||||||
} else if (!firewallNameRegexp.test(e.name)) {
|
|
||||||
propertiesError[`files.${i}.name`].push(ValidationErrorType.WrongFormat)
|
|
||||||
} else if (seen.has(e.name)) {
|
|
||||||
propertiesError[`files.${i}.name`].push(ValidationErrorType.Duplicate)
|
|
||||||
} else {
|
|
||||||
seen.set(e.name, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
propertiesError[`files.${i}.content`] = []
|
|
||||||
if (e.content.length > maxFirewallFileSize) {
|
|
||||||
propertiesError[`files.${i}.content`].push(ValidationErrorType.TooLong)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.values(propertiesError).find(e => e.length > 0)) {
|
|
||||||
const validationError = new ValidationError(
|
|
||||||
'AdminFirewallConfigurationValidationError',
|
|
||||||
await this._registerClientOptions.peertubeHelpers.translate(LOC_VALIDATION_ERROR),
|
|
||||||
propertiesError
|
|
||||||
)
|
|
||||||
throw validationError
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveConfiguration (
|
|
||||||
adminFirewallConfiguration: AdminFirewallConfiguration
|
|
||||||
): Promise<AdminFirewallConfiguration> {
|
|
||||||
if (!await this.validateConfiguration(adminFirewallConfiguration)) {
|
|
||||||
throw new Error('Invalid form data')
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
getBaseRoute(this._registerClientOptions) + '/api/admin/firewall/',
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: this._headers,
|
|
||||||
body: JSON.stringify(adminFirewallConfiguration)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to save configuration.')
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchConfiguration (): Promise<AdminFirewallConfiguration> {
|
|
||||||
const response = await fetch(
|
|
||||||
getBaseRoute(this._registerClientOptions) + '/api/admin/firewall/',
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
headers: this._headers
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Can\'t get firewall configuration.')
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
|
||||||
/* eslint-disable @stylistic/indent */
|
|
||||||
|
|
||||||
import type { AdminFirewallElement } from '../elements/admin-firewall'
|
|
||||||
import type { TemplateResult } from 'lit'
|
|
||||||
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
|
|
||||||
import { maxFirewallFiles, maxFirewallNameLength, maxFirewallFileSize } from 'shared/lib/admin-firewall'
|
|
||||||
import { ptTr } from '../../../lib/directives/translation'
|
|
||||||
import { html } from 'lit'
|
|
||||||
|
|
||||||
export function tplAdminFirewall (el: AdminFirewallElement): TemplateResult {
|
|
||||||
const tableHeaderList: DynamicFormHeader = {
|
|
||||||
enabled: {
|
|
||||||
colName: ptTr(LOC_PROSODY_FIREWALL_FILE_ENABLED)
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
colName: ptTr(LOC_PROSODY_FIREWALL_NAME),
|
|
||||||
description: ptTr(LOC_PROSODY_FIREWALL_NAME_DESC),
|
|
||||||
headerClassList: ['peertube-livechat-admin-firewall-col-name']
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
colName: ptTr(LOC_PROSODY_FIREWALL_CONTENT),
|
|
||||||
headerClassList: ['peertube-livechat-admin-firewall-col-content']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const tableSchema: DynamicFormSchema = {
|
|
||||||
enabled: {
|
|
||||||
inputType: 'checkbox',
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
inputType: 'text',
|
|
||||||
default: '',
|
|
||||||
maxlength: maxFirewallNameLength
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
inputType: 'textarea',
|
|
||||||
default: '',
|
|
||||||
maxlength: maxFirewallFileSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="margin-content peertube-plugin-livechat-admin-firewall">
|
|
||||||
<h1>
|
|
||||||
${ptTr(LOC_PROSODY_FIREWALL_CONFIGURATION)}
|
|
||||||
</h1>
|
|
||||||
<p>
|
|
||||||
${ptTr(LOC_PROSODY_FIREWALL_CONFIGURATION_HELP, true)}
|
|
||||||
<livechat-help-button .page=${'documentation/admin/mod_firewall'}>
|
|
||||||
</livechat-help-button>
|
|
||||||
</p>
|
|
||||||
${
|
|
||||||
el.firewallConfiguration?.enabled
|
|
||||||
? ''
|
|
||||||
: html`<p class="peertube-plugin-livechat-warning">${ptTr(LOC_PROSODY_FIREWALL_DISABLED_WARNING, true)}</p>`
|
|
||||||
}
|
|
||||||
|
|
||||||
<form role="form" @submit=${el.saveConfig} @change=${el.resetValidation}>
|
|
||||||
<livechat-dynamic-table-form
|
|
||||||
.header=${tableHeaderList}
|
|
||||||
.schema=${tableSchema}
|
|
||||||
.maxLines=${maxFirewallFiles}
|
|
||||||
.validation=${el.validationError?.properties}
|
|
||||||
.validationPrefix=${'files'}
|
|
||||||
.rows=${el.firewallConfiguration?.files ?? []}
|
|
||||||
@update=${(e: CustomEvent) => {
|
|
||||||
el.resetValidation(e)
|
|
||||||
if (el.firewallConfiguration) {
|
|
||||||
el.firewallConfiguration.files = e.detail
|
|
||||||
el.requestUpdate('firewallConfiguration')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
></livechat-dynamic-table-form>
|
|
||||||
|
|
||||||
<div class="form-group mt-5">
|
|
||||||
<button type="reset" @click=${el.reset} ?disabled=${el.actionDisabled}>
|
|
||||||
${ptTr(LOC_CANCEL)}
|
|
||||||
</button>
|
|
||||||
<button type="submit" ?disabled=${el.actionDisabled}>
|
|
||||||
${ptTr(LOC_SAVE)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>`
|
|
||||||
}
|
|
@ -32,7 +32,7 @@ export class ChannelConfigurationElement extends LivechatElement {
|
|||||||
public validationError?: ValidationError
|
public validationError?: ValidationError
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
public actionDisabled = false
|
public actionDisabled: boolean = false
|
||||||
|
|
||||||
private _asyncTaskRender: Task
|
private _asyncTaskRender: Task
|
||||||
|
|
||||||
@ -113,9 +113,9 @@ export class ChannelConfigurationElement extends LivechatElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly getInputValidationClass = (propertyName: string): Record<string, boolean> => {
|
public readonly getInputValidationClass = (propertyName: string): { [key: string]: boolean } => {
|
||||||
const validationErrorTypes: ValidationErrorType[] | undefined =
|
const validationErrorTypes: ValidationErrorType[] | undefined =
|
||||||
this.validationError?.properties[propertyName]
|
this.validationError?.properties[`${propertyName}`]
|
||||||
return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {}
|
return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ export class ChannelConfigurationElement extends LivechatElement {
|
|||||||
propertyName: string): TemplateResult | typeof nothing => {
|
propertyName: string): TemplateResult | typeof nothing => {
|
||||||
const errorMessages: TemplateResult[] = []
|
const errorMessages: TemplateResult[] = []
|
||||||
const validationErrorTypes: ValidationErrorType[] | undefined =
|
const validationErrorTypes: ValidationErrorType[] | undefined =
|
||||||
this.validationError?.properties[propertyName] ?? undefined
|
this.validationError?.properties[`${propertyName}`] ?? undefined
|
||||||
|
|
||||||
// FIXME: this code is duplicated in dymamic table form
|
// FIXME: this code is duplicated in dymamic table form
|
||||||
if (validationErrorTypes && validationErrorTypes.length !== 0) {
|
if (validationErrorTypes && validationErrorTypes.length !== 0) {
|
||||||
|
@ -30,7 +30,7 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
public validationError?: ValidationError
|
public validationError?: ValidationError
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
public actionDisabled = false
|
public actionDisabled: boolean = false
|
||||||
|
|
||||||
private _asyncTaskRender: Task
|
private _asyncTaskRender: Task
|
||||||
|
|
||||||
@ -192,7 +192,7 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
throw new Error('Invalid data')
|
throw new Error('Invalid data')
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = await this._convertImageToDataUrl(entry.url as string)
|
const url = await this._convertImageToDataUrl(entry.url)
|
||||||
const sn = entry.sn as string
|
const sn = entry.sn as string
|
||||||
|
|
||||||
const item: ChannelEmojisConfiguration['emojis']['customEmojis'][0] = {
|
const item: ChannelEmojisConfiguration['emojis']['customEmojis'][0] = {
|
||||||
@ -211,7 +211,7 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
await this.ptTranslate(LOC_ACTION_IMPORT_EMOJIS_INFO)
|
await this.ptTranslate(LOC_ACTION_IMPORT_EMOJIS_INFO)
|
||||||
)
|
)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.ptNotifier.error((err as Error).toString(), await this.ptTranslate(LOC_ERROR))
|
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR))
|
||||||
} finally {
|
} finally {
|
||||||
this.actionDisabled = false
|
this.actionDisabled = false
|
||||||
}
|
}
|
||||||
@ -250,27 +250,12 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
a.remove()
|
a.remove()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error(err)
|
this.logger.error(err)
|
||||||
this.ptNotifier.error((err as Error).toString())
|
this.ptNotifier.error(err.toString())
|
||||||
} finally {
|
} finally {
|
||||||
this.actionDisabled = false
|
this.actionDisabled = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enableEmojisOnlyModeOnAllRooms (ev: Event): Promise<void> {
|
|
||||||
ev.preventDefault()
|
|
||||||
if (!this._channelDetailsService || !this.channelId) {
|
|
||||||
this.ptNotifier.error(await this.ptTranslate(LOC_ERROR))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await this._channelDetailsService.enableEmojisOnlyModeOnAllRooms(this.channelId)
|
|
||||||
this.ptNotifier.info(await this.ptTranslate(LOC_SUCCESSFULLY_SAVED))
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
this.ptNotifier.error(await this.ptTranslate(LOC_ERROR))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes an url (or dataUrl), download the image, and converts to dataUrl.
|
* Takes an url (or dataUrl), download the image, and converts to dataUrl.
|
||||||
* @param url the url
|
* @param url the url
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
|
||||||
/* eslint-disable @stylistic/indent */
|
|
||||||
|
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
import { customElement, state } from 'lit/decorators.js'
|
import { customElement, state } from 'lit/decorators.js'
|
||||||
import { ptTr } from '../../lib/directives/translation'
|
import { ptTr } from '../../lib/directives/translation'
|
||||||
@ -53,7 +50,7 @@ export class ChannelHomeElement extends LivechatElement {
|
|||||||
<ul class="peertube-plugin-livechat-configuration-home-channels">
|
<ul class="peertube-plugin-livechat-configuration-home-channels">
|
||||||
${this._channels?.map((channel) => html`
|
${this._channels?.map((channel) => html`
|
||||||
<li>
|
<li>
|
||||||
<a href="${channel.livechatConfigurationUri}" aria-hidden="true">
|
<a href="${channel.livechatConfigurationUri}">
|
||||||
${channel.avatar
|
${channel.avatar
|
||||||
? html`<img class="avatar channel" src="${channel.avatar.path}">`
|
? html`<img class="avatar channel" src="${channel.avatar.path}">`
|
||||||
: html`<div class="avatar channel initial gray"></div>`
|
: html`<div class="avatar channel initial gray"></div>`
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
|
||||||
/* eslint-disable @stylistic/indent */
|
|
||||||
|
|
||||||
import { LivechatElement } from '../../lib/elements/livechat'
|
import { LivechatElement } from '../../lib/elements/livechat'
|
||||||
import { ptTr } from '../../lib/directives/translation'
|
import { ptTr } from '../../lib/directives/translation'
|
||||||
import { html, TemplateResult } from 'lit'
|
import { html, TemplateResult } from 'lit'
|
||||||
|
@ -2,18 +2,14 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
|
||||||
/* eslint-disable @stylistic/indent */
|
|
||||||
|
|
||||||
import type { ChannelConfigurationElement } from '../channel-configuration'
|
import type { ChannelConfigurationElement } from '../channel-configuration'
|
||||||
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
|
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
|
||||||
import { ptTr } from '../../../lib/directives/translation'
|
import { ptTr } from '../../../lib/directives/translation'
|
||||||
import { html, TemplateResult } from 'lit'
|
import { html, TemplateResult } from 'lit'
|
||||||
import { classMap } from 'lit/directives/class-map.js'
|
import { classMap } from 'lit/directives/class-map.js'
|
||||||
import { noDuplicateMaxDelay, forbidSpecialCharsMaxTolerance } from 'shared/lib/constants'
|
|
||||||
|
|
||||||
export function tplChannelConfiguration (el: ChannelConfigurationElement): TemplateResult {
|
export function tplChannelConfiguration (el: ChannelConfigurationElement): TemplateResult {
|
||||||
const tableHeaderList: Record<string, DynamicFormHeader> = {
|
const tableHeaderList: {[key: string]: DynamicFormHeader} = {
|
||||||
forbiddenWords: {
|
forbiddenWords: {
|
||||||
entries: {
|
entries: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL),
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL),
|
||||||
@ -24,16 +20,16 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC)
|
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC)
|
||||||
},
|
},
|
||||||
applyToModerators: {
|
applyToModerators: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL),
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL),
|
||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)
|
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC)
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL),
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL),
|
||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC)
|
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC)
|
||||||
},
|
},
|
||||||
reason: {
|
reason: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL),
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL),
|
||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)
|
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC)
|
||||||
},
|
},
|
||||||
comments: {
|
comments: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL),
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL),
|
||||||
@ -61,7 +57,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tableSchema: Record<string, DynamicFormSchema> = {
|
const tableSchema: {[key: string]: DynamicFormSchema} = {
|
||||||
forbiddenWords: {
|
forbiddenWords: {
|
||||||
entries: {
|
entries: {
|
||||||
inputType: 'tags',
|
inputType: 'tags',
|
||||||
@ -139,7 +135,6 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
</livechat-configuration-section-header>
|
</livechat-configuration-section-header>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<textarea
|
<textarea
|
||||||
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TERMS_LABEL) as any}
|
|
||||||
name="terms"
|
name="terms"
|
||||||
id="peertube-livechat-terms"
|
id="peertube-livechat-terms"
|
||||||
.value=${el.channelConfiguration?.configuration.terms ?? ''}
|
.value=${el.channelConfiguration?.configuration.terms ?? ''}
|
||||||
@ -172,7 +167,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="mute_anonymous"
|
name="bot"
|
||||||
id="peertube-livechat-mute-anonymous"
|
id="peertube-livechat-mute-anonymous"
|
||||||
@input=${(event: InputEvent) => {
|
@input=${(event: InputEvent) => {
|
||||||
if (event?.target && el.channelConfiguration) {
|
if (event?.target && el.channelConfiguration) {
|
||||||
@ -259,32 +254,6 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
${el.renderFeedback('peertube-livechat-moderation-delay-feedback', 'moderation.delay')}
|
${el.renderFeedback('peertube-livechat-moderation-delay-feedback', 'moderation.delay')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<livechat-configuration-section-header
|
|
||||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL)}
|
|
||||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_DESC, true)}
|
|
||||||
.helpPage=${'documentation/user/streamers/moderation'}>
|
|
||||||
</livechat-configuration-section-header>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="anonymize-moderation"
|
|
||||||
id="peertube-livechat-anonymize-moderation"
|
|
||||||
@input=${(event: InputEvent) => {
|
|
||||||
if (event?.target && el.channelConfiguration) {
|
|
||||||
el.channelConfiguration.configuration.moderation.anonymize =
|
|
||||||
(event.target as HTMLInputElement).checked
|
|
||||||
}
|
|
||||||
el.requestUpdate('channelConfiguration')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value="1"
|
|
||||||
?checked=${el.channelConfiguration?.configuration.moderation.anonymize}
|
|
||||||
/>
|
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL)}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<livechat-configuration-section-header
|
<livechat-configuration-section-header
|
||||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
|
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
|
||||||
.description=${''}
|
.description=${''}
|
||||||
@ -341,246 +310,6 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
${el.renderFeedback('peertube-livechat-bot-nickname-feedback', 'bot.nickname')}
|
${el.renderFeedback('peertube-livechat-bot-nickname-feedback', 'bot.nickname')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<livechat-configuration-section-header
|
|
||||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_LABEL)}
|
|
||||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_DESC)}
|
|
||||||
.helpPage=${'documentation/user/streamers/bot/special_chars'}>
|
|
||||||
</livechat-configuration-section-header>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="forbid_special_chars"
|
|
||||||
id="peertube-livechat-forbid-special-chars"
|
|
||||||
@input=${(event: InputEvent) => {
|
|
||||||
if (event?.target && el.channelConfiguration) {
|
|
||||||
el.channelConfiguration.configuration.bot.forbidSpecialChars.enabled =
|
|
||||||
(event.target as HTMLInputElement).checked
|
|
||||||
}
|
|
||||||
el.requestUpdate('channelConfiguration')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value="1"
|
|
||||||
?checked=${el.channelConfiguration?.configuration.bot.forbidSpecialChars.enabled}
|
|
||||||
/>
|
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_LABEL)}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
${!el.channelConfiguration?.configuration.bot.forbidSpecialChars.enabled
|
|
||||||
? ''
|
|
||||||
: html`
|
|
||||||
<div class="form-group">
|
|
||||||
<label>
|
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_LABEL)}
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="special_chars_tolerance"
|
|
||||||
class=${classMap(
|
|
||||||
Object.assign(
|
|
||||||
{ 'form-control': true },
|
|
||||||
el.getInputValidationClass('bot.forbidSpecialChars.tolerance')
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
min="0"
|
|
||||||
max="${forbidSpecialCharsMaxTolerance}"
|
|
||||||
id="peertube-livechat-forbid-special-chars-tolerance"
|
|
||||||
aria-describedby="peertube-livechat-forbid-special-chars-tolerance-feedback"
|
|
||||||
@input=${(event: InputEvent) => {
|
|
||||||
if (event?.target && el.channelConfiguration) {
|
|
||||||
el.channelConfiguration.configuration.bot.forbidSpecialChars.tolerance =
|
|
||||||
Number((event.target as HTMLInputElement).value)
|
|
||||||
}
|
|
||||||
el.requestUpdate('channelConfiguration')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value="${el.channelConfiguration?.configuration.bot.forbidSpecialChars.tolerance ?? '0'}"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<small class="form-text text-muted">
|
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_DESC)}
|
|
||||||
</small>
|
|
||||||
${el.renderFeedback('peertube-livechat-forbid-special-chars-tolerance-feedback',
|
|
||||||
'bot.forbidSpecialChars.tolerance')
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>
|
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL)}
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="special_chars_reason"
|
|
||||||
class=${classMap(
|
|
||||||
Object.assign(
|
|
||||||
{ 'form-control': true },
|
|
||||||
el.getInputValidationClass('bot.forbidSpecialChars.reason')
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
id="peertube-livechat-forbid-special-chars-reason"
|
|
||||||
aria-describedby="peertube-livechat-forbid-special-chars-reason-feedback"
|
|
||||||
@input=${(event: InputEvent) => {
|
|
||||||
if (event?.target && el.channelConfiguration) {
|
|
||||||
el.channelConfiguration.configuration.bot.forbidSpecialChars.reason =
|
|
||||||
(event.target as HTMLInputElement).value
|
|
||||||
}
|
|
||||||
el.requestUpdate('channelConfiguration')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value="${el.channelConfiguration?.configuration.bot.forbidSpecialChars.reason ?? ''}"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<small class="form-text text-muted">
|
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)}
|
|
||||||
</small>
|
|
||||||
${el.renderFeedback('peertube-livechat-forbid-special-chars-reason-feedback',
|
|
||||||
'bot.forbidSpecialChars.reason')
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="forbid_special_chars_applyToModerators"
|
|
||||||
id="peertube-livechat-forbid-special-chars-applyToModerators"
|
|
||||||
@input=${(event: InputEvent) => {
|
|
||||||
if (event?.target && el.channelConfiguration) {
|
|
||||||
el.channelConfiguration.configuration.bot.forbidSpecialChars.applyToModerators =
|
|
||||||
(event.target as HTMLInputElement).checked
|
|
||||||
}
|
|
||||||
el.requestUpdate('channelConfiguration')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value="1"
|
|
||||||
?checked=${el.channelConfiguration?.configuration.bot.forbidSpecialChars.applyToModerators}
|
|
||||||
/>
|
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL)}
|
|
||||||
</label>
|
|
||||||
<small class="form-text text-muted">
|
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
<livechat-configuration-section-header
|
|
||||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL)}
|
|
||||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DESC)}
|
|
||||||
.helpPage=${'documentation/user/streamers/bot/no_duplicate'}>
|
|
||||||
</livechat-configuration-section-header>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="no_duplicate"
|
|
||||||
id="peertube-livechat-no-duplicate"
|
|
||||||
@input=${(event: InputEvent) => {
|
|
||||||
if (event?.target && el.channelConfiguration) {
|
|
||||||
el.channelConfiguration.configuration.bot.noDuplicate.enabled =
|
|
||||||
(event.target as HTMLInputElement).checked
|
|
||||||
}
|
|
||||||
el.requestUpdate('channelConfiguration')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value="1"
|
|
||||||
?checked=${el.channelConfiguration?.configuration.bot.noDuplicate.enabled}
|
|
||||||
/>
|
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL)}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
${!el.channelConfiguration?.configuration.bot.noDuplicate.enabled
|
|
||||||
? ''
|
|
||||||
: html`
|
|
||||||
<div class="form-group">
|
|
||||||
<label>
|
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_LABEL)}
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="no_duplicate_delay"
|
|
||||||
class=${classMap(
|
|
||||||
Object.assign(
|
|
||||||
{ 'form-control': true },
|
|
||||||
el.getInputValidationClass('bot.noDuplicate.delay')
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
min="0"
|
|
||||||
max="${noDuplicateMaxDelay.toString()}"
|
|
||||||
id="peertube-livechat-no-duplicate-delay"
|
|
||||||
aria-describedby="peertube-livechat-no-duplicate-delay-feedback"
|
|
||||||
@input=${(event: InputEvent) => {
|
|
||||||
if (event?.target && el.channelConfiguration) {
|
|
||||||
el.channelConfiguration.configuration.bot.noDuplicate.delay =
|
|
||||||
Number((event.target as HTMLInputElement).value)
|
|
||||||
}
|
|
||||||
el.requestUpdate('channelConfiguration')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value="${el.channelConfiguration?.configuration.bot.noDuplicate.delay ?? '0'}"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<small class="form-text text-muted">
|
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_DESC)}
|
|
||||||
</small>
|
|
||||||
${el.renderFeedback('peertube-livechat-no-duplicate-delay-feedback',
|
|
||||||
'bot.noDuplicate.delay')
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>
|
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL)}
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="no_duplicate_reason"
|
|
||||||
class=${classMap(
|
|
||||||
Object.assign(
|
|
||||||
{ 'form-control': true },
|
|
||||||
el.getInputValidationClass('bot.noDuplicate.reason')
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
id="peertube-livechat-no-duplicate-reason"
|
|
||||||
aria-describedby="peertube-livechat-no-duplicate-reason-feedback"
|
|
||||||
@input=${(event: InputEvent) => {
|
|
||||||
if (event?.target && el.channelConfiguration) {
|
|
||||||
el.channelConfiguration.configuration.bot.noDuplicate.reason =
|
|
||||||
(event.target as HTMLInputElement).value
|
|
||||||
}
|
|
||||||
el.requestUpdate('channelConfiguration')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value="${el.channelConfiguration?.configuration.bot.noDuplicate.reason ?? ''}"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<small class="form-text text-muted">
|
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)}
|
|
||||||
</small>
|
|
||||||
${el.renderFeedback('peertube-livechat-no-duplicate-reason-feedback',
|
|
||||||
'bot.noDuplicate.reason')
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="no_duplicate_applyToModerators"
|
|
||||||
id="peertube-livechat-no-duplicate-applyToModerators"
|
|
||||||
@input=${(event: InputEvent) => {
|
|
||||||
if (event?.target && el.channelConfiguration) {
|
|
||||||
el.channelConfiguration.configuration.bot.noDuplicate.applyToModerators =
|
|
||||||
(event.target as HTMLInputElement).checked
|
|
||||||
}
|
|
||||||
el.requestUpdate('channelConfiguration')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value="1"
|
|
||||||
?checked=${el.channelConfiguration?.configuration.bot.noDuplicate.applyToModerators}
|
|
||||||
/>
|
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL)}
|
|
||||||
</label>
|
|
||||||
<small class="form-text text-muted">
|
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
<livechat-configuration-section-header
|
<livechat-configuration-section-header
|
||||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
|
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
|
||||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)}
|
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)}
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
|
||||||
/* eslint-disable @stylistic/indent */
|
|
||||||
|
|
||||||
import type { ChannelEmojisElement } from '../channel-emojis'
|
import type { ChannelEmojisElement } from '../channel-emojis'
|
||||||
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
|
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
|
||||||
import { maxEmojisPerChannel } from 'shared/lib/emojis'
|
import { maxEmojisPerChannel } from 'shared/lib/emojis'
|
||||||
@ -48,14 +45,13 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
|
|||||||
|
|
||||||
<livechat-channel-tabs .active=${'emojis'} .channelId=${el.channelId}></livechat-channel-tabs>
|
<livechat-channel-tabs .active=${'emojis'} .channelId=${el.channelId}></livechat-channel-tabs>
|
||||||
|
|
||||||
<h2>${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_TITLE)}</h2>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_DESC)}
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_DESC)}
|
||||||
<livechat-help-button .page=${'documentation/user/streamers/emojis'}>
|
<livechat-help-button .page=${'documentation/user/streamers/emojis'}>
|
||||||
</livechat-help-button>
|
</livechat-help-button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
<form role="form" @submit=${el.saveEmojis} @change=${el.resetValidation}>
|
<form role="form" @submit=${el.saveEmojis} @change=${el.resetValidation}>
|
||||||
<div class="peertube-plugin-livechat-configuration-actions">
|
<div class="peertube-plugin-livechat-configuration-actions">
|
||||||
${
|
${
|
||||||
@ -90,7 +86,7 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
|
|||||||
.maxLines=${maxEmojisPerChannel}
|
.maxLines=${maxEmojisPerChannel}
|
||||||
.validation=${el.validationError?.properties}
|
.validation=${el.validationError?.properties}
|
||||||
.validationPrefix=${'emojis'}
|
.validationPrefix=${'emojis'}
|
||||||
.rows=${el.channelEmojisConfiguration?.emojis.customEmojis ?? []}
|
.rows=${el.channelEmojisConfiguration?.emojis.customEmojis}
|
||||||
@update=${(e: CustomEvent) => {
|
@update=${(e: CustomEvent) => {
|
||||||
el.resetValidation(e)
|
el.resetValidation(e)
|
||||||
if (el.channelEmojisConfiguration) {
|
if (el.channelEmojisConfiguration) {
|
||||||
@ -110,23 +106,5 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h2>${ptTr(LOC_EMOJI_ONLY_MODE_TITLE)}</h2>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
${ptTr(LOC_EMOJI_ONLY_MODE_DESC_1, true)}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
${ptTr(LOC_EMOJI_ONLY_MODE_DESC_2, true)}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
${ptTr(LOC_EMOJI_ONLY_MODE_DESC_3, true)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="peertube-plugin-livechat-configuration-actions">
|
|
||||||
<button type="button" @click=${el.enableEmojisOnlyModeOnAllRooms}>
|
|
||||||
${ptTr(LOC_EMOJI_ONLY_ENABLE_ALL_ROOMS)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
|
@ -66,16 +66,15 @@ async function registerConfiguration (clientOptions: RegisterClientOptions): Pro
|
|||||||
for (const link of links) {
|
for (const link of links) {
|
||||||
if (typeof link !== 'object') { continue }
|
if (typeof link !== 'object') { continue }
|
||||||
if (!('key' in link)) { continue }
|
if (!('key' in link)) { continue }
|
||||||
if (link.key === 'in-my-library' || link.key === 'my-video-space') {
|
if (link.key !== 'in-my-library') { continue }
|
||||||
myLibraryLinks = link
|
myLibraryLinks = link
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!myLibraryLinks) { return links }
|
if (!myLibraryLinks) { return links }
|
||||||
if (!Array.isArray(myLibraryLinks.links)) { return links }
|
if (!Array.isArray(myLibraryLinks.links)) { return links }
|
||||||
|
|
||||||
const label = await peertubeHelpers.translate(LOC_MENU_CONFIGURATION_LABEL)
|
const label = await peertubeHelpers.translate(LOC_MENU_CONFIGURATION_LABEL)
|
||||||
myLibraryLinks.links.unshift({
|
myLibraryLinks.links.push({
|
||||||
label,
|
label,
|
||||||
shortLabel: label,
|
shortLabel: label,
|
||||||
path: '/p/livechat/configuration',
|
path: '/p/livechat/configuration',
|
||||||
|
@ -11,7 +11,7 @@ import type {
|
|||||||
import { ValidationError, ValidationErrorType } from '../../lib/models/validation'
|
import { ValidationError, ValidationErrorType } from '../../lib/models/validation'
|
||||||
import { getBaseRoute } from '../../../utils/uri'
|
import { getBaseRoute } from '../../../utils/uri'
|
||||||
import { maxEmojisPerChannel } from 'shared/lib/emojis'
|
import { maxEmojisPerChannel } from 'shared/lib/emojis'
|
||||||
import { channelTermsMaxLength, noDuplicateMaxDelay, forbidSpecialCharsMaxTolerance } from 'shared/lib/constants'
|
import { channelTermsMaxLength } from 'shared/lib/constants'
|
||||||
|
|
||||||
export class ChannelDetailsService {
|
export class ChannelDetailsService {
|
||||||
public _registerClientOptions: RegisterClientOptions
|
public _registerClientOptions: RegisterClientOptions
|
||||||
@ -67,43 +67,11 @@ export class ChannelDetailsService {
|
|||||||
// The backend will ignore those values.
|
// The backend will ignore those values.
|
||||||
if (botConf.enabled) {
|
if (botConf.enabled) {
|
||||||
propertiesError['bot.nickname'] = []
|
propertiesError['bot.nickname'] = []
|
||||||
propertiesError['bot.forbidSpecialChars.tolerance'] = []
|
|
||||||
propertiesError['bot.noDuplicate.delay'] = []
|
|
||||||
|
|
||||||
if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) {
|
if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) {
|
||||||
propertiesError['bot.nickname'].push(ValidationErrorType.WrongFormat)
|
propertiesError['bot.nickname'].push(ValidationErrorType.WrongFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (botConf.forbidSpecialChars.enabled) {
|
|
||||||
const forbidSpecialCharsTolerance = channelConfigurationOptions.bot.forbidSpecialChars.tolerance
|
|
||||||
if (
|
|
||||||
(typeof forbidSpecialCharsTolerance !== 'number') ||
|
|
||||||
isNaN(forbidSpecialCharsTolerance)
|
|
||||||
) {
|
|
||||||
propertiesError['bot.forbidSpecialChars.tolerance'].push(ValidationErrorType.WrongType)
|
|
||||||
} else if (
|
|
||||||
forbidSpecialCharsTolerance < 0 ||
|
|
||||||
forbidSpecialCharsTolerance > forbidSpecialCharsMaxTolerance
|
|
||||||
) {
|
|
||||||
propertiesError['bot.forbidSpecialChars.tolerance'].push(ValidationErrorType.NotInRange)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (botConf.noDuplicate.enabled) {
|
|
||||||
const noDuplicateDelay = channelConfigurationOptions.bot.noDuplicate.delay
|
|
||||||
if (
|
|
||||||
(typeof noDuplicateDelay !== 'number') ||
|
|
||||||
isNaN(noDuplicateDelay)
|
|
||||||
) {
|
|
||||||
propertiesError['bot.noDuplicate.delay'].push(ValidationErrorType.WrongType)
|
|
||||||
} else if (
|
|
||||||
noDuplicateDelay < 0 ||
|
|
||||||
noDuplicateDelay > noDuplicateMaxDelay
|
|
||||||
) {
|
|
||||||
propertiesError['bot.noDuplicate.delay'].push(ValidationErrorType.NotInRange)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [i, fw] of botConf.forbiddenWords.entries()) {
|
for (const [i, fw] of botConf.forbiddenWords.entries()) {
|
||||||
for (const v of fw.entries) {
|
for (const v of fw.entries) {
|
||||||
propertiesError[`bot.forbiddenWords.${i}.entries`] = []
|
propertiesError[`bot.forbiddenWords.${i}.entries`] = []
|
||||||
@ -178,8 +146,7 @@ export class ChannelDetailsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const channel of channels.data) {
|
for (const channel of channels.data) {
|
||||||
channel.livechatConfigurationUri =
|
channel.livechatConfigurationUri = '/p/livechat/configuration/channel?channelId=' + encodeURIComponent(channel.id)
|
||||||
'/p/livechat/configuration/channel?channelId=' + encodeURIComponent(channel.id as string | number)
|
|
||||||
|
|
||||||
// Note: since Peertube v6.0.0, channel.avatar is dropped, and we have to use channel.avatars.
|
// Note: since Peertube v6.0.0, channel.avatar is dropped, and we have to use channel.avatars.
|
||||||
// So, if !channel.avatar, we will search a suitable one in channel.avatars, and fill channel.avatar.
|
// So, if !channel.avatar, we will search a suitable one in channel.avatars, and fill channel.avatar.
|
||||||
@ -213,11 +180,10 @@ export class ChannelDetailsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async fetchEmojisConfiguration (channelId: number): Promise<ChannelEmojisConfiguration> {
|
public async fetchEmojisConfiguration (channelId: number): Promise<ChannelEmojisConfiguration> {
|
||||||
const url = getBaseRoute(this._registerClientOptions) +
|
|
||||||
'/api/configuration/channel/emojis/' +
|
|
||||||
encodeURIComponent(channelId)
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
url,
|
getBaseRoute(this._registerClientOptions) +
|
||||||
|
'/api/configuration/channel/emojis/' +
|
||||||
|
encodeURIComponent(channelId),
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: this._headers
|
headers: this._headers
|
||||||
@ -329,11 +295,10 @@ export class ChannelDetailsService {
|
|||||||
channelId: number,
|
channelId: number,
|
||||||
channelEmojis: ChannelEmojis
|
channelEmojis: ChannelEmojis
|
||||||
): Promise<ChannelEmojisConfiguration> {
|
): Promise<ChannelEmojisConfiguration> {
|
||||||
const url = getBaseRoute(this._registerClientOptions) +
|
|
||||||
'/api/configuration/channel/emojis/' +
|
|
||||||
encodeURIComponent(channelId)
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
url,
|
getBaseRoute(this._registerClientOptions) +
|
||||||
|
'/api/configuration/channel/emojis/' +
|
||||||
|
encodeURIComponent(channelId),
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this._headers,
|
headers: this._headers,
|
||||||
@ -347,24 +312,4 @@ export class ChannelDetailsService {
|
|||||||
|
|
||||||
return response.json()
|
return response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enableEmojisOnlyModeOnAllRooms (channelId: number): Promise<void> {
|
|
||||||
const url = getBaseRoute(this._registerClientOptions) +
|
|
||||||
'/api/configuration/channel/emojis/' +
|
|
||||||
encodeURIComponent(channelId) +
|
|
||||||
'/enable_emoji_only'
|
|
||||||
const response = await fetch(
|
|
||||||
url,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: this._headers
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Can\'t enable Emojis Only Mode on all rooms.')
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,8 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
|
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
|
||||||
export const AddSVG =
|
export const AddSVG: string =
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
||||||
aria-hidden="true"
|
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
stroke-linejoin="round" class="feather feather-plus-square">
|
stroke-linejoin="round" class="feather feather-plus-square">
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
@ -14,9 +13,8 @@ export const AddSVG =
|
|||||||
</svg>`
|
</svg>`
|
||||||
|
|
||||||
// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/
|
// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/
|
||||||
export const RemoveSVG =
|
export const RemoveSVG: string =
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
||||||
aria-hidden="true"
|
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
stroke-linejoin="round" class="feather feather-x-square">
|
stroke-linejoin="round" class="feather feather-x-square">
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
|
@ -13,8 +13,8 @@ import { getPtContext } from '../contexts/peertube'
|
|||||||
export class TranslationDirective extends AsyncDirective {
|
export class TranslationDirective extends AsyncDirective {
|
||||||
private readonly _peertubeHelpers: RegisterClientHelpers
|
private readonly _peertubeHelpers: RegisterClientHelpers
|
||||||
|
|
||||||
private _translatedValue = ''
|
private _translatedValue: string = ''
|
||||||
private _localizationId = ''
|
private _localizationId: string = ''
|
||||||
|
|
||||||
private _allowUnsafeHTML = false
|
private _allowUnsafeHTML = false
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ export class TranslationDirective extends AsyncDirective {
|
|||||||
this._asyncUpdateTranslation().then(() => {}, () => {})
|
this._asyncUpdateTranslation().then(() => {}, () => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
public override render = (locId: string, allowHTML = false): TemplateResult | string => {
|
public override render = (locId: string, allowHTML: boolean = false): TemplateResult | string => {
|
||||||
this._localizationId = locId // TODO Check current component for context (to infer the prefix)
|
this._localizationId = locId // TODO Check current component for context (to infer the prefix)
|
||||||
|
|
||||||
this._allowUnsafeHTML = allowHTML
|
this._allowUnsafeHTML = allowHTML
|
||||||
|
@ -3,9 +3,6 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
|
||||||
/* eslint-disable @stylistic/indent */
|
|
||||||
|
|
||||||
import { ptTr } from '../directives/translation'
|
import { ptTr } from '../directives/translation'
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
import { customElement, property } from 'lit/decorators.js'
|
import { customElement, property } from 'lit/decorators.js'
|
||||||
|
@ -3,9 +3,6 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
|
||||||
/* eslint-disable @stylistic/indent */
|
|
||||||
|
|
||||||
import type { TagsInputElement } from './tags-input'
|
import type { TagsInputElement } from './tags-input'
|
||||||
import type { DirectiveResult } from 'lit/directive'
|
import type { DirectiveResult } from 'lit/directive'
|
||||||
import { ValidationErrorType } from '../models/validation'
|
import { ValidationErrorType } from '../models/validation'
|
||||||
@ -50,11 +47,11 @@ interface CellDataSchema {
|
|||||||
minlength?: number
|
minlength?: number
|
||||||
maxlength?: number
|
maxlength?: number
|
||||||
size?: number
|
size?: number
|
||||||
options?: Record<string, string>
|
label?: TemplateResult | string
|
||||||
|
options?: { [key: string]: string }
|
||||||
datalist?: DynamicTableAcceptedTypes[]
|
datalist?: DynamicTableAcceptedTypes[]
|
||||||
separator?: string
|
separator?: string
|
||||||
inputType?: DynamicTableAcceptedInputTypes
|
inputType?: DynamicTableAcceptedInputTypes
|
||||||
inputTitle?: string
|
|
||||||
default?: DynamicTableAcceptedTypes
|
default?: DynamicTableAcceptedTypes
|
||||||
colClassList?: string[] // CSS classes to add to the <td> element.
|
colClassList?: string[] // CSS classes to add to the <td> element.
|
||||||
}
|
}
|
||||||
@ -62,17 +59,19 @@ interface CellDataSchema {
|
|||||||
interface DynamicTableRowData {
|
interface DynamicTableRowData {
|
||||||
_id: number
|
_id: number
|
||||||
_originalIndex: number
|
_originalIndex: number
|
||||||
row: Record<string, DynamicTableAcceptedTypes>
|
row: { [key: string]: DynamicTableAcceptedTypes }
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DynamicFormHeaderCellData {
|
interface DynamicFormHeaderCellData {
|
||||||
colName: TemplateResult | DirectiveResult
|
colName: TemplateResult | DirectiveResult
|
||||||
description?: TemplateResult | DirectiveResult
|
description: TemplateResult | DirectiveResult
|
||||||
headerClassList?: string[]
|
headerClassList?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DynamicFormHeader = Record<string, DynamicFormHeaderCellData>
|
export interface DynamicFormHeader {
|
||||||
export type DynamicFormSchema = Record<string, CellDataSchema>
|
[key: string]: DynamicFormHeaderCellData
|
||||||
|
}
|
||||||
|
export interface DynamicFormSchema { [key: string]: CellDataSchema }
|
||||||
|
|
||||||
@customElement('livechat-dynamic-table-form')
|
@customElement('livechat-dynamic-table-form')
|
||||||
export class DynamicTableFormElement extends LivechatElement {
|
export class DynamicTableFormElement extends LivechatElement {
|
||||||
@ -86,19 +85,19 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
public maxLines?: number = undefined
|
public maxLines?: number = undefined
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
public validation?: Record<string, ValidationErrorType[]>
|
public validation?: {[key: string]: ValidationErrorType[] }
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public validationPrefix = ''
|
public validationPrefix: string = ''
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public rows: Array<Record<string, DynamicTableAcceptedTypes>> = []
|
public rows: Array<{ [key: string]: DynamicTableAcceptedTypes }> = []
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
public _rowsById: DynamicTableRowData[] = []
|
public _rowsById: DynamicTableRowData[] = []
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public formName = ''
|
public formName: string = ''
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private _lastRowId = 1
|
private _lastRowId = 1
|
||||||
@ -113,7 +112,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly _getDefaultRow = (): Record<string, DynamicTableAcceptedTypes> => {
|
private readonly _getDefaultRow = (): { [key: string]: DynamicTableAcceptedTypes } => {
|
||||||
this._updateLastRowId()
|
this._updateLastRowId()
|
||||||
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? ''])])
|
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? ''])])
|
||||||
}
|
}
|
||||||
@ -237,7 +236,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
classList.push(...headerCellData.headerClassList)
|
classList.push(...headerCellData.headerClassList)
|
||||||
}
|
}
|
||||||
return html`<th scope="col" class=${classList.join(' ')}>
|
return html`<th scope="col" class=${classList.join(' ')}>
|
||||||
${headerCellData.description ?? ''}
|
${headerCellData.description}
|
||||||
</th>`
|
</th>`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,7 +295,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
const inputId =
|
const inputId =
|
||||||
`peertube-livechat-${this.formName.replace(/_/g, '-')}-${propertyName.toString().replace(/_/g, '-')}-${rowId}`
|
`peertube-livechat-${this.formName.replace(/_/g, '-')}-${propertyName.toString().replace(/_/g, '-')}-${rowId}`
|
||||||
|
|
||||||
const inputTitle: DirectiveResult | undefined = propertySchema.inputTitle ?? this.header[propertyName]?.colName
|
|
||||||
const feedback = this._renderFeedback(inputId, propertyName, originalIndex)
|
const feedback = this._renderFeedback(inputId, propertyName, originalIndex)
|
||||||
|
|
||||||
switch (propertySchema.default?.constructor) {
|
switch (propertySchema.default?.constructor) {
|
||||||
@ -322,7 +320,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderInput(rowId,
|
formElement = html`${this._renderInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
inputTitle,
|
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue as string,
|
propertyValue as string,
|
||||||
@ -335,7 +332,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderTextarea(rowId,
|
formElement = html`${this._renderTextarea(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
inputTitle,
|
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue as string,
|
propertyValue as string,
|
||||||
@ -348,7 +344,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderSelect(rowId,
|
formElement = html`${this._renderSelect(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
inputTitle,
|
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue as string,
|
propertyValue as string,
|
||||||
@ -361,7 +356,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderImageFileInput(rowId,
|
formElement = html`${this._renderImageFileInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
inputTitle,
|
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue?.toString(),
|
propertyValue?.toString(),
|
||||||
@ -382,7 +376,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderInput(rowId,
|
formElement = html`${this._renderInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
inputTitle,
|
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
(propertyValue as Date).toISOString(),
|
(propertyValue as Date).toISOString(),
|
||||||
@ -401,7 +394,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderInput(rowId,
|
formElement = html`${this._renderInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
inputTitle,
|
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue as string,
|
propertyValue as string,
|
||||||
@ -419,7 +411,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderCheckbox(rowId,
|
formElement = html`${this._renderCheckbox(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
inputTitle,
|
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue as boolean,
|
propertyValue as boolean,
|
||||||
@ -455,10 +446,10 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderInput(rowId,
|
formElement = html`${this._renderInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
inputTitle,
|
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
(propertyValue)?.join(propertySchema.separator ?? ',') ?? propertyValue ?? propertySchema.default ?? '',
|
(propertyValue)?.join(propertySchema.separator ?? ',') ??
|
||||||
|
propertyValue ?? propertySchema.default ?? '',
|
||||||
originalIndex)}
|
originalIndex)}
|
||||||
${feedback}
|
${feedback}
|
||||||
`
|
`
|
||||||
@ -470,10 +461,10 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderTextarea(rowId,
|
formElement = html`${this._renderTextarea(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
inputTitle,
|
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
(propertyValue)?.join(propertySchema.separator ?? ',') ?? propertyValue ?? propertySchema.default ?? '',
|
(propertyValue)?.join(propertySchema.separator ?? ',') ??
|
||||||
|
propertyValue ?? propertySchema.default ?? '',
|
||||||
originalIndex)}
|
originalIndex)}
|
||||||
${feedback}
|
${feedback}
|
||||||
`
|
`
|
||||||
@ -485,7 +476,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderTagsInput(rowId,
|
formElement = html`${this._renderTagsInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
inputTitle,
|
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue,
|
propertyValue,
|
||||||
@ -497,10 +487,8 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!formElement) {
|
if (!formElement) {
|
||||||
this.logger.warn(
|
this.logger.warn(`value type '${(propertyValue.constructor.toString())}' is incompatible` +
|
||||||
`value type '${(propertyValue.constructor.toString())}' is incompatible` +
|
`with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`)
|
||||||
`with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const classList = ['form-group']
|
const classList = ['form-group']
|
||||||
@ -513,7 +501,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderInput = (rowId: number,
|
_renderInput = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
inputTitle: string | DirectiveResult | undefined,
|
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: string,
|
propertyValue: string,
|
||||||
@ -528,7 +515,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
title=${ifDefined(inputTitle)}
|
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
list=${ifDefined(propertySchema.datalist ? inputId + '-datalist' : undefined)}
|
list=${ifDefined(propertySchema.datalist ? inputId + '-datalist' : undefined)}
|
||||||
min=${ifDefined(propertySchema.min)}
|
min=${ifDefined(propertySchema.min)}
|
||||||
@ -548,7 +534,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderTagsInput = (rowId: number,
|
_renderTagsInput = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
inputTitle: string | DirectiveResult | undefined,
|
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: Array<string | number>,
|
propertyValue: Array<string | number>,
|
||||||
@ -562,7 +547,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
.inputTitle=${inputTitle as any}
|
.inputPlaceholder=${propertySchema.label as any}
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
.min=${propertySchema.min}
|
.min=${propertySchema.min}
|
||||||
.max=${propertySchema.max}
|
.max=${propertySchema.max}
|
||||||
@ -578,7 +563,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderTextarea = (rowId: number,
|
_renderTextarea = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
inputTitle: string | DirectiveResult | undefined,
|
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: string,
|
propertyValue: string,
|
||||||
@ -592,7 +576,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
title=${ifDefined(inputTitle)}
|
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
min=${ifDefined(propertySchema.min)}
|
min=${ifDefined(propertySchema.min)}
|
||||||
max=${ifDefined(propertySchema.max)}
|
max=${ifDefined(propertySchema.max)}
|
||||||
@ -605,7 +588,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderCheckbox = (rowId: number,
|
_renderCheckbox = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
inputTitle: string | DirectiveResult | undefined,
|
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: boolean,
|
propertyValue: boolean,
|
||||||
@ -620,7 +602,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
title=${ifDefined(inputTitle)}
|
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
||||||
value="1"
|
value="1"
|
||||||
@ -630,7 +611,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderSelect = (rowId: number,
|
_renderSelect = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
inputTitle: string | DirectiveResult | undefined,
|
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: string,
|
propertyValue: string,
|
||||||
@ -643,12 +623,11 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
title=${ifDefined(inputTitle)}
|
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
aria-label=${inputName}
|
aria-label=${inputName}
|
||||||
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
||||||
>
|
>
|
||||||
<option ?selected=${!propertyValue}>${inputTitle ?? ''}</option>
|
<option ?selected=${!propertyValue}>${propertySchema.label ?? 'Choose your option'}</option>
|
||||||
${Object.entries(propertySchema.options ?? {})
|
${Object.entries(propertySchema.options ?? {})
|
||||||
?.map(([value, name]) =>
|
?.map(([value, name]) =>
|
||||||
html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`
|
html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`
|
||||||
@ -659,7 +638,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderImageFileInput = (rowId: number,
|
_renderImageFileInput = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
inputTitle: string | DirectiveResult | undefined,
|
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: string,
|
propertyValue: string,
|
||||||
@ -669,7 +647,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
.name=${inputName}
|
.name=${inputName}
|
||||||
class=${classMap(this._getInputValidationClass(propertyName, originalIndex))}
|
class=${classMap(this._getInputValidationClass(propertyName, originalIndex))}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
.inputTitle=${inputTitle as any}
|
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
||||||
.value=${propertyValue}
|
.value=${propertyValue}
|
||||||
@ -679,7 +656,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getInputValidationClass = (propertyName: string,
|
_getInputValidationClass = (propertyName: string,
|
||||||
originalIndex: number): Record<string, boolean> => {
|
originalIndex: number): { [key: string]: boolean } => {
|
||||||
const validationErrorTypes: ValidationErrorType[] | undefined =
|
const validationErrorTypes: ValidationErrorType[] | undefined =
|
||||||
this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`]
|
this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`]
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ export class HelpButtonElement extends LivechatElement {
|
|||||||
public buttonTitle: string | DirectiveResult = ptTr(LOC_ONLINE_HELP)
|
public buttonTitle: string | DirectiveResult = ptTr(LOC_ONLINE_HELP)
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public page = ''
|
public page: string = ''
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
public url: URL = new URL('https://lmddgtfy.net/')
|
public url: URL = new URL('https://lmddgtfy.net/')
|
||||||
@ -38,7 +38,7 @@ export class HelpButtonElement extends LivechatElement {
|
|||||||
href="${this.url.href}"
|
href="${this.url.href}"
|
||||||
target=_blank
|
target=_blank
|
||||||
title="${this.buttonTitle}"
|
title="${this.buttonTitle}"
|
||||||
class="primary-button orange-button peertube-button-link"
|
class="orange-button peertube-button-link"
|
||||||
>${unsafeHTML(helpButtonSVG())}</a>`
|
>${unsafeHTML(helpButtonSVG())}</a>`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,10 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
|
||||||
/* eslint-disable @stylistic/indent */
|
|
||||||
|
|
||||||
import { LivechatElement } from './livechat'
|
import { LivechatElement } from './livechat'
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
import type { DirectiveResult } from 'lit/directive'
|
|
||||||
import { customElement, property } from 'lit/decorators.js'
|
import { customElement, property } from 'lit/decorators.js'
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js'
|
|
||||||
/**
|
/**
|
||||||
* Special element to upload image files.
|
* Special element to upload image files.
|
||||||
* If no current value, displays an input type="file" field.
|
* If no current value, displays an input type="file" field.
|
||||||
@ -33,16 +29,13 @@ export class ImageFileInputElement extends LivechatElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public maxSize?: number
|
public maxSize?: number
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public inputTitle?: string | DirectiveResult
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public accept: string[] = ['image/jpg', 'image/png', 'image/gif']
|
public accept: string[] = ['image/jpg', 'image/png', 'image/gif']
|
||||||
|
|
||||||
protected override render = (): unknown => {
|
protected override render = (): unknown => {
|
||||||
return html`
|
return html`
|
||||||
${this.value
|
${this.value
|
||||||
? html`<img src=${this.value} alt=${ifDefined(this.inputTitle)} @click=${(ev: Event) => {
|
? html`<img src=${this.value} @click=${(ev: Event) => {
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
const upload: HTMLInputElement | null | undefined = this.parentElement?.querySelector('input[type="file"]')
|
const upload: HTMLInputElement | null | undefined = this.parentElement?.querySelector('input[type="file"]')
|
||||||
upload?.click()
|
upload?.click()
|
||||||
@ -51,7 +44,6 @@ export class ImageFileInputElement extends LivechatElement {
|
|||||||
}
|
}
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
title=${ifDefined(this.inputTitle)}
|
|
||||||
accept="${this.accept.join(',')}"
|
accept="${this.accept.join(',')}"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
style=${this.value ? 'display: none;' : ''}
|
style=${this.value ? 'display: none;' : ''}
|
||||||
|
@ -26,7 +26,7 @@ export class LivechatElement extends LitElement {
|
|||||||
this.logger = this.ptContext.logger.createLogger(this.tagName.toLowerCase())
|
this.logger = this.ptContext.logger.createLogger(this.tagName.toLowerCase())
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override createRenderRoot = (): HTMLElement | DocumentFragment => {
|
protected override createRenderRoot = (): Element | ShadowRoot => {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,6 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
|
||||||
/* eslint-disable @stylistic/indent */
|
|
||||||
|
|
||||||
import { LivechatElement } from './livechat'
|
import { LivechatElement } from './livechat'
|
||||||
import { ptTr } from '../directives/translation'
|
import { ptTr } from '../directives/translation'
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
@ -15,7 +12,6 @@ import { ifDefined } from 'lit/directives/if-defined.js'
|
|||||||
import { classMap } from 'lit/directives/class-map.js'
|
import { classMap } from 'lit/directives/class-map.js'
|
||||||
import { animate, fadeOut, fadeIn } from '@lit-labs/motion'
|
import { animate, fadeOut, fadeIn } from '@lit-labs/motion'
|
||||||
import { repeat } from 'lit/directives/repeat.js'
|
import { repeat } from 'lit/directives/repeat.js'
|
||||||
import type { DirectiveResult } from 'lit/directive'
|
|
||||||
|
|
||||||
// FIXME: find a better way to store this image.
|
// FIXME: find a better way to store this image.
|
||||||
// This content comes from the file assets/images/copy.svg, after svgo cleaning.
|
// This content comes from the file assets/images/copy.svg, after svgo cleaning.
|
||||||
@ -24,11 +20,10 @@ import type { DirectiveResult } from 'lit/directive'
|
|||||||
// Then replace the main color by «currentColor»
|
// Then replace the main color by «currentColor»
|
||||||
const copySVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 4.233 4.233">
|
const copySVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 4.233 4.233">
|
||||||
<g style="stroke-width:1.00021;stroke-miterlimit:4;stroke-dasharray:none">` +
|
<g style="stroke-width:1.00021;stroke-miterlimit:4;stroke-dasharray:none">` +
|
||||||
// eslint-disable-next-line max-len, @stylistic/indent-binary-ops
|
// eslint-disable-next-line max-len
|
||||||
'<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m4.084 4.046-.616.015-.645-.004a.942.942 0 0 1-.942-.942v-4.398a.94.94 0 0 1 .942-.943H7.22a.94.94 0 0 1 .942.943l-.006.334-.08.962" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' +
|
'<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m4.084 4.046-.616.015-.645-.004a.942.942 0 0 1-.942-.942v-4.398a.94.94 0 0 1 .942-.943H7.22a.94.94 0 0 1 .942.943l-.006.334-.08.962" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' +
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
'<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M8.434 5.85c-.422.009-1.338.009-1.76.01-.733.004-2.199 0-2.199 0a.94.94 0 0 1-.942-.941V.52a.94.94 0 0 1 .942-.942h4.398a.94.94 0 0 1 .943.942s.004 1.466 0 2.2c-.003.418-.019 1.251-.006 1.67.024.812-.382 1.439-1.376 1.46z" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' +
|
'<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M8.434 5.85c-.422.009-1.338.009-1.76.01-.733.004-2.199 0-2.199 0a.94.94 0 0 1-.942-.941V.52a.94.94 0 0 1 .942-.942h4.398a.94.94 0 0 1 .943.942s.004 1.466 0 2.2c-.003.418-.019 1.251-.006 1.67.024.812-.382 1.439-1.376 1.46z" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' +
|
||||||
// eslint-disable-next-line @stylistic/indent-binary-ops
|
|
||||||
`</g>
|
`</g>
|
||||||
</svg>`
|
</svg>`
|
||||||
|
|
||||||
@ -53,7 +48,7 @@ export class TagsInputElement extends LivechatElement {
|
|||||||
private _inputValue?: string = ''
|
private _inputValue?: string = ''
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public inputTitle?: string | DirectiveResult = ''
|
public inputPlaceholder?: string = ''
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public datalist?: string[]
|
public datalist?: string[]
|
||||||
@ -68,10 +63,10 @@ export class TagsInputElement extends LivechatElement {
|
|||||||
private readonly _isPressingKey: string[] = []
|
private readonly _isPressingKey: string[] = []
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public separator = '\n'
|
public separator: string = '\n'
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public animDuration = 200
|
public animDuration: number = 200
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overloading the standard focus method.
|
* Overloading the standard focus method.
|
||||||
@ -171,7 +166,7 @@ export class TagsInputElement extends LivechatElement {
|
|||||||
@input=${(e: InputEvent) => this._handleInputEvent(e)}
|
@input=${(e: InputEvent) => this._handleInputEvent(e)}
|
||||||
@change=${(e: Event) => e.stopPropagation()}
|
@change=${(e: Event) => e.stopPropagation()}
|
||||||
.value=${this._inputValue ?? ''}
|
.value=${this._inputValue ?? ''}
|
||||||
title=${ifDefined(this.inputTitle)} />
|
placeholder=${ifDefined(this.inputPlaceholder)} />
|
||||||
${(this.datalist)
|
${(this.datalist)
|
||||||
? html`<datalist id="${this.id ?? 'tags-input'}-datalist">
|
? html`<datalist id="${this.id ?? 'tags-input'}-datalist">
|
||||||
${(this.datalist ?? []).map((value) => html`<option value=${value}>`)}
|
${(this.datalist ?? []).map((value) => html`<option value=${value}>`)}
|
||||||
@ -249,9 +244,8 @@ export class TagsInputElement extends LivechatElement {
|
|||||||
if (!this._isPressingKey.includes(e.key)) {
|
if (!this._isPressingKey.includes(e.key)) {
|
||||||
this._isPressingKey.push(e.key)
|
this._isPressingKey.push(e.key)
|
||||||
|
|
||||||
if (
|
if ((target.selectionStart === target.selectionEnd) &&
|
||||||
(target.selectionStart === target.selectionEnd) && target.selectionStart === 0
|
target.selectionStart === 0) {
|
||||||
) {
|
|
||||||
this._handleDeleteTag((this._searchedTagsIndex.length)
|
this._handleDeleteTag((this._searchedTagsIndex.length)
|
||||||
? this._searchedTagsIndex.slice(-1)[0]
|
? this._searchedTagsIndex.slice(-1)[0]
|
||||||
: (this.value.length - 1))
|
: (this.value.length - 1))
|
||||||
@ -264,9 +258,8 @@ export class TagsInputElement extends LivechatElement {
|
|||||||
if (!this._isPressingKey.includes(e.key)) {
|
if (!this._isPressingKey.includes(e.key)) {
|
||||||
this._isPressingKey.push(e.key)
|
this._isPressingKey.push(e.key)
|
||||||
|
|
||||||
if (
|
if ((target.selectionStart === target.selectionEnd) &&
|
||||||
(target.selectionStart === target.selectionEnd) && target.selectionStart === target.value.length
|
target.selectionStart === target.value.length) {
|
||||||
) {
|
|
||||||
this._handleDeleteTag((this._searchedTagsIndex.length)
|
this._handleDeleteTag((this._searchedTagsIndex.length)
|
||||||
? this._searchedTagsIndex[0]
|
? this._searchedTagsIndex[0]
|
||||||
: 0)
|
: 0)
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
|
||||||
/* eslint-disable @stylistic/indent */
|
|
||||||
|
|
||||||
import type { LivechatTokenListElement } from '../token-list'
|
import type { LivechatTokenListElement } from '../token-list'
|
||||||
import { html, TemplateResult } from 'lit'
|
import { html, TemplateResult } from 'lit'
|
||||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
|
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
|
||||||
@ -26,11 +23,11 @@ export function tplTokenList (el: LivechatTokenListElement): TemplateResult {
|
|||||||
<tbody>
|
<tbody>
|
||||||
${
|
${
|
||||||
repeat(el.tokenList ?? [], (token) => token.id, (token) => {
|
repeat(el.tokenList ?? [], (token) => token.id, (token) => {
|
||||||
let dateStr = ''
|
let dateStr: string = ''
|
||||||
try {
|
try {
|
||||||
const date = new Date(token.date)
|
const date = new Date(token.date)
|
||||||
dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
||||||
} catch (_err) {}
|
} catch (err) {}
|
||||||
return html`<tr>
|
return html`<tr>
|
||||||
<td>${
|
<td>${
|
||||||
el.mode === 'select'
|
el.mode === 'select'
|
||||||
|
@ -27,7 +27,7 @@ export class LivechatTokenListElement extends LivechatElement {
|
|||||||
public currentSelectedToken?: LivechatToken
|
public currentSelectedToken?: LivechatToken
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public actionDisabled = false
|
public actionDisabled: boolean = false
|
||||||
|
|
||||||
private readonly _tokenListService: TokenListService
|
private readonly _tokenListService: TokenListService
|
||||||
private readonly _asyncTaskRender: Task
|
private readonly _asyncTaskRender: Task
|
||||||
@ -83,7 +83,7 @@ export class LivechatTokenListElement extends LivechatElement {
|
|||||||
this.dispatchEvent(new CustomEvent('update', {}))
|
this.dispatchEvent(new CustomEvent('update', {}))
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error(err)
|
this.logger.error(err)
|
||||||
this.ptNotifier.error((err as Error).toString(), await this.ptTranslate(LOC_ERROR))
|
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR))
|
||||||
} finally {
|
} finally {
|
||||||
this.actionDisabled = false
|
this.actionDisabled = false
|
||||||
}
|
}
|
||||||
@ -102,7 +102,7 @@ export class LivechatTokenListElement extends LivechatElement {
|
|||||||
this.dispatchEvent(new CustomEvent('update', {}))
|
this.dispatchEvent(new CustomEvent('update', {}))
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error(err)
|
this.logger.error(err)
|
||||||
this.ptNotifier.error((err as Error).toString(), await this.ptTranslate(LOC_ERROR))
|
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR))
|
||||||
} finally {
|
} finally {
|
||||||
this.actionDisabled = false
|
this.actionDisabled = false
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ export enum ValidationErrorType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ValidationError extends Error {
|
export class ValidationError extends Error {
|
||||||
properties: Record<string, ValidationErrorType[]> = {}
|
properties: {[key: string]: ValidationErrorType[] } = {}
|
||||||
|
|
||||||
constructor (name: string, message: string | undefined, properties: ValidationError['properties']) {
|
constructor (name: string, message: string | undefined, properties: ValidationError['properties']) {
|
||||||
super(message)
|
super(message)
|
||||||
|
@ -27,7 +27,7 @@ type displayButtonOptions = displayButtonOptionsCallback | displayButtonOptionsH
|
|||||||
function displayButton (dbo: displayButtonOptions): void {
|
function displayButton (dbo: displayButtonOptions): void {
|
||||||
const button = document.createElement('a')
|
const button = document.createElement('a')
|
||||||
button.classList.add(
|
button.classList.add(
|
||||||
'primary-button', 'orange-button', 'peertube-button-link',
|
'orange-button', 'peertube-button-link',
|
||||||
'peertube-plugin-livechat-button',
|
'peertube-plugin-livechat-button',
|
||||||
'peertube-plugin-livechat-button-' + dbo.name
|
'peertube-plugin-livechat-button-' + dbo.name
|
||||||
)
|
)
|
||||||
@ -42,23 +42,6 @@ function displayButton (dbo: displayButtonOptions): void {
|
|||||||
if ('href' in dbo) {
|
if ('href' in dbo) {
|
||||||
button.href = dbo.href
|
button.href = dbo.href
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!button.href || button.href === '#') {
|
|
||||||
// No href => it is not a link.
|
|
||||||
button.role = 'button'
|
|
||||||
button.tabIndex = 0
|
|
||||||
|
|
||||||
// We must also ensure that the enter key is triggering the onclick
|
|
||||||
if (button.onclick) {
|
|
||||||
button.onkeydown = ev => {
|
|
||||||
if (ev.key === 'Enter') {
|
|
||||||
ev.preventDefault()
|
|
||||||
button.click()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (('targetBlank' in dbo) && dbo.targetBlank) {
|
if (('targetBlank' in dbo) && dbo.targetBlank) {
|
||||||
button.target = '_blank'
|
button.target = '_blank'
|
||||||
}
|
}
|
||||||
@ -69,10 +52,6 @@ function displayButton (dbo: displayButtonOptions): void {
|
|||||||
tmp.innerHTML = svg.trim()
|
tmp.innerHTML = svg.trim()
|
||||||
const svgDom = tmp.firstChild
|
const svgDom = tmp.firstChild
|
||||||
if (svgDom) {
|
if (svgDom) {
|
||||||
if ('ariaHidden' in (svgDom as HTMLElement)) {
|
|
||||||
// Icon must be hidden for screen readers.
|
|
||||||
(svgDom as HTMLElement).ariaHidden = 'true'
|
|
||||||
}
|
|
||||||
button.prepend(svgDom)
|
button.prepend(svgDom)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -16,6 +16,8 @@ import { localizedHelpUrl } from '../../utils/help'
|
|||||||
import { getBaseRoute } from '../../utils/uri'
|
import { getBaseRoute } from '../../utils/uri'
|
||||||
import { displayConverseJS } from '../../utils/conversejs'
|
import { displayConverseJS } from '../../utils/conversejs'
|
||||||
|
|
||||||
|
let savedMyPluginFlexGrow: string | undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the chat for the current video
|
* Initialize the chat for the current video
|
||||||
* @param video the video
|
* @param video the video
|
||||||
@ -23,6 +25,7 @@ import { displayConverseJS } from '../../utils/conversejs'
|
|||||||
async function initChat (video: Video): Promise<void> {
|
async function initChat (video: Video): Promise<void> {
|
||||||
const ptContext = getPtContext()
|
const ptContext = getPtContext()
|
||||||
const logger = ptContext.logger
|
const logger = ptContext.logger
|
||||||
|
savedMyPluginFlexGrow = undefined
|
||||||
|
|
||||||
if (!video) {
|
if (!video) {
|
||||||
logger.error('No video provided')
|
logger.error('No video provided')
|
||||||
@ -43,8 +46,6 @@ async function initChat (video: Video): Promise<void> {
|
|||||||
container.setAttribute('id', 'peertube-plugin-livechat-container')
|
container.setAttribute('id', 'peertube-plugin-livechat-container')
|
||||||
container.setAttribute('peertube-plugin-livechat-state', 'initializing')
|
container.setAttribute('peertube-plugin-livechat-state', 'initializing')
|
||||||
container.setAttribute('peertube-plugin-livechat-current-url', window.location.href)
|
container.setAttribute('peertube-plugin-livechat-current-url', window.location.href)
|
||||||
container.role = 'region'
|
|
||||||
container.ariaLabel = await ptContext.ptOptions.peertubeHelpers.translate(LOC_CHAT)
|
|
||||||
placeholder.append(container)
|
placeholder.append(container)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -60,8 +61,8 @@ async function initChat (video: Video): Promise<void> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let showShareUrlButton = false
|
let showShareUrlButton: boolean = false
|
||||||
let showPromote = false
|
let showPromote: boolean = false
|
||||||
if (video.isLocal) { // No need for shareButton on remote chats.
|
if (video.isLocal) { // No need for shareButton on remote chats.
|
||||||
const chatShareUrl = settings['chat-share-url'] ?? ''
|
const chatShareUrl = settings['chat-share-url'] ?? ''
|
||||||
if (chatShareUrl === 'everyone') {
|
if (chatShareUrl === 'everyone') {
|
||||||
@ -187,10 +188,9 @@ async function _insertChatDom (
|
|||||||
callback: async () => {
|
callback: async () => {
|
||||||
try {
|
try {
|
||||||
// First we must get the room JID (can be video.uuid@ or channel.id@)
|
// First we must get the room JID (can be video.uuid@ or channel.id@)
|
||||||
const url = getBaseRoute(ptContext.ptOptions) + '/api/configuration/room/' +
|
|
||||||
encodeURIComponent(video.uuid)
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
url,
|
getBaseRoute(ptContext.ptOptions) + '/api/configuration/room/' +
|
||||||
|
encodeURIComponent(video.uuid),
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: peertubeHelpers.getAuthHeader()
|
headers: peertubeHelpers.getAuthHeader()
|
||||||
@ -304,7 +304,7 @@ async function _openChat (video: Video): Promise<void | false> {
|
|||||||
|
|
||||||
// Loading converseJS...
|
// Loading converseJS...
|
||||||
await displayConverseJS(ptContext.ptOptions, container, roomkey, 'peertube-video', false)
|
await displayConverseJS(ptContext.ptOptions, container, roomkey, 'peertube-video', false)
|
||||||
} catch (_err) {
|
} catch (err) {
|
||||||
// Displaying an error page.
|
// Displaying an error page.
|
||||||
if (container) {
|
if (container) {
|
||||||
const message = document.createElement('div')
|
const message = document.createElement('div')
|
||||||
@ -353,6 +353,19 @@ function _hackStyles (on: boolean): void {
|
|||||||
buttons.classList.remove('peertube-plugin-livechat-buttons-open')
|
buttons.classList.remove('peertube-plugin-livechat-buttons-open')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const myPluginPlaceholder: HTMLElement | null = document.querySelector('my-plugin-placeholder')
|
||||||
|
if (on) {
|
||||||
|
// Saving current style attributes and maximazing space for the chat
|
||||||
|
if (myPluginPlaceholder) {
|
||||||
|
savedMyPluginFlexGrow = myPluginPlaceholder.style.flexGrow // Should be "", but can be anything else.
|
||||||
|
myPluginPlaceholder.style.flexGrow = '1'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// restoring values...
|
||||||
|
if (savedMyPluginFlexGrow !== undefined && myPluginPlaceholder) {
|
||||||
|
myPluginPlaceholder.style.flexGrow = savedMyPluginFlexGrow
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
getPtContext().logger.error(`Failed hacking styles: '${err as string}'`)
|
getPtContext().logger.error(`Failed hacking styles: '${err as string}'`)
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import { getIframeUri, getXMPPAddr, UriOptions } from '../uri'
|
|||||||
import { isAnonymousUser } from '../../../utils/user'
|
import { isAnonymousUser } from '../../../utils/user'
|
||||||
|
|
||||||
// First is default tab.
|
// First is default tab.
|
||||||
const validTabNames: string[] = ['embed', 'dock', 'peertube', 'xmpp'] as const
|
const validTabNames = ['embed', 'dock', 'peertube', 'xmpp'] as const
|
||||||
|
|
||||||
type ValidTabNames = typeof validTabNames[number]
|
type ValidTabNames = typeof validTabNames[number]
|
||||||
|
|
||||||
@ -61,49 +61,49 @@ export class ShareChatElement extends LivechatElement {
|
|||||||
* Should we render the XMPP tab?
|
* Should we render the XMPP tab?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public xmppUriEnabled = false
|
public xmppUriEnabled: boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should we render the Dock tab?
|
* Should we render the Dock tab?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public dockEnabled = false
|
public dockEnabled: boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can we use autocolors?
|
* Can we use autocolors?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public autocolorsAvailable = false
|
public autocolorsAvailable: boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In the Embed tab, should we generated an iframe link.
|
* In the Embed tab, should we generated an iframe link.
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public embedIFrame = false
|
public embedIFrame: boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In the Embed tab, should we generated a read-only chat link.
|
* In the Embed tab, should we generated a read-only chat link.
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public embedReadOnly = false
|
public embedReadOnly: boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read-only, with scrollbar?
|
* Read-only, with scrollbar?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public embedReadOnlyScrollbar = false
|
public embedReadOnlyScrollbar: boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read-only, transparent background?
|
* Read-only, transparent background?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public embedReadOnlyTransparentBackground = false
|
public embedReadOnlyTransparentBackground: boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In the Embed tab, should we use current theme color?
|
* In the Embed tab, should we use current theme color?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public embedAutocolors = false
|
public embedAutocolors: boolean = false
|
||||||
|
|
||||||
protected override firstUpdated (changedProperties: PropertyValues): void {
|
protected override firstUpdated (changedProperties: PropertyValues): void {
|
||||||
super.firstUpdated(changedProperties)
|
super.firstUpdated(changedProperties)
|
||||||
@ -156,7 +156,7 @@ export class ShareChatElement extends LivechatElement {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.logger.log('Restoring previous state')
|
this.logger.log('Restoring previous state')
|
||||||
if (validTabNames.includes(v.currentTab as string)) {
|
if (validTabNames.includes(v.currentTab)) {
|
||||||
this.currentTab = v.currentTab
|
this.currentTab = v.currentTab
|
||||||
}
|
}
|
||||||
this.embedIFrame = !!v.embedIFrame
|
this.embedIFrame = !!v.embedIFrame
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
|
||||||
/* eslint-disable @stylistic/indent */
|
|
||||||
|
|
||||||
import type { ShareChatElement } from '../share-chat'
|
import type { ShareChatElement } from '../share-chat'
|
||||||
import { html, TemplateResult } from 'lit'
|
import { html, TemplateResult } from 'lit'
|
||||||
import { ptTr } from '../../../lib/directives/translation'
|
import { ptTr } from '../../../lib/directives/translation'
|
||||||
|
@ -71,7 +71,8 @@ async function shareChatUrl (
|
|||||||
addedNodes.forEach(node => {
|
addedNodes.forEach(node => {
|
||||||
if ((node as HTMLElement).localName === 'ngb-modal-window') {
|
if ((node as HTMLElement).localName === 'ngb-modal-window') {
|
||||||
logger.info('Detecting a new modal, checking if this is the good one...')
|
logger.info('Detecting a new modal, checking if this is the good one...')
|
||||||
const title = (node as HTMLElement).querySelector?.('.modal-title')
|
if (!(node as HTMLElement).querySelector) { return }
|
||||||
|
const title = (node as HTMLElement).querySelector('.modal-title')
|
||||||
if (!(title?.textContent === labelShare)) {
|
if (!(title?.textContent === labelShare)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
||||||
import type { Video } from '@peertube/peertube-types'
|
import type { Video } from '@peertube/peertube-types'
|
||||||
import type { LiveChatSettings } from '../lib/contexts/peertube'
|
|
||||||
import { AutoColors, isAutoColorsAvailable } from 'shared/lib/autocolors'
|
import { AutoColors, isAutoColorsAvailable } from 'shared/lib/autocolors'
|
||||||
import { getBaseRoute } from '../../utils/uri'
|
import { getBaseRoute } from '../../utils/uri'
|
||||||
import { logger } from '../../utils/logger'
|
import { logger } from '../../utils/logger'
|
||||||
@ -18,7 +17,7 @@ interface UriOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getIframeUri (
|
function getIframeUri (
|
||||||
registerOptions: RegisterClientOptions, settings: LiveChatSettings, video: Video, uriOptions: UriOptions = {}
|
registerOptions: RegisterClientOptions, settings: any, video: Video, uriOptions: UriOptions = {}
|
||||||
): string | null {
|
): string | null {
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
logger.error('Settings are not initialized, too soon to compute the iframeUri')
|
logger.error('Settings are not initialized, too soon to compute the iframeUri')
|
||||||
|
@ -156,12 +156,12 @@ function launchTests (): void {
|
|||||||
'content-type': 'application/json;charset=UTF-8'
|
'content-type': 'application/json;charset=UTF-8'
|
||||||
}),
|
}),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
test
|
test: test
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return {
|
return {
|
||||||
test,
|
test: test,
|
||||||
messages: [response.statusText ?? 'Unknown error'],
|
messages: [response.statusText ?? 'Unknown error'],
|
||||||
ok: false
|
ok: false
|
||||||
}
|
}
|
||||||
@ -169,7 +169,7 @@ function launchTests (): void {
|
|||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
if ((typeof data) !== 'object') {
|
if ((typeof data) !== 'object') {
|
||||||
return {
|
return {
|
||||||
test,
|
test: test,
|
||||||
messages: ['Incorrect reponse type: ' + (typeof data)],
|
messages: ['Incorrect reponse type: ' + (typeof data)],
|
||||||
ok: false
|
ok: false
|
||||||
}
|
}
|
||||||
@ -190,7 +190,6 @@ function launchTests (): void {
|
|||||||
waiting.innerHTML = '<i>Testing...</i>'
|
waiting.innerHTML = '<i>Testing...</i>'
|
||||||
ul.append(waiting)
|
ul.append(waiting)
|
||||||
if ((typeof result.next) === 'function') {
|
if ((typeof result.next) === 'function') {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
||||||
const r: Result = (result.next as Function)()
|
const r: Result = (result.next as Function)()
|
||||||
waiting.remove()
|
waiting.remove()
|
||||||
await machine(r)
|
await machine(r)
|
||||||
|
@ -24,41 +24,18 @@ function computeAutoColors (): AutoColors | null {
|
|||||||
const buttonStyles = window.getComputedStyle(button)
|
const buttonStyles = window.getComputedStyle(button)
|
||||||
|
|
||||||
const autocolors: AutoColors = {
|
const autocolors: AutoColors = {
|
||||||
mainForeground: styles.getPropertyValue('--fg').trim() ||
|
mainForeground: styles.getPropertyValue('--mainForegroundColor').trim(),
|
||||||
styles.getPropertyValue('--mainForegroundColor').trim(),
|
mainBackground: styles.getPropertyValue('--mainBackgroundColor').trim(),
|
||||||
|
greyForeground: styles.getPropertyValue('--greyForegroundColor').trim(),
|
||||||
mainBackground: styles.getPropertyValue('--bg').trim() ||
|
greyBackground: styles.getPropertyValue('--greyBackgroundColor').trim(),
|
||||||
styles.getPropertyValue('--mainBackgroundColor').trim(),
|
menuForeground: styles.getPropertyValue('--menuForegroundColor').trim(),
|
||||||
|
menuBackground: styles.getPropertyValue('--menuBackgroundColor').trim(),
|
||||||
greyForeground: styles.getPropertyValue('--fg-300').trim() ||
|
inputForeground: styles.getPropertyValue('--inputForegroundColor').trim(),
|
||||||
styles.getPropertyValue('--greyForegroundColor').trim(),
|
inputBackground: styles.getPropertyValue('--inputBackgroundColor').trim(),
|
||||||
|
buttonForeground: buttonStyles.color.trim(),
|
||||||
greyBackground: styles.getPropertyValue('--bg-secondary-300').trim() ||
|
buttonBackground: styles.getPropertyValue('--mainColor').trim(),
|
||||||
styles.getPropertyValue('--greyBackgroundColor').trim(),
|
link: styles.getPropertyValue('--mainForegroundColor').trim(),
|
||||||
|
linkHover: styles.getPropertyValue('--mainForegroundColor').trim()
|
||||||
menuForeground: styles.getPropertyValue('--fg').trim() ||
|
|
||||||
styles.getPropertyValue('--menuForegroundColor').trim(),
|
|
||||||
|
|
||||||
menuBackground: styles.getPropertyValue('--bg-secondary-400').trim() ||
|
|
||||||
styles.getPropertyValue('--menuBackgroundColor').trim(),
|
|
||||||
|
|
||||||
inputForeground: styles.getPropertyValue('--input-fg').trim() ||
|
|
||||||
styles.getPropertyValue('--inputForegroundColor').trim(),
|
|
||||||
|
|
||||||
inputBackground: styles.getPropertyValue('--input-bg').trim() ||
|
|
||||||
styles.getPropertyValue('--inputBackgroundColor').trim(),
|
|
||||||
|
|
||||||
buttonForeground: styles.getPropertyValue('--on-primary').trim() ||
|
|
||||||
buttonStyles.color.trim(),
|
|
||||||
|
|
||||||
buttonBackground: styles.getPropertyValue('--primary').trim() ||
|
|
||||||
styles.getPropertyValue('--mainColor').trim(),
|
|
||||||
|
|
||||||
link: styles.getPropertyValue('--fg').trim() ||
|
|
||||||
styles.getPropertyValue('--mainForegroundColor').trim(),
|
|
||||||
|
|
||||||
linkHover: styles.getPropertyValue('--fg-400').trim() ||
|
|
||||||
styles.getPropertyValue('--mainForegroundColor').trim()
|
|
||||||
}
|
}
|
||||||
const autoColorsTest = areAutoColorsValid(autocolors)
|
const autoColorsTest = areAutoColorsValid(autocolors)
|
||||||
if (autoColorsTest !== true) {
|
if (autoColorsTest !== true) {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
|
||||||
|
|
||||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
||||||
import type { InitConverseJSParams, ChatPeertubeIncludeMode } from 'shared/lib/types'
|
import type { InitConverseJSParams, ChatPeertubeIncludeMode } from 'shared/lib/types'
|
||||||
@ -18,7 +17,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let pollListenerInitiliazed = false
|
let pollListenerInitiliazed: boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* load the ConverseJS CSS.
|
* load the ConverseJS CSS.
|
||||||
@ -153,11 +152,10 @@ async function displayConverseJS (
|
|||||||
|
|
||||||
const authHeader = peertubeHelpers.getAuthHeader()
|
const authHeader = peertubeHelpers.getAuthHeader()
|
||||||
|
|
||||||
const url = getBaseRoute(clientOptions) + '/api/configuration/room/' +
|
|
||||||
encodeURIComponent(roomKey) +
|
|
||||||
(forceType ? '?forcetype=1' : '')
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
url,
|
getBaseRoute(clientOptions) + '/api/configuration/room/' +
|
||||||
|
encodeURIComponent(roomKey) +
|
||||||
|
(forceType ? '?forcetype=1' : ''),
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: authHeader
|
headers: authHeader
|
||||||
@ -169,7 +167,7 @@ async function displayConverseJS (
|
|||||||
const converseJSParams: InitConverseJSParams = await (response).json()
|
const converseJSParams: InitConverseJSParams = await (response).json()
|
||||||
|
|
||||||
if (!pollListenerInitiliazed) {
|
if (!pollListenerInitiliazed) {
|
||||||
// First time we got here, initialize this event:
|
// First time we got here, initiliaze this event:
|
||||||
const i18nVoteOk = await clientOptions.peertubeHelpers.translate(LOC_POLL_VOTE_OK)
|
const i18nVoteOk = await clientOptions.peertubeHelpers.translate(LOC_POLL_VOTE_OK)
|
||||||
pollListenerInitiliazed = true
|
pollListenerInitiliazed = true
|
||||||
document.addEventListener('livechat-poll-vote', () => {
|
document.addEventListener('livechat-poll-vote', () => {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
||||||
|
|
||||||
function getBaseRoute ({ peertubeHelpers }: RegisterClientOptions, permanent = false): string {
|
function getBaseRoute ({ peertubeHelpers }: RegisterClientOptions, permanent: boolean = false): string {
|
||||||
if (permanent) {
|
if (permanent) {
|
||||||
return '/plugins/livechat/router'
|
return '/plugins/livechat/router'
|
||||||
}
|
}
|
||||||
|
40
conversejs/.eslintrc.json
Normal file
40
conversejs/.eslintrc.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
3
conversejs/.eslintrc.json.license
Normal file
3
conversejs/.eslintrc.json.license
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
@ -4,7 +4,6 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
||||||
const fs = require('node:fs')
|
const fs = require('node:fs')
|
||||||
const path = require('node:path')
|
const path = require('node:path')
|
||||||
const YAML = require('yaml')
|
const YAML = require('yaml')
|
||||||
|
@ -15,26 +15,32 @@ set -x
|
|||||||
|
|
||||||
# Set CONVERSE_VERSION and CONVERSE_REPO to select which repo and tag/commit/branch use.
|
# Set CONVERSE_VERSION and CONVERSE_REPO to select which repo and tag/commit/branch use.
|
||||||
# Defaults values:
|
# Defaults values:
|
||||||
CONVERSE_VERSION="v11.0.0"
|
CONVERSE_VERSION="v10.1.6"
|
||||||
CONVERSE_REPO="https://github.com/conversejs/converse.js.git"
|
CONVERSE_REPO="https://github.com/conversejs/converse.js.git"
|
||||||
# You can eventually set CONVERSE_COMMIT to a specific commit ID, if you want to apply some patches.
|
# You can eventually set CONVERSE_COMMIT to a specific commit ID, if you want to apply some patches.
|
||||||
# 2024-09-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"
|
|
||||||
# # CONVERSE_COMMIT="4402fcc3fc60f6c9334f86528c33a0b463371d12"
|
|
||||||
# CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js"
|
|
||||||
# CONVERSE_COMMIT="xxxx"
|
|
||||||
|
|
||||||
# 2024-09-03: include badges short label and quick fix for sendMessage button
|
|
||||||
CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js"
|
|
||||||
CONVERSE_VERSION="livechat-12.0.1"
|
|
||||||
CONVERSE_COMMIT=""
|
CONVERSE_COMMIT=""
|
||||||
# 2024-12-05: fix emojis (don't want some upstream feature, but want this fix)
|
|
||||||
# CONVERSE_COMMIT="366a718f16e96c88c6d768722f1525801a2f0c4d"
|
# 2014-01-16: we are using a custom version, to wait for some PR to be apply upstream.
|
||||||
|
# This version includes following changes:
|
||||||
|
# - #converse.js/3300: Adding the maxWait option for `debouncedPruneHistory`
|
||||||
|
# - #converse.js/3302: debounce MUC sidebar rendering
|
||||||
|
# - Fix: refresh the MUC sidebar when participants collection is sorted
|
||||||
|
# - Fix: MUC occupant list does not sort itself on nicknames or roles changes
|
||||||
|
# - Fix inconsistency between browsers on textarea outlines
|
||||||
|
# - Fix: room information not correctly refreshed when modifications are made by other users
|
||||||
|
# This version already includes following changes that will not be merged in ConverseJS upstream:
|
||||||
|
# - Don't load vCards for all room occupants when the right menu is closed
|
||||||
|
# - Changing the default avatar, for something very light (to mitigate blinking effect when vCards are loaded)
|
||||||
|
# - Custom settings livechat_load_all_vcards for the readonly mode
|
||||||
|
# - Adding "users" icon in the menu toggle button
|
||||||
|
# - Removing unecessary plugins: headless/pubsub, minimize, notifications, profile, omemo, push, roomlist, dragresize.
|
||||||
|
# - Destroy room: remove the challenge, and the new JID
|
||||||
|
# - New config option [colorize_username](https://conversejs.org/docs/html/configuration.html#colorize_username)
|
||||||
|
# - New loadEmojis hook, to customize emojis at runtime.
|
||||||
|
# - Fix custom emojis path when assets_path is not the default path.
|
||||||
|
CONVERSE_VERSION="livechat-10.1.0"
|
||||||
|
# CONVERSE_COMMIT="4402fcc3fc60f6c9334f86528c33a0b463371d12"
|
||||||
|
CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js"
|
||||||
|
|
||||||
rootdir="$(pwd)"
|
rootdir="$(pwd)"
|
||||||
src_dir="$rootdir/conversejs"
|
src_dir="$rootdir/conversejs"
|
||||||
@ -44,7 +50,6 @@ if [ -n "$CONVERSE_COMMIT" ]; then
|
|||||||
fi
|
fi
|
||||||
converse_build_dir="$rootdir/build/conversejs"
|
converse_build_dir="$rootdir/build/conversejs"
|
||||||
converse_destination_dir="$rootdir/dist/client/conversejs"
|
converse_destination_dir="$rootdir/dist/client/conversejs"
|
||||||
converse_emoji_destination="$rootdir/dist/converse-emoji.json"
|
|
||||||
|
|
||||||
if [[ ! -d $src_dir ]]; then
|
if [[ ! -d $src_dir ]]; then
|
||||||
echo "$0 must be called from the plugin livechat root dir."
|
echo "$0 must be called from the plugin livechat root dir."
|
||||||
@ -124,9 +129,6 @@ cd $rootdir
|
|||||||
echo "Copying ConverseJS dist files..."
|
echo "Copying ConverseJS dist files..."
|
||||||
mkdir -p "$converse_destination_dir" && cp -r $converse_build_dir/dist/* "$converse_destination_dir/"
|
mkdir -p "$converse_destination_dir" && cp -r $converse_build_dir/dist/* "$converse_destination_dir/"
|
||||||
|
|
||||||
echo "Copying ConverseJS original emoji.json file..." # this is needed for some backend code.
|
|
||||||
cp "$converse_build_dir/src/headless/plugins/emoji/emoji.json" "$converse_emoji_destination"
|
|
||||||
|
|
||||||
echo "ConverseJS OK."
|
echo "ConverseJS OK."
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
|
||||||
|
|
||||||
import type { InitConverseJSParams, ChatIncludeMode, ExternalAuthResult } from 'shared/lib/types'
|
import type { InitConverseJSParams, ChatIncludeMode, ExternalAuthResult } from 'shared/lib/types'
|
||||||
import { inIframe } from './lib/utils'
|
import { inIframe } from './lib/utils'
|
||||||
import { initDom } from './lib/dom'
|
import { initDom } from './lib/dom'
|
||||||
@ -23,7 +21,6 @@ import { livechatViewerModePlugin } from './lib/plugins/livechat-viewer-mode'
|
|||||||
import { livechatMiniMucHeadPlugin } from './lib/plugins/livechat-mini-muc-head'
|
import { livechatMiniMucHeadPlugin } from './lib/plugins/livechat-mini-muc-head'
|
||||||
import { livechatEmojisPlugin } from './lib/plugins/livechat-emojis'
|
import { livechatEmojisPlugin } from './lib/plugins/livechat-emojis'
|
||||||
import { moderationDelayPlugin } from './lib/plugins/moderation-delay'
|
import { moderationDelayPlugin } from './lib/plugins/moderation-delay'
|
||||||
import { livechatAnnouncementsPlugin } from './lib/plugins/livechat-announcements'
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -37,18 +34,11 @@ declare global {
|
|||||||
env: {
|
env: {
|
||||||
html: Function
|
html: Function
|
||||||
sizzle: Function
|
sizzle: Function
|
||||||
dayjs: Function
|
|
||||||
__: Function
|
|
||||||
u: {
|
|
||||||
hasClass: Function
|
|
||||||
addClass: Function
|
|
||||||
removeClass: Function
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initConversePlugins: typeof initConversePlugins
|
initConversePlugins: typeof initConversePlugins
|
||||||
initConverse: typeof initConverse
|
initConverse: typeof initConverse
|
||||||
reconnectConverse?: (params: any) => void
|
reconnectConverse?: (room: string) => void
|
||||||
externalAuthGetResult?: (data: ExternalAuthResult) => void
|
externalAuthGetResult?: (data: ExternalAuthResult) => void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,8 +73,6 @@ function initConversePlugins (peertubeEmbedded: boolean): void {
|
|||||||
converse.plugins.add('livechatViewerModePlugin', livechatViewerModePlugin)
|
converse.plugins.add('livechatViewerModePlugin', livechatViewerModePlugin)
|
||||||
|
|
||||||
converse.plugins.add('converse-moderation-delay', moderationDelayPlugin)
|
converse.plugins.add('converse-moderation-delay', moderationDelayPlugin)
|
||||||
|
|
||||||
converse.plugins.add('livechatAnnouncementsPlugin', livechatAnnouncementsPlugin)
|
|
||||||
}
|
}
|
||||||
window.initConversePlugins = initConversePlugins
|
window.initConversePlugins = initConversePlugins
|
||||||
|
|
||||||
@ -97,7 +85,7 @@ window.initConversePlugins = initConversePlugins
|
|||||||
async function initConverse (
|
async function initConverse (
|
||||||
initConverseParams: InitConverseJSParams,
|
initConverseParams: InitConverseJSParams,
|
||||||
chatIncludeMode: ChatIncludeMode = 'chat-only',
|
chatIncludeMode: ChatIncludeMode = 'chat-only',
|
||||||
peertubeAuthHeader?: Record<string, string> | null
|
peertubeAuthHeader?: { [header: string]: string } | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// First, fixing relative websocket urls.
|
// First, fixing relative websocket urls.
|
||||||
if (initConverseParams.localWebsocketServiceUrl?.startsWith('/')) {
|
if (initConverseParams.localWebsocketServiceUrl?.startsWith('/')) {
|
||||||
@ -132,9 +120,9 @@ async function initConverse (
|
|||||||
params.view_mode = chatIncludeMode === 'chat-only' ? 'fullscreen' : 'embedded'
|
params.view_mode = chatIncludeMode === 'chat-only' ? 'fullscreen' : 'embedded'
|
||||||
params.allow_url_history_change = chatIncludeMode === 'chat-only'
|
params.allow_url_history_change = chatIncludeMode === 'chat-only'
|
||||||
|
|
||||||
let isAuthenticated = false
|
let isAuthenticated: boolean = false
|
||||||
let isAuthenticatedWithExternalAccount = false
|
let isAuthenticatedWithExternalAccount: boolean = false
|
||||||
let isRemoteWithNicknameSet = false
|
let isRemoteWithNicknameSet: boolean = false
|
||||||
|
|
||||||
// OIDC (OpenID Connect):
|
// OIDC (OpenID Connect):
|
||||||
const tryOIDC = (initConverseParams.externalAuthOIDC?.length ?? 0) > 0
|
const tryOIDC = (initConverseParams.externalAuthOIDC?.length ?? 0) > 0
|
||||||
@ -230,24 +218,20 @@ async function initConverse (
|
|||||||
// * mode === chat-only + !transparent + !readonly + is using a livechat token
|
// * mode === chat-only + !transparent + !readonly + is using a livechat token
|
||||||
// Technically it would work in 'chat-only' mode, but i don't want to add too many things to test
|
// Technically it would work in 'chat-only' mode, but i don't want to add too many things to test
|
||||||
// (and i now there is some CSS bugs in the task list).
|
// (and i now there is some CSS bugs in the task list).
|
||||||
// Same for the moderator notes app.
|
let enableTask = false
|
||||||
let enableApps = false
|
|
||||||
if (chatIncludeMode === 'peertube-video' || chatIncludeMode === 'peertube-fullpage') {
|
if (chatIncludeMode === 'peertube-video' || chatIncludeMode === 'peertube-fullpage') {
|
||||||
enableApps = true
|
enableTask = true
|
||||||
} else if (
|
} else if (
|
||||||
chatIncludeMode === 'chat-only' &&
|
chatIncludeMode === 'chat-only' &&
|
||||||
usedLivechatToken &&
|
usedLivechatToken &&
|
||||||
!initConverseParams.transparent &&
|
!initConverseParams.transparent &&
|
||||||
!initConverseParams.forceReadonly
|
!initConverseParams.forceReadonly
|
||||||
) {
|
) {
|
||||||
enableApps = true
|
enableTask = true
|
||||||
}
|
}
|
||||||
if (enableApps) {
|
if (enableTask) {
|
||||||
params.livechat_task_app_enabled = true
|
params.livechat_task_app_enabled = true
|
||||||
params.livechat_task_app_restore = chatIncludeMode === 'peertube-fullpage' || chatIncludeMode === 'chat-only'
|
params.livechat_task_app_restore = chatIncludeMode === 'peertube-fullpage' || chatIncludeMode === 'chat-only'
|
||||||
params.livechat_note_app_enabled = true
|
|
||||||
params.livechat_note_app_restore = chatIncludeMode === 'peertube-fullpage' || chatIncludeMode === 'chat-only'
|
|
||||||
params.livechat_mam_search_app_enabled = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -8,13 +8,14 @@
|
|||||||
* @description This files will override the original ConverseJS index.js file.
|
* @description This files will override the original ConverseJS index.js file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'shared/styles/index.scss'
|
import '@converse/headless'
|
||||||
|
|
||||||
import './i18n/index.js'
|
import './i18n/index.js'
|
||||||
import 'shared/registry.js'
|
import 'shared/registry.js'
|
||||||
import { CustomElement } from 'shared/components/element'
|
import { CustomElement } from 'shared/components/element'
|
||||||
import { VIEW_PLUGINS } from './shared/constants.js'
|
import { VIEW_PLUGINS } from './shared/constants.js'
|
||||||
import { _converse, converse } from '@converse/headless'
|
import { _converse, converse } from '@converse/headless/core'
|
||||||
|
|
||||||
|
import 'shared/styles/index.scss'
|
||||||
|
|
||||||
/* START: Removable plugins
|
/* START: Removable plugins
|
||||||
* ------------------------
|
* ------------------------
|
||||||
@ -44,16 +45,11 @@ import './plugins/singleton/index.js'
|
|||||||
import './plugins/fullscreen/index.js'
|
import './plugins/fullscreen/index.js'
|
||||||
|
|
||||||
import '../custom/plugins/size/index.js'
|
import '../custom/plugins/size/index.js'
|
||||||
import '../custom/plugins/mam-search/index.js'
|
|
||||||
import '../custom/plugins/notes/index.js'
|
|
||||||
import '../custom/plugins/tasks/index.js'
|
import '../custom/plugins/tasks/index.js'
|
||||||
import '../custom/plugins/terms/index.js'
|
import '../custom/plugins/terms/index.js'
|
||||||
import '../custom/plugins/poll/index.js'
|
import '../custom/plugins/poll/index.js'
|
||||||
/* END: Removable components */
|
/* END: Removable components */
|
||||||
|
|
||||||
// Running some specific livechat patches:
|
|
||||||
import '../custom/livechat-patch-vcard.js'
|
|
||||||
|
|
||||||
import { CORE_PLUGINS } from './headless/shared/constants.js'
|
import { CORE_PLUGINS } from './headless/shared/constants.js'
|
||||||
import { ROOM_FEATURES } from './headless/plugins/muc/constants.js'
|
import { ROOM_FEATURES } from './headless/plugins/muc/constants.js'
|
||||||
// We must add our custom plugins to CORE_PLUGINS (so it is white listed):
|
// We must add our custom plugins to CORE_PLUGINS (so it is white listed):
|
||||||
@ -61,14 +57,11 @@ CORE_PLUGINS.push('livechat-converse-size')
|
|||||||
CORE_PLUGINS.push('livechat-converse-tasks')
|
CORE_PLUGINS.push('livechat-converse-tasks')
|
||||||
CORE_PLUGINS.push('livechat-converse-terms')
|
CORE_PLUGINS.push('livechat-converse-terms')
|
||||||
CORE_PLUGINS.push('livechat-converse-poll')
|
CORE_PLUGINS.push('livechat-converse-poll')
|
||||||
CORE_PLUGINS.push('livechat-converse-notes')
|
|
||||||
CORE_PLUGINS.push('livechat-converse-mam-search')
|
|
||||||
// We must also add our custom ROOM_FEATURES, so that they correctly resets
|
// We must also add our custom ROOM_FEATURES, so that they correctly resets
|
||||||
// (see headless/plugins/muc, getDiscoInfoFeatures, which loops on this const)
|
// (see headless/plugins/muc, getDiscoInfoFeatures, which loops on this const)
|
||||||
ROOM_FEATURES.push('x_peertubelivechat_mute_anonymous')
|
ROOM_FEATURES.push('x_peertubelivechat_mute_anonymous')
|
||||||
ROOM_FEATURES.push('x_peertubelivechat_emoji_only_mode')
|
|
||||||
|
|
||||||
_converse.exports.CustomElement = CustomElement
|
_converse.CustomElement = CustomElement
|
||||||
|
|
||||||
const initialize = converse.initialize
|
const initialize = converse.initialize
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { api } from '@converse/headless/index.js'
|
import { api } from '@converse/headless/core.js'
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
import { CustomElement } from 'shared/components/element.js'
|
||||||
import { tplExternalLoginModal } from 'templates/livechat-external-login-modal.js'
|
import { tplExternalLoginModal } from 'templates/livechat-external-login-modal.js'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
// Here we are patching the vCard plugin, to add some specific optimizations.
|
|
||||||
|
|
||||||
import { _converse, api } from '@converse/headless/index.js'
|
|
||||||
import {
|
|
||||||
onOccupantAvatarChanged,
|
|
||||||
setVCardOnModel,
|
|
||||||
setVCardOnOccupant
|
|
||||||
} from '@converse/headless/plugins/vcard/utils.js'
|
|
||||||
|
|
||||||
const pluginDefinition = _converse.pluggable.plugins['converse-vcard']
|
|
||||||
const originalInitialize = pluginDefinition.initialize
|
|
||||||
|
|
||||||
pluginDefinition.initialize = function initialize () {
|
|
||||||
const previousListeners = _converse._events.chatRoomInitialized ?? []
|
|
||||||
originalInitialize.apply(this)
|
|
||||||
|
|
||||||
_converse.api.settings.extend({
|
|
||||||
livechat_load_all_vcards: false
|
|
||||||
})
|
|
||||||
|
|
||||||
// Now we must detect the new chatRoomInitialized listener, and remove it:
|
|
||||||
const listenersToRemove = []
|
|
||||||
for (const def of _converse._events.chatRoomInitialized ?? []) {
|
|
||||||
if (def.callback && !previousListeners.includes(def.callback)) {
|
|
||||||
listenersToRemove.push(def.callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const callback of listenersToRemove) {
|
|
||||||
console.debug('Livechat patching vcard: we must remove this listener', callback)
|
|
||||||
api.listen.not('chatRoomInitialized', callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adding the new listener:
|
|
||||||
api.listen.on('chatRoomInitialized', (m) => {
|
|
||||||
console.debug('Patched version of the vcard chatRoomInitialized event.')
|
|
||||||
setVCardOnModel(m)
|
|
||||||
|
|
||||||
// loadAll: when in readonly mode (ie: OBS integration), always load all avatars.
|
|
||||||
const loadAll = api.settings.get('livechat_load_all_vcards') === true
|
|
||||||
let hiddenOccupants = m.get('hidden_occupants')
|
|
||||||
if (hiddenOccupants !== true || loadAll) {
|
|
||||||
m.occupants.forEach(setVCardOnOccupant)
|
|
||||||
}
|
|
||||||
m.listenTo(m.occupants, 'add', (occupant) => {
|
|
||||||
if (hiddenOccupants !== true || loadAll) {
|
|
||||||
setVCardOnOccupant(occupant)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
m.on('change:hidden_occupants', () => {
|
|
||||||
hiddenOccupants = m.get('hidden_occupants')
|
|
||||||
if (hiddenOccupants !== true || loadAll) {
|
|
||||||
m.occupants.forEach(setVCardOnOccupant)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
m.listenTo(m.occupants, 'change:image_hash', o => onOccupantAvatarChanged(o))
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { api, converse } from '../../../src/headless/index.js'
|
|
||||||
import { XMLNS_MAM_SEARCH } from './constants.js'
|
|
||||||
|
|
||||||
const env = converse.env
|
|
||||||
const {
|
|
||||||
$iq,
|
|
||||||
Strophe,
|
|
||||||
sizzle,
|
|
||||||
log,
|
|
||||||
TimeoutError,
|
|
||||||
__,
|
|
||||||
u
|
|
||||||
} = env
|
|
||||||
const NS = Strophe.NS
|
|
||||||
|
|
||||||
async function query (options) {
|
|
||||||
if (!api.connection.connected()) {
|
|
||||||
throw new Error('Can\'t call `api.livechat_mam_search.query` before having established an XMPP session')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options?.room) {
|
|
||||||
throw new Error('api.livechat_mam_search.query: Missing room parameter.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const attrs = {
|
|
||||||
type: 'set',
|
|
||||||
to: options.room
|
|
||||||
}
|
|
||||||
|
|
||||||
const jid = attrs.to
|
|
||||||
const supported = await api.disco.supports(XMLNS_MAM_SEARCH, jid)
|
|
||||||
if (!supported) {
|
|
||||||
log.warn(`Did not search MAM archive for ${jid} because it doesn't support ${XMLNS_MAM_SEARCH}`)
|
|
||||||
return { messages: [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryid = u.getUniqueId()
|
|
||||||
const stanza = $iq(attrs).c('query', { xmlns: XMLNS_MAM_SEARCH, queryid: queryid })
|
|
||||||
|
|
||||||
stanza.c('x', { xmlns: NS.XFORM, type: 'submit' })
|
|
||||||
.c('field', { var: 'FORM_TYPE', type: 'hidden' })
|
|
||||||
.c('value').t(XMLNS_MAM_SEARCH).up().up()
|
|
||||||
|
|
||||||
if (options.from) {
|
|
||||||
stanza.c('field', { var: 'from' }).c('value')
|
|
||||||
.t(options.from).up().up()
|
|
||||||
}
|
|
||||||
if (options.occupant_id) {
|
|
||||||
stanza.c('field', { var: 'occupant_id' }).c('value')
|
|
||||||
.t(options.occupant_id).up().up()
|
|
||||||
}
|
|
||||||
stanza.up()
|
|
||||||
|
|
||||||
// TODO: handle RSM (pagination.)
|
|
||||||
|
|
||||||
const connection = api.connection.get()
|
|
||||||
|
|
||||||
const messages = []
|
|
||||||
const messageHandler = connection.addHandler((stanza) => {
|
|
||||||
const result = sizzle(`message > result[xmlns="${NS.MAM}"]`, stanza).pop()
|
|
||||||
if (result === undefined || result.getAttribute('queryid') !== queryid) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
const from = stanza.getAttribute('from')
|
|
||||||
if (from !== attrs.to) {
|
|
||||||
log.warn(`Ignoring alleged groupchat MAM message from ${from}`)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
messages.push(stanza)
|
|
||||||
return true
|
|
||||||
}, NS.MAM)
|
|
||||||
|
|
||||||
let error
|
|
||||||
const timeout = api.settings.get('message_archiving_timeout')
|
|
||||||
const iqResult = await api.sendIQ(stanza, timeout, false)
|
|
||||||
|
|
||||||
if (iqResult === null) {
|
|
||||||
const errMsg = __('Timeout while trying to fetch archived messages.')
|
|
||||||
log.error(errMsg)
|
|
||||||
error = new TimeoutError(errMsg)
|
|
||||||
return { messages, error }
|
|
||||||
} else if (u.isErrorStanza(iqResult)) {
|
|
||||||
const errMsg = __('An error occurred while querying for archived messages.')
|
|
||||||
log.error(errMsg)
|
|
||||||
log.error(iqResult)
|
|
||||||
error = new Error(errMsg)
|
|
||||||
return { messages, error }
|
|
||||||
}
|
|
||||||
connection.deleteHandler(messageHandler)
|
|
||||||
|
|
||||||
return { messages }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function showMessagesFrom (occupant) {
|
|
||||||
const appElement = document.querySelector('livechat-converse-muc-mam-search-app')
|
|
||||||
if (!appElement) {
|
|
||||||
throw new Error('Cant find Search App Element')
|
|
||||||
}
|
|
||||||
appElement.searchFrom(occupant)
|
|
||||||
await appElement.showApp()
|
|
||||||
await appElement.updateComplete // waiting for the app to be open
|
|
||||||
return appElement
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
query,
|
|
||||||
showMessagesFrom
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { api } from '@converse/headless'
|
|
||||||
import { parseMUCMessage } from '@converse/headless/plugins/muc/parsers.js'
|
|
||||||
import { MUCApp } from '../../../shared/components/muc-app/index.js'
|
|
||||||
import { tplMamSearchApp } from '../templates/muc-mam-search-app.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom Element to display the Mam Search Application.
|
|
||||||
*/
|
|
||||||
export default class MUCMamSearchApp extends MUCApp {
|
|
||||||
restoreSettingName = undefined
|
|
||||||
sessionStorageRestoreKey = undefined
|
|
||||||
|
|
||||||
static get properties () {
|
|
||||||
return {
|
|
||||||
model: { type: Object, attribute: true }, // the muc model
|
|
||||||
occupant: { type: Object, attribute: true }, // the occupant to search (can be undefined if no current search)
|
|
||||||
results: { type: Object, attribute: true } // a Collection with the results.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return tplMamSearchApp(this, this.model, this.occupant)
|
|
||||||
}
|
|
||||||
|
|
||||||
searchFrom (occupant) {
|
|
||||||
this.results = undefined
|
|
||||||
this.occupant = occupant
|
|
||||||
const p = api.livechat_mam_search.query({
|
|
||||||
room: this.model.get('jid'),
|
|
||||||
// FIXME: shouldn't we escape the nick? cant see any code that escapes it in Converse.
|
|
||||||
from: occupant.get('from') || this.model.get('jid') + '/' + (occupant.get('nick') ?? ''),
|
|
||||||
occupant_id: occupant.get('occupant_id')
|
|
||||||
})
|
|
||||||
|
|
||||||
// don't wait the result to show something! (there will be a spinner)
|
|
||||||
p.then(async (results) => {
|
|
||||||
this.occupant = occupant // in case user did simultaneous requests
|
|
||||||
|
|
||||||
const messages = await Promise.all(results.messages.map(s => parseMUCMessage(s, this.model)))
|
|
||||||
// Note: we are not using MUCMessage objects, because we don't want the objects
|
|
||||||
// used here to interract with objects in the chat rooms.
|
|
||||||
// We could have a lot of unwanted sideeffects.
|
|
||||||
this.results = messages.reverse()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.elements.define('livechat-converse-muc-mam-search-app', MUCMamSearchApp)
|
|
@ -1,82 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
|
||||||
import { tplMucMamSearchMessage } from '../templates/muc-mam-search-message.js'
|
|
||||||
import { api } from '@converse/headless'
|
|
||||||
|
|
||||||
import '../styles/muc-mam-search-message.scss'
|
|
||||||
|
|
||||||
export default class MUCMamSearchMessageView extends CustomElement {
|
|
||||||
static get properties () {
|
|
||||||
return {
|
|
||||||
message: { type: Object, attribute: true }, // /!\ this is not a model
|
|
||||||
mucModel: { type: Object, attribute: true },
|
|
||||||
searchOccupantModel: { type: Object, attribute: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize () {
|
|
||||||
this.listenTo(this.mucModel, 'change', () => this.requestUpdate())
|
|
||||||
this.listenTo(this.searchOccupantModel, 'change', () => this.requestUpdate())
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return tplMucMamSearchMessage(this, this.mucModel, this.searchOccupantModel, this.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
getMessageOccupant () {
|
|
||||||
const occupants = this.mucModel?.occupants
|
|
||||||
if (!occupants?.findOccupant) { return undefined }
|
|
||||||
|
|
||||||
const nick = this.message.nick
|
|
||||||
const jid = this.message.from
|
|
||||||
const occupantId = this.message.occupant_id
|
|
||||||
|
|
||||||
if (!nick && !jid && !occupantId) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
if (occupantId) {
|
|
||||||
const o = occupants.findOccupant({ occupant_id: occupantId })
|
|
||||||
if (o) {
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jid) {
|
|
||||||
const o = occupants.findOccupant({
|
|
||||||
jid,
|
|
||||||
nick
|
|
||||||
})
|
|
||||||
if (o) {
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we don't find it, maybe it is a user that has spoken a long time ago (or never spoked).
|
|
||||||
// In such case, we must create a dummy occupant:
|
|
||||||
const o = occupants.create({
|
|
||||||
nick,
|
|
||||||
occupant_id: occupantId,
|
|
||||||
jid
|
|
||||||
})
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
getDateTime () {
|
|
||||||
if (!this.message.time) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const d = new Date(this.message.time)
|
|
||||||
return d.toLocaleDateString() + ' - ' + d.toLocaleTimeString()
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.elements.define('livechat-converse-muc-mam-search-message', MUCMamSearchMessageView)
|
|
@ -1,28 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
|
||||||
import { tplMucMamSearchOccupant } from '../templates/muc-mam-search-occupant'
|
|
||||||
import { api } from '@converse/headless'
|
|
||||||
|
|
||||||
import '../styles/muc-mam-search-occupant.scss'
|
|
||||||
|
|
||||||
export default class MUCMamSearchOccupantView extends CustomElement {
|
|
||||||
static get properties () {
|
|
||||||
return {
|
|
||||||
model: { type: Object, attribute: true },
|
|
||||||
message: { type: Object, attribute: true } // optional message.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize () {
|
|
||||||
this.listenTo(this.model, 'change', () => this.requestUpdate())
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return tplMucMamSearchOccupant(this, this.model, this.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.elements.define('livechat-converse-muc-mam-search-occupant', MUCMamSearchOccupantView)
|
|
@ -1,5 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
export const XMLNS_MAM_SEARCH = 'urn:xmpp:mam:2#x-search'
|
|
@ -1,33 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { api, converse } from '../../../src/headless/index.js'
|
|
||||||
import { getMessageActionButtons, getOccupantActionButtons } from './utils.js'
|
|
||||||
import mamSearchApi from './api.js'
|
|
||||||
|
|
||||||
import './components/muc-mam-search-app-view.js'
|
|
||||||
import './components/muc-mam-search-occupant-view.js'
|
|
||||||
import './components/muc-mam-search-message-view.js'
|
|
||||||
|
|
||||||
converse.plugins.add('livechat-converse-mam-search', {
|
|
||||||
dependencies: ['converse-muc', 'converse-muc-views'],
|
|
||||||
async initialize () {
|
|
||||||
const _converse = this._converse
|
|
||||||
|
|
||||||
Object.assign(api, {
|
|
||||||
livechat_mam_search: mamSearchApi
|
|
||||||
})
|
|
||||||
|
|
||||||
_converse.api.settings.extend({
|
|
||||||
livechat_mam_search_app_enabled: false
|
|
||||||
})
|
|
||||||
|
|
||||||
// Adding buttons on messages:
|
|
||||||
_converse.api.listen.on('getMessageActionButtons', getMessageActionButtons)
|
|
||||||
// Adding buttons on occupants:
|
|
||||||
_converse.api.listen.on('getOccupantActionButtons', getOccupantActionButtons)
|
|
||||||
|
|
||||||
// FIXME: should we listen to any event (feature/affiliation change?, mam_enabled?) to refresh messageActionButtons?
|
|
||||||
}
|
|
||||||
})
|
|
@ -1,31 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
.conversejs {
|
|
||||||
livechat-converse-muc-mam-search-message {
|
|
||||||
border: 1px solid var(--chatroom-head-bg-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
display: block;
|
|
||||||
margin: 0.25em 0;
|
|
||||||
padding: 0.25em;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
converse-rich-text {
|
|
||||||
color: var(--message-text-color);
|
|
||||||
font-size: var(--message-font-size);
|
|
||||||
padding: 0;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.livechat-message-date {
|
|
||||||
font-size: 0.75em;
|
|
||||||
list-style: none;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
.conversejs {
|
|
||||||
livechat-converse-muc-mam-search-occupant {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.25em;
|
|
||||||
|
|
||||||
& > a {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-left: 0.5em;
|
|
||||||
max-width: 200px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& > ul {
|
|
||||||
font-weight: lighter;
|
|
||||||
font-size: 0.75em;
|
|
||||||
list-style: none;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
// 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 { converseLocalizedHelpUrl } from '../../../shared/lib/help'
|
|
||||||
import { tplMUCApp } from '../../../shared/components/muc-app/templates/muc-app.js'
|
|
||||||
import { html } from 'lit'
|
|
||||||
import { repeat } from 'lit/directives/repeat.js'
|
|
||||||
import { __ } from 'i18n'
|
|
||||||
|
|
||||||
function tplContent (el, mucModel, occupantModel) {
|
|
||||||
return html`
|
|
||||||
${
|
|
||||||
occupantModel
|
|
||||||
? html`
|
|
||||||
<livechat-converse-muc-mam-search-occupant
|
|
||||||
.model=${occupantModel}
|
|
||||||
></livechat-converse-muc-mam-search-occupant>
|
|
||||||
`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
<hr>
|
|
||||||
${
|
|
||||||
el.results
|
|
||||||
? repeat(el.results, (message) => message.id, message => {
|
|
||||||
return html`<livechat-converse-muc-mam-search-message
|
|
||||||
.message=${message} .mucModel=${mucModel} .searchOccupantModel=${occupantModel}
|
|
||||||
></livechat-converse-muc-mam-search-message>`
|
|
||||||
})
|
|
||||||
: html`<livechat-spinner></livechat-spinner>`
|
|
||||||
}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tplMamSearchApp (el, mucModel, occupantModel) {
|
|
||||||
if (!mucModel) {
|
|
||||||
// should not happen
|
|
||||||
return html``
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!el.show) {
|
|
||||||
return html``
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const i18nSearch = __(LOC_message_search)
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const i18nHelp = __(LOC_online_help)
|
|
||||||
const helpUrl = converseLocalizedHelpUrl({
|
|
||||||
page: 'documentation/user/streamers/moderation'
|
|
||||||
})
|
|
||||||
|
|
||||||
return tplMUCApp(
|
|
||||||
el,
|
|
||||||
i18nSearch,
|
|
||||||
helpUrl,
|
|
||||||
i18nHelp,
|
|
||||||
tplContent(el, mucModel, occupantModel)
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
// 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 { html } from 'lit'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the message as a search result.
|
|
||||||
* @param el The message element
|
|
||||||
* @param mucModel The MUC model
|
|
||||||
* @param searchOccupantModel The model of the occupant for which we are searching
|
|
||||||
* @param message The message (warning: this is not a model)
|
|
||||||
* @returns TemplateResult (or equivalent)
|
|
||||||
*/
|
|
||||||
export function tplMucMamSearchMessage (el, mucModel, searchOccupantModel, message) {
|
|
||||||
const occupant = el.getMessageOccupant()
|
|
||||||
return html`
|
|
||||||
${
|
|
||||||
occupant
|
|
||||||
? html`
|
|
||||||
<livechat-converse-muc-mam-search-occupant
|
|
||||||
.model=${occupant}
|
|
||||||
.message=${message}
|
|
||||||
></livechat-converse-muc-mam-search-occupant>`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
<converse-rich-text
|
|
||||||
render_styling
|
|
||||||
text=${message.body}>
|
|
||||||
</converse-rich-text>
|
|
||||||
<div class="livechat-message-date">${el.getDateTime()}</div>`
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
// 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 { html } from 'lit'
|
|
||||||
import { api } from '@converse/headless'
|
|
||||||
import { getAuthorStyle } from '../../../../src/utils/color.js'
|
|
||||||
import { __ } from 'i18n'
|
|
||||||
|
|
||||||
export function tplMucMamSearchOccupant (el, occupant, message) {
|
|
||||||
const authorStyle = getAuthorStyle(occupant)
|
|
||||||
const jid = occupant.get('jid')
|
|
||||||
const occupantId = occupant.get('occupant_id')
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<a @click=${(ev) => {
|
|
||||||
api.modal.show('converse-muc-occupant-modal', { model: occupant }, ev)
|
|
||||||
}}>
|
|
||||||
<converse-avatar
|
|
||||||
.model=${occupant}
|
|
||||||
class="avatar chat-msg__avatar"
|
|
||||||
name="${occupant.getDisplayName()}"
|
|
||||||
nonce=${occupant.vcard?.get('vcard_updated')}
|
|
||||||
height="30" width="30"></converse-avatar>
|
|
||||||
|
|
||||||
<span style=${authorStyle}>${occupant.getDisplayName()}</span>
|
|
||||||
</a>
|
|
||||||
<ul aria-hidden="true">
|
|
||||||
${
|
|
||||||
// user changed nick: display the original nick
|
|
||||||
message && message.nick !== undefined && message.nick !== occupant.get('nick')
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
? html`<li title=${__(LOC_message_search_original_nick)}>${message.nick}</li>`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
${jid ? html`<li title=${__('XMPP Address')}>${jid}</li>` : ''}
|
|
||||||
${occupantId ? html`<li title=${__('Occupant Id')}>${occupantId}</li>` : ''}
|
|
||||||
</ul>`
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { api } from '../../../src/headless/index.js'
|
|
||||||
import { XMLNS_MAM_SEARCH } from './constants.js'
|
|
||||||
import { __ } from 'i18n'
|
|
||||||
|
|
||||||
function getMessageActionButtons (messageActionsEl, buttons) {
|
|
||||||
const messageModel = messageActionsEl.model
|
|
||||||
if (!api.settings.get('livechat_mam_search_app_enabled')) {
|
|
||||||
return buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messageModel.get('type') !== 'groupchat') {
|
|
||||||
// only on groupchat message.
|
|
||||||
return buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!messageModel.occupant) {
|
|
||||||
return buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
const muc = messageModel.collection?.chatbox
|
|
||||||
if (!muc) {
|
|
||||||
return buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!muc.features?.get?.(XMLNS_MAM_SEARCH)) {
|
|
||||||
return buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
const myself = muc.getOwnOccupant()
|
|
||||||
if (!myself || !['admin', 'owner'].includes(myself.get('affiliation'))) {
|
|
||||||
return buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const i18nSearch = __(LOC_search_occupant_message)
|
|
||||||
|
|
||||||
buttons.push({
|
|
||||||
i18n_text: i18nSearch,
|
|
||||||
handler: async (ev) => {
|
|
||||||
ev.preventDefault()
|
|
||||||
api.livechat_mam_search.showMessagesFrom(messageModel.occupant)
|
|
||||||
},
|
|
||||||
button_class: '',
|
|
||||||
icon_class: 'fa fa-magnifying-glass',
|
|
||||||
name: 'muc-mam-search'
|
|
||||||
})
|
|
||||||
|
|
||||||
return buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOccupantActionButtons (occupant, buttons) {
|
|
||||||
if (!api.settings.get('livechat_mam_search_app_enabled')) {
|
|
||||||
return buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
const muc = occupant.collection?.chatroom
|
|
||||||
if (!muc) {
|
|
||||||
return buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!muc.features?.get?.(XMLNS_MAM_SEARCH)) {
|
|
||||||
return buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
const myself = muc.getOwnOccupant()
|
|
||||||
if (!myself || !['admin', 'owner'].includes(myself.get('affiliation'))) {
|
|
||||||
return buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const i18nSearch = __(LOC_search_occupant_message)
|
|
||||||
|
|
||||||
buttons.push({
|
|
||||||
i18n_text: i18nSearch,
|
|
||||||
handler: async (ev) => {
|
|
||||||
ev.preventDefault()
|
|
||||||
api.livechat_mam_search.showMessagesFrom(occupant)
|
|
||||||
},
|
|
||||||
button_class: '',
|
|
||||||
icon_class: 'fa fa-magnifying-glass',
|
|
||||||
name: 'muc-mam-search'
|
|
||||||
})
|
|
||||||
|
|
||||||
return buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
getMessageActionButtons,
|
|
||||||
getOccupantActionButtons
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
async function openNotes () {
|
|
||||||
const appElement = document.querySelector('livechat-converse-muc-note-app')
|
|
||||||
if (!appElement) {
|
|
||||||
throw new Error('Cant find Note App Element')
|
|
||||||
}
|
|
||||||
await appElement.showApp()
|
|
||||||
await appElement.updateComplete // waiting for the app to be open
|
|
||||||
|
|
||||||
const notesElement = appElement.querySelector('livechat-converse-muc-notes')
|
|
||||||
if (!notesElement) {
|
|
||||||
throw new Error('Cant find Notes Element')
|
|
||||||
}
|
|
||||||
await notesElement.updateComplete
|
|
||||||
return notesElement
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openCreateNoteForm (occupant) {
|
|
||||||
const notesElement = await openNotes()
|
|
||||||
await notesElement.openCreateNoteForm(undefined, occupant)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function searchNotesAbout (occupant) {
|
|
||||||
const notesElement = await openNotes()
|
|
||||||
await notesElement.filterNotes({ occupant })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
openNotes,
|
|
||||||
openCreateNoteForm,
|
|
||||||
searchNotesAbout
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { api } from '@converse/headless'
|
|
||||||
import { MUCApp } from '../../../shared/components/muc-app/index.js'
|
|
||||||
import { tplMUCNoteApp } from '../templates/muc-note-app.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom Element to display the Notes Application.
|
|
||||||
*/
|
|
||||||
export default class MUCNoteApp extends MUCApp {
|
|
||||||
restoreSettingName = 'livechat_note_app_restore'
|
|
||||||
sessionStorageRestoreKey = 'livechat-converse-note-app-show'
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return tplMUCNoteApp(this, this.model)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.elements.define('livechat-converse-muc-note-app', MUCNoteApp)
|
|
@ -1,29 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
|
||||||
import { tplMucNoteOccupant } from '../templates/muc-note-occupant'
|
|
||||||
import { api } from '@converse/headless'
|
|
||||||
|
|
||||||
import '../styles/muc-note-occupant.scss'
|
|
||||||
|
|
||||||
export default class MUCNoteOccupantView extends CustomElement {
|
|
||||||
static get properties () {
|
|
||||||
return {
|
|
||||||
model: { type: Object, attribute: true },
|
|
||||||
note: { type: Object, attribute: true }, // optional associated note
|
|
||||||
full_display: { type: Boolean, attribute: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize () {
|
|
||||||
this.listenTo(this.model, 'change', () => this.requestUpdate())
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return tplMucNoteOccupant(this, this.model, this.note)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.elements.define('livechat-converse-muc-note-occupant', MUCNoteOccupantView)
|
|
@ -1,110 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
|
||||||
import { api } from '@converse/headless'
|
|
||||||
import { tplMucNote } from '../templates/muc-note'
|
|
||||||
import { __ } from 'i18n'
|
|
||||||
|
|
||||||
import '../styles/muc-note.scss'
|
|
||||||
|
|
||||||
export default class MUCNoteView extends CustomElement {
|
|
||||||
static get properties () {
|
|
||||||
return {
|
|
||||||
model: { type: Object, attribute: true },
|
|
||||||
edit: { type: Boolean, attribute: false },
|
|
||||||
is_ocupant_filter: { type: Boolean, attribute: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize () {
|
|
||||||
this.edit = false
|
|
||||||
if (!this.model) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.listenTo(this.model, 'change', () => this.requestUpdate())
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return tplMucNote(this, this.model)
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldUpdate (changedProperties) {
|
|
||||||
if (!super.shouldUpdate(...arguments)) { return false }
|
|
||||||
// When a note is currently edited, and another users change the order,
|
|
||||||
// it could refresh losing the current form.
|
|
||||||
// To avoid this, we cancel update here.
|
|
||||||
// Note: of course, if 'edit' is part of the edited properties, we must update anyway
|
|
||||||
// (it means we just leaved the form)
|
|
||||||
if (this.edit && !changedProperties.has('edit')) {
|
|
||||||
console.info('Canceling an update on note, because it is currently edited', this)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveNote (ev) {
|
|
||||||
ev?.preventDefault?.()
|
|
||||||
|
|
||||||
const description = ev.target.description.value
|
|
||||||
|
|
||||||
if ((description ?? '') === '') { return }
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.querySelectorAll('input[type=submit]').forEach(el => {
|
|
||||||
el.setAttribute('disabled', true)
|
|
||||||
el.classList.add('disabled')
|
|
||||||
})
|
|
||||||
|
|
||||||
const note = this.model
|
|
||||||
note.set('description', description)
|
|
||||||
await note.saveItem()
|
|
||||||
|
|
||||||
this.edit = false
|
|
||||||
this.requestUpdate() // In case we cancel another update in shouldUpdate
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
} finally {
|
|
||||||
this.querySelectorAll('input[type=submit]').forEach(el => {
|
|
||||||
el.removeAttribute('disabled')
|
|
||||||
el.classList.remove('disabled')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteNote (ev) {
|
|
||||||
ev?.preventDefault?.()
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const i18nConfirmDelete = __(LOC_moderator_note_delete_confirm)
|
|
||||||
|
|
||||||
const result = await api.confirm(i18nConfirmDelete)
|
|
||||||
if (!result) { return }
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.model.deleteItem()
|
|
||||||
} catch (err) {
|
|
||||||
api.alert(
|
|
||||||
'error', __('Error'), [__('Error')]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async toggleEdit () {
|
|
||||||
this.edit = !this.edit
|
|
||||||
if (this.edit) {
|
|
||||||
await this.updateComplete
|
|
||||||
const textarea = this.querySelector('textarea[name="description"]')
|
|
||||||
if (textarea) {
|
|
||||||
textarea.focus()
|
|
||||||
// Placing cursor at the end:
|
|
||||||
textarea.selectionStart = textarea.value.length
|
|
||||||
textarea.selectionEnd = textarea.selectionStart
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.elements.define('livechat-converse-muc-note', MUCNoteView)
|
|
@ -1,133 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { api } from '@converse/headless'
|
|
||||||
import tplMucNotes from '../templates/muc-notes'
|
|
||||||
import { __ } from 'i18n'
|
|
||||||
import { DraggablesCustomElement } from '../../../shared/components/draggables/index.js'
|
|
||||||
|
|
||||||
import '../styles/muc-notes.scss'
|
|
||||||
|
|
||||||
export default class MUCNotesView extends DraggablesCustomElement {
|
|
||||||
static get properties () {
|
|
||||||
return {
|
|
||||||
model: { type: Object, attribute: true },
|
|
||||||
create_note_error_message: { type: String, attribute: false },
|
|
||||||
create_note_opened: { type: Boolean, attribute: false },
|
|
||||||
create_note_about_occupant: { type: Object, attribute: false },
|
|
||||||
occupant_filter: { type: Object, attribute: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize () {
|
|
||||||
this.create_note_error_message = ''
|
|
||||||
|
|
||||||
if (!this.model) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.draggableTagName = 'livechat-converse-muc-note'
|
|
||||||
this.droppableTagNames = ['livechat-converse-muc-note']
|
|
||||||
this.droppableAlwaysBottomTagNames = []
|
|
||||||
|
|
||||||
// Adding or removing a new note: we must update.
|
|
||||||
this.listenTo(this.model, 'add', () => this.requestUpdate())
|
|
||||||
this.listenTo(this.model, 'remove', () => this.requestUpdate())
|
|
||||||
this.listenTo(this.model, 'sort', () => this.requestUpdate())
|
|
||||||
|
|
||||||
await super.initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return tplMucNotes(this, this.model)
|
|
||||||
}
|
|
||||||
|
|
||||||
async openCreateNoteForm (ev, occupant) {
|
|
||||||
ev?.preventDefault?.()
|
|
||||||
this.create_note_opened = true
|
|
||||||
this.create_note_about_occupant = occupant ?? undefined
|
|
||||||
if (this.create_note_about_occupant === undefined && this.occupant_filter) {
|
|
||||||
// if we have a current filter, we can use it for the new note.
|
|
||||||
this.create_note_about_occupant = this.occupant_filter
|
|
||||||
}
|
|
||||||
await this.updateComplete
|
|
||||||
const textarea = this.querySelector('.notes-create-note textarea[name="description"]')
|
|
||||||
if (textarea) {
|
|
||||||
textarea.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closeCreateNoteForm (ev) {
|
|
||||||
ev?.preventDefault?.()
|
|
||||||
this.create_note_opened = false
|
|
||||||
this.create_note_about_occupant = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
filterNotes (filters) {
|
|
||||||
this.occupant_filter = filters?.occupant || undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
async submitCreateNote (ev) {
|
|
||||||
ev.preventDefault()
|
|
||||||
|
|
||||||
const description = ev.target.description.value
|
|
||||||
if (this.create_note_error_message) {
|
|
||||||
this.create_note_error_message = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((description ?? '') === '') { return }
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.querySelectorAll('input[type=submit]').forEach(el => {
|
|
||||||
el.setAttribute('disabled', true)
|
|
||||||
el.classList.add('disabled')
|
|
||||||
})
|
|
||||||
|
|
||||||
await this.model.createNote({
|
|
||||||
description: description,
|
|
||||||
about_jid: ev.target.about_jid?.value || undefined,
|
|
||||||
about_nick: ev.target.about_nick?.value || undefined,
|
|
||||||
about_occupant_id: ev.target.about_occupant_id?.value || undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
this.closeCreateNoteForm()
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
this.create_note_error_message = __(LOC_moderator_notes_create_error)
|
|
||||||
} finally {
|
|
||||||
this.querySelectorAll('input[type=submit]').forEach(el => {
|
|
||||||
el.removeAttribute('disabled')
|
|
||||||
el.classList.remove('disabled')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_dropDone (draggedEl, droppedOnEl, onTopHalf) {
|
|
||||||
super._dropDone(...arguments)
|
|
||||||
console.log('[livechat note drag&drop] Note dropped...')
|
|
||||||
|
|
||||||
const note = draggedEl.model
|
|
||||||
if (!note) {
|
|
||||||
throw new Error('No model for the draggedEl')
|
|
||||||
}
|
|
||||||
const targetNote = droppedOnEl.model
|
|
||||||
if (!targetNote) {
|
|
||||||
throw new Error('No model for the droppedOnEl')
|
|
||||||
}
|
|
||||||
if (note === targetNote) {
|
|
||||||
console.log('[livechat note drag&drop] Note dropped on itself, nothing to do')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let newOrder = targetNote.get('order') ?? 0
|
|
||||||
if (onTopHalf) { newOrder = Math.max(0, newOrder + 1) } // reverse order!
|
|
||||||
|
|
||||||
// Warning: the order of the collection is reversed!
|
|
||||||
// _saveOrders needs it in ascending order!
|
|
||||||
this._saveOrders(Array.from(this.model).reverse(), note, newOrder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.elements.define('livechat-converse-muc-notes', MUCNotesView)
|
|
@ -1,5 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
export const XMLNS_NOTE = 'urn:peertube-plugin-livechat:note'
|
|
@ -1,69 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { _converse, converse } from '../../../src/headless/index.js'
|
|
||||||
import { XMLNS_NOTE } from './constants.js'
|
|
||||||
import { ChatRoomNote } from './note.js'
|
|
||||||
import { ChatRoomNotes } from './notes.js'
|
|
||||||
import {
|
|
||||||
initOrDestroyChatRoomNotes, getHeadingButtons, getMessageActionButtons, getOccupantActionButtons
|
|
||||||
} from './utils.js'
|
|
||||||
import notesApi from './api.js'
|
|
||||||
|
|
||||||
import './components/muc-note-app-view.js'
|
|
||||||
import './components/muc-notes-view.js'
|
|
||||||
import './components/muc-note-view.js'
|
|
||||||
import './components/muc-note-occupant-view.js'
|
|
||||||
|
|
||||||
converse.plugins.add('livechat-converse-notes', {
|
|
||||||
dependencies: ['converse-muc', 'converse-disco', 'converse-pubsub'],
|
|
||||||
|
|
||||||
initialize () {
|
|
||||||
Object.assign(
|
|
||||||
_converse.exports,
|
|
||||||
{
|
|
||||||
ChatRoomNotes,
|
|
||||||
ChatRoomNote
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
_converse.api.settings.extend({
|
|
||||||
livechat_note_app_enabled: false,
|
|
||||||
livechat_note_app_restore: false // should we open the app by default if it was previously oppened?
|
|
||||||
})
|
|
||||||
|
|
||||||
Object.assign(_converse.api, {
|
|
||||||
livechat_notes: notesApi
|
|
||||||
})
|
|
||||||
|
|
||||||
_converse.api.listen.on('chatRoomInitialized', muc => {
|
|
||||||
muc.session.on('change:connection_status', _session => {
|
|
||||||
// When joining a room, initializing the Notes object (if user has access),
|
|
||||||
// When disconnected from a room, destroying the Notes object:
|
|
||||||
initOrDestroyChatRoomNotes(muc)
|
|
||||||
})
|
|
||||||
|
|
||||||
// When the current user affiliation changes, we must also delete or initialize the TaskLists object:
|
|
||||||
muc.occupants.on('change:affiliation', occupant => {
|
|
||||||
if (occupant.get('jid') !== _converse.bare_jid) { // only for myself
|
|
||||||
return
|
|
||||||
}
|
|
||||||
initOrDestroyChatRoomNotes(muc)
|
|
||||||
})
|
|
||||||
|
|
||||||
// To be sure that everything works in any case, we also must listen for addition in muc.features.
|
|
||||||
muc.features.on('change:' + XMLNS_NOTE, () => {
|
|
||||||
initOrDestroyChatRoomNotes(muc)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// adding the "Notes" button in the MUC heading buttons:
|
|
||||||
_converse.api.listen.on('getHeadingButtons', getHeadingButtons)
|
|
||||||
|
|
||||||
// Adding buttons on messages:
|
|
||||||
_converse.api.listen.on('getMessageActionButtons', getMessageActionButtons)
|
|
||||||
// Adding buttons on occupants:
|
|
||||||
_converse.api.listen.on('getOccupantActionButtons', getOccupantActionButtons)
|
|
||||||
}
|
|
||||||
})
|
|
@ -1,51 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { PubSubManager } from '../../shared/lib/pubsub-manager.js'
|
|
||||||
|
|
||||||
export class NotePubSubManager extends PubSubManager {
|
|
||||||
_additionalModelToData (item, data) {
|
|
||||||
super._additionalModelToData(item, data)
|
|
||||||
|
|
||||||
data.about_jid = item.get('about_jid')
|
|
||||||
data.about_occupant_id = item.get('about_occupant_id')
|
|
||||||
data.about_nick = item.get('about_nick')
|
|
||||||
}
|
|
||||||
|
|
||||||
_additionalDataToItemNode (data, item) {
|
|
||||||
super._additionalDataToItemNode(data, item)
|
|
||||||
|
|
||||||
const aboutAttributes = {}
|
|
||||||
if (data.about_jid !== undefined) {
|
|
||||||
aboutAttributes.jid = data.about_jid
|
|
||||||
}
|
|
||||||
if (data.about_nick !== undefined) {
|
|
||||||
aboutAttributes.nick = data.about_nick
|
|
||||||
}
|
|
||||||
const occupantId = data.about_occupant_id
|
|
||||||
|
|
||||||
if (occupantId !== undefined || Object.values(aboutAttributes).length) {
|
|
||||||
item.c('note-about', aboutAttributes)
|
|
||||||
if (occupantId) {
|
|
||||||
item.c('occupant-id', { xmlns: 'urn:xmpp:occupant-id:0', id: occupantId }).up()
|
|
||||||
}
|
|
||||||
item.up()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_additionalParseItemNode (itemNode, type, data) {
|
|
||||||
super._additionalParseItemNode(itemNode, type, data)
|
|
||||||
|
|
||||||
const about = itemNode.querySelector('& > note-about')
|
|
||||||
if (!about) { return }
|
|
||||||
|
|
||||||
data.about_jid = about.getAttribute('jid')
|
|
||||||
data.about_nick = about.getAttribute('nick')
|
|
||||||
|
|
||||||
const occupantIdEl = about.querySelector('& > occupant-id')
|
|
||||||
if (occupantIdEl) {
|
|
||||||
data.about_occupant_id = occupantIdEl.getAttribute('id')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { Model } from '@converse/skeletor/src/model.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A chat room note.
|
|
||||||
* @class
|
|
||||||
* @namespace _converse.exports.ChatRoomNote
|
|
||||||
* @memberof _converse
|
|
||||||
*/
|
|
||||||
class ChatRoomNote extends Model {
|
|
||||||
idAttribute = 'id'
|
|
||||||
_aboutOccupantCache = null
|
|
||||||
_aboutOccupantCacheFor = null
|
|
||||||
|
|
||||||
async saveItem () {
|
|
||||||
console.log('Saving note ' + this.get('id') + '...')
|
|
||||||
await this.collection.chatroom.noteManager.saveItem(this)
|
|
||||||
console.log('Note ' + this.get('id') + ' saved.')
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteItem () {
|
|
||||||
return this.collection.chatroom.noteManager.deleteItems([this])
|
|
||||||
}
|
|
||||||
|
|
||||||
getAboutOccupant () {
|
|
||||||
const occupants = this.collection.chatroom?.occupants
|
|
||||||
if (!occupants?.findOccupant) { return undefined }
|
|
||||||
|
|
||||||
const nick = this.get('about_nick')
|
|
||||||
const jid = this.get('about_jid')
|
|
||||||
const occupantId = this.get('about_occupant_id')
|
|
||||||
|
|
||||||
if (!nick && !jid && !occupantId) {
|
|
||||||
this._aboutOccupantCache = null
|
|
||||||
this._aboutOccupantCacheFor = null
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keeping some cache, to avoid intensive search on each rendering.
|
|
||||||
const cacheKey = `${occupantId ?? ''} ${jid ?? ''} ${nick ?? ''}`
|
|
||||||
if (this._aboutOccupantCacheFor === cacheKey && this._aboutOccupantCache) {
|
|
||||||
return this._aboutOccupantCache
|
|
||||||
}
|
|
||||||
|
|
||||||
this._aboutOccupantCacheFor = cacheKey
|
|
||||||
|
|
||||||
if (occupantId) {
|
|
||||||
const o = occupants.findOccupant({ occupant_id: occupantId })
|
|
||||||
if (o) {
|
|
||||||
this._aboutOccupantCache = o
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jid) {
|
|
||||||
const o = occupants.findOccupant({
|
|
||||||
jid,
|
|
||||||
nick
|
|
||||||
})
|
|
||||||
if (o) {
|
|
||||||
this._aboutOccupantCache = o
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we don't find it, maybe it is a user that has spoken a long time ago (or never spoked).
|
|
||||||
// In such case, we must create a dummy occupant:
|
|
||||||
this._aboutOccupantCache = occupants.create({
|
|
||||||
nick,
|
|
||||||
occupant_id: occupantId,
|
|
||||||
jid
|
|
||||||
})
|
|
||||||
return this._aboutOccupantCache
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
ChatRoomNote
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { Collection } from '@converse/skeletor/src/collection.js'
|
|
||||||
import { ChatRoomNote } from './note'
|
|
||||||
import { initStorage } from '@converse/headless/utils/storage.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of {@link _converse.exports.ChatRoomNote} instances, representing notes associated to a MUC.
|
|
||||||
* @class
|
|
||||||
* @namespace _converse.exports.ChatRoomNotes
|
|
||||||
* @memberOf _converse
|
|
||||||
*/
|
|
||||||
class ChatRoomNotes extends Collection {
|
|
||||||
model = ChatRoomNote
|
|
||||||
|
|
||||||
initialize (models, options) {
|
|
||||||
this.model = ChatRoomNote // don't know why, must do it again here
|
|
||||||
super.initialize(arguments)
|
|
||||||
this.chatroom = options.chatroom
|
|
||||||
|
|
||||||
const id = `converse-livechat-notes-${this.chatroom.get('jid')}`
|
|
||||||
initStorage(this, id, 'session')
|
|
||||||
|
|
||||||
this.on('change:order', () => this.sort())
|
|
||||||
}
|
|
||||||
|
|
||||||
comparator (n1, n2) {
|
|
||||||
// must reverse order
|
|
||||||
const o1 = n1.get('order') ?? 0
|
|
||||||
const o2 = n2.get('order') ?? 0
|
|
||||||
return o1 < o2 ? 1 : o1 > o2 ? -1 : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
async createNote (data) {
|
|
||||||
data = Object.assign({}, data)
|
|
||||||
|
|
||||||
if (!data.order) {
|
|
||||||
data.order = 1 + Math.max(
|
|
||||||
0,
|
|
||||||
...(this.map(n => n.get('order') ?? 0).filter(o => !isNaN(o)))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Creating note...')
|
|
||||||
await this.chatroom.noteManager.createItem(this, data)
|
|
||||||
console.log('Note created.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
ChatRoomNotes
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
.conversejs {
|
|
||||||
livechat-converse-muc-note-occupant {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.25em;
|
|
||||||
|
|
||||||
& > a {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-left: 0.5em;
|
|
||||||
max-width: 200px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& > ul {
|
|
||||||
font-weight: lighter;
|
|
||||||
font-size: 0.75em;
|
|
||||||
list-style: none;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
.conversejs {
|
|
||||||
livechat-converse-muc-note {
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.note-line {
|
|
||||||
border: 1px solid var(--chatroom-head-bg-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
justify-content: space-around;
|
|
||||||
margin: 0.25em 0;
|
|
||||||
padding: 0.25em;
|
|
||||||
column-gap: 0.25em;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.note-content {
|
|
||||||
flex-grow: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-description {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-action {
|
|
||||||
background: unset;
|
|
||||||
border: 0;
|
|
||||||
padding-left: 0.25em;
|
|
||||||
padding-right: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
.conversejs {
|
|
||||||
.notes-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
justify-content: right;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notes-action {
|
|
||||||
background: unset;
|
|
||||||
border: 0;
|
|
||||||
padding-left: 0.25em;
|
|
||||||
padding-right: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notes-filters {
|
|
||||||
border: 1px solid var(--chatroom-head-bg-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0.25em 0;
|
|
||||||
padding: 0.25em;
|
|
||||||
column-gap: 0.25em;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
livechat-converse-muc-note-occupant {
|
|
||||||
flex-grow: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
// 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 { converseLocalizedHelpUrl } from '../../../shared/lib/help'
|
|
||||||
import { tplMUCApp } from '../../../shared/components/muc-app/templates/muc-app.js'
|
|
||||||
import { html } from 'lit'
|
|
||||||
import { __ } from 'i18n'
|
|
||||||
|
|
||||||
export function tplMUCNoteApp (el, mucModel) {
|
|
||||||
if (!mucModel) {
|
|
||||||
// should not happen
|
|
||||||
return html``
|
|
||||||
}
|
|
||||||
if (!mucModel.notes) {
|
|
||||||
// too soon, not initialized yet (this will happen)
|
|
||||||
return html``
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!el.show) {
|
|
||||||
return html``
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const i18nNotes = __(LOC_moderator_notes)
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const i18nHelp = __(LOC_online_help)
|
|
||||||
const helpUrl = converseLocalizedHelpUrl({
|
|
||||||
page: 'documentation/user/streamers/moderation_notes'
|
|
||||||
})
|
|
||||||
|
|
||||||
return tplMUCApp(
|
|
||||||
el,
|
|
||||||
i18nNotes,
|
|
||||||
helpUrl,
|
|
||||||
i18nHelp,
|
|
||||||
html`<livechat-converse-muc-notes .model=${mucModel.notes}></livechat-converse-muc-notes>`
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
// 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 { html } from 'lit'
|
|
||||||
import { api } from '@converse/headless'
|
|
||||||
import { getAuthorStyle } from '../../../../src/utils/color.js'
|
|
||||||
import { __ } from 'i18n'
|
|
||||||
|
|
||||||
export function tplMucNoteOccupant (el, occupant, note) {
|
|
||||||
const authorStyle = getAuthorStyle(occupant)
|
|
||||||
const jid = occupant.get('jid')
|
|
||||||
const occupantId = occupant.get('occupant_id')
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<a @click=${(ev) => {
|
|
||||||
api.modal.show('converse-muc-occupant-modal', { model: occupant }, ev)
|
|
||||||
}}>
|
|
||||||
<converse-avatar
|
|
||||||
.model=${occupant}
|
|
||||||
class="avatar chat-msg__avatar"
|
|
||||||
name="${occupant.getDisplayName()}"
|
|
||||||
nonce=${occupant.vcard?.get('vcard_updated')}
|
|
||||||
height="30" width="30"></converse-avatar>
|
|
||||||
|
|
||||||
<span style=${authorStyle}>${occupant.getDisplayName()}</span>
|
|
||||||
</a>
|
|
||||||
${
|
|
||||||
el.full_display
|
|
||||||
? html`<ul aria-hidden="true">
|
|
||||||
${
|
|
||||||
// user changed nick: display the original nick
|
|
||||||
note && note.get('about_nick') && note.get('about_nick') !== occupant.get('nick')
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
? html`<li title=${__(LOC_moderator_note_original_nick)}>${note.get('about_nick')}</li>`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
${jid ? html`<li title=${__('XMPP Address')}>${jid}</li>` : ''}
|
|
||||||
${occupantId ? html`<li title=${__('Occupant Id')}>${occupantId}</li>` : ''}
|
|
||||||
</ul>`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
`
|
|
||||||
}
|
|
@ -1,133 +0,0 @@
|
|||||||
// 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 { api } from '@converse/headless'
|
|
||||||
import { html } from 'lit'
|
|
||||||
import { __ } from 'i18n'
|
|
||||||
|
|
||||||
export function tplMucNote (el, note) {
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const i18nDelete = __(LOC_moderator_note_delete)
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const i18nSearch = __(LOC_moderator_note_search_for_participant)
|
|
||||||
|
|
||||||
const aboutOccupant = note.getAboutOccupant()
|
|
||||||
|
|
||||||
return !el.edit
|
|
||||||
? html`
|
|
||||||
<div draggable="true" class="note-line draggables-line">
|
|
||||||
<div class="note-content">
|
|
||||||
${
|
|
||||||
aboutOccupant
|
|
||||||
? html`
|
|
||||||
<livechat-converse-muc-note-occupant
|
|
||||||
.full_display=${el.is_ocupant_filter}
|
|
||||||
.model=${aboutOccupant}
|
|
||||||
.note=${note}
|
|
||||||
></livechat-converse-muc-note-occupant>`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
<div class="note-description">${note.get('description') ?? ''}</div>
|
|
||||||
</div>
|
|
||||||
${
|
|
||||||
aboutOccupant && el.is_ocupant_filter
|
|
||||||
? ''
|
|
||||||
: html`
|
|
||||||
<button type="button" class="note-action" @click=${ev => {
|
|
||||||
ev.preventDefault()
|
|
||||||
api.livechat_notes.searchNotesAbout(aboutOccupant)
|
|
||||||
}}>
|
|
||||||
<converse-icon class="fa fa-magnifying-glass" size="1em" title=${i18nSearch}></converse-icon>
|
|
||||||
</button>`
|
|
||||||
}
|
|
||||||
<button type="button" class="note-action" title="${__('Edit')}"
|
|
||||||
@click=${el.toggleEdit}
|
|
||||||
>
|
|
||||||
<converse-icon class="fa fa-edit" size="1em"></converse-icon>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="note-action" title="${i18nDelete}"
|
|
||||||
@click=${el.deleteNote}
|
|
||||||
>
|
|
||||||
<converse-icon class="fa fa-trash-alt" size="1em"></converse-icon>
|
|
||||||
</button>
|
|
||||||
</div>`
|
|
||||||
: html`
|
|
||||||
<div class="note-line draggables-line">
|
|
||||||
<form class="converse-form" @submit=${el.saveNote}>
|
|
||||||
${
|
|
||||||
aboutOccupant
|
|
||||||
? html`
|
|
||||||
<livechat-converse-muc-note-occupant
|
|
||||||
full_display=${true}
|
|
||||||
.model=${aboutOccupant}
|
|
||||||
.note=${note}
|
|
||||||
></livechat-converse-muc-note-occupant>
|
|
||||||
`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
${_tplNoteForm(note)}
|
|
||||||
<fieldset>
|
|
||||||
<input type="submit" class="btn btn-primary" value="${__('Ok')}" />
|
|
||||||
<input type="button" class="btn btn-secondary button-cancel"
|
|
||||||
value="${__('Cancel')}" @click=${el.toggleEdit}
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
</div>`
|
|
||||||
}
|
|
||||||
|
|
||||||
function _tplNoteForm (note) {
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const i18nNoteDesc = __(LOC_moderator_note_description)
|
|
||||||
|
|
||||||
return html`<fieldset>
|
|
||||||
<textarea
|
|
||||||
class="form-control" name="description"
|
|
||||||
placeholder="${i18nNoteDesc}"
|
|
||||||
>${note ? note.get('description') : ''}</textarea>
|
|
||||||
</fieldset>`
|
|
||||||
}
|
|
||||||
|
|
||||||
function _tplNoteOccupantFormFields (occupant) {
|
|
||||||
if (!occupant) { return '' }
|
|
||||||
return html`
|
|
||||||
<input type="hidden" name="about_nick" value=${occupant.get('nick')} />
|
|
||||||
<input type="hidden" name="about_jid" value=${occupant.get('jid')} />
|
|
||||||
<input type="hidden" name="about_occupant_id" value=${occupant.get('occupant_id')} />
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tplMucCreateNoteForm (notesEl, occupant) {
|
|
||||||
const i18nOk = __('Ok')
|
|
||||||
const i18nCancel = __('Cancel')
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<form class="notes-create-note converse-form" @submit=${notesEl.submitCreateNote}>
|
|
||||||
${
|
|
||||||
occupant
|
|
||||||
? html`
|
|
||||||
${_tplNoteOccupantFormFields(occupant)}
|
|
||||||
<livechat-converse-muc-note-occupant
|
|
||||||
full_display=${true}
|
|
||||||
.model=${occupant}
|
|
||||||
></livechat-converse-muc-note-occupant>
|
|
||||||
`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
${_tplNoteForm(undefined)}
|
|
||||||
<fieldset>
|
|
||||||
<input type="submit" class="btn btn-primary" value="${i18nOk}" />
|
|
||||||
<input type="button" class="btn btn-secondary button-cancel"
|
|
||||||
value="${i18nCancel}" @click=${notesEl.closeCreateNoteForm}
|
|
||||||
/>
|
|
||||||
${!notesEl.create_note_error_message
|
|
||||||
? ''
|
|
||||||
: html`<div class="invalid-feedback d-block">${notesEl.create_note_error_message}</div>`
|
|
||||||
}
|
|
||||||
</fieldset>
|
|
||||||
</form>`
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
// 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 { html } from 'lit'
|
|
||||||
import { repeat } from 'lit/directives/repeat.js'
|
|
||||||
import { __ } from 'i18n'
|
|
||||||
import { tplMucCreateNoteForm } from './muc-note'
|
|
||||||
|
|
||||||
function tplFilters (el) {
|
|
||||||
const filterOccupant = el.occupant_filter
|
|
||||||
if (!filterOccupant) { return '' }
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const i18nSearch = __(LOC_moderator_note_filters)
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="notes-filters">
|
|
||||||
<converse-icon class="fa fa-magnifying-glass" size="1em" title=${i18nSearch}></converse-icon>
|
|
||||||
${
|
|
||||||
filterOccupant
|
|
||||||
? html`<livechat-converse-muc-note-occupant
|
|
||||||
full_display=${true}
|
|
||||||
.model=${filterOccupant}
|
|
||||||
></livechat-converse-muc-note-occupant>`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
<button type="button" class="notes-action" @click=${(ev) => {
|
|
||||||
ev?.preventDefault()
|
|
||||||
el.filterNotes({})
|
|
||||||
}} title="${__('Close')}">
|
|
||||||
<converse-icon class="fa fa-times" size="1em"></converse-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<hr/>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFiltered (el, note) {
|
|
||||||
const filterOccupant = el.occupant_filter
|
|
||||||
if (!filterOccupant) { return false }
|
|
||||||
|
|
||||||
const noteOccupant = note.getAboutOccupant()
|
|
||||||
// there is an occupant filter, so if current note has no associated occupant, we can pass.
|
|
||||||
if (!noteOccupant) { return true }
|
|
||||||
|
|
||||||
if (noteOccupant === filterOccupant) {
|
|
||||||
// Yes!
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// We will also test for nickname, so that we can found anonymous users
|
|
||||||
// (they can have multiple associated occupants)
|
|
||||||
if (filterOccupant.get('nick') && filterOccupant.get('nick') === noteOccupant.get('nick')) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function tplMucNotes (el, notes) {
|
|
||||||
if (!notes) { // if user loses rights
|
|
||||||
return html`` // FIXME: add a message like "you dont have access"?
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
${
|
|
||||||
el.create_note_opened ? tplMucCreateNoteForm(el, el.create_note_about_occupant) : tplCreateButton(el)
|
|
||||||
}
|
|
||||||
${tplFilters(el)}
|
|
||||||
${
|
|
||||||
repeat(notes, (note) => note.get('id'), (note) => {
|
|
||||||
return isFiltered(el, note)
|
|
||||||
? ''
|
|
||||||
: html`<livechat-converse-muc-note
|
|
||||||
.model=${note}
|
|
||||||
.is_ocupant_filter=${!!el.occupant_filter}
|
|
||||||
></livechat-converse-muc-note>`
|
|
||||||
})
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function tplCreateButton (el) {
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const i18nCreateNote = __(LOC_moderator_note_create)
|
|
||||||
return html`
|
|
||||||
<div class="notes-actions">
|
|
||||||
<button type="button" class="notes-action" title="${i18nCreateNote}" @click=${el.openCreateNoteForm}>
|
|
||||||
<converse-icon class="fa fa-plus" size="1em"></converse-icon>
|
|
||||||
</button>
|
|
||||||
</div>`
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user