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
|
||||
uses: peaceiris/actions-hugo@v2
|
||||
with:
|
||||
hugo-version: '0.80.0'
|
||||
hugo-version: '0.132.2'
|
||||
extended: true
|
||||
|
||||
- name: Generate documentation translations
|
||||
|
@ -22,7 +22,7 @@ pages:
|
||||
image: registry.gitlab.com/pages/hugo/hugo_extended:latest
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
GIT_SUBMODULE_PATHS: support/documentation/themes/hugo-theme-learn
|
||||
GIT_SUBMODULE_PATHS: support/documentation/themes/hugo-theme-relearn
|
||||
script:
|
||||
# gitlab need the generated documentation to be in the /public dir.
|
||||
- hugo -s support/documentation/ --minify -d ../../public/ --baseURL='https://livingston.frama.io/peertube-plugin-livechat/'
|
||||
|
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -2,6 +2,6 @@
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
[submodule "documentation/themes/hugo-theme-learn"]
|
||||
path = support/documentation/themes/hugo-theme-learn
|
||||
url = https://github.com/matcornic/hugo-theme-learn.git
|
||||
[submodule "support/documentation/themes/hugo-theme-relearn"]
|
||||
path = support/documentation/themes/hugo-theme-relearn
|
||||
url = https://github.com/McShelby/hugo-theme-relearn.git
|
||||
|
@ -32,3 +32,7 @@ License: AGPL-3.0-only
|
||||
Files: .github/PULL_REQUEST_TEMPLATE.md
|
||||
Copyright: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
License: AGPL-3.0-only
|
||||
|
||||
Files: prosody-modules/mod_firewall/*
|
||||
Copyright: Prosody Community Modules <https://modules.prosody.im/mod_firewall>
|
||||
License: MIT
|
||||
|
@ -2,7 +2,7 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
'use strict';
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
@ -14,7 +14,7 @@ module.exports = {
|
||||
// extending the kebab-case to accept ConverseJS class names.
|
||||
'^([a-z][a-z0-9]*)(-[a-z0-9]+)*((__|--)[a-z]+(-[a-z0-9]+)*)?$',
|
||||
{
|
||||
message: 'Expected class selector to be kebab-case, or ConverseJS-style.',
|
||||
message: 'Expected class selector to be kebab-case, or ConverseJS-style.'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
69
CHANGELOG.md
69
CHANGELOG.md
@ -1,5 +1,74 @@
|
||||
# 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
|
||||
|
||||
### 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 {
|
||||
/* See Peertube .video-channel-names */
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
/* stylelint-disable-next-line value-keyword-case */
|
||||
color: var(--mainForegroundColor);
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
@ -184,12 +190,6 @@ $small-view: 800px;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
/* stylelint-disable-next-line value-keyword-case */
|
||||
color: var(--mainForegroundColor);
|
||||
|
||||
div:first-child {
|
||||
/* See Peertube .video-channel-display-name */
|
||||
font-weight: variables.$font-semibold;
|
||||
|
@ -15,9 +15,9 @@ livechat-spinner,
|
||||
height: 48px;
|
||||
margin: 20px;
|
||||
/* 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 */
|
||||
border-bottom-color: var(--mainColor);
|
||||
border-bottom-color: var(--mainColor) !important; // !important is required for it to work in ConverseJS
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
|
@ -9,10 +9,10 @@
|
||||
|
||||
livechat-token-list {
|
||||
table {
|
||||
@include tables.data-table;
|
||||
|
||||
width: 100%;
|
||||
|
||||
@include tables.data-table;
|
||||
|
||||
tr th:first-child,
|
||||
tr th:last-child {
|
||||
width: 50px;
|
||||
|
@ -9,4 +9,5 @@
|
||||
@use "elements/index";
|
||||
@use "video";
|
||||
@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
|
||||
choose nickname or log in with external account), can be something like
|
||||
~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.
|
||||
Note: We also must ensure that when the user has choosen its nickname, and there is an
|
||||
ongoing poll, the user can see the chat when the poll is folded.
|
||||
*/
|
||||
#peertube-plugin-livechat-container converse-root {
|
||||
display: block;
|
||||
border: 1px solid black;
|
||||
min-height: max(30vh, 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%;
|
||||
min-width: min(400px, 25vw);
|
||||
|
||||
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'
|
||||
]
|
||||
|
||||
function loadLocs() {
|
||||
function loadLocs(globalFile) {
|
||||
// Loading english strings, so we can inject them as constants.
|
||||
const refFile = path.resolve(__dirname, 'dist', 'languages', 'en.reference.json')
|
||||
if (!fs.existsSync(refFile)) {
|
||||
@ -25,7 +25,6 @@ function loadLocs() {
|
||||
|
||||
// Reading client/@types/global.d.ts, to have a list of needed localized strings.
|
||||
const r = {}
|
||||
const globalFile = path.resolve(__dirname, 'client', '@types', 'global.d.ts')
|
||||
const globalFileContent = '' + fs.readFileSync(globalFile)
|
||||
const matches = globalFileContent.matchAll(/^declare const LOC_(\w+)\b/gm)
|
||||
for (const match of matches) {
|
||||
@ -41,7 +40,7 @@ function loadLocs() {
|
||||
const define = Object.assign({
|
||||
PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name),
|
||||
PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, ''))
|
||||
}, loadLocs())
|
||||
}, loadLocs(path.resolve(__dirname, 'client', '@types', 'global.d.ts')))
|
||||
|
||||
const configs = clientFiles.map(f => ({
|
||||
entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ],
|
||||
@ -59,8 +58,14 @@ const configs = clientFiles.map(f => ({
|
||||
outfile: path.resolve(__dirname, 'dist/client', f + '.js'),
|
||||
}))
|
||||
|
||||
const defineBuiltin = Object.assign(
|
||||
{},
|
||||
loadLocs(path.resolve(__dirname, 'conversejs', 'lib', '@types', 'global.d.ts'))
|
||||
)
|
||||
|
||||
configs.push({
|
||||
entryPoints: ["./conversejs/builtin.ts"],
|
||||
define: defineBuiltin,
|
||||
bundle: true,
|
||||
minify: true,
|
||||
sourcemap,
|
||||
|
@ -10,12 +10,12 @@ set -euo pipefail
|
||||
# This script download the Prosody AppImage from the https://github.com/JohnXLivingston/prosody-appimage project.
|
||||
|
||||
repo_base_url='https://github.com/JohnXLivingston/prosody-appimage/releases/download'
|
||||
wanted_release='v0.12.3-1'
|
||||
wanted_release='v0.12.4-3'
|
||||
|
||||
x86_64_filename='prosody-x86_64.AppImage'
|
||||
x86_64_sha256sum='f4af9bfefa2f804ad7e8b03a68f04194abb801f070ae620b3d4bcedb144e8523'
|
||||
x86_64_sha256sum='83a583ac7036387514bed17afab257dab4161ccdd0ab7453818c78b51f830357'
|
||||
aarch64_filename='prosody-aarch64.AppImage'
|
||||
aarch64_sha256sum='878c5be719e1e36a84d637fd2bd44e3059aa91ddb6906ad05f1dd0334078df09'
|
||||
aarch64_sha256sum='7b7e6bf30d4498fc99a40022232c3065707ee4f4df24dc17947b007621634304'
|
||||
|
||||
download_dir="$(pwd)/vendor/prosody-appimage"
|
||||
dist_dir="$(pwd)/dist/server/prosody"
|
||||
|
@ -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.
|
||||
// See the online documentation: https://livingston.frama.io/peertube-plugin-livechat/contributing/translate/
|
||||
declare const LOC_ONLINE_HELP: string
|
||||
declare const LOC_CHAT: string
|
||||
declare const LOC_OPEN_CHAT: string
|
||||
declare const LOC_OPEN_CHAT_NEW_WINDOW: string
|
||||
declare const LOC_CLOSE_CHAT: string
|
||||
@ -56,12 +57,12 @@ declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL: string
|
||||
@ -133,3 +134,29 @@ declare const LOC_POLL_VOTE_OK: string
|
||||
|
||||
declare const LOC_MODERATION_DELAY: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_MODERATION_DELAY_DESC: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_DESC: string
|
||||
|
||||
declare const LOC_PROSODY_FIREWALL_CONFIGURATION: string
|
||||
declare const LOC_PROSODY_FIREWALL_CONFIGURATION_HELP: string
|
||||
declare const LOC_PROSODY_FIREWALL_DISABLED_WARNING: string
|
||||
declare const LOC_PROSODY_FIREWALL_FILE_ENABLED: string
|
||||
declare const LOC_PROSODY_FIREWALL_NAME: string
|
||||
declare const LOC_PROSODY_FIREWALL_NAME_DESC: string
|
||||
declare const LOC_PROSODY_FIREWALL_CONTENT: string
|
||||
|
||||
declare const LOC_EMOJI_ONLY_MODE_TITLE: string
|
||||
declare const LOC_EMOJI_ONLY_MODE_DESC_1: string
|
||||
declare const LOC_EMOJI_ONLY_MODE_DESC_2: string
|
||||
declare const LOC_EMOJI_ONLY_MODE_DESC_3: string
|
||||
declare const LOC_EMOJI_ONLY_ENABLE_ALL_ROOMS: string
|
||||
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_LABEL: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_DESC: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_LABEL: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_DESC: string
|
||||
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DESC: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_LABEL: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_DESC: string
|
||||
|
@ -243,7 +243,10 @@ function register (clientOptions: RegisterClientOptions): void {
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
peertubeHelpers.notifier.error(error.toString(), await peertubeHelpers.translate(LOC_LOADING_ERROR))
|
||||
peertubeHelpers.notifier.error(
|
||||
(error as Error).toString(),
|
||||
await peertubeHelpers.translate(LOC_LOADING_ERROR)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -270,6 +273,8 @@ function register (clientOptions: RegisterClientOptions): void {
|
||||
return !(options.formValues['chat-all-lives'] === true && options.formValues['chat-per-live-video'] === true)
|
||||
case 'auto-ban-anonymous-ip':
|
||||
return options.formValues['chat-no-anonymous'] !== false
|
||||
case 'prosody-firewall-configure-button':
|
||||
return options.formValues['prosody-firewall-enabled'] !== true
|
||||
}
|
||||
|
||||
if (name?.startsWith('external-auth-')) {
|
||||
|
@ -8,6 +8,7 @@ import { registerConfiguration } from './common/configuration/register'
|
||||
import { registerVideoWatch } from './common/videowatch/register'
|
||||
import { registerRoom } from './common/room/register'
|
||||
import { initPtContext } from './common/lib/contexts/peertube'
|
||||
import { registerAdminFirewall } from './common/admin/firewall/register'
|
||||
import './common/lib/elements' // Import shared elements.
|
||||
|
||||
async function register (clientOptions: RegisterClientOptions): Promise<void> {
|
||||
@ -45,7 +46,7 @@ async function register (clientOptions: RegisterClientOptions): Promise<void> {
|
||||
])
|
||||
const webchatFieldOptions: RegisterClientFormFieldOptions = {
|
||||
name: 'livechat-active',
|
||||
label: label,
|
||||
label,
|
||||
descriptionHTML: description,
|
||||
type: 'input-checkbox',
|
||||
default: true,
|
||||
@ -69,7 +70,8 @@ async function register (clientOptions: RegisterClientOptions): Promise<void> {
|
||||
await Promise.all([
|
||||
registerVideoWatch(),
|
||||
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
|
||||
|
||||
@state()
|
||||
public actionDisabled: boolean = false
|
||||
public actionDisabled = false
|
||||
|
||||
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 =
|
||||
this.validationError?.properties[`${propertyName}`]
|
||||
return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {}
|
||||
|
@ -30,7 +30,7 @@ export class ChannelEmojisElement extends LivechatElement {
|
||||
public validationError?: ValidationError
|
||||
|
||||
@state()
|
||||
public actionDisabled: boolean = false
|
||||
public actionDisabled = false
|
||||
|
||||
private _asyncTaskRender: Task
|
||||
|
||||
@ -192,7 +192,7 @@ export class ChannelEmojisElement extends LivechatElement {
|
||||
throw new Error('Invalid data')
|
||||
}
|
||||
|
||||
const url = await this._convertImageToDataUrl(entry.url)
|
||||
const url = await this._convertImageToDataUrl(entry.url as string)
|
||||
const sn = entry.sn as string
|
||||
|
||||
const item: ChannelEmojisConfiguration['emojis']['customEmojis'][0] = {
|
||||
@ -211,7 +211,7 @@ export class ChannelEmojisElement extends LivechatElement {
|
||||
await this.ptTranslate(LOC_ACTION_IMPORT_EMOJIS_INFO)
|
||||
)
|
||||
} catch (err: any) {
|
||||
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR))
|
||||
this.ptNotifier.error((err as Error).toString(), await this.ptTranslate(LOC_ERROR))
|
||||
} finally {
|
||||
this.actionDisabled = false
|
||||
}
|
||||
@ -250,12 +250,27 @@ export class ChannelEmojisElement extends LivechatElement {
|
||||
a.remove()
|
||||
} catch (err: any) {
|
||||
this.logger.error(err)
|
||||
this.ptNotifier.error(err.toString())
|
||||
this.ptNotifier.error((err as Error).toString())
|
||||
} finally {
|
||||
this.actionDisabled = false
|
||||
}
|
||||
}
|
||||
|
||||
public async enableEmojisOnlyModeOnAllRooms (ev: Event): Promise<void> {
|
||||
ev.preventDefault()
|
||||
if (!this._channelDetailsService || !this.channelId) {
|
||||
this.ptNotifier.error(await this.ptTranslate(LOC_ERROR))
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this._channelDetailsService.enableEmojisOnlyModeOnAllRooms(this.channelId)
|
||||
this.ptNotifier.info(await this.ptTranslate(LOC_SUCCESSFULLY_SAVED))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
this.ptNotifier.error(await this.ptTranslate(LOC_ERROR))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an url (or dataUrl), download the image, and converts to dataUrl.
|
||||
* @param url the url
|
||||
|
@ -2,6 +2,9 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||
/* eslint-disable @stylistic/indent */
|
||||
|
||||
import { html } from 'lit'
|
||||
import { customElement, state } from 'lit/decorators.js'
|
||||
import { ptTr } from '../../lib/directives/translation'
|
||||
@ -50,7 +53,7 @@ export class ChannelHomeElement extends LivechatElement {
|
||||
<ul class="peertube-plugin-livechat-configuration-home-channels">
|
||||
${this._channels?.map((channel) => html`
|
||||
<li>
|
||||
<a href="${channel.livechatConfigurationUri}">
|
||||
<a href="${channel.livechatConfigurationUri}" aria-hidden="true">
|
||||
${channel.avatar
|
||||
? html`<img class="avatar channel" src="${channel.avatar.path}">`
|
||||
: html`<div class="avatar channel initial gray"></div>`
|
||||
|
@ -2,6 +2,9 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||
/* eslint-disable @stylistic/indent */
|
||||
|
||||
import { LivechatElement } from '../../lib/elements/livechat'
|
||||
import { ptTr } from '../../lib/directives/translation'
|
||||
import { html, TemplateResult } from 'lit'
|
||||
|
@ -2,14 +2,18 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||
/* eslint-disable @stylistic/indent */
|
||||
|
||||
import type { ChannelConfigurationElement } from '../channel-configuration'
|
||||
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
|
||||
import { ptTr } from '../../../lib/directives/translation'
|
||||
import { html, TemplateResult } from 'lit'
|
||||
import { classMap } from 'lit/directives/class-map.js'
|
||||
import { noDuplicateMaxDelay, forbidSpecialCharsMaxTolerance } from 'shared/lib/constants'
|
||||
|
||||
export function tplChannelConfiguration (el: ChannelConfigurationElement): TemplateResult {
|
||||
const tableHeaderList: {[key: string]: DynamicFormHeader} = {
|
||||
const tableHeaderList: Record<string, DynamicFormHeader> = {
|
||||
forbiddenWords: {
|
||||
entries: {
|
||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL),
|
||||
@ -20,16 +24,16 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC)
|
||||
},
|
||||
applyToModerators: {
|
||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL),
|
||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC)
|
||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL),
|
||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)
|
||||
},
|
||||
label: {
|
||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL),
|
||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC)
|
||||
},
|
||||
reason: {
|
||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL),
|
||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC)
|
||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL),
|
||||
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)
|
||||
},
|
||||
comments: {
|
||||
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL),
|
||||
@ -57,7 +61,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
||||
}
|
||||
}
|
||||
}
|
||||
const tableSchema: {[key: string]: DynamicFormSchema} = {
|
||||
const tableSchema: Record<string, DynamicFormSchema> = {
|
||||
forbiddenWords: {
|
||||
entries: {
|
||||
inputType: 'tags',
|
||||
@ -135,6 +139,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
||||
</livechat-configuration-section-header>
|
||||
<div class="form-group">
|
||||
<textarea
|
||||
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TERMS_LABEL) as any}
|
||||
name="terms"
|
||||
id="peertube-livechat-terms"
|
||||
.value=${el.channelConfiguration?.configuration.terms ?? ''}
|
||||
@ -167,7 +172,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bot"
|
||||
name="mute_anonymous"
|
||||
id="peertube-livechat-mute-anonymous"
|
||||
@input=${(event: InputEvent) => {
|
||||
if (event?.target && el.channelConfiguration) {
|
||||
@ -254,6 +259,32 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
||||
${el.renderFeedback('peertube-livechat-moderation-delay-feedback', 'moderation.delay')}
|
||||
</div>
|
||||
|
||||
<livechat-configuration-section-header
|
||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL)}
|
||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_DESC, true)}
|
||||
.helpPage=${'documentation/user/streamers/moderation'}>
|
||||
</livechat-configuration-section-header>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="anonymize-moderation"
|
||||
id="peertube-livechat-anonymize-moderation"
|
||||
@input=${(event: InputEvent) => {
|
||||
if (event?.target && el.channelConfiguration) {
|
||||
el.channelConfiguration.configuration.moderation.anonymize =
|
||||
(event.target as HTMLInputElement).checked
|
||||
}
|
||||
el.requestUpdate('channelConfiguration')
|
||||
}
|
||||
}
|
||||
value="1"
|
||||
?checked=${el.channelConfiguration?.configuration.moderation.anonymize}
|
||||
/>
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<livechat-configuration-section-header
|
||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
|
||||
.description=${''}
|
||||
@ -310,6 +341,246 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
||||
${el.renderFeedback('peertube-livechat-bot-nickname-feedback', 'bot.nickname')}
|
||||
</div>
|
||||
|
||||
<livechat-configuration-section-header
|
||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_LABEL)}
|
||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_DESC)}
|
||||
.helpPage=${'documentation/user/streamers/bot/special_chars'}>
|
||||
</livechat-configuration-section-header>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="forbid_special_chars"
|
||||
id="peertube-livechat-forbid-special-chars"
|
||||
@input=${(event: InputEvent) => {
|
||||
if (event?.target && el.channelConfiguration) {
|
||||
el.channelConfiguration.configuration.bot.forbidSpecialChars.enabled =
|
||||
(event.target as HTMLInputElement).checked
|
||||
}
|
||||
el.requestUpdate('channelConfiguration')
|
||||
}
|
||||
}
|
||||
value="1"
|
||||
?checked=${el.channelConfiguration?.configuration.bot.forbidSpecialChars.enabled}
|
||||
/>
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_LABEL)}
|
||||
</label>
|
||||
</div>
|
||||
${!el.channelConfiguration?.configuration.bot.forbidSpecialChars.enabled
|
||||
? ''
|
||||
: html`
|
||||
<div class="form-group">
|
||||
<label>
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_LABEL)}
|
||||
<input
|
||||
type="number"
|
||||
name="special_chars_tolerance"
|
||||
class=${classMap(
|
||||
Object.assign(
|
||||
{ 'form-control': true },
|
||||
el.getInputValidationClass('bot.forbidSpecialChars.tolerance')
|
||||
)
|
||||
)}
|
||||
min="0"
|
||||
max="${forbidSpecialCharsMaxTolerance}"
|
||||
id="peertube-livechat-forbid-special-chars-tolerance"
|
||||
aria-describedby="peertube-livechat-forbid-special-chars-tolerance-feedback"
|
||||
@input=${(event: InputEvent) => {
|
||||
if (event?.target && el.channelConfiguration) {
|
||||
el.channelConfiguration.configuration.bot.forbidSpecialChars.tolerance =
|
||||
Number((event.target as HTMLInputElement).value)
|
||||
}
|
||||
el.requestUpdate('channelConfiguration')
|
||||
}
|
||||
}
|
||||
value="${el.channelConfiguration?.configuration.bot.forbidSpecialChars.tolerance ?? '0'}"
|
||||
/>
|
||||
</label>
|
||||
<small class="form-text text-muted">
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_DESC)}
|
||||
</small>
|
||||
${el.renderFeedback('peertube-livechat-forbid-special-chars-tolerance-feedback',
|
||||
'bot.forbidSpecialChars.tolerance')
|
||||
}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL)}
|
||||
<input
|
||||
type="text"
|
||||
name="special_chars_reason"
|
||||
class=${classMap(
|
||||
Object.assign(
|
||||
{ 'form-control': true },
|
||||
el.getInputValidationClass('bot.forbidSpecialChars.reason')
|
||||
)
|
||||
)}
|
||||
id="peertube-livechat-forbid-special-chars-reason"
|
||||
aria-describedby="peertube-livechat-forbid-special-chars-reason-feedback"
|
||||
@input=${(event: InputEvent) => {
|
||||
if (event?.target && el.channelConfiguration) {
|
||||
el.channelConfiguration.configuration.bot.forbidSpecialChars.reason =
|
||||
(event.target as HTMLInputElement).value
|
||||
}
|
||||
el.requestUpdate('channelConfiguration')
|
||||
}
|
||||
}
|
||||
value="${el.channelConfiguration?.configuration.bot.forbidSpecialChars.reason ?? ''}"
|
||||
/>
|
||||
</label>
|
||||
<small class="form-text text-muted">
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)}
|
||||
</small>
|
||||
${el.renderFeedback('peertube-livechat-forbid-special-chars-reason-feedback',
|
||||
'bot.forbidSpecialChars.reason')
|
||||
}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="forbid_special_chars_applyToModerators"
|
||||
id="peertube-livechat-forbid-special-chars-applyToModerators"
|
||||
@input=${(event: InputEvent) => {
|
||||
if (event?.target && el.channelConfiguration) {
|
||||
el.channelConfiguration.configuration.bot.forbidSpecialChars.applyToModerators =
|
||||
(event.target as HTMLInputElement).checked
|
||||
}
|
||||
el.requestUpdate('channelConfiguration')
|
||||
}
|
||||
}
|
||||
value="1"
|
||||
?checked=${el.channelConfiguration?.configuration.bot.forbidSpecialChars.applyToModerators}
|
||||
/>
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL)}
|
||||
</label>
|
||||
<small class="form-text text-muted">
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)}
|
||||
</small>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
<livechat-configuration-section-header
|
||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL)}
|
||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DESC)}
|
||||
.helpPage=${'documentation/user/streamers/bot/no_duplicate'}>
|
||||
</livechat-configuration-section-header>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="no_duplicate"
|
||||
id="peertube-livechat-no-duplicate"
|
||||
@input=${(event: InputEvent) => {
|
||||
if (event?.target && el.channelConfiguration) {
|
||||
el.channelConfiguration.configuration.bot.noDuplicate.enabled =
|
||||
(event.target as HTMLInputElement).checked
|
||||
}
|
||||
el.requestUpdate('channelConfiguration')
|
||||
}
|
||||
}
|
||||
value="1"
|
||||
?checked=${el.channelConfiguration?.configuration.bot.noDuplicate.enabled}
|
||||
/>
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL)}
|
||||
</label>
|
||||
</div>
|
||||
${!el.channelConfiguration?.configuration.bot.noDuplicate.enabled
|
||||
? ''
|
||||
: html`
|
||||
<div class="form-group">
|
||||
<label>
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_LABEL)}
|
||||
<input
|
||||
type="number"
|
||||
name="no_duplicate_delay"
|
||||
class=${classMap(
|
||||
Object.assign(
|
||||
{ 'form-control': true },
|
||||
el.getInputValidationClass('bot.noDuplicate.delay')
|
||||
)
|
||||
)}
|
||||
min="0"
|
||||
max="${noDuplicateMaxDelay.toString()}"
|
||||
id="peertube-livechat-no-duplicate-delay"
|
||||
aria-describedby="peertube-livechat-no-duplicate-delay-feedback"
|
||||
@input=${(event: InputEvent) => {
|
||||
if (event?.target && el.channelConfiguration) {
|
||||
el.channelConfiguration.configuration.bot.noDuplicate.delay =
|
||||
Number((event.target as HTMLInputElement).value)
|
||||
}
|
||||
el.requestUpdate('channelConfiguration')
|
||||
}
|
||||
}
|
||||
value="${el.channelConfiguration?.configuration.bot.noDuplicate.delay ?? '0'}"
|
||||
/>
|
||||
</label>
|
||||
<small class="form-text text-muted">
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_DESC)}
|
||||
</small>
|
||||
${el.renderFeedback('peertube-livechat-no-duplicate-delay-feedback',
|
||||
'bot.noDuplicate.delay')
|
||||
}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL)}
|
||||
<input
|
||||
type="text"
|
||||
name="no_duplicate_reason"
|
||||
class=${classMap(
|
||||
Object.assign(
|
||||
{ 'form-control': true },
|
||||
el.getInputValidationClass('bot.noDuplicate.reason')
|
||||
)
|
||||
)}
|
||||
id="peertube-livechat-no-duplicate-reason"
|
||||
aria-describedby="peertube-livechat-no-duplicate-reason-feedback"
|
||||
@input=${(event: InputEvent) => {
|
||||
if (event?.target && el.channelConfiguration) {
|
||||
el.channelConfiguration.configuration.bot.noDuplicate.reason =
|
||||
(event.target as HTMLInputElement).value
|
||||
}
|
||||
el.requestUpdate('channelConfiguration')
|
||||
}
|
||||
}
|
||||
value="${el.channelConfiguration?.configuration.bot.noDuplicate.reason ?? ''}"
|
||||
/>
|
||||
</label>
|
||||
<small class="form-text text-muted">
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)}
|
||||
</small>
|
||||
${el.renderFeedback('peertube-livechat-no-duplicate-reason-feedback',
|
||||
'bot.noDuplicate.reason')
|
||||
}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="no_duplicate_applyToModerators"
|
||||
id="peertube-livechat-no-duplicate-applyToModerators"
|
||||
@input=${(event: InputEvent) => {
|
||||
if (event?.target && el.channelConfiguration) {
|
||||
el.channelConfiguration.configuration.bot.noDuplicate.applyToModerators =
|
||||
(event.target as HTMLInputElement).checked
|
||||
}
|
||||
el.requestUpdate('channelConfiguration')
|
||||
}
|
||||
}
|
||||
value="1"
|
||||
?checked=${el.channelConfiguration?.configuration.bot.noDuplicate.applyToModerators}
|
||||
/>
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL)}
|
||||
</label>
|
||||
<small class="form-text text-muted">
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)}
|
||||
</small>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
<livechat-configuration-section-header
|
||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
|
||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)}
|
||||
|
@ -2,6 +2,9 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||
/* eslint-disable @stylistic/indent */
|
||||
|
||||
import type { ChannelEmojisElement } from '../channel-emojis'
|
||||
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
|
||||
import { maxEmojisPerChannel } from 'shared/lib/emojis'
|
||||
@ -45,13 +48,14 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
|
||||
|
||||
<livechat-channel-tabs .active=${'emojis'} .channelId=${el.channelId}></livechat-channel-tabs>
|
||||
|
||||
<h2>${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_TITLE)}</h2>
|
||||
|
||||
<p>
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_DESC)}
|
||||
<livechat-help-button .page=${'documentation/user/streamers/emojis'}>
|
||||
</livechat-help-button>
|
||||
</p>
|
||||
|
||||
|
||||
<form role="form" @submit=${el.saveEmojis} @change=${el.resetValidation}>
|
||||
<div class="peertube-plugin-livechat-configuration-actions">
|
||||
${
|
||||
@ -86,7 +90,7 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
|
||||
.maxLines=${maxEmojisPerChannel}
|
||||
.validation=${el.validationError?.properties}
|
||||
.validationPrefix=${'emojis'}
|
||||
.rows=${el.channelEmojisConfiguration?.emojis.customEmojis}
|
||||
.rows=${el.channelEmojisConfiguration?.emojis.customEmojis ?? []}
|
||||
@update=${(e: CustomEvent) => {
|
||||
el.resetValidation(e)
|
||||
if (el.channelEmojisConfiguration) {
|
||||
@ -106,5 +110,23 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h2>${ptTr(LOC_EMOJI_ONLY_MODE_TITLE)}</h2>
|
||||
|
||||
<p>
|
||||
${ptTr(LOC_EMOJI_ONLY_MODE_DESC_1, true)}
|
||||
</p>
|
||||
<p>
|
||||
${ptTr(LOC_EMOJI_ONLY_MODE_DESC_2, true)}
|
||||
</p>
|
||||
<p>
|
||||
${ptTr(LOC_EMOJI_ONLY_MODE_DESC_3, true)}
|
||||
</p>
|
||||
|
||||
<div class="peertube-plugin-livechat-configuration-actions">
|
||||
<button type="button" @click=${el.enableEmojisOnlyModeOnAllRooms}>
|
||||
${ptTr(LOC_EMOJI_ONLY_ENABLE_ALL_ROOMS)}
|
||||
</button>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import type {
|
||||
import { ValidationError, ValidationErrorType } from '../../lib/models/validation'
|
||||
import { getBaseRoute } from '../../../utils/uri'
|
||||
import { maxEmojisPerChannel } from 'shared/lib/emojis'
|
||||
import { channelTermsMaxLength } from 'shared/lib/constants'
|
||||
import { channelTermsMaxLength, noDuplicateMaxDelay, forbidSpecialCharsMaxTolerance } from 'shared/lib/constants'
|
||||
|
||||
export class ChannelDetailsService {
|
||||
public _registerClientOptions: RegisterClientOptions
|
||||
@ -67,11 +67,43 @@ export class ChannelDetailsService {
|
||||
// The backend will ignore those values.
|
||||
if (botConf.enabled) {
|
||||
propertiesError['bot.nickname'] = []
|
||||
propertiesError['bot.forbidSpecialChars.tolerance'] = []
|
||||
propertiesError['bot.noDuplicate.delay'] = []
|
||||
|
||||
if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) {
|
||||
propertiesError['bot.nickname'].push(ValidationErrorType.WrongFormat)
|
||||
}
|
||||
|
||||
if (botConf.forbidSpecialChars.enabled) {
|
||||
const forbidSpecialCharsTolerance = channelConfigurationOptions.bot.forbidSpecialChars.tolerance
|
||||
if (
|
||||
(typeof forbidSpecialCharsTolerance !== 'number') ||
|
||||
isNaN(forbidSpecialCharsTolerance)
|
||||
) {
|
||||
propertiesError['bot.forbidSpecialChars.tolerance'].push(ValidationErrorType.WrongType)
|
||||
} else if (
|
||||
forbidSpecialCharsTolerance < 0 ||
|
||||
forbidSpecialCharsTolerance > forbidSpecialCharsMaxTolerance
|
||||
) {
|
||||
propertiesError['bot.forbidSpecialChars.tolerance'].push(ValidationErrorType.NotInRange)
|
||||
}
|
||||
}
|
||||
|
||||
if (botConf.noDuplicate.enabled) {
|
||||
const noDuplicateDelay = channelConfigurationOptions.bot.noDuplicate.delay
|
||||
if (
|
||||
(typeof noDuplicateDelay !== 'number') ||
|
||||
isNaN(noDuplicateDelay)
|
||||
) {
|
||||
propertiesError['bot.noDuplicate.delay'].push(ValidationErrorType.WrongType)
|
||||
} else if (
|
||||
noDuplicateDelay < 0 ||
|
||||
noDuplicateDelay > noDuplicateMaxDelay
|
||||
) {
|
||||
propertiesError['bot.noDuplicate.delay'].push(ValidationErrorType.NotInRange)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [i, fw] of botConf.forbiddenWords.entries()) {
|
||||
for (const v of fw.entries) {
|
||||
propertiesError[`bot.forbiddenWords.${i}.entries`] = []
|
||||
@ -146,7 +178,8 @@ export class ChannelDetailsService {
|
||||
}
|
||||
|
||||
for (const channel of channels.data) {
|
||||
channel.livechatConfigurationUri = '/p/livechat/configuration/channel?channelId=' + encodeURIComponent(channel.id)
|
||||
channel.livechatConfigurationUri =
|
||||
'/p/livechat/configuration/channel?channelId=' + encodeURIComponent(channel.id as string | number)
|
||||
|
||||
// Note: since Peertube v6.0.0, channel.avatar is dropped, and we have to use channel.avatars.
|
||||
// So, if !channel.avatar, we will search a suitable one in channel.avatars, and fill channel.avatar.
|
||||
@ -180,10 +213,11 @@ export class ChannelDetailsService {
|
||||
}
|
||||
|
||||
public async fetchEmojisConfiguration (channelId: number): Promise<ChannelEmojisConfiguration> {
|
||||
const url = getBaseRoute(this._registerClientOptions) +
|
||||
'/api/configuration/channel/emojis/' +
|
||||
encodeURIComponent(channelId)
|
||||
const response = await fetch(
|
||||
getBaseRoute(this._registerClientOptions) +
|
||||
'/api/configuration/channel/emojis/' +
|
||||
encodeURIComponent(channelId),
|
||||
url,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: this._headers
|
||||
@ -295,10 +329,11 @@ export class ChannelDetailsService {
|
||||
channelId: number,
|
||||
channelEmojis: ChannelEmojis
|
||||
): Promise<ChannelEmojisConfiguration> {
|
||||
const url = getBaseRoute(this._registerClientOptions) +
|
||||
'/api/configuration/channel/emojis/' +
|
||||
encodeURIComponent(channelId)
|
||||
const response = await fetch(
|
||||
getBaseRoute(this._registerClientOptions) +
|
||||
'/api/configuration/channel/emojis/' +
|
||||
encodeURIComponent(channelId),
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: this._headers,
|
||||
@ -312,4 +347,24 @@ export class ChannelDetailsService {
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
public async enableEmojisOnlyModeOnAllRooms (channelId: number): Promise<void> {
|
||||
const url = getBaseRoute(this._registerClientOptions) +
|
||||
'/api/configuration/channel/emojis/' +
|
||||
encodeURIComponent(channelId) +
|
||||
'/enable_emoji_only'
|
||||
const response = await fetch(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: this._headers
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Can\'t enable Emojis Only Mode on all rooms.')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
|
||||
export const AddSVG: string =
|
||||
export const AddSVG =
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" class="feather feather-plus-square">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
@ -13,8 +14,9 @@ export const AddSVG: string =
|
||||
</svg>`
|
||||
|
||||
// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/
|
||||
export const RemoveSVG: string =
|
||||
export const RemoveSVG =
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" class="feather feather-x-square">
|
||||
<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 {
|
||||
private readonly _peertubeHelpers: RegisterClientHelpers
|
||||
|
||||
private _translatedValue: string = ''
|
||||
private _localizationId: string = ''
|
||||
private _translatedValue = ''
|
||||
private _localizationId = ''
|
||||
|
||||
private _allowUnsafeHTML = false
|
||||
|
||||
@ -25,7 +25,7 @@ export class TranslationDirective extends AsyncDirective {
|
||||
this._asyncUpdateTranslation().then(() => {}, () => {})
|
||||
}
|
||||
|
||||
public override render = (locId: string, allowHTML: boolean = false): TemplateResult | string => {
|
||||
public override render = (locId: string, allowHTML = false): TemplateResult | string => {
|
||||
this._localizationId = locId // TODO Check current component for context (to infer the prefix)
|
||||
|
||||
this._allowUnsafeHTML = allowHTML
|
||||
|
@ -3,6 +3,9 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||
/* eslint-disable @stylistic/indent */
|
||||
|
||||
import { ptTr } from '../directives/translation'
|
||||
import { html } from 'lit'
|
||||
import { customElement, property } from 'lit/decorators.js'
|
||||
|
@ -3,6 +3,9 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||
/* eslint-disable @stylistic/indent */
|
||||
|
||||
import type { TagsInputElement } from './tags-input'
|
||||
import type { DirectiveResult } from 'lit/directive'
|
||||
import { ValidationErrorType } from '../models/validation'
|
||||
@ -20,26 +23,26 @@ import { AddSVG, RemoveSVG } from '../buttons'
|
||||
type DynamicTableAcceptedTypes = number | string | boolean | Date | Array<number | string>
|
||||
|
||||
type DynamicTableAcceptedInputTypes = 'textarea'
|
||||
| 'select'
|
||||
| 'checkbox'
|
||||
| 'range'
|
||||
| 'color'
|
||||
| 'date'
|
||||
| 'datetime'
|
||||
| 'datetime-local'
|
||||
| 'email'
|
||||
| 'file'
|
||||
| 'image'
|
||||
| 'month'
|
||||
| 'number'
|
||||
| 'password'
|
||||
| 'tel'
|
||||
| 'text'
|
||||
| 'time'
|
||||
| 'url'
|
||||
| 'week'
|
||||
| 'tags'
|
||||
| 'image-file'
|
||||
| 'select'
|
||||
| 'checkbox'
|
||||
| 'range'
|
||||
| 'color'
|
||||
| 'date'
|
||||
| 'datetime'
|
||||
| 'datetime-local'
|
||||
| 'email'
|
||||
| 'file'
|
||||
| 'image'
|
||||
| 'month'
|
||||
| 'number'
|
||||
| 'password'
|
||||
| 'tel'
|
||||
| 'text'
|
||||
| 'time'
|
||||
| 'url'
|
||||
| 'week'
|
||||
| 'tags'
|
||||
| 'image-file'
|
||||
|
||||
interface CellDataSchema {
|
||||
min?: number
|
||||
@ -47,11 +50,11 @@ interface CellDataSchema {
|
||||
minlength?: number
|
||||
maxlength?: number
|
||||
size?: number
|
||||
label?: TemplateResult | string
|
||||
options?: { [key: string]: string }
|
||||
options?: Record<string, string>
|
||||
datalist?: DynamicTableAcceptedTypes[]
|
||||
separator?: string
|
||||
inputType?: DynamicTableAcceptedInputTypes
|
||||
inputTitle?: string
|
||||
default?: DynamicTableAcceptedTypes
|
||||
colClassList?: string[] // CSS classes to add to the <td> element.
|
||||
}
|
||||
@ -59,19 +62,17 @@ interface CellDataSchema {
|
||||
interface DynamicTableRowData {
|
||||
_id: number
|
||||
_originalIndex: number
|
||||
row: { [key: string]: DynamicTableAcceptedTypes }
|
||||
row: Record<string, DynamicTableAcceptedTypes>
|
||||
}
|
||||
|
||||
interface DynamicFormHeaderCellData {
|
||||
colName: TemplateResult | DirectiveResult
|
||||
description: TemplateResult | DirectiveResult
|
||||
description?: TemplateResult | DirectiveResult
|
||||
headerClassList?: string[]
|
||||
}
|
||||
|
||||
export interface DynamicFormHeader {
|
||||
[key: string]: DynamicFormHeaderCellData
|
||||
}
|
||||
export interface DynamicFormSchema { [key: string]: CellDataSchema }
|
||||
export type DynamicFormHeader = Record<string, DynamicFormHeaderCellData>
|
||||
export type DynamicFormSchema = Record<string, CellDataSchema>
|
||||
|
||||
@customElement('livechat-dynamic-table-form')
|
||||
export class DynamicTableFormElement extends LivechatElement {
|
||||
@ -85,19 +86,19 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
public maxLines?: number = undefined
|
||||
|
||||
@property()
|
||||
public validation?: {[key: string]: ValidationErrorType[] }
|
||||
public validation?: Record<string, ValidationErrorType[]>
|
||||
|
||||
@property({ attribute: false })
|
||||
public validationPrefix: string = ''
|
||||
public validationPrefix = ''
|
||||
|
||||
@property({ attribute: false })
|
||||
public rows: Array<{ [key: string]: DynamicTableAcceptedTypes }> = []
|
||||
public rows: Array<Record<string, DynamicTableAcceptedTypes>> = []
|
||||
|
||||
@state()
|
||||
public _rowsById: DynamicTableRowData[] = []
|
||||
|
||||
@property({ attribute: false })
|
||||
public formName: string = ''
|
||||
public formName = ''
|
||||
|
||||
@state()
|
||||
private _lastRowId = 1
|
||||
@ -112,7 +113,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
}
|
||||
}
|
||||
|
||||
private readonly _getDefaultRow = (): { [key: string]: DynamicTableAcceptedTypes } => {
|
||||
private readonly _getDefaultRow = (): Record<string, DynamicTableAcceptedTypes> => {
|
||||
this._updateLastRowId()
|
||||
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? ''])])
|
||||
}
|
||||
@ -236,7 +237,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
classList.push(...headerCellData.headerClassList)
|
||||
}
|
||||
return html`<th scope="col" class=${classList.join(' ')}>
|
||||
${headerCellData.description}
|
||||
${headerCellData.description ?? ''}
|
||||
</th>`
|
||||
}
|
||||
|
||||
@ -245,11 +246,11 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
|
||||
return html`<tr id=${inputId}>
|
||||
${Object.keys(this.header)
|
||||
.sort((k1, k2) => this.columnOrder.indexOf(k1) - this.columnOrder.indexOf(k2))
|
||||
.map(key => this.renderDataCell(key,
|
||||
rowData.row[key] ?? this.schema[key].default,
|
||||
rowData._id,
|
||||
rowData._originalIndex))}
|
||||
.sort((k1, k2) => this.columnOrder.indexOf(k1) - this.columnOrder.indexOf(k2))
|
||||
.map(key => this.renderDataCell(key,
|
||||
rowData.row[key] ?? this.schema[key].default,
|
||||
rowData._id,
|
||||
rowData._originalIndex))}
|
||||
<td class="form-group">
|
||||
<button type="button"
|
||||
class="dynamic-table-remove-row"
|
||||
@ -295,6 +296,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
const inputId =
|
||||
`peertube-livechat-${this.formName.replace(/_/g, '-')}-${propertyName.toString().replace(/_/g, '-')}-${rowId}`
|
||||
|
||||
const inputTitle: DirectiveResult | undefined = propertySchema.inputTitle ?? this.header[propertyName]?.colName
|
||||
const feedback = this._renderFeedback(inputId, propertyName, originalIndex)
|
||||
|
||||
switch (propertySchema.default?.constructor) {
|
||||
@ -320,6 +322,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderInput(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
propertyValue as string,
|
||||
@ -332,6 +335,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderTextarea(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
propertyValue as string,
|
||||
@ -344,6 +348,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderSelect(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
propertyValue as string,
|
||||
@ -356,6 +361,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderImageFileInput(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
propertyValue?.toString(),
|
||||
@ -376,6 +382,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderInput(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
(propertyValue as Date).toISOString(),
|
||||
@ -394,6 +401,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderInput(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
propertyValue as string,
|
||||
@ -411,6 +419,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderCheckbox(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
propertyValue as boolean,
|
||||
@ -446,10 +455,10 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderInput(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
(propertyValue)?.join(propertySchema.separator ?? ',') ??
|
||||
propertyValue ?? propertySchema.default ?? '',
|
||||
(propertyValue)?.join(propertySchema.separator ?? ',') ?? propertyValue ?? propertySchema.default ?? '',
|
||||
originalIndex)}
|
||||
${feedback}
|
||||
`
|
||||
@ -461,10 +470,10 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderTextarea(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
(propertyValue)?.join(propertySchema.separator ?? ',') ??
|
||||
propertyValue ?? propertySchema.default ?? '',
|
||||
(propertyValue)?.join(propertySchema.separator ?? ',') ?? propertyValue ?? propertySchema.default ?? '',
|
||||
originalIndex)}
|
||||
${feedback}
|
||||
`
|
||||
@ -476,6 +485,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderTagsInput(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
propertyValue,
|
||||
@ -487,8 +497,10 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
}
|
||||
|
||||
if (!formElement) {
|
||||
this.logger.warn(`value type '${(propertyValue.constructor.toString())}' is incompatible` +
|
||||
`with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`)
|
||||
this.logger.warn(
|
||||
`value type '${(propertyValue.constructor.toString())}' is incompatible` +
|
||||
`with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`
|
||||
)
|
||||
}
|
||||
|
||||
const classList = ['form-group']
|
||||
@ -501,6 +513,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
_renderInput = (rowId: number,
|
||||
inputId: string,
|
||||
inputName: string,
|
||||
inputTitle: string | DirectiveResult | undefined,
|
||||
propertyName: string,
|
||||
propertySchema: CellDataSchema,
|
||||
propertyValue: string,
|
||||
@ -515,6 +528,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
)
|
||||
)}
|
||||
id=${inputId}
|
||||
title=${ifDefined(inputTitle)}
|
||||
aria-describedby="${inputId}-feedback"
|
||||
list=${ifDefined(propertySchema.datalist ? inputId + '-datalist' : undefined)}
|
||||
min=${ifDefined(propertySchema.min)}
|
||||
@ -534,6 +548,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
_renderTagsInput = (rowId: number,
|
||||
inputId: string,
|
||||
inputName: string,
|
||||
inputTitle: string | DirectiveResult | undefined,
|
||||
propertyName: string,
|
||||
propertySchema: CellDataSchema,
|
||||
propertyValue: Array<string | number>,
|
||||
@ -547,7 +562,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
)
|
||||
)}
|
||||
id=${inputId}
|
||||
.inputPlaceholder=${propertySchema.label as any}
|
||||
.inputTitle=${inputTitle as any}
|
||||
aria-describedby="${inputId}-feedback"
|
||||
.min=${propertySchema.min}
|
||||
.max=${propertySchema.max}
|
||||
@ -563,6 +578,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
_renderTextarea = (rowId: number,
|
||||
inputId: string,
|
||||
inputName: string,
|
||||
inputTitle: string | DirectiveResult | undefined,
|
||||
propertyName: string,
|
||||
propertySchema: CellDataSchema,
|
||||
propertyValue: string,
|
||||
@ -576,6 +592,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
)
|
||||
)}
|
||||
id=${inputId}
|
||||
title=${ifDefined(inputTitle)}
|
||||
aria-describedby="${inputId}-feedback"
|
||||
min=${ifDefined(propertySchema.min)}
|
||||
max=${ifDefined(propertySchema.max)}
|
||||
@ -588,6 +605,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
_renderCheckbox = (rowId: number,
|
||||
inputId: string,
|
||||
inputName: string,
|
||||
inputTitle: string | DirectiveResult | undefined,
|
||||
propertyName: string,
|
||||
propertySchema: CellDataSchema,
|
||||
propertyValue: boolean,
|
||||
@ -602,6 +620,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
)
|
||||
)}
|
||||
id=${inputId}
|
||||
title=${ifDefined(inputTitle)}
|
||||
aria-describedby="${inputId}-feedback"
|
||||
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
||||
value="1"
|
||||
@ -611,6 +630,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
_renderSelect = (rowId: number,
|
||||
inputId: string,
|
||||
inputName: string,
|
||||
inputTitle: string | DirectiveResult | undefined,
|
||||
propertyName: string,
|
||||
propertySchema: CellDataSchema,
|
||||
propertyValue: string,
|
||||
@ -623,11 +643,12 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
)
|
||||
)}
|
||||
id=${inputId}
|
||||
title=${ifDefined(inputTitle)}
|
||||
aria-describedby="${inputId}-feedback"
|
||||
aria-label=${inputName}
|
||||
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
||||
>
|
||||
<option ?selected=${!propertyValue}>${propertySchema.label ?? 'Choose your option'}</option>
|
||||
<option ?selected=${!propertyValue}>${inputTitle ?? ''}</option>
|
||||
${Object.entries(propertySchema.options ?? {})
|
||||
?.map(([value, name]) =>
|
||||
html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`
|
||||
@ -638,6 +659,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
_renderImageFileInput = (rowId: number,
|
||||
inputId: string,
|
||||
inputName: string,
|
||||
inputTitle: string | DirectiveResult | undefined,
|
||||
propertyName: string,
|
||||
propertySchema: CellDataSchema,
|
||||
propertyValue: string,
|
||||
@ -647,6 +669,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
.name=${inputName}
|
||||
class=${classMap(this._getInputValidationClass(propertyName, originalIndex))}
|
||||
id=${inputId}
|
||||
.inputTitle=${inputTitle as any}
|
||||
aria-describedby="${inputId}-feedback"
|
||||
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
||||
.value=${propertyValue}
|
||||
@ -656,7 +679,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
}
|
||||
|
||||
_getInputValidationClass = (propertyName: string,
|
||||
originalIndex: number): { [key: string]: boolean } => {
|
||||
originalIndex: number): Record<string, boolean> => {
|
||||
const validationErrorTypes: ValidationErrorType[] | undefined =
|
||||
this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`]
|
||||
|
||||
|
@ -18,7 +18,7 @@ export class HelpButtonElement extends LivechatElement {
|
||||
public buttonTitle: string | DirectiveResult = ptTr(LOC_ONLINE_HELP)
|
||||
|
||||
@property({ attribute: false })
|
||||
public page: string = ''
|
||||
public page = ''
|
||||
|
||||
@state()
|
||||
public url: URL = new URL('https://lmddgtfy.net/')
|
||||
|
@ -2,10 +2,14 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||
/* eslint-disable @stylistic/indent */
|
||||
|
||||
import { LivechatElement } from './livechat'
|
||||
import { html } from 'lit'
|
||||
import type { DirectiveResult } from 'lit/directive'
|
||||
import { customElement, property } from 'lit/decorators.js'
|
||||
|
||||
import { ifDefined } from 'lit/directives/if-defined.js'
|
||||
/**
|
||||
* Special element to upload image files.
|
||||
* If no current value, displays an input type="file" field.
|
||||
@ -29,13 +33,16 @@ export class ImageFileInputElement extends LivechatElement {
|
||||
@property({ attribute: false })
|
||||
public maxSize?: number
|
||||
|
||||
@property({ attribute: false })
|
||||
public inputTitle?: string | DirectiveResult
|
||||
|
||||
@property({ attribute: false })
|
||||
public accept: string[] = ['image/jpg', 'image/png', 'image/gif']
|
||||
|
||||
protected override render = (): unknown => {
|
||||
return html`
|
||||
${this.value
|
||||
? html`<img src=${this.value} @click=${(ev: Event) => {
|
||||
? html`<img src=${this.value} alt=${ifDefined(this.inputTitle)} @click=${(ev: Event) => {
|
||||
ev.preventDefault()
|
||||
const upload: HTMLInputElement | null | undefined = this.parentElement?.querySelector('input[type="file"]')
|
||||
upload?.click()
|
||||
@ -44,6 +51,7 @@ export class ImageFileInputElement extends LivechatElement {
|
||||
}
|
||||
<input
|
||||
type="file"
|
||||
title=${ifDefined(this.inputTitle)}
|
||||
accept="${this.accept.join(',')}"
|
||||
class="form-control"
|
||||
style=${this.value ? 'display: none;' : ''}
|
||||
|
@ -26,7 +26,7 @@ export class LivechatElement extends LitElement {
|
||||
this.logger = this.ptContext.logger.createLogger(this.tagName.toLowerCase())
|
||||
}
|
||||
|
||||
protected override createRenderRoot = (): Element | ShadowRoot => {
|
||||
protected override createRenderRoot = (): HTMLElement | DocumentFragment => {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,9 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||
/* eslint-disable @stylistic/indent */
|
||||
|
||||
import { LivechatElement } from './livechat'
|
||||
import { ptTr } from '../directives/translation'
|
||||
import { html } from 'lit'
|
||||
@ -12,6 +15,7 @@ import { ifDefined } from 'lit/directives/if-defined.js'
|
||||
import { classMap } from 'lit/directives/class-map.js'
|
||||
import { animate, fadeOut, fadeIn } from '@lit-labs/motion'
|
||||
import { repeat } from 'lit/directives/repeat.js'
|
||||
import type { DirectiveResult } from 'lit/directive'
|
||||
|
||||
// FIXME: find a better way to store this image.
|
||||
// This content comes from the file assets/images/copy.svg, after svgo cleaning.
|
||||
@ -20,10 +24,11 @@ import { repeat } from 'lit/directives/repeat.js'
|
||||
// Then replace the main color by «currentColor»
|
||||
const copySVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 4.233 4.233">
|
||||
<g style="stroke-width:1.00021;stroke-miterlimit:4;stroke-dasharray:none">` +
|
||||
// eslint-disable-next-line max-len
|
||||
// eslint-disable-next-line max-len, @stylistic/indent-binary-ops
|
||||
'<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m4.084 4.046-.616.015-.645-.004a.942.942 0 0 1-.942-.942v-4.398a.94.94 0 0 1 .942-.943H7.22a.94.94 0 0 1 .942.943l-.006.334-.08.962" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' +
|
||||
// eslint-disable-next-line max-len
|
||||
'<path style="opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M8.434 5.85c-.422.009-1.338.009-1.76.01-.733.004-2.199 0-2.199 0a.94.94 0 0 1-.942-.941V.52a.94.94 0 0 1 .942-.942h4.398a.94.94 0 0 1 .943.942s.004 1.466 0 2.2c-.003.418-.019 1.251-.006 1.67.024.812-.382 1.439-1.376 1.46z" transform="matrix(.45208 0 0 .45208 -.528 1.295)"/>' +
|
||||
// eslint-disable-next-line @stylistic/indent-binary-ops
|
||||
`</g>
|
||||
</svg>`
|
||||
|
||||
@ -48,7 +53,7 @@ export class TagsInputElement extends LivechatElement {
|
||||
private _inputValue?: string = ''
|
||||
|
||||
@property({ attribute: false })
|
||||
public inputPlaceholder?: string = ''
|
||||
public inputTitle?: string | DirectiveResult = ''
|
||||
|
||||
@property({ attribute: false })
|
||||
public datalist?: string[]
|
||||
@ -63,10 +68,10 @@ export class TagsInputElement extends LivechatElement {
|
||||
private readonly _isPressingKey: string[] = []
|
||||
|
||||
@property({ attribute: false })
|
||||
public separator: string = '\n'
|
||||
public separator = '\n'
|
||||
|
||||
@property({ attribute: false })
|
||||
public animDuration: number = 200
|
||||
public animDuration = 200
|
||||
|
||||
/**
|
||||
* Overloading the standard focus method.
|
||||
@ -166,7 +171,7 @@ export class TagsInputElement extends LivechatElement {
|
||||
@input=${(e: InputEvent) => this._handleInputEvent(e)}
|
||||
@change=${(e: Event) => e.stopPropagation()}
|
||||
.value=${this._inputValue ?? ''}
|
||||
placeholder=${ifDefined(this.inputPlaceholder)} />
|
||||
title=${ifDefined(this.inputTitle)} />
|
||||
${(this.datalist)
|
||||
? html`<datalist id="${this.id ?? 'tags-input'}-datalist">
|
||||
${(this.datalist ?? []).map((value) => html`<option value=${value}>`)}
|
||||
@ -244,8 +249,9 @@ export class TagsInputElement extends LivechatElement {
|
||||
if (!this._isPressingKey.includes(e.key)) {
|
||||
this._isPressingKey.push(e.key)
|
||||
|
||||
if ((target.selectionStart === target.selectionEnd) &&
|
||||
target.selectionStart === 0) {
|
||||
if (
|
||||
(target.selectionStart === target.selectionEnd) && target.selectionStart === 0
|
||||
) {
|
||||
this._handleDeleteTag((this._searchedTagsIndex.length)
|
||||
? this._searchedTagsIndex.slice(-1)[0]
|
||||
: (this.value.length - 1))
|
||||
@ -258,8 +264,9 @@ export class TagsInputElement extends LivechatElement {
|
||||
if (!this._isPressingKey.includes(e.key)) {
|
||||
this._isPressingKey.push(e.key)
|
||||
|
||||
if ((target.selectionStart === target.selectionEnd) &&
|
||||
target.selectionStart === target.value.length) {
|
||||
if (
|
||||
(target.selectionStart === target.selectionEnd) && target.selectionStart === target.value.length
|
||||
) {
|
||||
this._handleDeleteTag((this._searchedTagsIndex.length)
|
||||
? this._searchedTagsIndex[0]
|
||||
: 0)
|
||||
|
@ -2,6 +2,9 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||
/* eslint-disable @stylistic/indent */
|
||||
|
||||
import type { LivechatTokenListElement } from '../token-list'
|
||||
import { html, TemplateResult } from 'lit'
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
|
||||
@ -23,11 +26,11 @@ export function tplTokenList (el: LivechatTokenListElement): TemplateResult {
|
||||
<tbody>
|
||||
${
|
||||
repeat(el.tokenList ?? [], (token) => token.id, (token) => {
|
||||
let dateStr: string = ''
|
||||
let dateStr = ''
|
||||
try {
|
||||
const date = new Date(token.date)
|
||||
dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
||||
} catch (err) {}
|
||||
} catch (_err) {}
|
||||
return html`<tr>
|
||||
<td>${
|
||||
el.mode === 'select'
|
||||
|
@ -27,7 +27,7 @@ export class LivechatTokenListElement extends LivechatElement {
|
||||
public currentSelectedToken?: LivechatToken
|
||||
|
||||
@property({ attribute: false })
|
||||
public actionDisabled: boolean = false
|
||||
public actionDisabled = false
|
||||
|
||||
private readonly _tokenListService: TokenListService
|
||||
private readonly _asyncTaskRender: Task
|
||||
@ -83,7 +83,7 @@ export class LivechatTokenListElement extends LivechatElement {
|
||||
this.dispatchEvent(new CustomEvent('update', {}))
|
||||
} catch (err: any) {
|
||||
this.logger.error(err)
|
||||
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR))
|
||||
this.ptNotifier.error((err as Error).toString(), await this.ptTranslate(LOC_ERROR))
|
||||
} finally {
|
||||
this.actionDisabled = false
|
||||
}
|
||||
@ -102,7 +102,7 @@ export class LivechatTokenListElement extends LivechatElement {
|
||||
this.dispatchEvent(new CustomEvent('update', {}))
|
||||
} catch (err: any) {
|
||||
this.logger.error(err)
|
||||
this.ptNotifier.error(err.toString(), await this.ptTranslate(LOC_ERROR))
|
||||
this.ptNotifier.error((err as Error).toString(), await this.ptTranslate(LOC_ERROR))
|
||||
} finally {
|
||||
this.actionDisabled = false
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ export enum ValidationErrorType {
|
||||
}
|
||||
|
||||
export class ValidationError extends Error {
|
||||
properties: {[key: string]: ValidationErrorType[] } = {}
|
||||
properties: Record<string, ValidationErrorType[]> = {}
|
||||
|
||||
constructor (name: string, message: string | undefined, properties: ValidationError['properties']) {
|
||||
super(message)
|
||||
|
@ -42,6 +42,23 @@ function displayButton (dbo: displayButtonOptions): void {
|
||||
if ('href' in dbo) {
|
||||
button.href = dbo.href
|
||||
}
|
||||
|
||||
if (!button.href || button.href === '#') {
|
||||
// No href => it is not a link.
|
||||
button.role = 'button'
|
||||
button.tabIndex = 0
|
||||
|
||||
// We must also ensure that the enter key is triggering the onclick
|
||||
if (button.onclick) {
|
||||
button.onkeydown = ev => {
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault()
|
||||
button.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (('targetBlank' in dbo) && dbo.targetBlank) {
|
||||
button.target = '_blank'
|
||||
}
|
||||
@ -52,6 +69,10 @@ function displayButton (dbo: displayButtonOptions): void {
|
||||
tmp.innerHTML = svg.trim()
|
||||
const svgDom = tmp.firstChild
|
||||
if (svgDom) {
|
||||
if ('ariaHidden' in (svgDom as HTMLElement)) {
|
||||
// Icon must be hidden for screen readers.
|
||||
(svgDom as HTMLElement).ariaHidden = 'true'
|
||||
}
|
||||
button.prepend(svgDom)
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -16,8 +16,6 @@ import { localizedHelpUrl } from '../../utils/help'
|
||||
import { getBaseRoute } from '../../utils/uri'
|
||||
import { displayConverseJS } from '../../utils/conversejs'
|
||||
|
||||
let savedMyPluginFlexGrow: string | undefined
|
||||
|
||||
/**
|
||||
* Initialize the chat for the current video
|
||||
* @param video the video
|
||||
@ -25,7 +23,6 @@ let savedMyPluginFlexGrow: string | undefined
|
||||
async function initChat (video: Video): Promise<void> {
|
||||
const ptContext = getPtContext()
|
||||
const logger = ptContext.logger
|
||||
savedMyPluginFlexGrow = undefined
|
||||
|
||||
if (!video) {
|
||||
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('peertube-plugin-livechat-state', 'initializing')
|
||||
container.setAttribute('peertube-plugin-livechat-current-url', window.location.href)
|
||||
container.role = 'region'
|
||||
container.ariaLabel = await ptContext.ptOptions.peertubeHelpers.translate(LOC_CHAT)
|
||||
placeholder.append(container)
|
||||
|
||||
try {
|
||||
@ -61,8 +60,8 @@ async function initChat (video: Video): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
let showShareUrlButton: boolean = false
|
||||
let showPromote: boolean = false
|
||||
let showShareUrlButton = false
|
||||
let showPromote = false
|
||||
if (video.isLocal) { // No need for shareButton on remote chats.
|
||||
const chatShareUrl = settings['chat-share-url'] ?? ''
|
||||
if (chatShareUrl === 'everyone') {
|
||||
@ -188,9 +187,10 @@ async function _insertChatDom (
|
||||
callback: async () => {
|
||||
try {
|
||||
// First we must get the room JID (can be video.uuid@ or channel.id@)
|
||||
const url = getBaseRoute(ptContext.ptOptions) + '/api/configuration/room/' +
|
||||
encodeURIComponent(video.uuid)
|
||||
const response = await fetch(
|
||||
getBaseRoute(ptContext.ptOptions) + '/api/configuration/room/' +
|
||||
encodeURIComponent(video.uuid),
|
||||
url,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: peertubeHelpers.getAuthHeader()
|
||||
@ -304,7 +304,7 @@ async function _openChat (video: Video): Promise<void | false> {
|
||||
|
||||
// Loading converseJS...
|
||||
await displayConverseJS(ptContext.ptOptions, container, roomkey, 'peertube-video', false)
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
// Displaying an error page.
|
||||
if (container) {
|
||||
const message = document.createElement('div')
|
||||
@ -353,19 +353,6 @@ function _hackStyles (on: boolean): void {
|
||||
buttons.classList.remove('peertube-plugin-livechat-buttons-open')
|
||||
}
|
||||
})
|
||||
const myPluginPlaceholder: HTMLElement | null = document.querySelector('my-plugin-placeholder')
|
||||
if (on) {
|
||||
// Saving current style attributes and maximazing space for the chat
|
||||
if (myPluginPlaceholder) {
|
||||
savedMyPluginFlexGrow = myPluginPlaceholder.style.flexGrow // Should be "", but can be anything else.
|
||||
myPluginPlaceholder.style.flexGrow = '1'
|
||||
}
|
||||
} else {
|
||||
// restoring values...
|
||||
if (savedMyPluginFlexGrow !== undefined && myPluginPlaceholder) {
|
||||
myPluginPlaceholder.style.flexGrow = savedMyPluginFlexGrow
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
getPtContext().logger.error(`Failed hacking styles: '${err as string}'`)
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import { getIframeUri, getXMPPAddr, UriOptions } from '../uri'
|
||||
import { isAnonymousUser } from '../../../utils/user'
|
||||
|
||||
// First is default tab.
|
||||
const validTabNames = ['embed', 'dock', 'peertube', 'xmpp'] as const
|
||||
const validTabNames: string[] = ['embed', 'dock', 'peertube', 'xmpp'] as const
|
||||
|
||||
type ValidTabNames = typeof validTabNames[number]
|
||||
|
||||
@ -61,49 +61,49 @@ export class ShareChatElement extends LivechatElement {
|
||||
* Should we render the XMPP tab?
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public xmppUriEnabled: boolean = false
|
||||
public xmppUriEnabled = false
|
||||
|
||||
/**
|
||||
* Should we render the Dock tab?
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public dockEnabled: boolean = false
|
||||
public dockEnabled = false
|
||||
|
||||
/**
|
||||
* Can we use autocolors?
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public autocolorsAvailable: boolean = false
|
||||
public autocolorsAvailable = false
|
||||
|
||||
/**
|
||||
* In the Embed tab, should we generated an iframe link.
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public embedIFrame: boolean = false
|
||||
public embedIFrame = false
|
||||
|
||||
/**
|
||||
* In the Embed tab, should we generated a read-only chat link.
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public embedReadOnly: boolean = false
|
||||
public embedReadOnly = false
|
||||
|
||||
/**
|
||||
* Read-only, with scrollbar?
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public embedReadOnlyScrollbar: boolean = false
|
||||
public embedReadOnlyScrollbar = false
|
||||
|
||||
/**
|
||||
* Read-only, transparent background?
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public embedReadOnlyTransparentBackground: boolean = false
|
||||
public embedReadOnlyTransparentBackground = false
|
||||
|
||||
/**
|
||||
* In the Embed tab, should we use current theme color?
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public embedAutocolors: boolean = false
|
||||
public embedAutocolors = false
|
||||
|
||||
protected override firstUpdated (changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties)
|
||||
@ -156,7 +156,7 @@ export class ShareChatElement extends LivechatElement {
|
||||
return
|
||||
}
|
||||
this.logger.log('Restoring previous state')
|
||||
if (validTabNames.includes(v.currentTab)) {
|
||||
if (validTabNames.includes(v.currentTab as string)) {
|
||||
this.currentTab = v.currentTab
|
||||
}
|
||||
this.embedIFrame = !!v.embedIFrame
|
||||
|
@ -2,6 +2,9 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||
/* eslint-disable @stylistic/indent */
|
||||
|
||||
import type { ShareChatElement } from '../share-chat'
|
||||
import { html, TemplateResult } from 'lit'
|
||||
import { ptTr } from '../../../lib/directives/translation'
|
||||
|
@ -71,8 +71,7 @@ async function shareChatUrl (
|
||||
addedNodes.forEach(node => {
|
||||
if ((node as HTMLElement).localName === 'ngb-modal-window') {
|
||||
logger.info('Detecting a new modal, checking if this is the good one...')
|
||||
if (!(node as HTMLElement).querySelector) { return }
|
||||
const title = (node as HTMLElement).querySelector('.modal-title')
|
||||
const title = (node as HTMLElement).querySelector?.('.modal-title')
|
||||
if (!(title?.textContent === labelShare)) {
|
||||
return
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
||||
import type { Video } from '@peertube/peertube-types'
|
||||
import type { LiveChatSettings } from '../lib/contexts/peertube'
|
||||
import { AutoColors, isAutoColorsAvailable } from 'shared/lib/autocolors'
|
||||
import { getBaseRoute } from '../../utils/uri'
|
||||
import { logger } from '../../utils/logger'
|
||||
@ -17,7 +18,7 @@ interface UriOptions {
|
||||
}
|
||||
|
||||
function getIframeUri (
|
||||
registerOptions: RegisterClientOptions, settings: any, video: Video, uriOptions: UriOptions = {}
|
||||
registerOptions: RegisterClientOptions, settings: LiveChatSettings, video: Video, uriOptions: UriOptions = {}
|
||||
): string | null {
|
||||
if (!settings) {
|
||||
logger.error('Settings are not initialized, too soon to compute the iframeUri')
|
||||
|
@ -156,12 +156,12 @@ function launchTests (): void {
|
||||
'content-type': 'application/json;charset=UTF-8'
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
test: test
|
||||
test
|
||||
})
|
||||
})
|
||||
if (!response.ok) {
|
||||
return {
|
||||
test: test,
|
||||
test,
|
||||
messages: [response.statusText ?? 'Unknown error'],
|
||||
ok: false
|
||||
}
|
||||
@ -169,7 +169,7 @@ function launchTests (): void {
|
||||
const data = await response.json()
|
||||
if ((typeof data) !== 'object') {
|
||||
return {
|
||||
test: test,
|
||||
test,
|
||||
messages: ['Incorrect reponse type: ' + (typeof data)],
|
||||
ok: false
|
||||
}
|
||||
@ -190,6 +190,7 @@ function launchTests (): void {
|
||||
waiting.innerHTML = '<i>Testing...</i>'
|
||||
ul.append(waiting)
|
||||
if ((typeof result.next) === 'function') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
const r: Result = (result.next as Function)()
|
||||
waiting.remove()
|
||||
await machine(r)
|
||||
|
@ -1,6 +1,7 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
||||
|
||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
||||
import type { InitConverseJSParams, ChatPeertubeIncludeMode } from 'shared/lib/types'
|
||||
@ -17,7 +18,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
let pollListenerInitiliazed: boolean = false
|
||||
let pollListenerInitiliazed = false
|
||||
|
||||
/**
|
||||
* load the ConverseJS CSS.
|
||||
@ -152,10 +153,11 @@ async function displayConverseJS (
|
||||
|
||||
const authHeader = peertubeHelpers.getAuthHeader()
|
||||
|
||||
const url = getBaseRoute(clientOptions) + '/api/configuration/room/' +
|
||||
encodeURIComponent(roomKey) +
|
||||
(forceType ? '?forcetype=1' : '')
|
||||
const response = await fetch(
|
||||
getBaseRoute(clientOptions) + '/api/configuration/room/' +
|
||||
encodeURIComponent(roomKey) +
|
||||
(forceType ? '?forcetype=1' : ''),
|
||||
url,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: authHeader
|
||||
@ -167,7 +169,7 @@ async function displayConverseJS (
|
||||
const converseJSParams: InitConverseJSParams = await (response).json()
|
||||
|
||||
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)
|
||||
pollListenerInitiliazed = true
|
||||
document.addEventListener('livechat-poll-vote', () => {
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
||||
|
||||
function getBaseRoute ({ peertubeHelpers }: RegisterClientOptions, permanent: boolean = false): string {
|
||||
function getBaseRoute ({ peertubeHelpers }: RegisterClientOptions, permanent = false): string {
|
||||
if (permanent) {
|
||||
return '/plugins/livechat/router'
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
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.
|
||||
# Defaults values:
|
||||
CONVERSE_VERSION="v10.1.6"
|
||||
CONVERSE_VERSION="v11.0.0"
|
||||
CONVERSE_REPO="https://github.com/conversejs/converse.js.git"
|
||||
# You can eventually set CONVERSE_COMMIT to a specific commit ID, if you want to apply some patches.
|
||||
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.
|
||||
# This version includes following changes:
|
||||
# - #converse.js/3300: Adding the maxWait option for `debouncedPruneHistory`
|
||||
# - #converse.js/3302: debounce MUC sidebar rendering
|
||||
# - Fix: refresh the MUC sidebar when participants collection is sorted
|
||||
# - Fix: MUC occupant list does not sort itself on nicknames or roles changes
|
||||
# - Fix inconsistency between browsers on textarea outlines
|
||||
# - Fix: room information not correctly refreshed when modifications are made by other users
|
||||
# This version already includes following changes that will not be merged in ConverseJS upstream:
|
||||
# - Don't load vCards for all room occupants when the right menu is closed
|
||||
# - Changing the default avatar, for something very light (to mitigate blinking effect when vCards are loaded)
|
||||
# - Custom settings livechat_load_all_vcards for the readonly mode
|
||||
# - Adding "users" icon in the menu toggle button
|
||||
# - Removing unecessary plugins: headless/pubsub, minimize, notifications, profile, omemo, push, roomlist, dragresize.
|
||||
# - Destroy room: remove the challenge, and the new JID
|
||||
# - New config option [colorize_username](https://conversejs.org/docs/html/configuration.html#colorize_username)
|
||||
# - New loadEmojis hook, to customize emojis at runtime.
|
||||
# - Fix custom emojis path when assets_path is not the default path.
|
||||
CONVERSE_VERSION="livechat-10.1.0"
|
||||
# CONVERSE_COMMIT="4402fcc3fc60f6c9334f86528c33a0b463371d12"
|
||||
# It is possible to use another repository, if we want some customization that are not upstream (yet):
|
||||
# CONVERSE_VERSION="livechat"
|
||||
# # CONVERSE_COMMIT="4402fcc3fc60f6c9334f86528c33a0b463371d12"
|
||||
# CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js"
|
||||
# CONVERSE_COMMIT="xxxx"
|
||||
|
||||
# 2024-09-03: include badges short label and quick fix for sendMessage button
|
||||
CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js"
|
||||
CONVERSE_VERSION="livechat-12.0.0"
|
||||
# CONVERSE_COMMIT=""
|
||||
|
||||
rootdir="$(pwd)"
|
||||
src_dir="$rootdir/conversejs"
|
||||
@ -50,6 +40,7 @@ if [ -n "$CONVERSE_COMMIT" ]; then
|
||||
fi
|
||||
converse_build_dir="$rootdir/build/conversejs"
|
||||
converse_destination_dir="$rootdir/dist/client/conversejs"
|
||||
converse_emoji_destination="$rootdir/dist/converse-emoji.json"
|
||||
|
||||
if [[ ! -d $src_dir ]]; then
|
||||
echo "$0 must be called from the plugin livechat root dir."
|
||||
@ -129,6 +120,9 @@ cd $rootdir
|
||||
echo "Copying ConverseJS dist files..."
|
||||
mkdir -p "$converse_destination_dir" && cp -r $converse_build_dir/dist/* "$converse_destination_dir/"
|
||||
|
||||
echo "Copying ConverseJS original emoji.json file..." # this is needed for some backend code.
|
||||
cp "$converse_build_dir/src/headless/plugins/emoji/emoji.json" "$converse_emoji_destination"
|
||||
|
||||
echo "ConverseJS OK."
|
||||
|
||||
exit 0
|
||||
|
@ -2,6 +2,8 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
||||
|
||||
import type { InitConverseJSParams, ChatIncludeMode, ExternalAuthResult } from 'shared/lib/types'
|
||||
import { inIframe } from './lib/utils'
|
||||
import { initDom } from './lib/dom'
|
||||
@ -21,6 +23,7 @@ import { livechatViewerModePlugin } from './lib/plugins/livechat-viewer-mode'
|
||||
import { livechatMiniMucHeadPlugin } from './lib/plugins/livechat-mini-muc-head'
|
||||
import { livechatEmojisPlugin } from './lib/plugins/livechat-emojis'
|
||||
import { moderationDelayPlugin } from './lib/plugins/moderation-delay'
|
||||
import { livechatAnnouncementsPlugin } from './lib/plugins/livechat-announcements'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@ -34,11 +37,18 @@ declare global {
|
||||
env: {
|
||||
html: Function
|
||||
sizzle: Function
|
||||
dayjs: Function
|
||||
__: Function
|
||||
u: {
|
||||
hasClass: Function
|
||||
addClass: Function
|
||||
removeClass: Function
|
||||
}
|
||||
}
|
||||
}
|
||||
initConversePlugins: typeof initConversePlugins
|
||||
initConverse: typeof initConverse
|
||||
reconnectConverse?: (room: string) => void
|
||||
reconnectConverse?: (params: any) => void
|
||||
externalAuthGetResult?: (data: ExternalAuthResult) => void
|
||||
}
|
||||
}
|
||||
@ -73,6 +83,8 @@ function initConversePlugins (peertubeEmbedded: boolean): void {
|
||||
converse.plugins.add('livechatViewerModePlugin', livechatViewerModePlugin)
|
||||
|
||||
converse.plugins.add('converse-moderation-delay', moderationDelayPlugin)
|
||||
|
||||
converse.plugins.add('livechatAnnouncementsPlugin', livechatAnnouncementsPlugin)
|
||||
}
|
||||
window.initConversePlugins = initConversePlugins
|
||||
|
||||
@ -85,7 +97,7 @@ window.initConversePlugins = initConversePlugins
|
||||
async function initConverse (
|
||||
initConverseParams: InitConverseJSParams,
|
||||
chatIncludeMode: ChatIncludeMode = 'chat-only',
|
||||
peertubeAuthHeader?: { [header: string]: string } | null
|
||||
peertubeAuthHeader?: Record<string, string> | null
|
||||
): Promise<void> {
|
||||
// First, fixing relative websocket urls.
|
||||
if (initConverseParams.localWebsocketServiceUrl?.startsWith('/')) {
|
||||
@ -120,9 +132,9 @@ async function initConverse (
|
||||
params.view_mode = chatIncludeMode === 'chat-only' ? 'fullscreen' : 'embedded'
|
||||
params.allow_url_history_change = chatIncludeMode === 'chat-only'
|
||||
|
||||
let isAuthenticated: boolean = false
|
||||
let isAuthenticatedWithExternalAccount: boolean = false
|
||||
let isRemoteWithNicknameSet: boolean = false
|
||||
let isAuthenticated = false
|
||||
let isAuthenticatedWithExternalAccount = false
|
||||
let isRemoteWithNicknameSet = false
|
||||
|
||||
// OIDC (OpenID Connect):
|
||||
const tryOIDC = (initConverseParams.externalAuthOIDC?.length ?? 0) > 0
|
||||
@ -218,20 +230,24 @@ async function initConverse (
|
||||
// * 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
|
||||
// (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') {
|
||||
enableTask = true
|
||||
enableApps = true
|
||||
} else if (
|
||||
chatIncludeMode === 'chat-only' &&
|
||||
usedLivechatToken &&
|
||||
!initConverseParams.transparent &&
|
||||
!initConverseParams.forceReadonly
|
||||
) {
|
||||
enableTask = true
|
||||
enableApps = true
|
||||
}
|
||||
if (enableTask) {
|
||||
if (enableApps) {
|
||||
params.livechat_task_app_enabled = true
|
||||
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 {
|
||||
|
@ -8,14 +8,13 @@
|
||||
* @description This files will override the original ConverseJS index.js file.
|
||||
*/
|
||||
|
||||
import '@converse/headless'
|
||||
import 'shared/styles/index.scss'
|
||||
|
||||
import './i18n/index.js'
|
||||
import 'shared/registry.js'
|
||||
import { CustomElement } from 'shared/components/element'
|
||||
import { VIEW_PLUGINS } from './shared/constants.js'
|
||||
import { _converse, converse } from '@converse/headless/core'
|
||||
|
||||
import 'shared/styles/index.scss'
|
||||
import { _converse, converse } from '@converse/headless'
|
||||
|
||||
/* START: Removable plugins
|
||||
* ------------------------
|
||||
@ -45,11 +44,16 @@ import './plugins/singleton/index.js'
|
||||
import './plugins/fullscreen/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/terms/index.js'
|
||||
import '../custom/plugins/poll/index.js'
|
||||
/* END: Removable components */
|
||||
|
||||
// Running some specific livechat patches:
|
||||
import '../custom/livechat-patch-vcard.js'
|
||||
|
||||
import { CORE_PLUGINS } from './headless/shared/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):
|
||||
@ -57,11 +61,14 @@ CORE_PLUGINS.push('livechat-converse-size')
|
||||
CORE_PLUGINS.push('livechat-converse-tasks')
|
||||
CORE_PLUGINS.push('livechat-converse-terms')
|
||||
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
|
||||
// (see headless/plugins/muc, getDiscoInfoFeatures, which loops on this const)
|
||||
ROOM_FEATURES.push('x_peertubelivechat_mute_anonymous')
|
||||
ROOM_FEATURES.push('x_peertubelivechat_emoji_only_mode')
|
||||
|
||||
_converse.CustomElement = CustomElement
|
||||
_converse.exports.CustomElement = CustomElement
|
||||
|
||||
const initialize = converse.initialize
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
//
|
||||
// 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 { tplExternalLoginModal } from 'templates/livechat-external-login-modal.js'
|
||||
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 { tplPollForm } from '../templates/poll-form.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 { __ } from 'i18n'
|
||||
import '../styles/poll-form.scss'
|
||||
@ -18,7 +18,6 @@ export default class MUCPollFormView extends CustomElement {
|
||||
return {
|
||||
model: { type: Object, attribute: true },
|
||||
modal: { type: Object, attribute: true },
|
||||
form_fields: { type: Object, attribute: false },
|
||||
alert_message: { type: Object, attribute: false },
|
||||
title: { type: String, attribute: false },
|
||||
instructions: { type: String, attribute: false }
|
||||
@ -27,6 +26,8 @@ export default class MUCPollFormView extends CustomElement {
|
||||
|
||||
_fieldTranslationMap = new Map()
|
||||
|
||||
xform = undefined
|
||||
|
||||
async initialize () {
|
||||
this.alert_message = undefined
|
||||
if (!this.model) {
|
||||
@ -36,20 +37,18 @@ export default class MUCPollFormView extends CustomElement {
|
||||
try {
|
||||
this._initFieldTranslations()
|
||||
const stanza = await this._fetchPollForm()
|
||||
const query = stanza.querySelector('query')
|
||||
const xform = sizzle(`x[xmlns="${Strophe.NS.XFORM}"]`, query)[0]
|
||||
const xform = parsers.parseXForm(stanza)
|
||||
if (!xform) {
|
||||
throw Error('Missing xform in stanza')
|
||||
}
|
||||
|
||||
xform.fields?.map(f => this._translateField(f))
|
||||
this.xform = xform
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
this.title = __(LOC_poll_title) // xform.querySelector('title')?.textContent ?? ''
|
||||
// eslint-disable-next-line no-undef
|
||||
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) {
|
||||
console.error(err)
|
||||
this.alert_message = __('Error')
|
||||
@ -86,10 +85,10 @@ export default class MUCPollFormView extends CustomElement {
|
||||
}
|
||||
|
||||
_translateField (field) {
|
||||
const v = field.getAttribute('var')
|
||||
const v = field.var
|
||||
const label = this._fieldTranslationMap.get(v)
|
||||
if (label) {
|
||||
field.setAttribute('label', label)
|
||||
field.label = label
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,7 +113,7 @@ export default class MUCPollFormView extends CustomElement {
|
||||
await api.sendIQ(iq)
|
||||
|
||||
if (this.modal) {
|
||||
this.modal.onHide()
|
||||
this.modal.close()
|
||||
}
|
||||
} catch (err) {
|
||||
if (u.isErrorStanza(err)) {
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
import { tplPoll } from '../templates/poll.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'
|
||||
|
||||
export default class MUCPollView extends CustomElement {
|
||||
|
@ -2,7 +2,7 @@
|
||||
//
|
||||
// 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 { POLL_MESSAGE_TAG, POLL_QUESTION_TAG, POLL_CHOICE_TAG } from './constants.js'
|
||||
import { __ } from 'i18n'
|
||||
|
@ -2,9 +2,12 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||
/* eslint-disable @stylistic/indent */
|
||||
|
||||
import { __ } from 'i18n'
|
||||
import BaseModal from 'plugins/modal/modal.js'
|
||||
import { api } from '@converse/headless/core'
|
||||
import { api } from '@converse/headless'
|
||||
import { modal_close_button as ModalCloseButton } from 'plugins/modal/templates/buttons.js'
|
||||
import { html } from 'lit'
|
||||
|
||||
@ -13,8 +16,8 @@ class PollFormModal extends BaseModal {
|
||||
super.initialize()
|
||||
}
|
||||
|
||||
onHide () {
|
||||
super.onHide()
|
||||
close () {
|
||||
super.close()
|
||||
api.modal.remove('livechat-converse-poll-form-modal')
|
||||
}
|
||||
|
||||
|
@ -2,9 +2,16 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||
/* eslint-disable @stylistic/indent */
|
||||
|
||||
import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
|
||||
import { html } from 'lit'
|
||||
import { __ } from 'i18n'
|
||||
import { converse } from '@converse/headless'
|
||||
|
||||
const u = converse.env.utils
|
||||
|
||||
export function tplPollForm (el) {
|
||||
const i18nOk = __('Ok')
|
||||
// eslint-disable-next-line no-undef
|
||||
@ -13,10 +20,18 @@ export function tplPollForm (el) {
|
||||
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`
|
||||
${el.alert_message ? html`<div class="error">${el.alert_message}</div>` : ''}
|
||||
${
|
||||
el.form_fields
|
||||
formFieldTemplates
|
||||
? html`
|
||||
<form class="converse-form" @submit=${ev => el.formSubmit(ev)}>
|
||||
<p class="title">
|
||||
@ -30,9 +45,9 @@ export function tplPollForm (el) {
|
||||
<p class="form-help instructions">${el.instructions}</p>
|
||||
<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}" />
|
||||
</fieldset>
|
||||
</form>`
|
||||
|
@ -2,6 +2,9 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// FIXME: @stylistic/indent is buggy with strings literrals.
|
||||
/* eslint-disable @stylistic/indent */
|
||||
|
||||
import { html } from 'lit'
|
||||
import { repeat } from 'lit/directives/repeat.js'
|
||||
import { __ } from 'i18n'
|
||||
@ -63,7 +66,7 @@ function _tplChoice (el, currentPoll, choice, canVote) {
|
||||
<div class="livechat-progress-bar">
|
||||
<div
|
||||
role="progressbar"
|
||||
style="width: ${percent}%;"
|
||||
style=${'width: ' + percent + '%;'}
|
||||
aria-valuenow="${percent}" aria-valuemin="0" aria-valuemax="100"
|
||||
></div>
|
||||
<p>
|
||||
@ -83,21 +86,21 @@ export function tplPoll (el, currentPoll, canVote) {
|
||||
return html`<div class="${currentPoll.over ? 'livechat-poll-over' : ''}">
|
||||
<p class="livechat-poll-question">
|
||||
${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>
|
||||
</button>`
|
||||
: ''
|
||||
}
|
||||
${el.collapsed
|
||||
? html`
|
||||
<button @click=${el.toggle} class="livechat-poll-toggle">
|
||||
<button type="button" @click=${el.toggle} class="livechat-poll-toggle">
|
||||
<converse-icon
|
||||
color="var(--muc-toolbar-btn-color)"
|
||||
class="fa fa-angle-right"
|
||||
size="1em"></converse-icon>
|
||||
</button>`
|
||||
: html`
|
||||
<button @click=${el.toggle} class="livechat-poll-toggle">
|
||||
<button type="button" @click=${el.toggle} class="livechat-poll-toggle">
|
||||
<converse-icon
|
||||
color="var(--muc-toolbar-btn-color)"
|
||||
class="fa fa-angle-down"
|
||||
|
@ -3,12 +3,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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'
|
||||
|
||||
export function getHeadingButtons (view, buttons) {
|
||||
const muc = view.model
|
||||
if (muc.get('type') !== _converse.CHATROOMS_TYPE) {
|
||||
if (muc.get('type') !== _converse.constants.CHATROOMS_TYPE) {
|
||||
// only on MUC.
|
||||
return buttons
|
||||
}
|
||||
|
@ -2,7 +2,9 @@
|
||||
//
|
||||
// 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
|
||||
@ -16,6 +18,27 @@ converse.plugins.add('livechat-converse-size', {
|
||||
dependencies: [],
|
||||
|
||||
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('reconnected', start)
|
||||
_converse.api.listen.on('disconnected', stop)
|
||||
@ -42,6 +65,7 @@ function start () {
|
||||
}
|
||||
|
||||
function stop () {
|
||||
currentSize = undefined
|
||||
rootResizeObserver.disconnect()
|
||||
const root = document.querySelector('converse-root')
|
||||
if (root) {
|
||||
@ -60,8 +84,9 @@ function handle (el) {
|
||||
|
||||
el.setAttribute('livechat-converse-root-width', width)
|
||||
el.setAttribute('livechat-converse-root-height', height)
|
||||
api.trigger('livechatSizeChanged', {
|
||||
currentSize = {
|
||||
height: height,
|
||||
width: width
|
||||
})
|
||||
}
|
||||
api.trigger('livechatSizeChanged', Object.assign({}, currentSize)) // cloning...
|
||||
}
|
||||
|
@ -2,36 +2,20 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { api } from '@converse/headless/core'
|
||||
import { CustomElement } from 'shared/components/element.js'
|
||||
import { api } from '@converse/headless'
|
||||
import { MUCApp } from '../../../shared/components/muc-app/index.js'
|
||||
import { tplMUCTaskApp } from '../templates/muc-task-app.js'
|
||||
|
||||
import '../styles/muc-task-app.scss'
|
||||
|
||||
/**
|
||||
* Custom Element to display the Task Application.
|
||||
*/
|
||||
export default class MUCTaskApp extends CustomElement {
|
||||
static get properties () {
|
||||
return {
|
||||
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')
|
||||
}
|
||||
export default class MUCTaskApp extends MUCApp {
|
||||
restoreSettingName = 'livechat_task_app_restore'
|
||||
sessionStorageRestoreKey = 'livechat-converse-task-app-show'
|
||||
|
||||
render () {
|
||||
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)
|
||||
|
@ -3,7 +3,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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 { __ } 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