Custom channel emoticons WIP (#130) + various fix/refactoring
This commit is contained in:
parent
04403225fb
commit
5c87eef915
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_BOT_NICKNAME: string
|
||||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FOR_MORE_INFO: 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: string
|
||||||
declare const LOC_INVALID_VALUE_WRONG_TYPE: string
|
declare const LOC_INVALID_VALUE_WRONG_TYPE: string
|
||||||
declare const LOC_INVALID_VALUE_WRONG_FORMAT: string
|
declare const LOC_INVALID_VALUE_WRONG_FORMAT: string
|
||||||
|
@ -45,23 +45,30 @@ export class ChannelConfigurationElement extends LivechatElement {
|
|||||||
args: () => [this.registerClientOptions]
|
args: () => [this.registerClientOptions]
|
||||||
})
|
})
|
||||||
|
|
||||||
private readonly _saveConfig = (event?: Event): void => {
|
private readonly _saveConfig = async (event?: Event): Promise<void> => {
|
||||||
event?.preventDefault()
|
event?.preventDefault()
|
||||||
if (this._channelDetailsService && this._channelConfiguration) {
|
if (this._channelDetailsService && this._channelConfiguration) {
|
||||||
this._channelDetailsService.saveOptions(this._channelConfiguration.channel.id,
|
this._channelDetailsService.saveOptions(this._channelConfiguration.channel.id,
|
||||||
this._channelConfiguration.configuration)
|
this._channelConfiguration.configuration)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this._validationError = undefined
|
this._validationError = undefined
|
||||||
|
this.registerClientOptions?.peertubeHelpers.translate(LOC_SUCCESSFULLY_SAVED).then((msg) => {
|
||||||
this.registerClientOptions
|
this.registerClientOptions
|
||||||
?.peertubeHelpers.notifier.info('Livechat configuration has been properly updated.')
|
?.peertubeHelpers.notifier.info(msg)
|
||||||
|
})
|
||||||
this.requestUpdate('_validationError')
|
this.requestUpdate('_validationError')
|
||||||
})
|
})
|
||||||
.catch((error: ValidationError) => {
|
.catch(async (error: Error) => {
|
||||||
|
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}`)
|
console.warn(`A validation error occurred in saving configuration. ${error.name}: ${error.message}`)
|
||||||
this.registerClientOptions
|
this.registerClientOptions?.peertubeHelpers.notifier.error(
|
||||||
?.peertubeHelpers.notifier.error(
|
error.message
|
||||||
`An error occurred. ${(error.message) ? `${error.message}` : ''}`)
|
? error.message
|
||||||
|
: await this.registerClientOptions.peertubeHelpers.translate('error')
|
||||||
|
)
|
||||||
this.requestUpdate('_validationError')
|
this.requestUpdate('_validationError')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -38,21 +38,21 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
|
|
||||||
protected override render = (): unknown => {
|
protected override render = (): unknown => {
|
||||||
const tableHeaderList: DynamicFormHeader = {
|
const tableHeaderList: DynamicFormHeader = {
|
||||||
shortname: {
|
sn: {
|
||||||
colName: ptTr(LOC_LIVECHAT_EMOJIS_SHORTNAME),
|
colName: ptTr(LOC_LIVECHAT_EMOJIS_SHORTNAME),
|
||||||
description: ptTr(LOC_LIVECHAT_EMOJIS_SHORTNAME_DESC)
|
description: ptTr(LOC_LIVECHAT_EMOJIS_SHORTNAME_DESC)
|
||||||
},
|
},
|
||||||
file: {
|
url: {
|
||||||
colName: ptTr(LOC_LIVECHAT_EMOJIS_FILE),
|
colName: ptTr(LOC_LIVECHAT_EMOJIS_FILE),
|
||||||
description: ptTr(LOC_LIVECHAT_EMOJIS_FILE_DESC)
|
description: ptTr(LOC_LIVECHAT_EMOJIS_FILE_DESC)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tableSchema: DynamicFormSchema = {
|
const tableSchema: DynamicFormSchema = {
|
||||||
shortname: {
|
sn: {
|
||||||
inputType: 'text',
|
inputType: 'text',
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
file: {
|
url: {
|
||||||
inputType: 'image-file',
|
inputType: 'image-file',
|
||||||
default: ''
|
default: ''
|
||||||
}
|
}
|
||||||
@ -81,11 +81,11 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
.validation=${this._validationError?.properties}
|
.validation=${this._validationError?.properties}
|
||||||
.validationPrefix=${'emojis'}
|
.validationPrefix=${'emojis'}
|
||||||
.rows=${this._channelEmojisConfiguration?.emojis.customEmojis}
|
.rows=${this._channelEmojisConfiguration?.emojis.customEmojis}
|
||||||
@update=${(_e: CustomEvent) => {
|
@update=${(e: CustomEvent) => {
|
||||||
// if (this._channelEmojisConfiguration) {
|
if (this._channelEmojisConfiguration) {
|
||||||
// this._channelEmojisConfiguration.configuration.emojis.customEmojis = e.detail
|
this._channelEmojisConfiguration.emojis.customEmojis = e.detail
|
||||||
// this.requestUpdate('_channelEmojisConfiguration')
|
this.requestUpdate('_channelEmojisConfiguration')
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
></livechat-dynamic-table-form>
|
></livechat-dynamic-table-form>
|
||||||
@ -118,9 +118,32 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
args: () => []
|
args: () => []
|
||||||
})
|
})
|
||||||
|
|
||||||
private readonly _saveEmojis = (ev?: Event): void => {
|
private async _saveEmojis (ev?: Event): Promise<void> {
|
||||||
ev?.preventDefault()
|
ev?.preventDefault()
|
||||||
// TODO
|
const peertubeHelpers = this.registerClientOptions?.peertubeHelpers
|
||||||
this.registerClientOptions?.peertubeHelpers.notifier.error('TODO')
|
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 Mehdi Benadel <https://mehdibenadel.com>
|
||||||
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
||||||
import type { ValidationError } from '../../lib/models/validation'
|
|
||||||
import type {
|
import type {
|
||||||
ChannelLiveChatInfos, ChannelConfiguration, ChannelConfigurationOptions, ChannelEmojisConfiguration
|
ChannelLiveChatInfos, ChannelConfiguration, ChannelConfigurationOptions, ChannelEmojisConfiguration, ChannelEmojis
|
||||||
} from 'shared/lib/types'
|
} from 'shared/lib/types'
|
||||||
import { ValidationErrorType } from '../../lib/models/validation'
|
import { ValidationError, ValidationErrorType } from '../../lib/models/validation'
|
||||||
import { getBaseRoute } from '../../../utils/uri'
|
import { getBaseRoute } from '../../../utils/uri'
|
||||||
|
|
||||||
export class ChannelDetailsService {
|
export class ChannelDetailsService {
|
||||||
@ -22,53 +22,46 @@ export class ChannelDetailsService {
|
|||||||
this._headers['content-type'] = 'application/json;charset=UTF-8'
|
this._headers['content-type'] = 'application/json;charset=UTF-8'
|
||||||
}
|
}
|
||||||
|
|
||||||
validateOptions = (channelConfigurationOptions: ChannelConfigurationOptions): boolean => {
|
validateOptions = async (channelConfigurationOptions: ChannelConfigurationOptions): Promise<boolean> => {
|
||||||
let hasErrors = false
|
const propertiesError: ValidationError['properties'] = {}
|
||||||
const validationError: ValidationError = {
|
|
||||||
name: 'ChannelConfigurationOptionsValidationError',
|
|
||||||
message: 'There was an error during validation',
|
|
||||||
properties: {}
|
|
||||||
}
|
|
||||||
const botConf = channelConfigurationOptions.bot
|
const botConf = channelConfigurationOptions.bot
|
||||||
const slowModeDuration = channelConfigurationOptions.slowMode.duration
|
const slowModeDuration = channelConfigurationOptions.slowMode.duration
|
||||||
|
|
||||||
validationError.properties['slowMode.duration'] = []
|
propertiesError['slowMode.duration'] = []
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(typeof slowModeDuration !== 'number') ||
|
(typeof slowModeDuration !== 'number') ||
|
||||||
isNaN(slowModeDuration)) {
|
isNaN(slowModeDuration)
|
||||||
validationError.properties['slowMode.duration'].push(ValidationErrorType.WrongType)
|
) {
|
||||||
hasErrors = true
|
propertiesError['slowMode.duration'].push(ValidationErrorType.WrongType)
|
||||||
} else if (
|
} else if (
|
||||||
slowModeDuration < 0 ||
|
slowModeDuration < 0 ||
|
||||||
slowModeDuration > 1000
|
slowModeDuration > 1000
|
||||||
) {
|
) {
|
||||||
validationError.properties['slowMode.duration'].push(ValidationErrorType.NotInRange)
|
propertiesError['slowMode.duration'].push(ValidationErrorType.NotInRange)
|
||||||
hasErrors = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If !bot.enabled, we don't have to validate these fields:
|
// If !bot.enabled, we don't have to validate these fields:
|
||||||
// The backend will ignore those values.
|
// The backend will ignore those values.
|
||||||
if (botConf.enabled) {
|
if (botConf.enabled) {
|
||||||
validationError.properties['bot.nickname'] = []
|
propertiesError['bot.nickname'] = []
|
||||||
|
|
||||||
if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) {
|
if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) {
|
||||||
validationError.properties['bot.nickname'].push(ValidationErrorType.WrongFormat)
|
propertiesError['bot.nickname'].push(ValidationErrorType.WrongFormat)
|
||||||
hasErrors = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [i, fw] of botConf.forbiddenWords.entries()) {
|
for (const [i, fw] of botConf.forbiddenWords.entries()) {
|
||||||
for (const v of fw.entries) {
|
for (const v of fw.entries) {
|
||||||
validationError.properties[`bot.forbiddenWords.${i}.entries`] = []
|
propertiesError[`bot.forbiddenWords.${i}.entries`] = []
|
||||||
if (fw.regexp) {
|
if (fw.regexp) {
|
||||||
if (v.trim() !== '') {
|
if (v.trim() !== '') {
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line no-new
|
// eslint-disable-next-line no-new
|
||||||
new RegExp(v)
|
new RegExp(v)
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
validationError.properties[`bot.forbiddenWords.${i}.entries`]
|
propertiesError[`bot.forbiddenWords.${i}.entries`]
|
||||||
.push(ValidationErrorType.WrongFormat)
|
.push(ValidationErrorType.WrongFormat)
|
||||||
hasErrors = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,16 +69,20 @@ export class ChannelDetailsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [i, cd] of botConf.commands.entries()) {
|
for (const [i, cd] of botConf.commands.entries()) {
|
||||||
validationError.properties[`bot.commands.${i}.command`] = []
|
propertiesError[`bot.commands.${i}.command`] = []
|
||||||
|
|
||||||
if (/\s+/.test(cd.command)) {
|
if (/\s+/.test(cd.command)) {
|
||||||
validationError.properties[`bot.commands.${i}.command`].push(ValidationErrorType.WrongFormat)
|
propertiesError[`bot.commands.${i}.command`].push(ValidationErrorType.WrongFormat)
|
||||||
hasErrors = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
throw validationError
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +158,7 @@ export class ChannelDetailsService {
|
|||||||
return response.json()
|
return response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchEmojisConfiguration = async (channelId: number): Promise<ChannelEmojisConfiguration> => {
|
public async fetchEmojisConfiguration (channelId: number): Promise<ChannelEmojisConfiguration> {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
getBaseRoute(this._registerClientOptions) +
|
getBaseRoute(this._registerClientOptions) +
|
||||||
'/api/configuration/channel/emojis/' +
|
'/api/configuration/channel/emojis/' +
|
||||||
@ -181,4 +178,56 @@ export class ChannelDetailsService {
|
|||||||
|
|
||||||
return response.json()
|
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> {
|
private async _upload (ev: Event): Promise<void> {
|
||||||
ev.preventDefault()
|
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 target = ev.target
|
||||||
const file = (target as HTMLInputElement).files?.[0]
|
const file = (target as HTMLInputElement).files?.[0]
|
||||||
if (!file) {
|
if (!file) {
|
||||||
this.value = ''
|
this.value = ''
|
||||||
|
const event = new Event('change')
|
||||||
|
this.dispatchEvent(event)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,6 +84,8 @@ export class ImageFileInputElement extends LivechatElement {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.value = base64
|
this.value = base64
|
||||||
|
const event = new Event('change')
|
||||||
|
this.dispatchEvent(event)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// FIXME: use peertube notifier?
|
// FIXME: use peertube notifier?
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
@ -10,4 +10,10 @@ export enum ValidationErrorType {
|
|||||||
|
|
||||||
export class ValidationError extends Error {
|
export class ValidationError extends Error {
|
||||||
properties: {[key: string]: ValidationErrorType[] } = {}
|
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_banned_jids_label: "Banned users and patterns"
|
||||||
livechat_configuration_channel_bot_nickname: "Bot nickname"
|
livechat_configuration_channel_bot_nickname: "Bot nickname"
|
||||||
|
|
||||||
|
validation_error: "There was an error during validation."
|
||||||
invalid_value: "Invalid value."
|
invalid_value: "Invalid value."
|
||||||
invalid_value_wrong_type: "Value is of the wrong type."
|
invalid_value_wrong_type: "Value is of the wrong type."
|
||||||
invalid_value_wrong_format: "Value is in the wrong format."
|
invalid_value_wrong_format: "Value is in the wrong format."
|
||||||
|
Loading…
x
Reference in New Issue
Block a user