Technically working options. Need CSS fix (plugin CSS not loaded) and data validation
This commit is contained in:
		
							
								
								
									
										1
									
								
								assets/images/plus-square.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								assets/images/plus-square.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-square"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg> | ||||
| After Width: | Height: | Size: 373 B | 
							
								
								
									
										1
									
								
								assets/images/x-square.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								assets/images/x-square.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-square"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></svg> | ||||
| After Width: | Height: | Size: 368 B | 
| @ -39,23 +39,10 @@ function loadLocs() { | ||||
|   return r | ||||
| } | ||||
|  | ||||
| function loadMustaches () { | ||||
|   // Loading mustache templates, dans filling constants. | ||||
|   const r = [] | ||||
|   r['MUSTACHE_CONFIGURATION_HOME'] = loadMustache('client/common/configuration/templates/home.mustache') | ||||
|   r['MUSTACHE_CONFIGURATION_CHANNEL'] = loadMustache('client/common/configuration/templates/channel.mustache') | ||||
|   return r | ||||
| } | ||||
|  | ||||
| function loadMustache (file) { | ||||
|   const filePath = path.resolve(__dirname, file) | ||||
|   return JSON.stringify(fs.readFileSync(filePath).toString()) | ||||
| } | ||||
|  | ||||
| const define = Object.assign({ | ||||
|   PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name), | ||||
|   PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, '')) | ||||
| }, loadLocs(), loadMustaches()) | ||||
| }, loadLocs()) | ||||
|  | ||||
| const configs = clientFiles.map(f => ({ | ||||
|   entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ], | ||||
|  | ||||
| @ -29,7 +29,8 @@ async function registerConfiguration (clientOptions: RegisterClientOptions): Pro | ||||
|     onMount: async ({ rootEl }) => { | ||||
|       const urlParams = new URLSearchParams(window.location.search) | ||||
|       const channelId = urlParams.get('channelId') ?? '' | ||||
|       render(html`<channel-configuration .registerClientOptions=${clientOptions}></channel-configuration>`, rootEl) | ||||
|       render(html`<channel-configuration .registerClientOptions=${clientOptions} | ||||
|                                          .channelId=${channelId}></channel-configuration>`, rootEl) | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|  | ||||
| @ -1,33 +1,53 @@ | ||||
| import { RegisterClientOptions } from '@peertube/peertube-types/client' | ||||
| import { html, LitElement } from 'lit' | ||||
| import { css, html, LitElement } from 'lit' | ||||
| import { repeat } from 'lit-html/directives/repeat.js' | ||||
| import { customElement, property } from 'lit/decorators.js' | ||||
| import { customElement, property, state } from 'lit/decorators.js' | ||||
| import { ptTr } from './TranslationDirective' | ||||
| import { localizedHelpUrl } from '../../../utils/help' | ||||
| import './DynamicTableFormElement' | ||||
| import './PluginConfigurationRow' | ||||
| import './HelpButtonElement' | ||||
| import { until } from 'async' | ||||
| import { Task } from '@lit/task'; | ||||
| import { ChannelConfiguration } from 'shared/lib/types' | ||||
| import { ChannelConfigurationService } from './ChannelConfigurationService' | ||||
| import { createContext, provide } from '@lit/context' | ||||
| import { getGlobalStyleSheets } from '../../global-styles' | ||||
|  | ||||
| export const registerClientOptionsContext = createContext<RegisterClientOptions | undefined>(Symbol('register-client-options')); | ||||
| export const channelConfigurationContext = createContext<ChannelConfiguration | undefined>(Symbol('channel-configuration')); | ||||
| export const channelConfigurationServiceContext = createContext<ChannelConfigurationService | undefined>(Symbol('channel-configuration-service')); | ||||
|  | ||||
| @customElement('channel-configuration') | ||||
| export class ChannelConfigurationElement extends LitElement { | ||||
|  | ||||
|   @provide({ context: registerClientOptionsContext }) | ||||
|   @property({ attribute: false }) | ||||
|   public registerClientOptions: RegisterClientOptions | undefined | ||||
|  | ||||
|   createRenderRoot = () => { | ||||
|     return this | ||||
|   } | ||||
|   @property({ attribute: false }) | ||||
|   public channelId: number | undefined | ||||
|  | ||||
|   @provide({ context: channelConfigurationContext }) | ||||
|   @state() | ||||
|   public _channelConfiguration: ChannelConfiguration | undefined | ||||
|  | ||||
|   @provide({ context: channelConfigurationServiceContext }) | ||||
|   private _configurationService: ChannelConfigurationService | undefined | ||||
|  | ||||
|   static styles = [ | ||||
|     ...getGlobalStyleSheets() | ||||
|   ]; | ||||
|  | ||||
|   @state() | ||||
|   public _formStatus: boolean | any = undefined | ||||
|  | ||||
|   private _asyncTaskRender = new Task(this, { | ||||
|  | ||||
|     task: async ([registerClientOptions], {signal}) => { | ||||
|       let link = registerClientOptions ? await localizedHelpUrl(registerClientOptions, { page: 'documentation/user/streamers/bot/forbidden_words' }) : ''; | ||||
|  | ||||
|       return { | ||||
|         url : new URL(link), | ||||
|         title: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC) | ||||
|       if (this.registerClientOptions) { | ||||
|         this._configurationService = new ChannelConfigurationService(this.registerClientOptions) | ||||
|         this._channelConfiguration = await this._configurationService.fetchConfiguration(this.channelId ?? 0) | ||||
|       } | ||||
|     }, | ||||
|  | ||||
| @ -35,9 +55,26 @@ export class ChannelConfigurationElement extends LitElement { | ||||
|  | ||||
|   }); | ||||
|  | ||||
|   private _saveConfig = () => { | ||||
|     if(this._configurationService && this._channelConfiguration) { | ||||
|       this._configurationService.saveOptions(this._channelConfiguration.channel.id, this._channelConfiguration.configuration) | ||||
|       .then((value) => { | ||||
|         this._formStatus = { success: true } | ||||
|         console.log(`Configuration has been updated`) | ||||
|         this.requestUpdate('_formStatus') | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         this._formStatus = error | ||||
|         console.log(`An error occurred : ${JSON.stringify(this._formStatus)}`) | ||||
|         this.requestUpdate('_formStatus') | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   render = () => { | ||||
|     let tableHeader = { | ||||
|       words: { | ||||
|     let tableHeaderList = { | ||||
|       forbiddenWords: { | ||||
|         entries: { | ||||
|           colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL), | ||||
|           description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2) | ||||
|         }, | ||||
| @ -61,15 +98,38 @@ export class ChannelConfigurationElement extends LitElement { | ||||
|           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) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     let tableSchema = { | ||||
|       words: { | ||||
|         inputType: 'text', | ||||
|         default: 'helloqwesad' | ||||
|       forbiddenWords: { | ||||
|         entries: { | ||||
|           inputType: 'textarea', | ||||
|           default: ['helloqwesad'], | ||||
|           separator: '\n', | ||||
|         }, | ||||
|         regex: { | ||||
|           inputType: 'text', | ||||
|         default: 'helloaxzca' | ||||
|           default: 'helloaxzca', | ||||
|         }, | ||||
|         applyToModerators: { | ||||
|           inputType: 'checkbox', | ||||
| @ -80,10 +140,9 @@ export class ChannelConfigurationElement extends LitElement { | ||||
|           default: 'helloasx' | ||||
|         }, | ||||
|         reason: { | ||||
|         inputType: 'select', | ||||
|           inputType: 'text', | ||||
|           default: 'transphobia', | ||||
|         label: 'choose your poison', | ||||
|         options: {'racism': 'Racism', 'sexism': 'Sexism', 'transphobia': 'Transphobia', 'bigotry': 'Bigotry'} | ||||
|           datalist: ['Racism', 'Sexism', 'Transphobia', 'Bigotry'] | ||||
|         }, | ||||
|         comments: { | ||||
|           inputType: 'textarea', | ||||
| @ -95,52 +154,179 @@ export class ChannelConfigurationElement extends LitElement { | ||||
|           pariatur. Excepteur sint occaecat cupidatat non proident, sunt in | ||||
|           culpa qui officia deserunt mollit anim id est laborum.` | ||||
|         }, | ||||
|       }, | ||||
|       quotes: { | ||||
|         messages: { | ||||
|           inputType: 'textarea', | ||||
|           default: ['default message'], | ||||
|           separator: '\n', | ||||
|         }, | ||||
|         delay: { | ||||
|           inputType: 'number', | ||||
|           default: 100, | ||||
|         } | ||||
|     let tableRows = [ | ||||
|       { | ||||
|         words: 'teweqwst', | ||||
|         regex: 'tesdgst', | ||||
|         applyToModerators: false, | ||||
|         label: 'teswet', | ||||
|         reason: 'sexism', | ||||
|         comments: 'tsdaswest', | ||||
|       }, | ||||
|       { | ||||
|         words: 'tedsadst', | ||||
|         regex: 'tezxccst', | ||||
|         applyToModerators: true, | ||||
|         label: 'tewest', | ||||
|         reason: 'racism', | ||||
|         comments: 'tesxzct', | ||||
|       commands: { | ||||
|         command: { | ||||
|           inputType: 'text', | ||||
|           default: 'default command', | ||||
|         }, | ||||
|       { | ||||
|         words: 'tesadsdxst', | ||||
|         regex: 'dsfsdf', | ||||
|         applyToModerators: false, | ||||
|         label: 'tesdadst', | ||||
|         reason: 'bigotry', | ||||
|         comments: 'tsadest', | ||||
|       }, | ||||
|     ] | ||||
|         message: { | ||||
|           inputType: 'text', | ||||
|           default: 'default message', | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return this._asyncTaskRender.render({ | ||||
|       complete: (helpLink) => html` | ||||
|         <div class="container"> | ||||
|           <channel-configuration></channel-configuration> | ||||
|       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> | ||||
|             <help-button .page="documentation/user/streamers/channel"> | ||||
|             </help-button> | ||||
|           </h1> | ||||
|           <p>${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_DESC)}</p> | ||||
|           <form livechat-configuration-channel-options role="form"> | ||||
|           <div class="row mt-3"> | ||||
|             <plugin-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"}> | ||||
|               <div class="form-group"> | ||||
|                 <label> | ||||
|                   <input | ||||
|                     type="number" | ||||
|                     name="slow_mode_duration" | ||||
|                     class="form-control" | ||||
|                     min="0" | ||||
|                     max="1000" | ||||
|                     id="peertube-livechat-slow-mode-duration" | ||||
|                     @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> | ||||
|               </div> | ||||
|             </plugin-configuration-row> | ||||
|             <plugin-configuration-row | ||||
|               .title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)} | ||||
|               .description=${''} | ||||
|               .helpPage=${"documentation/user/streamers/channel"}> | ||||
|               <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" | ||||
|                   id="peertube-livechat-bot-nickname" | ||||
|                   @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}" | ||||
|                 /> | ||||
|               </div>` | ||||
|                 : '' | ||||
|               } | ||||
|             </plugin-configuration-row> | ||||
|             ${this._channelConfiguration?.configuration.bot.enabled ? | ||||
|               html`<plugin-configuration-row | ||||
|                 .title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)} | ||||
|                 .description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)} | ||||
|             .helpLink=${helpLink} | ||||
|           > | ||||
|                 .helpPage=${"documentation/user/streamers/bot/forbidden_words"}> | ||||
|                 <dynamic-table-form | ||||
|               .header=${tableHeader} | ||||
|               .schema=${tableSchema} | ||||
|               .rows=${tableRows} | ||||
|               .formName=${'forbidden-words'} | ||||
|             > | ||||
|                   .header=${tableHeaderList.forbiddenWords} | ||||
|                   .schema=${tableSchema.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'}> | ||||
|                 </dynamic-table-form> | ||||
|               </plugin-configuration-row> | ||||
|               <plugin-configuration-row | ||||
|                 .title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL)} | ||||
|                 .description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC)} | ||||
|                 .helpPage=${"documentation/user/streamers/bot/quotes"}> | ||||
|                 <dynamic-table-form | ||||
|                   .header=${tableHeaderList.quotes} | ||||
|                   .schema=${tableSchema.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'}> | ||||
|                 </dynamic-table-form> | ||||
|               </plugin-configuration-row> | ||||
|               <plugin-configuration-row | ||||
|                 .title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_LABEL)} | ||||
|                 .description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_DESC)} | ||||
|                 .helpPage=${"documentation/user/streamers/bot/commands"}> | ||||
|                 <dynamic-table-form | ||||
|                   .header=${tableHeaderList.commands} | ||||
|                   .schema=${tableSchema.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'}> | ||||
|                 </dynamic-table-form> | ||||
|               </plugin-configuration-row>` | ||||
|               : '' | ||||
|             } | ||||
|             <div class="form-group mt-5"> | ||||
|               <button type="button" class="orange-button" @click=${this._saveConfig}>${ptTr(LOC_SAVE)}</button> | ||||
|             </div> | ||||
|             ${(this._formStatus && this._formStatus.success === undefined) ? | ||||
|               html`<div class="alert alert-warning" role="alert"> | ||||
|                 An error occurred : ${JSON.stringify(this._formStatus)} | ||||
|               </div>` | ||||
|               : '' | ||||
|             } | ||||
|             ${(this._formStatus && this._formStatus.success === true) ? | ||||
|               html`<div class="alert alert-success" role="alert"> | ||||
|                 Configuration has been updated | ||||
|               </div>` | ||||
|               : '' | ||||
|             } | ||||
|           </form> | ||||
|         </div>${JSON.stringify(this._channelConfiguration)}` | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,60 @@ | ||||
| import { RegisterClientOptions } from "@peertube/peertube-types/client" | ||||
| import { ChannelConfiguration, ChannelConfigurationOptions } from "shared/lib/types" | ||||
| import { getBaseRoute } from "../../../utils/uri" | ||||
|  | ||||
|  | ||||
| export class ChannelConfigurationService { | ||||
|  | ||||
|   public _registerClientOptions: RegisterClientOptions | ||||
|  | ||||
|   private _headers : any = {} | ||||
|  | ||||
|   constructor(registerClientOptions: RegisterClientOptions) { | ||||
|     this._registerClientOptions = registerClientOptions | ||||
|  | ||||
|     this._headers = this._registerClientOptions.peertubeHelpers.getAuthHeader() ?? {} | ||||
|     this._headers['content-type'] = 'application/json;charset=UTF-8' | ||||
|   } | ||||
|  | ||||
|   validateOptions = (channelConfigurationOptions: ChannelConfigurationOptions) => { | ||||
|     return true | ||||
|   } | ||||
|  | ||||
|   saveOptions = async (channelId: number, channelConfigurationOptions: ChannelConfigurationOptions) => { | ||||
|     if (!await this.validateOptions(channelConfigurationOptions)) { | ||||
|       throw new Error('Invalid form data') | ||||
|     } | ||||
|  | ||||
|  | ||||
|     const response = await fetch( | ||||
|       getBaseRoute(this._registerClientOptions) + '/api/configuration/channel/' + encodeURIComponent(channelId), | ||||
|       { | ||||
|         method: 'POST', | ||||
|         headers: this._headers, | ||||
|         body: JSON.stringify(channelConfigurationOptions) | ||||
|       } | ||||
|     ) | ||||
|  | ||||
|     if (!response.ok) { | ||||
|       throw new Error('Failed to save configuration options.') | ||||
|     } | ||||
|  | ||||
|     return await response.json() | ||||
|   } | ||||
|  | ||||
|   fetchConfiguration = async (channelId: number): Promise<ChannelConfiguration> => { | ||||
|     const response = await fetch( | ||||
|       getBaseRoute(this._registerClientOptions) + '/api/configuration/channel/' + encodeURIComponent(channelId), | ||||
|       { | ||||
|         method: 'GET', | ||||
|         headers: this._headers | ||||
|       } | ||||
|     ) | ||||
|  | ||||
|     if (!response.ok) { | ||||
|       throw new Error('Can\'t get channel configuration options.') | ||||
|     } | ||||
|  | ||||
|     return await response.json() | ||||
|   } | ||||
| } | ||||
| @ -1,11 +1,26 @@ | ||||
| import { html, LitElement, TemplateResult } from 'lit' | ||||
| import { css, html, LitElement, nothing, TemplateResult } from 'lit' | ||||
| import { repeat } from 'lit/directives/repeat.js' | ||||
| import { customElement, property, state } from 'lit/decorators.js' | ||||
| import { unsafeHTML } from 'lit/directives/unsafe-html.js' | ||||
| import { ifDefined } from 'lit/directives/if-defined.js' | ||||
| import { StaticValue, unsafeStatic } from 'lit/static-html.js' | ||||
| import { getGlobalStyleSheets } from '../../global-styles' | ||||
| import { unsafeHTML } from 'lit/directives/unsafe-html.js' | ||||
|  | ||||
| type DynamicTableAcceptedTypes = number | string | boolean | Date | ||||
|   // This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/ | ||||
| const AddSVG: string = | ||||
|   `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-square"> | ||||
|     <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect> | ||||
|     <line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line> | ||||
|   </svg>` | ||||
|  | ||||
|   // This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/ | ||||
| const RemoveSVG: string = | ||||
|   `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-square"> | ||||
|     <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect> | ||||
|     <line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line> | ||||
|   </svg>` | ||||
|  | ||||
|  | ||||
| type DynamicTableAcceptedTypes = number | string | boolean | Date | Array<number | string> | ||||
|  | ||||
| type DynamicTableAcceptedInputTypes = 'textarea' | ||||
|   | 'select' | ||||
| @ -36,6 +51,8 @@ interface CellDataSchema { | ||||
|   size?: number | ||||
|   label?: string | ||||
|   options?: { [key: string]: string } | ||||
|   datalist?: DynamicTableAcceptedTypes[] | ||||
|   separator?: string | ||||
|   inputType?: DynamicTableAcceptedInputTypes | ||||
|   default?: DynamicTableAcceptedTypes | ||||
| } | ||||
| @ -50,49 +67,94 @@ export class DynamicTableFormElement extends LitElement { | ||||
|   @property({ attribute: false }) | ||||
|   public schema: { [key: string]: CellDataSchema } = {} | ||||
|  | ||||
|   @property({ attribute: false }) | ||||
|   public rows: { [key: string]: DynamicTableAcceptedTypes }[] = [] | ||||
|  | ||||
|   @property({ reflect: true }) | ||||
|   public rows: { _id: number; [key : string]: DynamicTableAcceptedTypes }[] = [] | ||||
|  | ||||
|   @state() | ||||
|   public _rowsById: { _id: number; row: { [key: string]: DynamicTableAcceptedTypes } }[] = [] | ||||
|  | ||||
|   @property({ attribute: false }) | ||||
|   public formName: string = '' | ||||
|  | ||||
|  | ||||
|   @state() | ||||
|   private _lastRowId = 1 | ||||
|  | ||||
|   createRenderRoot = () => { | ||||
|     return this | ||||
|   @property({ attribute: false }) | ||||
|   private _colOrder: string[] = [] | ||||
|  | ||||
|   static styles = [ | ||||
|     ...getGlobalStyleSheets(), | ||||
|     css` | ||||
|       :host table { | ||||
|         table-layout: fixed; | ||||
|         text-align: center; | ||||
|       } | ||||
|  | ||||
|   private _getDefaultRow = () => { | ||||
|     return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? '']), ['_id', this._lastRowId++]]) | ||||
|       :host table td, table th { | ||||
|         word-wrap:break-word; | ||||
|         vertical-align: top; | ||||
|         padding: 5px 7px; | ||||
|       } | ||||
|  | ||||
|       :host table tbody > :nth-child(odd) { | ||||
|         background-color: var(--greySecondaryBackgroundColor); | ||||
|       } | ||||
|  | ||||
|       :host button { | ||||
|         padding: 2px; | ||||
|       } | ||||
|  | ||||
|       :host .dynamic-table-add-row { | ||||
|         background-color: var(--bs-green); | ||||
|       } | ||||
|  | ||||
|       :host .dynamic-table-remove-row { | ||||
|         background-color: var(--bs-orange); | ||||
|       } | ||||
|     ` | ||||
|   ]; | ||||
|  | ||||
|   // fixes situations when list has been reinitialized or changed outside of CustomElement | ||||
|   private _updateLastRowId = () => { | ||||
|     for (let rowById of this._rowsById) { | ||||
|       this._lastRowId = Math.max(this._lastRowId, rowById._id + 1); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _getDefaultRow = () : { [key: string]: DynamicTableAcceptedTypes } => { | ||||
|     this._updateLastRowId() | ||||
|  | ||||
|     return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? ''])]) | ||||
|  | ||||
|   } | ||||
|  | ||||
|   private _addRow = () => { | ||||
|       this.rows.push(this._getDefaultRow()) | ||||
|  | ||||
|     let newRow = this._getDefaultRow() | ||||
|     this._rowsById.push({_id:this._lastRowId++, row: newRow}) | ||||
|     this.rows.push(newRow) | ||||
|     this.requestUpdate('rows') | ||||
|  | ||||
|     this.dispatchEvent(new CustomEvent('update', { detail: this.rows })) | ||||
|   } | ||||
|  | ||||
|  | ||||
|   private _removeRow = (rowId: number) => { | ||||
|       this.rows = this.rows.filter((x) => x._id != rowId) | ||||
|  | ||||
|     let rowToRemove = this._rowsById.filter(rowById => rowById._id == rowId).map(rowById => rowById.row)[0] | ||||
|     this._rowsById = this._rowsById.filter((rowById) => rowById._id !== rowId) | ||||
|     this.rows = this.rows.filter((row) => row !== rowToRemove) | ||||
|     this.requestUpdate('rows') | ||||
|  | ||||
|     this.dispatchEvent(new CustomEvent('update', { detail: this.rows })) | ||||
|   } | ||||
|  | ||||
|  | ||||
|   render = () => { | ||||
|     const inputId = `peertube-livechat-${this.formName.replaceAll('_', '-')}-table` | ||||
|  | ||||
|     this._updateLastRowId() | ||||
|  | ||||
|     this._rowsById.filter(rowById => this.rows.includes(rowById.row)) | ||||
|  | ||||
|     for (let row of this.rows) { | ||||
|       if (!row._id) { | ||||
|         row._id = this._lastRowId++ | ||||
|       if (this._rowsById.filter(rowById => rowById.row === row).length == 0) { | ||||
|         this._rowsById.push({_id: this._lastRowId++, row }) | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @ -100,44 +162,55 @@ export class DynamicTableFormElement extends LitElement { | ||||
|       <table class="table table-striped table-hover table-sm" id=${inputId}> | ||||
|         ${this._renderHeader()} | ||||
|         <tbody> | ||||
|           ${repeat(this.rows,(row) => row._id, this._renderDataRow)} | ||||
|           ${repeat(this._rowsById, (rowById) => rowById._id, this._renderDataRow)} | ||||
|         </tbody> | ||||
|         <tfoot> | ||||
|           <tr><td><button @click=${this._addRow}>Add Row</button></td></tr> | ||||
|         </tfoot> | ||||
|         ${this._renderFooter()} | ||||
|       </table> | ||||
|     ` | ||||
|  | ||||
|   } | ||||
|  | ||||
|   private _renderHeader = () => { | ||||
|     if (this._colOrder.length !== Object.keys(this.header).length) { | ||||
|       this._colOrder = this._colOrder.filter(key => Object.keys(this.header).includes(key)) | ||||
|       this._colOrder.push(...Object.keys(this.header).filter(key => !this._colOrder.includes(key))) | ||||
|     } | ||||
|  | ||||
|     return html`<thead> | ||||
|       <tr> | ||||
|         <!-- <th scope="col">#</th> --> | ||||
|         ${Object.values(this.header).map(this._renderHeaderCell)} | ||||
|         <th scope="col">Remove Row</th> | ||||
|         ${Object.entries(this.header).sort(([k1,_1], [k2,_2]) => this._colOrder.indexOf(k1) - this._colOrder.indexOf(k2)) | ||||
|                                      .map(([k,v]) => this._renderHeaderCell(v))} | ||||
|         <th scope="col"></th> | ||||
|       </tr> | ||||
|     </thead>` | ||||
|  | ||||
|   } | ||||
|  | ||||
|   private _renderHeaderCell = (headerCellData: { colName: TemplateResult, description: TemplateResult }) => { | ||||
|     return html`<th scope="col"> | ||||
|       <div data-toggle="tooltip" data-placement="bottom" data-html="true" title="${headerCellData.description}">${headerCellData.colName}</div> | ||||
|       <div data-toggle="tooltip" data-placement="bottom" data-html="true" title=${headerCellData.description}>${headerCellData.colName}</div> | ||||
|     </th>` | ||||
|   } | ||||
|  | ||||
|   private _renderDataRow = (rowData: { _id: number; [key : string]: DynamicTableAcceptedTypes }) => { | ||||
|   private _renderDataRow = (rowData: { _id: number; row: {[key: string]: DynamicTableAcceptedTypes} }) => { | ||||
|     const inputId = `peertube-livechat-${this.formName.replaceAll('_', '-')}-row-${rowData._id}` | ||||
|  | ||||
|     return html`<tr id=${inputId}> | ||||
|       <!-- <td class="form-group">${rowData._id}</td> --> | ||||
|       ${Object.entries(rowData).filter(([k,v]) => k != '_id').map((data) => this.renderDataCell(data, rowData._id))} | ||||
|       <td class="form-group"><button @click=${() => this._removeRow(rowData._id)}>Remove</button></td> | ||||
|       ${Object.entries(rowData.row).filter(([k, v]) => k != '_id') | ||||
|                                    .sort(([k1,_1], [k2,_2]) => this._colOrder.indexOf(k1) - this._colOrder.indexOf(k2)) | ||||
|                                    .map((data) => this.renderDataCell(data, rowData._id))} | ||||
|       <td class="form-group"><button class="btn dynamic-table-remove-row" @click=${() => this._removeRow(rowData._id)}>${unsafeHTML(RemoveSVG)}</button></td> | ||||
|     </tr>` | ||||
|  | ||||
|   } | ||||
|  | ||||
|   private _renderFooter = () => { | ||||
|     return html`<tfoot> | ||||
|     <tr> | ||||
|       ${Object.values(this.header).map(() => html`<td></td>`)} | ||||
|       <td><button class="btn dynamic-table-add-row" @click=${this._addRow}>${unsafeHTML(AddSVG)}</button></td> | ||||
|     </tr> | ||||
|   </tfoot>` | ||||
|   } | ||||
|  | ||||
|   renderDataCell = (property: [string, DynamicTableAcceptedTypes], rowId: number) => { | ||||
|     const [propertyName, propertyValue] = property | ||||
|     const propertySchema = this.schema[propertyName] ?? {} | ||||
| @ -169,48 +242,16 @@ export class DynamicTableFormElement extends LitElement { | ||||
|           case 'time': | ||||
|           case 'url': | ||||
|           case 'week': | ||||
|             formElement = html`<input | ||||
|               type=${propertySchema.inputType} | ||||
|               name=${inputName} | ||||
|               class="form-control" | ||||
|               id=${inputId} | ||||
|               min=${ifDefined(propertySchema?.min)} | ||||
|               max=${ifDefined(propertySchema?.max)} | ||||
|               minlength=${ifDefined(propertySchema?.minlength)} | ||||
|               maxlength=${ifDefined(propertySchema?.maxlength)} | ||||
|               @input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)} | ||||
|               .value=${propertyValue as string} | ||||
|             />` | ||||
|             formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string) | ||||
|             break | ||||
|  | ||||
|           case 'textarea': | ||||
|             formElement = html`<textarea | ||||
|               name=${inputName} | ||||
|               class="form-control" | ||||
|               id=${inputId} | ||||
|               min=${ifDefined(propertySchema?.min)} | ||||
|               max=${ifDefined(propertySchema?.max)} | ||||
|               minlength=${ifDefined(propertySchema?.minlength)} | ||||
|               maxlength=${ifDefined(propertySchema?.maxlength)} | ||||
|               @input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)} | ||||
|               .value=${propertyValue as string} | ||||
|             ></textarea>` | ||||
|             formElement = this._renderTextarea(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string) | ||||
|             break | ||||
|  | ||||
|           case 'select': | ||||
|             formElement = html`<select | ||||
|               class="form-select" | ||||
|               aria-label="Default select example" | ||||
|               @change=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)} | ||||
|             > | ||||
|               <option ?selected=${!propertyValue}>${propertySchema?.label ?? 'Choose your option'}</option> | ||||
|               ${Object.entries(propertySchema?.options ?? {}) | ||||
|                       ?.map(([value,name]) =>  | ||||
|                         html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>` | ||||
|                       )} | ||||
|             </select>` | ||||
|             formElement = this._renderSelect(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string) | ||||
|             break | ||||
|  | ||||
|         } | ||||
|         break | ||||
|  | ||||
| @ -223,20 +264,8 @@ export class DynamicTableFormElement extends LitElement { | ||||
|           case 'datetime': | ||||
|           case 'datetime-local': | ||||
|           case 'time': | ||||
|               formElement = html`<input | ||||
|                 type=${propertySchema.inputType} | ||||
|                 name=${inputName} | ||||
|                 class="form-control" | ||||
|                 id=${inputId} | ||||
|               min=${ifDefined(propertySchema?.min)} | ||||
|               max=${ifDefined(propertySchema?.max)} | ||||
|               minlength=${ifDefined(propertySchema?.minlength)} | ||||
|               maxlength=${ifDefined(propertySchema?.maxlength)} | ||||
|               @input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)} | ||||
|               .value=${(propertyValue as Date).toISOString()} | ||||
|               />` | ||||
|             formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema, (propertyValue as Date).toISOString()) | ||||
|             break | ||||
|  | ||||
|         } | ||||
|         break | ||||
|  | ||||
| @ -247,20 +276,8 @@ export class DynamicTableFormElement extends LitElement { | ||||
|  | ||||
|           case 'number': | ||||
|           case 'range': | ||||
|             formElement = html`<input | ||||
|               type=${propertySchema.inputType} | ||||
|               name=${inputName} | ||||
|               class="form-control" | ||||
|               id=${inputId} | ||||
|               min=${ifDefined(propertySchema?.min)} | ||||
|               max=${ifDefined(propertySchema?.max)} | ||||
|               minlength=${ifDefined(propertySchema?.minlength)} | ||||
|               maxlength=${ifDefined(propertySchema?.maxlength)} | ||||
|               @input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)} | ||||
|               .value=${propertyValue as String} | ||||
|             />` | ||||
|             formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as string) | ||||
|             break | ||||
|  | ||||
|         } | ||||
|         break | ||||
|  | ||||
| @ -270,44 +287,130 @@ export class DynamicTableFormElement extends LitElement { | ||||
|             propertySchema.inputType = 'checkbox' | ||||
|  | ||||
|           case 'checkbox': | ||||
|             formElement = html`<input | ||||
|               type="checkbox" | ||||
|               name=${inputName} | ||||
|               class="form-check-input" | ||||
|               id=${inputId} | ||||
|               @input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, rowId)} | ||||
|               .value=${propertyValue as String} | ||||
|               ?checked=${propertyValue as Boolean} | ||||
|             />` | ||||
|             formElement = this._renderCheckbox(rowId, inputId, inputName, propertyName, propertySchema, propertyValue as boolean) | ||||
|             break | ||||
|  | ||||
|         } | ||||
|         break | ||||
|  | ||||
|       case Array: | ||||
|         switch (propertySchema.inputType) { | ||||
|           case undefined: | ||||
|             propertySchema.inputType = 'text' | ||||
|  | ||||
|           case 'text': | ||||
|           case 'color': | ||||
|           case 'date': | ||||
|           case 'datetime': | ||||
|           case 'datetime-local': | ||||
|           case 'email': | ||||
|           case 'file': | ||||
|           case 'image': | ||||
|           case 'month': | ||||
|           case 'number': | ||||
|           case 'password': | ||||
|           case 'range': | ||||
|           case 'tel': | ||||
|           case 'time': | ||||
|           case 'url': | ||||
|           case 'week': | ||||
|             formElement = this._renderInput(rowId, inputId, inputName, propertyName, propertySchema, | ||||
|               (propertyValue as Array<number | string>).join(propertySchema.separator ?? ',')) | ||||
|             break | ||||
|           case 'textarea': | ||||
|             formElement = this._renderTextarea(rowId, inputId, inputName, propertyName, propertySchema, | ||||
|               (propertyValue as Array<number | string>).join(propertySchema.separator ?? ',')) | ||||
|             break | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (!formElement) { | ||||
|       console.warn(`value type '${propertyValue.constructor}' is incompatible` | ||||
|         + `with field type '${propertySchema.inputType}' for form entry '${propertyName.toString()}'.`) | ||||
|  | ||||
|     } | ||||
|  | ||||
|     return html`<td class="form-group">${formElement}</td>` | ||||
|   } | ||||
|  | ||||
|   _renderInput = (rowId: number, inputId: string, inputName: string, propertyName: string, propertySchema: CellDataSchema, propertyValue: string) => { | ||||
|     return html`<input | ||||
|       type=${propertySchema.inputType} | ||||
|       name=${inputName} | ||||
|       class="form-control" | ||||
|       id=${inputId} | ||||
|       list=${(propertySchema?.datalist) ? inputId + '-datalist' : nothing} | ||||
|       min=${ifDefined(propertySchema?.min)} | ||||
|       max=${ifDefined(propertySchema?.max)} | ||||
|       minlength=${ifDefined(propertySchema?.minlength)} | ||||
|       maxlength=${ifDefined(propertySchema?.maxlength)} | ||||
|       @input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)} | ||||
|       .value=${propertyValue} | ||||
|     /> | ||||
|     ${(propertySchema?.datalist) ? html`<datalist id=${inputId + '-datalist'}> | ||||
|       ${(propertySchema?.datalist ?? []).map((value) => html`<option value=${value} />`)} | ||||
|     </datalist>` : nothing} | ||||
|     ` | ||||
|   } | ||||
|  | ||||
|   _renderTextarea = (rowId: number, inputId: string, inputName: string, propertyName: string, propertySchema: CellDataSchema, propertyValue: string) => { | ||||
|     return html`<textarea | ||||
|       name=${inputName} | ||||
|       class="form-control" | ||||
|       id=${inputId} | ||||
|       min=${ifDefined(propertySchema?.min)} | ||||
|       max=${ifDefined(propertySchema?.max)} | ||||
|       minlength=${ifDefined(propertySchema?.minlength)} | ||||
|       maxlength=${ifDefined(propertySchema?.maxlength)} | ||||
|       @input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)} | ||||
|       .value=${propertyValue} | ||||
|     ></textarea>` | ||||
|   } | ||||
|  | ||||
|   _renderCheckbox = (rowId: number, inputId: string, inputName: string, propertyName: string, propertySchema: CellDataSchema, propertyValue: boolean) => { | ||||
|     return html`<input | ||||
|       type="checkbox" | ||||
|       name=${inputName} | ||||
|       class="form-check-input" | ||||
|       id=${inputId} | ||||
|       @input=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)} | ||||
|       .value=${propertyValue} | ||||
|       ?checked=${propertyValue} | ||||
|     />` | ||||
|   } | ||||
|  | ||||
|   _renderSelect = (rowId: number, inputId: string, inputName: string, propertyName: string, propertySchema: CellDataSchema, propertyValue: string) => { | ||||
|     return html`<select | ||||
|       class="form-select" | ||||
|       aria-label="Default select example" | ||||
|       @change=${(event: InputEvent) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)} | ||||
|     > | ||||
|       <option ?selected=${!propertyValue}>${propertySchema?.label ?? 'Choose your option'}</option> | ||||
|       ${Object.entries(propertySchema?.options ?? {}) | ||||
|         ?.map(([value, name]) => | ||||
|           html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>` | ||||
|         )} | ||||
|     </select>` | ||||
|   } | ||||
|  | ||||
|  | ||||
|   _updatePropertyFromValue(event: Event, propertyName: string, rowId : number) { | ||||
|   _updatePropertyFromValue = (event: Event, propertyName: string, propertySchema: CellDataSchema, rowId: number) => { | ||||
|     let target = event?.target as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement) | ||||
|     let value = (target && target instanceof HTMLInputElement && target.type == "checkbox") ? !!(target?.checked) : target?.value | ||||
|  | ||||
|     if (value !== undefined) { | ||||
|       for(let row of this.rows) { | ||||
|         if(row._id === rowId) { | ||||
|           row[propertyName] = value | ||||
|       for (let rowById of this._rowsById) { | ||||
|         if (rowById._id === rowId) { | ||||
|           switch (rowById.row[propertyName].constructor) { | ||||
|             case Array: | ||||
|               rowById.row[propertyName] = (value as string).split(propertySchema.separator ?? ',') | ||||
|             default: | ||||
|               rowById.row[propertyName] = value | ||||
|           } | ||||
|  | ||||
|           this.rows = this._rowsById.map(rowById => rowById.row) | ||||
|  | ||||
|           this.requestUpdate('rows') | ||||
|  | ||||
|           this.requestUpdate('rowsById') | ||||
|           this.dispatchEvent(new CustomEvent('update', { detail: this.rows })) | ||||
|           return | ||||
|         } | ||||
|       } | ||||
|  | ||||
							
								
								
									
										50
									
								
								client/common/configuration/templates/HelpButtonElement.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								client/common/configuration/templates/HelpButtonElement.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | ||||
| import { css, html, LitElement } from 'lit' | ||||
| import { customElement, property, state } from 'lit/decorators.js' | ||||
| import { unsafeHTML } from 'lit/directives/unsafe-html.js' | ||||
| import { helpButtonSVG } from '../../../videowatch/buttons' | ||||
| import { consume } from '@lit/context' | ||||
| import { registerClientOptionsContext } from './ChannelConfigurationElement' | ||||
| import { RegisterClientOptions } from '@peertube/peertube-types/client' | ||||
| import { Task } from '@lit/task' | ||||
| import { localizedHelpUrl } from '../../../utils/help' | ||||
| import { ptTr } from './TranslationDirective' | ||||
| import { DirectiveResult } from 'lit/directive' | ||||
| import { getGlobalStyleSheets } from '../../global-styles' | ||||
|  | ||||
| @customElement('help-button') | ||||
| export class HelpButtonElement extends LitElement { | ||||
|  | ||||
|   @consume({context: registerClientOptionsContext}) | ||||
|   public registerClientOptions: RegisterClientOptions | undefined | ||||
|  | ||||
|   @property({ attribute: false }) | ||||
|   public buttonTitle: string | DirectiveResult = ptTr(LOC_ONLINE_HELP) | ||||
|  | ||||
|   @property({ attribute: false }) | ||||
|   public page: string = '' | ||||
|  | ||||
|   @state() | ||||
|   public url: URL = new URL('https://lmddgtfy.net/') | ||||
|  | ||||
|   static styles = [ | ||||
|     ...getGlobalStyleSheets() | ||||
|   ]; | ||||
|  | ||||
|   private _asyncTaskRender = new Task(this, { | ||||
|     task: async ([registerClientOptions], {signal}) => { | ||||
|       this.url = new URL(registerClientOptions ? await localizedHelpUrl(registerClientOptions, { page: this.page }) : '') | ||||
|     }, | ||||
|     args: () => [this.registerClientOptions] | ||||
|   }); | ||||
|  | ||||
|   render() { | ||||
|     return this._asyncTaskRender.render({ | ||||
|       complete: () => html`<a | ||||
|         href="${this.url.href}" | ||||
|         target=_blank | ||||
|         title="${this.buttonTitle}" | ||||
|         class="orange-button peertube-button-link" | ||||
|       >${unsafeHTML(helpButtonSVG())}</a>` | ||||
|     }) | ||||
|   } | ||||
| } | ||||
| @ -1,11 +1,10 @@ | ||||
| import { html, LitElement } from 'lit' | ||||
| import { css, html, LitElement } from 'lit' | ||||
| import { customElement, property } from 'lit/decorators.js' | ||||
| import { unsafeSVG } from 'lit/directives/unsafe-svg.js' | ||||
| import { StaticValue } from 'lit/static-html.js' | ||||
| import { helpButtonSVG } from '../../../videowatch/buttons' | ||||
| import './HelpButtonElement' | ||||
| import { getGlobalStyleSheets } from '../../global-styles' | ||||
|  | ||||
| @customElement('plugin-configuration-row') | ||||
| export class PLuginConfigurationRow extends LitElement { | ||||
| export class PluginConfigurationRow extends LitElement { | ||||
|  | ||||
|   @property({ attribute: false }) | ||||
|   public title: string = `title` | ||||
| @ -14,11 +13,11 @@ export class PLuginConfigurationRow extends LitElement { | ||||
|   public description: string = `Here's a description` | ||||
|  | ||||
|   @property({ attribute: false }) | ||||
|   public helpLink: { url: URL, title: string } = { url : new URL('https://lmddgtfy.net/'), title: 'Online Help'} | ||||
|   public helpPage: string = 'documentation' | ||||
|  | ||||
|   createRenderRoot = () => { | ||||
|     return this | ||||
|   } | ||||
|   static styles = [ | ||||
|     ...getGlobalStyleSheets() | ||||
|   ]; | ||||
|  | ||||
|   render() { | ||||
|     return html` | ||||
| @ -26,12 +25,8 @@ export class PLuginConfigurationRow extends LitElement { | ||||
|       <div class="col-12 col-lg-4 col-xl-3"> | ||||
|         <h2>${this.title}</h2> | ||||
|         <p>${this.description}</p> | ||||
|         <a | ||||
|           href="${this.helpLink.url.href}" | ||||
|           target=_blank | ||||
|           title="${this.helpLink.title}" | ||||
|           class="orange-button peertube-button-link" | ||||
|         >${unsafeSVG(helpButtonSVG())}</a> | ||||
|         <help-button .page=${this.helpPage}> | ||||
|         </help-button> | ||||
|       </div> | ||||
|       <div class="col-12 col-lg-8 col-xl-9"> | ||||
|         <slot><p>Nothing in this row.</p></slot> | ||||
|  | ||||
| @ -1,6 +1,9 @@ | ||||
| import { PartInfo, directive } from 'lit/directive.js' | ||||
| import { PartInfo, PartType, directive } from 'lit/directive.js' | ||||
| import { AsyncDirective } from 'lit/async-directive.js' | ||||
| import { RegisterClientHelpers } from '@peertube/peertube-types/client'; | ||||
| import { unsafeHTML } from 'lit/directives/unsafe-html.js'; | ||||
| import { html } from 'lit'; | ||||
| import { unsafeStatic } from 'lit/static-html.js'; | ||||
|  | ||||
| export class TranslationDirective extends AsyncDirective { | ||||
|  | ||||
| @ -9,22 +12,35 @@ export class TranslationDirective extends AsyncDirective { | ||||
|     private _translatedValue : string = '' | ||||
|     private _localizationId : string = '' | ||||
|  | ||||
|     private _allowUnsafeHTML = false | ||||
|  | ||||
|     constructor(partInfo: PartInfo) { | ||||
|         super(partInfo); | ||||
|  | ||||
|         //_peertubeOptionsPromise.then((options) => this._peertubeHelpers = options.peertubeHelpers) | ||||
|     } | ||||
|  | ||||
|     override render = (locId: string) => { | ||||
|     // update = (part: ElementPart) => { | ||||
|     //     if (part) console.log(`Element : ${part?.element?.getAttributeNames?.().join(' ')}`); | ||||
|     //     return this.render(this._localizationId); | ||||
|     // } | ||||
|  | ||||
|     override render = (locId: string, allowHTML: boolean = false) => { | ||||
|         this._localizationId = locId // TODO Check current component for context (to infer the prefix) | ||||
|  | ||||
|         this._allowUnsafeHTML = allowHTML | ||||
|  | ||||
|         if (this._translatedValue === '') { | ||||
|             this._translatedValue = locId | ||||
|         } | ||||
|  | ||||
|         this._asyncUpdateTranslation() | ||||
|  | ||||
|         return this._translatedValue | ||||
|         return this._internalRender() | ||||
|     } | ||||
|  | ||||
|     _internalRender = () => { | ||||
|         return this._allowUnsafeHTML ? html`${unsafeHTML(this._translatedValue)}` : this._translatedValue | ||||
|     } | ||||
|  | ||||
|     _asyncUpdateTranslation = async () => { | ||||
| @ -32,7 +48,7 @@ export class TranslationDirective extends AsyncDirective { | ||||
|  | ||||
|         if (newValue !== '' && newValue !== this._translatedValue) { | ||||
|             this._translatedValue = newValue | ||||
|             this.setValue(newValue) | ||||
|             this.setValue(this._internalRender()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,233 +0,0 @@ | ||||
| <div class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-channel"> | ||||
|   <h1> | ||||
|     {{title}}: | ||||
|     <span class="peertube-plugin-livechat-configuration-channel-info"> | ||||
|       <span>{{channelConfiguration.channel.displayName}}</span> | ||||
|       <span>{{channelConfiguration.channel.name}}</span> | ||||
|     </span> | ||||
|     {{{helpButton}}} | ||||
|   </h1> | ||||
|   <p>{{description}}</p> | ||||
|   <form livechat-configuration-channel-options role="form"> | ||||
|     <div class="row mt-3"> | ||||
|       <div class="col-12 col-lg-4 col-xl-3"> | ||||
|         <h2>{{slowModeLabel}}</h2> | ||||
|         <p>{{{slowModeDesc}}}</p> | ||||
|         {{{helpButtonSlowMode}}} | ||||
|       </div> | ||||
|       <div class="col-12 col-lg-8 col-xl-9"> | ||||
|         <div class="form-group"> | ||||
|           <label> | ||||
|             <input | ||||
|               type="number" | ||||
|               name="slow_mode_duration" | ||||
|               class="form-control" | ||||
|               min="0" | ||||
|               max="1000" | ||||
|               id="peertube-livechat-slow-mode-duration" | ||||
|               value="{{channelConfiguration.configuration.slowMode.duration}}" | ||||
|             /> | ||||
|           </label> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="row mt-3"> | ||||
|       <div class="col-12 col-lg-4 col-xl-3"> | ||||
|         <h2>{{botOptions}}</h2> | ||||
|         {{{helpButtonBot}}} | ||||
|       </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" | ||||
|               value="1" | ||||
|               {{#channelConfiguration.configuration.bot.enabled}} | ||||
|                 checked="checked" | ||||
|               {{/channelConfiguration.configuration.bot.enabled}} | ||||
|             /> | ||||
|             {{enableBot}} | ||||
|           </label> | ||||
|         </div> | ||||
|         <div class="form-group" livechat-configuration-channel-options-bot-enabled> | ||||
|           <label for="peertube-livechat-bot-nickname">{{botNickname}}</label> | ||||
|           <input | ||||
|             type="text" | ||||
|             name="bot_nickname" | ||||
|             class="form-control" | ||||
|             id="peertube-livechat-bot-nickname" | ||||
|             value="{{channelConfiguration.configuration.bot.nickname}}" | ||||
|           /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="row mt-5" livechat-configuration-channel-options-bot-enabled> | ||||
|       <div class="col-12 col-lg-4 col-xl-3"> | ||||
|         <h2>{{forbiddenWords}} #{{displayNumber}}</h2> | ||||
|         {{#displayHelp}} | ||||
|           <p>{{forbiddenWordsDesc}} {{moreInfo}}</p> | ||||
|           {{{helpButtonForbiddenWords}}} | ||||
|         {{/displayHelp}} | ||||
|       </div> | ||||
|       <table class="col-12 col-lg-4 col-xl-3 forbidden_words_table"> | ||||
|         <thead> | ||||
|           <tr> | ||||
|             <th scope="col">{{forbiddenWords}}  <span class="form-group-description">{{forbiddenWordsDesc2}}</span></th> | ||||
|             <th scope="col">{{forbiddenWordsRegexp}} <span class="form-group-description">{{forbiddenWordsRegexpDesc}}</span></th> | ||||
|             <th scope="col">{{forbiddenWordsApplyToModerators}} <span class="form-group-description">{{forbiddenWordsApplyToModeratorsDesc}}</span></th> | ||||
|             <th scope="col">{{forbiddenWordsLabel}}  <span class="form-group-description">{{forbiddenWordsLabelDesc}}</span></th> | ||||
|             <th scope="col">{{forbiddenWordsReason}}  <span class="form-group-description">{{forbiddenWordsReasonDesc}}</span></th> | ||||
|             <th scope="col">{{forbiddenWordsComments}}  <span class="form-group-description">{{forbiddenWordsCommentsDesc}}</span></th> | ||||
|             <th scope="col">Remove  <span class="form-group-description">Remove Row</span></th> | ||||
|           </tr> | ||||
|         </thead> | ||||
|         {{#forbiddenWordsArray}}{{! iterating on forbiddenWordsArray to display N fields }} | ||||
|           <tbody> | ||||
|             <tr class="peertube-livechat-forbidden-words-row-{{fieldNumber}}"> | ||||
|               <td> | ||||
|                 {{! warning: don't add extra line break in textarea! }} | ||||
|                 <textarea | ||||
|                   name="forbidden_words_{{fieldNumber}}" | ||||
|                   id="peertube-livechat-forbidden-words-{{fieldNumber}}" | ||||
|                   class="form-control" | ||||
|                 >{{joinedEntries}}</textarea> | ||||
|               </td> | ||||
|               <td> | ||||
|                 <input | ||||
|                   type="checkbox" | ||||
|                   name="forbidden_words_regexp_{{fieldNumber}}" | ||||
|                   value="1" | ||||
|                   {{#regexp}} | ||||
|                     checked="checked" | ||||
|                   {{/regexp}} | ||||
|                 /> | ||||
|               </td> | ||||
|               <td> | ||||
|                 <input | ||||
|                   type="checkbox" | ||||
|                   name="forbidden_words_applytomoderators_{{fieldNumber}}" | ||||
|                   value="1" | ||||
|                   {{#applyToModerators}} | ||||
|                     checked="checked" | ||||
|                   {{/applyToModerators}} | ||||
|                 /> | ||||
|               </td> | ||||
|               <td> | ||||
|                 <input | ||||
|                   type="text" | ||||
|                   name="forbidden_words_label_{{fieldNumber}}" | ||||
|                   class="form-control" | ||||
|                   id="peertube-livechat-forbidden-words-label-{{fieldNumber}}" | ||||
|                   value="{{label}}" | ||||
|                 /> | ||||
|               </td> | ||||
|               <td> | ||||
|                 <input | ||||
|                   type="text" | ||||
|                   name="forbidden_words_reason_{{fieldNumber}}" | ||||
|                   class="form-control" | ||||
|                   id="peertube-livechat-forbidden-words-reason-{{fieldNumber}}" | ||||
|                   value="{{reason}}" | ||||
|                 /> | ||||
|               </td> | ||||
|               <td> | ||||
|                 {{! warning: don't add extra line break in textarea! }} | ||||
|                 <textarea | ||||
|                   name="forbidden_words_comments_{{fieldNumber}}" | ||||
|                   id="peertube-livechat-forbidden-words-comments-{{fieldNumber}}" | ||||
|                   class="form-control" | ||||
|                 >{{comments}}</textarea> | ||||
|               </td> | ||||
|               <td> | ||||
|                 <button type="button" class="btn btn-danger peertube-livechat-forbidden-words-{{fieldNumber}}-remove">x</button> | ||||
|               </td> | ||||
|             </tr> | ||||
|       {{/forbiddenWordsArray}} | ||||
|             <tr> | ||||
|               <button type="button" class="btn btn-success peertube-livechat-forbidden-words-add">+</button> | ||||
|             </tr> | ||||
|           </tbody> | ||||
|         </table> | ||||
|       </div> | ||||
|  | ||||
|     {{#quotesArray}}{{! iterating on quotesArray to display N fields }} | ||||
|       <div class="row mt-5" livechat-configuration-channel-options-bot-enabled> | ||||
|         <div class="col-12 col-lg-4 col-xl-3"> | ||||
|           <h2>{{quoteLabel}} #{{displayNumber}}</h2> | ||||
|           {{#displayHelp}} | ||||
|             <p>{{quoteDesc}} {{moreInfo}}</p> | ||||
|             {{{helpButtonQuotes}}} | ||||
|           {{/displayHelp}} | ||||
|         </div> | ||||
|         <div class="col-12 col-lg-8 col-xl-9"> | ||||
|           <div class="form-group"> | ||||
|             <label for="peertube-livechat-quote-{{fieldNumber}}">{{quoteLabel2}}</label> | ||||
|             {{! warning: don't add extra line break in textarea! }} | ||||
|             <textarea | ||||
|               name="quote_{{fieldNumber}}" | ||||
|               id="peertube-livechat-quote-{{fieldNumber}}" | ||||
|               class="form-control" | ||||
|             >{{joinedMessages}}</textarea> | ||||
|             <p class="form-group-description">{{quoteDesc2}}</p> | ||||
|           </div> | ||||
|           <div class="form-group"> | ||||
|             <label for="peertube-livechat-quote-delay-{{fieldNumber}}">{{quoteDelayLabel}}</label> | ||||
|             <input | ||||
|               type="number" | ||||
|               min="1" | ||||
|               max="6000" | ||||
|               step="1" | ||||
|               name="quote_delay_{{fieldNumber}}" | ||||
|               class="form-control" | ||||
|               id="peertube-livechat-quote-delay-{{fieldNumber}}" | ||||
|               value="{{delay}}" | ||||
|             /> | ||||
|             <p class="form-group-description">{{quoteDelayDesc}}</p> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     {{/quotesArray}} | ||||
|  | ||||
|     {{#cmdsArray}}{{! iterating on cmdsArray to display N fields }} | ||||
|       <div class="row mt-5" livechat-configuration-channel-options-bot-enabled> | ||||
|         <div class="col-12 col-lg-4 col-xl-3"> | ||||
|           <h2>{{commandLabel}} #{{displayNumber}}</h2> | ||||
|           {{#displayHelp}} | ||||
|             <p>{{commandDesc}} {{moreInfo}}</p> | ||||
|             {{{helpButtonCommands}}} | ||||
|           {{/displayHelp}} | ||||
|         </div> | ||||
|         <div class="col-12 col-lg-8 col-xl-9"> | ||||
|           <div class="form-group"> | ||||
|             <label for="peertube-livechat-command-{{fieldNumber}}">{{commandCmdLabel}}</label> | ||||
|             <input | ||||
|               type="text" | ||||
|               name="command_{{fieldNumber}}" | ||||
|               class="form-control" | ||||
|               id="peertube-livechat-command-{{fieldNumber}}" | ||||
|               value="{{command}}" | ||||
|             /> | ||||
|             <p class="form-group-description">{{commandCmdDesc}}</p> | ||||
|           </div> | ||||
|           <div class="form-group"> | ||||
|             <label for="peertube-livechat-command-message-{{fieldNumber}}">{{commandMessageLabel}}</label> | ||||
|             <input | ||||
|               type="text" | ||||
|               name="command_message_{{fieldNumber}}" | ||||
|               class="form-control" | ||||
|               id="peertube-livechat-command-message-{{fieldNumber}}" | ||||
|               value="{{message}}" | ||||
|             /> | ||||
|             <p class="form-group-description">{{commandMessageDesc}}</p> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     {{/cmdsArray}} | ||||
|     <div class="form-group mt-5"> | ||||
|       <input type="submit" value="{{save}}" /> | ||||
|       <input type="reset" value="{{cancel}}" /> | ||||
|     </div> | ||||
|   </form> | ||||
| </div> | ||||
| @ -1,246 +0,0 @@ | ||||
| // SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/> | ||||
| // | ||||
| // SPDX-License-Identifier: AGPL-3.0-only | ||||
|  | ||||
| import type { RegisterClientHelpers, RegisterClientOptions } from '@peertube/peertube-types/client' | ||||
| import { localizedHelpUrl } from '../../../utils/help' | ||||
| import { helpButtonSVG } from '../../../videowatch/buttons' | ||||
| import {  getConfigurationChannelViewData } from './logic/channel' | ||||
| import { TemplateResult, html } from 'lit' | ||||
| import { unsafeHTML } from 'lit/directives/unsafe-html.js' | ||||
| import { unsafeSVG } from 'lit/directives/unsafe-svg.js'; | ||||
| // Must use require for mustache, import seems buggy. | ||||
| const Mustache = require('mustache') | ||||
|  | ||||
| import './DynamicTableFormElement' | ||||
| import './ChannelConfigurationElement' | ||||
| import './PluginConfigurationRow' | ||||
| import { ptTr } from './TranslationDirective' | ||||
|  | ||||
| /** | ||||
|  * Renders the configuration settings page for a given channel, | ||||
|  * and set it as innerHTML to rootEl. | ||||
|  * The page content can be empty. In such case, the notifier will be used to display a message. | ||||
|  * @param registerClientOptions Peertube client options | ||||
|  * @param channelId The channel id | ||||
|  * @param rootEl The HTMLElement in which insert the generated DOM. | ||||
|  */ | ||||
| async function renderConfigurationChannel ( | ||||
|   registerClientOptions: RegisterClientOptions, | ||||
|   channelId: string, | ||||
|   rootEl: HTMLElement | ||||
| ): Promise<TemplateResult> { | ||||
|   const peertubeHelpers = registerClientOptions.peertubeHelpers | ||||
|  | ||||
|   try { | ||||
|     const view : {[key: string] : any} = await getConfigurationChannelViewData(registerClientOptions, channelId) | ||||
|     await fillViewHelpButtons(registerClientOptions, view) | ||||
|     await fillLabels(registerClientOptions, view) | ||||
|  | ||||
|     //await vivifyConfigurationChannel(registerClientOptions, rootEl, channelId) | ||||
|  | ||||
|     let tableHeader = { | ||||
|       words: { | ||||
|         colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL), | ||||
|         description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2) | ||||
|       }, | ||||
|       regex: { | ||||
|         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) | ||||
|       } | ||||
|     } | ||||
|     let tableSchema = { | ||||
|       words: { | ||||
|         inputType: 'text', | ||||
|         default: 'helloqwesad' | ||||
|       }, | ||||
|       regex: { | ||||
|         inputType: 'text', | ||||
|         default: 'helloaxzca' | ||||
|       }, | ||||
|       applyToModerators: { | ||||
|         inputType: 'checkbox', | ||||
|         default: true | ||||
|       }, | ||||
|       label: { | ||||
|         inputType: 'text', | ||||
|         default: 'helloasx' | ||||
|       }, | ||||
|       reason: { | ||||
|         inputType: 'select', | ||||
|         default: 'transphobia', | ||||
|         label: 'choose your poison', | ||||
|         options: {'racism': 'Racism', 'sexism': 'Sexism', 'transphobia': 'Transphobia', 'bigotry': 'Bigotry'} | ||||
|       }, | ||||
|       comments: { | ||||
|         inputType: 'textarea', | ||||
|         default: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, | ||||
|         sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. | ||||
|         Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris | ||||
|         nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in | ||||
|         reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla | ||||
|         pariatur. Excepteur sint occaecat cupidatat non proident, sunt in | ||||
|         culpa qui officia deserunt mollit anim id est laborum.` | ||||
|       }, | ||||
|     } | ||||
|     let tableRows = [ | ||||
|       { | ||||
|         words: 'teweqwst', | ||||
|         regex: 'tesdgst', | ||||
|         applyToModerators: false, | ||||
|         label: 'teswet', | ||||
|         reason: 'sexism', | ||||
|         comments: 'tsdaswest', | ||||
|       }, | ||||
|       { | ||||
|         words: 'tedsadst', | ||||
|         regex: 'tezxccst', | ||||
|         applyToModerators: true, | ||||
|         label: 'tewest', | ||||
|         reason: 'racism', | ||||
|         comments: 'tesxzct', | ||||
|       }, | ||||
|       { | ||||
|         words: 'tesadsdxst', | ||||
|         regex: 'dsfsdf', | ||||
|         applyToModerators: false, | ||||
|         label: 'tesdadst', | ||||
|         reason: 'bigotry', | ||||
|         comments: 'tsadest', | ||||
|       }, | ||||
|     ] | ||||
|  | ||||
|     let helpLink = { | ||||
|       url : new URL(await localizedHelpUrl(registerClientOptions, { page: 'documentation/user/streamers/bot/forbidden_words' })), | ||||
|       title: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC) | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|     <div class="container"> | ||||
|       <channel-configuration></channel-configuration> | ||||
|       <plugin-configuration-row | ||||
|         .title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)} | ||||
|         .description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)} | ||||
|         .helpLink=${helpLink} | ||||
|       > | ||||
|         <dynamic-table-form | ||||
|           .header=${tableHeader} | ||||
|           .schema=${tableSchema} | ||||
|           .rows=${tableRows} | ||||
|           .formName=${'forbidden-words'} | ||||
|         > | ||||
|         </dynamic-table-form> | ||||
|       </plugin-configuration-row> | ||||
|     </div>` | ||||
|   } catch (err: any) { | ||||
|     peertubeHelpers.notifier.error(err.toString()) | ||||
|     return html`` | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function fillViewHelpButtons ( | ||||
|   registerClientOptions: RegisterClientOptions, | ||||
|   view: {[key: string]: string} | ||||
| ): Promise<void> { | ||||
|   const title = await registerClientOptions.peertubeHelpers.translate(LOC_ONLINE_HELP) | ||||
|  | ||||
|   const button = async (page: string): Promise<string> => { | ||||
|     const helpUrl = await localizedHelpUrl(registerClientOptions, { | ||||
|       page | ||||
|     }) | ||||
|     const helpIcon = helpButtonSVG() | ||||
|     return `<a | ||||
|         href="${helpUrl}" | ||||
|         target=_blank | ||||
|         title="${title}" | ||||
|         class="orange-button peertube-button-link" | ||||
|       >${helpIcon}</a>` | ||||
|   } | ||||
|  | ||||
|   view.helpButton = await button('documentation/user/streamers/channel') | ||||
|   view.helpButtonBot = await button('documentation/user/streamers/bot') | ||||
|   view.helpButtonForbiddenWords = await button('documentation/user/streamers/bot/forbidden_words') | ||||
|   view.helpButtonQuotes = await button('documentation/user/streamers/bot/quotes') | ||||
|   view.helpButtonCommands = await button('documentation/user/streamers/bot/commands') | ||||
|   view.helpButtonSlowMode = await button('documentation/user/streamers/slow_mode') | ||||
| } | ||||
|  | ||||
| async function fillLabels ( | ||||
|   registerClientOptions: RegisterClientOptions, | ||||
|   view: {[key: string] : string} | ||||
| ): Promise<void> { | ||||
|   const peertubeHelpers = registerClientOptions.peertubeHelpers | ||||
|    | ||||
|   view.title = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TITLE) | ||||
|   view.description = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_DESC) | ||||
|  | ||||
|   view.slowModeLabel = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_LABEL) | ||||
|   view.slowModeDesc = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_DESC) | ||||
|   view.enableBot = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ENABLE_BOT_LABEL) | ||||
|   view.botOptions = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE) | ||||
|   view.forbiddenWords = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL) | ||||
|   view.forbiddenWordsDesc = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC) | ||||
|   view.forbiddenWordsDesc2 = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC2) | ||||
|   view.forbiddenWordsReason = await peertubeHelpers.translate( | ||||
|     LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_LABEL | ||||
|   ) | ||||
|   view.forbiddenWordsReasonDesc = await peertubeHelpers.translate( | ||||
|     LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REASON_DESC | ||||
|   ) | ||||
|   view.forbiddenWordsRegexp = await peertubeHelpers.translate( | ||||
|     LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL | ||||
|   ) | ||||
|   view.forbiddenWordsRegexpDesc = await peertubeHelpers.translate( | ||||
|     LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC | ||||
|   ) | ||||
|   view.forbiddenWordsApplyToModerators = await peertubeHelpers.translate( | ||||
|     LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_LABEL | ||||
|   ) | ||||
|   view.forbiddenWordsApplyToModeratorsDesc = await peertubeHelpers.translate( | ||||
|     LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_APPLYTOMODERATORS_DESC | ||||
|   ) | ||||
|   view.forbiddenWordsComments = await peertubeHelpers.translate( | ||||
|     LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_LABEL | ||||
|   ) | ||||
|   view.forbiddenWordsCommentsDesc = await peertubeHelpers.translate( | ||||
|     LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_COMMENTS_DESC | ||||
|   ) | ||||
|   view.quoteLabel = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL) | ||||
|   view.quoteLabel2 = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_LABEL2) | ||||
|   view.quoteDesc = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC) | ||||
|   view.quoteDesc2 = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DESC2) | ||||
|   view.quoteDelayLabel = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DELAY_LABEL) | ||||
|   view.quoteDelayDesc = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DELAY_DESC) | ||||
|   view.commandLabel = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_LABEL) | ||||
|   view.commandDesc = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_DESC) | ||||
|   view.commandCmdLabel = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_CMD_LABEL) | ||||
|   view.commandCmdDesc = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_CMD_DESC) | ||||
|   view.commandMessageLabel = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_MESSAGE_LABEL) | ||||
|   view.commandMessageDesc = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_COMMAND_MESSAGE_DESC) | ||||
|   // view.bannedJIDs = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BANNED_JIDS_LABEL) | ||||
|  | ||||
|   view.save = await peertubeHelpers.translate(LOC_SAVE) | ||||
|   view.cancel = await peertubeHelpers.translate(LOC_CANCEL) | ||||
|   view.botNickname = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_NICKNAME) | ||||
|   view.moreInfo = await peertubeHelpers.translate(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FOR_MORE_INFO) | ||||
| } | ||||
|  | ||||
| export { | ||||
|   renderConfigurationChannel | ||||
| } | ||||
| @ -1,28 +0,0 @@ | ||||
| <div class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-home"> | ||||
|   <h1> | ||||
|     {{title}} | ||||
|     {{{helpButton}}} | ||||
|   </h1> | ||||
|   <p>{{description}}</p> | ||||
|   <p>{{please_select}}</p> | ||||
|   <ul class="peertube-plugin-livechat-configuration-home-channels"> | ||||
|   {{#channels}} | ||||
|     <li> | ||||
|       <a href="{{livechatConfigurationUri}}"> | ||||
|         {{#avatar}} | ||||
|           <img class="avatar channel" src="{{path}}"> | ||||
|         {{/avatar}} | ||||
|         {{^avatar}} | ||||
|           <div class="avatar channel initial gray"></div> | ||||
|         {{/avatar}} | ||||
|       </a> | ||||
|       <div class="peertube-plugin-livechat-configuration-home-info"> | ||||
|         <a href="{{livechatConfigurationUri}}"> | ||||
|           <div>{{displayName}}</div> | ||||
|           <div>{{name}}</div> | ||||
|         </a> | ||||
|       </div> | ||||
|     </li> | ||||
|   {{/channels}} | ||||
|   </ul> | ||||
| </div> | ||||
| @ -6,9 +6,7 @@ import type { RegisterClientOptions } from '@peertube/peertube-types/client' | ||||
| import { localizedHelpUrl } from '../../../utils/help' | ||||
| import { helpButtonSVG } from '../../../videowatch/buttons' | ||||
| import { TemplateResult, html } from 'lit' | ||||
| import { unsafeHTML } from 'lit/directives/unsafe-html.js' | ||||
| import { ptTr } from './TranslationDirective' | ||||
| import { unsafeSVG } from 'lit/directives/unsafe-svg.js'; | ||||
| import { unsafeHTML } from 'lit/directives/unsafe-html.js'; | ||||
|  | ||||
| interface HomeViewData { | ||||
|   title: string | ||||
| @ -89,7 +87,7 @@ async function _fillViewHelpButtons ( // TODO: refactor with the similar functio | ||||
|     return html`<a | ||||
|         href="${helpUrl}" | ||||
|         target=_blank | ||||
|         title="${ptTr(LOC_ONLINE_HELP)}" | ||||
|         title="${title}" | ||||
|         class="orange-button peertube-button-link" | ||||
|       >${unsafeHTML(helpIcon)}</a>` | ||||
|   } | ||||
|  | ||||
| @ -1,432 +0,0 @@ | ||||
| // 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 { ChannelConfiguration, ChannelConfigurationOptions } from 'shared/lib/types' | ||||
| import { getBaseRoute } from '../../../../utils/uri' | ||||
|  | ||||
| /** | ||||
|  * Returns the data that can be feed into the template view | ||||
|  * @param registerClientOptions | ||||
|  * @param channelId | ||||
|  */ | ||||
| async function getConfigurationChannelViewData ( | ||||
|   registerClientOptions: RegisterClientOptions, | ||||
|   channelId: string | ||||
| ): Promise<{[key: string] : any}> { | ||||
|   if (!channelId || !/^\d+$/.test(channelId)) { | ||||
|     throw new Error('Missing or invalid channel id.') | ||||
|   } | ||||
|  | ||||
|   const { peertubeHelpers } = registerClientOptions | ||||
|   const response = await fetch( | ||||
|     getBaseRoute(registerClientOptions) + '/api/configuration/channel/' + encodeURIComponent(channelId), | ||||
|     { | ||||
|       method: 'GET', | ||||
|       headers: peertubeHelpers.getAuthHeader() | ||||
|     } | ||||
|   ) | ||||
|   if (!response.ok) { | ||||
|     throw new Error('Can\'t get channel configuration options.') | ||||
|   } | ||||
|   const channelConfiguration: ChannelConfiguration = await (response).json() | ||||
|  | ||||
|   // Basic testing that channelConfiguration has the correct format | ||||
|   if ((typeof channelConfiguration !== 'object') || !channelConfiguration.channel) { | ||||
|     throw new Error('Invalid channel configuration options.') | ||||
|   } | ||||
|  | ||||
|   const forbiddenWordsArray: Object[] = [] | ||||
|   for (let i = 0; i < channelConfiguration.configuration.bot.forbiddenWords.length; i++) { | ||||
|     const fw = channelConfiguration.configuration.bot.forbiddenWords[i] | ||||
|     forbiddenWordsArray.push({ | ||||
|       displayNumber: i + 1, | ||||
|       fieldNumber: i, | ||||
|       displayHelp: i === 0, | ||||
|       joinedEntries: fw.entries.join('\n'), | ||||
|       regexp: !!fw.regexp, | ||||
|       applyToModerators: fw.applyToModerators, | ||||
|       label:fw.label, | ||||
|       reason: fw.reason, | ||||
|       comments: fw.comments | ||||
|     }) | ||||
|   } | ||||
|   // Ensuring we have at least N blocks: | ||||
|   while (forbiddenWordsArray.length < 1) { | ||||
|     const i = forbiddenWordsArray.length | ||||
|     // default value | ||||
|     forbiddenWordsArray.push({ | ||||
|       displayNumber: i + 1, | ||||
|       fieldNumber: i, | ||||
|       displayHelp: i === 0, | ||||
|       joinedEntries: '', | ||||
|       regexp: false, | ||||
|       applyToModerators: false, | ||||
|       label:'', | ||||
|       reason: '', | ||||
|       comments: '' | ||||
|     }) | ||||
|     continue | ||||
|   } | ||||
|  | ||||
|   const quotesArray: Object[] = [] | ||||
|   for (let i = 0; i < channelConfiguration.configuration.bot.quotes.length; i++) { | ||||
|     const qs = channelConfiguration.configuration.bot.quotes[i] | ||||
|     quotesArray.push({ | ||||
|       displayNumber: i + 1, | ||||
|       fieldNumber: i, | ||||
|       displayHelp: i === 0, | ||||
|       joinedMessages: qs.messages.join('\n'), | ||||
|       delay: Math.round(qs.delay / 60) // converting to minutes | ||||
|     }) | ||||
|   } | ||||
|   // Ensuring we have at least N blocks: | ||||
|   while (quotesArray.length < 1) { | ||||
|     const i = quotesArray.length | ||||
|     // default value | ||||
|     quotesArray.push({ | ||||
|       displayNumber: i + 1, | ||||
|       fieldNumber: i, | ||||
|       displayHelp: i === 0, | ||||
|       joinedMessages: '', | ||||
|       delay: 5 | ||||
|     }) | ||||
|     continue | ||||
|   } | ||||
|  | ||||
|   const cmdsArray: Object[] = [] | ||||
|   for (let i = 0; i < channelConfiguration.configuration.bot.commands.length; i++) { | ||||
|     const cs = channelConfiguration.configuration.bot.commands[i] | ||||
|     cmdsArray.push({ | ||||
|       displayNumber: i + 1, | ||||
|       fieldNumber: i, | ||||
|       displayHelp: i === 0, | ||||
|       message: cs.message, | ||||
|       command: cs.command | ||||
|     }) | ||||
|   } | ||||
|   // Ensuring we have at least N blocks: | ||||
|   while (cmdsArray.length < 1) { | ||||
|     const i = cmdsArray.length | ||||
|     // default value | ||||
|     cmdsArray.push({ | ||||
|       displayNumber: i + 1, | ||||
|       fieldNumber: i, | ||||
|       displayHelp: i === 0, | ||||
|       message: '', | ||||
|       command: '' | ||||
|     }) | ||||
|     continue | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     channelConfiguration, | ||||
|     forbiddenWordsArray, | ||||
|     quotesArray, | ||||
|     cmdsArray | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Adds the front-end logic on the generated html for the channel configuration options. | ||||
|  * @param clientOptions Peertube client options | ||||
|  * @param rootEl The root element in which the template was rendered | ||||
|  */ | ||||
| async function vivifyConfigurationChannel ( | ||||
|   clientOptions: RegisterClientOptions, | ||||
|   rootEl: HTMLElement, | ||||
|   channelId: string | ||||
| ): Promise<void> { | ||||
|   const form = rootEl.querySelector('form[livechat-configuration-channel-options]') as HTMLFormElement | ||||
|   if (!form) { return } | ||||
|   const translate = clientOptions.peertubeHelpers.translate | ||||
|   const labelSaved = await translate(LOC_SUCCESSFULLY_SAVED) | ||||
|   const labelError = await translate(LOC_ERROR) | ||||
|   const enableBotCB = form.querySelector('input[name=bot]') as HTMLInputElement | ||||
|   const botEnabledEl = form.querySelectorAll('[livechat-configuration-channel-options-bot-enabled]') | ||||
|  | ||||
|   const dataClasses = ['forbidden-words', 'command', 'quote'] | ||||
|   type ChannelConfigClass = (typeof dataClasses)[number] | ||||
|  | ||||
|   type ChannelRowData = Record<ChannelConfigClass,{ rows: HTMLTableRowElement[], addButton: HTMLButtonElement, removeButtons: HTMLButtonElement[]}> | ||||
|  | ||||
|   const populateRowData: Function = () => { | ||||
|     let modifiers : ChannelRowData = {}; | ||||
|     for (let dataClass in dataClasses) { | ||||
|       let rows : HTMLTableRowElement[] = []; | ||||
|       let removeButtons : HTMLButtonElement[] = []; | ||||
|  | ||||
|       for (let i = 0, row : HTMLTableRowElement; row = form.querySelector(`button.peertube-livechat-${dataClass}-${i}-row`) as HTMLTableRowElement; i++) { | ||||
|         rows.push(row) | ||||
|       } | ||||
|  | ||||
|       for (let i = 0, button : HTMLButtonElement; button = form.querySelector(`button.peertube-livechat-${dataClass}-${i}-remove`) as HTMLButtonElement; i++) { | ||||
|         removeButtons.push(button) | ||||
|       } | ||||
|  | ||||
|       modifiers[dataClass] = { | ||||
|         rows, | ||||
|         addButton: form.querySelector(`button.peertube-livechat-${dataClass}-add`) as HTMLButtonElement, | ||||
|         removeButtons | ||||
|       } | ||||
|     } | ||||
|     return modifiers | ||||
|   } | ||||
|  | ||||
|   let rowDataRecords : ChannelRowData = populateRowData(); | ||||
|  | ||||
|   function removeRow(dataClass: ChannelConfigClass, index: number): any { | ||||
|     let {rows} = rowDataRecords[dataClass] | ||||
|  | ||||
|     let rowToDelete = rows.splice(index,1)[0] | ||||
|  | ||||
|     rowToDelete | ||||
|      | ||||
|     for (let i = index, row : HTMLTableRowElement; row = form.querySelector(`button.peertube-livechat-${dataClass}-${i}-row`) as HTMLTableRowElement; i++) { | ||||
|       rows.push(row) | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   function addRow(dataClass: ChannelConfigClass): any { | ||||
|     throw new Error('Function not implemented.') | ||||
|   } | ||||
|  | ||||
|   const refresh: Function = () => { | ||||
|     botEnabledEl.forEach(el => { | ||||
|       if (enableBotCB.checked) { | ||||
|         (el as HTMLElement).style.removeProperty('display') | ||||
|       } else { | ||||
|         (el as HTMLElement).style.display = 'none' | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   const removeDisplayedErrors = (): void => { | ||||
|     form.querySelectorAll('.form-error').forEach(el => el.remove()) | ||||
|   } | ||||
|  | ||||
|   const displayError = async (fieldSelector: string, message: string): Promise<void> => { | ||||
|     form.querySelectorAll(fieldSelector).forEach(el => { | ||||
|       const erEl = document.createElement('div') | ||||
|       erEl.classList.add('form-error') | ||||
|       erEl.textContent = message | ||||
|       el.after(erEl) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   const validateData: Function = async (channelConfigurationOptions: ChannelConfigurationOptions): Promise<boolean> => { | ||||
|     const botConf = channelConfigurationOptions.bot | ||||
|     const slowModeDuration = channelConfigurationOptions.slowMode.duration | ||||
|     const errorFieldSelectors = [] | ||||
|  | ||||
|     if ( | ||||
|       (typeof slowModeDuration !== 'number') || | ||||
|       isNaN(slowModeDuration) || | ||||
|       slowModeDuration < 0 || | ||||
|       slowModeDuration > 1000 | ||||
|     ) { | ||||
|       const selector = '#peertube-livechat-slow-mode-duration' | ||||
|       errorFieldSelectors.push(selector) | ||||
|       await displayError(selector, await translate(LOC_INVALID_VALUE)) | ||||
|     } | ||||
|  | ||||
|     // If !bot.enabled, we don't have to validate these fields: | ||||
|     // The backend will ignore those values. | ||||
|     if (botConf.enabled) { | ||||
|       if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) { | ||||
|         const selector = '#peertube-livechat-bot-nickname' | ||||
|         errorFieldSelectors.push(selector) | ||||
|         await displayError(selector, await translate(LOC_INVALID_VALUE)) | ||||
|       } | ||||
|  | ||||
|       for (let iFw = 0; iFw < botConf.forbiddenWords.length; iFw++) { | ||||
|         const fw = botConf.forbiddenWords[iFw] | ||||
|         if (fw.regexp) { | ||||
|           for (const v of fw.entries) { | ||||
|             if (v === '' || /^\s+$/.test(v)) { continue } | ||||
|             try { | ||||
|               // eslint-disable-next-line no-new | ||||
|               new RegExp(v) | ||||
|             } catch (err) { | ||||
|               const selector = '#peertube-livechat-forbidden-words-' + iFw.toString() | ||||
|               errorFieldSelectors.push(selector) | ||||
|               let message = await translate(LOC_INVALID_VALUE) | ||||
|               message += ` "${v}": ${err as string}` | ||||
|               await displayError(selector, message) | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       for (let iQt = 0; iQt < botConf.quotes.length; iQt++) { | ||||
|         const qt = botConf.quotes[iQt] | ||||
|         if (qt.messages.some(/\s+/.test)) { | ||||
|           const selector = '#peertube-livechat-quote-' + iQt.toString() | ||||
|           errorFieldSelectors.push(selector) | ||||
|           const message = await translate(LOC_INVALID_VALUE) | ||||
|           await displayError(selector, message) | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       for (let iCd = 0; iCd < botConf.commands.length; iCd++) { | ||||
|         const cd = botConf.commands[iCd] | ||||
|         if (/\s+/.test(cd.command)) { | ||||
|           const selector = '#peertube-livechat-command-' + iCd.toString() | ||||
|           errorFieldSelectors.push(selector) | ||||
|           const message = await translate(LOC_INVALID_VALUE) | ||||
|           await displayError(selector, message) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (errorFieldSelectors.length) { | ||||
|       // Set the focus to the first in-error field: | ||||
|       const el: HTMLInputElement | HTMLTextAreaElement | null = document.querySelector(errorFieldSelectors[0]) | ||||
|       el?.focus() | ||||
|       return false | ||||
|     } | ||||
|  | ||||
|     return true | ||||
|   } | ||||
|  | ||||
|   const submitForm: Function = async () => { | ||||
|     const data = new FormData(form) | ||||
|     removeDisplayedErrors() | ||||
|     const channelConfigurationOptions: ChannelConfigurationOptions = { | ||||
|       slowMode: { | ||||
|         duration: parseInt(data.get('slow_mode_duration')?.toString() ?? '0') | ||||
|       }, | ||||
|       bot: { | ||||
|         enabled: data.get('bot') === '1', | ||||
|         nickname: data.get('bot_nickname')?.toString() ?? '', | ||||
|         // TODO bannedJIDs | ||||
|         forbiddenWords: [], | ||||
|         quotes: [], | ||||
|         commands: [] | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Note: but data in order, because validateData assume index are okay to find associated fields. | ||||
|     for (let i = 0; data.has('forbidden_words_' + i.toString()); i++) { | ||||
|       const entries = (data.get('forbidden_words_' + i.toString())?.toString() ?? '') | ||||
|         .split(/\r?\n|\r|\n/g) | ||||
|         .filter(s => !/^\s*$/.test(s)) // filtering empty lines | ||||
|       const regexp = data.get('forbidden_words_regexp_' + i.toString()) | ||||
|       const applyToModerators = data.get('forbidden_words_applytomoderators_' + i.toString()) | ||||
|       const label = data.get('forbidden_words_label_' + i.toString())?.toString() | ||||
|       const reason = data.get('forbidden_words_reason_' + i.toString())?.toString() | ||||
|       const comments = data.get('forbidden_words_comments_' + i.toString())?.toString() | ||||
|       const fw: ChannelConfigurationOptions['bot']['forbiddenWords'][0] = { | ||||
|         entries, | ||||
|         applyToModerators: !!applyToModerators, | ||||
|         regexp: !!regexp | ||||
|       } | ||||
|       if (label) { | ||||
|         fw.label = label | ||||
|       } | ||||
|       if (reason) { | ||||
|         fw.reason = reason | ||||
|       } | ||||
|       if (comments) { | ||||
|         fw.comments = comments | ||||
|       } | ||||
|       channelConfigurationOptions.bot.forbiddenWords.push(fw) | ||||
|     } | ||||
|  | ||||
|     // Note: but data in order, because validateData assume index are okay to find associated fields. | ||||
|     for (let i = 0; data.has('quote_' + i.toString()); i++) { | ||||
|       const messages = (data.get('quote_' + i.toString())?.toString() ?? '') | ||||
|         .split(/\r?\n|\r|\n/g) | ||||
|         .filter(s => !/^\s*$/.test(s)) // filtering empty lines | ||||
|       let delay = parseInt(data.get('quote_delay_' + i.toString())?.toString() ?? '') | ||||
|       if (!delay || isNaN(delay) || delay < 1) { | ||||
|         delay = 5 | ||||
|       } | ||||
|       delay = delay * 60 // converting to seconds | ||||
|       const q: ChannelConfigurationOptions['bot']['quotes'][0] = { | ||||
|         messages, | ||||
|         delay | ||||
|       } | ||||
|       channelConfigurationOptions.bot.quotes.push(q) | ||||
|     } | ||||
|  | ||||
|     // Note: but data in order, because validateData assume index are okay to find associated fields. | ||||
|     for (let i = 0; data.has('command_' + i.toString()); i++) { | ||||
|       const command = (data.get('command_' + i.toString())?.toString() ?? '') | ||||
|       const message = (data.get('command_message_' + i.toString())?.toString() ?? '') | ||||
|       const c: ChannelConfigurationOptions['bot']['commands'][0] = { | ||||
|         command, | ||||
|         message | ||||
|       } | ||||
|       channelConfigurationOptions.bot.commands.push(c) | ||||
|     } | ||||
|  | ||||
|     if (!await validateData(channelConfigurationOptions)) { | ||||
|       throw new Error('Invalid form data') | ||||
|     } | ||||
|  | ||||
|     const headers: any = clientOptions.peertubeHelpers.getAuthHeader() ?? {} | ||||
|     headers['content-type'] = 'application/json;charset=UTF-8' | ||||
|  | ||||
|     const response = await fetch( | ||||
|       getBaseRoute(clientOptions) + '/api/configuration/channel/' + encodeURIComponent(channelId), | ||||
|       { | ||||
|         method: 'POST', | ||||
|         headers, | ||||
|         body: JSON.stringify(channelConfigurationOptions) | ||||
|       } | ||||
|     ) | ||||
|  | ||||
|     if (!response.ok) { | ||||
|       throw new Error('Failed to save configuration options.') | ||||
|     } | ||||
|   } | ||||
|   const toggleSubmit: Function = (disabled: boolean) => { | ||||
|     form.querySelectorAll('input[type=submit], input[type=reset]').forEach((el) => { | ||||
|       if (disabled) { | ||||
|         el.setAttribute('disabled', 'disabled') | ||||
|       } else { | ||||
|         el.removeAttribute('disabled') | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   enableBotCB.onclick = () => refresh() | ||||
|  | ||||
|   for(let [dataClass, rowData] of Object.entries(rowDataRecords)) { | ||||
|     rowData.addButton.onclick = () => addRow(dataClass) | ||||
|  | ||||
|     for (let i = 0; i < rowData.removeButtons.length; i++) { | ||||
|       rowData.removeButtons[i].onclick = () => removeRow(dataClass, i) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   form.onsubmit = () => { | ||||
|     toggleSubmit(true) | ||||
|     if (!form.checkValidity()) { | ||||
|       return false | ||||
|     } | ||||
|     submitForm().then( | ||||
|       () => { | ||||
|         clientOptions.peertubeHelpers.notifier.success(labelSaved) | ||||
|         toggleSubmit(false) | ||||
|       }, | ||||
|       () => { | ||||
|         clientOptions.peertubeHelpers.notifier.error(labelError) | ||||
|         toggleSubmit(false) | ||||
|       } | ||||
|     ) | ||||
|     return false | ||||
|   } | ||||
|   form.onreset = () => { | ||||
|     // Must refresh in a setTimeout, otherwise the checkbox state is not up to date. | ||||
|     setTimeout(() => refresh(), 1) | ||||
|   } | ||||
|   refresh() | ||||
| } | ||||
|  | ||||
| export { | ||||
|   getConfigurationChannelViewData, | ||||
|   vivifyConfigurationChannel | ||||
| } | ||||
							
								
								
									
										21
									
								
								client/common/global-styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								client/common/global-styles.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| let globalSheets: CSSStyleSheet[] | undefined = undefined; | ||||
|  | ||||
| export function getGlobalStyleSheets() { | ||||
|   if (globalSheets === undefined) { | ||||
|     globalSheets = Array.from(document.styleSheets) | ||||
|       .map(x => { | ||||
|         const sheet = new CSSStyleSheet(); | ||||
|         const css = Array.from(x.cssRules).map(rule => rule.cssText).join(' '); | ||||
|         sheet.replaceSync(css); | ||||
|         return sheet; | ||||
|       }); | ||||
|   } | ||||
|  | ||||
|   return globalSheets; | ||||
| } | ||||
|  | ||||
| export function addGlobalStylesToShadowRoot(shadowRoot: ShadowRoot) { | ||||
|   shadowRoot.adoptedStyleSheets.push( | ||||
|     ...getGlobalStyleSheets() | ||||
|   ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user