Merge branch 'main' of https://github.com/JohnXLivingston/peertube-plugin-livechat
This commit is contained in:
commit
7e0cfee8f1
2
.github/workflows/gh-pages.yml
vendored
2
.github/workflows/gh-pages.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
|||||||
- name: Setup Hugo
|
- name: Setup Hugo
|
||||||
uses: peaceiris/actions-hugo@v2
|
uses: peaceiris/actions-hugo@v2
|
||||||
with:
|
with:
|
||||||
hugo-version: '0.80.0'
|
hugo-version: '0.132.2'
|
||||||
extended: true
|
extended: true
|
||||||
|
|
||||||
- name: Generate documentation translations
|
- name: Generate documentation translations
|
||||||
|
@ -22,7 +22,7 @@ pages:
|
|||||||
image: registry.gitlab.com/pages/hugo/hugo_extended:latest
|
image: registry.gitlab.com/pages/hugo/hugo_extended:latest
|
||||||
variables:
|
variables:
|
||||||
GIT_SUBMODULE_STRATEGY: recursive
|
GIT_SUBMODULE_STRATEGY: recursive
|
||||||
GIT_SUBMODULE_PATHS: support/documentation/themes/hugo-theme-learn
|
GIT_SUBMODULE_PATHS: support/documentation/themes/hugo-theme-relearn
|
||||||
script:
|
script:
|
||||||
# gitlab need the generated documentation to be in the /public dir.
|
# gitlab need the generated documentation to be in the /public dir.
|
||||||
- hugo -s support/documentation/ --minify -d ../../public/ --baseURL='https://livingston.frama.io/peertube-plugin-livechat/'
|
- hugo -s support/documentation/ --minify -d ../../public/ --baseURL='https://livingston.frama.io/peertube-plugin-livechat/'
|
||||||
|
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -2,6 +2,6 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
[submodule "documentation/themes/hugo-theme-learn"]
|
[submodule "support/documentation/themes/hugo-theme-relearn"]
|
||||||
path = support/documentation/themes/hugo-theme-learn
|
path = support/documentation/themes/hugo-theme-relearn
|
||||||
url = https://github.com/matcornic/hugo-theme-learn.git
|
url = https://github.com/McShelby/hugo-theme-relearn.git
|
||||||
|
@ -32,3 +32,7 @@ License: AGPL-3.0-only
|
|||||||
Files: .github/PULL_REQUEST_TEMPLATE.md
|
Files: .github/PULL_REQUEST_TEMPLATE.md
|
||||||
Copyright: 2024 John Livingston <https://www.john-livingston.fr/>
|
Copyright: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
License: AGPL-3.0-only
|
License: AGPL-3.0-only
|
||||||
|
|
||||||
|
Files: prosody-modules/mod_firewall/*
|
||||||
|
Copyright: Prosody Community Modules <https://modules.prosody.im/mod_firewall>
|
||||||
|
License: MIT
|
||||||
|
37
CHANGELOG.md
37
CHANGELOG.md
@ -1,5 +1,42 @@
|
|||||||
# Changelog
|
# 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
|
## 10.3.3
|
||||||
|
|
||||||
### Minor changes and fixes
|
### Minor changes and fixes
|
||||||
|
94
assets/styles/admin/firewall/_firewall.scss
Normal file
94
assets/styles/admin/firewall/_firewall.scss
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* stylelint-disable custom-property-pattern */
|
||||||
|
|
||||||
|
@use "sass:color";
|
||||||
|
@use "../../variables";
|
||||||
|
|
||||||
|
.peertube-plugin-livechat-admin-firewall {
|
||||||
|
h1 {
|
||||||
|
padding-top: 40px;
|
||||||
|
|
||||||
|
/* See Peertube sub-menu-h1 mixin */
|
||||||
|
font-size: 1.3rem;
|
||||||
|
border-bottom: 2px solid var(--greyBackgroundColor);
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea[name^="_content_"] {
|
||||||
|
min-height: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"],
|
||||||
|
input[type="reset"],
|
||||||
|
button[type="submit"],
|
||||||
|
button[type="reset"] {
|
||||||
|
// Peertube rounded-line-height-1-5 mixins
|
||||||
|
line-height: variables.$button-calc-line-height;
|
||||||
|
|
||||||
|
// Peertube peertube-button mixin
|
||||||
|
padding: 4px 13px;
|
||||||
|
border: 0;
|
||||||
|
font-weight: variables.$font-semibold;
|
||||||
|
border-radius: 3px !important;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: variables.$button-font-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"],
|
||||||
|
button[type="submit"] {
|
||||||
|
// Peertube orange-button mixin
|
||||||
|
&,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--mainColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--mainHoverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled],
|
||||||
|
&.disabled {
|
||||||
|
cursor: default;
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--inputBorderColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="reset"],
|
||||||
|
button[type="reset"] {
|
||||||
|
// Peertube grey-button mixin
|
||||||
|
background-color: var(--greyBackgroundColor);
|
||||||
|
color: var(--greyForegroundColor);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&[disabled],
|
||||||
|
&.disabled {
|
||||||
|
color: var(--greyForegroundColor);
|
||||||
|
background-color: var(--greySecondaryBackgroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled],
|
||||||
|
&.disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.peertube-livechat-admin-firewall-col-name {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peertube-livechat-admin-firewall-col-content {
|
||||||
|
width: 65%;
|
||||||
|
}
|
||||||
|
}
|
@ -15,9 +15,9 @@ livechat-spinner,
|
|||||||
height: 48px;
|
height: 48px;
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
/* stylelint-disable-next-line custom-property-pattern */
|
/* stylelint-disable-next-line custom-property-pattern */
|
||||||
border: 5px solid var(--greyBackgroundColor);
|
border: 5px solid var(--greyBackgroundColor) !important; // !important is required for it to work in ConverseJS
|
||||||
/* stylelint-disable-next-line custom-property-pattern */
|
/* stylelint-disable-next-line custom-property-pattern */
|
||||||
border-bottom-color: var(--mainColor);
|
border-bottom-color: var(--mainColor) !important; // !important is required for it to work in ConverseJS
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -9,4 +9,5 @@
|
|||||||
@use "elements/index";
|
@use "elements/index";
|
||||||
@use "video";
|
@use "video";
|
||||||
@use "configuration/configuration";
|
@use "configuration/configuration";
|
||||||
|
@use "admin/firewall/firewall";
|
||||||
@use "list-rooms/list-rooms.scss";
|
@use "list-rooms/list-rooms.scss";
|
||||||
|
@ -18,17 +18,31 @@
|
|||||||
/* Note: livechat-viewer-mode-content (the form where anonymous users can
|
/* Note: livechat-viewer-mode-content (the form where anonymous users can
|
||||||
choose nickname or log in with external account), can be something like
|
choose nickname or log in with external account), can be something like
|
||||||
~180px height (at time of writing).
|
~180px height (at time of writing).
|
||||||
We must ensure that the 200px limit for converse-muc and converse-root is
|
We must ensure that the px height limit for converse-muc and converse-root is
|
||||||
always higher than livechat-viewer-mode-content max size.
|
always higher than livechat-viewer-mode-content max size.
|
||||||
|
Note: We also must ensure that when the user has choosen its nickname, and there is an
|
||||||
|
ongoing poll, the user can see the chat when the poll is folded.
|
||||||
*/
|
*/
|
||||||
#peertube-plugin-livechat-container converse-root {
|
#peertube-plugin-livechat-container converse-root {
|
||||||
display: block;
|
display: block;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
min-height: max(30vh, 200px); // Always at least 200px, and ideally at least 30% of viewport.
|
min-height: max(30vh, 300px); // Always at least 200px, and ideally at least 30% of viewport.
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-width: min(400px, 25vw);
|
||||||
|
|
||||||
converse-muc {
|
converse-muc {
|
||||||
min-height: max(59vh, 400px);
|
min-height: max(30vh, 300px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (orientation: portrait) and (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.
|
// Constants that begins with "LOC_" are loaded by build-client.js, reading the english locale file.
|
||||||
// See the online documentation: https://livingston.frama.io/peertube-plugin-livechat/contributing/translate/
|
// See the online documentation: https://livingston.frama.io/peertube-plugin-livechat/contributing/translate/
|
||||||
declare const LOC_ONLINE_HELP: string
|
declare const LOC_ONLINE_HELP: string
|
||||||
|
declare const LOC_CHAT: string
|
||||||
declare const LOC_OPEN_CHAT: string
|
declare const LOC_OPEN_CHAT: string
|
||||||
declare const LOC_OPEN_CHAT_NEW_WINDOW: string
|
declare const LOC_OPEN_CHAT_NEW_WINDOW: string
|
||||||
declare const LOC_CLOSE_CHAT: string
|
declare const LOC_CLOSE_CHAT: string
|
||||||
@ -133,3 +134,13 @@ declare const LOC_POLL_VOTE_OK: string
|
|||||||
|
|
||||||
declare const LOC_MODERATION_DELAY: string
|
declare const LOC_MODERATION_DELAY: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_MODERATION_DELAY_DESC: string
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_MODERATION_DELAY_DESC: string
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL: string
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_DESC: string
|
||||||
|
|
||||||
|
declare const LOC_PROSODY_FIREWALL_CONFIGURATION: string
|
||||||
|
declare const LOC_PROSODY_FIREWALL_CONFIGURATION_HELP: string
|
||||||
|
declare const LOC_PROSODY_FIREWALL_DISABLED_WARNING: string
|
||||||
|
declare const LOC_PROSODY_FIREWALL_FILE_ENABLED: string
|
||||||
|
declare const LOC_PROSODY_FIREWALL_NAME: string
|
||||||
|
declare const LOC_PROSODY_FIREWALL_NAME_DESC: string
|
||||||
|
declare const LOC_PROSODY_FIREWALL_CONTENT: string
|
||||||
|
@ -270,6 +270,8 @@ function register (clientOptions: RegisterClientOptions): void {
|
|||||||
return !(options.formValues['chat-all-lives'] === true && options.formValues['chat-per-live-video'] === true)
|
return !(options.formValues['chat-all-lives'] === true && options.formValues['chat-per-live-video'] === true)
|
||||||
case 'auto-ban-anonymous-ip':
|
case 'auto-ban-anonymous-ip':
|
||||||
return options.formValues['chat-no-anonymous'] !== false
|
return options.formValues['chat-no-anonymous'] !== false
|
||||||
|
case 'prosody-firewall-configure-button':
|
||||||
|
return options.formValues['prosody-firewall-enabled'] !== true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name?.startsWith('external-auth-')) {
|
if (name?.startsWith('external-auth-')) {
|
||||||
|
@ -8,6 +8,7 @@ import { registerConfiguration } from './common/configuration/register'
|
|||||||
import { registerVideoWatch } from './common/videowatch/register'
|
import { registerVideoWatch } from './common/videowatch/register'
|
||||||
import { registerRoom } from './common/room/register'
|
import { registerRoom } from './common/room/register'
|
||||||
import { initPtContext } from './common/lib/contexts/peertube'
|
import { initPtContext } from './common/lib/contexts/peertube'
|
||||||
|
import { registerAdminFirewall } from './common/admin/firewall/register'
|
||||||
import './common/lib/elements' // Import shared elements.
|
import './common/lib/elements' // Import shared elements.
|
||||||
|
|
||||||
async function register (clientOptions: RegisterClientOptions): Promise<void> {
|
async function register (clientOptions: RegisterClientOptions): Promise<void> {
|
||||||
@ -69,7 +70,8 @@ async function register (clientOptions: RegisterClientOptions): Promise<void> {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
registerVideoWatch(),
|
registerVideoWatch(),
|
||||||
registerRoom(clientOptions),
|
registerRoom(clientOptions),
|
||||||
registerConfiguration(clientOptions)
|
registerConfiguration(clientOptions),
|
||||||
|
registerAdminFirewall(clientOptions)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
131
client/common/admin/firewall/elements/admin-firewall.ts
Normal file
131
client/common/admin/firewall/elements/admin-firewall.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { AdminFirewallConfiguration } from 'shared/lib/types'
|
||||||
|
import { AdminFirewallService } from '../services/admin-firewall'
|
||||||
|
import { LivechatElement } from '../../../lib/elements/livechat'
|
||||||
|
import { ValidationError, ValidationErrorType } from '../../../lib/models/validation'
|
||||||
|
import { tplAdminFirewall } from '../templates/admin-firewall'
|
||||||
|
import { TemplateResult, html, nothing } from 'lit'
|
||||||
|
import { customElement, state } from 'lit/decorators.js'
|
||||||
|
import { Task } from '@lit/task'
|
||||||
|
|
||||||
|
@customElement('livechat-admin-firewall')
|
||||||
|
export class AdminFirewallElement extends LivechatElement {
|
||||||
|
private _adminFirewallService?: AdminFirewallService
|
||||||
|
|
||||||
|
@state()
|
||||||
|
public firewallConfiguration?: AdminFirewallConfiguration
|
||||||
|
|
||||||
|
@state()
|
||||||
|
public validationError?: ValidationError
|
||||||
|
|
||||||
|
@state()
|
||||||
|
public actionDisabled: 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">
|
<ul class="peertube-plugin-livechat-configuration-home-channels">
|
||||||
${this._channels?.map((channel) => html`
|
${this._channels?.map((channel) => html`
|
||||||
<li>
|
<li>
|
||||||
<a href="${channel.livechatConfigurationUri}">
|
<a href="${channel.livechatConfigurationUri}" aria-hidden="true">
|
||||||
${channel.avatar
|
${channel.avatar
|
||||||
? html`<img class="avatar channel" src="${channel.avatar.path}">`
|
? html`<img class="avatar channel" src="${channel.avatar.path}">`
|
||||||
: html`<div class="avatar channel initial gray"></div>`
|
: html`<div class="avatar channel initial gray"></div>`
|
||||||
|
@ -135,6 +135,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
</livechat-configuration-section-header>
|
</livechat-configuration-section-header>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<textarea
|
<textarea
|
||||||
|
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TERMS_LABEL) as any}
|
||||||
name="terms"
|
name="terms"
|
||||||
id="peertube-livechat-terms"
|
id="peertube-livechat-terms"
|
||||||
.value=${el.channelConfiguration?.configuration.terms ?? ''}
|
.value=${el.channelConfiguration?.configuration.terms ?? ''}
|
||||||
@ -167,7 +168,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="bot"
|
name="mute_anonymous"
|
||||||
id="peertube-livechat-mute-anonymous"
|
id="peertube-livechat-mute-anonymous"
|
||||||
@input=${(event: InputEvent) => {
|
@input=${(event: InputEvent) => {
|
||||||
if (event?.target && el.channelConfiguration) {
|
if (event?.target && el.channelConfiguration) {
|
||||||
@ -254,6 +255,32 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
|||||||
${el.renderFeedback('peertube-livechat-moderation-delay-feedback', 'moderation.delay')}
|
${el.renderFeedback('peertube-livechat-moderation-delay-feedback', 'moderation.delay')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<livechat-configuration-section-header
|
||||||
|
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL)}
|
||||||
|
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_DESC, true)}
|
||||||
|
.helpPage=${'documentation/user/streamers/moderation'}>
|
||||||
|
</livechat-configuration-section-header>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="anonymize-moderation"
|
||||||
|
id="peertube-livechat-anonymize-moderation"
|
||||||
|
@input=${(event: InputEvent) => {
|
||||||
|
if (event?.target && el.channelConfiguration) {
|
||||||
|
el.channelConfiguration.configuration.moderation.anonymize =
|
||||||
|
(event.target as HTMLInputElement).checked
|
||||||
|
}
|
||||||
|
el.requestUpdate('channelConfiguration')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="1"
|
||||||
|
?checked=${el.channelConfiguration?.configuration.moderation.anonymize}
|
||||||
|
/>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<livechat-configuration-section-header
|
<livechat-configuration-section-header
|
||||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
|
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
|
||||||
.description=${''}
|
.description=${''}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
|
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
|
||||||
export const AddSVG: string =
|
export const AddSVG: string =
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
stroke-linejoin="round" class="feather feather-plus-square">
|
stroke-linejoin="round" class="feather feather-plus-square">
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
@ -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/
|
// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/
|
||||||
export const RemoveSVG: string =
|
export const RemoveSVG: string =
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
stroke-linejoin="round" class="feather feather-x-square">
|
stroke-linejoin="round" class="feather feather-x-square">
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
|
@ -47,11 +47,11 @@ interface CellDataSchema {
|
|||||||
minlength?: number
|
minlength?: number
|
||||||
maxlength?: number
|
maxlength?: number
|
||||||
size?: number
|
size?: number
|
||||||
label?: TemplateResult | string
|
|
||||||
options?: { [key: string]: string }
|
options?: { [key: string]: string }
|
||||||
datalist?: DynamicTableAcceptedTypes[]
|
datalist?: DynamicTableAcceptedTypes[]
|
||||||
separator?: string
|
separator?: string
|
||||||
inputType?: DynamicTableAcceptedInputTypes
|
inputType?: DynamicTableAcceptedInputTypes
|
||||||
|
inputTitle?: string
|
||||||
default?: DynamicTableAcceptedTypes
|
default?: DynamicTableAcceptedTypes
|
||||||
colClassList?: string[] // CSS classes to add to the <td> element.
|
colClassList?: string[] // CSS classes to add to the <td> element.
|
||||||
}
|
}
|
||||||
@ -64,7 +64,7 @@ interface DynamicTableRowData {
|
|||||||
|
|
||||||
interface DynamicFormHeaderCellData {
|
interface DynamicFormHeaderCellData {
|
||||||
colName: TemplateResult | DirectiveResult
|
colName: TemplateResult | DirectiveResult
|
||||||
description: TemplateResult | DirectiveResult
|
description?: TemplateResult | DirectiveResult
|
||||||
headerClassList?: string[]
|
headerClassList?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +236,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
classList.push(...headerCellData.headerClassList)
|
classList.push(...headerCellData.headerClassList)
|
||||||
}
|
}
|
||||||
return html`<th scope="col" class=${classList.join(' ')}>
|
return html`<th scope="col" class=${classList.join(' ')}>
|
||||||
${headerCellData.description}
|
${headerCellData.description ?? ''}
|
||||||
</th>`
|
</th>`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,6 +295,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
const inputId =
|
const inputId =
|
||||||
`peertube-livechat-${this.formName.replace(/_/g, '-')}-${propertyName.toString().replace(/_/g, '-')}-${rowId}`
|
`peertube-livechat-${this.formName.replace(/_/g, '-')}-${propertyName.toString().replace(/_/g, '-')}-${rowId}`
|
||||||
|
|
||||||
|
const inputTitle: DirectiveResult | undefined = propertySchema.inputTitle ?? this.header[propertyName]?.colName
|
||||||
const feedback = this._renderFeedback(inputId, propertyName, originalIndex)
|
const feedback = this._renderFeedback(inputId, propertyName, originalIndex)
|
||||||
|
|
||||||
switch (propertySchema.default?.constructor) {
|
switch (propertySchema.default?.constructor) {
|
||||||
@ -320,6 +321,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderInput(rowId,
|
formElement = html`${this._renderInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue as string,
|
propertyValue as string,
|
||||||
@ -332,6 +334,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderTextarea(rowId,
|
formElement = html`${this._renderTextarea(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue as string,
|
propertyValue as string,
|
||||||
@ -344,6 +347,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderSelect(rowId,
|
formElement = html`${this._renderSelect(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue as string,
|
propertyValue as string,
|
||||||
@ -356,6 +360,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderImageFileInput(rowId,
|
formElement = html`${this._renderImageFileInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue?.toString(),
|
propertyValue?.toString(),
|
||||||
@ -376,6 +381,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderInput(rowId,
|
formElement = html`${this._renderInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
(propertyValue as Date).toISOString(),
|
(propertyValue as Date).toISOString(),
|
||||||
@ -394,6 +400,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderInput(rowId,
|
formElement = html`${this._renderInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue as string,
|
propertyValue as string,
|
||||||
@ -411,6 +418,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderCheckbox(rowId,
|
formElement = html`${this._renderCheckbox(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue as boolean,
|
propertyValue as boolean,
|
||||||
@ -446,6 +454,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderInput(rowId,
|
formElement = html`${this._renderInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
(propertyValue)?.join(propertySchema.separator ?? ',') ??
|
(propertyValue)?.join(propertySchema.separator ?? ',') ??
|
||||||
@ -461,6 +470,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderTextarea(rowId,
|
formElement = html`${this._renderTextarea(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
(propertyValue)?.join(propertySchema.separator ?? ',') ??
|
(propertyValue)?.join(propertySchema.separator ?? ',') ??
|
||||||
@ -476,6 +486,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
formElement = html`${this._renderTagsInput(rowId,
|
formElement = html`${this._renderTagsInput(rowId,
|
||||||
inputId,
|
inputId,
|
||||||
inputName,
|
inputName,
|
||||||
|
inputTitle,
|
||||||
propertyName,
|
propertyName,
|
||||||
propertySchema,
|
propertySchema,
|
||||||
propertyValue,
|
propertyValue,
|
||||||
@ -501,6 +512,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderInput = (rowId: number,
|
_renderInput = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
|
inputTitle: string | DirectiveResult | undefined,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: string,
|
propertyValue: string,
|
||||||
@ -515,6 +527,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
|
title=${ifDefined(inputTitle)}
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
list=${ifDefined(propertySchema.datalist ? inputId + '-datalist' : undefined)}
|
list=${ifDefined(propertySchema.datalist ? inputId + '-datalist' : undefined)}
|
||||||
min=${ifDefined(propertySchema.min)}
|
min=${ifDefined(propertySchema.min)}
|
||||||
@ -534,6 +547,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderTagsInput = (rowId: number,
|
_renderTagsInput = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
|
inputTitle: string | DirectiveResult | undefined,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: Array<string | number>,
|
propertyValue: Array<string | number>,
|
||||||
@ -547,7 +561,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
.inputPlaceholder=${propertySchema.label as any}
|
.inputTitle=${inputTitle as any}
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
.min=${propertySchema.min}
|
.min=${propertySchema.min}
|
||||||
.max=${propertySchema.max}
|
.max=${propertySchema.max}
|
||||||
@ -563,6 +577,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderTextarea = (rowId: number,
|
_renderTextarea = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
|
inputTitle: string | DirectiveResult | undefined,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: string,
|
propertyValue: string,
|
||||||
@ -576,6 +591,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
|
title=${ifDefined(inputTitle)}
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
min=${ifDefined(propertySchema.min)}
|
min=${ifDefined(propertySchema.min)}
|
||||||
max=${ifDefined(propertySchema.max)}
|
max=${ifDefined(propertySchema.max)}
|
||||||
@ -588,6 +604,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderCheckbox = (rowId: number,
|
_renderCheckbox = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
|
inputTitle: string | DirectiveResult | undefined,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: boolean,
|
propertyValue: boolean,
|
||||||
@ -602,6 +619,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
|
title=${ifDefined(inputTitle)}
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
||||||
value="1"
|
value="1"
|
||||||
@ -611,6 +629,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderSelect = (rowId: number,
|
_renderSelect = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
|
inputTitle: string | DirectiveResult | undefined,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: string,
|
propertyValue: string,
|
||||||
@ -623,11 +642,12 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
|
title=${ifDefined(inputTitle)}
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
aria-label=${inputName}
|
aria-label=${inputName}
|
||||||
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
||||||
>
|
>
|
||||||
<option ?selected=${!propertyValue}>${propertySchema.label ?? 'Choose your option'}</option>
|
<option ?selected=${!propertyValue}>${inputTitle ?? ''}</option>
|
||||||
${Object.entries(propertySchema.options ?? {})
|
${Object.entries(propertySchema.options ?? {})
|
||||||
?.map(([value, name]) =>
|
?.map(([value, name]) =>
|
||||||
html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`
|
html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`
|
||||||
@ -638,6 +658,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
_renderImageFileInput = (rowId: number,
|
_renderImageFileInput = (rowId: number,
|
||||||
inputId: string,
|
inputId: string,
|
||||||
inputName: string,
|
inputName: string,
|
||||||
|
inputTitle: string | DirectiveResult | undefined,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
propertySchema: CellDataSchema,
|
propertySchema: CellDataSchema,
|
||||||
propertyValue: string,
|
propertyValue: string,
|
||||||
@ -647,6 +668,7 @@ export class DynamicTableFormElement extends LivechatElement {
|
|||||||
.name=${inputName}
|
.name=${inputName}
|
||||||
class=${classMap(this._getInputValidationClass(propertyName, originalIndex))}
|
class=${classMap(this._getInputValidationClass(propertyName, originalIndex))}
|
||||||
id=${inputId}
|
id=${inputId}
|
||||||
|
.inputTitle=${inputTitle as any}
|
||||||
aria-describedby="${inputId}-feedback"
|
aria-describedby="${inputId}-feedback"
|
||||||
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
||||||
.value=${propertyValue}
|
.value=${propertyValue}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { LivechatElement } from './livechat'
|
import { LivechatElement } from './livechat'
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
|
import type { DirectiveResult } from 'lit/directive'
|
||||||
import { customElement, property } from 'lit/decorators.js'
|
import { customElement, property } from 'lit/decorators.js'
|
||||||
|
import { ifDefined } from 'lit/directives/if-defined.js'
|
||||||
/**
|
/**
|
||||||
* Special element to upload image files.
|
* Special element to upload image files.
|
||||||
* If no current value, displays an input type="file" field.
|
* If no current value, displays an input type="file" field.
|
||||||
@ -29,13 +29,16 @@ export class ImageFileInputElement extends LivechatElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public maxSize?: number
|
public maxSize?: number
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public inputTitle?: string | DirectiveResult
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public accept: string[] = ['image/jpg', 'image/png', 'image/gif']
|
public accept: string[] = ['image/jpg', 'image/png', 'image/gif']
|
||||||
|
|
||||||
protected override render = (): unknown => {
|
protected override render = (): unknown => {
|
||||||
return html`
|
return html`
|
||||||
${this.value
|
${this.value
|
||||||
? html`<img src=${this.value} @click=${(ev: Event) => {
|
? html`<img src=${this.value} alt=${ifDefined(this.inputTitle)} @click=${(ev: Event) => {
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
const upload: HTMLInputElement | null | undefined = this.parentElement?.querySelector('input[type="file"]')
|
const upload: HTMLInputElement | null | undefined = this.parentElement?.querySelector('input[type="file"]')
|
||||||
upload?.click()
|
upload?.click()
|
||||||
@ -44,6 +47,7 @@ export class ImageFileInputElement extends LivechatElement {
|
|||||||
}
|
}
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
title=${ifDefined(this.inputTitle)}
|
||||||
accept="${this.accept.join(',')}"
|
accept="${this.accept.join(',')}"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
style=${this.value ? 'display: none;' : ''}
|
style=${this.value ? 'display: none;' : ''}
|
||||||
|
@ -12,6 +12,7 @@ import { ifDefined } from 'lit/directives/if-defined.js'
|
|||||||
import { classMap } from 'lit/directives/class-map.js'
|
import { classMap } from 'lit/directives/class-map.js'
|
||||||
import { animate, fadeOut, fadeIn } from '@lit-labs/motion'
|
import { animate, fadeOut, fadeIn } from '@lit-labs/motion'
|
||||||
import { repeat } from 'lit/directives/repeat.js'
|
import { repeat } from 'lit/directives/repeat.js'
|
||||||
|
import type { DirectiveResult } from 'lit/directive'
|
||||||
|
|
||||||
// FIXME: find a better way to store this image.
|
// FIXME: find a better way to store this image.
|
||||||
// This content comes from the file assets/images/copy.svg, after svgo cleaning.
|
// This content comes from the file assets/images/copy.svg, after svgo cleaning.
|
||||||
@ -48,7 +49,7 @@ export class TagsInputElement extends LivechatElement {
|
|||||||
private _inputValue?: string = ''
|
private _inputValue?: string = ''
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public inputPlaceholder?: string = ''
|
public inputTitle?: string | DirectiveResult = ''
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public datalist?: string[]
|
public datalist?: string[]
|
||||||
@ -166,7 +167,7 @@ export class TagsInputElement extends LivechatElement {
|
|||||||
@input=${(e: InputEvent) => this._handleInputEvent(e)}
|
@input=${(e: InputEvent) => this._handleInputEvent(e)}
|
||||||
@change=${(e: Event) => e.stopPropagation()}
|
@change=${(e: Event) => e.stopPropagation()}
|
||||||
.value=${this._inputValue ?? ''}
|
.value=${this._inputValue ?? ''}
|
||||||
placeholder=${ifDefined(this.inputPlaceholder)} />
|
title=${ifDefined(this.inputTitle)} />
|
||||||
${(this.datalist)
|
${(this.datalist)
|
||||||
? html`<datalist id="${this.id ?? 'tags-input'}-datalist">
|
? html`<datalist id="${this.id ?? 'tags-input'}-datalist">
|
||||||
${(this.datalist ?? []).map((value) => html`<option value=${value}>`)}
|
${(this.datalist ?? []).map((value) => html`<option value=${value}>`)}
|
||||||
|
@ -42,6 +42,23 @@ function displayButton (dbo: displayButtonOptions): void {
|
|||||||
if ('href' in dbo) {
|
if ('href' in dbo) {
|
||||||
button.href = dbo.href
|
button.href = dbo.href
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!button.href || button.href === '#') {
|
||||||
|
// No href => it is not a link.
|
||||||
|
button.role = 'button'
|
||||||
|
button.tabIndex = 0
|
||||||
|
|
||||||
|
// We must also ensure that the enter key is triggering the onclick
|
||||||
|
if (button.onclick) {
|
||||||
|
button.onkeydown = ev => {
|
||||||
|
if (ev.key === 'Enter') {
|
||||||
|
ev.preventDefault()
|
||||||
|
button.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (('targetBlank' in dbo) && dbo.targetBlank) {
|
if (('targetBlank' in dbo) && dbo.targetBlank) {
|
||||||
button.target = '_blank'
|
button.target = '_blank'
|
||||||
}
|
}
|
||||||
@ -52,6 +69,10 @@ function displayButton (dbo: displayButtonOptions): void {
|
|||||||
tmp.innerHTML = svg.trim()
|
tmp.innerHTML = svg.trim()
|
||||||
const svgDom = tmp.firstChild
|
const svgDom = tmp.firstChild
|
||||||
if (svgDom) {
|
if (svgDom) {
|
||||||
|
if ('ariaHidden' in (svgDom as HTMLElement)) {
|
||||||
|
// Icon must be hidden for screen readers.
|
||||||
|
(svgDom as HTMLElement).ariaHidden = 'true'
|
||||||
|
}
|
||||||
button.prepend(svgDom)
|
button.prepend(svgDom)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -16,8 +16,6 @@ import { localizedHelpUrl } from '../../utils/help'
|
|||||||
import { getBaseRoute } from '../../utils/uri'
|
import { getBaseRoute } from '../../utils/uri'
|
||||||
import { displayConverseJS } from '../../utils/conversejs'
|
import { displayConverseJS } from '../../utils/conversejs'
|
||||||
|
|
||||||
let savedMyPluginFlexGrow: string | undefined
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the chat for the current video
|
* Initialize the chat for the current video
|
||||||
* @param video the video
|
* @param video the video
|
||||||
@ -25,7 +23,6 @@ let savedMyPluginFlexGrow: string | undefined
|
|||||||
async function initChat (video: Video): Promise<void> {
|
async function initChat (video: Video): Promise<void> {
|
||||||
const ptContext = getPtContext()
|
const ptContext = getPtContext()
|
||||||
const logger = ptContext.logger
|
const logger = ptContext.logger
|
||||||
savedMyPluginFlexGrow = undefined
|
|
||||||
|
|
||||||
if (!video) {
|
if (!video) {
|
||||||
logger.error('No video provided')
|
logger.error('No video provided')
|
||||||
@ -46,6 +43,8 @@ async function initChat (video: Video): Promise<void> {
|
|||||||
container.setAttribute('id', 'peertube-plugin-livechat-container')
|
container.setAttribute('id', 'peertube-plugin-livechat-container')
|
||||||
container.setAttribute('peertube-plugin-livechat-state', 'initializing')
|
container.setAttribute('peertube-plugin-livechat-state', 'initializing')
|
||||||
container.setAttribute('peertube-plugin-livechat-current-url', window.location.href)
|
container.setAttribute('peertube-plugin-livechat-current-url', window.location.href)
|
||||||
|
container.role = 'region'
|
||||||
|
container.ariaLabel = await ptContext.ptOptions.peertubeHelpers.translate(LOC_CHAT)
|
||||||
placeholder.append(container)
|
placeholder.append(container)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -353,19 +352,6 @@ function _hackStyles (on: boolean): void {
|
|||||||
buttons.classList.remove('peertube-plugin-livechat-buttons-open')
|
buttons.classList.remove('peertube-plugin-livechat-buttons-open')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const myPluginPlaceholder: HTMLElement | null = document.querySelector('my-plugin-placeholder')
|
|
||||||
if (on) {
|
|
||||||
// Saving current style attributes and maximazing space for the chat
|
|
||||||
if (myPluginPlaceholder) {
|
|
||||||
savedMyPluginFlexGrow = myPluginPlaceholder.style.flexGrow // Should be "", but can be anything else.
|
|
||||||
myPluginPlaceholder.style.flexGrow = '1'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// restoring values...
|
|
||||||
if (savedMyPluginFlexGrow !== undefined && myPluginPlaceholder) {
|
|
||||||
myPluginPlaceholder.style.flexGrow = savedMyPluginFlexGrow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
getPtContext().logger.error(`Failed hacking styles: '${err as string}'`)
|
getPtContext().logger.error(`Failed hacking styles: '${err as string}'`)
|
||||||
}
|
}
|
||||||
|
@ -167,7 +167,7 @@ async function displayConverseJS (
|
|||||||
const converseJSParams: InitConverseJSParams = await (response).json()
|
const converseJSParams: InitConverseJSParams = await (response).json()
|
||||||
|
|
||||||
if (!pollListenerInitiliazed) {
|
if (!pollListenerInitiliazed) {
|
||||||
// First time we got here, initiliaze this event:
|
// First time we got here, initialize this event:
|
||||||
const i18nVoteOk = await clientOptions.peertubeHelpers.translate(LOC_POLL_VOTE_OK)
|
const i18nVoteOk = await clientOptions.peertubeHelpers.translate(LOC_POLL_VOTE_OK)
|
||||||
pollListenerInitiliazed = true
|
pollListenerInitiliazed = true
|
||||||
document.addEventListener('livechat-poll-vote', () => {
|
document.addEventListener('livechat-poll-vote', () => {
|
||||||
|
@ -15,32 +15,22 @@ set -x
|
|||||||
|
|
||||||
# Set CONVERSE_VERSION and CONVERSE_REPO to select which repo and tag/commit/branch use.
|
# Set CONVERSE_VERSION and CONVERSE_REPO to select which repo and tag/commit/branch use.
|
||||||
# Defaults values:
|
# Defaults values:
|
||||||
CONVERSE_VERSION="v10.1.6"
|
CONVERSE_VERSION="v11.0.0"
|
||||||
CONVERSE_REPO="https://github.com/conversejs/converse.js.git"
|
CONVERSE_REPO="https://github.com/conversejs/converse.js.git"
|
||||||
# You can eventually set CONVERSE_COMMIT to a specific commit ID, if you want to apply some patches.
|
# You can eventually set CONVERSE_COMMIT to a specific commit ID, if you want to apply some patches.
|
||||||
CONVERSE_COMMIT=""
|
# 2024-09-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.
|
# It is possible to use another repository, if we want some customization that are not upstream (yet):
|
||||||
# This version includes following changes:
|
# CONVERSE_VERSION="livechat"
|
||||||
# - #converse.js/3300: Adding the maxWait option for `debouncedPruneHistory`
|
# # CONVERSE_COMMIT="4402fcc3fc60f6c9334f86528c33a0b463371d12"
|
||||||
# - #converse.js/3302: debounce MUC sidebar rendering
|
# CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js"
|
||||||
# - Fix: refresh the MUC sidebar when participants collection is sorted
|
# CONVERSE_COMMIT="xxxx"
|
||||||
# - Fix: MUC occupant list does not sort itself on nicknames or roles changes
|
|
||||||
# - Fix inconsistency between browsers on textarea outlines
|
# 2024-09-03: include badges short label and quick fix for sendMessage button
|
||||||
# - Fix: room information not correctly refreshed when modifications are made by other users
|
|
||||||
# This version already includes following changes that will not be merged in ConverseJS upstream:
|
|
||||||
# - Don't load vCards for all room occupants when the right menu is closed
|
|
||||||
# - Changing the default avatar, for something very light (to mitigate blinking effect when vCards are loaded)
|
|
||||||
# - Custom settings livechat_load_all_vcards for the readonly mode
|
|
||||||
# - Adding "users" icon in the menu toggle button
|
|
||||||
# - Removing unecessary plugins: headless/pubsub, minimize, notifications, profile, omemo, push, roomlist, dragresize.
|
|
||||||
# - Destroy room: remove the challenge, and the new JID
|
|
||||||
# - New config option [colorize_username](https://conversejs.org/docs/html/configuration.html#colorize_username)
|
|
||||||
# - New loadEmojis hook, to customize emojis at runtime.
|
|
||||||
# - Fix custom emojis path when assets_path is not the default path.
|
|
||||||
CONVERSE_VERSION="livechat-10.1.0"
|
|
||||||
# CONVERSE_COMMIT="4402fcc3fc60f6c9334f86528c33a0b463371d12"
|
|
||||||
CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js"
|
CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js"
|
||||||
|
CONVERSE_VERSION="livechat-11.0.1"
|
||||||
|
CONVERSE_COMMIT=""
|
||||||
|
|
||||||
rootdir="$(pwd)"
|
rootdir="$(pwd)"
|
||||||
src_dir="$rootdir/conversejs"
|
src_dir="$rootdir/conversejs"
|
||||||
|
@ -34,6 +34,7 @@ declare global {
|
|||||||
env: {
|
env: {
|
||||||
html: Function
|
html: Function
|
||||||
sizzle: Function
|
sizzle: Function
|
||||||
|
dayjs: Function
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initConversePlugins: typeof initConversePlugins
|
initConversePlugins: typeof initConversePlugins
|
||||||
@ -218,20 +219,24 @@ async function initConverse (
|
|||||||
// * mode === chat-only + !transparent + !readonly + is using a livechat token
|
// * mode === chat-only + !transparent + !readonly + is using a livechat token
|
||||||
// Technically it would work in 'chat-only' mode, but i don't want to add too many things to test
|
// Technically it would work in 'chat-only' mode, but i don't want to add too many things to test
|
||||||
// (and i now there is some CSS bugs in the task list).
|
// (and i now there is some CSS bugs in the task list).
|
||||||
let enableTask = false
|
// Same for the moderator notes app.
|
||||||
|
let enableApps = false
|
||||||
if (chatIncludeMode === 'peertube-video' || chatIncludeMode === 'peertube-fullpage') {
|
if (chatIncludeMode === 'peertube-video' || chatIncludeMode === 'peertube-fullpage') {
|
||||||
enableTask = true
|
enableApps = true
|
||||||
} else if (
|
} else if (
|
||||||
chatIncludeMode === 'chat-only' &&
|
chatIncludeMode === 'chat-only' &&
|
||||||
usedLivechatToken &&
|
usedLivechatToken &&
|
||||||
!initConverseParams.transparent &&
|
!initConverseParams.transparent &&
|
||||||
!initConverseParams.forceReadonly
|
!initConverseParams.forceReadonly
|
||||||
) {
|
) {
|
||||||
enableTask = true
|
enableApps = true
|
||||||
}
|
}
|
||||||
if (enableTask) {
|
if (enableApps) {
|
||||||
params.livechat_task_app_enabled = true
|
params.livechat_task_app_enabled = true
|
||||||
params.livechat_task_app_restore = chatIncludeMode === 'peertube-fullpage' || chatIncludeMode === 'chat-only'
|
params.livechat_task_app_restore = chatIncludeMode === 'peertube-fullpage' || chatIncludeMode === 'chat-only'
|
||||||
|
params.livechat_note_app_enabled = true
|
||||||
|
params.livechat_note_app_restore = chatIncludeMode === 'peertube-fullpage' || chatIncludeMode === 'chat-only'
|
||||||
|
params.livechat_mam_search_app_enabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -8,14 +8,13 @@
|
|||||||
* @description This files will override the original ConverseJS index.js file.
|
* @description This files will override the original ConverseJS index.js file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import '@converse/headless'
|
import 'shared/styles/index.scss'
|
||||||
|
|
||||||
import './i18n/index.js'
|
import './i18n/index.js'
|
||||||
import 'shared/registry.js'
|
import 'shared/registry.js'
|
||||||
import { CustomElement } from 'shared/components/element'
|
import { CustomElement } from 'shared/components/element'
|
||||||
import { VIEW_PLUGINS } from './shared/constants.js'
|
import { VIEW_PLUGINS } from './shared/constants.js'
|
||||||
import { _converse, converse } from '@converse/headless/core'
|
import { _converse, converse } from '@converse/headless'
|
||||||
|
|
||||||
import 'shared/styles/index.scss'
|
|
||||||
|
|
||||||
/* START: Removable plugins
|
/* START: Removable plugins
|
||||||
* ------------------------
|
* ------------------------
|
||||||
@ -45,11 +44,16 @@ import './plugins/singleton/index.js'
|
|||||||
import './plugins/fullscreen/index.js'
|
import './plugins/fullscreen/index.js'
|
||||||
|
|
||||||
import '../custom/plugins/size/index.js'
|
import '../custom/plugins/size/index.js'
|
||||||
|
import '../custom/plugins/mam-search/index.js'
|
||||||
|
import '../custom/plugins/notes/index.js'
|
||||||
import '../custom/plugins/tasks/index.js'
|
import '../custom/plugins/tasks/index.js'
|
||||||
import '../custom/plugins/terms/index.js'
|
import '../custom/plugins/terms/index.js'
|
||||||
import '../custom/plugins/poll/index.js'
|
import '../custom/plugins/poll/index.js'
|
||||||
/* END: Removable components */
|
/* END: Removable components */
|
||||||
|
|
||||||
|
// Running some specific livechat patches:
|
||||||
|
import '../custom/livechat-patch-vcard.js'
|
||||||
|
|
||||||
import { CORE_PLUGINS } from './headless/shared/constants.js'
|
import { CORE_PLUGINS } from './headless/shared/constants.js'
|
||||||
import { ROOM_FEATURES } from './headless/plugins/muc/constants.js'
|
import { ROOM_FEATURES } from './headless/plugins/muc/constants.js'
|
||||||
// We must add our custom plugins to CORE_PLUGINS (so it is white listed):
|
// We must add our custom plugins to CORE_PLUGINS (so it is white listed):
|
||||||
@ -57,11 +61,13 @@ CORE_PLUGINS.push('livechat-converse-size')
|
|||||||
CORE_PLUGINS.push('livechat-converse-tasks')
|
CORE_PLUGINS.push('livechat-converse-tasks')
|
||||||
CORE_PLUGINS.push('livechat-converse-terms')
|
CORE_PLUGINS.push('livechat-converse-terms')
|
||||||
CORE_PLUGINS.push('livechat-converse-poll')
|
CORE_PLUGINS.push('livechat-converse-poll')
|
||||||
|
CORE_PLUGINS.push('livechat-converse-notes')
|
||||||
|
CORE_PLUGINS.push('livechat-converse-mam-search')
|
||||||
// We must also add our custom ROOM_FEATURES, so that they correctly resets
|
// We must also add our custom ROOM_FEATURES, so that they correctly resets
|
||||||
// (see headless/plugins/muc, getDiscoInfoFeatures, which loops on this const)
|
// (see headless/plugins/muc, getDiscoInfoFeatures, which loops on this const)
|
||||||
ROOM_FEATURES.push('x_peertubelivechat_mute_anonymous')
|
ROOM_FEATURES.push('x_peertubelivechat_mute_anonymous')
|
||||||
|
|
||||||
_converse.CustomElement = CustomElement
|
_converse.exports.CustomElement = CustomElement
|
||||||
|
|
||||||
const initialize = converse.initialize
|
const initialize = converse.initialize
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { api } from '@converse/headless/core.js'
|
import { api } from '@converse/headless/index.js'
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
import { CustomElement } from 'shared/components/element.js'
|
||||||
import { tplExternalLoginModal } from 'templates/livechat-external-login-modal.js'
|
import { tplExternalLoginModal } from 'templates/livechat-external-login-modal.js'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
|
61
conversejs/custom/livechat-patch-vcard.js
Normal file
61
conversejs/custom/livechat-patch-vcard.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// Here we are patching the vCard plugin, to add some specific optimizations.
|
||||||
|
|
||||||
|
import { _converse, api } from '@converse/headless/index.js'
|
||||||
|
import {
|
||||||
|
onOccupantAvatarChanged,
|
||||||
|
setVCardOnModel,
|
||||||
|
setVCardOnOccupant
|
||||||
|
} from '@converse/headless/plugins/vcard/utils.js'
|
||||||
|
|
||||||
|
const pluginDefinition = _converse.pluggable.plugins['converse-vcard']
|
||||||
|
const originalInitialize = pluginDefinition.initialize
|
||||||
|
|
||||||
|
pluginDefinition.initialize = function initialize () {
|
||||||
|
const previousListeners = _converse._events.chatRoomInitialized ?? []
|
||||||
|
originalInitialize.apply(this)
|
||||||
|
|
||||||
|
_converse.api.settings.extend({
|
||||||
|
livechat_load_all_vcards: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Now we must detect the new chatRoomInitialized listener, and remove it:
|
||||||
|
const listenersToRemove = []
|
||||||
|
for (const def of _converse._events.chatRoomInitialized ?? []) {
|
||||||
|
if (def.callback && !previousListeners.includes(def.callback)) {
|
||||||
|
listenersToRemove.push(def.callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const callback of listenersToRemove) {
|
||||||
|
console.debug('Livechat patching vcard: we must remove this listener', callback)
|
||||||
|
api.listen.not('chatRoomInitialized', callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding the new listener:
|
||||||
|
api.listen.on('chatRoomInitialized', (m) => {
|
||||||
|
console.debug('Patched version of the vcard chatRoomInitialized event.')
|
||||||
|
setVCardOnModel(m)
|
||||||
|
|
||||||
|
// loadAll: when in readonly mode (ie: OBS integration), always load all avatars.
|
||||||
|
const loadAll = api.settings.get('livechat_load_all_vcards') === true
|
||||||
|
let hiddenOccupants = m.get('hidden_occupants')
|
||||||
|
if (hiddenOccupants !== true || loadAll) {
|
||||||
|
m.occupants.forEach(setVCardOnOccupant)
|
||||||
|
}
|
||||||
|
m.listenTo(m.occupants, 'add', (occupant) => {
|
||||||
|
if (hiddenOccupants !== true || loadAll) {
|
||||||
|
setVCardOnOccupant(occupant)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
m.on('change:hidden_occupants', () => {
|
||||||
|
hiddenOccupants = m.get('hidden_occupants')
|
||||||
|
if (hiddenOccupants !== true || loadAll) {
|
||||||
|
m.occupants.forEach(setVCardOnOccupant)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
m.listenTo(m.occupants, 'change:image_hash', o => onOccupantAvatarChanged(o))
|
||||||
|
})
|
||||||
|
}
|
112
conversejs/custom/plugins/mam-search/api.js
Normal file
112
conversejs/custom/plugins/mam-search/api.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { api, converse } from '../../../src/headless/index.js'
|
||||||
|
import { XMLNS_MAM_SEARCH } from './constants.js'
|
||||||
|
|
||||||
|
const env = converse.env
|
||||||
|
const {
|
||||||
|
$iq,
|
||||||
|
Strophe,
|
||||||
|
sizzle,
|
||||||
|
log,
|
||||||
|
TimeoutError,
|
||||||
|
__,
|
||||||
|
u
|
||||||
|
} = env
|
||||||
|
const NS = Strophe.NS
|
||||||
|
|
||||||
|
async function query (options) {
|
||||||
|
if (!api.connection.connected()) {
|
||||||
|
throw new Error('Can\'t call `api.livechat_mam_search.query` before having established an XMPP session')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options?.room) {
|
||||||
|
throw new Error('api.livechat_mam_search.query: Missing room parameter.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const attrs = {
|
||||||
|
type: 'set',
|
||||||
|
to: options.room
|
||||||
|
}
|
||||||
|
|
||||||
|
const jid = attrs.to
|
||||||
|
const supported = await api.disco.supports(XMLNS_MAM_SEARCH, jid)
|
||||||
|
if (!supported) {
|
||||||
|
log.warn(`Did not search MAM archive for ${jid} because it doesn't support ${XMLNS_MAM_SEARCH}`)
|
||||||
|
return { messages: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryid = u.getUniqueId()
|
||||||
|
const stanza = $iq(attrs).c('query', { xmlns: XMLNS_MAM_SEARCH, queryid: queryid })
|
||||||
|
|
||||||
|
stanza.c('x', { xmlns: NS.XFORM, type: 'submit' })
|
||||||
|
.c('field', { var: 'FORM_TYPE', type: 'hidden' })
|
||||||
|
.c('value').t(XMLNS_MAM_SEARCH).up().up()
|
||||||
|
|
||||||
|
if (options.from) {
|
||||||
|
stanza.c('field', { var: 'from' }).c('value')
|
||||||
|
.t(options.from).up().up()
|
||||||
|
}
|
||||||
|
if (options.occupant_id) {
|
||||||
|
stanza.c('field', { var: 'occupant_id' }).c('value')
|
||||||
|
.t(options.occupant_id).up().up()
|
||||||
|
}
|
||||||
|
stanza.up()
|
||||||
|
|
||||||
|
// TODO: handle RSM (pagination.)
|
||||||
|
|
||||||
|
const connection = api.connection.get()
|
||||||
|
|
||||||
|
const messages = []
|
||||||
|
const messageHandler = connection.addHandler((stanza) => {
|
||||||
|
const result = sizzle(`message > result[xmlns="${NS.MAM}"]`, stanza).pop()
|
||||||
|
if (result === undefined || result.getAttribute('queryid') !== queryid) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const from = stanza.getAttribute('from')
|
||||||
|
if (from !== attrs.to) {
|
||||||
|
log.warn(`Ignoring alleged groupchat MAM message from ${from}`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
messages.push(stanza)
|
||||||
|
return true
|
||||||
|
}, NS.MAM)
|
||||||
|
|
||||||
|
let error
|
||||||
|
const timeout = api.settings.get('message_archiving_timeout')
|
||||||
|
const iqResult = await api.sendIQ(stanza, timeout, false)
|
||||||
|
|
||||||
|
if (iqResult === null) {
|
||||||
|
const errMsg = __('Timeout while trying to fetch archived messages.')
|
||||||
|
log.error(errMsg)
|
||||||
|
error = new TimeoutError(errMsg)
|
||||||
|
return { messages, error }
|
||||||
|
} else if (u.isErrorStanza(iqResult)) {
|
||||||
|
const errMsg = __('An error occurred while querying for archived messages.')
|
||||||
|
log.error(errMsg)
|
||||||
|
log.error(iqResult)
|
||||||
|
error = new Error(errMsg)
|
||||||
|
return { messages, error }
|
||||||
|
}
|
||||||
|
connection.deleteHandler(messageHandler)
|
||||||
|
|
||||||
|
return { messages }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showMessagesFrom (occupant) {
|
||||||
|
const appElement = document.querySelector('livechat-converse-muc-mam-search-app')
|
||||||
|
if (!appElement) {
|
||||||
|
throw new Error('Cant find Search App Element')
|
||||||
|
}
|
||||||
|
appElement.searchFrom(occupant)
|
||||||
|
await appElement.showApp()
|
||||||
|
await appElement.updateComplete // waiting for the app to be open
|
||||||
|
return appElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
query,
|
||||||
|
showMessagesFrom
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { api } from '@converse/headless'
|
||||||
|
import { parseMUCMessage } from '@converse/headless/plugins/muc/parsers.js'
|
||||||
|
import { MUCApp } from '../../../shared/components/muc-app/index.js'
|
||||||
|
import { tplMamSearchApp } from '../templates/muc-mam-search-app.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Element to display the Mam Search Application.
|
||||||
|
*/
|
||||||
|
export default class MUCMamSearchApp extends MUCApp {
|
||||||
|
restoreSettingName = undefined
|
||||||
|
sessionStorageRestoreKey = undefined
|
||||||
|
|
||||||
|
static get properties () {
|
||||||
|
return {
|
||||||
|
model: { type: Object, attribute: true }, // the muc model
|
||||||
|
occupant: { type: Object, attribute: true }, // the occupant to search (can be undefined if no current search)
|
||||||
|
results: { type: Object, attribute: true } // a Collection with the results.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return tplMamSearchApp(this, this.model, this.occupant)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchFrom (occupant) {
|
||||||
|
this.results = undefined
|
||||||
|
this.occupant = occupant
|
||||||
|
const p = api.livechat_mam_search.query({
|
||||||
|
room: this.model.get('jid'),
|
||||||
|
// FIXME: shouldn't we escape the nick? cant see any code that escapes it in Converse.
|
||||||
|
from: occupant.get('from') || this.model.get('jid') + '/' + (occupant.get('nick') ?? ''),
|
||||||
|
occupant_id: occupant.get('occupant_id')
|
||||||
|
})
|
||||||
|
|
||||||
|
// don't wait the result to show something! (there will be a spinner)
|
||||||
|
p.then(async (results) => {
|
||||||
|
this.occupant = occupant // in case user did simultaneous requests
|
||||||
|
|
||||||
|
const messages = await Promise.all(results.messages.map(s => parseMUCMessage(s, this.model)))
|
||||||
|
// Note: we are not using MUCMessage objects, because we don't want the objects
|
||||||
|
// used here to interract with objects in the chat rooms.
|
||||||
|
// We could have a lot of unwanted sideeffects.
|
||||||
|
this.results = messages.reverse()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.elements.define('livechat-converse-muc-mam-search-app', MUCMamSearchApp)
|
@ -0,0 +1,82 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { CustomElement } from 'shared/components/element.js'
|
||||||
|
import { tplMucMamSearchMessage } from '../templates/muc-mam-search-message.js'
|
||||||
|
import { api } from '@converse/headless'
|
||||||
|
|
||||||
|
import '../styles/muc-mam-search-message.scss'
|
||||||
|
|
||||||
|
export default class MUCMamSearchMessageView extends CustomElement {
|
||||||
|
static get properties () {
|
||||||
|
return {
|
||||||
|
message: { type: Object, attribute: true }, // /!\ this is not a model
|
||||||
|
mucModel: { type: Object, attribute: true },
|
||||||
|
searchOccupantModel: { type: Object, attribute: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize () {
|
||||||
|
this.listenTo(this.mucModel, 'change', () => this.requestUpdate())
|
||||||
|
this.listenTo(this.searchOccupantModel, 'change', () => this.requestUpdate())
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return tplMucMamSearchMessage(this, this.mucModel, this.searchOccupantModel, this.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
getMessageOccupant () {
|
||||||
|
const occupants = this.mucModel?.occupants
|
||||||
|
if (!occupants?.findOccupant) { return undefined }
|
||||||
|
|
||||||
|
const nick = this.message.nick
|
||||||
|
const jid = this.message.from
|
||||||
|
const occupantId = this.message.occupant_id
|
||||||
|
|
||||||
|
if (!nick && !jid && !occupantId) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (occupantId) {
|
||||||
|
const o = occupants.findOccupant({ occupant_id: occupantId })
|
||||||
|
if (o) {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jid) {
|
||||||
|
const o = occupants.findOccupant({
|
||||||
|
jid,
|
||||||
|
nick
|
||||||
|
})
|
||||||
|
if (o) {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't find it, maybe it is a user that has spoken a long time ago (or never spoked).
|
||||||
|
// In such case, we must create a dummy occupant:
|
||||||
|
const o = occupants.create({
|
||||||
|
nick,
|
||||||
|
occupant_id: occupantId,
|
||||||
|
jid
|
||||||
|
})
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
getDateTime () {
|
||||||
|
if (!this.message.time) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const d = new Date(this.message.time)
|
||||||
|
return d.toLocaleDateString() + ' - ' + d.toLocaleTimeString()
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.elements.define('livechat-converse-muc-mam-search-message', MUCMamSearchMessageView)
|
@ -0,0 +1,28 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { CustomElement } from 'shared/components/element.js'
|
||||||
|
import { tplMucMamSearchOccupant } from '../templates/muc-mam-search-occupant'
|
||||||
|
import { api } from '@converse/headless'
|
||||||
|
|
||||||
|
import '../styles/muc-mam-search-occupant.scss'
|
||||||
|
|
||||||
|
export default class MUCMamSearchOccupantView extends CustomElement {
|
||||||
|
static get properties () {
|
||||||
|
return {
|
||||||
|
model: { type: Object, attribute: true },
|
||||||
|
message: { type: Object, attribute: true } // optional message.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize () {
|
||||||
|
this.listenTo(this.model, 'change', () => this.requestUpdate())
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return tplMucMamSearchOccupant(this, this.model, this.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.elements.define('livechat-converse-muc-mam-search-occupant', MUCMamSearchOccupantView)
|
5
conversejs/custom/plugins/mam-search/constants.js
Normal file
5
conversejs/custom/plugins/mam-search/constants.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
export const XMLNS_MAM_SEARCH = 'urn:xmpp:mam:2#x-search'
|
33
conversejs/custom/plugins/mam-search/index.js
Normal file
33
conversejs/custom/plugins/mam-search/index.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { api, converse } from '../../../src/headless/index.js'
|
||||||
|
import { getMessageActionButtons, getOccupantActionButtons } from './utils.js'
|
||||||
|
import mamSearchApi from './api.js'
|
||||||
|
|
||||||
|
import './components/muc-mam-search-app-view.js'
|
||||||
|
import './components/muc-mam-search-occupant-view.js'
|
||||||
|
import './components/muc-mam-search-message-view.js'
|
||||||
|
|
||||||
|
converse.plugins.add('livechat-converse-mam-search', {
|
||||||
|
dependencies: ['converse-muc', 'converse-muc-views'],
|
||||||
|
async initialize () {
|
||||||
|
const _converse = this._converse
|
||||||
|
|
||||||
|
Object.assign(api, {
|
||||||
|
livechat_mam_search: mamSearchApi
|
||||||
|
})
|
||||||
|
|
||||||
|
_converse.api.settings.extend({
|
||||||
|
livechat_mam_search_app_enabled: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Adding buttons on messages:
|
||||||
|
_converse.api.listen.on('getMessageActionButtons', getMessageActionButtons)
|
||||||
|
// Adding buttons on occupants:
|
||||||
|
_converse.api.listen.on('getOccupantActionButtons', getOccupantActionButtons)
|
||||||
|
|
||||||
|
// FIXME: should we listen to any event (feature/affiliation change?, mam_enabled?) to refresh messageActionButtons?
|
||||||
|
}
|
||||||
|
})
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
.conversejs {
|
||||||
|
livechat-converse-muc-mam-search-message {
|
||||||
|
border: 1px solid var(--chatroom-head-bg-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
display: block;
|
||||||
|
margin: 0.25em 0;
|
||||||
|
padding: 0.25em;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
converse-rich-text {
|
||||||
|
color: var(--message-text-color);
|
||||||
|
font-size: var(--message-font-size);
|
||||||
|
padding: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livechat-message-date {
|
||||||
|
font-size: 0.75em;
|
||||||
|
list-style: none;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
.conversejs {
|
||||||
|
livechat-converse-muc-mam-search-occupant {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.25em;
|
||||||
|
|
||||||
|
& > a {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > ul {
|
||||||
|
font-weight: lighter;
|
||||||
|
font-size: 0.75em;
|
||||||
|
list-style: none;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,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 { XMLNS_POLL } from '../constants.js'
|
||||||
import { tplPollForm } from '../templates/poll-form.js'
|
import { tplPollForm } from '../templates/poll-form.js'
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
import { CustomElement } from 'shared/components/element.js'
|
||||||
import { converse, api } from '@converse/headless/core'
|
import { converse, api, parsers } from '@converse/headless'
|
||||||
import { webForm2xForm } from '@converse/headless/utils/form'
|
import { webForm2xForm } from '@converse/headless/utils/form'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
import '../styles/poll-form.scss'
|
import '../styles/poll-form.scss'
|
||||||
@ -18,7 +18,6 @@ export default class MUCPollFormView extends CustomElement {
|
|||||||
return {
|
return {
|
||||||
model: { type: Object, attribute: true },
|
model: { type: Object, attribute: true },
|
||||||
modal: { type: Object, attribute: true },
|
modal: { type: Object, attribute: true },
|
||||||
form_fields: { type: Object, attribute: false },
|
|
||||||
alert_message: { type: Object, attribute: false },
|
alert_message: { type: Object, attribute: false },
|
||||||
title: { type: String, attribute: false },
|
title: { type: String, attribute: false },
|
||||||
instructions: { type: String, attribute: false }
|
instructions: { type: String, attribute: false }
|
||||||
@ -27,6 +26,8 @@ export default class MUCPollFormView extends CustomElement {
|
|||||||
|
|
||||||
_fieldTranslationMap = new Map()
|
_fieldTranslationMap = new Map()
|
||||||
|
|
||||||
|
xform = undefined
|
||||||
|
|
||||||
async initialize () {
|
async initialize () {
|
||||||
this.alert_message = undefined
|
this.alert_message = undefined
|
||||||
if (!this.model) {
|
if (!this.model) {
|
||||||
@ -36,20 +37,18 @@ export default class MUCPollFormView extends CustomElement {
|
|||||||
try {
|
try {
|
||||||
this._initFieldTranslations()
|
this._initFieldTranslations()
|
||||||
const stanza = await this._fetchPollForm()
|
const stanza = await this._fetchPollForm()
|
||||||
const query = stanza.querySelector('query')
|
const xform = parsers.parseXForm(stanza)
|
||||||
const xform = sizzle(`x[xmlns="${Strophe.NS.XFORM}"]`, query)[0]
|
|
||||||
if (!xform) {
|
if (!xform) {
|
||||||
throw Error('Missing xform in stanza')
|
throw Error('Missing xform in stanza')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
xform.fields?.map(f => this._translateField(f))
|
||||||
|
this.xform = xform
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
this.title = __(LOC_poll_title) // xform.querySelector('title')?.textContent ?? ''
|
this.title = __(LOC_poll_title) // xform.querySelector('title')?.textContent ?? ''
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
this.instructions = __(LOC_poll_instructions) // xform.querySelector('instructions')?.textContent ?? ''
|
this.instructions = __(LOC_poll_instructions) // xform.querySelector('instructions')?.textContent ?? ''
|
||||||
this.form_fields = Array.from(xform.querySelectorAll('field')).map(field => {
|
|
||||||
this._translateField(field)
|
|
||||||
return u.xForm2TemplateResult(field, stanza)
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
this.alert_message = __('Error')
|
this.alert_message = __('Error')
|
||||||
@ -86,10 +85,10 @@ export default class MUCPollFormView extends CustomElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_translateField (field) {
|
_translateField (field) {
|
||||||
const v = field.getAttribute('var')
|
const v = field.var
|
||||||
const label = this._fieldTranslationMap.get(v)
|
const label = this._fieldTranslationMap.get(v)
|
||||||
if (label) {
|
if (label) {
|
||||||
field.setAttribute('label', label)
|
field.label = label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +113,7 @@ export default class MUCPollFormView extends CustomElement {
|
|||||||
await api.sendIQ(iq)
|
await api.sendIQ(iq)
|
||||||
|
|
||||||
if (this.modal) {
|
if (this.modal) {
|
||||||
this.modal.onHide()
|
this.modal.close()
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (u.isErrorStanza(err)) {
|
if (u.isErrorStanza(err)) {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { tplPoll } from '../templates/poll.js'
|
import { tplPoll } from '../templates/poll.js'
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
import { CustomElement } from 'shared/components/element.js'
|
||||||
import { converse, _converse, api } from '@converse/headless/core'
|
import { converse, _converse, api } from '@converse/headless'
|
||||||
import '../styles/poll.scss'
|
import '../styles/poll.scss'
|
||||||
|
|
||||||
export default class MUCPollView extends CustomElement {
|
export default class MUCPollView extends CustomElement {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { _converse, converse } from '../../../src/headless/core.js'
|
import { _converse, converse } from '../../../src/headless/index.js'
|
||||||
import { getHeadingButtons } from './utils.js'
|
import { getHeadingButtons } from './utils.js'
|
||||||
import { POLL_MESSAGE_TAG, POLL_QUESTION_TAG, POLL_CHOICE_TAG } from './constants.js'
|
import { POLL_MESSAGE_TAG, POLL_QUESTION_TAG, POLL_CHOICE_TAG } from './constants.js'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
import BaseModal from 'plugins/modal/modal.js'
|
import BaseModal from 'plugins/modal/modal.js'
|
||||||
import { api } from '@converse/headless/core'
|
import { api } from '@converse/headless'
|
||||||
import { modal_close_button as ModalCloseButton } from 'plugins/modal/templates/buttons.js'
|
import { modal_close_button as ModalCloseButton } from 'plugins/modal/templates/buttons.js'
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
|
|
||||||
@ -13,8 +13,8 @@ class PollFormModal extends BaseModal {
|
|||||||
super.initialize()
|
super.initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
onHide () {
|
close () {
|
||||||
super.onHide()
|
super.close()
|
||||||
api.modal.remove('livechat-converse-poll-form-modal')
|
api.modal.remove('livechat-converse-poll-form-modal')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
|
import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
|
import { converse } from '@converse/headless'
|
||||||
|
|
||||||
|
const u = converse.env.utils
|
||||||
|
|
||||||
export function tplPollForm (el) {
|
export function tplPollForm (el) {
|
||||||
const i18nOk = __('Ok')
|
const i18nOk = __('Ok')
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
@ -13,10 +17,18 @@ export function tplPollForm (el) {
|
|||||||
page: 'documentation/user/streamers/polls'
|
page: 'documentation/user/streamers/polls'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let formFieldTemplates
|
||||||
|
if (el.xform) {
|
||||||
|
const fields = el.xform.fields
|
||||||
|
formFieldTemplates = fields.map(field => {
|
||||||
|
return u.xFormField2TemplateResult(field)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${el.alert_message ? html`<div class="error">${el.alert_message}</div>` : ''}
|
${el.alert_message ? html`<div class="error">${el.alert_message}</div>` : ''}
|
||||||
${
|
${
|
||||||
el.form_fields
|
formFieldTemplates
|
||||||
? html`
|
? html`
|
||||||
<form class="converse-form" @submit=${ev => el.formSubmit(ev)}>
|
<form class="converse-form" @submit=${ev => el.formSubmit(ev)}>
|
||||||
<p class="title">
|
<p class="title">
|
||||||
@ -30,9 +42,9 @@ export function tplPollForm (el) {
|
|||||||
<p class="form-help instructions">${el.instructions}</p>
|
<p class="form-help instructions">${el.instructions}</p>
|
||||||
<div class="form-errors hidden"></div>
|
<div class="form-errors hidden"></div>
|
||||||
|
|
||||||
${el.form_fields}
|
${formFieldTemplates}
|
||||||
|
|
||||||
<fieldset class="buttons form-group">
|
<fieldset class="buttons">
|
||||||
<input type="submit" class="btn btn-primary" value="${i18nOk}" />
|
<input type="submit" class="btn btn-primary" value="${i18nOk}" />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>`
|
</form>`
|
||||||
|
@ -63,7 +63,7 @@ function _tplChoice (el, currentPoll, choice, canVote) {
|
|||||||
<div class="livechat-progress-bar">
|
<div class="livechat-progress-bar">
|
||||||
<div
|
<div
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
style="width: ${percent}%;"
|
style=${'width: ' + percent + '%;'}
|
||||||
aria-valuenow="${percent}" aria-valuemin="0" aria-valuemax="100"
|
aria-valuenow="${percent}" aria-valuemin="0" aria-valuemax="100"
|
||||||
></div>
|
></div>
|
||||||
<p>
|
<p>
|
||||||
@ -83,21 +83,21 @@ export function tplPoll (el, currentPoll, canVote) {
|
|||||||
return html`<div class="${currentPoll.over ? 'livechat-poll-over' : ''}">
|
return html`<div class="${currentPoll.over ? 'livechat-poll-over' : ''}">
|
||||||
<p class="livechat-poll-question">
|
<p class="livechat-poll-question">
|
||||||
${currentPoll.over
|
${currentPoll.over
|
||||||
? html`<button class="livechat-poll-close" @click=${el.closePoll} title="${__('Close')}">
|
? html`<button type="button" class="livechat-poll-close" @click=${el.closePoll} title="${__('Close')}">
|
||||||
<converse-icon class="fa fa-times" size="1em"></converse-icon>
|
<converse-icon class="fa fa-times" size="1em"></converse-icon>
|
||||||
</button>`
|
</button>`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
${el.collapsed
|
${el.collapsed
|
||||||
? html`
|
? html`
|
||||||
<button @click=${el.toggle} class="livechat-poll-toggle">
|
<button type="button" @click=${el.toggle} class="livechat-poll-toggle">
|
||||||
<converse-icon
|
<converse-icon
|
||||||
color="var(--muc-toolbar-btn-color)"
|
color="var(--muc-toolbar-btn-color)"
|
||||||
class="fa fa-angle-right"
|
class="fa fa-angle-right"
|
||||||
size="1em"></converse-icon>
|
size="1em"></converse-icon>
|
||||||
</button>`
|
</button>`
|
||||||
: html`
|
: html`
|
||||||
<button @click=${el.toggle} class="livechat-poll-toggle">
|
<button type="button" @click=${el.toggle} class="livechat-poll-toggle">
|
||||||
<converse-icon
|
<converse-icon
|
||||||
color="var(--muc-toolbar-btn-color)"
|
color="var(--muc-toolbar-btn-color)"
|
||||||
class="fa fa-angle-down"
|
class="fa fa-angle-down"
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { XMLNS_POLL } from './constants.js'
|
import { XMLNS_POLL } from './constants.js'
|
||||||
import { _converse, api } from '../../../src/headless/core.js'
|
import { _converse, api } from '../../../src/headless/index.js'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
|
|
||||||
export function getHeadingButtons (view, buttons) {
|
export function getHeadingButtons (view, buttons) {
|
||||||
const muc = view.model
|
const muc = view.model
|
||||||
if (muc.get('type') !== _converse.CHATROOMS_TYPE) {
|
if (muc.get('type') !== _converse.constants.CHATROOMS_TYPE) {
|
||||||
// only on MUC.
|
// only on MUC.
|
||||||
return buttons
|
return buttons
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { _converse, converse, api } from '../../../src/headless/core.js'
|
import { _converse, converse, api } from '../../../src/headless/index.js'
|
||||||
|
|
||||||
|
let currentSize
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This plugin computes the available width of converse-root, and adds classes
|
* This plugin computes the available width of converse-root, and adds classes
|
||||||
@ -16,6 +18,27 @@ converse.plugins.add('livechat-converse-size', {
|
|||||||
dependencies: [],
|
dependencies: [],
|
||||||
|
|
||||||
initialize () {
|
initialize () {
|
||||||
|
Object.assign(api, {
|
||||||
|
livechat_size: {
|
||||||
|
current: () => {
|
||||||
|
return currentSize
|
||||||
|
},
|
||||||
|
width_is: (sizes) => {
|
||||||
|
if (!Array.isArray(sizes)) {
|
||||||
|
sizes = [sizes]
|
||||||
|
}
|
||||||
|
if (!currentSize) { return false }
|
||||||
|
return sizes.includes(currentSize.width)
|
||||||
|
},
|
||||||
|
height_is: (sizes) => {
|
||||||
|
if (!Array.isArray(sizes)) {
|
||||||
|
sizes = [sizes]
|
||||||
|
}
|
||||||
|
if (!currentSize) { return false }
|
||||||
|
return sizes.includes(currentSize.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
_converse.api.listen.on('connected', start)
|
_converse.api.listen.on('connected', start)
|
||||||
_converse.api.listen.on('reconnected', start)
|
_converse.api.listen.on('reconnected', start)
|
||||||
_converse.api.listen.on('disconnected', stop)
|
_converse.api.listen.on('disconnected', stop)
|
||||||
@ -42,6 +65,7 @@ function start () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stop () {
|
function stop () {
|
||||||
|
currentSize = undefined
|
||||||
rootResizeObserver.disconnect()
|
rootResizeObserver.disconnect()
|
||||||
const root = document.querySelector('converse-root')
|
const root = document.querySelector('converse-root')
|
||||||
if (root) {
|
if (root) {
|
||||||
@ -60,8 +84,9 @@ function handle (el) {
|
|||||||
|
|
||||||
el.setAttribute('livechat-converse-root-width', width)
|
el.setAttribute('livechat-converse-root-width', width)
|
||||||
el.setAttribute('livechat-converse-root-height', height)
|
el.setAttribute('livechat-converse-root-height', height)
|
||||||
api.trigger('livechatSizeChanged', {
|
currentSize = {
|
||||||
height: height,
|
height: height,
|
||||||
width: width
|
width: width
|
||||||
})
|
}
|
||||||
|
api.trigger('livechatSizeChanged', Object.assign({}, currentSize)) // cloning...
|
||||||
}
|
}
|
||||||
|
@ -2,36 +2,20 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { api } from '@converse/headless/core'
|
import { api } from '@converse/headless'
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
import { MUCApp } from '../../../shared/components/muc-app/index.js'
|
||||||
import { tplMUCTaskApp } from '../templates/muc-task-app.js'
|
import { tplMUCTaskApp } from '../templates/muc-task-app.js'
|
||||||
|
|
||||||
import '../styles/muc-task-app.scss'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Element to display the Task Application.
|
* Custom Element to display the Task Application.
|
||||||
*/
|
*/
|
||||||
export default class MUCTaskApp extends CustomElement {
|
export default class MUCTaskApp extends MUCApp {
|
||||||
static get properties () {
|
restoreSettingName = 'livechat_task_app_restore'
|
||||||
return {
|
sessionStorageRestoreKey = 'livechat-converse-task-app-show'
|
||||||
model: { type: Object, attribute: true }, // mucModel
|
|
||||||
show: { type: Boolean, attribute: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize () {
|
|
||||||
this.show = api.settings.get('livechat_task_app_restore') &&
|
|
||||||
(window.sessionStorage?.getItem?.('livechat-converse-task-app-show') === '1')
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return tplMUCTaskApp(this, this.model)
|
return tplMUCTaskApp(this, this.model)
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleApp () {
|
|
||||||
this.show = !this.show
|
|
||||||
window.sessionStorage?.setItem?.('livechat-converse-task-app-show', this.show ? '1' : '')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
api.elements.define('livechat-converse-muc-task-app', MUCTaskApp)
|
api.elements.define('livechat-converse-muc-task-app', MUCTaskApp)
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
import { CustomElement } from 'shared/components/element.js'
|
||||||
import { api } from '@converse/headless/core'
|
import { api } from '@converse/headless'
|
||||||
import tplMucTaskList from '../templates/muc-task-list'
|
import tplMucTaskList from '../templates/muc-task-list'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
|
|
||||||
|
@ -2,17 +2,14 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
import { api } from '@converse/headless'
|
||||||
import { api } from '@converse/headless/core'
|
|
||||||
import tplMucTaskLists from '../templates/muc-task-lists'
|
import tplMucTaskLists from '../templates/muc-task-lists'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
|
import { DraggablesCustomElement } from '../../../shared/components/draggables/index.js'
|
||||||
|
|
||||||
import '../styles/muc-task-lists.scss'
|
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 () {
|
static get properties () {
|
||||||
return {
|
return {
|
||||||
model: { type: Object, attribute: true },
|
model: { type: Object, attribute: true },
|
||||||
@ -27,42 +24,22 @@ export default class MUCTaskListsView extends CustomElement {
|
|||||||
return
|
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.
|
// Adding or removing a new task list: we must update.
|
||||||
this.listenTo(this.model, 'add', () => this.requestUpdate())
|
this.listenTo(this.model, 'add', () => this.requestUpdate())
|
||||||
this.listenTo(this.model, 'remove', () => this.requestUpdate())
|
this.listenTo(this.model, 'remove', () => this.requestUpdate())
|
||||||
this.listenTo(this.model, 'sort', () => this.requestUpdate())
|
this.listenTo(this.model, 'sort', () => this.requestUpdate())
|
||||||
|
|
||||||
this._handleDragStartBinded = this._handleDragStart.bind(this)
|
return super.initialize()
|
||||||
this._handleDragOverBinded = this._handleDragOver.bind(this)
|
|
||||||
this._handleDragLeaveBinded = this._handleDragLeave.bind(this)
|
|
||||||
this._handleDragEndBinded = this._handleDragEnd.bind(this)
|
|
||||||
this._handleDropBinded = this._handleDrop.bind(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return tplMucTaskLists(this, this.model)
|
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) {
|
async submitCreateTaskList (ev) {
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
|
|
||||||
@ -96,15 +73,7 @@ export default class MUCTaskListsView extends CustomElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getParentTaskEl (target) {
|
isATaskEl (target) {
|
||||||
return target.closest?.('livechat-converse-muc-task')
|
|
||||||
}
|
|
||||||
|
|
||||||
_getParentTaskOrTaskListEl (target) {
|
|
||||||
return target.closest?.('livechat-converse-muc-task, livechat-converse-muc-task-list')
|
|
||||||
}
|
|
||||||
|
|
||||||
_isATaskEl (target) {
|
|
||||||
return target.nodeName?.toLowerCase() === 'livechat-converse-muc-task'
|
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'
|
return target.nodeName?.toLowerCase() === 'livechat-converse-muc-task-list'
|
||||||
}
|
}
|
||||||
|
|
||||||
_isOnTopHalf (ev, taskEl) {
|
_dropDone (draggedEl, droppedOnEl, onTopHalf) {
|
||||||
const y = ev.clientY
|
super._dropDone(...arguments)
|
||||||
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 }
|
|
||||||
|
|
||||||
console.log('[livechat task drag&drop] Task dropped...')
|
console.log('[livechat task drag&drop] Task dropped...')
|
||||||
|
|
||||||
const task = this.currentDraggedTask.model
|
const task = draggedEl.model
|
||||||
|
|
||||||
let newOrder, targetTasklist
|
let newOrder, targetTasklist
|
||||||
if (this.isATaskListEl(droppedOntaskOrTaskListEl)) {
|
if (this.isATaskListEl(droppedOnEl)) {
|
||||||
// We dropped on a task list, we must add as first entry.
|
// We dropped on a task list, we must add as first entry.
|
||||||
newOrder = 0
|
newOrder = 0
|
||||||
|
|
||||||
targetTasklist = droppedOntaskOrTaskListEl.model
|
targetTasklist = droppedOnEl.model
|
||||||
if (task.get('list') !== targetTasklist.get('id')) {
|
if (task.get('list') !== targetTasklist.get('id')) {
|
||||||
console.log('[livechat task drag&drop] Changing task list...')
|
console.log('[livechat task drag&drop] Changing task list...')
|
||||||
task.set('list', targetTasklist.get('id'))
|
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')
|
console.log('[livechat task drag&drop] Task dropped on tasklist, but already first item, nothing to do')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if (this._isATaskEl(droppedOntaskOrTaskListEl)) {
|
} else if (this.isATaskEl(droppedOnEl)) {
|
||||||
// We dropped on a task, we must get its order (+1 if !onTopHalf)
|
// We dropped on a task, we must get its order (+1 if !onTopHalf)
|
||||||
const droppedOnTask = droppedOntaskOrTaskListEl.model
|
const droppedOnTask = droppedOnEl.model
|
||||||
if (task === droppedOnTask) {
|
if (task === droppedOnTask) {
|
||||||
// But of course, if dropped on itself there is nothing to do.
|
// 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')
|
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'))
|
task.set('list', droppedOnTask.get('list'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const topHalf = droppedOnEl.classList.contains('livechat-drag-top-half')
|
|
||||||
newOrder = droppedOnTask.get('order') ?? 0
|
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)) {
|
if (typeof newOrder !== 'number' || isNaN(newOrder)) {
|
||||||
console.error(
|
console.error(
|
||||||
@ -217,45 +132,7 @@ export default class MUCTaskListsView extends CustomElement {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof newOrder !== 'number' || isNaN(newOrder)) {
|
this._saveOrders(targetTasklist.getTasks(), task, 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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
import { CustomElement } from 'shared/components/element.js'
|
||||||
import { api } from '@converse/headless/core'
|
import { api } from '@converse/headless'
|
||||||
import { tplMucTask } from '../templates/muc-task'
|
import { tplMucTask } from '../templates/muc-task'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { _converse, converse } from '../../../src/headless/core.js'
|
import { _converse, converse } from '../../../src/headless/index.js'
|
||||||
import { ChatRoomTaskLists } from './task-lists.js'
|
import { ChatRoomTaskLists } from './task-lists.js'
|
||||||
import { ChatRoomTaskList } from './task-list.js'
|
import { ChatRoomTaskList } from './task-list.js'
|
||||||
import { ChatRoomTasks } from './tasks.js'
|
import { ChatRoomTasks } from './tasks.js'
|
||||||
@ -18,9 +18,14 @@ converse.plugins.add('livechat-converse-tasks', {
|
|||||||
dependencies: ['converse-muc', 'converse-disco', 'converse-pubsub'],
|
dependencies: ['converse-muc', 'converse-disco', 'converse-pubsub'],
|
||||||
|
|
||||||
initialize () {
|
initialize () {
|
||||||
_converse.ChatRoomTaskLists = ChatRoomTaskLists
|
Object.assign(
|
||||||
_converse.ChatRoomTaskList = ChatRoomTaskList
|
_converse.exports,
|
||||||
_converse.ChatRoomTasks = ChatRoomTasks
|
{
|
||||||
|
ChatRoomTaskLists,
|
||||||
|
ChatRoomTaskList,
|
||||||
|
ChatRoomTasks
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
_converse.api.settings.extend({
|
_converse.api.settings.extend({
|
||||||
livechat_task_app_enabled: false,
|
livechat_task_app_enabled: false,
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import BaseModal from 'plugins/modal/modal.js'
|
import BaseModal from 'plugins/modal/modal.js'
|
||||||
import tplPickTaskList from './templates/pick-task-list.js'
|
import tplPickTaskList from './templates/pick-task-list.js'
|
||||||
import { api } from '@converse/headless/core'
|
import { api } from '@converse/headless'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
|
|
||||||
export default class PickTaskListModal extends BaseModal {
|
export default class PickTaskListModal extends BaseModal {
|
||||||
|
@ -19,22 +19,22 @@ export default function (el) {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<form class="converse-form converse-form--modal confirm" action="#" @submit=${ev => el.onPick(ev)}>
|
<form class="converse-form converse-form--modal confirm" action="#" @submit=${ev => el.onPick(ev)}>
|
||||||
<div class="form-group">
|
<fieldset>
|
||||||
<select class="form-control" name="tasklist">
|
<select class="form-control" name="tasklist">
|
||||||
${
|
${
|
||||||
repeat(muc.tasklists, (tasklist) => tasklist.get('id'), (tasklist) => {
|
repeat(muc.tasklists, (tasklist) => tasklist.get('id'), (tasklist) => {
|
||||||
return html`<option value="${tasklist.get('id')}">${tasklist.get('name')}</option>`
|
return html`<option value="${tasklist.get('id')}">${tasklist.get('name')}</option>`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
<small class="form-text text-muted">
|
<small class="form-text text-muted">
|
||||||
${i18nMessage}
|
${i18nMessage}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</fieldset>
|
||||||
|
|
||||||
<div class="form-group">
|
<fieldset>
|
||||||
<button type="submit" class="btn btn-primary">${__('OK')}</button>
|
<button type="submit" class="btn btn-primary">${__('OK')}</button>
|
||||||
<input type="button" class="btn btn-secondary" data-dismiss="modal" value="${__('Cancel')}"/>
|
<input type="button" class="btn btn-secondary" data-dismiss="modal" value="${__('Cancel')}"/>
|
||||||
</div>
|
</fieldset>
|
||||||
</form>`
|
</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.
|
* A chat room task list.
|
||||||
* @class
|
* @class
|
||||||
* @namespace _converse.ChatRoomTaskList
|
* @namespace _converse.exports.ChatRoomTaskList
|
||||||
* @memberof _converse
|
* @memberof _converse
|
||||||
*/
|
*/
|
||||||
class ChatRoomTaskList extends Model {
|
class ChatRoomTaskList extends Model {
|
||||||
@ -40,7 +40,7 @@ class ChatRoomTaskList extends Model {
|
|||||||
|
|
||||||
data.list = this.get('id')
|
data.list = this.get('id')
|
||||||
if (!data.order) {
|
if (!data.order) {
|
||||||
data.order = 0 + Math.max(
|
data.order = 1 + Math.max(
|
||||||
0,
|
0,
|
||||||
...(this.getTasks().map(t => t.get('order') ?? 0).filter(o => !isNaN(o)))
|
...(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'
|
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
|
* @class
|
||||||
* @namespace _converse.ChatRoomTaskLists
|
* @namespace _converse.exports.ChatRoomTaskLists
|
||||||
* @memberOf _converse
|
* @memberOf _converse
|
||||||
*/
|
*/
|
||||||
class ChatRoomTaskLists extends Collection {
|
class ChatRoomTaskLists extends Collection {
|
||||||
|
@ -7,7 +7,7 @@ import { Model } from '@converse/skeletor/src/model.js'
|
|||||||
/**
|
/**
|
||||||
* A chat room task.
|
* A chat room task.
|
||||||
* @class
|
* @class
|
||||||
* @namespace _converse.ChatRoomTask
|
* @namespace _converse.exports.ChatRoomTask
|
||||||
* @memberof _converse
|
* @memberof _converse
|
||||||
*/
|
*/
|
||||||
class ChatRoomTask extends Model {
|
class ChatRoomTask extends Model {
|
||||||
|
@ -7,9 +7,9 @@ import { ChatRoomTask } from './task'
|
|||||||
import { initStorage } from '@converse/headless/utils/storage.js'
|
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
|
* @class
|
||||||
* @namespace _converse.ChatRoomTasks
|
* @namespace _converse.exports.ChatRoomTasks
|
||||||
* @memberOf _converse
|
* @memberOf _converse
|
||||||
*/
|
*/
|
||||||
class ChatRoomTasks extends Collection {
|
class ChatRoomTasks extends Collection {
|
||||||
|
@ -3,28 +3,24 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
|
import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
|
||||||
|
import { tplMUCApp } from '../../../shared/components/muc-app/templates/muc-app.js'
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
|
|
||||||
export function tplMUCTaskApp (el, mucModel) {
|
export function tplMUCTaskApp (el, mucModel) {
|
||||||
if (!mucModel) {
|
if (!mucModel) {
|
||||||
// should not happen
|
// should not happen
|
||||||
el.classList.add('hidden') // we must do this, otherwise will have CSS side effects
|
|
||||||
return html``
|
return html``
|
||||||
}
|
}
|
||||||
if (!mucModel.tasklists) {
|
if (!mucModel.tasklists) {
|
||||||
// too soon, not initialized yet (this will happen)
|
// too soon, not initialized yet (this will happen)
|
||||||
el.classList.add('hidden') // we must do this, otherwise will have CSS side effects
|
|
||||||
return html``
|
return html``
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!el.show) {
|
if (!el.show) {
|
||||||
el.classList.add('hidden')
|
|
||||||
return html``
|
return html``
|
||||||
}
|
}
|
||||||
|
|
||||||
el.classList.remove('hidden')
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
const i18nTasks = __(LOC_tasks)
|
const i18nTasks = __(LOC_tasks)
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
@ -33,19 +29,11 @@ export function tplMUCTaskApp (el, mucModel) {
|
|||||||
page: 'documentation/user/streamers/tasks'
|
page: 'documentation/user/streamers/tasks'
|
||||||
})
|
})
|
||||||
|
|
||||||
return html`
|
return tplMUCApp(
|
||||||
<div class="livechat-converse-muc-app-header">
|
el,
|
||||||
<h5>${i18nTasks}</h5>
|
i18nTasks,
|
||||||
<a href="${helpUrl}" target="_blank"><converse-icon
|
helpUrl,
|
||||||
class="fa fa-circle-question"
|
i18nHelp,
|
||||||
size="1em"
|
html`<livechat-converse-muc-task-lists .model=${mucModel.tasklists}></livechat-converse-muc-task-lists>`
|
||||||
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>`
|
|
||||||
}
|
}
|
||||||
|
@ -16,17 +16,17 @@ export default function tplMucTaskList (el, tasklist) {
|
|||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
const i18nTaskListName = __(LOC_task_list_name)
|
const i18nTaskListName = __(LOC_task_list_name)
|
||||||
return html`
|
return html`
|
||||||
<div class="task-list-line">
|
<div class="task-list-line draggables-line">
|
||||||
${el.collapsed
|
${el.collapsed
|
||||||
? html`
|
? html`
|
||||||
<button @click=${el.toggleTasks} class="task-list-toggle-tasks">
|
<button type="button" @click=${el.toggleTasks} class="task-list-toggle-tasks">
|
||||||
<converse-icon
|
<converse-icon
|
||||||
color="var(--muc-toolbar-btn-color)"
|
color="var(--muc-toolbar-btn-color)"
|
||||||
class="fa fa-angle-right"
|
class="fa fa-angle-right"
|
||||||
size="1em"></converse-icon>
|
size="1em"></converse-icon>
|
||||||
</button>`
|
</button>`
|
||||||
: html`
|
: html`
|
||||||
<button @click=${el.toggleTasks} class="task-list-toggle-tasks">
|
<button type="button" @click=${el.toggleTasks} class="task-list-toggle-tasks">
|
||||||
<converse-icon
|
<converse-icon
|
||||||
color="var(--muc-toolbar-btn-color)"
|
color="var(--muc-toolbar-btn-color)"
|
||||||
class="fa fa-angle-down"
|
class="fa fa-angle-down"
|
||||||
@ -38,15 +38,15 @@ export default function tplMucTaskList (el, tasklist) {
|
|||||||
<div class="task-list-name">
|
<div class="task-list-name">
|
||||||
<a @click=${el.toggleTasks}>${tasklist.get('name')}</a>
|
<a @click=${el.toggleTasks}>${tasklist.get('name')}</a>
|
||||||
</div>
|
</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>
|
<converse-icon class="fa fa-plus" size="1em"></converse-icon>
|
||||||
</button>
|
</button>
|
||||||
<button class="task-list-action" title="${__('Edit')}"
|
<button type="button" class="task-list-action" title="${__('Edit')}"
|
||||||
@click=${el.toggleEdit}
|
@click=${el.toggleEdit}
|
||||||
>
|
>
|
||||||
<converse-icon class="fa fa-edit" size="1em"></converse-icon>
|
<converse-icon class="fa fa-edit" size="1em"></converse-icon>
|
||||||
</button>
|
</button>
|
||||||
<button class="task-list-action" title="${i18nDelete}"
|
<button type="button" class="task-list-action" title="${i18nDelete}"
|
||||||
@click=${el.deleteTaskList}
|
@click=${el.deleteTaskList}
|
||||||
>
|
>
|
||||||
<converse-icon class="fa fa-trash-alt" size="1em"></converse-icon>
|
<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}>
|
<form class="converse-form" @submit=${el.submitCreateTaskList}>
|
||||||
<div class="form-group">
|
<fieldset>
|
||||||
<label>
|
<label>
|
||||||
${i18nCreateTaskList}
|
${i18nCreateTaskList}
|
||||||
<input type="text" value="" class="form-control" name="name" placeholder="${i18nTaskListName}" />
|
<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>`
|
: html`<div class="invalid-feedback d-block">${el.create_tasklist_error_message}</div>`
|
||||||
}
|
}
|
||||||
</div>
|
</fieldset>
|
||||||
</form>`
|
</form>`
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ export function tplMucTask (el, task) {
|
|||||||
const doneId = 'livechat-task-done-id-' + task.get('id')
|
const doneId = 'livechat-task-done-id-' + task.get('id')
|
||||||
return !el.edit
|
return !el.edit
|
||||||
? html`
|
? 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">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
id="${doneId}"
|
id="${doneId}"
|
||||||
@ -30,22 +30,22 @@ export function tplMucTask (el, task) {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="task-description">${task.get('description') ?? ''}</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}
|
@click=${el.toggleEdit}
|
||||||
>
|
>
|
||||||
<converse-icon class="fa fa-edit" size="1em"></converse-icon>
|
<converse-icon class="fa fa-edit" size="1em"></converse-icon>
|
||||||
</button>
|
</button>
|
||||||
<button class="task-action" title="${i18nDelete}"
|
<button type="button" class="task-action" title="${i18nDelete}"
|
||||||
@click=${el.deleteTask}
|
@click=${el.deleteTask}
|
||||||
>
|
>
|
||||||
<converse-icon class="fa fa-trash-alt" size="1em"></converse-icon>
|
<converse-icon class="fa fa-trash-alt" size="1em"></converse-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>`
|
</div>`
|
||||||
: html`
|
: html`
|
||||||
<div class="task-line">
|
<div class="task-line draggables-line">
|
||||||
<form class="converse-form" @submit=${el.saveTask}>
|
<form class="converse-form" @submit=${el.saveTask}>
|
||||||
${_tplTaskForm(task)}
|
${_tplTaskForm(task)}
|
||||||
<fieldset class="form-group">
|
<fieldset>
|
||||||
<input type="submit" class="btn btn-primary" value="${__('Ok')}" />
|
<input type="submit" class="btn btn-primary" value="${__('Ok')}" />
|
||||||
<input type="button" class="btn btn-secondary button-cancel"
|
<input type="button" class="btn btn-secondary button-cancel"
|
||||||
value="${__('Cancel')}" @click=${el.toggleEdit}
|
value="${__('Cancel')}" @click=${el.toggleEdit}
|
||||||
@ -61,7 +61,7 @@ function _tplTaskForm (task) {
|
|||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
const i18nTaskDesc = __(LOC_task_description)
|
const i18nTaskDesc = __(LOC_task_description)
|
||||||
|
|
||||||
return html`<fieldset class="form-group">
|
return html`<fieldset>
|
||||||
<input type="text" name="name"
|
<input type="text" name="name"
|
||||||
class="form-control" value="${task ? task.get('name') : ''}"
|
class="form-control" value="${task ? task.get('name') : ''}"
|
||||||
placeholder="${i18nTaskName}"
|
placeholder="${i18nTaskName}"
|
||||||
@ -80,7 +80,7 @@ export function tplMucAddTaskForm (tasklistEl, _tasklist) {
|
|||||||
return html`
|
return html`
|
||||||
<form class="task-list-add-task converse-form" @submit=${tasklistEl.submitAddTask}>
|
<form class="task-list-add-task converse-form" @submit=${tasklistEl.submitAddTask}>
|
||||||
${_tplTaskForm(undefined)}
|
${_tplTaskForm(undefined)}
|
||||||
<fieldset class="form-group">
|
<fieldset>
|
||||||
<input type="submit" class="btn btn-primary" value="${i18nOk}" />
|
<input type="submit" class="btn btn-primary" value="${i18nOk}" />
|
||||||
<input type="button" class="btn btn-secondary button-cancel"
|
<input type="button" class="btn btn-secondary button-cancel"
|
||||||
value="${i18nCancel}" @click=${tasklistEl.closeAddTaskForm}
|
value="${i18nCancel}" @click=${tasklistEl.closeAddTaskForm}
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
|
|
||||||
import { XMLNS_TASKLIST, XMLNS_TASK } from './constants.js'
|
import { XMLNS_TASKLIST, XMLNS_TASK } from './constants.js'
|
||||||
import { PubSubManager } from '../../shared/lib/pubsub-manager.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'
|
import { __ } from 'i18n'
|
||||||
|
|
||||||
export function getHeadingButtons (view, buttons) {
|
export function getHeadingButtons (view, buttons) {
|
||||||
const muc = view.model
|
const muc = view.model
|
||||||
if (muc.get('type') !== _converse.CHATROOMS_TYPE) {
|
if (muc.get('type') !== _converse.constants.CHATROOMS_TYPE) {
|
||||||
// only on MUC.
|
// only on MUC.
|
||||||
return buttons
|
return buttons
|
||||||
}
|
}
|
||||||
@ -74,8 +74,8 @@ function _initChatRoomTaskLists (mucModel) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mucModel.tasklists = new _converse.ChatRoomTaskLists(undefined, { chatroom: mucModel })
|
mucModel.tasklists = new _converse.exports.ChatRoomTaskLists(undefined, { chatroom: mucModel })
|
||||||
mucModel.tasks = new _converse.ChatRoomTasks(undefined, { chatroom: mucModel })
|
mucModel.tasks = new _converse.exports.ChatRoomTasks(undefined, { chatroom: mucModel })
|
||||||
|
|
||||||
mucModel.taskManager = new PubSubManager(
|
mucModel.taskManager = new PubSubManager(
|
||||||
mucModel.get('jid'),
|
mucModel.get('jid'),
|
||||||
@ -127,7 +127,7 @@ function _destroyChatRoomTaskLists (mucModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initOrDestroyChatRoomTaskLists (mucModel) {
|
export function initOrDestroyChatRoomTaskLists (mucModel) {
|
||||||
if (mucModel.get('type') !== _converse.CHATROOMS_TYPE) {
|
if (mucModel.get('type') !== _converse.constants.CHATROOMS_TYPE) {
|
||||||
// only on MUC.
|
// only on MUC.
|
||||||
return _destroyChatRoomTaskLists(mucModel)
|
return _destroyChatRoomTaskLists(mucModel)
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { CustomElement } from 'shared/components/element.js'
|
import { CustomElement } from 'shared/components/element.js'
|
||||||
import { api } from '@converse/headless/core'
|
import { api } from '@converse/headless'
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
import { __ } from 'i18n'
|
import { __ } from 'i18n'
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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'
|
import './components/muc-terms.js'
|
||||||
|
|
||||||
const { sizzle } = converse.env
|
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 */
|
/* eslint-disable max-len */
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
import tplIcons from '../../../src/shared/templates/icons.js'
|
import tplIcons from '../../../src/shared/components/templates/icons.js'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
// Here we are adding some additonal icons to ConverseJS defaults
|
// 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">
|
<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"/>
|
<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>
|
</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>
|
</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 {
|
.conversejs {
|
||||||
livechat-converse-muc-task-app {
|
.livechat-converse-muc-app {
|
||||||
border: var(--occupants-border-left);
|
border: var(--occupants-border-left);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
@ -42,8 +42,8 @@
|
|||||||
|
|
||||||
&[livechat-converse-root-width="small"],
|
&[livechat-converse-root-width="small"],
|
||||||
&[livechat-converse-root-width="medium"] {
|
&[livechat-converse-root-width="medium"] {
|
||||||
converse-muc-chatarea livechat-converse-muc-task-app:not(.hidden) ~ * {
|
converse-muc-chatarea .livechat-converse-muc-app:not(.hidden) ~ * {
|
||||||
// on small and medium width, we hide all subsequent siblings of the task app
|
// on small and medium width, we hide all subsequent siblings of the app
|
||||||
// (when app is not hidden)
|
// (when app is not hidden)
|
||||||
display: none !important;
|
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
|
// 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
|
const { $build, Strophe, $iq, sizzle } = converse.env
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,7 +50,7 @@ export class PubSubManager {
|
|||||||
async start () {
|
async start () {
|
||||||
// FIXME: handle errors. Find a way to display to user that this failed.
|
// 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) => {
|
(message) => {
|
||||||
try {
|
try {
|
||||||
this._handleMessage(message)
|
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.
|
// Note: no need to unsubscribe from the pubsub node, the backend will do when users leave the room.
|
||||||
|
|
||||||
if (this.stanzaHandler) {
|
if (this.stanzaHandler) {
|
||||||
_converse.connection.deleteHandler(this.stanzaHandler)
|
api.connection.get().deleteHandler(this.stanzaHandler)
|
||||||
this.stanzaHandler = undefined
|
this.stanzaHandler = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,6 +123,7 @@ export class PubSubManager {
|
|||||||
if (v === undefined) { continue }
|
if (v === undefined) { continue }
|
||||||
data[field] = v
|
data[field] = v
|
||||||
}
|
}
|
||||||
|
this._additionalModelToData(item, data)
|
||||||
|
|
||||||
console.log('Saving item...')
|
console.log('Saving item...')
|
||||||
await this._save(type, data, id)
|
await this._save(type, data, id)
|
||||||
@ -178,6 +179,8 @@ export class PubSubManager {
|
|||||||
item.c(fieldName).t(data[fieldName]).up()
|
item.c(fieldName).t(data[fieldName]).up()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._additionalDataToItemNode(data, item)
|
||||||
|
|
||||||
await api.pubsub.publish(this.roomJID, this.node, item)
|
await api.pubsub.publish(this.roomJID, this.node, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,6 +339,7 @@ export class PubSubManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this._additionalParseItemNode(itemNode, type, data)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,4 +355,19 @@ export class PubSubManager {
|
|||||||
_typeFromCollection (collection) {
|
_typeFromCollection (collection) {
|
||||||
return Object.values(this.types).find(type => type.collection === 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 { __ } from 'i18n'
|
||||||
import BaseModal from 'plugins/modal/modal.js'
|
import BaseModal from 'plugins/modal/modal.js'
|
||||||
import { api } from '@converse/headless/core'
|
import { api } from '@converse/headless'
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
import 'livechat-external-login-content.js'
|
import 'livechat-external-login-content.js'
|
||||||
|
|
||||||
@ -20,8 +20,8 @@ class ExternalLoginModal extends BaseModal {
|
|||||||
return __(LOC_login_using_external_account)
|
return __(LOC_login_using_external_account)
|
||||||
}
|
}
|
||||||
|
|
||||||
onHide () {
|
close () {
|
||||||
super.onHide()
|
super.close()
|
||||||
// kill the externalAuthGetResult handler if still there
|
// kill the externalAuthGetResult handler if still there
|
||||||
try {
|
try {
|
||||||
if (window.externalAuthGetResult) { window.externalAuthGetResult() }
|
if (window.externalAuthGetResult) { window.externalAuthGetResult() }
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
// Fixing all dropdown colors
|
// Fixing all dropdown colors
|
||||||
--text-color: #212529; // default bootstrap color for dropdown-items
|
--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
|
background-color: #fff; // this is the default bootstrap color, used by ConverseJS
|
||||||
|
|
||||||
@ -27,6 +27,7 @@
|
|||||||
border: 1px dashed var(--peertube-menu-background);
|
border: 1px dashed var(--peertube-menu-background);
|
||||||
color: var(--peertube-main-foreground);
|
color: var(--peertube-main-foreground);
|
||||||
background-color: var(--peertube-main-background);
|
background-color: var(--peertube-main-background);
|
||||||
|
margin: 0 5px;
|
||||||
|
|
||||||
.livechat-hide-slow-mode-info-box {
|
.livechat-hide-slow-mode-info-box {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -34,12 +34,16 @@ body.converse-fullscreen.theme-peertube,
|
|||||||
body.converse-embedded converse-root.theme-peertube {
|
body.converse-embedded converse-root.theme-peertube {
|
||||||
--foreground: var(--peertube-main-foreground);
|
--foreground: var(--peertube-main-foreground);
|
||||||
--background: var(--peertube-main-background);
|
--background: var(--peertube-main-background);
|
||||||
|
--badge-color: var(--background);
|
||||||
|
--button-hover-text-color: var(--background);
|
||||||
--subdued-color: #a8aba1;
|
--subdued-color: #a8aba1;
|
||||||
|
--muc-color: var(--peertube-button-background);
|
||||||
--green: #3aa569; // only in this file
|
--green: #3aa569; // only in this file
|
||||||
--redder-orange: #e77051; // only in this file
|
--redder-orange: #e77051; // only in this file
|
||||||
--orange: #e7a151; // only in this file
|
--orange: #e7a151; // only in this file
|
||||||
--light-blue: #578ea9; // only in this file
|
--light-blue: #578ea9; // only in this file
|
||||||
--lighter-blue: #85b47b; // 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-online: var(--green);
|
||||||
--chat-status-busy: var(--redder-orange);
|
--chat-status-busy: var(--redder-orange);
|
||||||
--chat-status-away: var(--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-shadow-color: var(--peertube-main-background); // FIXME: should be a little different from background
|
||||||
--text-color: var(--peertube-input-foreground);
|
--text-color: var(--peertube-input-foreground);
|
||||||
--controlbox-text-color: var(--peertube-input-foreground); // Note: controlbox is not used
|
--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-text-color: var(--peertube-input-foreground);
|
||||||
--message-receipt-color: var(--green);
|
--message-receipt-color: var(--green);
|
||||||
--save-button-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-correcting-color: var(--peertube-grey-background);
|
||||||
--chat-head-color-dark: #1e9652; // should not be used in this plugin
|
--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-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-color: var(--green);
|
||||||
--chat-head-text-color: var(--peertube-input-foreground);
|
--chat-head-text-color: var(--peertube-input-foreground);
|
||||||
--chat-toolbar-btn-color: var(--peertube-button-background);
|
--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-background-color: #333;
|
||||||
--controlbox-pane-bg-hover-color: #464646;
|
--controlbox-pane-bg-hover-color: #464646;
|
||||||
--panel-divider-color: #333;
|
--panel-divider-color: #333;
|
||||||
--chat-gutter: 0.5em;
|
|
||||||
--minimized-chats-width: 130px;
|
--minimized-chats-width: 130px;
|
||||||
--mobile-chat-width: 100%;
|
--mobile-chat-width: 100%;
|
||||||
--mobile-chat-height: 400px;
|
--mobile-chat-height: 400px;
|
||||||
@ -119,9 +120,10 @@ body.converse-embedded converse-root.theme-peertube {
|
|||||||
--chatroom-badge-color: var(--peertube-button-background);
|
--chatroom-badge-color: var(--peertube-button-background);
|
||||||
--chatroom-badge-hover-color: var(--peertube-button-background);
|
--chatroom-badge-hover-color: var(--peertube-button-background);
|
||||||
--chatroom-correcting-color: var(--peertube-grey-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-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-button-color: #999;
|
||||||
--chatroom-head-color: var(--peertube-menu-foreground);
|
--chatroom-head-color: var(--peertube-menu-foreground);
|
||||||
--chatroom-head-description-border-left: 1px solid #ddd;
|
--chatroom-head-description-border-left: 1px solid #ddd;
|
||||||
@ -163,6 +165,7 @@ body.converse-embedded converse-root.theme-peertube {
|
|||||||
--fullpage-chat-width: 100%;
|
--fullpage-chat-width: 100%;
|
||||||
--fullpage-emoji-picker-height: 300px;
|
--fullpage-emoji-picker-height: 300px;
|
||||||
--fullpage-max-chat-textarea-height: 15em;
|
--fullpage-max-chat-textarea-height: 15em;
|
||||||
|
--overlayed-chat-gutter: 1em;
|
||||||
--overlayed-chat-head-height: 55px;
|
--overlayed-chat-head-height: 55px;
|
||||||
--overlayed-chat-height: 450px;
|
--overlayed-chat-height: 450px;
|
||||||
--overlayed-chat-width: 300px;
|
--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 {
|
.livechat-viewer-mode-content {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ body.livechat-readonly.livechat-noscroll {
|
|||||||
gap: 0.5em 10px;
|
gap: 0.5em 10px;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
|
||||||
.form-group,
|
fieldset,
|
||||||
label {
|
label {
|
||||||
margin-bottom: 0 !important; // replaced by the gap on .livechat-viewer-mode-content
|
margin-bottom: 0 !important; // replaced by the gap on .livechat-viewer-mode-content
|
||||||
}
|
}
|
||||||
@ -171,7 +171,8 @@ body.converse-embedded {
|
|||||||
#peertube-plugin-livechat-container {
|
#peertube-plugin-livechat-container {
|
||||||
converse-muc-message-form {
|
converse-muc-message-form {
|
||||||
// For an unknown reason, message field in truncated... so adding a bottom margin.
|
// 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:
|
// So we must revert appearance:
|
||||||
appearance: revert !important;
|
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
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { html } from 'lit'
|
import { html } from 'lit'
|
||||||
import { api } from '@converse/headless/core.js'
|
import { api } from '@converse/headless/index.js'
|
||||||
|
|
||||||
export default () => html`
|
export default () => html`
|
||||||
<div class="inner-content converse-brand row">
|
<div class="inner-content converse-brand row">
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user