// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com> // // SPDX-License-Identifier: AGPL-3.0-only import type { RegisterClientOptions } from '@peertube/peertube-types/client' import type { ChannelConfiguration } from 'shared/lib/types' import { TemplateResult, html, nothing } from 'lit' import { customElement, property, state } from 'lit/decorators.js' import { ptTr } from '../../lib/directives/translation' import { Task } from '@lit/task' import { ChannelDetailsService } from '../services/channel-details' import { provide } from '@lit/context' import { channelConfigurationContext, channelDetailsServiceContext } from '../contexts/channel' import { registerClientOptionsContext } from '../../lib/contexts/peertube' import { LivechatElement } from '../../lib/elements/livechat' import { ValidationError, ValidationErrorType } from '../../lib/models/validation' import { classMap } from 'lit/directives/class-map.js' @customElement('livechat-channel-configuration') export class ChannelConfigurationElement extends LivechatElement { @provide({ context: registerClientOptionsContext }) @property({ attribute: false }) public registerClientOptions?: RegisterClientOptions @property({ attribute: false }) public channelId?: number @provide({ context: channelConfigurationContext }) @state() public _channelConfiguration?: ChannelConfiguration @provide({ context: channelDetailsServiceContext }) private _channelDetailsService?: ChannelDetailsService @state() public _validationError?: ValidationError private readonly _asyncTaskRender = new Task(this, { task: async ([registerClientOptions]) => { if (registerClientOptions) { this._channelDetailsService = new ChannelDetailsService(registerClientOptions) this._channelConfiguration = await this._channelDetailsService.fetchConfiguration(this.channelId ?? 0) } }, args: () => [this.registerClientOptions] }) private readonly _saveConfig = (event?: Event): void => { event?.preventDefault() if (this._channelDetailsService && this._channelConfiguration) { this._channelDetailsService.saveOptions(this._channelConfiguration.channel.id, this._channelConfiguration.configuration) .then(() => { this._validationError = undefined this.registerClientOptions ?.peertubeHelpers.notifier.info('Livechat configuration has been properly updated.') this.requestUpdate('_validationError') }) .catch((error: ValidationError) => { this._validationError = error console.warn(`A validation error occurred in saving configuration. ${error.name}: ${error.message}`) this.registerClientOptions ?.peertubeHelpers.notifier.error( `An error occurred. ${(error.message) ? `${error.message}` : ''}`) this.requestUpdate('_validationError') }) } } private 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 }) : {} } private readonly _renderFeedback = (feedbackId: string, propertyName: string): TemplateResult | typeof nothing => { const errorMessages: TemplateResult[] = [] const validationErrorTypes: ValidationErrorType[] | undefined = this._validationError?.properties[`${propertyName}`] ?? undefined if (validationErrorTypes && validationErrorTypes.length !== 0) { if (validationErrorTypes.includes(ValidationErrorType.WrongType)) { errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_WRONG_TYPE)}`) } if (validationErrorTypes.includes(ValidationErrorType.WrongFormat)) { errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_WRONG_FORMAT)}`) } if (validationErrorTypes.includes(ValidationErrorType.NotInRange)) { errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_NOT_IN_RANGE)}`) } return html`<div id=${feedbackId} class="invalid-feedback">${errorMessages}</div>` } else { return nothing } } protected override render = (): unknown => { const tableHeaderList = { forbiddenWords: { entries: { colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL), description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2) }, regexp: { 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) } }, quotes: { messages: { colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL2), description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC2) }, delay: { colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DELAY_LABEL), description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DELAY_DESC) } }, commands: { command: { colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_CMD_LABEL), description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_CMD_DESC) }, message: { colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_MESSAGE_LABEL), description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_MESSAGE_DESC) } } } const tableSchema = { forbiddenWords: { entries: { inputType: 'tags', default: [], separators: ['\n', '\t', ';'] }, regexp: { inputType: 'checkbox', default: false }, applyToModerators: { inputType: 'checkbox', default: false }, label: { inputType: 'text', default: '' }, reason: { inputType: 'text', default: '', datalist: [] }, comments: { inputType: 'textarea', default: '' } }, quotes: { messages: { inputType: 'tags', default: [], separators: ['\n', '\t', ';'] }, delay: { inputType: 'number', default: 10 } }, commands: { command: { inputType: 'text', default: '' }, message: { inputType: 'text', default: '' } } } return this._asyncTaskRender.render({ complete: () => html` <div class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-channel"> <h1> ${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TITLE)}: <span class="peertube-plugin-livechat-configuration-channel-info"> <span>${this._channelConfiguration?.channel.displayName}</span> <span>${this._channelConfiguration?.channel.name}</span> </span> <livechat-help-button .page="documentation/user/streamers/channel"> </livechat-help-button> </h1> <p>${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_DESC)}</p> <form livechat-configuration-channel-options role="form" @submit=${this._saveConfig}> <div class="row mt-3"> <div class="row mt-5"> <div class="col-12 col-lg-4 col-xl-3"> <livechat-configuration-row .title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_LABEL)} .description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_DESC, true)} .helpPage=${'documentation/user/streamers/slow_mode'}> </livechat-configuration-row> </div> <div class="col-12 col-lg-8 col-xl-9"> <div class="form-group"> <label> <input type="number" name="slowmode_duration" class="form-control ${classMap(this._getInputValidationClass('slowMode.duration'))}" min="0" max="1000" id="peertube-livechat-slowmode-duration" aria-describedby="peertube-livechat-slowmode-duration-feedback" @input=${(event: InputEvent) => { if (event?.target && this._channelConfiguration) { this._channelConfiguration.configuration.slowMode.duration = Number((event.target as HTMLInputElement).value) } this.requestUpdate('_channelConfiguration') } } value="${this._channelConfiguration?.configuration.slowMode.duration}" /> </label> ${this._renderFeedback('peertube-livechat-slowmode-duration-feedback', 'slowMode.duration')} </div> </div> </div> <div class="row mt-5"> <div class="col-12 col-lg-4 col-xl-3"> <livechat-configuration-row .title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)} .description=${''} .helpPage=${'documentation/user/streamers/channel'}> </livechat-configuration-row> </div> <div class="col-12 col-lg-8 col-xl-9"> <div class="form-group"> <label> <input type="checkbox" name="bot" id="peertube-livechat-bot" @input=${(event: InputEvent) => { if (event?.target && this._channelConfiguration) { this._channelConfiguration.configuration.bot.enabled = (event.target as HTMLInputElement).checked } this.requestUpdate('_channelConfiguration') } } .value=${this._channelConfiguration?.configuration.bot.enabled} ?checked=${this._channelConfiguration?.configuration.bot.enabled} /> ${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ENABLE_BOT_LABEL)} </label> </div> ${this._channelConfiguration?.configuration.bot.enabled ? html`<div class="form-group"> <label for="peertube-livechat-bot-nickname"> ${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_NICKNAME)} </label> <input type="text" name="bot_nickname" class="form-control ${classMap(this._getInputValidationClass('bot.nickname'))}" id="peertube-livechat-bot-nickname" aria-describedby="peertube-livechat-bot-nickname-feedback" @input=${(event: InputEvent) => { if (event?.target && this._channelConfiguration) { this._channelConfiguration.configuration.bot.nickname = (event.target as HTMLInputElement).value } this.requestUpdate('_channelConfiguration') } } value="${this._channelConfiguration?.configuration.bot.nickname}" /> ${this._renderFeedback('peertube-livechat-bot-nickname-feedback', 'bot.nickname')} </div>` : '' } </div> </div> ${this._channelConfiguration?.configuration.bot.enabled ? html`<div class="row mt-5"> <div class="col-12 col-lg-4 col-xl-3"> <livechat-configuration-row .title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)} .description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)} .helpPage=${'documentation/user/streamers/bot/forbidden_words'}> </livechat-configuration-row> </div> <div class="col-12 col-lg-8 col-xl-9"> <livechat-dynamic-table-form .header=${tableHeaderList.forbiddenWords} .schema=${tableSchema.forbiddenWords} .validation=${this._validationError?.properties} .validationPrefix=${'bot.forbiddenWords'} .rows=${this._channelConfiguration?.configuration.bot.forbiddenWords} @update=${(e: CustomEvent) => { if (this._channelConfiguration) { this._channelConfiguration.configuration.bot.forbiddenWords = e.detail this.requestUpdate('_channelConfiguration') } } } .formName=${'forbidden-words'}> </livechat-dynamic-table-form> </div> </div> <div class="row mt-5"> <div class="col-12 col-lg-4 col-xl-3"> <livechat-configuration-row .title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL)} .description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC)} .helpPage=${'documentation/user/streamers/bot/quotes'}> </livechat-configuration-row> </div> <div class="col-12 col-lg-8 col-xl-9"> <livechat-dynamic-table-form .header=${tableHeaderList.quotes} .schema=${tableSchema.quotes} .validation=${this._validationError?.properties} .validationPrefix=${'bot.quotes'} .rows=${this._channelConfiguration?.configuration.bot.quotes} @update=${(e: CustomEvent) => { if (this._channelConfiguration) { this._channelConfiguration.configuration.bot.quotes = e.detail this.requestUpdate('_channelConfiguration') } } } .formName=${'quote'}> </livechat-dynamic-table-form> </div> </div> <div class="row mt-5"> <div class="col-12 col-lg-4 col-xl-3"> <livechat-configuration-row .title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_LABEL)} .description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_DESC)} .helpPage=${'documentation/user/streamers/bot/commands'}> </livechat-configuration-row> </div> <div class="col-12 col-lg-8 col-xl-9"> <livechat-dynamic-table-form .header=${tableHeaderList.commands} .schema=${tableSchema.commands} .validation=${this._validationError?.properties} .validationPrefix=${'bot.commands'} .rows=${this._channelConfiguration?.configuration.bot.commands} @update=${(e: CustomEvent) => { if (this._channelConfiguration) { this._channelConfiguration.configuration.bot.commands = e.detail this.requestUpdate('_channelConfiguration') } } } .formName=${'command'}> </livechat-dynamic-table-form> </div> </div>` : '' } <div class="form-group mt-5"> <input type="submit" class="peertube-button-link orange-button" value=${ptTr(LOC_SAVE)} /> </div> </form> </div>` }) } }