Using ChannelConfigurationElement + Adding TranslationDirective

and a lots of fixes
This commit is contained in:
Mehdi Benadel 2024-05-13 16:40:36 +02:00
parent 0fe9fb4dca
commit 9ea26dfd26
11 changed files with 420 additions and 117 deletions

View File

@ -61,6 +61,8 @@ declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL: s
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_DESC: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_LABEL: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_LABEL: string

View File

@ -4,8 +4,8 @@
import type { RegisterClientOptions } from '@peertube/peertube-types/client' import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import { renderConfigurationHome } from './templates/home' import { renderConfigurationHome } from './templates/home'
import { renderConfigurationChannel } from './templates/channel' import './templates/ChannelConfigurationElement'
import { render } from 'lit' import { html, render } from 'lit'
/** /**
* Registers stuff related to the user's configuration pages. * Registers stuff related to the user's configuration pages.
@ -29,7 +29,7 @@ async function registerConfiguration (clientOptions: RegisterClientOptions): Pro
onMount: async ({ rootEl }) => { onMount: async ({ rootEl }) => {
const urlParams = new URLSearchParams(window.location.search) const urlParams = new URLSearchParams(window.location.search)
const channelId = urlParams.get('channelId') ?? '' const channelId = urlParams.get('channelId') ?? ''
render(await renderConfigurationChannel(clientOptions, channelId, rootEl), rootEl) render(html`<channel-configuration .registerClientOptions=${clientOptions}></channel-configuration>`, rootEl)
} }
}) })

View File

@ -1,42 +1,146 @@
import { RegisterClientOptions } from '@peertube/peertube-types/client'
import { html, LitElement } from 'lit' import { html, LitElement } from 'lit'
import { repeat } from 'lit-html/directives/repeat.js' import { repeat } from 'lit-html/directives/repeat.js'
import { customElement, property } from 'lit/decorators.js' import { customElement, property } from 'lit/decorators.js'
import { ptTr } from './TranslationDirective'
import { localizedHelpUrl } from '../../../utils/help'
import './DynamicTableFormElement'
import './PluginConfigurationRow'
import { until } from 'async'
import { Task } from '@lit/task';
@customElement('channel-configuration') @customElement('channel-configuration')
export class ChannelConfigurationElement extends LitElement { export class ChannelConfigurationElement extends LitElement {
@property() @property({ attribute: false })
public list: string[] = ["foo", "bar", "baz"] public registerClientOptions: RegisterClientOptions | undefined
@property()
public newEl: string = 'change_me'
createRenderRoot = () => { createRenderRoot = () => {
return this return this
} }
render() { private _asyncTaskRender = new Task(this, {
return html`
<ul>
${repeat(this.list, (el: string, index) => html`<li>${el}<button @click=${this._removeFromList(index)}>remove</button></li>`
)}
<li><input .value=${this.newEl}/><button @click=${this._addToList(this.newEl)}>add</button></li>
</ul>
`
}
private _addToList(newEl: string) { task: async ([registerClientOptions], {signal}) => {
return () => { let link = registerClientOptions ? await localizedHelpUrl(registerClientOptions, { page: 'documentation/user/streamers/bot/forbidden_words' }) : '';
this.list.push(newEl)
this.requestUpdate('list')
}
}
private _removeFromList(index: number) { return {
return () => { url : new URL(link),
this.list.splice(index, 1) title: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)
this.requestUpdate('list')
} }
},
args: () => [this.registerClientOptions]
});
render = () => {
let tableHeader = {
words: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2)
},
regex: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC)
},
applyToModerators: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC)
},
label: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC)
},
reason: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC)
},
comments: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_DESC)
}
}
let tableSchema = {
words: {
inputType: 'text',
default: 'helloqwesad'
},
regex: {
inputType: 'text',
default: 'helloaxzca'
},
applyToModerators: {
inputType: 'checkbox',
default: true
},
label: {
inputType: 'text',
default: 'helloasx'
},
reason: {
inputType: 'select',
default: 'transphobia',
label: 'choose your poison',
options: {'racism': 'Racism', 'sexism': 'Sexism', 'transphobia': 'Transphobia', 'bigotry': 'Bigotry'}
},
comments: {
inputType: 'textarea',
default: `Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.`
},
}
let tableRows = [
{
words: 'teweqwst',
regex: 'tesdgst',
applyToModerators: false,
label: 'teswet',
reason: 'sexism',
comments: 'tsdaswest',
},
{
words: 'tedsadst',
regex: 'tezxccst',
applyToModerators: true,
label: 'tewest',
reason: 'racism',
comments: 'tesxzct',
},
{
words: 'tesadsdxst',
regex: 'dsfsdf',
applyToModerators: false,
label: 'tesdadst',
reason: 'bigotry',
comments: 'tsadest',
},
]
return this._asyncTaskRender.render({
complete: (helpLink) => html`
<div class="container">
<channel-configuration></channel-configuration>
<plugin-configuration-row
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)}
.helpLink=${helpLink}
>
<dynamic-table-form
.header=${tableHeader}
.schema=${tableSchema}
.rows=${tableRows}
.formName=${'forbidden-words'}
>
</dynamic-table-form>
</plugin-configuration-row>
</div>`
})
} }
} }

View File

@ -1,6 +1,9 @@
import { html, LitElement, TemplateResult } from 'lit' import { html, LitElement, TemplateResult } from 'lit'
import { repeat } from 'lit/directives/repeat.js' import { repeat } from 'lit/directives/repeat.js'
import { customElement, property, state } from 'lit/decorators.js' import { customElement, property, state } from 'lit/decorators.js'
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { StaticValue, unsafeStatic } from 'lit/static-html.js'
type DynamicTableAcceptedTypes = number | string | boolean | Date type DynamicTableAcceptedTypes = number | string | boolean | Date
@ -41,7 +44,7 @@ interface CellDataSchema {
export class DynamicTableFormElement extends LitElement { export class DynamicTableFormElement extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public header: { [key : string]: TemplateResult<1> } = {} public header: { [key : string]: { colName: TemplateResult, description: TemplateResult } } = {}
@property({ attribute: false }) @property({ attribute: false })
@ -87,55 +90,51 @@ export class DynamicTableFormElement extends LitElement {
render = () => { render = () => {
const inputId = `peertube-livechat-${this.formName.replaceAll('_','-')}-table` const inputId = `peertube-livechat-${this.formName.replaceAll('_','-')}-table`
for(let row of this.rows) {
if (!row._id) {
row._id = this._lastRowId++
}
}
return html` return html`
<div class="row mt-5">
<div class="col-12 col-lg-4 col-xl-3">
<h2>Bot command #1</h2>
<p>You can configure the bot to respond to commands. A command is a message starting with a "!", like for example "!help" that calls the "help" command. For more information about how to configure this feature, please refer to the documentation by clicking on the help button.</p>
<a href="https://livingston.frama.io/peertube-plugin-livechat/documentation/user/streamers/bot/commands/" target="_blank" title="Online help" class="orange-button peertube-button-link">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 4.233 4.233">
<path style="display:inline;opacity:.998;fill:none;fill-opacity:1;stroke:currentColor;stroke-width:.529167;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M1.48 1.583V.86c0-.171.085-.31.19-.31h.893c.106 0 .19.139.19.31v.838c0 .171-.107.219-.19.284l-.404.314c-.136.106-.219.234-.221.489l-.003.247"></path>
<path style="display:inline;fill:currentColor;stroke-width:.235169" d="M1.67 3.429h.529v.597H1.67z"></path>
</svg>
</a>
</div>
<div class="col-12 col-lg-8 col-xl-9">
<table class="table table-striped table-hover table-sm" id=${inputId}> <table class="table table-striped table-hover table-sm" id=${inputId}>
${this._renderHeader()} ${this._renderHeader()}
<tbody> <tbody>
${repeat(this.rows, this._renderDataRow)} ${repeat(this.rows,(row) => row._id, this._renderDataRow)}
</tbody> </tbody>
<tfoot> <tfoot>
<tr><td><button @click=${this._addRow}>Add Row</button></td></tr> <tr><td><button @click=${this._addRow}>Add Row</button></td></tr>
</tfoot> </tfoot>
</table> </table>
</div>
</div>
${JSON.stringify(this.rows)}
` `
} }
private _renderHeader = () => { private _renderHeader = () => {
return html`<thead><tr><th scope="col">#</th>${Object.values(this.header).map(this._renderHeaderCell)}<th scope="col">Remove Row</th></tr></thead>` return html`<thead>
<tr>
<!-- <th scope="col">#</th> -->
${Object.values(this.header).map(this._renderHeaderCell)}
<th scope="col">Remove Row</th>
</tr>
</thead>`
} }
private _renderHeaderCell = (headerCellData: TemplateResult<1> | any) => { private _renderHeaderCell = (headerCellData: { colName: TemplateResult, description: TemplateResult }) => {
return html`<th scope="col">${headerCellData}</th>` return html`<th scope="col">
<div data-toggle="tooltip" data-placement="bottom" data-html="true" title="${headerCellData.description}">${headerCellData.colName}</div>
</th>`
} }
private _renderDataRow = (rowData: { _id: number; [key : string]: DynamicTableAcceptedTypes }) => { private _renderDataRow = (rowData: { _id: number; [key : string]: DynamicTableAcceptedTypes }) => {
if (!rowData._id) {
rowData._id = this._lastRowId++
}
const inputId = `peertube-livechat-${this.formName.replaceAll('_','-')}-row-${rowData._id}` const inputId = `peertube-livechat-${this.formName.replaceAll('_','-')}-row-${rowData._id}`
return html`<tr id=${inputId}><td class="form-group">${rowData._id}</td>${repeat(Object.entries(rowData).filter(([k,v]) => k != '_id'), (data) => this.renderDataCell(data, rowData._id))}<td class="form-group"><button @click=${() => this._removeRow(rowData._id)}>Remove</button></td></tr>` return html`<tr id=${inputId}>
<!-- <td class="form-group">${rowData._id}</td> -->
${Object.entries(rowData).filter(([k,v]) => k != '_id').map((data) => this.renderDataCell(data, rowData._id))}
<td class="form-group"><button @click=${() => this._removeRow(rowData._id)}>Remove</button></td>
</tr>`
} }
@ -175,12 +174,12 @@ export class DynamicTableFormElement extends LitElement {
name=${inputName} name=${inputName}
class="form-control" class="form-control"
id=${inputId} id=${inputId}
min=${propertySchema?.min} min=${ifDefined(propertySchema?.min)}
max=${propertySchema?.max} max=${ifDefined(propertySchema?.max)}
minlength=${propertySchema?.minlength} minlength=${ifDefined(propertySchema?.minlength)}
maxlength=${propertySchema?.maxlength} maxlength=${ifDefined(propertySchema?.maxlength)}
@oninput=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)} @input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
.value=${propertyValue} .value=${propertyValue as string}
/>` />`
break break
@ -189,19 +188,26 @@ export class DynamicTableFormElement extends LitElement {
name=${inputName} name=${inputName}
class="form-control" class="form-control"
id=${inputId} id=${inputId}
min=${propertySchema?.min} min=${ifDefined(propertySchema?.min)}
max=${propertySchema?.max} max=${ifDefined(propertySchema?.max)}
minlength=${propertySchema?.minlength} minlength=${ifDefined(propertySchema?.minlength)}
maxlength=${propertySchema?.maxlength} maxlength=${ifDefined(propertySchema?.maxlength)}
@oninput=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)} @input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
.value=${propertyValue} .value=${propertyValue as string}
></textarea>` ></textarea>`
break break
case 'select': case 'select':
formElement = html`<select class="form-select" aria-label="Default select example"> formElement = html`<select
class="form-select"
aria-label="Default select example"
@change=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
>
<option ?selected=${!propertyValue}>${propertySchema?.label ?? 'Choose your option'}</option> <option ?selected=${!propertyValue}>${propertySchema?.label ?? 'Choose your option'}</option>
${Object.entries(propertySchema?.options ?? {})?.map(([value,name]) => html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`)} ${Object.entries(propertySchema?.options ?? {})
?.map(([value,name]) =>
html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`
)}
</select>` </select>`
break break
@ -222,12 +228,12 @@ export class DynamicTableFormElement extends LitElement {
name=${inputName} name=${inputName}
class="form-control" class="form-control"
id=${inputId} id=${inputId}
min=${propertySchema?.min} min=${ifDefined(propertySchema?.min)}
max=${propertySchema?.max} max=${ifDefined(propertySchema?.max)}
minlength=${propertySchema?.minlength} minlength=${ifDefined(propertySchema?.minlength)}
maxlength=${propertySchema?.maxlength} maxlength=${ifDefined(propertySchema?.maxlength)}
@oninput=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)} @input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
.value=${propertyValue} .value=${(propertyValue as Date).toISOString()}
/>` />`
break break
@ -246,12 +252,12 @@ export class DynamicTableFormElement extends LitElement {
name=${inputName} name=${inputName}
class="form-control" class="form-control"
id=${inputId} id=${inputId}
min=${propertySchema?.min} min=${ifDefined(propertySchema?.min)}
max=${propertySchema?.max} max=${ifDefined(propertySchema?.max)}
minlength=${propertySchema?.minlength} minlength=${ifDefined(propertySchema?.minlength)}
maxlength=${propertySchema?.maxlength} maxlength=${ifDefined(propertySchema?.maxlength)}
@oninput=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)} @input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
.value=${propertyValue} .value=${propertyValue as String}
/>` />`
break break
@ -269,9 +275,9 @@ export class DynamicTableFormElement extends LitElement {
name=${inputName} name=${inputName}
class="form-check-input" class="form-check-input"
id=${inputId} id=${inputId}
@oninput=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)} @input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)}
value="" .value=${propertyValue as String}
?checked=${propertyValue} ?checked=${propertyValue as Boolean}
/>` />`
break break
@ -281,24 +287,26 @@ export class DynamicTableFormElement extends LitElement {
} }
if (!formElement) { if (!formElement) {
console.warn(`value type '${propertyValue.constructor}' is incompatible with field type '${propertySchema.inputType}' for form entry '${propertyName.toString()}'.`) console.warn(`value type '${propertyValue.constructor}' is incompatible`
+ `with field type '${propertySchema.inputType}' for form entry '${propertyName.toString()}'.`)
} }
console.log
return html`<td class="form-group">${formElement}</td>` return html`<td class="form-group">${formElement}</td>`
} }
_updatePropertyFromValue(event: InputEvent, propertyName: string, rowId : number) { _updatePropertyFromValue(event: Event, propertyName: string, rowId : number) {
let target = event?.target as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement) let target = event?.target as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)
let value = (target && target instanceof HTMLInputElement && target.type == "checkbox") ? !!(target?.checked) : target?.value
if(target?.value) { if(value !== undefined) {
for(let row of this.rows) { for(let row of this.rows) {
if(row._id === rowId) { if(row._id === rowId) {
row[propertyName] = target?.value row[propertyName] = value
this.requestUpdate('rows')
return return
} }

View File

@ -0,0 +1,41 @@
import { html, LitElement } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import { unsafeSVG } from 'lit/directives/unsafe-svg.js'
import { StaticValue } from 'lit/static-html.js'
import { helpButtonSVG } from '../../../videowatch/buttons'
@customElement('plugin-configuration-row')
export class PLuginConfigurationRow extends LitElement {
@property({ attribute: false })
public title: string = `title`
@property({ attribute: false })
public description: string = `Here's a description`
@property({ attribute: false })
public helpLink: { url: URL, title: string } = { url : new URL('https://lmddgtfy.net/'), title: 'Online Help'}
createRenderRoot = () => {
return this
}
render() {
return html`
<div class="row mt-5">
<div class="col-12 col-lg-4 col-xl-3">
<h2>${this.title}</h2>
<p>${this.description}</p>
<a
href="${this.helpLink.url.href}"
target=_blank
title="${this.helpLink.title}"
class="orange-button peertube-button-link"
>${unsafeSVG(helpButtonSVG())}</a>
</div>
<div class="col-12 col-lg-8 col-xl-9">
<slot><p>Nothing in this row.</p></slot>
</div>
</div>`
}
}

View File

@ -0,0 +1,41 @@
import { PartInfo, directive } from 'lit/directive.js'
import { AsyncDirective } from 'lit/async-directive.js'
import { RegisterClientHelpers } from '@peertube/peertube-types/client';
export class TranslationDirective extends AsyncDirective {
private _peertubeHelpers?: RegisterClientHelpers
private _translatedValue : string = ''
private _localizationId : string = ''
constructor(partInfo: PartInfo) {
super(partInfo);
//_peertubeOptionsPromise.then((options) => this._peertubeHelpers = options.peertubeHelpers)
}
override render = (locId: string) => {
this._localizationId = locId // TODO Check current component for context (to infer the prefix)
if (this._translatedValue === '') {
this._translatedValue = locId
}
this._asyncUpdateTranslation()
return this._translatedValue
}
_asyncUpdateTranslation = async () => {
let newValue = await this._peertubeHelpers?.translate(this._localizationId) ?? ''
if (newValue !== '' && newValue !== this._translatedValue) {
this._translatedValue = newValue
this.setValue(newValue)
}
}
}
export const ptTr = directive(TranslationDirective)

View File

@ -2,16 +2,20 @@
// //
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { RegisterClientOptions } from '@peertube/peertube-types/client' import type { RegisterClientHelpers, RegisterClientOptions } from '@peertube/peertube-types/client'
import { localizedHelpUrl } from '../../../utils/help' import { localizedHelpUrl } from '../../../utils/help'
import { helpButtonSVG } from '../../../videowatch/buttons' import { helpButtonSVG } from '../../../videowatch/buttons'
import { getConfigurationChannelViewData } from './logic/channel' import { getConfigurationChannelViewData } from './logic/channel'
import { TemplateResult, html } from 'lit' import { TemplateResult, html } from 'lit'
import { unsafeHTML } from 'lit/directives/unsafe-html.js' import { unsafeHTML } from 'lit/directives/unsafe-html.js'
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
// Must use require for mustache, import seems buggy. // Must use require for mustache, import seems buggy.
const Mustache = require('mustache') const Mustache = require('mustache')
import './DynamicTableFormElement' import './DynamicTableFormElement'
import './ChannelConfigurationElement' import './ChannelConfigurationElement'
import './PluginConfigurationRow'
import { ptTr } from './TranslationDirective'
/** /**
* Renders the configuration settings page for a given channel, * Renders the configuration settings page for a given channel,
@ -26,6 +30,8 @@ async function renderConfigurationChannel (
channelId: string, channelId: string,
rootEl: HTMLElement rootEl: HTMLElement
): Promise<TemplateResult> { ): Promise<TemplateResult> {
const peertubeHelpers = registerClientOptions.peertubeHelpers
try { try {
const view : {[key: string] : any} = await getConfigurationChannelViewData(registerClientOptions, channelId) const view : {[key: string] : any} = await getConfigurationChannelViewData(registerClientOptions, channelId)
await fillViewHelpButtons(registerClientOptions, view) await fillViewHelpButtons(registerClientOptions, view)
@ -34,12 +40,30 @@ async function renderConfigurationChannel (
//await vivifyConfigurationChannel(registerClientOptions, rootEl, channelId) //await vivifyConfigurationChannel(registerClientOptions, rootEl, channelId)
let tableHeader = { let tableHeader = {
words: html`${view.forbiddenWords}<div data-toggle="tooltip" data-placement="bottom" data-html="true" title=${view.forbiddenWordsDesc}></div>`, words: {
regex: html`${view.forbiddenWordsRegexp}<div data-toggle="tooltip" data-placement="bottom" data-html="true" title=${view.forbiddenWordsRegexpDesc}></div>`, colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL),
applyToModerators: html`${view.forbiddenWordsApplyToModerators}<div data-toggle="tooltip" data-placement="bottom" data-html="true" title=${view.forbiddenWordsApplyToModeratorsDesc}></div>`, description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2)
label: html`${view.forbiddenWordsLabel}<div data-toggle="tooltip" data-placement="bottom" data-html="true" title=${view.forbiddenWordsLabelDesc}></div>`, },
reason: html`${view.forbiddenWordsReason}<div data-toggle="tooltip" data-placement="bottom" data-html="true" title=${view.forbiddenWordsReasonDesc}></div>`, regex: {
comments: html`${view.forbiddenWordsComments}<div data-toggle="tooltip" data-placement="bottom" data-html="true" title=${view.forbiddenWordsCommentsDesc}></div>`, colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC)
},
applyToModerators: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC)
},
label: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL_DESC)
},
reason: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC)
},
comments: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_DESC)
}
} }
let tableSchema = { let tableSchema = {
words: { words: {
@ -102,19 +126,30 @@ async function renderConfigurationChannel (
}, },
] ]
return html`${unsafeHTML(Mustache.render(MUSTACHE_CONFIGURATION_CHANNEL, view))} let helpLink = {
url : new URL(await localizedHelpUrl(registerClientOptions, { page: 'documentation/user/streamers/bot/forbidden_words' })),
title: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)
}
return html`
<div class="container"> <div class="container">
<channel-configuration></channel-configuration> <channel-configuration></channel-configuration>
<plugin-configuration-row
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)}
.helpLink=${helpLink}
>
<dynamic-table-form <dynamic-table-form
.header=${tableHeader} .header=${tableHeader}
.schema=${tableSchema} .schema=${tableSchema}
.rows=${tableRows} .rows=${tableRows}
.formName=${'dynamic-table-form'} .formName=${'forbidden-words'}
> >
</dynamic-table-form> </dynamic-table-form>
</div>${JSON.stringify(tableRows)}` </plugin-configuration-row>
</div>`
} catch (err: any) { } catch (err: any) {
registerClientOptions.peertubeHelpers.notifier.error(err.toString()) peertubeHelpers.notifier.error(err.toString())
return html`` return html``
} }
} }
@ -150,7 +185,8 @@ async function fillLabels (
registerClientOptions: RegisterClientOptions, registerClientOptions: RegisterClientOptions,
view: {[key: string] : string} view: {[key: string] : string}
): Promise<void> { ): Promise<void> {
const { peertubeHelpers } = registerClientOptions const peertubeHelpers = registerClientOptions.peertubeHelpers
view.title = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TITLE) view.title = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TITLE)
view.description = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_DESC) view.description = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_DESC)

View File

@ -5,7 +5,10 @@
import type { RegisterClientOptions } from '@peertube/peertube-types/client' import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import { localizedHelpUrl } from '../../../utils/help' import { localizedHelpUrl } from '../../../utils/help'
import { helpButtonSVG } from '../../../videowatch/buttons' import { helpButtonSVG } from '../../../videowatch/buttons'
import { TemplateResult, html } from 'lit'; import { TemplateResult, html } from 'lit'
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
import { ptTr } from './TranslationDirective'
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
interface HomeViewData { interface HomeViewData {
title: string title: string
@ -86,9 +89,9 @@ async function _fillViewHelpButtons ( // TODO: refactor with the similar functio
return html`<a return html`<a
href="${helpUrl}" href="${helpUrl}"
target=_blank target=_blank
title="${title}" title="${ptTr(LOC_ONLINE_HELP)}"
class="orange-button peertube-button-link" class="orange-button peertube-button-link"
>${helpIcon}</a>` >${unsafeHTML(helpIcon)}</a>`
} }
return button('documentation/user/streamers/channel') return button('documentation/user/streamers/channel')

View File

@ -398,6 +398,8 @@ livechat_configuration_channel_forbidden_words_reason_label: "Reason"
livechat_configuration_channel_forbidden_words_reason_desc: "Reason to display besides deleted messages" livechat_configuration_channel_forbidden_words_reason_desc: "Reason to display besides deleted messages"
livechat_configuration_channel_forbidden_words_regexp_label: "Consider as regular expressions" livechat_configuration_channel_forbidden_words_regexp_label: "Consider as regular expressions"
livechat_configuration_channel_forbidden_words_regexp_desc: "By checking this option, you can use regular expressions." livechat_configuration_channel_forbidden_words_regexp_desc: "By checking this option, you can use regular expressions."
livechat_configuration_channel_forbidden_words_label_label: "Label"
livechat_configuration_channel_forbidden_words_label_desc: "Label for this forbidden words rule"
livechat_configuration_channel_forbidden_words_applytomoderators_label: "Also moderate messages from moderators" livechat_configuration_channel_forbidden_words_applytomoderators_label: "Also moderate messages from moderators"
livechat_configuration_channel_forbidden_words_applytomoderators_desc: | livechat_configuration_channel_forbidden_words_applytomoderators_desc: |
By default, moderator messages will not be deleted when containing forbidden words. By default, moderator messages will not be deleted when containing forbidden words.

63
package-lock.json generated
View File

@ -9,6 +9,8 @@
"version": "10.0.2", "version": "10.0.2",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@lit/context": "^1.1.1",
"@lit/task": "^1.0.0",
"@xmpp/jid": "^0.13.1", "@xmpp/jid": "^0.13.1",
"async": "^3.2.2", "async": "^3.2.2",
"decache": "^4.6.0", "decache": "^4.6.0",
@ -18,6 +20,7 @@
"lit": "^3.1.3", "lit": "^3.1.3",
"log-rotate": "^0.2.8", "log-rotate": "^0.2.8",
"openid-client": "^5.6.5", "openid-client": "^5.6.5",
"rxjs": "^7.8.1",
"validate-color": "^2.2.1", "validate-color": "^2.2.1",
"xmppjs-chat-bot": "^0.3.0" "xmppjs-chat-bot": "^0.3.0"
}, },
@ -2670,6 +2673,14 @@
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz",
"integrity": "sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==" "integrity": "sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g=="
}, },
"node_modules/@lit/context": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.1.tgz",
"integrity": "sha512-q/Rw7oWSJidUP43f/RUPwqZ6f5VlY8HzinTWxL/gW1Hvm2S5q2hZvV+qM8WFcC+oLNNknc3JKsd5TwxLk1hbdg==",
"dependencies": {
"@lit/reactive-element": "^1.6.2 || ^2.0.0"
}
},
"node_modules/@lit/reactive-element": { "node_modules/@lit/reactive-element": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz",
@ -2678,6 +2689,14 @@
"@lit-labs/ssr-dom-shim": "^1.2.0" "@lit-labs/ssr-dom-shim": "^1.2.0"
} }
}, },
"node_modules/@lit/task": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@lit/task/-/task-1.0.0.tgz",
"integrity": "sha512-7jocGBh3yGlo3kKxQggZph2txK4X5GYNWp2FAsmV9u2spzUypwrzRzXe8I72icAb02B00+k2nlvxVcrQB6vyrw==",
"dependencies": {
"@lit/reactive-element": "^1.0.0 || ^2.0.0"
}
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz",
@ -10516,6 +10535,19 @@
"integrity": "sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==", "integrity": "sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==",
"dev": true "dev": true
}, },
"node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/rxjs/node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/safe-array-concat": { "node_modules/safe-array-concat": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz",
@ -14289,6 +14321,14 @@
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz",
"integrity": "sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==" "integrity": "sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g=="
}, },
"@lit/context": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.1.tgz",
"integrity": "sha512-q/Rw7oWSJidUP43f/RUPwqZ6f5VlY8HzinTWxL/gW1Hvm2S5q2hZvV+qM8WFcC+oLNNknc3JKsd5TwxLk1hbdg==",
"requires": {
"@lit/reactive-element": "^1.6.2 || ^2.0.0"
}
},
"@lit/reactive-element": { "@lit/reactive-element": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz",
@ -14297,6 +14337,14 @@
"@lit-labs/ssr-dom-shim": "^1.2.0" "@lit-labs/ssr-dom-shim": "^1.2.0"
} }
}, },
"@lit/task": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@lit/task/-/task-1.0.0.tgz",
"integrity": "sha512-7jocGBh3yGlo3kKxQggZph2txK4X5GYNWp2FAsmV9u2spzUypwrzRzXe8I72icAb02B00+k2nlvxVcrQB6vyrw==",
"requires": {
"@lit/reactive-element": "^1.0.0 || ^2.0.0"
}
},
"@msgpackr-extract/msgpackr-extract-darwin-arm64": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz",
@ -20289,6 +20337,21 @@
"integrity": "sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==", "integrity": "sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==",
"dev": true "dev": true
}, },
"rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"requires": {
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
}
}
},
"safe-array-concat": { "safe-array-concat": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz",

View File

@ -33,6 +33,8 @@
"dist/assets/styles/configuration.css" "dist/assets/styles/configuration.css"
], ],
"dependencies": { "dependencies": {
"@lit/context": "^1.1.1",
"@lit/task": "^1.0.0",
"@xmpp/jid": "^0.13.1", "@xmpp/jid": "^0.13.1",
"async": "^3.2.2", "async": "^3.2.2",
"decache": "^4.6.0", "decache": "^4.6.0",
@ -42,6 +44,7 @@
"lit": "^3.1.3", "lit": "^3.1.3",
"log-rotate": "^0.2.8", "log-rotate": "^0.2.8",
"openid-client": "^5.6.5", "openid-client": "^5.6.5",
"rxjs": "^7.8.1",
"validate-color": "^2.2.1", "validate-color": "^2.2.1",
"xmppjs-chat-bot": "^0.3.0" "xmppjs-chat-bot": "^0.3.0"
}, },