diff --git a/client/@types/global.d.ts b/client/@types/global.d.ts index 1633c118..c736bd91 100644 --- a/client/@types/global.d.ts +++ b/client/@types/global.d.ts @@ -81,6 +81,7 @@ declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_BANNED_JIDS_LABEL: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_NICKNAME: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FOR_MORE_INFO: string +declare const LOC_VALIDATION_ERROR: string declare const LOC_INVALID_VALUE: string declare const LOC_INVALID_VALUE_WRONG_TYPE: string declare const LOC_INVALID_VALUE_WRONG_FORMAT: string diff --git a/client/common/configuration/elements/channel-configuration.ts b/client/common/configuration/elements/channel-configuration.ts index 683f8083..7aa1edac 100644 --- a/client/common/configuration/elements/channel-configuration.ts +++ b/client/common/configuration/elements/channel-configuration.ts @@ -45,23 +45,30 @@ export class ChannelConfigurationElement extends LivechatElement { args: () => [this.registerClientOptions] }) - private readonly _saveConfig = (event?: Event): void => { + private readonly _saveConfig = async (event?: Event): Promise => { 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.registerClientOptions?.peertubeHelpers.translate(LOC_SUCCESSFULLY_SAVED).then((msg) => { + this.registerClientOptions + ?.peertubeHelpers.notifier.info(msg) + }) this.requestUpdate('_validationError') }) - .catch((error: ValidationError) => { - this._validationError = error + .catch(async (error: Error) => { + this._validationError = undefined + if (error instanceof 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.registerClientOptions?.peertubeHelpers.notifier.error( + error.message + ? error.message + : await this.registerClientOptions.peertubeHelpers.translate('error') + ) this.requestUpdate('_validationError') }) } diff --git a/client/common/configuration/elements/channel-emojis.ts b/client/common/configuration/elements/channel-emojis.ts index 72c874ef..cfb457ac 100644 --- a/client/common/configuration/elements/channel-emojis.ts +++ b/client/common/configuration/elements/channel-emojis.ts @@ -38,21 +38,21 @@ export class ChannelEmojisElement extends LivechatElement { protected override render = (): unknown => { const tableHeaderList: DynamicFormHeader = { - shortname: { + sn: { colName: ptTr(LOC_LIVECHAT_EMOJIS_SHORTNAME), description: ptTr(LOC_LIVECHAT_EMOJIS_SHORTNAME_DESC) }, - file: { + url: { colName: ptTr(LOC_LIVECHAT_EMOJIS_FILE), description: ptTr(LOC_LIVECHAT_EMOJIS_FILE_DESC) } } const tableSchema: DynamicFormSchema = { - shortname: { + sn: { inputType: 'text', default: '' }, - file: { + url: { inputType: 'image-file', default: '' } @@ -81,11 +81,11 @@ export class ChannelEmojisElement extends LivechatElement { .validation=${this._validationError?.properties} .validationPrefix=${'emojis'} .rows=${this._channelEmojisConfiguration?.emojis.customEmojis} - @update=${(_e: CustomEvent) => { - // if (this._channelEmojisConfiguration) { - // this._channelEmojisConfiguration.configuration.emojis.customEmojis = e.detail - // this.requestUpdate('_channelEmojisConfiguration') - // } + @update=${(e: CustomEvent) => { + if (this._channelEmojisConfiguration) { + this._channelEmojisConfiguration.emojis.customEmojis = e.detail + this.requestUpdate('_channelEmojisConfiguration') + } } } > @@ -118,9 +118,32 @@ export class ChannelEmojisElement extends LivechatElement { args: () => [] }) - private readonly _saveEmojis = (ev?: Event): void => { + private async _saveEmojis (ev?: Event): Promise { ev?.preventDefault() - // TODO - this.registerClientOptions?.peertubeHelpers.notifier.error('TODO') + const peertubeHelpers = this.registerClientOptions?.peertubeHelpers + if (!peertubeHelpers) { return } // Should not happen + + if (!this._channelDetailsService || !this._channelEmojisConfiguration || !this.channelId) { + peertubeHelpers.notifier.error(await peertubeHelpers.translate(LOC_ERROR)) + return + } + + try { + await this._channelDetailsService.saveEmojisConfiguration(this.channelId, this._channelEmojisConfiguration.emojis) + this._validationError = undefined + this.requestUpdate('_validationError') + } catch (error) { + this._validationError = undefined + let msg: string + if ((error instanceof ValidationError)) { + this._validationError = error + if (error.message) { + msg = error.message + } + } + msg ??= await peertubeHelpers.translate(LOC_ERROR) + peertubeHelpers.notifier.error(msg) + this.requestUpdate('_validationError') + } } } diff --git a/client/common/configuration/services/channel-details.ts b/client/common/configuration/services/channel-details.ts index 51fbf5f1..2a723971 100644 --- a/client/common/configuration/services/channel-details.ts +++ b/client/common/configuration/services/channel-details.ts @@ -1,13 +1,13 @@ // SPDX-FileCopyrightText: 2024 Mehdi Benadel +// SPDX-FileCopyrightText: 2024 John Livingston // // SPDX-License-Identifier: AGPL-3.0-only import type { RegisterClientOptions } from '@peertube/peertube-types/client' -import type { ValidationError } from '../../lib/models/validation' import type { - ChannelLiveChatInfos, ChannelConfiguration, ChannelConfigurationOptions, ChannelEmojisConfiguration + ChannelLiveChatInfos, ChannelConfiguration, ChannelConfigurationOptions, ChannelEmojisConfiguration, ChannelEmojis } from 'shared/lib/types' -import { ValidationErrorType } from '../../lib/models/validation' +import { ValidationError, ValidationErrorType } from '../../lib/models/validation' import { getBaseRoute } from '../../../utils/uri' export class ChannelDetailsService { @@ -22,53 +22,46 @@ export class ChannelDetailsService { this._headers['content-type'] = 'application/json;charset=UTF-8' } - validateOptions = (channelConfigurationOptions: ChannelConfigurationOptions): boolean => { - let hasErrors = false - const validationError: ValidationError = { - name: 'ChannelConfigurationOptionsValidationError', - message: 'There was an error during validation', - properties: {} - } + validateOptions = async (channelConfigurationOptions: ChannelConfigurationOptions): Promise => { + const propertiesError: ValidationError['properties'] = {} + const botConf = channelConfigurationOptions.bot const slowModeDuration = channelConfigurationOptions.slowMode.duration - validationError.properties['slowMode.duration'] = [] + propertiesError['slowMode.duration'] = [] if ( (typeof slowModeDuration !== 'number') || - isNaN(slowModeDuration)) { - validationError.properties['slowMode.duration'].push(ValidationErrorType.WrongType) - hasErrors = true + isNaN(slowModeDuration) + ) { + propertiesError['slowMode.duration'].push(ValidationErrorType.WrongType) } else if ( slowModeDuration < 0 || slowModeDuration > 1000 ) { - validationError.properties['slowMode.duration'].push(ValidationErrorType.NotInRange) - hasErrors = true + propertiesError['slowMode.duration'].push(ValidationErrorType.NotInRange) } // If !bot.enabled, we don't have to validate these fields: // The backend will ignore those values. if (botConf.enabled) { - validationError.properties['bot.nickname'] = [] + propertiesError['bot.nickname'] = [] if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) { - validationError.properties['bot.nickname'].push(ValidationErrorType.WrongFormat) - hasErrors = true + propertiesError['bot.nickname'].push(ValidationErrorType.WrongFormat) } for (const [i, fw] of botConf.forbiddenWords.entries()) { for (const v of fw.entries) { - validationError.properties[`bot.forbiddenWords.${i}.entries`] = [] + propertiesError[`bot.forbiddenWords.${i}.entries`] = [] if (fw.regexp) { if (v.trim() !== '') { try { // eslint-disable-next-line no-new new RegExp(v) } catch (_) { - validationError.properties[`bot.forbiddenWords.${i}.entries`] + propertiesError[`bot.forbiddenWords.${i}.entries`] .push(ValidationErrorType.WrongFormat) - hasErrors = true } } } @@ -76,16 +69,20 @@ export class ChannelDetailsService { } for (const [i, cd] of botConf.commands.entries()) { - validationError.properties[`bot.commands.${i}.command`] = [] + propertiesError[`bot.commands.${i}.command`] = [] if (/\s+/.test(cd.command)) { - validationError.properties[`bot.commands.${i}.command`].push(ValidationErrorType.WrongFormat) - hasErrors = true + propertiesError[`bot.commands.${i}.command`].push(ValidationErrorType.WrongFormat) } } } - if (hasErrors) { + if (Object.values(propertiesError).find(e => e.length > 0)) { + const validationError = new ValidationError( + 'ChannelConfigurationOptionsValidationError', + await this._registerClientOptions.peertubeHelpers.translate(LOC_VALIDATION_ERROR), + propertiesError + ) throw validationError } @@ -161,7 +158,7 @@ export class ChannelDetailsService { return response.json() } - fetchEmojisConfiguration = async (channelId: number): Promise => { + public async fetchEmojisConfiguration (channelId: number): Promise { const response = await fetch( getBaseRoute(this._registerClientOptions) + '/api/configuration/channel/emojis/' + @@ -181,4 +178,56 @@ export class ChannelDetailsService { return response.json() } + + public async validateEmojisConfiguration (channelEmojis: ChannelEmojis): Promise { + const propertiesError: ValidationError['properties'] = {} + + for (const [i, e] of channelEmojis.customEmojis.entries()) { + propertiesError[`emojis.${i}.sn`] = [] + // FIXME: the ":" should not be in the value, but added afterward. + if (!/^:[\w-]+:$/.test(e.sn)) { + propertiesError[`emojis.${i}.sn`].push(ValidationErrorType.WrongFormat) + } + } + + if (Object.values(propertiesError).find(e => e.length > 0)) { + const validationError = new ValidationError( + 'ChannelEmojisValidationError', + await this._registerClientOptions.peertubeHelpers.translate(LOC_VALIDATION_ERROR), + propertiesError + ) + throw validationError + } + + return true + } + + public async saveEmojisConfiguration ( + channelId: number, + channelEmojis: ChannelEmojis + ): Promise { + if (!await this.validateEmojisConfiguration(channelEmojis)) { + throw new Error('Invalid form data') + } + + const response = await fetch( + getBaseRoute(this._registerClientOptions) + + '/api/configuration/channel/emojis/' + + encodeURIComponent(channelId), + { + method: 'POST', + headers: this._headers, + body: JSON.stringify(channelEmojis) + } + ) + + if (!response.ok) { + if (response.status === 404) { + // File does not exist yet, that is a normal use case. + } + throw new Error('Can\'t get channel emojis options.') + } + + return response.json() + } } diff --git a/client/common/lib/elements/image-file-input.ts b/client/common/lib/elements/image-file-input.ts index c1bff073..392b1e68 100644 --- a/client/common/lib/elements/image-file-input.ts +++ b/client/common/lib/elements/image-file-input.ts @@ -55,10 +55,13 @@ export class ImageFileInputElement extends LivechatElement { private async _upload (ev: Event): Promise { ev.preventDefault() + ev.stopImmediatePropagation() // we dont want to propage the change from the input field, only from the hidden field const target = ev.target const file = (target as HTMLInputElement).files?.[0] if (!file) { this.value = '' + const event = new Event('change') + this.dispatchEvent(event) return } @@ -81,6 +84,8 @@ export class ImageFileInputElement extends LivechatElement { }) this.value = base64 + const event = new Event('change') + this.dispatchEvent(event) } catch (err) { // FIXME: use peertube notifier? console.error(err) diff --git a/client/common/lib/models/validation.ts b/client/common/lib/models/validation.ts index f1047aaa..484a455a 100644 --- a/client/common/lib/models/validation.ts +++ b/client/common/lib/models/validation.ts @@ -10,4 +10,10 @@ export enum ValidationErrorType { export class ValidationError extends Error { properties: {[key: string]: ValidationErrorType[] } = {} + + constructor (name: string, message: string | undefined, properties: ValidationError['properties']) { + super(message) + this.name = name + this.properties = properties + } } diff --git a/languages/en.yml b/languages/en.yml index 454a1a90..df0c2bb4 100644 --- a/languages/en.yml +++ b/languages/en.yml @@ -434,6 +434,7 @@ livechat_configuration_channel_for_more_info: | livechat_configuration_channel_banned_jids_label: "Banned users and patterns" livechat_configuration_channel_bot_nickname: "Bot nickname" +validation_error: "There was an error during validation." invalid_value: "Invalid value." invalid_value_wrong_type: "Value is of the wrong type." invalid_value_wrong_format: "Value is in the wrong format."