Custom channel emoticons WIP (#130) + various fix/refactoring
This commit is contained in:
		
							
								
								
									
										1
									
								
								client/@types/global.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								client/@types/global.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -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 | ||||
|  | ||||
| @ -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<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.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') | ||||
|         }) | ||||
|     } | ||||
|  | ||||
| @ -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') | ||||
|                     } | ||||
|                   } | ||||
|                 } | ||||
|               ></livechat-dynamic-table-form> | ||||
| @ -118,9 +118,32 @@ export class ChannelEmojisElement extends LivechatElement { | ||||
|     args: () => [] | ||||
|   }) | ||||
|  | ||||
|   private readonly _saveEmojis = (ev?: Event): void => { | ||||
|   private async _saveEmojis (ev?: Event): Promise<void> { | ||||
|     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') | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,13 +1,13 @@ | ||||
| // SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com> | ||||
| // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/> | ||||
| // | ||||
| // SPDX-License-Identifier: AGPL-3.0-only | ||||
|  | ||||
| import type { RegisterClientOptions } from '@peertube/peertube-types/client' | ||||
| import type { 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<boolean> => { | ||||
|     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<ChannelEmojisConfiguration> => { | ||||
|   public async fetchEmojisConfiguration (channelId: number): Promise<ChannelEmojisConfiguration> { | ||||
|     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<boolean> { | ||||
|     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<void> { | ||||
|     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() | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -55,10 +55,13 @@ export class ImageFileInputElement extends LivechatElement { | ||||
|  | ||||
|   private async _upload (ev: Event): Promise<void> { | ||||
|     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) | ||||
|  | ||||
| @ -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 | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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." | ||||
|  | ||||
		Reference in New Issue
	
	Block a user