Refactoring: moving some template to separate files + classMap fix.

This commit is contained in:
John Livingston 2024-06-12 17:08:48 +02:00
parent 2c3739f633
commit 4976a4f282
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
7 changed files with 518 additions and 438 deletions

View File

@ -4,16 +4,16 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { ChannelConfiguration } from 'shared/lib/types'
import { ChannelDetailsService } from '../services/channel-details'
import { channelConfigurationContext, channelDetailsServiceContext } from '../contexts/channel'
import { LivechatElement } from '../../lib/elements/livechat'
import { ValidationError, ValidationErrorType } from '../../lib/models/validation'
import { tplChannelConfiguration } from './templates/channel-configuration'
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 { 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 {
@ -22,16 +22,16 @@ export class ChannelConfigurationElement extends LivechatElement {
@provide({ context: channelConfigurationContext })
@state()
public _channelConfiguration?: ChannelConfiguration
public channelConfiguration?: ChannelConfiguration
@provide({ context: channelDetailsServiceContext })
private _channelDetailsService?: ChannelDetailsService
@state()
public _validationError?: ValidationError
public validationError?: ValidationError
@state()
private _actionDisabled: boolean = false
public actionDisabled: boolean = false
private _asyncTaskRender: Task
@ -44,37 +44,44 @@ export class ChannelConfigurationElement extends LivechatElement {
return new Task(this, {
task: async () => {
this._channelDetailsService = new ChannelDetailsService(this.ptOptions)
this._channelConfiguration = await this._channelDetailsService.fetchConfiguration(this.channelId ?? 0)
this._actionDisabled = false // in case of reset
this.channelConfiguration = await this._channelDetailsService.fetchConfiguration(this.channelId ?? 0)
this.actionDisabled = false // in case of reset
},
args: () => []
})
}
private async _reset (event?: Event): Promise<void> {
/**
* Resets the form by reloading data from backend.
*/
public async reset (event?: Event): Promise<void> {
event?.preventDefault()
this._actionDisabled = true
this.actionDisabled = true
this._asyncTaskRender = this._initTask()
this.requestUpdate()
}
private readonly _saveConfig = async (event?: Event): Promise<void> => {
/**
* Saves the channel configuration.
* @param event event
*/
public readonly saveConfig = async (event?: Event): Promise<void> => {
event?.preventDefault()
if (this._channelDetailsService && this._channelConfiguration) {
this._actionDisabled = true
this._channelDetailsService.saveOptions(this._channelConfiguration.channel.id,
this._channelConfiguration.configuration)
if (this._channelDetailsService && this.channelConfiguration) {
this.actionDisabled = true
this._channelDetailsService.saveOptions(this.channelConfiguration.channel.id,
this.channelConfiguration.configuration)
.then(() => {
this._validationError = undefined
this.validationError = undefined
this.ptTranslate(LOC_SUCCESSFULLY_SAVED).then((msg) => {
this.ptNotifier.info(msg)
}, () => {})
this.requestUpdate('_validationError')
})
.catch(async (error: Error) => {
this._validationError = undefined
this.validationError = undefined
if (error instanceof ValidationError) {
this._validationError = error
this.validationError = error
}
console.warn(`A validation error occurred in saving configuration. ${error.name}: ${error.message}`)
this.ptNotifier.error(
@ -85,22 +92,22 @@ export class ChannelConfigurationElement extends LivechatElement {
this.requestUpdate('_validationError')
})
.finally(() => {
this._actionDisabled = false
this.actionDisabled = false
})
}
}
private readonly _getInputValidationClass = (propertyName: string): { [key: string]: boolean } => {
public readonly getInputValidationClass = (propertyName: string): { [key: string]: boolean } => {
const validationErrorTypes: ValidationErrorType[] | undefined =
this._validationError?.properties[`${propertyName}`]
this.validationError?.properties[`${propertyName}`]
return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {}
}
private readonly _renderFeedback = (feedbackId: string,
public readonly renderFeedback = (feedbackId: string,
propertyName: string): TemplateResult | typeof nothing => {
const errorMessages: TemplateResult[] = []
const validationErrorTypes: ValidationErrorType[] | undefined =
this._validationError?.properties[`${propertyName}`] ?? undefined
this.validationError?.properties[`${propertyName}`] ?? undefined
if (validationErrorTypes && validationErrorTypes.length !== 0) {
if (validationErrorTypes.includes(ValidationErrorType.WrongType)) {
@ -120,283 +127,10 @@ export class ChannelConfigurationElement extends LivechatElement {
}
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({
pending: () => html`<livechat-spinner></livechat-spinner>`,
error: () => html`<livechat-error></livechat-error>`,
complete: () => html`
<div class="margin-content peertube-plugin-livechat-configuration
peertube-plugin-livechat-configuration-channel">
<h1>
<span class="peertube-plugin-livechat-configuration-channel-info">
<span>${this._channelConfiguration?.channel.displayName}</span>
<span>${this._channelConfiguration?.channel.name}</span>
</span>
</h1>
<livechat-channel-tabs .active=${'configuration'} .channelId=${this.channelId}></livechat-channel-tabs>
<p>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_DESC)}
<livechat-help-button .page=${'documentation/user/streamers/channel'}>
</livechat-help-button>
</p>
<form livechat-configuration-channel-options role="form" @submit=${this._saveConfig}>
<livechat-configuration-section-header
.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-section-header>
<div class="form-group">
<label>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_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>
<livechat-configuration-section-header
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
.description=${''}
.helpPage=${'documentation/user/streamers/channel'}>
</livechat-configuration-section-header>
<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="1"
?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>
<livechat-configuration-section-header
.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-section-header>
<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>
<livechat-configuration-section-header
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC)}
.helpPage=${'documentation/user/streamers/bot/quotes'}>
</livechat-configuration-section-header>
<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>
<livechat-configuration-section-header
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_DESC)}
.helpPage=${'documentation/user/streamers/bot/commands'}>
</livechat-configuration-section-header>
<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 class="form-group mt-5">
<button type="reset" @click=${this._reset} ?disabled=${this._actionDisabled}>
${ptTr(LOC_CANCEL)}
</button>
<button type="submit" ?disabled=${this._actionDisabled}>
${ptTr(LOC_SAVE)}
</button>
</div>
</form>
</div>`
complete: () => tplChannelConfiguration(this)
})
}
}

View File

@ -3,13 +3,11 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { ChannelEmojisConfiguration } from 'shared/lib/types'
import type { DynamicFormHeader, DynamicFormSchema } from '../../lib/elements/dynamic-table-form'
import { LivechatElement } from '../../lib/elements/livechat'
import { ChannelDetailsService } from '../services/channel-details'
import { channelDetailsServiceContext } from '../contexts/channel'
import { maxEmojisPerChannel } from 'shared/lib/emojis'
import { ptTr } from '../../lib/directives/translation'
import { ValidationError } from '../../lib/models/validation'
import { tplChannelEmojis } from './templates/channel-emojis'
import { Task } from '@lit/task'
import { customElement, property, state } from 'lit/decorators.js'
import { provide } from '@lit/context'
@ -23,16 +21,16 @@ export class ChannelEmojisElement extends LivechatElement {
@property({ attribute: false })
public channelId?: number
private _channelEmojisConfiguration?: ChannelEmojisConfiguration
public channelEmojisConfiguration?: ChannelEmojisConfiguration
@provide({ context: channelDetailsServiceContext })
private _channelDetailsService?: ChannelDetailsService
@state()
private _validationError?: ValidationError
public validationError?: ValidationError
@state()
private _actionDisabled: boolean = false
public actionDisabled: boolean = false
private _asyncTaskRender: Task
@ -42,111 +40,10 @@ export class ChannelEmojisElement extends LivechatElement {
}
protected override render = (): unknown => {
const tableHeaderList: DynamicFormHeader = {
sn: {
colName: ptTr(LOC_LIVECHAT_EMOJIS_SHORTNAME),
description: ptTr(LOC_LIVECHAT_EMOJIS_SHORTNAME_DESC),
headerClassList: ['peertube-livechat-emojis-col-sn']
},
url: {
colName: ptTr(LOC_LIVECHAT_EMOJIS_FILE),
description: ptTr(LOC_LIVECHAT_EMOJIS_FILE_DESC),
headerClassList: ['peertube-livechat-emojis-col-file']
}
}
const tableSchema: DynamicFormSchema = {
sn: {
inputType: 'text',
default: ''
},
url: {
inputType: 'image-file',
default: '',
colClassList: ['peertube-livechat-emojis-col-file']
}
}
return this._asyncTaskRender.render({
pending: () => html`<livechat-spinner></livechat-spinner>`,
error: () => html`<livechat-error></livechat-error>`,
complete: () => html`
<div
class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-channel"
>
<h1>
<span class="peertube-plugin-livechat-configuration-channel-info">
<span>${this._channelEmojisConfiguration?.channel.displayName}</span>
<span>${this._channelEmojisConfiguration?.channel.name}</span>
</span>
</h1>
<livechat-channel-tabs .active=${'emojis'} .channelId=${this.channelId}></livechat-channel-tabs>
<p>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_DESC)}
<livechat-help-button .page=${'documentation/user/streamers/emojis'}>
</livechat-help-button>
</p>
<form role="form" @submit=${this._saveEmojis}>
<div class="peertube-plugin-livechat-configuration-actions">
${
this._channelEmojisConfiguration?.emojis?.customEmojis?.length
? html`
<button
@click=${this._exportEmojis}
?disabled=${this._actionDisabled}
>
${ptTr(LOC_ACTION_EXPORT)}
</button>`
: ''
}
${
(this._channelEmojisConfiguration?.emojis?.customEmojis?.length ?? 0) < maxEmojisPerChannel
? html`
<button
@click=${this._importEmojis}
?disabled=${this._actionDisabled}
>
${ptTr(LOC_ACTION_IMPORT)}
</button>`
: ''
}
</div>
<livechat-dynamic-table-form
.header=${tableHeaderList}
.schema=${tableSchema}
.maxLines=${maxEmojisPerChannel}
.validation=${this._validationError?.properties}
.validationPrefix=${'emojis'}
.rows=${this._channelEmojisConfiguration?.emojis.customEmojis}
@update=${(e: CustomEvent) => {
if (this._channelEmojisConfiguration) {
this._channelEmojisConfiguration.emojis.customEmojis = e.detail
// Fixing missing ':' for shortnames:
for (const desc of this._channelEmojisConfiguration.emojis.customEmojis) {
if (desc.sn === '') { continue }
if (!desc.sn.startsWith(':')) { desc.sn = ':' + desc.sn }
if (!desc.sn.endsWith(':')) { desc.sn += ':' }
}
this.requestUpdate('_channelEmojisConfiguration')
}
}
}
></livechat-dynamic-table-form>
<div class="form-group mt-5">
<button type="reset" @click=${this._reset} ?disabled=${this._actionDisabled}>
${ptTr(LOC_CANCEL)}
</button>
<button type="submit" ?disabled=${this._actionDisabled}>
${ptTr(LOC_SAVE)}
</button>
</div>
</form>
</div>
`
complete: () => tplChannelEmojis(this)
})
}
@ -157,39 +54,46 @@ export class ChannelEmojisElement extends LivechatElement {
throw new Error('Missing channelId')
}
this._channelDetailsService = new ChannelDetailsService(this.ptOptions)
this._channelEmojisConfiguration = await this._channelDetailsService.fetchEmojisConfiguration(this.channelId)
this._actionDisabled = false // in case of reset
this.channelEmojisConfiguration = await this._channelDetailsService.fetchEmojisConfiguration(this.channelId)
this.actionDisabled = false // in case of reset
},
args: () => []
})
}
private async _reset (ev?: Event): Promise<void> {
/**
* Resets the page, by reloading data from backend.
*/
public async reset (ev?: Event): Promise<void> {
ev?.preventDefault()
this._actionDisabled = true
this.actionDisabled = true
this._asyncTaskRender = this._initTask()
this.requestUpdate()
}
private async _saveEmojis (ev?: Event): Promise<void> {
/**
* Saves the emojis form.
* @param ev event
*/
public async saveEmojis (ev?: Event): Promise<void> {
ev?.preventDefault()
if (!this._channelDetailsService || !this._channelEmojisConfiguration || !this.channelId) {
if (!this._channelDetailsService || !this.channelEmojisConfiguration || !this.channelId) {
this.ptNotifier.error(await this.ptTranslate(LOC_ERROR))
return
}
try {
this._actionDisabled = true
await this._channelDetailsService.saveEmojisConfiguration(this.channelId, this._channelEmojisConfiguration.emojis)
this._validationError = undefined
this.actionDisabled = true
await this._channelDetailsService.saveEmojisConfiguration(this.channelId, this.channelEmojisConfiguration.emojis)
this.validationError = undefined
this.ptNotifier.info(await this.ptTranslate(LOC_SUCCESSFULLY_SAVED))
this.requestUpdate('_validationError')
} catch (error) {
this._validationError = undefined
this.validationError = undefined
let msg: string
if ((error instanceof ValidationError)) {
this._validationError = error
this.validationError = error
if (error.message) {
msg = error.message
}
@ -198,13 +102,16 @@ export class ChannelEmojisElement extends LivechatElement {
this.ptNotifier.error(msg)
this.requestUpdate('_validationError')
} finally {
this._actionDisabled = false
this.actionDisabled = false
}
}
private async _importEmojis (ev: Event): Promise<void> {
/**
* Import emojis action.
*/
public async importEmojis (ev: Event): Promise<void> {
ev.preventDefault()
this._actionDisabled = true
this.actionDisabled = true
try {
// download a json file:
const file = await new Promise<File>((resolve, reject) => {
@ -266,10 +173,10 @@ export class ChannelEmojisElement extends LivechatElement {
if (entry.isCategoryEmoji === true) {
item.isCategoryEmoji = true
}
this._channelEmojisConfiguration?.emojis.customEmojis.push(item)
this.channelEmojisConfiguration?.emojis.customEmojis.push(item)
}
this.requestUpdate('_channelEmojisConfiguration')
this.requestUpdate('channelEmojisConfiguration')
this.ptNotifier.info(
await this.ptTranslate(LOC_ACTION_IMPORT_EMOJIS_INFO)
@ -277,16 +184,19 @@ export class ChannelEmojisElement extends LivechatElement {
} catch (err: any) {
this.ptNotifier.error(err.toString())
} finally {
this._actionDisabled = false
this.actionDisabled = false
}
}
private async _exportEmojis (ev: Event): Promise<void> {
/**
* Export emojis action.
*/
public async exportEmojis (ev: Event): Promise<void> {
ev.preventDefault()
this._actionDisabled = true
this.actionDisabled = true
try {
const result: ChannelEmojisConfiguration['emojis']['customEmojis'] = []
for (const ed of this._channelEmojisConfiguration?.emojis?.customEmojis ?? []) {
for (const ed of this.channelEmojisConfiguration?.emojis?.customEmojis ?? []) {
if (!ed.sn || !ed.url) { continue }
// Here url can be:
// * the dataUrl representation of a newly uploaded file
@ -313,10 +223,15 @@ export class ChannelEmojisElement extends LivechatElement {
console.error(err)
this.ptNotifier.error(err.toString())
} finally {
this._actionDisabled = false
this.actionDisabled = false
}
}
/**
* Takes an url (or dataUrl), download the image, and converts to dataUrl.
* @param url the url
* @returns A dataUrl representation of the image.
*/
private async _convertImageToDataUrl (url: string): Promise<string> {
if (url.startsWith('data:')) { return url }
// There is a trick to convert img to dataUrl: using a canvas.

View File

@ -20,9 +20,6 @@ export class ChannelHomeElement extends LivechatElement {
@provide({ context: channelDetailsServiceContext })
private _channelDetailsService?: ChannelDetailsService
@state()
public _formStatus: boolean | any = undefined
private readonly _asyncTaskRender = new Task(this, {
task: async () => {
// Getting the current username in localStorage. Don't know any cleaner way to do.

View File

@ -0,0 +1,296 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import type { ChannelConfigurationElement } from '../channel-configuration'
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
import { ptTr } from '../../../lib/directives/translation'
import { html, TemplateResult } from 'lit'
import { classMap } from 'lit/directives/class-map.js'
export function tplChannelConfiguration (el: ChannelConfigurationElement): TemplateResult {
const tableHeaderList: {[key: string]: DynamicFormHeader} = {
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: {[key: string]: DynamicFormSchema} = {
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 html`
<div class="margin-content peertube-plugin-livechat-configuration
peertube-plugin-livechat-configuration-channel">
<h1>
<span class="peertube-plugin-livechat-configuration-channel-info">
<span>${el.channelConfiguration?.channel.displayName}</span>
<span>${el.channelConfiguration?.channel.name}</span>
</span>
</h1>
<livechat-channel-tabs .active=${'configuration'} .channelId=${el.channelId}></livechat-channel-tabs>
<p>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_DESC)}
<livechat-help-button .page=${'documentation/user/streamers/channel'}>
</livechat-help-button>
</p>
<form livechat-configuration-channel-options role="form" @submit=${el.saveConfig}>
<livechat-configuration-section-header
.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-section-header>
<div class="form-group">
<label>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_LABEL)}
<input
type="number"
name="slowmode_duration"
class=${classMap(
Object.assign(
{ 'form-control': true },
el.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 && el.channelConfiguration) {
el.channelConfiguration.configuration.slowMode.duration =
Number((event.target as HTMLInputElement).value)
}
el.requestUpdate('channelConfiguration')
}
}
value="${el.channelConfiguration?.configuration.slowMode.duration ?? ''}"
/>
</label>
${el.renderFeedback('peertube-livechat-slowmode-duration-feedback', 'slowMode.duration')}
</div>
<livechat-configuration-section-header
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
.description=${''}
.helpPage=${'documentation/user/streamers/channel'}>
</livechat-configuration-section-header>
<div class="form-group">
<label>
<input
type="checkbox"
name="bot"
id="peertube-livechat-bot"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.bot.enabled =
(event.target as HTMLInputElement).checked
}
el.requestUpdate('channelConfiguration')
}
}
value="1"
?checked=${el.channelConfiguration?.configuration.bot.enabled}
/>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ENABLE_BOT_LABEL)}
</label>
</div>
${!el.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=${classMap(
Object.assign(
{ 'form-control': true },
el.getInputValidationClass('bot.nickname')
)
)}
id="peertube-livechat-bot-nickname"
aria-describedby="peertube-livechat-bot-nickname-feedback"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.bot.nickname =
(event.target as HTMLInputElement).value
}
el.requestUpdate('channelConfiguration')
}
}
value="${el.channelConfiguration?.configuration.bot.nickname ?? ''}"
/>
${el.renderFeedback('peertube-livechat-bot-nickname-feedback', 'bot.nickname')}
</div>
<livechat-configuration-section-header
.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-section-header>
<livechat-dynamic-table-form
.header=${tableHeaderList.forbiddenWords}
.schema=${tableSchema.forbiddenWords}
.validation=${el.validationError?.properties}
.validationPrefix=${'bot.forbiddenWords'}
.rows=${el.channelConfiguration?.configuration.bot.forbiddenWords}
@update=${(e: CustomEvent) => {
if (el.channelConfiguration) {
el.channelConfiguration.configuration.bot.forbiddenWords = e.detail
el.requestUpdate('channelConfiguration')
}
}
}
.formName=${'forbidden-words'}
></livechat-dynamic-table-form>
<livechat-configuration-section-header
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC)}
.helpPage=${'documentation/user/streamers/bot/quotes'}>
</livechat-configuration-section-header>
<livechat-dynamic-table-form
.header=${tableHeaderList.quotes}
.schema=${tableSchema.quotes}
.validation=${el.validationError?.properties}
.validationPrefix=${'bot.quotes'}
.rows=${el.channelConfiguration?.configuration.bot.quotes}
@update=${(e: CustomEvent) => {
if (el.channelConfiguration) {
el.channelConfiguration.configuration.bot.quotes = e.detail
el.requestUpdate('channelConfiguration')
}
}
}
.formName=${'quote'}
></livechat-dynamic-table-form>
<livechat-configuration-section-header
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_DESC)}
.helpPage=${'documentation/user/streamers/bot/commands'}>
</livechat-configuration-section-header>
<livechat-dynamic-table-form
.header=${tableHeaderList.commands}
.schema=${tableSchema.commands}
.validation=${el.validationError?.properties}
.validationPrefix=${'bot.commands'}
.rows=${el.channelConfiguration?.configuration.bot.commands}
@update=${(e: CustomEvent) => {
if (el.channelConfiguration) {
el.channelConfiguration.configuration.bot.commands = e.detail
el.requestUpdate('channelConfiguration')
}
}
}
.formName=${'command'}
></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>`
}

View File

@ -0,0 +1,113 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import type { ChannelEmojisElement } from '../channel-emojis'
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
import { maxEmojisPerChannel } from 'shared/lib/emojis'
import { ptTr } from '../../../lib/directives/translation'
import { html, TemplateResult } from 'lit'
export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
const tableHeaderList: DynamicFormHeader = {
sn: {
colName: ptTr(LOC_LIVECHAT_EMOJIS_SHORTNAME),
description: ptTr(LOC_LIVECHAT_EMOJIS_SHORTNAME_DESC),
headerClassList: ['peertube-livechat-emojis-col-sn']
},
url: {
colName: ptTr(LOC_LIVECHAT_EMOJIS_FILE),
description: ptTr(LOC_LIVECHAT_EMOJIS_FILE_DESC),
headerClassList: ['peertube-livechat-emojis-col-file']
}
}
const tableSchema: DynamicFormSchema = {
sn: {
inputType: 'text',
default: ''
},
url: {
inputType: 'image-file',
default: '',
colClassList: ['peertube-livechat-emojis-col-file']
}
}
return html`
<div
class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-channel"
>
<h1>
<span class="peertube-plugin-livechat-configuration-channel-info">
<span>${el.channelEmojisConfiguration?.channel.displayName}</span>
<span>${el.channelEmojisConfiguration?.channel.name}</span>
</span>
</h1>
<livechat-channel-tabs .active=${'emojis'} .channelId=${el.channelId}></livechat-channel-tabs>
<p>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_DESC)}
<livechat-help-button .page=${'documentation/user/streamers/emojis'}>
</livechat-help-button>
</p>
<form role="form" @submit=${el.saveEmojis}>
<div class="peertube-plugin-livechat-configuration-actions">
${
el.channelEmojisConfiguration?.emojis?.customEmojis?.length
? html`
<button
@click=${el.exportEmojis}
?disabled=${el.actionDisabled}
>
${ptTr(LOC_ACTION_EXPORT)}
</button>`
: ''
}
${
(el.channelEmojisConfiguration?.emojis?.customEmojis?.length ?? 0) < maxEmojisPerChannel
? html`
<button
@click=${el.importEmojis}
?disabled=${el.actionDisabled}
>
${ptTr(LOC_ACTION_IMPORT)}
</button>`
: ''
}
</div>
<livechat-dynamic-table-form
.header=${tableHeaderList}
.schema=${tableSchema}
.maxLines=${maxEmojisPerChannel}
.validation=${el.validationError?.properties}
.validationPrefix=${'emojis'}
.rows=${el.channelEmojisConfiguration?.emojis.customEmojis}
@update=${(e: CustomEvent) => {
if (el.channelEmojisConfiguration) {
el.channelEmojisConfiguration.emojis.customEmojis = e.detail
// Fixing missing ':' for shortnames:
for (const desc of el.channelEmojisConfiguration.emojis.customEmojis) {
if (desc.sn === '') { continue }
if (!desc.sn.startsWith(':')) { desc.sn = ':' + desc.sn }
if (!desc.sn.endsWith(':')) { desc.sn += ':' }
}
el.requestUpdate('channelEmojisConfiguration')
}
}
}
></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>`
}

View File

@ -7,7 +7,7 @@ import { customElement, property } from 'lit/decorators.js'
import { LivechatElement } from './livechat'
@customElement('livechat-configuration-section-header')
export class ConfigurationRowElement extends LivechatElement {
export class ConfigurationSectionHeaderElement extends LivechatElement {
@property({ attribute: false })
public override title: string = 'title'

View File

@ -522,7 +522,12 @@ export class DynamicTableFormElement extends LivechatElement {
return html`<input
type=${propertySchema.inputType}
name=${inputName}
class="form-control ${classMap(this._getInputValidationClass(propertyName, originalIndex))}"
class=${classMap(
Object.assign(
{ 'form-control': true },
this._getInputValidationClass(propertyName, originalIndex)
)
)}
id=${inputId}
aria-describedby="${inputId}-feedback"
list=${(propertySchema.datalist) ? inputId + '-datalist' : nothing}
@ -550,7 +555,12 @@ export class DynamicTableFormElement extends LivechatElement {
return html`<livechat-tags-input
.type=${'text'}
.name=${inputName}
class="form-control ${classMap(this._getInputValidationClass(propertyName, originalIndex))}"
class=${classMap(
Object.assign(
{ 'form-control': true },
this._getInputValidationClass(propertyName, originalIndex)
)
)}
id=${inputId}
.inputPlaceholder=${ifDefined(propertySchema.label)}
aria-describedby="${inputId}-feedback"
@ -573,7 +583,12 @@ export class DynamicTableFormElement extends LivechatElement {
originalIndex: number): TemplateResult => {
return html`<textarea
name=${inputName}
class="form-control ${classMap(this._getInputValidationClass(propertyName, originalIndex))}"
class=${classMap(
Object.assign(
{ 'form-control': true },
this._getInputValidationClass(propertyName, originalIndex)
)
)}
id=${inputId}
aria-describedby="${inputId}-feedback"
min=${ifDefined(propertySchema.min)}
@ -594,7 +609,12 @@ export class DynamicTableFormElement extends LivechatElement {
return html`<input
type="checkbox"
name=${inputName}
class="form-check-input ${classMap(this._getInputValidationClass(propertyName, originalIndex))}"
class=${classMap(
Object.assign(
{ 'form-check-input': true },
this._getInputValidationClass(propertyName, originalIndex)
)
)}
id=${inputId}
aria-describedby="${inputId}-feedback"
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
@ -610,7 +630,12 @@ export class DynamicTableFormElement extends LivechatElement {
propertyValue: string,
originalIndex: number): TemplateResult => {
return html`<select
class="form-select ${classMap(this._getInputValidationClass(propertyName, originalIndex))}"
class=${classMap(
Object.assign(
{ 'form-select': true },
this._getInputValidationClass(propertyName, originalIndex)
)
)}
id=${inputId}
aria-describedby="${inputId}-feedback"
aria-label=${inputName}
@ -634,7 +659,7 @@ export class DynamicTableFormElement extends LivechatElement {
): TemplateResult => {
return html`<livechat-image-file-input
.name=${inputName}
class="${classMap(this._getInputValidationClass(propertyName, originalIndex))}"
class=${classMap(this._getInputValidationClass(propertyName, originalIndex))}
id=${inputId}
aria-describedby="${inputId}-feedback"
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}