Compare commits
163 Commits
18a1c2f71c
...
7e0cfee8f1
Author | SHA1 | Date | |
---|---|---|---|
7e0cfee8f1 | |||
|
2f78b901e3 | ||
|
575703a7e5 | ||
|
8e0f239993 | ||
|
8a1948520d | ||
|
0fe0ebfb3e | ||
|
9ee4476f4d | ||
|
0e98cbaba5 | ||
|
22dc4db61b | ||
|
42147148ea | ||
|
87e8f9fd39 | ||
|
033a86b3a6 | ||
|
8394e7222d | ||
|
d8fc90dd1f | ||
|
04db24b6af | ||
|
b0d65add1f | ||
|
6b69f0bf46 | ||
|
6373af32ba | ||
|
d87cdbb1ff | ||
|
5d87121631 | ||
|
c205acbb17 | ||
|
5462e3d52f | ||
|
ef5bc3cb8a | ||
|
d8da3ca3b8 | ||
|
3d8fbba767 | ||
|
8183dc82bb | ||
|
a799a9c07e | ||
|
6eeb19607f | ||
|
56547cc084 | ||
|
75925b1117 | ||
|
2824bd1e38 | ||
|
7293f4a6d5 | ||
|
f7ddd58a2c | ||
|
4a747d7314 | ||
|
f5074934e4 | ||
|
6c0b5e1c19 | ||
|
1b75f3d504 | ||
|
b673a49af6 | ||
|
944bdcebb7 | ||
|
c6d012cfb4 | ||
|
0732bd1de3 | ||
|
e65bd5c426 | ||
|
3177c31b08 | ||
|
cee42b4bcc | ||
|
9e252193d4 | ||
|
08eb466e27 | ||
|
0cf5647a89 | ||
|
4113259975 | ||
|
4bdf40e905 | ||
|
a385204256 | ||
|
e1d1dd94e6 | ||
|
940e8c9ac4 | ||
|
9a22ab7f18 | ||
|
8e99199f29 | ||
|
481f265a44 | ||
|
6fd8383439 | ||
|
54c31500a3 | ||
|
e08c413682 | ||
|
73845eb5d4 | ||
|
a47737967a | ||
|
67b89f1aef | ||
|
e1e91c2984 | ||
|
a813ceb723 | ||
|
cd0813fb14 | ||
|
62caa63dc5 | ||
|
ef1b49f291 | ||
|
b8db486410 | ||
|
df75659a05 | ||
|
a3555ed3cd | ||
|
fd7d24c121 | ||
|
a9ae96622a | ||
|
4afc0b6ab8 | ||
|
ad5397b3c7 | ||
|
49e11d2b6b | ||
|
df7981f896 | ||
|
003cb24dd8 | ||
|
33da4314af | ||
|
c2c1211b9a | ||
|
5d843ebf92 | ||
|
18fa3aec10 | ||
|
631d8c7a6b | ||
|
9746f3d86e | ||
|
d412f86577 | ||
|
e665823f5b | ||
|
966669ebbc | ||
|
4181661faf | ||
|
dd03075831 | ||
|
a4497739fa | ||
|
cdbe97137e | ||
|
7892fd6c03 | ||
|
0543a720f2 | ||
|
af2941f4e0 | ||
|
3004105b5e | ||
|
bb2aca71c1 | ||
|
48763e6173 | ||
|
70f702f78e | ||
|
a46425d51f | ||
|
e81a7c90b8 | ||
|
9c2b84027a | ||
|
704e660f37 | ||
|
31c4e5a646 | ||
|
1c749f68bc | ||
|
fbc9a39485 | ||
|
eb76e7ebb9 | ||
|
20cb668e09 | ||
|
86cac34ef3 | ||
|
074e688ed8 | ||
|
34da786b65 | ||
|
a700263eda | ||
|
1b92e7287d | ||
|
bf8f3a08ec | ||
|
faef584f8b | ||
|
ce5114afc9 | ||
|
c5bcb9fc14 | ||
|
6e92882176 | ||
|
ebc8fc8797 | ||
|
38f2b2af57 | ||
|
b7c595214b | ||
|
58676a5508 | ||
|
12f11e4468 | ||
|
e1252709b3 | ||
|
80d458c445 | ||
|
f88520d925 | ||
|
dd4bca8c06 | ||
|
81632fa467 | ||
|
c6c365abf0 | ||
|
099ff28c76 | ||
|
b40b3a2716 | ||
|
731be16e53 | ||
|
a7bd0c1c3e | ||
|
e938f79182 | ||
|
36323569c0 | ||
|
e7b1376a43 | ||
|
1b4ccf6693 | ||
|
d4ecafb6de | ||
|
20550bc3d7 | ||
|
548b79a3a6 | ||
|
ed0b2eb913 | ||
|
bdc11cb92e | ||
|
8b0d72bf13 | ||
|
64f03e5454 | ||
|
522265db5c | ||
|
22daa45b92 | ||
|
76cd519c00 | ||
|
123f9a5a8a | ||
|
717a5c75de | ||
|
12a1300df6 | ||
|
5fb50b9221 | ||
|
00960652fe | ||
|
712d2bcdcb | ||
|
897f111e9d | ||
|
2f69e45b26 | ||
|
b9473cada9 | ||
|
cbcf51d1eb | ||
|
1226162b50 | ||
|
f1ac80d468 | ||
|
e8f287b8a9 | ||
|
e97cd1d78e | ||
|
6218d65b72 | ||
|
d0ab3d94ae | ||
|
a0d5c4a368 | ||
|
51b603c894 | ||
|
9679aec739 |
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
|
||||
|
37
CHANGELOG.md
37
CHANGELOG.md
@ -1,5 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
## 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%;
|
||||
}
|
||||
}
|
@ -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,4 +9,5 @@
|
||||
@use "elements/index";
|
||||
@use "video";
|
||||
@use "configuration/configuration";
|
||||
@use "admin/firewall/firewall";
|
||||
@use "list-rooms/list-rooms.scss";
|
||||
|
@ -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 (max-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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
11
client/@types/global.d.ts
vendored
11
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
|
||||
@ -133,3 +134,13 @@ 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
|
||||
|
@ -270,6 +270,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> {
|
||||
@ -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: boolean = 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): { [key: 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()
|
||||
}
|
||||
}
|
88
client/common/admin/firewall/templates/admin-firewall.ts
Normal file
88
client/common/admin/firewall/templates/admin-firewall.ts
Normal file
@ -0,0 +1,88 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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>`
|
||||
}
|
@ -50,7 +50,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>`
|
||||
|
@ -135,6 +135,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 +168,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 +255,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=${''}
|
||||
|
@ -6,6 +6,7 @@
|
||||
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
|
||||
export const AddSVG: string =
|
||||
`<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>
|
||||
@ -15,6 +16,7 @@ export const AddSVG: string =
|
||||
// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/
|
||||
export const RemoveSVG: string =
|
||||
`<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>
|
||||
|
@ -47,11 +47,11 @@ interface CellDataSchema {
|
||||
minlength?: number
|
||||
maxlength?: number
|
||||
size?: number
|
||||
label?: TemplateResult | string
|
||||
options?: { [key: string]: string }
|
||||
datalist?: DynamicTableAcceptedTypes[]
|
||||
separator?: string
|
||||
inputType?: DynamicTableAcceptedInputTypes
|
||||
inputTitle?: string
|
||||
default?: DynamicTableAcceptedTypes
|
||||
colClassList?: string[] // CSS classes to add to the <td> element.
|
||||
}
|
||||
@ -64,7 +64,7 @@ interface DynamicTableRowData {
|
||||
|
||||
interface DynamicFormHeaderCellData {
|
||||
colName: TemplateResult | DirectiveResult
|
||||
description: TemplateResult | DirectiveResult
|
||||
description?: TemplateResult | DirectiveResult
|
||||
headerClassList?: string[]
|
||||
}
|
||||
|
||||
@ -236,7 +236,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
classList.push(...headerCellData.headerClassList)
|
||||
}
|
||||
return html`<th scope="col" class=${classList.join(' ')}>
|
||||
${headerCellData.description}
|
||||
${headerCellData.description ?? ''}
|
||||
</th>`
|
||||
}
|
||||
|
||||
@ -295,6 +295,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 +321,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderInput(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
propertyValue as string,
|
||||
@ -332,6 +334,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderTextarea(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
propertyValue as string,
|
||||
@ -344,6 +347,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderSelect(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
propertyValue as string,
|
||||
@ -356,6 +360,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderImageFileInput(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
propertyValue?.toString(),
|
||||
@ -376,6 +381,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderInput(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
(propertyValue as Date).toISOString(),
|
||||
@ -394,6 +400,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderInput(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
propertyValue as string,
|
||||
@ -411,6 +418,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderCheckbox(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
propertyValue as boolean,
|
||||
@ -446,6 +454,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderInput(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
(propertyValue)?.join(propertySchema.separator ?? ',') ??
|
||||
@ -461,6 +470,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderTextarea(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
(propertyValue)?.join(propertySchema.separator ?? ',') ??
|
||||
@ -476,6 +486,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
formElement = html`${this._renderTagsInput(rowId,
|
||||
inputId,
|
||||
inputName,
|
||||
inputTitle,
|
||||
propertyName,
|
||||
propertySchema,
|
||||
propertyValue,
|
||||
@ -501,6 +512,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 +527,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 +547,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 +561,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 +577,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 +591,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 +604,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 +619,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 +629,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 +642,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 +658,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 +668,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}
|
||||
|
@ -1,11 +1,11 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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 +29,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 +47,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;' : ''}
|
||||
|
@ -12,6 +12,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.
|
||||
@ -48,7 +49,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[]
|
||||
@ -166,7 +167,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}>`)}
|
||||
|
@ -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 {
|
||||
@ -353,19 +352,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}'`)
|
||||
}
|
||||
|
@ -167,7 +167,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', () => {
|
||||
|
@ -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-02: using Converse upstream (v11 WIP).
|
||||
CONVERSE_COMMIT="9952046d580bc2930e29833f4c9987a3d4c95bc2"
|
||||
|
||||
# 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-11.0.1"
|
||||
CONVERSE_COMMIT=""
|
||||
|
||||
rootdir="$(pwd)"
|
||||
src_dir="$rootdir/conversejs"
|
||||
|
@ -34,6 +34,7 @@ declare global {
|
||||
env: {
|
||||
html: Function
|
||||
sizzle: Function
|
||||
dayjs: Function
|
||||
}
|
||||
}
|
||||
initConversePlugins: typeof initConversePlugins
|
||||
@ -218,20 +219,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,13 @@ 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')
|
||||
|
||||
_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,60 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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,32 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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,39 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
39
conversejs/custom/plugins/notes/templates/muc-note-app.js
Normal file
39
conversejs/custom/plugins/notes/templates/muc-note-app.js
Normal file
@ -0,0 +1,39 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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,44 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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>`
|
||||
: ''
|
||||
}
|
||||
`
|
||||
}
|
130
conversejs/custom/plugins/notes/templates/muc-note.js
Normal file
130
conversejs/custom/plugins/notes/templates/muc-note.js
Normal file
@ -0,0 +1,130 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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>`
|
||||
}
|
92
conversejs/custom/plugins/notes/templates/muc-notes.js
Normal file
92
conversejs/custom/plugins/notes/templates/muc-notes.js
Normal file
@ -0,0 +1,92 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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'
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
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 +13,8 @@ class PollFormModal extends BaseModal {
|
||||
super.initialize()
|
||||
}
|
||||
|
||||
onHide () {
|
||||
super.onHide()
|
||||
close () {
|
||||
super.close()
|
||||
api.modal.remove('livechat-converse-poll-form-modal')
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,10 @@
|
||||
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 +17,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 +42,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>`
|
||||
|
@ -63,7 +63,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 +83,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'
|
||||
|
||||
|
@ -2,17 +2,14 @@
|
||||
//
|
||||
// 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 tplMucTaskLists from '../templates/muc-task-lists'
|
||||
import { __ } from 'i18n'
|
||||
import { DraggablesCustomElement } from '../../../shared/components/draggables/index.js'
|
||||
|
||||
import '../styles/muc-task-lists.scss'
|
||||
import '../styles/muc-task-drag.scss'
|
||||
|
||||
export default class MUCTaskListsView extends CustomElement {
|
||||
currentDraggedTask = null
|
||||
|
||||
export default class MUCTaskListsView extends DraggablesCustomElement {
|
||||
static get properties () {
|
||||
return {
|
||||
model: { type: Object, attribute: true },
|
||||
@ -27,42 +24,22 @@ export default class MUCTaskListsView extends CustomElement {
|
||||
return
|
||||
}
|
||||
|
||||
this.draggableTagName = 'livechat-converse-muc-task'
|
||||
this.droppableTagNames = ['livechat-converse-muc-task', 'livechat-converse-muc-task-list']
|
||||
this.droppableAlwaysBottomTagNames = ['livechat-converse-muc-task-list']
|
||||
|
||||
// Adding or removing a new task list: we must update.
|
||||
this.listenTo(this.model, 'add', () => this.requestUpdate())
|
||||
this.listenTo(this.model, 'remove', () => this.requestUpdate())
|
||||
this.listenTo(this.model, 'sort', () => this.requestUpdate())
|
||||
|
||||
this._handleDragStartBinded = this._handleDragStart.bind(this)
|
||||
this._handleDragOverBinded = this._handleDragOver.bind(this)
|
||||
this._handleDragLeaveBinded = this._handleDragLeave.bind(this)
|
||||
this._handleDragEndBinded = this._handleDragEnd.bind(this)
|
||||
this._handleDropBinded = this._handleDrop.bind(this)
|
||||
return super.initialize()
|
||||
}
|
||||
|
||||
render () {
|
||||
return tplMucTaskLists(this, this.model)
|
||||
}
|
||||
|
||||
connectedCallback () {
|
||||
super.connectedCallback()
|
||||
this.currentDraggedTask = null
|
||||
this.addEventListener('dragstart', this._handleDragStartBinded)
|
||||
this.addEventListener('dragover', this._handleDragOverBinded)
|
||||
this.addEventListener('dragleave', this._handleDragLeaveBinded)
|
||||
this.addEventListener('dragend', this._handleDragEndBinded)
|
||||
this.addEventListener('drop', this._handleDropBinded)
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
super.disconnectedCallback()
|
||||
this.currentDraggedTask = null
|
||||
this.removeEventListener('dragstart', this._handleDragStartBinded)
|
||||
this.removeEventListener('dragover', this._handleDragOverBinded)
|
||||
this.removeEventListener('dragleave', this._handleDragLeaveBinded)
|
||||
this.removeEventListener('dragend', this._handleDragEndBinded)
|
||||
this.removeEventListener('drop', this._handleDropBinded)
|
||||
}
|
||||
|
||||
async submitCreateTaskList (ev) {
|
||||
ev.preventDefault()
|
||||
|
||||
@ -96,15 +73,7 @@ export default class MUCTaskListsView extends CustomElement {
|
||||
}
|
||||
}
|
||||
|
||||
_getParentTaskEl (target) {
|
||||
return target.closest?.('livechat-converse-muc-task')
|
||||
}
|
||||
|
||||
_getParentTaskOrTaskListEl (target) {
|
||||
return target.closest?.('livechat-converse-muc-task, livechat-converse-muc-task-list')
|
||||
}
|
||||
|
||||
_isATaskEl (target) {
|
||||
isATaskEl (target) {
|
||||
return target.nodeName?.toLowerCase() === 'livechat-converse-muc-task'
|
||||
}
|
||||
|
||||
@ -112,71 +81,18 @@ export default class MUCTaskListsView extends CustomElement {
|
||||
return target.nodeName?.toLowerCase() === 'livechat-converse-muc-task-list'
|
||||
}
|
||||
|
||||
_isOnTopHalf (ev, taskEl) {
|
||||
const y = ev.clientY
|
||||
const bounding = taskEl.getBoundingClientRect()
|
||||
return (y <= bounding.y + (bounding.height / 2))
|
||||
}
|
||||
|
||||
_resetDropOver () {
|
||||
document.querySelectorAll('.livechat-drag-bottom-half, .livechat-drag-top-half').forEach(
|
||||
el => el.classList.remove('livechat-drag-bottom-half', 'livechat-drag-top-half')
|
||||
)
|
||||
}
|
||||
|
||||
_handleDragStart (ev) {
|
||||
// The draggable=true is on a livechat-converse-muc-task child
|
||||
const possibleTaskEl = ev.target.parentElement
|
||||
if (!this._isATaskEl(possibleTaskEl)) { return }
|
||||
console.log('[livechat task drag&drop] Starting to drag a task...')
|
||||
this.currentDraggedTask = possibleTaskEl
|
||||
this._resetDropOver()
|
||||
}
|
||||
|
||||
_handleDragOver (ev) {
|
||||
if (!this.currentDraggedTask) { return }
|
||||
const taskOrTaskListEl = this._getParentTaskOrTaskListEl(ev.target)
|
||||
if (!taskOrTaskListEl) { return }
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/drop_event says we should preventDefault
|
||||
ev.preventDefault()
|
||||
|
||||
// Are we on the top or bottom part of the taskEl?
|
||||
// Note: for task list, we always add the task in the task list, so no need to test here.
|
||||
const topHalf = this._isATaskEl(taskOrTaskListEl) ? this._isOnTopHalf(ev, taskOrTaskListEl) : false
|
||||
taskOrTaskListEl.classList.add(topHalf ? 'livechat-drag-top-half' : 'livechat-drag-bottom-half')
|
||||
taskOrTaskListEl.classList.remove(topHalf ? 'livechat-drag-bottom-half' : 'livechat-drag-top-half')
|
||||
}
|
||||
|
||||
_handleDragLeave (ev) {
|
||||
if (!this.currentDraggedTask) { return }
|
||||
const taskOrTaskListEl = this._getParentTaskOrTaskListEl(ev.target)
|
||||
if (!taskOrTaskListEl) { return }
|
||||
taskOrTaskListEl.classList.remove('livechat-drag-bottom-half', 'livechat-drag-top-half')
|
||||
}
|
||||
|
||||
_handleDragEnd (_ev) {
|
||||
this.currentDraggedTask = null
|
||||
this._resetDropOver()
|
||||
}
|
||||
|
||||
_handleDrop (_ev) {
|
||||
if (!this.currentDraggedTask) { return }
|
||||
|
||||
const droppedOnEl = document.querySelector('.livechat-drag-bottom-half, .livechat-drag-top-half')
|
||||
const droppedOntaskOrTaskListEl = this._getParentTaskOrTaskListEl(droppedOnEl)
|
||||
if (!droppedOntaskOrTaskListEl) { return }
|
||||
|
||||
_dropDone (draggedEl, droppedOnEl, onTopHalf) {
|
||||
super._dropDone(...arguments)
|
||||
console.log('[livechat task drag&drop] Task dropped...')
|
||||
|
||||
const task = this.currentDraggedTask.model
|
||||
const task = draggedEl.model
|
||||
|
||||
let newOrder, targetTasklist
|
||||
if (this.isATaskListEl(droppedOntaskOrTaskListEl)) {
|
||||
if (this.isATaskListEl(droppedOnEl)) {
|
||||
// We dropped on a task list, we must add as first entry.
|
||||
newOrder = 0
|
||||
|
||||
targetTasklist = droppedOntaskOrTaskListEl.model
|
||||
targetTasklist = droppedOnEl.model
|
||||
if (task.get('list') !== targetTasklist.get('id')) {
|
||||
console.log('[livechat task drag&drop] Changing task list...')
|
||||
task.set('list', targetTasklist.get('id'))
|
||||
@ -185,9 +101,9 @@ export default class MUCTaskListsView extends CustomElement {
|
||||
console.log('[livechat task drag&drop] Task dropped on tasklist, but already first item, nothing to do')
|
||||
return
|
||||
}
|
||||
} else if (this._isATaskEl(droppedOntaskOrTaskListEl)) {
|
||||
} else if (this.isATaskEl(droppedOnEl)) {
|
||||
// We dropped on a task, we must get its order (+1 if !onTopHalf)
|
||||
const droppedOnTask = droppedOntaskOrTaskListEl.model
|
||||
const droppedOnTask = droppedOnEl.model
|
||||
if (task === droppedOnTask) {
|
||||
// But of course, if dropped on itself there is nothing to do.
|
||||
console.log('[livechat task drag&drop] Task dropped on itself, nothing to do')
|
||||
@ -199,9 +115,8 @@ export default class MUCTaskListsView extends CustomElement {
|
||||
task.set('list', droppedOnTask.get('list'))
|
||||
}
|
||||
|
||||
const topHalf = droppedOnEl.classList.contains('livechat-drag-top-half')
|
||||
newOrder = droppedOnTask.get('order') ?? 0
|
||||
if (!topHalf) { newOrder = Math.max(0, newOrder + 1) }
|
||||
if (!onTopHalf) { newOrder = Math.max(0, newOrder + 1) }
|
||||
|
||||
if (typeof newOrder !== 'number' || isNaN(newOrder)) {
|
||||
console.error(
|
||||
@ -217,45 +132,7 @@ export default class MUCTaskListsView extends CustomElement {
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof newOrder !== 'number' || isNaN(newOrder)) {
|
||||
console.error('[livechat task drag&drop] Computed new order is not a number, aborting.')
|
||||
return
|
||||
}
|
||||
console.log('[livechat task drag&drop] Task new order will be ' + newOrder)
|
||||
|
||||
console.log('[livechat task drag&drop] Reordering tasks...')
|
||||
let currentOrder = newOrder + 1
|
||||
for (const t of targetTasklist.getTasks()) {
|
||||
if (t === task) {
|
||||
console.log('[livechat task drag&drop] Skipping the currently moved task')
|
||||
continue
|
||||
}
|
||||
|
||||
let order = t.get('order') ?? 0
|
||||
if (typeof order !== 'number' || isNaN(order)) {
|
||||
console.error('[livechat task drag&drop] Found a task with an invalid order, fixing it.')
|
||||
order = currentOrder // this will cause the code bellow to increment task order
|
||||
}
|
||||
if (order < newOrder) { continue }
|
||||
|
||||
currentOrder++
|
||||
if (order > currentOrder) {
|
||||
console.log(
|
||||
`Task "${t.get('name')}" as already on order greater than ${currentOrder.toString()}, stoping.`
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
console.log(`Changing order of task "${t.get('name')}" to ${currentOrder}`)
|
||||
t.set('order', currentOrder)
|
||||
t.saveItem() // TODO: handle errors?
|
||||
}
|
||||
|
||||
console.log('[livechat task drag&drop] Setting new order on the moved task')
|
||||
task.set('order', newOrder)
|
||||
task.saveItem() // TODO: handle errors?
|
||||
|
||||
this._resetDropOver()
|
||||
this._saveOrders(targetTasklist.getTasks(), task, newOrder)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 { tplMucTask } from '../templates/muc-task'
|
||||
import { __ } from 'i18n'
|
||||
|
||||
|
@ -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 { ChatRoomTaskLists } from './task-lists.js'
|
||||
import { ChatRoomTaskList } from './task-list.js'
|
||||
import { ChatRoomTasks } from './tasks.js'
|
||||
@ -18,9 +18,14 @@ converse.plugins.add('livechat-converse-tasks', {
|
||||
dependencies: ['converse-muc', 'converse-disco', 'converse-pubsub'],
|
||||
|
||||
initialize () {
|
||||
_converse.ChatRoomTaskLists = ChatRoomTaskLists
|
||||
_converse.ChatRoomTaskList = ChatRoomTaskList
|
||||
_converse.ChatRoomTasks = ChatRoomTasks
|
||||
Object.assign(
|
||||
_converse.exports,
|
||||
{
|
||||
ChatRoomTaskLists,
|
||||
ChatRoomTaskList,
|
||||
ChatRoomTasks
|
||||
}
|
||||
)
|
||||
|
||||
_converse.api.settings.extend({
|
||||
livechat_task_app_enabled: false,
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
import BaseModal from 'plugins/modal/modal.js'
|
||||
import tplPickTaskList from './templates/pick-task-list.js'
|
||||
import { api } from '@converse/headless/core'
|
||||
import { api } from '@converse/headless'
|
||||
import { __ } from 'i18n'
|
||||
|
||||
export default class PickTaskListModal extends BaseModal {
|
||||
|
@ -19,22 +19,22 @@ export default function (el) {
|
||||
|
||||
return html`
|
||||
<form class="converse-form converse-form--modal confirm" action="#" @submit=${ev => el.onPick(ev)}>
|
||||
<div class="form-group">
|
||||
<select class="form-control" name="tasklist">
|
||||
${
|
||||
repeat(muc.tasklists, (tasklist) => tasklist.get('id'), (tasklist) => {
|
||||
return html`<option value="${tasklist.get('id')}">${tasklist.get('name')}</option>`
|
||||
})
|
||||
}
|
||||
</select>
|
||||
<small class="form-text text-muted">
|
||||
${i18nMessage}
|
||||
</small>
|
||||
</div>
|
||||
<fieldset>
|
||||
<select class="form-control" name="tasklist">
|
||||
${
|
||||
repeat(muc.tasklists, (tasklist) => tasklist.get('id'), (tasklist) => {
|
||||
return html`<option value="${tasklist.get('id')}">${tasklist.get('name')}</option>`
|
||||
})
|
||||
}
|
||||
</select>
|
||||
<small class="form-text text-muted">
|
||||
${i18nMessage}
|
||||
</small>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">${__('OK')}</button>
|
||||
<input type="button" class="btn btn-secondary" data-dismiss="modal" value="${__('Cancel')}"/>
|
||||
</div>
|
||||
<fieldset>
|
||||
<button type="submit" class="btn btn-primary">${__('OK')}</button>
|
||||
<input type="button" class="btn btn-secondary" data-dismiss="modal" value="${__('Cancel')}"/>
|
||||
</fieldset>
|
||||
</form>`
|
||||
}
|
||||
|
@ -1,27 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
.conversejs {
|
||||
livechat-converse-muc-task {
|
||||
&.livechat-drag-bottom-half .task-line {
|
||||
border-bottom: 4px solid blue;
|
||||
}
|
||||
|
||||
&.livechat-drag-top-half .task-line {
|
||||
border-top: 4px solid blue;
|
||||
}
|
||||
}
|
||||
|
||||
livechat-converse-muc-task-list {
|
||||
&.livechat-drag-bottom-half .task-list-line {
|
||||
border-bottom: 4px solid blue;
|
||||
}
|
||||
|
||||
&.livechat-drag-top-half .task-list-line {
|
||||
border-top: 4px solid blue;
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ import { Model } from '@converse/skeletor/src/model.js'
|
||||
/**
|
||||
* A chat room task list.
|
||||
* @class
|
||||
* @namespace _converse.ChatRoomTaskList
|
||||
* @namespace _converse.exports.ChatRoomTaskList
|
||||
* @memberof _converse
|
||||
*/
|
||||
class ChatRoomTaskList extends Model {
|
||||
@ -40,7 +40,7 @@ class ChatRoomTaskList extends Model {
|
||||
|
||||
data.list = this.get('id')
|
||||
if (!data.order) {
|
||||
data.order = 0 + Math.max(
|
||||
data.order = 1 + Math.max(
|
||||
0,
|
||||
...(this.getTasks().map(t => t.get('order') ?? 0).filter(o => !isNaN(o)))
|
||||
)
|
||||
|
@ -7,9 +7,9 @@ import { ChatRoomTaskList } from './task-list'
|
||||
import { initStorage } from '@converse/headless/utils/storage.js'
|
||||
|
||||
/**
|
||||
* A list of {@link _converse.ChatRoomTaskList} instances, representing task lists associated to a MUC.
|
||||
* A list of {@link _converse.exports.ChatRoomTaskList} instances, representing task lists associated to a MUC.
|
||||
* @class
|
||||
* @namespace _converse.ChatRoomTaskLists
|
||||
* @namespace _converse.exports.ChatRoomTaskLists
|
||||
* @memberOf _converse
|
||||
*/
|
||||
class ChatRoomTaskLists extends Collection {
|
||||
|
@ -7,7 +7,7 @@ import { Model } from '@converse/skeletor/src/model.js'
|
||||
/**
|
||||
* A chat room task.
|
||||
* @class
|
||||
* @namespace _converse.ChatRoomTask
|
||||
* @namespace _converse.exports.ChatRoomTask
|
||||
* @memberof _converse
|
||||
*/
|
||||
class ChatRoomTask extends Model {
|
||||
|
@ -7,9 +7,9 @@ import { ChatRoomTask } from './task'
|
||||
import { initStorage } from '@converse/headless/utils/storage.js'
|
||||
|
||||
/**
|
||||
* A list of {@link _converse.ChatRoomTask} instances, representing all tasks associated to a MUC.
|
||||
* A list of {@link _converse.exports.ChatRoomTask} instances, representing all tasks associated to a MUC.
|
||||
* @class
|
||||
* @namespace _converse.ChatRoomTasks
|
||||
* @namespace _converse.exports.ChatRoomTasks
|
||||
* @memberOf _converse
|
||||
*/
|
||||
class ChatRoomTasks extends Collection {
|
||||
|
@ -3,28 +3,24 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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 tplMUCTaskApp (el, mucModel) {
|
||||
if (!mucModel) {
|
||||
// should not happen
|
||||
el.classList.add('hidden') // we must do this, otherwise will have CSS side effects
|
||||
return html``
|
||||
}
|
||||
if (!mucModel.tasklists) {
|
||||
// too soon, not initialized yet (this will happen)
|
||||
el.classList.add('hidden') // we must do this, otherwise will have CSS side effects
|
||||
return html``
|
||||
}
|
||||
|
||||
if (!el.show) {
|
||||
el.classList.add('hidden')
|
||||
return html``
|
||||
}
|
||||
|
||||
el.classList.remove('hidden')
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const i18nTasks = __(LOC_tasks)
|
||||
// eslint-disable-next-line no-undef
|
||||
@ -33,19 +29,11 @@ export function tplMUCTaskApp (el, mucModel) {
|
||||
page: 'documentation/user/streamers/tasks'
|
||||
})
|
||||
|
||||
return html`
|
||||
<div class="livechat-converse-muc-app-header">
|
||||
<h5>${i18nTasks}</h5>
|
||||
<a href="${helpUrl}" target="_blank"><converse-icon
|
||||
class="fa fa-circle-question"
|
||||
size="1em"
|
||||
title="${i18nHelp}"
|
||||
></converse-icon></a>
|
||||
<button class="livechat-converse-muc-app-close" @click=${el.toggleApp} title="${__('Close')}">
|
||||
<converse-icon class="fa fa-times" size="1em"></converse-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="livechat-converse-muc-app-body">
|
||||
<livechat-converse-muc-task-lists .model=${mucModel.tasklists}></livechat-converse-muc-task-lists>
|
||||
</div>`
|
||||
return tplMUCApp(
|
||||
el,
|
||||
i18nTasks,
|
||||
helpUrl,
|
||||
i18nHelp,
|
||||
html`<livechat-converse-muc-task-lists .model=${mucModel.tasklists}></livechat-converse-muc-task-lists>`
|
||||
)
|
||||
}
|
||||
|
@ -16,17 +16,17 @@ export default function tplMucTaskList (el, tasklist) {
|
||||
// eslint-disable-next-line no-undef
|
||||
const i18nTaskListName = __(LOC_task_list_name)
|
||||
return html`
|
||||
<div class="task-list-line">
|
||||
<div class="task-list-line draggables-line">
|
||||
${el.collapsed
|
||||
? html`
|
||||
<button @click=${el.toggleTasks} class="task-list-toggle-tasks">
|
||||
<button type="button" @click=${el.toggleTasks} class="task-list-toggle-tasks">
|
||||
<converse-icon
|
||||
color="var(--muc-toolbar-btn-color)"
|
||||
class="fa fa-angle-right"
|
||||
size="1em"></converse-icon>
|
||||
</button>`
|
||||
: html`
|
||||
<button @click=${el.toggleTasks} class="task-list-toggle-tasks">
|
||||
<button type="button" @click=${el.toggleTasks} class="task-list-toggle-tasks">
|
||||
<converse-icon
|
||||
color="var(--muc-toolbar-btn-color)"
|
||||
class="fa fa-angle-down"
|
||||
@ -38,15 +38,15 @@ export default function tplMucTaskList (el, tasklist) {
|
||||
<div class="task-list-name">
|
||||
<a @click=${el.toggleTasks}>${tasklist.get('name')}</a>
|
||||
</div>
|
||||
<button class="task-list-action" title="${i18nCreateTask}" @click=${el.openAddTaskForm}>
|
||||
<button type="button" class="task-list-action" title="${i18nCreateTask}" @click=${el.openAddTaskForm}>
|
||||
<converse-icon class="fa fa-plus" size="1em"></converse-icon>
|
||||
</button>
|
||||
<button class="task-list-action" title="${__('Edit')}"
|
||||
<button type="button" class="task-list-action" title="${__('Edit')}"
|
||||
@click=${el.toggleEdit}
|
||||
>
|
||||
<converse-icon class="fa fa-edit" size="1em"></converse-icon>
|
||||
</button>
|
||||
<button class="task-list-action" title="${i18nDelete}"
|
||||
<button type="button" class="task-list-action" title="${i18nDelete}"
|
||||
@click=${el.deleteTaskList}
|
||||
>
|
||||
<converse-icon class="fa fa-trash-alt" size="1em"></converse-icon>
|
||||
|
@ -24,7 +24,7 @@ export default function tplMucTaskLists (el, tasklists) {
|
||||
})
|
||||
}
|
||||
<form class="converse-form" @submit=${el.submitCreateTaskList}>
|
||||
<div class="form-group">
|
||||
<fieldset>
|
||||
<label>
|
||||
${i18nCreateTaskList}
|
||||
<input type="text" value="" class="form-control" name="name" placeholder="${i18nTaskListName}" />
|
||||
@ -34,6 +34,6 @@ export default function tplMucTaskLists (el, tasklists) {
|
||||
? ''
|
||||
: html`<div class="invalid-feedback d-block">${el.create_tasklist_error_message}</div>`
|
||||
}
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>`
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ export function tplMucTask (el, task) {
|
||||
const doneId = 'livechat-task-done-id-' + task.get('id')
|
||||
return !el.edit
|
||||
? html`
|
||||
<div draggable="true" class="task-line" ?task-is-done=${done}>
|
||||
<div draggable="true" class="task-line draggables-line" ?task-is-done=${done}>
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="${doneId}"
|
||||
@ -30,22 +30,22 @@ export function tplMucTask (el, task) {
|
||||
</label>
|
||||
</div>
|
||||
<div class="task-description">${task.get('description') ?? ''}</div>
|
||||
<button class="task-action" title="${__('Edit')}"
|
||||
<button type="button" class="task-action" title="${__('Edit')}"
|
||||
@click=${el.toggleEdit}
|
||||
>
|
||||
<converse-icon class="fa fa-edit" size="1em"></converse-icon>
|
||||
</button>
|
||||
<button class="task-action" title="${i18nDelete}"
|
||||
<button type="button" class="task-action" title="${i18nDelete}"
|
||||
@click=${el.deleteTask}
|
||||
>
|
||||
<converse-icon class="fa fa-trash-alt" size="1em"></converse-icon>
|
||||
</button>
|
||||
</div>`
|
||||
: html`
|
||||
<div class="task-line">
|
||||
<div class="task-line draggables-line">
|
||||
<form class="converse-form" @submit=${el.saveTask}>
|
||||
${_tplTaskForm(task)}
|
||||
<fieldset class="form-group">
|
||||
<fieldset>
|
||||
<input type="submit" class="btn btn-primary" value="${__('Ok')}" />
|
||||
<input type="button" class="btn btn-secondary button-cancel"
|
||||
value="${__('Cancel')}" @click=${el.toggleEdit}
|
||||
@ -61,7 +61,7 @@ function _tplTaskForm (task) {
|
||||
// eslint-disable-next-line no-undef
|
||||
const i18nTaskDesc = __(LOC_task_description)
|
||||
|
||||
return html`<fieldset class="form-group">
|
||||
return html`<fieldset>
|
||||
<input type="text" name="name"
|
||||
class="form-control" value="${task ? task.get('name') : ''}"
|
||||
placeholder="${i18nTaskName}"
|
||||
@ -80,7 +80,7 @@ export function tplMucAddTaskForm (tasklistEl, _tasklist) {
|
||||
return html`
|
||||
<form class="task-list-add-task converse-form" @submit=${tasklistEl.submitAddTask}>
|
||||
${_tplTaskForm(undefined)}
|
||||
<fieldset class="form-group">
|
||||
<fieldset>
|
||||
<input type="submit" class="btn btn-primary" value="${i18nOk}" />
|
||||
<input type="button" class="btn btn-secondary button-cancel"
|
||||
value="${i18nCancel}" @click=${tasklistEl.closeAddTaskForm}
|
||||
|
@ -4,12 +4,12 @@
|
||||
|
||||
import { XMLNS_TASKLIST, XMLNS_TASK } from './constants.js'
|
||||
import { PubSubManager } from '../../shared/lib/pubsub-manager.js'
|
||||
import { converse, _converse, api } from '../../../src/headless/core.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.CHATROOMS_TYPE) {
|
||||
if (muc.get('type') !== _converse.constants.CHATROOMS_TYPE) {
|
||||
// only on MUC.
|
||||
return buttons
|
||||
}
|
||||
@ -74,8 +74,8 @@ function _initChatRoomTaskLists (mucModel) {
|
||||
return
|
||||
}
|
||||
|
||||
mucModel.tasklists = new _converse.ChatRoomTaskLists(undefined, { chatroom: mucModel })
|
||||
mucModel.tasks = new _converse.ChatRoomTasks(undefined, { chatroom: mucModel })
|
||||
mucModel.tasklists = new _converse.exports.ChatRoomTaskLists(undefined, { chatroom: mucModel })
|
||||
mucModel.tasks = new _converse.exports.ChatRoomTasks(undefined, { chatroom: mucModel })
|
||||
|
||||
mucModel.taskManager = new PubSubManager(
|
||||
mucModel.get('jid'),
|
||||
@ -127,7 +127,7 @@ function _destroyChatRoomTaskLists (mucModel) {
|
||||
}
|
||||
|
||||
export function initOrDestroyChatRoomTaskLists (mucModel) {
|
||||
if (mucModel.get('type') !== _converse.CHATROOMS_TYPE) {
|
||||
if (mucModel.get('type') !== _converse.constants.CHATROOMS_TYPE) {
|
||||
// only on MUC.
|
||||
return _destroyChatRoomTaskLists(mucModel)
|
||||
}
|
||||
|
@ -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 { html } from 'lit'
|
||||
import { __ } from 'i18n'
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { converse, api } from '../../../src/headless/core.js'
|
||||
import { converse, api } from '../../../src/headless/index.js'
|
||||
import './components/muc-terms.js'
|
||||
|
||||
const { sizzle } = converse.env
|
||||
|
193
conversejs/custom/shared/components/draggables/index.js
Normal file
193
conversejs/custom/shared/components/draggables/index.js
Normal file
@ -0,0 +1,193 @@
|
||||
// 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 './styles/draggables.scss'
|
||||
|
||||
/**
|
||||
* This is the base class for custom elements that contains draggable items.
|
||||
*/
|
||||
export class DraggablesCustomElement extends CustomElement {
|
||||
currentDragged = null
|
||||
|
||||
/**
|
||||
* The tag name for draggable elements.
|
||||
* Example: livechat-converse-muc-note.
|
||||
* Must be set in derived class.
|
||||
*/
|
||||
draggableTagName = 'invalid-tag-name'
|
||||
|
||||
/**
|
||||
* The tag names on which we can drop the element.
|
||||
* Examples: livechat-converse-muc-note, livechat-converse-muc-task, livechat-converse-muc-task-list.
|
||||
* Must be set in derived class.
|
||||
*/
|
||||
droppableTagNames = []
|
||||
|
||||
/**
|
||||
* Tag names for which we will always drop to bottom (for example: task lists)
|
||||
*/
|
||||
droppableAlwaysBottomTagNames = []
|
||||
|
||||
initialize () {
|
||||
this._handleDragStartBinded = this._handleDragStart.bind(this)
|
||||
this._handleDragOverBinded = this._handleDragOver.bind(this)
|
||||
this._handleDragLeaveBinded = this._handleDragLeave.bind(this)
|
||||
this._handleDragEndBinded = this._handleDragEnd.bind(this)
|
||||
this._handleDropBinded = this._handleDrop.bind(this)
|
||||
|
||||
return super.initialize()
|
||||
}
|
||||
|
||||
connectedCallback () {
|
||||
super.connectedCallback()
|
||||
this.currentDragged = null
|
||||
this.addEventListener('dragstart', this._handleDragStartBinded)
|
||||
this.addEventListener('dragover', this._handleDragOverBinded)
|
||||
this.addEventListener('dragleave', this._handleDragLeaveBinded)
|
||||
this.addEventListener('dragend', this._handleDragEndBinded)
|
||||
this.addEventListener('drop', this._handleDropBinded)
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
super.disconnectedCallback()
|
||||
this.currentDragged = null
|
||||
this.removeEventListener('dragstart', this._handleDragStartBinded)
|
||||
this.removeEventListener('dragover', this._handleDragOverBinded)
|
||||
this.removeEventListener('dragleave', this._handleDragLeaveBinded)
|
||||
this.removeEventListener('dragend', this._handleDragEndBinded)
|
||||
this.removeEventListener('drop', this._handleDropBinded)
|
||||
}
|
||||
|
||||
_isADraggableEl (target) {
|
||||
return target.nodeName?.toLowerCase() === this.draggableTagName
|
||||
}
|
||||
|
||||
_getParentDroppableEl (target) {
|
||||
return target.closest?.(this.droppableTagNames.join(','))
|
||||
}
|
||||
|
||||
_isOnTopHalf (ev, el) {
|
||||
const y = ev.clientY
|
||||
const bounding = el.getBoundingClientRect()
|
||||
return (y <= bounding.y + (bounding.height / 2))
|
||||
}
|
||||
|
||||
_resetDropOver () {
|
||||
document.querySelectorAll('.livechat-drag-bottom-half, .livechat-drag-top-half').forEach(
|
||||
el => el.classList.remove('livechat-drag-bottom-half', 'livechat-drag-top-half')
|
||||
)
|
||||
}
|
||||
|
||||
_handleDragStart (ev) {
|
||||
// The draggable=true is on a child bode
|
||||
const possibleEl = ev.target.parentElement
|
||||
if (!this._isADraggableEl(possibleEl)) { return }
|
||||
console.log('[livechat drag&drop] Starting to drag a ' + this.draggableTagName + '...')
|
||||
this.currentDragged = possibleEl
|
||||
this._resetDropOver()
|
||||
}
|
||||
|
||||
_handleDragOver (ev) {
|
||||
if (!this.currentDragged) { return }
|
||||
const droppableEl = this._getParentDroppableEl(ev.target)
|
||||
if (!droppableEl) { return }
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/drop_event says we should preventDefault
|
||||
ev.preventDefault()
|
||||
|
||||
// Are we on the top or bottom part of the droppableEl?
|
||||
let topHalf = false
|
||||
if (!this.droppableAlwaysBottomTagNames.includes(droppableEl.nodeName.toLowerCase())) {
|
||||
topHalf = this._isOnTopHalf(ev, droppableEl)
|
||||
}
|
||||
droppableEl.classList.add(topHalf ? 'livechat-drag-top-half' : 'livechat-drag-bottom-half')
|
||||
droppableEl.classList.remove(topHalf ? 'livechat-drag-bottom-half' : 'livechat-drag-top-half')
|
||||
}
|
||||
|
||||
_handleDragLeave (ev) {
|
||||
if (!this.currentDragged) { return }
|
||||
const el = this._getParentDroppableEl(ev.target)
|
||||
if (!el) { return }
|
||||
el.classList.remove('livechat-drag-bottom-half', 'livechat-drag-top-half')
|
||||
}
|
||||
|
||||
_handleDragEnd (_ev) {
|
||||
this.currentDragged = null
|
||||
this._resetDropOver()
|
||||
}
|
||||
|
||||
_handleDrop (_ev) {
|
||||
if (!this.currentDragged) { return }
|
||||
|
||||
let droppedOnEl = document.querySelector('.livechat-drag-bottom-half, .livechat-drag-top-half')
|
||||
droppedOnEl = this._getParentDroppableEl(droppedOnEl)
|
||||
if (!droppedOnEl) { return }
|
||||
|
||||
console.log('[livechat drag&drop] ' + this.draggableTagName + ' dropped...')
|
||||
|
||||
try {
|
||||
this._dropDone(this.currentDragged, droppedOnEl, droppedOnEl.classList.contains('livechat-drag-top-half'))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
this._resetDropOver()
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback when a valid drop occurs.
|
||||
* Must be overloaded.
|
||||
*/
|
||||
_dropDone (draggedEl, droppedOnEl, onTopHalf) {
|
||||
console.debug('[livechat drag&drop] Drop done:', draggedEl, droppedOnEl, onTopHalf)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method can be called from _dropDone to save the new objects orders.
|
||||
* For it to work, models must respect following constraints:
|
||||
* * be a Model
|
||||
* * have the order attribute
|
||||
* * have an id attribute (for logging)
|
||||
* * have get, set and saveItem methods
|
||||
*/
|
||||
_saveOrders (models, currentModel, newOrder) {
|
||||
if (typeof newOrder !== 'number' || isNaN(newOrder)) {
|
||||
console.error('[livechat drag&drop] Computed new order is not a number, aborting.')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[livechat drag&drop] Reordering models... Model new order will be ' + newOrder)
|
||||
let currentOrder = newOrder + 1
|
||||
for (const m of models) {
|
||||
if (m === currentModel) {
|
||||
console.log('[livechat drag&drop] Skipping the currently moved model')
|
||||
continue
|
||||
}
|
||||
|
||||
let order = m.get('order') ?? 0
|
||||
if (typeof order !== 'number' || isNaN(order)) {
|
||||
console.error('[livechat drag&drop] Found a model with an invalid order, fixing it.')
|
||||
order = currentOrder // this will cause the code bellow to increment model order
|
||||
}
|
||||
if (order < newOrder) { continue }
|
||||
|
||||
currentOrder++
|
||||
if (order > currentOrder) {
|
||||
console.log(
|
||||
`Object "${m.get('id')}" as already on order greater than ${currentOrder.toString()}, stoping.`
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
console.log(`Changing order of model "${m.get('id')}" to ${currentOrder}`)
|
||||
m.set('order', currentOrder)
|
||||
m.saveItem() // TODO: handle errors?
|
||||
}
|
||||
|
||||
console.log('[livechat drag&drop] Setting new order on the moved model')
|
||||
currentModel.set('order', newOrder)
|
||||
currentModel.saveItem() // TODO: handle errors?
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
.conversejs {
|
||||
// FIXME: the use of ">" only works if the draggables-lines is a direct
|
||||
// child of the element.
|
||||
// We should find a better way to do this (and that will not break for nested
|
||||
// elements, like task in tast-list).
|
||||
.livechat-drag-bottom-half > .draggables-line {
|
||||
border-bottom: 4px solid blue;
|
||||
}
|
||||
|
||||
.livechat-drag-top-half > .draggables-line {
|
||||
border-top: 4px solid blue;
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
|
||||
/* eslint-disable max-len */
|
||||
import { html } from 'lit'
|
||||
import tplIcons from '../../../src/shared/templates/icons.js'
|
||||
import tplIcons from '../../../src/shared/components/templates/icons.js'
|
||||
|
||||
export default () => {
|
||||
// Here we are adding some additonal icons to ConverseJS defaults
|
||||
@ -28,6 +28,16 @@ export default () => {
|
||||
<symbol id="icon-square-poll-horizontal" viewBox="0 0 448 512">
|
||||
<path d="M448 96c0-35.3-28.7-64-64-64L64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-320zM256 160c0 17.7-14.3 32-32 32l-96 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l96 0c17.7 0 32 14.3 32 32zm64 64c17.7 0 32 14.3 32 32s-14.3 32-32 32l-192 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l192 0zM192 352c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l32 0c17.7 0 32 14.3 32 32z"/>
|
||||
</symbol>
|
||||
|
||||
<!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
||||
<symbol id="icon-note-sticky" viewBox="0 0 448 512">
|
||||
<path d="M64 80c-8.8 0-16 7.2-16 16l0 320c0 8.8 7.2 16 16 16l224 0 0-80c0-17.7 14.3-32 32-32l80 0 0-224c0-8.8-7.2-16-16-16L64 80zM288 480L64 480c-35.3 0-64-28.7-64-64L0 96C0 60.7 28.7 32 64 32l320 0c35.3 0 64 28.7 64 64l0 224 0 5.5c0 17-6.7 33.3-18.7 45.3l-90.5 90.5c-12 12-28.3 18.7-45.3 18.7l-5.5 0z"/>
|
||||
</symbol>
|
||||
|
||||
<!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
||||
<symbol id="icon-magnifying-glass" viewBox="0 0 512 512">
|
||||
<path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
`
|
||||
}
|
||||
|
95
conversejs/custom/shared/components/muc-app/index.js
Normal file
95
conversejs/custom/shared/components/muc-app/index.js
Normal file
@ -0,0 +1,95 @@
|
||||
// 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, _converse } from '@converse/headless'
|
||||
import './styles/muc-app.scss'
|
||||
|
||||
/**
|
||||
* Base class for MUC App custom elements (task app, notes app, ...).
|
||||
* This is an abstract class, should not be called directly.
|
||||
*/
|
||||
export class MUCApp extends CustomElement {
|
||||
restoreSettingName = undefined // must be overloaded
|
||||
sessionStorageRestoreKey = undefined // must be overloaded
|
||||
|
||||
static get properties () {
|
||||
return {
|
||||
model: { type: Object, attribute: true }, // mucModel
|
||||
show: { type: Boolean, attribute: false }
|
||||
}
|
||||
}
|
||||
|
||||
async initialize () {
|
||||
this.classList.add('livechat-converse-muc-app')
|
||||
this.show = this.restoreSettingName &&
|
||||
api.settings.get(this.restoreSettingName) &&
|
||||
this.sessionStorageRestoreKey &&
|
||||
(window.sessionStorage?.getItem?.(this.sessionStorageRestoreKey) === '1')
|
||||
|
||||
// we listen for livechatSizeChanged event,
|
||||
// and close all apps except the first if small or medium width.
|
||||
// Note: this will also be triggered when we first open the page
|
||||
this.listenTo(_converse, 'livechatSizeChanged', () => {
|
||||
if (!this.show || !api.livechat_size?.width_is(['small', 'medium'])) {
|
||||
return
|
||||
}
|
||||
// are we the first opened app?
|
||||
for (const el of document.querySelectorAll('.livechat-converse-muc-app')) {
|
||||
if (el === this) { break }
|
||||
if (!el.show) { continue }
|
||||
console.debug('The livechat size is small or medium, there is already an opened app, so closing myself', this)
|
||||
// ok, there is already an opened app.
|
||||
this.toggleApp() // we know we are open
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
render () { // must be overloaded.
|
||||
return ''
|
||||
}
|
||||
|
||||
updated () {
|
||||
if (this.innerText.trim() === '') {
|
||||
this.classList.add('hidden') // we must do this, otherwise will have CSS side effects
|
||||
} else {
|
||||
this.classList.remove('hidden')
|
||||
}
|
||||
|
||||
super.updated()
|
||||
}
|
||||
|
||||
toggleApp () {
|
||||
this.show = !this.show
|
||||
if (this.sessionStorageRestoreKey) {
|
||||
window.sessionStorage?.setItem?.(this.sessionStorageRestoreKey, this.show ? '1' : '')
|
||||
}
|
||||
|
||||
if (
|
||||
this.show &&
|
||||
api.livechat_size?.width_is(['small', 'medium'])
|
||||
) {
|
||||
// When showing an App, if the screen width is small or medium, we hide the others.
|
||||
this._closeOtherApps()
|
||||
}
|
||||
}
|
||||
|
||||
showApp () {
|
||||
if (!this.show) { return this.toggleApp() }
|
||||
}
|
||||
|
||||
hideApp () {
|
||||
if (this.show) { return this.toggleApp() }
|
||||
}
|
||||
|
||||
_closeOtherApps () {
|
||||
document.querySelectorAll('.livechat-converse-muc-app').forEach((el) => {
|
||||
if (el !== this && el.show) {
|
||||
console.debug('Closing another app, because livechat width is small or medium', el)
|
||||
el.toggleApp()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
.conversejs {
|
||||
livechat-converse-muc-task-app {
|
||||
.livechat-converse-muc-app {
|
||||
border: var(--occupants-border-left);
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
@ -42,8 +42,8 @@
|
||||
|
||||
&[livechat-converse-root-width="small"],
|
||||
&[livechat-converse-root-width="medium"] {
|
||||
converse-muc-chatarea livechat-converse-muc-task-app:not(.hidden) ~ * {
|
||||
// on small and medium width, we hide all subsequent siblings of the task app
|
||||
converse-muc-chatarea .livechat-converse-muc-app:not(.hidden) ~ * {
|
||||
// on small and medium width, we hide all subsequent siblings of the app
|
||||
// (when app is not hidden)
|
||||
display: none !important;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { html } from 'lit'
|
||||
import { __ } from 'i18n'
|
||||
|
||||
export function tplMUCApp (el, i18nTitle, helpUrl, i18nHelp, content) {
|
||||
return html`
|
||||
<div class="livechat-converse-muc-app-header">
|
||||
<h5>${i18nTitle}</h5>
|
||||
<a href="${helpUrl}" target="_blank"><converse-icon
|
||||
class="fa fa-circle-question"
|
||||
size="1em"
|
||||
title="${i18nHelp}"
|
||||
></converse-icon></a>
|
||||
<button type="button" class="livechat-converse-muc-app-close" @click=${el.toggleApp} title="${__('Close')}">
|
||||
<converse-icon class="fa fa-times" size="1em"></converse-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="livechat-converse-muc-app-body">
|
||||
${content}
|
||||
</div>`
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
//
|
||||
// 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'
|
||||
const { $build, Strophe, $iq, sizzle } = converse.env
|
||||
|
||||
/**
|
||||
@ -50,7 +50,7 @@ export class PubSubManager {
|
||||
async start () {
|
||||
// FIXME: handle errors. Find a way to display to user that this failed.
|
||||
|
||||
this.stanzaHandler = _converse.connection.addHandler(
|
||||
this.stanzaHandler = api.connection.get().addHandler(
|
||||
(message) => {
|
||||
try {
|
||||
this._handleMessage(message)
|
||||
@ -79,7 +79,7 @@ export class PubSubManager {
|
||||
// Note: no need to unsubscribe from the pubsub node, the backend will do when users leave the room.
|
||||
|
||||
if (this.stanzaHandler) {
|
||||
_converse.connection.deleteHandler(this.stanzaHandler)
|
||||
api.connection.get().deleteHandler(this.stanzaHandler)
|
||||
this.stanzaHandler = undefined
|
||||
}
|
||||
}
|
||||
@ -123,6 +123,7 @@ export class PubSubManager {
|
||||
if (v === undefined) { continue }
|
||||
data[field] = v
|
||||
}
|
||||
this._additionalModelToData(item, data)
|
||||
|
||||
console.log('Saving item...')
|
||||
await this._save(type, data, id)
|
||||
@ -178,6 +179,8 @@ export class PubSubManager {
|
||||
item.c(fieldName).t(data[fieldName]).up()
|
||||
}
|
||||
|
||||
this._additionalDataToItemNode(data, item)
|
||||
|
||||
await api.pubsub.publish(this.roomJID, this.node, item)
|
||||
}
|
||||
|
||||
@ -336,6 +339,7 @@ export class PubSubManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
this._additionalParseItemNode(itemNode, type, data)
|
||||
return data
|
||||
}
|
||||
|
||||
@ -351,4 +355,19 @@ export class PubSubManager {
|
||||
_typeFromCollection (collection) {
|
||||
return Object.values(this.types).find(type => type.collection === collection)
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload to add some custom code for model to data conversion.
|
||||
*/
|
||||
_additionalModelToData (_item, _data) {}
|
||||
|
||||
/**
|
||||
* Overload to add some custom code for data to stanza conversion.
|
||||
*/
|
||||
_additionalDataToItemNode (_data, _item) {}
|
||||
|
||||
/**
|
||||
* Overload to add some custom code item parsing.
|
||||
*/
|
||||
_additionalParseItemNode (_itemNode, _type, _data) {}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
import { __ } from 'i18n'
|
||||
import BaseModal from 'plugins/modal/modal.js'
|
||||
import { api } from '@converse/headless/core'
|
||||
import { api } from '@converse/headless'
|
||||
import { html } from 'lit'
|
||||
import 'livechat-external-login-content.js'
|
||||
|
||||
@ -20,8 +20,8 @@ class ExternalLoginModal extends BaseModal {
|
||||
return __(LOC_login_using_external_account)
|
||||
}
|
||||
|
||||
onHide () {
|
||||
super.onHide()
|
||||
close () {
|
||||
super.close()
|
||||
// kill the externalAuthGetResult handler if still there
|
||||
try {
|
||||
if (window.externalAuthGetResult) { window.externalAuthGetResult() }
|
||||
|
@ -8,7 +8,7 @@
|
||||
.dropdown-menu {
|
||||
// Fixing all dropdown colors
|
||||
--text-color: #212529; // default bootstrap color for dropdown-items
|
||||
--text-color-lighten-15-percent: #8c8c8c; // default ConverseJS theme color
|
||||
--inverse-link-color: #8c8c8c; // default ConverseJS theme color
|
||||
|
||||
background-color: #fff; // this is the default bootstrap color, used by ConverseJS
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
border: 1px dashed var(--peertube-menu-background);
|
||||
color: var(--peertube-main-foreground);
|
||||
background-color: var(--peertube-main-background);
|
||||
margin: 0 5px;
|
||||
|
||||
.livechat-hide-slow-mode-info-box {
|
||||
cursor: pointer;
|
||||
|
@ -34,12 +34,16 @@ body.converse-fullscreen.theme-peertube,
|
||||
body.converse-embedded converse-root.theme-peertube {
|
||||
--foreground: var(--peertube-main-foreground);
|
||||
--background: var(--peertube-main-background);
|
||||
--badge-color: var(--background);
|
||||
--button-hover-text-color: var(--background);
|
||||
--subdued-color: #a8aba1;
|
||||
--muc-color: var(--peertube-button-background);
|
||||
--green: #3aa569; // only in this file
|
||||
--redder-orange: #e77051; // only in this file
|
||||
--orange: #e7a151; // only in this file
|
||||
--light-blue: #578ea9; // only in this file
|
||||
--lighter-blue: #85b47b; // only in this file
|
||||
--chat-color: var(--green); // FIXME: copied from Converse. Is there side effects?
|
||||
--chat-status-online: var(--green);
|
||||
--chat-status-busy: var(--redder-orange);
|
||||
--chat-status-away: var(--orange);
|
||||
@ -55,7 +59,6 @@ body.converse-embedded converse-root.theme-peertube {
|
||||
--text-shadow-color: var(--peertube-main-background); // FIXME: should be a little different from background
|
||||
--text-color: var(--peertube-input-foreground);
|
||||
--controlbox-text-color: var(--peertube-input-foreground); // Note: controlbox is not used
|
||||
--text-color-lighten-15-percent: var(--peertube-input-foreground);
|
||||
--message-text-color: var(--peertube-input-foreground);
|
||||
--message-receipt-color: var(--green);
|
||||
--save-button-color: var(--green);
|
||||
@ -73,7 +76,6 @@ body.converse-embedded converse-root.theme-peertube {
|
||||
--chat-correcting-color: var(--peertube-grey-background);
|
||||
--chat-head-color-dark: #1e9652; // should not be used in this plugin
|
||||
--chat-head-color-darker: #0e763b; // should not be used in this plugin
|
||||
--chat-head-color-lighten-50-percent: #e7f7ee; // should not be used in this plugin
|
||||
--chat-head-color: var(--green);
|
||||
--chat-head-text-color: var(--peertube-input-foreground);
|
||||
--chat-toolbar-btn-color: var(--peertube-button-background);
|
||||
@ -106,7 +108,6 @@ body.converse-embedded converse-root.theme-peertube {
|
||||
--controlbox-pane-background-color: #333;
|
||||
--controlbox-pane-bg-hover-color: #464646;
|
||||
--panel-divider-color: #333;
|
||||
--chat-gutter: 0.5em;
|
||||
--minimized-chats-width: 130px;
|
||||
--mobile-chat-width: 100%;
|
||||
--mobile-chat-height: 400px;
|
||||
@ -119,9 +120,10 @@ body.converse-embedded converse-root.theme-peertube {
|
||||
--chatroom-badge-color: var(--peertube-button-background);
|
||||
--chatroom-badge-hover-color: var(--peertube-button-background);
|
||||
--chatroom-correcting-color: var(--peertube-grey-background);
|
||||
--chatroom-head-bg-color-dark: #d24e2b;
|
||||
--chatroom-head-bg-color-dark: var(--peertube-button-background);
|
||||
--chatroom-head-bg-color: var(--peertube-menu-background);
|
||||
--chatroom-head-border-bottom: 1px solid var(--peertube-grey-foreground);
|
||||
--chatroom-head-border-bottom: 0.15em solid var(--peertube-grey-foreground);
|
||||
--chatroom-head-fg-color: var(--subdued-color);
|
||||
--chatroom-head-button-color: #999;
|
||||
--chatroom-head-color: var(--peertube-menu-foreground);
|
||||
--chatroom-head-description-border-left: 1px solid #ddd;
|
||||
@ -163,6 +165,7 @@ body.converse-embedded converse-root.theme-peertube {
|
||||
--fullpage-chat-width: 100%;
|
||||
--fullpage-emoji-picker-height: 300px;
|
||||
--fullpage-max-chat-textarea-height: 15em;
|
||||
--overlayed-chat-gutter: 1em;
|
||||
--overlayed-chat-head-height: 55px;
|
||||
--overlayed-chat-height: 450px;
|
||||
--overlayed-chat-width: 300px;
|
||||
|
@ -60,7 +60,7 @@ body.livechat-readonly.livechat-noscroll {
|
||||
}
|
||||
}
|
||||
|
||||
// Viewer mode
|
||||
// Viewer mode (before the user has chosen its nickname)
|
||||
.livechat-viewer-mode-content {
|
||||
display: none;
|
||||
|
||||
@ -73,7 +73,7 @@ body.livechat-readonly.livechat-noscroll {
|
||||
gap: 0.5em 10px;
|
||||
align-items: baseline;
|
||||
|
||||
.form-group,
|
||||
fieldset,
|
||||
label {
|
||||
margin-bottom: 0 !important; // replaced by the gap on .livechat-viewer-mode-content
|
||||
}
|
||||
@ -171,7 +171,8 @@ body.converse-embedded {
|
||||
#peertube-plugin-livechat-container {
|
||||
converse-muc-message-form {
|
||||
// For an unknown reason, message field in truncated... so adding a bottom margin.
|
||||
margin-bottom: 6px;
|
||||
// We also add left and right margin, as Converse v11 adds a g-0 class on converse-muc-chatarea
|
||||
margin: 0 1px 6px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,4 +188,44 @@ body.converse-embedded {
|
||||
// So we must revert appearance:
|
||||
appearance: revert !important;
|
||||
}
|
||||
|
||||
.toolbar-buttons {
|
||||
// Converse v11 removed the toggle_occupant button on the right.
|
||||
// To add it back, we must ensure that this toolbar takes all the width, and
|
||||
// that the toggle-occupants button is on the right.
|
||||
flex-grow: 2;
|
||||
|
||||
.toggle-occupants {
|
||||
// Cancelling the flex-grow from btn-group
|
||||
flex-grow: 0 !important;
|
||||
|
||||
// This margin-left trick is to align the button on the right.
|
||||
margin-left: auto !important;
|
||||
order: 99;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
#conversejs { // here we use the id have gretter priority
|
||||
// These CSS are tricks: Converse v11 tries to hide the MUC when screen width is under 768px.
|
||||
// We don't want that, so we cancel the d-none.
|
||||
// FIXME: these hacks should be temporary, waiting for some improvement on Converse.
|
||||
converse-muc-chatarea {
|
||||
.chat-area.d-none {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
converse-muc-sidebar {
|
||||
// we must not use !important for flex, it would break resizing.
|
||||
// That's why we use #conversejs insteand of .conversejs for this block.
|
||||
flex: 0 0 min(400px, 50%);
|
||||
min-width: min(200px, 50%) !important;
|
||||
|
||||
.occupants {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { html } from 'lit'
|
||||
import { api } from '@converse/headless/core.js'
|
||||
import { api } from '@converse/headless/index.js'
|
||||
|
||||
export default () => html`
|
||||
<div class="inner-content converse-brand row">
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user