Merge branch 'main' of https://github.com/JohnXLivingston/peertube-plugin-livechat
This commit is contained in:
commit
b5e18faaaa
@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
"env": {},
|
|
||||||
"extends": [],
|
|
||||||
"globals": {},
|
|
||||||
"plugins": [],
|
|
||||||
"ignorePatterns": [
|
|
||||||
"node_modules/", "dist/", "webpack.config.js",
|
|
||||||
"build/",
|
|
||||||
"vendor/",
|
|
||||||
"support/documentation",
|
|
||||||
"build-*js"],
|
|
||||||
"rules": {}
|
|
||||||
}
|
|
31
.github/dependabot.yml
vendored
Normal file
31
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# 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
|
||||||
|
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.
|
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.80.0'
|
hugo-version: '0.132.2'
|
||||||
extended: true
|
extended: true
|
||||||
|
|
||||||
- name: Generate documentation translations
|
- name: Generate documentation translations
|
||||||
|
@ -22,7 +22,7 @@ pages:
|
|||||||
image: registry.gitlab.com/pages/hugo/hugo_extended:latest
|
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-learn
|
GIT_SUBMODULE_PATHS: support/documentation/themes/hugo-theme-relearn
|
||||||
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 "documentation/themes/hugo-theme-learn"]
|
[submodule "support/documentation/themes/hugo-theme-relearn"]
|
||||||
path = support/documentation/themes/hugo-theme-learn
|
path = support/documentation/themes/hugo-theme-relearn
|
||||||
url = https://github.com/matcornic/hugo-theme-learn.git
|
url = https://github.com/McShelby/hugo-theme-relearn.git
|
||||||
|
@ -32,3 +32,7 @@ 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.'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
69
CHANGELOG.md
69
CHANGELOG.md
@ -1,5 +1,74 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 12.0.0 (Not Released Yet)
|
||||||
|
|
||||||
|
TODO Before releasing:
|
||||||
|
* update ConverseJS with latest merges (there are currently some known bugs).
|
||||||
|
* as the Prosody version changes, check these stress test https://github.com/JohnXLivingston/livechat-perf-test/tree/main/tests/33-prosody-gc and apply the correct gc parameter. Also see https://github.com/JohnXLivingston/peertube-plugin-livechat/issues/300
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### 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
|
||||||
|
94
assets/styles/admin/firewall/_firewall.scss
Normal file
94
assets/styles/admin/firewall/_firewall.scss
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* 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(--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"] {
|
||||||
|
// Peertube orange-button mixin
|
||||||
|
&,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--mainColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--mainHoverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled],
|
||||||
|
&.disabled {
|
||||||
|
cursor: default;
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--inputBorderColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="reset"],
|
||||||
|
button[type="reset"] {
|
||||||
|
// Peertube grey-button mixin
|
||||||
|
background-color: var(--greyBackgroundColor);
|
||||||
|
color: var(--greyForegroundColor);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&[disabled],
|
||||||
|
&.disabled {
|
||||||
|
color: var(--greyForegroundColor);
|
||||||
|
background-color: var(--greySecondaryBackgroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled],
|
||||||
|
&.disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.peertube-livechat-admin-firewall-col-name {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peertube-livechat-admin-firewall-col-content {
|
||||||
|
width: 65%;
|
||||||
|
}
|
||||||
|
}
|
@ -174,6 +174,12 @@ $small-view: 800px;
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
/* See Peertube .video-channel-names */
|
/* See Peertube .video-channel-names */
|
||||||
|
width: fit-content;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
/* stylelint-disable-next-line value-keyword-case */
|
||||||
|
color: var(--mainForegroundColor);
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:active {
|
&:active {
|
||||||
@ -184,12 +190,6 @@ $small-view: 800px;
|
|||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
width: fit-content;
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
/* stylelint-disable-next-line value-keyword-case */
|
|
||||||
color: var(--mainForegroundColor);
|
|
||||||
|
|
||||||
div:first-child {
|
div:first-child {
|
||||||
/* See Peertube .video-channel-display-name */
|
/* See Peertube .video-channel-display-name */
|
||||||
font-weight: variables.$font-semibold;
|
font-weight: variables.$font-semibold;
|
||||||
|
@ -15,9 +15,9 @@ livechat-spinner,
|
|||||||
height: 48px;
|
height: 48px;
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
/* stylelint-disable-next-line custom-property-pattern */
|
/* stylelint-disable-next-line custom-property-pattern */
|
||||||
border: 5px solid var(--greyBackgroundColor);
|
border: 5px solid var(--greyBackgroundColor) !important; // !important is required for it to work in ConverseJS
|
||||||
/* stylelint-disable-next-line custom-property-pattern */
|
/* stylelint-disable-next-line custom-property-pattern */
|
||||||
border-bottom-color: var(--mainColor);
|
border-bottom-color: var(--mainColor) !important; // !important is required for it to work in ConverseJS
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -9,10 +9,10 @@
|
|||||||
|
|
||||||
livechat-token-list {
|
livechat-token-list {
|
||||||
table {
|
table {
|
||||||
@include tables.data-table;
|
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@include tables.data-table;
|
||||||
|
|
||||||
tr th:first-child,
|
tr th:first-child,
|
||||||
tr th:last-child {
|
tr th:last-child {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
|
@ -9,4 +9,5 @@
|
|||||||
@use "elements/index";
|
@use "elements/index";
|
||||||
@use "video";
|
@use "video";
|
||||||
@use "configuration/configuration";
|
@use "configuration/configuration";
|
||||||
@use "list-rooms/list-rooms.scss";
|
@use "admin/firewall/firewall";
|
||||||
|
@use "list-rooms/list-rooms";
|
||||||
|
@ -18,17 +18,31 @@
|
|||||||
/* 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 200px limit for converse-muc and converse-root is
|
We must ensure that the px height 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, 200px); // Always at least 200px, and ideally at least 30% of viewport.
|
min-height: max(30vh, 300px); // 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(59vh, 400px);
|
min-height: max(30vh, 300px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (orientation: portrait) and (width <= 767px) {
|
||||||
|
/* On small screen, and when portrait mode, we are giving the chat more vertical space.
|
||||||
|
It should go under the video.
|
||||||
|
*/
|
||||||
|
min-height: max(58vh, 300px);
|
||||||
|
|
||||||
|
converse-muc {
|
||||||
|
min-height: max(58vh, 300px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ const clientFiles = [
|
|||||||
'admin-plugin-client-plugin'
|
'admin-plugin-client-plugin'
|
||||||
]
|
]
|
||||||
|
|
||||||
function loadLocs() {
|
function loadLocs(globalFile) {
|
||||||
// Loading english strings, so we can inject them as constants.
|
// Loading english strings, so we can inject them as constants.
|
||||||
const refFile = path.resolve(__dirname, 'dist', 'languages', 'en.reference.json')
|
const refFile = path.resolve(__dirname, 'dist', 'languages', 'en.reference.json')
|
||||||
if (!fs.existsSync(refFile)) {
|
if (!fs.existsSync(refFile)) {
|
||||||
@ -25,7 +25,6 @@ function loadLocs() {
|
|||||||
|
|
||||||
// Reading client/@types/global.d.ts, to have a list of needed localized strings.
|
// Reading client/@types/global.d.ts, to have a list of needed localized strings.
|
||||||
const r = {}
|
const r = {}
|
||||||
const globalFile = path.resolve(__dirname, 'client', '@types', 'global.d.ts')
|
|
||||||
const globalFileContent = '' + fs.readFileSync(globalFile)
|
const globalFileContent = '' + fs.readFileSync(globalFile)
|
||||||
const matches = globalFileContent.matchAll(/^declare const LOC_(\w+)\b/gm)
|
const matches = globalFileContent.matchAll(/^declare const LOC_(\w+)\b/gm)
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
@ -41,7 +40,7 @@ function loadLocs() {
|
|||||||
const define = Object.assign({
|
const define = Object.assign({
|
||||||
PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name),
|
PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name),
|
||||||
PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, ''))
|
PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, ''))
|
||||||
}, loadLocs())
|
}, loadLocs(path.resolve(__dirname, 'client', '@types', 'global.d.ts')))
|
||||||
|
|
||||||
const configs = clientFiles.map(f => ({
|
const configs = clientFiles.map(f => ({
|
||||||
entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ],
|
entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ],
|
||||||
@ -59,8 +58,14 @@ const configs = clientFiles.map(f => ({
|
|||||||
outfile: path.resolve(__dirname, 'dist/client', f + '.js'),
|
outfile: path.resolve(__dirname, 'dist/client', f + '.js'),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const defineBuiltin = Object.assign(
|
||||||
|
{},
|
||||||
|
loadLocs(path.resolve(__dirname, 'conversejs', 'lib', '@types', 'global.d.ts'))
|
||||||
|
)
|
||||||
|
|
||||||
configs.push({
|
configs.push({
|
||||||
entryPoints: ["./conversejs/builtin.ts"],
|
entryPoints: ["./conversejs/builtin.ts"],
|
||||||
|
define: defineBuiltin,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
minify: true,
|
minify: true,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
|
@ -10,12 +10,12 @@ set -euo pipefail
|
|||||||
# This script download the Prosody AppImage from the https://github.com/JohnXLivingston/prosody-appimage project.
|
# This script download the Prosody AppImage from the https://github.com/JohnXLivingston/prosody-appimage project.
|
||||||
|
|
||||||
repo_base_url='https://github.com/JohnXLivingston/prosody-appimage/releases/download'
|
repo_base_url='https://github.com/JohnXLivingston/prosody-appimage/releases/download'
|
||||||
wanted_release='v0.12.3-1'
|
wanted_release='v0.12.4-3'
|
||||||
|
|
||||||
x86_64_filename='prosody-x86_64.AppImage'
|
x86_64_filename='prosody-x86_64.AppImage'
|
||||||
x86_64_sha256sum='f4af9bfefa2f804ad7e8b03a68f04194abb801f070ae620b3d4bcedb144e8523'
|
x86_64_sha256sum='83a583ac7036387514bed17afab257dab4161ccdd0ab7453818c78b51f830357'
|
||||||
aarch64_filename='prosody-aarch64.AppImage'
|
aarch64_filename='prosody-aarch64.AppImage'
|
||||||
aarch64_sha256sum='878c5be719e1e36a84d637fd2bd44e3059aa91ddb6906ad05f1dd0334078df09'
|
aarch64_sha256sum='7b7e6bf30d4498fc99a40022232c3065707ee4f4df24dc17947b007621634304'
|
||||||
|
|
||||||
download_dir="$(pwd)/vendor/prosody-appimage"
|
download_dir="$(pwd)/vendor/prosody-appimage"
|
||||||
dist_dir="$(pwd)/dist/server/prosody"
|
dist_dir="$(pwd)/dist/server/prosody"
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"standard-with-typescript",
|
|
||||||
"plugin:lit/recommended"
|
|
||||||
],
|
|
||||||
"globals": {},
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 2018,
|
|
||||||
"project": [
|
|
||||||
"./client/tsconfig.json"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"@typescript-eslint"
|
|
||||||
],
|
|
||||||
"ignorePatterns": [],
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/no-unused-vars": [2, {"argsIgnorePattern": "^_"}],
|
|
||||||
"@typescript-eslint/no-floating-promises": "error",
|
|
||||||
"@typescript-eslint/no-misused-promises": "error",
|
|
||||||
"@typescript-eslint/no-var-requires": "off",
|
|
||||||
"@typescript-eslint/strict-boolean-expressions": "off",
|
|
||||||
"@typescript-eslint/return-await": [2, "in-try-catch"], // FIXME: correct?
|
|
||||||
"@typescript-eslint/no-invalid-void-type": "off",
|
|
||||||
"@typescript-eslint/triple-slash-reference": "off",
|
|
||||||
"max-len": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"code": 120,
|
|
||||||
"comments": 120
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-unused-vars": "off"
|
|
||||||
}
|
|
||||||
}
|
|
35
client/@types/global.d.ts
vendored
35
client/@types/global.d.ts
vendored
@ -12,6 +12,7 @@ declare const MUSTACHE_CONFIGURATION_CHANNEL: string
|
|||||||
// Constants that begins with "LOC_" are loaded by build-client.js, reading the english locale file.
|
// Constants that begins with "LOC_" are loaded by build-client.js, reading the english locale file.
|
||||||
// See the online documentation: https://livingston.frama.io/peertube-plugin-livechat/contributing/translate/
|
// See the online documentation: https://livingston.frama.io/peertube-plugin-livechat/contributing/translate/
|
||||||
declare const LOC_ONLINE_HELP: string
|
declare const LOC_ONLINE_HELP: string
|
||||||
|
declare const LOC_CHAT: string
|
||||||
declare const LOC_OPEN_CHAT: string
|
declare const LOC_OPEN_CHAT: string
|
||||||
declare const LOC_OPEN_CHAT_NEW_WINDOW: string
|
declare const LOC_OPEN_CHAT_NEW_WINDOW: string
|
||||||
declare const LOC_CLOSE_CHAT: string
|
declare const LOC_CLOSE_CHAT: string
|
||||||
@ -56,12 +57,12 @@ declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE: string
|
|||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC: string
|
declare const LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL: string
|
||||||
@ -133,3 +134,29 @@ 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
|
||||||
|
@ -243,7 +243,10 @@ function register (clientOptions: RegisterClientOptions): void {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
peertubeHelpers.notifier.error(error.toString(), await peertubeHelpers.translate(LOC_LOADING_ERROR))
|
peertubeHelpers.notifier.error(
|
||||||
|
(error as Error).toString(),
|
||||||
|
await peertubeHelpers.translate(LOC_LOADING_ERROR)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -270,6 +273,8 @@ 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,6 +8,7 @@ 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> {
|
||||||
@ -45,7 +46,7 @@ async function register (clientOptions: RegisterClientOptions): Promise<void> {
|
|||||||
])
|
])
|
||||||
const webchatFieldOptions: RegisterClientFormFieldOptions = {
|
const webchatFieldOptions: RegisterClientFormFieldOptions = {
|
||||||
name: 'livechat-active',
|
name: 'livechat-active',
|
||||||
label: label,
|
label,
|
||||||
descriptionHTML: description,
|
descriptionHTML: description,
|
||||||
type: 'input-checkbox',
|
type: 'input-checkbox',
|
||||||
default: true,
|
default: true,
|
||||||
@ -69,7 +70,8 @@ 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)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
131
client/common/admin/firewall/elements/admin-firewall.ts
Normal file
131
client/common/admin/firewall/elements/admin-firewall.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
5
client/common/admin/firewall/elements/index.ts
Normal file
5
client/common/admin/firewall/elements/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import './admin-firewall'
|
26
client/common/admin/firewall/register.ts
Normal file
26
client/common/admin/firewall/register.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// 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
|
||||||
|
}
|
108
client/common/admin/firewall/services/admin-firewall.ts
Normal file
108
client/common/admin/firewall/services/admin-firewall.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
91
client/common/admin/firewall/templates/admin-firewall.ts
Normal file
91
client/common/admin/firewall/templates/admin-firewall.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
// 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: boolean = false
|
public actionDisabled = false
|
||||||
|
|
||||||
private _asyncTaskRender: Task
|
private _asyncTaskRender: Task
|
||||||
|
|
||||||
@ -113,7 +113,7 @@ export class ChannelConfigurationElement extends LivechatElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly getInputValidationClass = (propertyName: string): { [key: string]: boolean } => {
|
public readonly getInputValidationClass = (propertyName: string): Record<string, boolean> => {
|
||||||
const validationErrorTypes: ValidationErrorType[] | undefined =
|
const validationErrorTypes: ValidationErrorType[] | undefined =
|
||||||
this.validationError?.properties[`${propertyName}`]
|
this.validationError?.properties[`${propertyName}`]
|
||||||
return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {}
|
return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {}
|
||||||
|
@ -30,7 +30,7 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
public validationError?: ValidationError
|
public validationError?: ValidationError
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
public actionDisabled: boolean = false
|
public actionDisabled = false
|
||||||
|
|
||||||
private _asyncTaskRender: Task
|
private _asyncTaskRender: Task
|
||||||
|
|
||||||
@ -192,7 +192,7 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
throw new Error('Invalid data')
|
throw new Error('Invalid data')
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = await this._convertImageToDataUrl(entry.url)
|
const url = await this._convertImageToDataUrl(entry.url as string)
|
||||||
const sn = entry.sn as string
|
const sn = entry.sn as string
|
||||||
|
|
||||||
const item: ChannelEmojisConfiguration['emojis']['customEmojis'][0] = {
|
const item: ChannelEmojisConfiguration['emojis']['customEmojis'][0] = {
|
||||||
@ -211,7 +211,7 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
await this.ptTranslate(LOC_ACTION_IMPORT_EMOJIS_INFO)
|
await this.ptTranslate(LOC_ACTION_IMPORT_EMOJIS_INFO)
|
||||||
)
|
)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR))
|
this.ptNotifier.error((err as Error).toString(), await this.ptTranslate(LOC_ERROR))
|
||||||
} finally {
|
} finally {
|
||||||
this.actionDisabled = false
|
this.actionDisabled = false
|
||||||
}
|
}
|
||||||
@ -250,12 +250,27 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
a.remove()
|
a.remove()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error(err)
|
this.logger.error(err)
|
||||||
this.ptNotifier.error(err.toString())
|
this.ptNotifier.error((err as Error).toString())
|
||||||
} finally {
|
} finally {
|
||||||
this.actionDisabled = false
|
this.actionDisabled = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async enableEmojisOnlyModeOnAllRooms (ev: Event): Promise<void> {
|
||||||
|
ev.preventDefault()
|
||||||
|
if (!this._channelDetailsService || !this.channelId) {
|
||||||
|
this.ptNotifier.error(await this.ptTranslate(LOC_ERROR))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this._channelDetailsService.enableEmojisOnlyModeOnAllRooms(this.channelId)
|
||||||
|
this.ptNotifier.info(await this.ptTranslate(LOC_SUCCESSFULLY_SAVED))
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
this.ptNotifier.error(await this.ptTranslate(LOC_ERROR))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes an url (or dataUrl), download the image, and converts to dataUrl.
|
* Takes an url (or dataUrl), download the image, and converts to dataUrl.
|
||||||
* @param url the url
|
* @param url the url
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
import { customElement, state } from 'lit/decorators.js'
|
import { customElement, state } from 'lit/decorators.js'
|
||||||
import { ptTr } from '../../lib/directives/translation'
|
import { ptTr } from '../../lib/directives/translation'
|
||||||
@ -50,7 +53,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}">
|
<a href="${channel.livechatConfigurationUri}" aria-hidden="true">
|
||||||
${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,6 +2,9 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import { LivechatElement } from '../../lib/elements/livechat'
|
import { LivechatElement } from '../../lib/elements/livechat'
|
||||||
import { ptTr } from '../../lib/directives/translation'
|
import { ptTr } from '../../lib/directives/translation'
|
||||||
import { html, TemplateResult } from 'lit'
|
import { html, TemplateResult } from 'lit'
|
||||||
|
@ -2,14 +2,18 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import type { ChannelConfigurationElement } from '../channel-configuration'
|
import type { ChannelConfigurationElement } from '../channel-configuration'
|
||||||
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
|
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
|
||||||
import { ptTr } from '../../../lib/directives/translation'
|
import { ptTr } from '../../../lib/directives/translation'
|
||||||
import { html, TemplateResult } from 'lit'
|
import { html, TemplateResult } from 'lit'
|
||||||
import { classMap } from 'lit/directives/class-map.js'
|
import { classMap } from 'lit/directives/class-map.js'
|
||||||
|
import { noDuplicateMaxDelay, forbidSpecialCharsMaxTolerance } from 'shared/lib/constants'
|
||||||
|
|
||||||
export function tplChannelConfiguration (el: ChannelConfigurationElement): TemplateResult {
|
export function tplChannelConfiguration (el: ChannelConfigurationElement): TemplateResult {
|
||||||
const tableHeaderList: {[key: string]: DynamicFormHeader} = {
|
const tableHeaderList: Record<string, DynamicFormHeader> = {
|
||||||
forbiddenWords: {
|
forbiddenWords: {
|
||||||
entries: {
|
entries: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL),
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL),
|
||||||
@ -20,16 +24,16 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC)
|
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC)
|
||||||
},
|
},
|
||||||
applyToModerators: {
|
applyToModerators: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL),
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL),
|
||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC)
|
description: ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL),
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL),
|
||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC)
|
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC)
|
||||||
},
|
},
|
||||||
reason: {
|
reason: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL),
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL),
|
||||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC)
|
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)
|
||||||
},
|
},
|
||||||
comments: {
|
comments: {
|
||||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL),
|
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL),
|
||||||
@ -57,7 +61,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tableSchema: {[key: string]: DynamicFormSchema} = {
|
const tableSchema: Record<string, DynamicFormSchema> = {
|
||||||
forbiddenWords: {
|
forbiddenWords: {
|
||||||
entries: {
|
entries: {
|
||||||
inputType: 'tags',
|
inputType: 'tags',
|
||||||
@ -135,6 +139,7 @@ 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 ?? ''}
|
||||||
@ -167,7 +172,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="bot"
|
name="mute_anonymous"
|
||||||
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) {
|
||||||
@ -254,6 +259,32 @@ 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=${''}
|
||||||
@ -310,6 +341,246 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
${el.renderFeedback('peertube-livechat-bot-nickname-feedback', 'bot.nickname')}
|
${el.renderFeedback('peertube-livechat-bot-nickname-feedback', 'bot.nickname')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<livechat-configuration-section-header
|
||||||
|
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_LABEL)}
|
||||||
|
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_DESC)}
|
||||||
|
.helpPage=${'documentation/user/streamers/bot/special_chars'}>
|
||||||
|
</livechat-configuration-section-header>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="forbid_special_chars"
|
||||||
|
id="peertube-livechat-forbid-special-chars"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.bot.forbidSpecialChars.enabled =
|
||||||
|
(event.target as HTMLInputElement).checked
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="1"
|
||||||
|
?checked=${el.channelConfiguration?.configuration.bot.forbidSpecialChars.enabled}
|
||||||
|
/>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_LABEL)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
${!el.channelConfiguration?.configuration.bot.forbidSpecialChars.enabled
|
||||||
|
? ''
|
||||||
|
: html`
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_LABEL)}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="special_chars_tolerance"
|
||||||
|
class=${classMap(
|
||||||
|
Object.assign(
|
||||||
|
{ 'form-control': true },
|
||||||
|
el.getInputValidationClass('bot.forbidSpecialChars.tolerance')
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
min="0"
|
||||||
|
max="${forbidSpecialCharsMaxTolerance}"
|
||||||
|
id="peertube-livechat-forbid-special-chars-tolerance"
|
||||||
|
aria-describedby="peertube-livechat-forbid-special-chars-tolerance-feedback"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.bot.forbidSpecialChars.tolerance =
|
||||||
|
Number((event.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="${el.channelConfiguration?.configuration.bot.forbidSpecialChars.tolerance ?? '0'}"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_DESC)}
|
||||||
|
</small>
|
||||||
|
${el.renderFeedback('peertube-livechat-forbid-special-chars-tolerance-feedback',
|
||||||
|
'bot.forbidSpecialChars.tolerance')
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL)}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="special_chars_reason"
|
||||||
|
class=${classMap(
|
||||||
|
Object.assign(
|
||||||
|
{ 'form-control': true },
|
||||||
|
el.getInputValidationClass('bot.forbidSpecialChars.reason')
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
id="peertube-livechat-forbid-special-chars-reason"
|
||||||
|
aria-describedby="peertube-livechat-forbid-special-chars-reason-feedback"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.bot.forbidSpecialChars.reason =
|
||||||
|
(event.target as HTMLInputElement).value
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="${el.channelConfiguration?.configuration.bot.forbidSpecialChars.reason ?? ''}"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)}
|
||||||
|
</small>
|
||||||
|
${el.renderFeedback('peertube-livechat-forbid-special-chars-reason-feedback',
|
||||||
|
'bot.forbidSpecialChars.reason')
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="forbid_special_chars_applyToModerators"
|
||||||
|
id="peertube-livechat-forbid-special-chars-applyToModerators"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.bot.forbidSpecialChars.applyToModerators =
|
||||||
|
(event.target as HTMLInputElement).checked
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="1"
|
||||||
|
?checked=${el.channelConfiguration?.configuration.bot.forbidSpecialChars.applyToModerators}
|
||||||
|
/>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL)}
|
||||||
|
</label>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
<livechat-configuration-section-header
|
||||||
|
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL)}
|
||||||
|
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DESC)}
|
||||||
|
.helpPage=${'documentation/user/streamers/bot/no_duplicate'}>
|
||||||
|
</livechat-configuration-section-header>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="no_duplicate"
|
||||||
|
id="peertube-livechat-no-duplicate"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.bot.noDuplicate.enabled =
|
||||||
|
(event.target as HTMLInputElement).checked
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="1"
|
||||||
|
?checked=${el.channelConfiguration?.configuration.bot.noDuplicate.enabled}
|
||||||
|
/>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
${!el.channelConfiguration?.configuration.bot.noDuplicate.enabled
|
||||||
|
? ''
|
||||||
|
: html`
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_LABEL)}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="no_duplicate_delay"
|
||||||
|
class=${classMap(
|
||||||
|
Object.assign(
|
||||||
|
{ 'form-control': true },
|
||||||
|
el.getInputValidationClass('bot.noDuplicate.delay')
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
min="0"
|
||||||
|
max="${noDuplicateMaxDelay.toString()}"
|
||||||
|
id="peertube-livechat-no-duplicate-delay"
|
||||||
|
aria-describedby="peertube-livechat-no-duplicate-delay-feedback"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.bot.noDuplicate.delay =
|
||||||
|
Number((event.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="${el.channelConfiguration?.configuration.bot.noDuplicate.delay ?? '0'}"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_DESC)}
|
||||||
|
</small>
|
||||||
|
${el.renderFeedback('peertube-livechat-no-duplicate-delay-feedback',
|
||||||
|
'bot.noDuplicate.delay')
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL)}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="no_duplicate_reason"
|
||||||
|
class=${classMap(
|
||||||
|
Object.assign(
|
||||||
|
{ 'form-control': true },
|
||||||
|
el.getInputValidationClass('bot.noDuplicate.reason')
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
id="peertube-livechat-no-duplicate-reason"
|
||||||
|
aria-describedby="peertube-livechat-no-duplicate-reason-feedback"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.bot.noDuplicate.reason =
|
||||||
|
(event.target as HTMLInputElement).value
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="${el.channelConfiguration?.configuration.bot.noDuplicate.reason ?? ''}"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)}
|
||||||
|
</small>
|
||||||
|
${el.renderFeedback('peertube-livechat-no-duplicate-reason-feedback',
|
||||||
|
'bot.noDuplicate.reason')
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="no_duplicate_applyToModerators"
|
||||||
|
id="peertube-livechat-no-duplicate-applyToModerators"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.bot.noDuplicate.applyToModerators =
|
||||||
|
(event.target as HTMLInputElement).checked
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="1"
|
||||||
|
?checked=${el.channelConfiguration?.configuration.bot.noDuplicate.applyToModerators}
|
||||||
|
/>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL)}
|
||||||
|
</label>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
<livechat-configuration-section-header
|
<livechat-configuration-section-header
|
||||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
|
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
|
||||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)}
|
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)}
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import type { ChannelEmojisElement } from '../channel-emojis'
|
import type { ChannelEmojisElement } from '../channel-emojis'
|
||||||
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
|
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
|
||||||
import { maxEmojisPerChannel } from 'shared/lib/emojis'
|
import { maxEmojisPerChannel } from 'shared/lib/emojis'
|
||||||
@ -45,13 +48,14 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
|
|||||||
|
|
||||||
<livechat-channel-tabs .active=${'emojis'} .channelId=${el.channelId}></livechat-channel-tabs>
|
<livechat-channel-tabs .active=${'emojis'} .channelId=${el.channelId}></livechat-channel-tabs>
|
||||||
|
|
||||||
|
<h2>${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_TITLE)}</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_DESC)}
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_DESC)}
|
||||||
<livechat-help-button .page=${'documentation/user/streamers/emojis'}>
|
<livechat-help-button .page=${'documentation/user/streamers/emojis'}>
|
||||||
</livechat-help-button>
|
</livechat-help-button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
<form role="form" @submit=${el.saveEmojis} @change=${el.resetValidation}>
|
<form role="form" @submit=${el.saveEmojis} @change=${el.resetValidation}>
|
||||||
<div class="peertube-plugin-livechat-configuration-actions">
|
<div class="peertube-plugin-livechat-configuration-actions">
|
||||||
${
|
${
|
||||||
@ -86,7 +90,7 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
|
|||||||
.maxLines=${maxEmojisPerChannel}
|
.maxLines=${maxEmojisPerChannel}
|
||||||
.validation=${el.validationError?.properties}
|
.validation=${el.validationError?.properties}
|
||||||
.validationPrefix=${'emojis'}
|
.validationPrefix=${'emojis'}
|
||||||
.rows=${el.channelEmojisConfiguration?.emojis.customEmojis}
|
.rows=${el.channelEmojisConfiguration?.emojis.customEmojis ?? []}
|
||||||
@update=${(e: CustomEvent) => {
|
@update=${(e: CustomEvent) => {
|
||||||
el.resetValidation(e)
|
el.resetValidation(e)
|
||||||
if (el.channelEmojisConfiguration) {
|
if (el.channelEmojisConfiguration) {
|
||||||
@ -106,5 +110,23 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<h2>${ptTr(LOC_EMOJI_ONLY_MODE_TITLE)}</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
${ptTr(LOC_EMOJI_ONLY_MODE_DESC_1, true)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
${ptTr(LOC_EMOJI_ONLY_MODE_DESC_2, true)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
${ptTr(LOC_EMOJI_ONLY_MODE_DESC_3, true)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="peertube-plugin-livechat-configuration-actions">
|
||||||
|
<button type="button" @click=${el.enableEmojisOnlyModeOnAllRooms}>
|
||||||
|
${ptTr(LOC_EMOJI_ONLY_ENABLE_ALL_ROOMS)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import type {
|
|||||||
import { ValidationError, ValidationErrorType } from '../../lib/models/validation'
|
import { ValidationError, ValidationErrorType } from '../../lib/models/validation'
|
||||||
import { getBaseRoute } from '../../../utils/uri'
|
import { getBaseRoute } from '../../../utils/uri'
|
||||||
import { maxEmojisPerChannel } from 'shared/lib/emojis'
|
import { maxEmojisPerChannel } from 'shared/lib/emojis'
|
||||||
import { channelTermsMaxLength } from 'shared/lib/constants'
|
import { channelTermsMaxLength, noDuplicateMaxDelay, forbidSpecialCharsMaxTolerance } from 'shared/lib/constants'
|
||||||
|
|
||||||
export class ChannelDetailsService {
|
export class ChannelDetailsService {
|
||||||
public _registerClientOptions: RegisterClientOptions
|
public _registerClientOptions: RegisterClientOptions
|
||||||
@ -67,11 +67,43 @@ export class ChannelDetailsService {
|
|||||||
// The backend will ignore those values.
|
// The backend will ignore those values.
|
||||||
if (botConf.enabled) {
|
if (botConf.enabled) {
|
||||||
propertiesError['bot.nickname'] = []
|
propertiesError['bot.nickname'] = []
|
||||||
|
propertiesError['bot.forbidSpecialChars.tolerance'] = []
|
||||||
|
propertiesError['bot.noDuplicate.delay'] = []
|
||||||
|
|
||||||
if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) {
|
if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) {
|
||||||
propertiesError['bot.nickname'].push(ValidationErrorType.WrongFormat)
|
propertiesError['bot.nickname'].push(ValidationErrorType.WrongFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (botConf.forbidSpecialChars.enabled) {
|
||||||
|
const forbidSpecialCharsTolerance = channelConfigurationOptions.bot.forbidSpecialChars.tolerance
|
||||||
|
if (
|
||||||
|
(typeof forbidSpecialCharsTolerance !== 'number') ||
|
||||||
|
isNaN(forbidSpecialCharsTolerance)
|
||||||
|
) {
|
||||||
|
propertiesError['bot.forbidSpecialChars.tolerance'].push(ValidationErrorType.WrongType)
|
||||||
|
} else if (
|
||||||
|
forbidSpecialCharsTolerance < 0 ||
|
||||||
|
forbidSpecialCharsTolerance > forbidSpecialCharsMaxTolerance
|
||||||
|
) {
|
||||||
|
propertiesError['bot.forbidSpecialChars.tolerance'].push(ValidationErrorType.NotInRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (botConf.noDuplicate.enabled) {
|
||||||
|
const noDuplicateDelay = channelConfigurationOptions.bot.noDuplicate.delay
|
||||||
|
if (
|
||||||
|
(typeof noDuplicateDelay !== 'number') ||
|
||||||
|
isNaN(noDuplicateDelay)
|
||||||
|
) {
|
||||||
|
propertiesError['bot.noDuplicate.delay'].push(ValidationErrorType.WrongType)
|
||||||
|
} else if (
|
||||||
|
noDuplicateDelay < 0 ||
|
||||||
|
noDuplicateDelay > noDuplicateMaxDelay
|
||||||
|
) {
|
||||||
|
propertiesError['bot.noDuplicate.delay'].push(ValidationErrorType.NotInRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [i, fw] of botConf.forbiddenWords.entries()) {
|
for (const [i, fw] of botConf.forbiddenWords.entries()) {
|
||||||
for (const v of fw.entries) {
|
for (const v of fw.entries) {
|
||||||
propertiesError[`bot.forbiddenWords.${i}.entries`] = []
|
propertiesError[`bot.forbiddenWords.${i}.entries`] = []
|
||||||
@ -146,7 +178,8 @@ export class ChannelDetailsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const channel of channels.data) {
|
for (const channel of channels.data) {
|
||||||
channel.livechatConfigurationUri = '/p/livechat/configuration/channel?channelId=' + encodeURIComponent(channel.id)
|
channel.livechatConfigurationUri =
|
||||||
|
'/p/livechat/configuration/channel?channelId=' + encodeURIComponent(channel.id as string | number)
|
||||||
|
|
||||||
// Note: since Peertube v6.0.0, channel.avatar is dropped, and we have to use channel.avatars.
|
// Note: since Peertube v6.0.0, channel.avatar is dropped, and we have to use channel.avatars.
|
||||||
// So, if !channel.avatar, we will search a suitable one in channel.avatars, and fill channel.avatar.
|
// So, if !channel.avatar, we will search a suitable one in channel.avatars, and fill channel.avatar.
|
||||||
@ -180,10 +213,11 @@ export class ChannelDetailsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async fetchEmojisConfiguration (channelId: number): Promise<ChannelEmojisConfiguration> {
|
public async fetchEmojisConfiguration (channelId: number): Promise<ChannelEmojisConfiguration> {
|
||||||
|
const url = getBaseRoute(this._registerClientOptions) +
|
||||||
|
'/api/configuration/channel/emojis/' +
|
||||||
|
encodeURIComponent(channelId)
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
getBaseRoute(this._registerClientOptions) +
|
url,
|
||||||
'/api/configuration/channel/emojis/' +
|
|
||||||
encodeURIComponent(channelId),
|
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: this._headers
|
headers: this._headers
|
||||||
@ -295,10 +329,11 @@ export class ChannelDetailsService {
|
|||||||
channelId: number,
|
channelId: number,
|
||||||
channelEmojis: ChannelEmojis
|
channelEmojis: ChannelEmojis
|
||||||
): Promise<ChannelEmojisConfiguration> {
|
): Promise<ChannelEmojisConfiguration> {
|
||||||
|
const url = getBaseRoute(this._registerClientOptions) +
|
||||||
|
'/api/configuration/channel/emojis/' +
|
||||||
|
encodeURIComponent(channelId)
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
getBaseRoute(this._registerClientOptions) +
|
url,
|
||||||
'/api/configuration/channel/emojis/' +
|
|
||||||
encodeURIComponent(channelId),
|
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this._headers,
|
headers: this._headers,
|
||||||
@ -312,4 +347,24 @@ export class ChannelDetailsService {
|
|||||||
|
|
||||||
return response.json()
|
return response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async enableEmojisOnlyModeOnAllRooms (channelId: number): Promise<void> {
|
||||||
|
const url = getBaseRoute(this._registerClientOptions) +
|
||||||
|
'/api/configuration/channel/emojis/' +
|
||||||
|
encodeURIComponent(channelId) +
|
||||||
|
'/enable_emoji_only'
|
||||||
|
const response = await fetch(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: this._headers
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Can\'t enable Emojis Only Mode on all rooms.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,9 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
|
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
|
||||||
export const AddSVG: string =
|
export const AddSVG =
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
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>
|
||||||
@ -13,8 +14,9 @@ export const AddSVG: string =
|
|||||||
</svg>`
|
</svg>`
|
||||||
|
|
||||||
// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/
|
// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/
|
||||||
export const RemoveSVG: string =
|
export const RemoveSVG =
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
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: string = ''
|
private _translatedValue = ''
|
||||||
private _localizationId: string = ''
|
private _localizationId = ''
|
||||||
|
|
||||||
private _allowUnsafeHTML = false
|
private _allowUnsafeHTML = false
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ export class TranslationDirective extends AsyncDirective {
|
|||||||
this._asyncUpdateTranslation().then(() => {}, () => {})
|
this._asyncUpdateTranslation().then(() => {}, () => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
public override render = (locId: string, allowHTML: boolean = false): TemplateResult | string => {
|
public override render = (locId: string, allowHTML = false): TemplateResult | string => {
|
||||||
this._localizationId = locId // TODO Check current component for context (to infer the prefix)
|
this._localizationId = locId // TODO Check current component for context (to infer the prefix)
|
||||||
|
|
||||||
this._allowUnsafeHTML = allowHTML
|
this._allowUnsafeHTML = allowHTML
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import { ptTr } from '../directives/translation'
|
import { ptTr } from '../directives/translation'
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
import { customElement, property } from 'lit/decorators.js'
|
import { customElement, property } from 'lit/decorators.js'
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import type { TagsInputElement } from './tags-input'
|
import type { TagsInputElement } from './tags-input'
|
||||||
import type { DirectiveResult } from 'lit/directive'
|
import type { DirectiveResult } from 'lit/directive'
|
||||||
import { ValidationErrorType } from '../models/validation'
|
import { ValidationErrorType } from '../models/validation'
|
||||||
@ -20,26 +23,26 @@ import { AddSVG, RemoveSVG } from '../buttons'
|
|||||||
type DynamicTableAcceptedTypes = number | string | boolean | Date | Array<number | string>
|
type DynamicTableAcceptedTypes = number | string | boolean | Date | Array<number | string>
|
||||||
|
|
||||||
type DynamicTableAcceptedInputTypes = 'textarea'
|
type DynamicTableAcceptedInputTypes = 'textarea'
|
||||||
| 'select'
|
| 'select'
|
||||||
| 'checkbox'
|
| 'checkbox'
|
||||||
| 'range'
|
| 'range'
|
||||||
| 'color'
|
| 'color'
|
||||||
| 'date'
|
| 'date'
|
||||||
| 'datetime'
|
| 'datetime'
|
||||||
| 'datetime-local'
|
| 'datetime-local'
|
||||||
| 'email'
|
| 'email'
|
||||||
| 'file'
|
| 'file'
|
||||||
| 'image'
|
| 'image'
|
||||||
| 'month'
|
| 'month'
|
||||||
| 'number'
|
| 'number'
|
||||||
| 'password'
|
| 'password'
|
||||||
| 'tel'
|
| 'tel'
|
||||||
| 'text'
|
| 'text'
|
||||||
| 'time'
|
| 'time'
|
||||||
| 'url'
|
| 'url'
|
||||||
| 'week'
|
| 'week'
|
||||||
| 'tags'
|
| 'tags'
|
||||||
| 'image-file'
|
| 'image-file'
|
||||||
|
|
||||||
interface CellDataSchema {
|
interface CellDataSchema {
|
||||||
min?: number
|
min?: number
|
||||||
@ -47,11 +50,11 @@ interface CellDataSchema {
|
|||||||
minlength?: number
|
minlength?: number
|
||||||
maxlength?: number
|
maxlength?: number
|
||||||
size?: number
|
size?: number
|
||||||
label?: TemplateResult | string
|
options?: Record<string, 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.
|
||||||
}
|
}
|
||||||
@ -59,19 +62,17 @@ interface CellDataSchema {
|
|||||||
interface DynamicTableRowData {
|
interface DynamicTableRowData {
|
||||||
_id: number
|
_id: number
|
||||||
_originalIndex: number
|
_originalIndex: number
|
||||||
row: { [key: string]: DynamicTableAcceptedTypes }
|
row: Record<string, DynamicTableAcceptedTypes>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DynamicFormHeaderCellData {
|
interface DynamicFormHeaderCellData {
|
||||||
colName: TemplateResult | DirectiveResult
|
colName: TemplateResult | DirectiveResult
|
||||||
description: TemplateResult | DirectiveResult
|
description?: TemplateResult | DirectiveResult
|
||||||
headerClassList?: string[]
|
headerClassList?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DynamicFormHeader {
|
export type DynamicFormHeader = Record<string, DynamicFormHeaderCellData>
|
||||||
[key: string]: DynamicFormHeaderCellData
|
export type DynamicFormSchema = Record<string, CellDataSchema>
|
||||||
}
|
|
||||||
export interface DynamicFormSchema { [key: string]: CellDataSchema }
|
|
||||||
|
|
||||||
@customElement('livechat-dynamic-table-form')
|
@customElement('livechat-dynamic-table-form')
|
||||||
export class DynamicTableFormElement extends LivechatElement {
|
export class DynamicTableFormElement extends LivechatElement {
|
||||||
@ -85,19 +86,19 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
public maxLines?: number = undefined
|
public maxLines?: number = undefined
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
public validation?: {[key: string]: ValidationErrorType[] }
|
public validation?: Record<string, ValidationErrorType[]>
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public validationPrefix: string = ''
|
public validationPrefix = ''
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public rows: Array<{ [key: string]: DynamicTableAcceptedTypes }> = []
|
public rows: Array<Record<string, DynamicTableAcceptedTypes>> = []
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
public _rowsById: DynamicTableRowData[] = []
|
public _rowsById: DynamicTableRowData[] = []
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public formName: string = ''
|
public formName = ''
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private _lastRowId = 1
|
private _lastRowId = 1
|
||||||
@ -112,7 +113,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly _getDefaultRow = (): { [key: string]: DynamicTableAcceptedTypes } => {
|
private readonly _getDefaultRow = (): Record<string, DynamicTableAcceptedTypes> => {
|
||||||
this._updateLastRowId()
|
this._updateLastRowId()
|
||||||
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? ''])])
|
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? ''])])
|
||||||
}
|
}
|
||||||
@ -236,7 +237,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>`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,11 +246,11 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
|
|
||||||
return html`<tr id=${inputId}>
|
return html`<tr id=${inputId}>
|
||||||
${Object.keys(this.header)
|
${Object.keys(this.header)
|
||||||
.sort((k1, k2) => this.columnOrder.indexOf(k1) - this.columnOrder.indexOf(k2))
|
.sort((k1, k2) => this.columnOrder.indexOf(k1) - this.columnOrder.indexOf(k2))
|
||||||
.map(key => this.renderDataCell(key,
|
.map(key => this.renderDataCell(key,
|
||||||
rowData.row[key] ?? this.schema[key].default,
|
rowData.row[key] ?? this.schema[key].default,
|
||||||
rowData._id,
|
rowData._id,
|
||||||
rowData._originalIndex))}
|
rowData._originalIndex))}
|
||||||
<td class="form-group">
|
<td class="form-group">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="dynamic-table-remove-row"
|
class="dynamic-table-remove-row"
|
||||||
@ -295,6 +296,7 @@ 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) {
|
||||||
@ -320,6 +322,7 @@ 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,
|
||||||
@ -332,6 +335,7 @@ 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,
|
||||||
@ -344,6 +348,7 @@ 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,
|
||||||
@ -356,6 +361,7 @@ 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(),
|
||||||
@ -376,6 +382,7 @@ 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(),
|
||||||
@ -394,6 +401,7 @@ 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,
|
||||||
@ -411,6 +419,7 @@ 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,
|
||||||
@ -446,10 +455,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)?.join(propertySchema.separator ?? ',') ?? propertyValue ?? propertySchema.default ?? '',
|
||||||
propertyValue ?? propertySchema.default ?? '',
|
|
||||||
originalIndex)}
|
originalIndex)}
|
||||||
${feedback}
|
${feedback}
|
||||||
`
|
`
|
||||||
@ -461,10 +470,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)?.join(propertySchema.separator ?? ',') ?? propertyValue ?? propertySchema.default ?? '',
|
||||||
propertyValue ?? propertySchema.default ?? '',
|
|
||||||
originalIndex)}
|
originalIndex)}
|
||||||
${feedback}
|
${feedback}
|
||||||
`
|
`
|
||||||
@ -476,6 +485,7 @@ 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,
|
||||||
@ -487,8 +497,10 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!formElement) {
|
if (!formElement) {
|
||||||
this.logger.warn(`value type '${(propertyValue.constructor.toString())}' is incompatible` +
|
this.logger.warn(
|
||||||
`with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`)
|
`value type '${(propertyValue.constructor.toString())}' is incompatible` +
|
||||||
|
`with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const classList = ['form-group']
|
const classList = ['form-group']
|
||||||
@ -501,6 +513,7 @@ 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,
|
||||||
@ -515,6 +528,7 @@ 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)}
|
||||||
@ -534,6 +548,7 @@ 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>,
|
||||||
@ -547,7 +562,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
.inputPlaceholder=${propertySchema.label as any}
|
.inputTitle=${inputTitle as any}
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
.min=${propertySchema.min}
|
.min=${propertySchema.min}
|
||||||
.max=${propertySchema.max}
|
.max=${propertySchema.max}
|
||||||
@ -563,6 +578,7 @@ 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,
|
||||||
@ -576,6 +592,7 @@ 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)}
|
||||||
@ -588,6 +605,7 @@ 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,
|
||||||
@ -602,6 +620,7 @@ 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"
|
||||||
@ -611,6 +630,7 @@ 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,
|
||||||
@ -623,11 +643,12 @@ 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}>${propertySchema.label ?? 'Choose your option'}</option>
|
<option ?selected=${!propertyValue}>${inputTitle ?? ''}</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>`
|
||||||
@ -638,6 +659,7 @@ 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,
|
||||||
@ -647,6 +669,7 @@ 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}
|
||||||
@ -656,7 +679,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getInputValidationClass = (propertyName: string,
|
_getInputValidationClass = (propertyName: string,
|
||||||
originalIndex: number): { [key: string]: boolean } => {
|
originalIndex: number): Record<string, boolean> => {
|
||||||
const validationErrorTypes: ValidationErrorType[] | undefined =
|
const validationErrorTypes: ValidationErrorType[] | undefined =
|
||||||
this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`]
|
this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`]
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ export class HelpButtonElement extends LivechatElement {
|
|||||||
public buttonTitle: string | DirectiveResult = ptTr(LOC_ONLINE_HELP)
|
public buttonTitle: string | DirectiveResult = ptTr(LOC_ONLINE_HELP)
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public page: string = ''
|
public page = ''
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
public url: URL = new URL('https://lmddgtfy.net/')
|
public url: URL = new URL('https://lmddgtfy.net/')
|
||||||
|
@ -2,10 +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 { 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.
|
||||||
@ -29,13 +33,16 @@ 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} @click=${(ev: Event) => {
|
? html`<img src=${this.value} alt=${ifDefined(this.inputTitle)} @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()
|
||||||
@ -44,6 +51,7 @@ 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 = (): Element | ShadowRoot => {
|
protected override createRenderRoot = (): HTMLElement | DocumentFragment => {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import { LivechatElement } from './livechat'
|
import { LivechatElement } from './livechat'
|
||||||
import { ptTr } from '../directives/translation'
|
import { ptTr } from '../directives/translation'
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
@ -12,6 +15,7 @@ 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.
|
||||||
@ -20,10 +24,11 @@ import { repeat } from 'lit/directives/repeat.js'
|
|||||||
// Then replace the main color by «currentColor»
|
// Then replace the main color by «currentColor»
|
||||||
const copySVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 4.233 4.233">
|
const copySVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 4.233 4.233">
|
||||||
<g style="stroke-width:1.00021;stroke-miterlimit:4;stroke-dasharray:none">` +
|
<g style="stroke-width:1.00021;stroke-miterlimit:4;stroke-dasharray:none">` +
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len, @stylistic/indent-binary-ops
|
||||||
'<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m4.084 4.046-.616.015-.645-.004a.942.942 0 0 1-.942-.942v-4.398a.94.94 0 0 1 .942-.943H7.22a.94.94 0 0 1 .942.943l-.006.334-.08.962" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' +
|
'<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m4.084 4.046-.616.015-.645-.004a.942.942 0 0 1-.942-.942v-4.398a.94.94 0 0 1 .942-.943H7.22a.94.94 0 0 1 .942.943l-.006.334-.08.962" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' +
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
'<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M8.434 5.85c-.422.009-1.338.009-1.76.01-.733.004-2.199 0-2.199 0a.94.94 0 0 1-.942-.941V.52a.94.94 0 0 1 .942-.942h4.398a.94.94 0 0 1 .943.942s.004 1.466 0 2.2c-.003.418-.019 1.251-.006 1.67.024.812-.382 1.439-1.376 1.46z" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' +
|
'<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M8.434 5.85c-.422.009-1.338.009-1.76.01-.733.004-2.199 0-2.199 0a.94.94 0 0 1-.942-.941V.52a.94.94 0 0 1 .942-.942h4.398a.94.94 0 0 1 .943.942s.004 1.466 0 2.2c-.003.418-.019 1.251-.006 1.67.024.812-.382 1.439-1.376 1.46z" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' +
|
||||||
|
// eslint-disable-next-line @stylistic/indent-binary-ops
|
||||||
`</g>
|
`</g>
|
||||||
</svg>`
|
</svg>`
|
||||||
|
|
||||||
@ -48,7 +53,7 @@ export class TagsInputElement extends LivechatElement {
|
|||||||
private _inputValue?: string = ''
|
private _inputValue?: string = ''
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public inputPlaceholder?: string = ''
|
public inputTitle?: string | DirectiveResult = ''
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public datalist?: string[]
|
public datalist?: string[]
|
||||||
@ -63,10 +68,10 @@ export class TagsInputElement extends LivechatElement {
|
|||||||
private readonly _isPressingKey: string[] = []
|
private readonly _isPressingKey: string[] = []
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public separator: string = '\n'
|
public separator = '\n'
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public animDuration: number = 200
|
public animDuration = 200
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overloading the standard focus method.
|
* Overloading the standard focus method.
|
||||||
@ -166,7 +171,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 ?? ''}
|
||||||
placeholder=${ifDefined(this.inputPlaceholder)} />
|
title=${ifDefined(this.inputTitle)} />
|
||||||
${(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}>`)}
|
||||||
@ -244,8 +249,9 @@ export class TagsInputElement extends LivechatElement {
|
|||||||
if (!this._isPressingKey.includes(e.key)) {
|
if (!this._isPressingKey.includes(e.key)) {
|
||||||
this._isPressingKey.push(e.key)
|
this._isPressingKey.push(e.key)
|
||||||
|
|
||||||
if ((target.selectionStart === target.selectionEnd) &&
|
if (
|
||||||
target.selectionStart === 0) {
|
(target.selectionStart === target.selectionEnd) && target.selectionStart === 0
|
||||||
|
) {
|
||||||
this._handleDeleteTag((this._searchedTagsIndex.length)
|
this._handleDeleteTag((this._searchedTagsIndex.length)
|
||||||
? this._searchedTagsIndex.slice(-1)[0]
|
? this._searchedTagsIndex.slice(-1)[0]
|
||||||
: (this.value.length - 1))
|
: (this.value.length - 1))
|
||||||
@ -258,8 +264,9 @@ export class TagsInputElement extends LivechatElement {
|
|||||||
if (!this._isPressingKey.includes(e.key)) {
|
if (!this._isPressingKey.includes(e.key)) {
|
||||||
this._isPressingKey.push(e.key)
|
this._isPressingKey.push(e.key)
|
||||||
|
|
||||||
if ((target.selectionStart === target.selectionEnd) &&
|
if (
|
||||||
target.selectionStart === target.value.length) {
|
(target.selectionStart === target.selectionEnd) && target.selectionStart === target.value.length
|
||||||
|
) {
|
||||||
this._handleDeleteTag((this._searchedTagsIndex.length)
|
this._handleDeleteTag((this._searchedTagsIndex.length)
|
||||||
? this._searchedTagsIndex[0]
|
? this._searchedTagsIndex[0]
|
||||||
: 0)
|
: 0)
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import type { LivechatTokenListElement } from '../token-list'
|
import type { LivechatTokenListElement } from '../token-list'
|
||||||
import { html, TemplateResult } from 'lit'
|
import { html, TemplateResult } from 'lit'
|
||||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
|
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
|
||||||
@ -23,11 +26,11 @@ export function tplTokenList (el: LivechatTokenListElement): TemplateResult {
|
|||||||
<tbody>
|
<tbody>
|
||||||
${
|
${
|
||||||
repeat(el.tokenList ?? [], (token) => token.id, (token) => {
|
repeat(el.tokenList ?? [], (token) => token.id, (token) => {
|
||||||
let dateStr: string = ''
|
let dateStr = ''
|
||||||
try {
|
try {
|
||||||
const date = new Date(token.date)
|
const date = new Date(token.date)
|
||||||
dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
||||||
} catch (err) {}
|
} catch (_err) {}
|
||||||
return html`<tr>
|
return html`<tr>
|
||||||
<td>${
|
<td>${
|
||||||
el.mode === 'select'
|
el.mode === 'select'
|
||||||
|
@ -27,7 +27,7 @@ export class LivechatTokenListElement extends LivechatElement {
|
|||||||
public currentSelectedToken?: LivechatToken
|
public currentSelectedToken?: LivechatToken
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public actionDisabled: boolean = false
|
public actionDisabled = false
|
||||||
|
|
||||||
private readonly _tokenListService: TokenListService
|
private readonly _tokenListService: TokenListService
|
||||||
private readonly _asyncTaskRender: Task
|
private readonly _asyncTaskRender: Task
|
||||||
@ -83,7 +83,7 @@ export class LivechatTokenListElement extends LivechatElement {
|
|||||||
this.dispatchEvent(new CustomEvent('update', {}))
|
this.dispatchEvent(new CustomEvent('update', {}))
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error(err)
|
this.logger.error(err)
|
||||||
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR))
|
this.ptNotifier.error((err as Error).toString(), await this.ptTranslate(LOC_ERROR))
|
||||||
} finally {
|
} finally {
|
||||||
this.actionDisabled = false
|
this.actionDisabled = false
|
||||||
}
|
}
|
||||||
@ -102,7 +102,7 @@ export class LivechatTokenListElement extends LivechatElement {
|
|||||||
this.dispatchEvent(new CustomEvent('update', {}))
|
this.dispatchEvent(new CustomEvent('update', {}))
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.error(err)
|
this.logger.error(err)
|
||||||
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR))
|
this.ptNotifier.error((err as Error).toString(), await this.ptTranslate(LOC_ERROR))
|
||||||
} finally {
|
} finally {
|
||||||
this.actionDisabled = false
|
this.actionDisabled = false
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ export enum ValidationErrorType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ValidationError extends Error {
|
export class ValidationError extends Error {
|
||||||
properties: {[key: string]: ValidationErrorType[] } = {}
|
properties: Record<string, ValidationErrorType[]> = {}
|
||||||
|
|
||||||
constructor (name: string, message: string | undefined, properties: ValidationError['properties']) {
|
constructor (name: string, message: string | undefined, properties: ValidationError['properties']) {
|
||||||
super(message)
|
super(message)
|
||||||
|
@ -42,6 +42,23 @@ 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'
|
||||||
}
|
}
|
||||||
@ -52,6 +69,10 @@ 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,8 +16,6 @@ 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
|
||||||
@ -25,7 +23,6 @@ let savedMyPluginFlexGrow: string | undefined
|
|||||||
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')
|
||||||
@ -46,6 +43,8 @@ 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 {
|
||||||
@ -61,8 +60,8 @@ async function initChat (video: Video): Promise<void> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let showShareUrlButton: boolean = false
|
let showShareUrlButton = false
|
||||||
let showPromote: boolean = false
|
let showPromote = false
|
||||||
if (video.isLocal) { // No need for shareButton on remote chats.
|
if (video.isLocal) { // No need for shareButton on remote chats.
|
||||||
const chatShareUrl = settings['chat-share-url'] ?? ''
|
const chatShareUrl = settings['chat-share-url'] ?? ''
|
||||||
if (chatShareUrl === 'everyone') {
|
if (chatShareUrl === 'everyone') {
|
||||||
@ -188,9 +187,10 @@ async function _insertChatDom (
|
|||||||
callback: async () => {
|
callback: async () => {
|
||||||
try {
|
try {
|
||||||
// First we must get the room JID (can be video.uuid@ or channel.id@)
|
// First we must get the room JID (can be video.uuid@ or channel.id@)
|
||||||
|
const url = getBaseRoute(ptContext.ptOptions) + '/api/configuration/room/' +
|
||||||
|
encodeURIComponent(video.uuid)
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
getBaseRoute(ptContext.ptOptions) + '/api/configuration/room/' +
|
url,
|
||||||
encodeURIComponent(video.uuid),
|
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: peertubeHelpers.getAuthHeader()
|
headers: peertubeHelpers.getAuthHeader()
|
||||||
@ -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,19 +353,6 @@ 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 = ['embed', 'dock', 'peertube', 'xmpp'] as const
|
const validTabNames: string[] = ['embed', 'dock', 'peertube', 'xmpp'] as const
|
||||||
|
|
||||||
type ValidTabNames = typeof validTabNames[number]
|
type ValidTabNames = typeof validTabNames[number]
|
||||||
|
|
||||||
@ -61,49 +61,49 @@ export class ShareChatElement extends LivechatElement {
|
|||||||
* Should we render the XMPP tab?
|
* Should we render the XMPP tab?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public xmppUriEnabled: boolean = false
|
public xmppUriEnabled = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should we render the Dock tab?
|
* Should we render the Dock tab?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public dockEnabled: boolean = false
|
public dockEnabled = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can we use autocolors?
|
* Can we use autocolors?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public autocolorsAvailable: boolean = false
|
public autocolorsAvailable = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In the Embed tab, should we generated an iframe link.
|
* In the Embed tab, should we generated an iframe link.
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public embedIFrame: boolean = false
|
public embedIFrame = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In the Embed tab, should we generated a read-only chat link.
|
* In the Embed tab, should we generated a read-only chat link.
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public embedReadOnly: boolean = false
|
public embedReadOnly = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read-only, with scrollbar?
|
* Read-only, with scrollbar?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public embedReadOnlyScrollbar: boolean = false
|
public embedReadOnlyScrollbar = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read-only, transparent background?
|
* Read-only, transparent background?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public embedReadOnlyTransparentBackground: boolean = false
|
public embedReadOnlyTransparentBackground = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In the Embed tab, should we use current theme color?
|
* In the Embed tab, should we use current theme color?
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public embedAutocolors: boolean = false
|
public embedAutocolors = false
|
||||||
|
|
||||||
protected override firstUpdated (changedProperties: PropertyValues): void {
|
protected override firstUpdated (changedProperties: PropertyValues): void {
|
||||||
super.firstUpdated(changedProperties)
|
super.firstUpdated(changedProperties)
|
||||||
@ -156,7 +156,7 @@ export class ShareChatElement extends LivechatElement {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.logger.log('Restoring previous state')
|
this.logger.log('Restoring previous state')
|
||||||
if (validTabNames.includes(v.currentTab)) {
|
if (validTabNames.includes(v.currentTab as string)) {
|
||||||
this.currentTab = v.currentTab
|
this.currentTab = v.currentTab
|
||||||
}
|
}
|
||||||
this.embedIFrame = !!v.embedIFrame
|
this.embedIFrame = !!v.embedIFrame
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import type { ShareChatElement } from '../share-chat'
|
import type { ShareChatElement } from '../share-chat'
|
||||||
import { html, TemplateResult } from 'lit'
|
import { html, TemplateResult } from 'lit'
|
||||||
import { ptTr } from '../../../lib/directives/translation'
|
import { ptTr } from '../../../lib/directives/translation'
|
||||||
|
@ -71,8 +71,7 @@ async function shareChatUrl (
|
|||||||
addedNodes.forEach(node => {
|
addedNodes.forEach(node => {
|
||||||
if ((node as HTMLElement).localName === 'ngb-modal-window') {
|
if ((node as HTMLElement).localName === 'ngb-modal-window') {
|
||||||
logger.info('Detecting a new modal, checking if this is the good one...')
|
logger.info('Detecting a new modal, checking if this is the good one...')
|
||||||
if (!(node as HTMLElement).querySelector) { return }
|
const title = (node as HTMLElement).querySelector?.('.modal-title')
|
||||||
const title = (node as HTMLElement).querySelector('.modal-title')
|
|
||||||
if (!(title?.textContent === labelShare)) {
|
if (!(title?.textContent === labelShare)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
||||||
import type { Video } from '@peertube/peertube-types'
|
import type { Video } from '@peertube/peertube-types'
|
||||||
|
import type { LiveChatSettings } from '../lib/contexts/peertube'
|
||||||
import { AutoColors, isAutoColorsAvailable } from 'shared/lib/autocolors'
|
import { AutoColors, isAutoColorsAvailable } from 'shared/lib/autocolors'
|
||||||
import { getBaseRoute } from '../../utils/uri'
|
import { getBaseRoute } from '../../utils/uri'
|
||||||
import { logger } from '../../utils/logger'
|
import { logger } from '../../utils/logger'
|
||||||
@ -17,7 +18,7 @@ interface UriOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getIframeUri (
|
function getIframeUri (
|
||||||
registerOptions: RegisterClientOptions, settings: any, video: Video, uriOptions: UriOptions = {}
|
registerOptions: RegisterClientOptions, settings: LiveChatSettings, video: Video, uriOptions: UriOptions = {}
|
||||||
): string | null {
|
): string | null {
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
logger.error('Settings are not initialized, too soon to compute the iframeUri')
|
logger.error('Settings are not initialized, too soon to compute the iframeUri')
|
||||||
|
@ -156,12 +156,12 @@ function launchTests (): void {
|
|||||||
'content-type': 'application/json;charset=UTF-8'
|
'content-type': 'application/json;charset=UTF-8'
|
||||||
}),
|
}),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
test: test
|
test
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return {
|
return {
|
||||||
test: test,
|
test,
|
||||||
messages: [response.statusText ?? 'Unknown error'],
|
messages: [response.statusText ?? 'Unknown error'],
|
||||||
ok: false
|
ok: false
|
||||||
}
|
}
|
||||||
@ -169,7 +169,7 @@ function launchTests (): void {
|
|||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
if ((typeof data) !== 'object') {
|
if ((typeof data) !== 'object') {
|
||||||
return {
|
return {
|
||||||
test: test,
|
test,
|
||||||
messages: ['Incorrect reponse type: ' + (typeof data)],
|
messages: ['Incorrect reponse type: ' + (typeof data)],
|
||||||
ok: false
|
ok: false
|
||||||
}
|
}
|
||||||
@ -190,6 +190,7 @@ function launchTests (): void {
|
|||||||
waiting.innerHTML = '<i>Testing...</i>'
|
waiting.innerHTML = '<i>Testing...</i>'
|
||||||
ul.append(waiting)
|
ul.append(waiting)
|
||||||
if ((typeof result.next) === 'function') {
|
if ((typeof result.next) === 'function') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
const r: Result = (result.next as Function)()
|
const r: Result = (result.next as Function)()
|
||||||
waiting.remove()
|
waiting.remove()
|
||||||
await machine(r)
|
await machine(r)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
||||||
|
|
||||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
||||||
import type { InitConverseJSParams, ChatPeertubeIncludeMode } from 'shared/lib/types'
|
import type { InitConverseJSParams, ChatPeertubeIncludeMode } from 'shared/lib/types'
|
||||||
@ -17,7 +18,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let pollListenerInitiliazed: boolean = false
|
let pollListenerInitiliazed = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* load the ConverseJS CSS.
|
* load the ConverseJS CSS.
|
||||||
@ -152,10 +153,11 @@ async function displayConverseJS (
|
|||||||
|
|
||||||
const authHeader = peertubeHelpers.getAuthHeader()
|
const authHeader = peertubeHelpers.getAuthHeader()
|
||||||
|
|
||||||
|
const url = getBaseRoute(clientOptions) + '/api/configuration/room/' +
|
||||||
|
encodeURIComponent(roomKey) +
|
||||||
|
(forceType ? '?forcetype=1' : '')
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
getBaseRoute(clientOptions) + '/api/configuration/room/' +
|
url,
|
||||||
encodeURIComponent(roomKey) +
|
|
||||||
(forceType ? '?forcetype=1' : ''),
|
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: authHeader
|
headers: authHeader
|
||||||
@ -167,7 +169,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, initiliaze this event:
|
// First time we got here, initialize 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: boolean = false): string {
|
function getBaseRoute ({ peertubeHelpers }: RegisterClientOptions, permanent = false): string {
|
||||||
if (permanent) {
|
if (permanent) {
|
||||||
return '/plugins/livechat/router'
|
return '/plugins/livechat/router'
|
||||||
}
|
}
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"standard-with-typescript"
|
|
||||||
],
|
|
||||||
"globals": {},
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 2018,
|
|
||||||
"project": [
|
|
||||||
"./conversejs/tsconfig.json"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"@typescript-eslint"
|
|
||||||
],
|
|
||||||
"ignorePatterns": [],
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/no-unused-vars": [2, {"argsIgnorePattern": "^_"}],
|
|
||||||
"@typescript-eslint/no-floating-promises": "error",
|
|
||||||
"@typescript-eslint/no-misused-promises": "error",
|
|
||||||
"@typescript-eslint/no-var-requires": "off",
|
|
||||||
"@typescript-eslint/strict-boolean-expressions": "off",
|
|
||||||
"@typescript-eslint/return-await": [2, "in-try-catch"], // FIXME: correct?
|
|
||||||
"@typescript-eslint/no-invalid-void-type": "off",
|
|
||||||
"@typescript-eslint/triple-slash-reference": "off",
|
|
||||||
"max-len": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"code": 120,
|
|
||||||
"comments": 120
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-unused-vars": "off"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
|
@ -4,6 +4,7 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
const fs = require('node:fs')
|
const fs = require('node:fs')
|
||||||
const path = require('node:path')
|
const path = require('node:path')
|
||||||
const YAML = require('yaml')
|
const YAML = require('yaml')
|
||||||
|
@ -15,32 +15,22 @@ 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="v10.1.6"
|
CONVERSE_VERSION="v11.0.0"
|
||||||
CONVERSE_REPO="https://github.com/conversejs/converse.js.git"
|
CONVERSE_REPO="https://github.com/conversejs/converse.js.git"
|
||||||
# You can eventually set CONVERSE_COMMIT to a specific commit ID, if you want to apply some patches.
|
# You can eventually set CONVERSE_COMMIT to a specific commit ID, if you want to apply some patches.
|
||||||
CONVERSE_COMMIT=""
|
# 2024-09-17: using Converse upstream (v11 WIP).
|
||||||
|
CONVERSE_COMMIT="07dc6f4f5da5890b02a46a8a2f2d0498649786bc"
|
||||||
|
|
||||||
# 2014-01-16: we are using a custom version, to wait for some PR to be apply upstream.
|
# It is possible to use another repository, if we want some customization that are not upstream (yet):
|
||||||
# This version includes following changes:
|
# CONVERSE_VERSION="livechat"
|
||||||
# - #converse.js/3300: Adding the maxWait option for `debouncedPruneHistory`
|
# # CONVERSE_COMMIT="4402fcc3fc60f6c9334f86528c33a0b463371d12"
|
||||||
# - #converse.js/3302: debounce MUC sidebar rendering
|
# CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js"
|
||||||
# - Fix: refresh the MUC sidebar when participants collection is sorted
|
# CONVERSE_COMMIT="xxxx"
|
||||||
# - Fix: MUC occupant list does not sort itself on nicknames or roles changes
|
|
||||||
# - Fix inconsistency between browsers on textarea outlines
|
# 2024-09-03: include badges short label and quick fix for sendMessage button
|
||||||
# - 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"
|
CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js"
|
||||||
|
CONVERSE_VERSION="livechat-12.0.0"
|
||||||
|
# CONVERSE_COMMIT=""
|
||||||
|
|
||||||
rootdir="$(pwd)"
|
rootdir="$(pwd)"
|
||||||
src_dir="$rootdir/conversejs"
|
src_dir="$rootdir/conversejs"
|
||||||
@ -50,6 +40,7 @@ if [ -n "$CONVERSE_COMMIT" ]; then
|
|||||||
fi
|
fi
|
||||||
converse_build_dir="$rootdir/build/conversejs"
|
converse_build_dir="$rootdir/build/conversejs"
|
||||||
converse_destination_dir="$rootdir/dist/client/conversejs"
|
converse_destination_dir="$rootdir/dist/client/conversejs"
|
||||||
|
converse_emoji_destination="$rootdir/dist/converse-emoji.json"
|
||||||
|
|
||||||
if [[ ! -d $src_dir ]]; then
|
if [[ ! -d $src_dir ]]; then
|
||||||
echo "$0 must be called from the plugin livechat root dir."
|
echo "$0 must be called from the plugin livechat root dir."
|
||||||
@ -129,6 +120,9 @@ cd $rootdir
|
|||||||
echo "Copying ConverseJS dist files..."
|
echo "Copying ConverseJS dist files..."
|
||||||
mkdir -p "$converse_destination_dir" && cp -r $converse_build_dir/dist/* "$converse_destination_dir/"
|
mkdir -p "$converse_destination_dir" && cp -r $converse_build_dir/dist/* "$converse_destination_dir/"
|
||||||
|
|
||||||
|
echo "Copying ConverseJS original emoji.json file..." # this is needed for some backend code.
|
||||||
|
cp "$converse_build_dir/src/headless/plugins/emoji/emoji.json" "$converse_emoji_destination"
|
||||||
|
|
||||||
echo "ConverseJS OK."
|
echo "ConverseJS OK."
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
||||||
|
|
||||||
import type { InitConverseJSParams, ChatIncludeMode, ExternalAuthResult } from 'shared/lib/types'
|
import type { InitConverseJSParams, ChatIncludeMode, ExternalAuthResult } from 'shared/lib/types'
|
||||||
import { inIframe } from './lib/utils'
|
import { inIframe } from './lib/utils'
|
||||||
import { initDom } from './lib/dom'
|
import { initDom } from './lib/dom'
|
||||||
@ -21,6 +23,7 @@ import { livechatViewerModePlugin } from './lib/plugins/livechat-viewer-mode'
|
|||||||
import { livechatMiniMucHeadPlugin } from './lib/plugins/livechat-mini-muc-head'
|
import { livechatMiniMucHeadPlugin } from './lib/plugins/livechat-mini-muc-head'
|
||||||
import { livechatEmojisPlugin } from './lib/plugins/livechat-emojis'
|
import { livechatEmojisPlugin } from './lib/plugins/livechat-emojis'
|
||||||
import { moderationDelayPlugin } from './lib/plugins/moderation-delay'
|
import { moderationDelayPlugin } from './lib/plugins/moderation-delay'
|
||||||
|
import { livechatAnnouncementsPlugin } from './lib/plugins/livechat-announcements'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -34,11 +37,18 @@ 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?: (room: string) => void
|
reconnectConverse?: (params: any) => void
|
||||||
externalAuthGetResult?: (data: ExternalAuthResult) => void
|
externalAuthGetResult?: (data: ExternalAuthResult) => void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,6 +83,8 @@ function initConversePlugins (peertubeEmbedded: boolean): void {
|
|||||||
converse.plugins.add('livechatViewerModePlugin', livechatViewerModePlugin)
|
converse.plugins.add('livechatViewerModePlugin', livechatViewerModePlugin)
|
||||||
|
|
||||||
converse.plugins.add('converse-moderation-delay', moderationDelayPlugin)
|
converse.plugins.add('converse-moderation-delay', moderationDelayPlugin)
|
||||||
|
|
||||||
|
converse.plugins.add('livechatAnnouncementsPlugin', livechatAnnouncementsPlugin)
|
||||||
}
|
}
|
||||||
window.initConversePlugins = initConversePlugins
|
window.initConversePlugins = initConversePlugins
|
||||||
|
|
||||||
@ -85,7 +97,7 @@ window.initConversePlugins = initConversePlugins
|
|||||||
async function initConverse (
|
async function initConverse (
|
||||||
initConverseParams: InitConverseJSParams,
|
initConverseParams: InitConverseJSParams,
|
||||||
chatIncludeMode: ChatIncludeMode = 'chat-only',
|
chatIncludeMode: ChatIncludeMode = 'chat-only',
|
||||||
peertubeAuthHeader?: { [header: string]: string } | null
|
peertubeAuthHeader?: Record<string, string> | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// First, fixing relative websocket urls.
|
// First, fixing relative websocket urls.
|
||||||
if (initConverseParams.localWebsocketServiceUrl?.startsWith('/')) {
|
if (initConverseParams.localWebsocketServiceUrl?.startsWith('/')) {
|
||||||
@ -120,9 +132,9 @@ async function initConverse (
|
|||||||
params.view_mode = chatIncludeMode === 'chat-only' ? 'fullscreen' : 'embedded'
|
params.view_mode = chatIncludeMode === 'chat-only' ? 'fullscreen' : 'embedded'
|
||||||
params.allow_url_history_change = chatIncludeMode === 'chat-only'
|
params.allow_url_history_change = chatIncludeMode === 'chat-only'
|
||||||
|
|
||||||
let isAuthenticated: boolean = false
|
let isAuthenticated = false
|
||||||
let isAuthenticatedWithExternalAccount: boolean = false
|
let isAuthenticatedWithExternalAccount = false
|
||||||
let isRemoteWithNicknameSet: boolean = false
|
let isRemoteWithNicknameSet = false
|
||||||
|
|
||||||
// OIDC (OpenID Connect):
|
// OIDC (OpenID Connect):
|
||||||
const tryOIDC = (initConverseParams.externalAuthOIDC?.length ?? 0) > 0
|
const tryOIDC = (initConverseParams.externalAuthOIDC?.length ?? 0) > 0
|
||||||
@ -218,20 +230,24 @@ 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).
|
||||||
let enableTask = false
|
// Same for the moderator notes app.
|
||||||
|
let enableApps = false
|
||||||
if (chatIncludeMode === 'peertube-video' || chatIncludeMode === 'peertube-fullpage') {
|
if (chatIncludeMode === 'peertube-video' || chatIncludeMode === 'peertube-fullpage') {
|
||||||
enableTask = true
|
enableApps = true
|
||||||
} else if (
|
} else if (
|
||||||
chatIncludeMode === 'chat-only' &&
|
chatIncludeMode === 'chat-only' &&
|
||||||
usedLivechatToken &&
|
usedLivechatToken &&
|
||||||
!initConverseParams.transparent &&
|
!initConverseParams.transparent &&
|
||||||
!initConverseParams.forceReadonly
|
!initConverseParams.forceReadonly
|
||||||
) {
|
) {
|
||||||
enableTask = true
|
enableApps = true
|
||||||
}
|
}
|
||||||
if (enableTask) {
|
if (enableApps) {
|
||||||
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,14 +8,13 @@
|
|||||||
* @description This files will override the original ConverseJS index.js file.
|
* @description This files will override the original ConverseJS index.js file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import '@converse/headless'
|
import 'shared/styles/index.scss'
|
||||||
|
|
||||||
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/core'
|
import { _converse, converse } from '@converse/headless'
|
||||||
|
|
||||||
import 'shared/styles/index.scss'
|
|
||||||
|
|
||||||
/* START: Removable plugins
|
/* START: Removable plugins
|
||||||
* ------------------------
|
* ------------------------
|
||||||
@ -45,11 +44,16 @@ 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):
|
||||||
@ -57,11 +61,14 @@ 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.CustomElement = CustomElement
|
_converse.exports.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/core.js'
|
import { api } from '@converse/headless/index.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'
|
||||||
|
61
conversejs/custom/livechat-patch-vcard.js
Normal file
61
conversejs/custom/livechat-patch-vcard.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// 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))
|
||||||
|
})
|
||||||
|
}
|
112
conversejs/custom/plugins/mam-search/api.js
Normal file
112
conversejs/custom/plugins/mam-search/api.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
// 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
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
// 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)
|
@ -0,0 +1,82 @@
|
|||||||
|
// 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)
|
@ -0,0 +1,28 @@
|
|||||||
|
// 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)
|
5
conversejs/custom/plugins/mam-search/constants.js
Normal file
5
conversejs/custom/plugins/mam-search/constants.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// 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'
|
33
conversejs/custom/plugins/mam-search/index.js
Normal file
33
conversejs/custom/plugins/mam-search/index.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// 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?
|
||||||
|
}
|
||||||
|
})
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
// 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)
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
// 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>`
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
// 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>`
|
||||||
|
}
|
94
conversejs/custom/plugins/mam-search/utils.js
Normal file
94
conversejs/custom/plugins/mam-search/utils.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
// 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
|
||||||
|
}
|
35
conversejs/custom/plugins/notes/api.js
Normal file
35
conversejs/custom/plugins/notes/api.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// 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
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
// 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)
|
@ -0,0 +1,29 @@
|
|||||||
|
// 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)
|
110
conversejs/custom/plugins/notes/components/muc-note-view.js
Normal file
110
conversejs/custom/plugins/notes/components/muc-note-view.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// 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)
|
133
conversejs/custom/plugins/notes/components/muc-notes-view.js
Normal file
133
conversejs/custom/plugins/notes/components/muc-notes-view.js
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
// 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)
|
5
conversejs/custom/plugins/notes/constants.js
Normal file
5
conversejs/custom/plugins/notes/constants.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// 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'
|
69
conversejs/custom/plugins/notes/index.js
Normal file
69
conversejs/custom/plugins/notes/index.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// 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)
|
||||||
|
}
|
||||||
|
})
|
51
conversejs/custom/plugins/notes/note-pubsub-manager.js
Normal file
51
conversejs/custom/plugins/notes/note-pubsub-manager.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// 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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
82
conversejs/custom/plugins/notes/note.js
Normal file
82
conversejs/custom/plugins/notes/note.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// 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
|
||||||
|
}
|
54
conversejs/custom/plugins/notes/notes.js
Normal file
54
conversejs/custom/plugins/notes/notes.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// 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
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
conversejs/custom/plugins/notes/styles/muc-note.scss
Normal file
43
conversejs/custom/plugins/notes/styles/muc-note.scss
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
conversejs/custom/plugins/notes/styles/muc-notes.scss
Normal file
38
conversejs/custom/plugins/notes/styles/muc-notes.scss
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
conversejs/custom/plugins/notes/templates/muc-note-app.js
Normal file
42
conversejs/custom/plugins/notes/templates/muc-note-app.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// 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>`
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
// 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>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
133
conversejs/custom/plugins/notes/templates/muc-note.js
Normal file
133
conversejs/custom/plugins/notes/templates/muc-note.js
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
// 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>`
|
||||||
|
}
|
95
conversejs/custom/plugins/notes/templates/muc-notes.js
Normal file
95
conversejs/custom/plugins/notes/templates/muc-notes.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// 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>`
|
||||||
|
}
|
195
conversejs/custom/plugins/notes/utils.js
Normal file
195
conversejs/custom/plugins/notes/utils.js
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { XMLNS_NOTE } from './constants.js'
|
||||||
|
import { NotePubSubManager } from './note-pubsub-manager.js'
|
||||||
|
import { converse, _converse, api } from '../../../src/headless/index.js'
|
||||||
|
import { __ } from 'i18n'
|
||||||
|
|
||||||
|
export function getHeadingButtons (view, buttons) {
|
||||||
|
const muc = view.model
|
||||||
|
if (muc.get('type') !== _converse.constants.CHATROOMS_TYPE) {
|
||||||
|
// only on MUC.
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!muc.notes) { // this is defined only if user has access (see initOrDestroyChatRoomNotes)
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding a "Open moderator noteds" button.
|
||||||
|
buttons.unshift({
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
i18n_text: __(LOC_moderator_notes),
|
||||||
|
handler: async (ev) => {
|
||||||
|
ev.preventDefault()
|
||||||
|
// opening or closing the muc notes:
|
||||||
|
const NoteAppEl = ev.target.closest('converse-root').querySelector('livechat-converse-muc-note-app')
|
||||||
|
NoteAppEl.toggleApp()
|
||||||
|
},
|
||||||
|
a_class: '',
|
||||||
|
icon_class: 'fa-note-sticky',
|
||||||
|
name: 'muc-notes'
|
||||||
|
})
|
||||||
|
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMessageActionButtons (messageActionsEl, buttons) {
|
||||||
|
const messageModel = messageActionsEl.model
|
||||||
|
if (messageModel.get('type') !== 'groupchat') {
|
||||||
|
// only on groupchat message.
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!messageModel.occupant) {
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
const muc = messageModel.collection?.chatbox
|
||||||
|
if (!muc?.notes) {
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const i18nCreate = __(LOC_moderator_note_create_for_participant)
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const i18nSearch = __(LOC_moderator_note_search_for_participant)
|
||||||
|
|
||||||
|
buttons.push({
|
||||||
|
i18n_text: i18nCreate,
|
||||||
|
handler: async (ev) => {
|
||||||
|
ev.preventDefault()
|
||||||
|
await api.livechat_notes.openCreateNoteForm(messageModel.occupant)
|
||||||
|
},
|
||||||
|
button_class: '',
|
||||||
|
icon_class: 'fa fa-note-sticky',
|
||||||
|
name: 'muc-note-create-for-occupant'
|
||||||
|
})
|
||||||
|
|
||||||
|
buttons.push({
|
||||||
|
i18n_text: i18nSearch,
|
||||||
|
handler: async (ev) => {
|
||||||
|
ev.preventDefault()
|
||||||
|
await api.livechat_notes.searchNotesAbout(messageModel.occupant)
|
||||||
|
},
|
||||||
|
button_class: '',
|
||||||
|
icon_class: 'fa fa-magnifying-glass',
|
||||||
|
name: 'muc-note-search-for-occupant'
|
||||||
|
})
|
||||||
|
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOccupantActionButtons (occupant, buttons) {
|
||||||
|
const muc = occupant.collection?.chatroom
|
||||||
|
if (!muc?.notes) {
|
||||||
|
// We dont have access.
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const i18nCreate = __(LOC_moderator_note_create_for_participant)
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const i18nSearch = __(LOC_moderator_note_search_for_participant)
|
||||||
|
|
||||||
|
buttons.push({
|
||||||
|
i18n_text: i18nCreate,
|
||||||
|
handler: async (ev) => {
|
||||||
|
ev.preventDefault()
|
||||||
|
await api.livechat_notes.openCreateNoteForm(occupant)
|
||||||
|
},
|
||||||
|
button_class: '',
|
||||||
|
icon_class: 'fa fa-note-sticky',
|
||||||
|
name: 'muc-note-create-for-occupant'
|
||||||
|
})
|
||||||
|
|
||||||
|
buttons.push({
|
||||||
|
i18n_text: i18nSearch,
|
||||||
|
handler: async (ev) => {
|
||||||
|
ev.preventDefault()
|
||||||
|
await api.livechat_notes.searchNotesAbout(occupant)
|
||||||
|
},
|
||||||
|
button_class: '',
|
||||||
|
icon_class: 'fa fa-magnifying-glass',
|
||||||
|
name: 'muc-note-search-for-occupant'
|
||||||
|
})
|
||||||
|
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
function _initChatRoomNotes (mucModel) {
|
||||||
|
if (mucModel.noteManager) {
|
||||||
|
// already initiliazed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mucModel.notes = new _converse.exports.ChatRoomNotes(undefined, { chatroom: mucModel })
|
||||||
|
|
||||||
|
mucModel.noteManager = new NotePubSubManager(
|
||||||
|
mucModel.get('jid'),
|
||||||
|
'livechat-notes', // the node name
|
||||||
|
{
|
||||||
|
note: {
|
||||||
|
itemTag: 'note',
|
||||||
|
xmlns: XMLNS_NOTE,
|
||||||
|
collection: mucModel.notes,
|
||||||
|
fields: {
|
||||||
|
description: String
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
order: Number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
mucModel.noteManager.start().catch(err => console.log(err))
|
||||||
|
|
||||||
|
// We must requestUpdate for all message actions, to add the "create note" button.
|
||||||
|
// FIXME: this should not be done here (but it is simplier for now)
|
||||||
|
document.querySelectorAll('converse-message-actions').forEach(el => el.requestUpdate())
|
||||||
|
}
|
||||||
|
|
||||||
|
function _destroyChatRoomNotes (mucModel) {
|
||||||
|
if (!mucModel.noteManager) { return }
|
||||||
|
|
||||||
|
mucModel.noteManager.stop().catch(err => console.log(err))
|
||||||
|
mucModel.noteManager = undefined
|
||||||
|
|
||||||
|
mucModel.notes = undefined
|
||||||
|
|
||||||
|
// We must requestUpdate for all message actions, to remove the "create note" button.
|
||||||
|
// FIXME: this should not be done here (but it is simplier for now)
|
||||||
|
document.querySelectorAll('converse-message-actions').forEach(el => el.requestUpdate())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initOrDestroyChatRoomNotes (mucModel) {
|
||||||
|
if (mucModel.get('type') !== _converse.constants.CHATROOMS_TYPE) {
|
||||||
|
// only on MUC.
|
||||||
|
return _destroyChatRoomNotes(mucModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!api.settings.get('livechat_note_app_enabled')) {
|
||||||
|
// Feature disabled, no need to handle notes.
|
||||||
|
return _destroyChatRoomNotes(mucModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mucModel.session.get('connection_status') !== converse.ROOMSTATUS.ENTERED) {
|
||||||
|
return _destroyChatRoomNotes(mucModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We must check disco features
|
||||||
|
// (if the chat is remote, the server could use a livechat version that does not support this feature)
|
||||||
|
if (!mucModel.features?.get?.(XMLNS_NOTE)) {
|
||||||
|
return _destroyChatRoomNotes(mucModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
const myself = mucModel.getOwnOccupant()
|
||||||
|
if (!myself || !['admin', 'owner'].includes(myself.get('affiliation'))) {
|
||||||
|
// User must be admin or owner
|
||||||
|
return _destroyChatRoomNotes(mucModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return _initChatRoomNotes(mucModel)
|
||||||
|
}
|
@ -4,7 +4,7 @@
|
|||||||
import { XMLNS_POLL } from '../constants.js'
|
import { XMLNS_POLL } from '../constants.js'
|
||||||
import { tplPollForm } from '../templates/poll-form.js'
|
import { tplPollForm } from '../templates/poll-form.js'
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
import { CustomElement } from 'shared/components/element.js'
|
||||||
import { converse, api } from '@converse/headless/core'
|
import { converse, api, parsers } from '@converse/headless'
|
||||||
import { webForm2xForm } from '@converse/headless/utils/form'
|
import { webForm2xForm } from '@converse/headless/utils/form'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
import '../styles/poll-form.scss'
|
import '../styles/poll-form.scss'
|
||||||
@ -18,7 +18,6 @@ export default class MUCPollFormView extends CustomElement {
|
|||||||
return {
|
return {
|
||||||
model: { type: Object, attribute: true },
|
model: { type: Object, attribute: true },
|
||||||
modal: { type: Object, attribute: true },
|
modal: { type: Object, attribute: true },
|
||||||
form_fields: { type: Object, attribute: false },
|
|
||||||
alert_message: { type: Object, attribute: false },
|
alert_message: { type: Object, attribute: false },
|
||||||
title: { type: String, attribute: false },
|
title: { type: String, attribute: false },
|
||||||
instructions: { type: String, attribute: false }
|
instructions: { type: String, attribute: false }
|
||||||
@ -27,6 +26,8 @@ export default class MUCPollFormView extends CustomElement {
|
|||||||
|
|
||||||
_fieldTranslationMap = new Map()
|
_fieldTranslationMap = new Map()
|
||||||
|
|
||||||
|
xform = undefined
|
||||||
|
|
||||||
async initialize () {
|
async initialize () {
|
||||||
this.alert_message = undefined
|
this.alert_message = undefined
|
||||||
if (!this.model) {
|
if (!this.model) {
|
||||||
@ -36,20 +37,18 @@ export default class MUCPollFormView extends CustomElement {
|
|||||||
try {
|
try {
|
||||||
this._initFieldTranslations()
|
this._initFieldTranslations()
|
||||||
const stanza = await this._fetchPollForm()
|
const stanza = await this._fetchPollForm()
|
||||||
const query = stanza.querySelector('query')
|
const xform = parsers.parseXForm(stanza)
|
||||||
const xform = sizzle(`x[xmlns="${Strophe.NS.XFORM}"]`, query)[0]
|
|
||||||
if (!xform) {
|
if (!xform) {
|
||||||
throw Error('Missing xform in stanza')
|
throw Error('Missing xform in stanza')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
xform.fields?.map(f => this._translateField(f))
|
||||||
|
this.xform = xform
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
this.title = __(LOC_poll_title) // xform.querySelector('title')?.textContent ?? ''
|
this.title = __(LOC_poll_title) // xform.querySelector('title')?.textContent ?? ''
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
this.instructions = __(LOC_poll_instructions) // xform.querySelector('instructions')?.textContent ?? ''
|
this.instructions = __(LOC_poll_instructions) // xform.querySelector('instructions')?.textContent ?? ''
|
||||||
this.form_fields = Array.from(xform.querySelectorAll('field')).map(field => {
|
|
||||||
this._translateField(field)
|
|
||||||
return u.xForm2TemplateResult(field, stanza)
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
this.alert_message = __('Error')
|
this.alert_message = __('Error')
|
||||||
@ -86,10 +85,10 @@ export default class MUCPollFormView extends CustomElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_translateField (field) {
|
_translateField (field) {
|
||||||
const v = field.getAttribute('var')
|
const v = field.var
|
||||||
const label = this._fieldTranslationMap.get(v)
|
const label = this._fieldTranslationMap.get(v)
|
||||||
if (label) {
|
if (label) {
|
||||||
field.setAttribute('label', label)
|
field.label = label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +113,7 @@ export default class MUCPollFormView extends CustomElement {
|
|||||||
await api.sendIQ(iq)
|
await api.sendIQ(iq)
|
||||||
|
|
||||||
if (this.modal) {
|
if (this.modal) {
|
||||||
this.modal.onHide()
|
this.modal.close()
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (u.isErrorStanza(err)) {
|
if (u.isErrorStanza(err)) {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { tplPoll } from '../templates/poll.js'
|
import { tplPoll } from '../templates/poll.js'
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
import { CustomElement } from 'shared/components/element.js'
|
||||||
import { converse, _converse, api } from '@converse/headless/core'
|
import { converse, _converse, api } from '@converse/headless'
|
||||||
import '../styles/poll.scss'
|
import '../styles/poll.scss'
|
||||||
|
|
||||||
export default class MUCPollView extends CustomElement {
|
export default class MUCPollView extends CustomElement {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { _converse, converse } from '../../../src/headless/core.js'
|
import { _converse, converse } from '../../../src/headless/index.js'
|
||||||
import { getHeadingButtons } from './utils.js'
|
import { getHeadingButtons } from './utils.js'
|
||||||
import { POLL_MESSAGE_TAG, POLL_QUESTION_TAG, POLL_CHOICE_TAG } from './constants.js'
|
import { POLL_MESSAGE_TAG, POLL_QUESTION_TAG, POLL_CHOICE_TAG } from './constants.js'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
|
@ -2,9 +2,12 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
import BaseModal from 'plugins/modal/modal.js'
|
import BaseModal from 'plugins/modal/modal.js'
|
||||||
import { api } from '@converse/headless/core'
|
import { api } from '@converse/headless'
|
||||||
import { modal_close_button as ModalCloseButton } from 'plugins/modal/templates/buttons.js'
|
import { modal_close_button as ModalCloseButton } from 'plugins/modal/templates/buttons.js'
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
|
|
||||||
@ -13,8 +16,8 @@ class PollFormModal extends BaseModal {
|
|||||||
super.initialize()
|
super.initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
onHide () {
|
close () {
|
||||||
super.onHide()
|
super.close()
|
||||||
api.modal.remove('livechat-converse-poll-form-modal')
|
api.modal.remove('livechat-converse-poll-form-modal')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,9 +2,16 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
|
import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
|
import { converse } from '@converse/headless'
|
||||||
|
|
||||||
|
const u = converse.env.utils
|
||||||
|
|
||||||
export function tplPollForm (el) {
|
export function tplPollForm (el) {
|
||||||
const i18nOk = __('Ok')
|
const i18nOk = __('Ok')
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
@ -13,10 +20,18 @@ export function tplPollForm (el) {
|
|||||||
page: 'documentation/user/streamers/polls'
|
page: 'documentation/user/streamers/polls'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let formFieldTemplates
|
||||||
|
if (el.xform) {
|
||||||
|
const fields = el.xform.fields
|
||||||
|
formFieldTemplates = fields.map(field => {
|
||||||
|
return u.xFormField2TemplateResult(field)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${el.alert_message ? html`<div class="error">${el.alert_message}</div>` : ''}
|
${el.alert_message ? html`<div class="error">${el.alert_message}</div>` : ''}
|
||||||
${
|
${
|
||||||
el.form_fields
|
formFieldTemplates
|
||||||
? html`
|
? html`
|
||||||
<form class="converse-form" @submit=${ev => el.formSubmit(ev)}>
|
<form class="converse-form" @submit=${ev => el.formSubmit(ev)}>
|
||||||
<p class="title">
|
<p class="title">
|
||||||
@ -30,9 +45,9 @@ export function tplPollForm (el) {
|
|||||||
<p class="form-help instructions">${el.instructions}</p>
|
<p class="form-help instructions">${el.instructions}</p>
|
||||||
<div class="form-errors hidden"></div>
|
<div class="form-errors hidden"></div>
|
||||||
|
|
||||||
${el.form_fields}
|
${formFieldTemplates}
|
||||||
|
|
||||||
<fieldset class="buttons form-group">
|
<fieldset class="buttons">
|
||||||
<input type="submit" class="btn btn-primary" value="${i18nOk}" />
|
<input type="submit" class="btn btn-primary" value="${i18nOk}" />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>`
|
</form>`
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||||
|
/* eslint-disable @stylistic/indent */
|
||||||
|
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
import { repeat } from 'lit/directives/repeat.js'
|
import { repeat } from 'lit/directives/repeat.js'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
@ -63,7 +66,7 @@ function _tplChoice (el, currentPoll, choice, canVote) {
|
|||||||
<div class="livechat-progress-bar">
|
<div class="livechat-progress-bar">
|
||||||
<div
|
<div
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
style="width: ${percent}%;"
|
style=${'width: ' + percent + '%;'}
|
||||||
aria-valuenow="${percent}" aria-valuemin="0" aria-valuemax="100"
|
aria-valuenow="${percent}" aria-valuemin="0" aria-valuemax="100"
|
||||||
></div>
|
></div>
|
||||||
<p>
|
<p>
|
||||||
@ -83,21 +86,21 @@ export function tplPoll (el, currentPoll, canVote) {
|
|||||||
return html`<div class="${currentPoll.over ? 'livechat-poll-over' : ''}">
|
return html`<div class="${currentPoll.over ? 'livechat-poll-over' : ''}">
|
||||||
<p class="livechat-poll-question">
|
<p class="livechat-poll-question">
|
||||||
${currentPoll.over
|
${currentPoll.over
|
||||||
? html`<button class="livechat-poll-close" @click=${el.closePoll} title="${__('Close')}">
|
? html`<button type="button" class="livechat-poll-close" @click=${el.closePoll} title="${__('Close')}">
|
||||||
<converse-icon class="fa fa-times" size="1em"></converse-icon>
|
<converse-icon class="fa fa-times" size="1em"></converse-icon>
|
||||||
</button>`
|
</button>`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
${el.collapsed
|
${el.collapsed
|
||||||
? html`
|
? html`
|
||||||
<button @click=${el.toggle} class="livechat-poll-toggle">
|
<button type="button" @click=${el.toggle} class="livechat-poll-toggle">
|
||||||
<converse-icon
|
<converse-icon
|
||||||
color="var(--muc-toolbar-btn-color)"
|
color="var(--muc-toolbar-btn-color)"
|
||||||
class="fa fa-angle-right"
|
class="fa fa-angle-right"
|
||||||
size="1em"></converse-icon>
|
size="1em"></converse-icon>
|
||||||
</button>`
|
</button>`
|
||||||
: html`
|
: html`
|
||||||
<button @click=${el.toggle} class="livechat-poll-toggle">
|
<button type="button" @click=${el.toggle} class="livechat-poll-toggle">
|
||||||
<converse-icon
|
<converse-icon
|
||||||
color="var(--muc-toolbar-btn-color)"
|
color="var(--muc-toolbar-btn-color)"
|
||||||
class="fa fa-angle-down"
|
class="fa fa-angle-down"
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { XMLNS_POLL } from './constants.js'
|
import { XMLNS_POLL } from './constants.js'
|
||||||
import { _converse, api } from '../../../src/headless/core.js'
|
import { _converse, api } from '../../../src/headless/index.js'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
|
|
||||||
export function getHeadingButtons (view, buttons) {
|
export function getHeadingButtons (view, buttons) {
|
||||||
const muc = view.model
|
const muc = view.model
|
||||||
if (muc.get('type') !== _converse.CHATROOMS_TYPE) {
|
if (muc.get('type') !== _converse.constants.CHATROOMS_TYPE) {
|
||||||
// only on MUC.
|
// only on MUC.
|
||||||
return buttons
|
return buttons
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { _converse, converse, api } from '../../../src/headless/core.js'
|
import { _converse, converse, api } from '../../../src/headless/index.js'
|
||||||
|
|
||||||
|
let currentSize
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This plugin computes the available width of converse-root, and adds classes
|
* This plugin computes the available width of converse-root, and adds classes
|
||||||
@ -16,6 +18,27 @@ converse.plugins.add('livechat-converse-size', {
|
|||||||
dependencies: [],
|
dependencies: [],
|
||||||
|
|
||||||
initialize () {
|
initialize () {
|
||||||
|
Object.assign(api, {
|
||||||
|
livechat_size: {
|
||||||
|
current: () => {
|
||||||
|
return currentSize
|
||||||
|
},
|
||||||
|
width_is: (sizes) => {
|
||||||
|
if (!Array.isArray(sizes)) {
|
||||||
|
sizes = [sizes]
|
||||||
|
}
|
||||||
|
if (!currentSize) { return false }
|
||||||
|
return sizes.includes(currentSize.width)
|
||||||
|
},
|
||||||
|
height_is: (sizes) => {
|
||||||
|
if (!Array.isArray(sizes)) {
|
||||||
|
sizes = [sizes]
|
||||||
|
}
|
||||||
|
if (!currentSize) { return false }
|
||||||
|
return sizes.includes(currentSize.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
_converse.api.listen.on('connected', start)
|
_converse.api.listen.on('connected', start)
|
||||||
_converse.api.listen.on('reconnected', start)
|
_converse.api.listen.on('reconnected', start)
|
||||||
_converse.api.listen.on('disconnected', stop)
|
_converse.api.listen.on('disconnected', stop)
|
||||||
@ -42,6 +65,7 @@ function start () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stop () {
|
function stop () {
|
||||||
|
currentSize = undefined
|
||||||
rootResizeObserver.disconnect()
|
rootResizeObserver.disconnect()
|
||||||
const root = document.querySelector('converse-root')
|
const root = document.querySelector('converse-root')
|
||||||
if (root) {
|
if (root) {
|
||||||
@ -60,8 +84,9 @@ function handle (el) {
|
|||||||
|
|
||||||
el.setAttribute('livechat-converse-root-width', width)
|
el.setAttribute('livechat-converse-root-width', width)
|
||||||
el.setAttribute('livechat-converse-root-height', height)
|
el.setAttribute('livechat-converse-root-height', height)
|
||||||
api.trigger('livechatSizeChanged', {
|
currentSize = {
|
||||||
height: height,
|
height: height,
|
||||||
width: width
|
width: width
|
||||||
})
|
}
|
||||||
|
api.trigger('livechatSizeChanged', Object.assign({}, currentSize)) // cloning...
|
||||||
}
|
}
|
||||||
|
@ -2,36 +2,20 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { api } from '@converse/headless/core'
|
import { api } from '@converse/headless'
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
import { MUCApp } from '../../../shared/components/muc-app/index.js'
|
||||||
import { tplMUCTaskApp } from '../templates/muc-task-app.js'
|
import { tplMUCTaskApp } from '../templates/muc-task-app.js'
|
||||||
|
|
||||||
import '../styles/muc-task-app.scss'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Element to display the Task Application.
|
* Custom Element to display the Task Application.
|
||||||
*/
|
*/
|
||||||
export default class MUCTaskApp extends CustomElement {
|
export default class MUCTaskApp extends MUCApp {
|
||||||
static get properties () {
|
restoreSettingName = 'livechat_task_app_restore'
|
||||||
return {
|
sessionStorageRestoreKey = 'livechat-converse-task-app-show'
|
||||||
model: { type: Object, attribute: true }, // mucModel
|
|
||||||
show: { type: Boolean, attribute: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize () {
|
|
||||||
this.show = api.settings.get('livechat_task_app_restore') &&
|
|
||||||
(window.sessionStorage?.getItem?.('livechat-converse-task-app-show') === '1')
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return tplMUCTaskApp(this, this.model)
|
return tplMUCTaskApp(this, this.model)
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleApp () {
|
|
||||||
this.show = !this.show
|
|
||||||
window.sessionStorage?.setItem?.('livechat-converse-task-app-show', this.show ? '1' : '')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
api.elements.define('livechat-converse-muc-task-app', MUCTaskApp)
|
api.elements.define('livechat-converse-muc-task-app', MUCTaskApp)
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
import { CustomElement } from 'shared/components/element.js'
|
||||||
import { api } from '@converse/headless/core'
|
import { api } from '@converse/headless'
|
||||||
import tplMucTaskList from '../templates/muc-task-list'
|
import tplMucTaskList from '../templates/muc-task-list'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
|
|
||||||
|
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