mirror of https://github.com/Chocobozzz/PeerTube
Support broadcast messages
parent
8adf0a767f
commit
72c33e716f
|
@ -276,6 +276,58 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row mt-4"> <!-- broadcast grid -->
|
||||
<div class="form-group col-12 col-lg-4 col-xl-3">
|
||||
<div i18n class="inner-form-title">BROADCAST MESSAGE</div>
|
||||
<div i18n class="inner-for-description">
|
||||
Display a message on your instance
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
|
||||
|
||||
<ng-container formGroupName="broadcastMessage">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="broadcastMessageEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable broadcast message"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="broadcastMessageDismissable" formControlName="dismissable"
|
||||
i18n-labelText labelText="Allow users to dismiss the broadcast message "
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="broadcastMessageLevel">Broadcast message level</label>
|
||||
<div class="peertube-select-container">
|
||||
<select id="broadcastMessageLevel" formControlName="level" class="form-control">
|
||||
<option value="info">info</option>
|
||||
<option value="warning">warning</option>
|
||||
<option value="error">error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div *ngIf="formErrors.broadcastMessage.level" class="form-error">{{ formErrors.broadcastMessage.level }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="broadcastMessageMessage">Message</label><my-help helpType="markdownText"></my-help>
|
||||
<my-markdown-textarea
|
||||
name="broadcastMessageMessage" formControlName="message" textareaMaxWidth="500px"
|
||||
[classes]="{ 'input-error': formErrors['broadcastMessage.message'] }"
|
||||
></my-markdown-textarea>
|
||||
<div *ngIf="formErrors.broadcastMessage.message" class="form-error">{{ formErrors.broadcastMessage.message }}</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row mt-4"> <!-- new users grid -->
|
||||
<div class="form-group col-12 col-lg-4 col-xl-3">
|
||||
<div i18n class="inner-form-title">NEW USERS</div>
|
||||
|
@ -801,8 +853,9 @@
|
|||
<div class="form-row mt-4"> <!-- submit placement block -->
|
||||
<div class="col-md-7 col-xl-5"></div>
|
||||
<div class="col-md-5 col-xl-5">
|
||||
<span class="form-error submit-error" i18n *ngIf="!form.valid">It seems like the configuration is invalid. Please search for potential errors in the different tabs.</span>
|
||||
|
||||
<input (click)="formValidated()" type="submit" i18n-value value="Update configuration" [disabled]="!form.valid">
|
||||
<span class="form-error" i18n *ngIf="!form.valid">It seems like the configuration is invalid. Please search for potential errors in the different tabs.</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -76,4 +76,8 @@ ngb-tabset:not(.previews) ::ng-deep {
|
|||
.nav-link {
|
||||
font-size: 105%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submit-error {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
|
|
@ -215,6 +215,12 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
|
|||
indexUrl: this.customConfigValidatorsService.INDEX_URL
|
||||
}
|
||||
}
|
||||
},
|
||||
broadcastMessage: {
|
||||
enabled: null,
|
||||
level: null,
|
||||
dismissable: null,
|
||||
message: null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,16 @@
|
|||
<div id="content" tabindex="-1" class="main-col" [ngClass]="{ expanded: menu.isMenuDisplayed === false }">
|
||||
|
||||
<div class="main-row">
|
||||
|
||||
<div *ngIf="broadcastMessage" class="broadcast-message alert" [ngClass]="broadcastMessage.class">
|
||||
<div [innerHTML]="broadcastMessage.message"></div>
|
||||
|
||||
<my-global-icon
|
||||
*ngIf="broadcastMessage.dismissable" (click)="hideBroadcastMessage()"
|
||||
iconName="cross" role="button" title="Close this message" i18n-title
|
||||
></my-global-icon>
|
||||
</div>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
@import '~bootstrap/scss/functions';
|
||||
@import '~bootstrap/scss/variables';
|
||||
|
||||
.peertube-container {
|
||||
padding-bottom: 20px;
|
||||
|
@ -88,3 +90,39 @@
|
|||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.broadcast-message {
|
||||
min-height: 50px;
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
border-radius: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 30px;
|
||||
column-gap: 10px;
|
||||
|
||||
my-global-icon {
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
cursor: pointer;
|
||||
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
@each $color, $value in $theme-colors {
|
||||
&.alert-#{$color} {
|
||||
my-global-icon {
|
||||
@include apply-svg-color(theme-color-level($color, $alert-color-level));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
p {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular
|
|||
import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core'
|
||||
import { is18nPath } from '../../../shared/models/i18n'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
import { filter, map, pairwise } from 'rxjs/operators'
|
||||
import { filter, map, pairwise, first } from 'rxjs/operators'
|
||||
import { Hotkey, HotkeysService } from 'angular2-hotkeys'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { PlatformLocation, ViewportScroller } from '@angular/common'
|
||||
|
@ -19,6 +19,10 @@ import { ServerConfig, UserRole } from '@shared/models'
|
|||
import { User } from '@app/shared'
|
||||
import { InstanceService } from '@app/shared/instance/instance.service'
|
||||
import { MenuService } from './core/menu/menu.service'
|
||||
import { BroadcastMessageLevel } from '@shared/models/server'
|
||||
import { MarkdownService } from './shared/renderer'
|
||||
import { concat } from 'rxjs'
|
||||
import { peertubeLocalStorage } from './shared/misc/peertube-web-storage'
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
|
@ -26,11 +30,14 @@ import { MenuService } from './core/menu/menu.service'
|
|||
styleUrls: [ './app.component.scss' ]
|
||||
})
|
||||
export class AppComponent implements OnInit, AfterViewInit {
|
||||
private static BROADCAST_MESSAGE_KEY = 'app-broadcast-message-dismissed'
|
||||
|
||||
@ViewChild('welcomeModal') welcomeModal: WelcomeModalComponent
|
||||
@ViewChild('instanceConfigWarningModal') instanceConfigWarningModal: InstanceConfigWarningModalComponent
|
||||
@ViewChild('customModal') customModal: CustomModalComponent
|
||||
|
||||
customCSS: SafeHtml
|
||||
broadcastMessage: { message: string, dismissable: boolean, class: string } | null = null
|
||||
|
||||
private serverConfig: ServerConfig
|
||||
|
||||
|
@ -50,6 +57,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||
private hooks: HooksService,
|
||||
private location: PlatformLocation,
|
||||
private modalService: NgbModal,
|
||||
private markdownService: MarkdownService,
|
||||
public menu: MenuService
|
||||
) { }
|
||||
|
||||
|
@ -81,6 +89,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||
this.initRouteEvents()
|
||||
this.injectJS()
|
||||
this.injectCSS()
|
||||
this.injectBroadcastMessage()
|
||||
|
||||
this.initHotkeys()
|
||||
|
||||
|
@ -97,6 +106,12 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||
return this.authService.isLoggedIn()
|
||||
}
|
||||
|
||||
hideBroadcastMessage () {
|
||||
peertubeLocalStorage.setItem(AppComponent.BROADCAST_MESSAGE_KEY, this.serverConfig.broadcastMessage.message)
|
||||
|
||||
this.broadcastMessage = null
|
||||
}
|
||||
|
||||
private initRouteEvents () {
|
||||
let resetScroll = true
|
||||
const eventsObs = this.router.events
|
||||
|
@ -165,6 +180,36 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||
).subscribe(() => this.menu.isMenuDisplayed = false) // User clicked on a link in the menu, change the page
|
||||
}
|
||||
|
||||
private injectBroadcastMessage () {
|
||||
concat(
|
||||
this.serverService.getConfig().pipe(first()),
|
||||
this.serverService.configReloaded
|
||||
).subscribe(async config => {
|
||||
this.broadcastMessage = null
|
||||
|
||||
const messageConfig = config.broadcastMessage
|
||||
|
||||
if (messageConfig.enabled) {
|
||||
// Already dismissed this message?
|
||||
if (messageConfig.dismissable && localStorage.getItem(AppComponent.BROADCAST_MESSAGE_KEY) === messageConfig.message) {
|
||||
return
|
||||
}
|
||||
|
||||
const classes: { [id in BroadcastMessageLevel]: string } = {
|
||||
info: 'alert-info',
|
||||
warning: 'alert-warning',
|
||||
error: 'alert-danger'
|
||||
}
|
||||
|
||||
this.broadcastMessage = {
|
||||
message: await this.markdownService.completeMarkdownToHTML(messageConfig.message),
|
||||
dismissable: messageConfig.dismissable,
|
||||
class: classes[messageConfig.level]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private injectJS () {
|
||||
// Inject JS
|
||||
this.serverService.getConfig()
|
||||
|
@ -182,17 +227,19 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||
|
||||
private injectCSS () {
|
||||
// Inject CSS if modified (admin config settings)
|
||||
this.serverService.configReloaded
|
||||
.subscribe(() => {
|
||||
const headStyle = document.querySelector('style.custom-css-style')
|
||||
if (headStyle) headStyle.parentNode.removeChild(headStyle)
|
||||
concat(
|
||||
this.serverService.getConfig().pipe(first()),
|
||||
this.serverService.configReloaded
|
||||
).subscribe(config => {
|
||||
const headStyle = document.querySelector('style.custom-css-style')
|
||||
if (headStyle) headStyle.parentNode.removeChild(headStyle)
|
||||
|
||||
// We test customCSS if the admin removed the css
|
||||
if (this.customCSS || this.serverConfig.instance.customizations.css) {
|
||||
const styleTag = '<style>' + this.serverConfig.instance.customizations.css + '</style>'
|
||||
this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag)
|
||||
}
|
||||
})
|
||||
// We test customCSS if the admin removed the css
|
||||
if (this.customCSS || config.instance.customizations.css) {
|
||||
const styleTag = '<style>' + config.instance.customizations.css + '</style>'
|
||||
this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async loadPlugins () {
|
||||
|
|
|
@ -21,7 +21,7 @@ export class ServerService {
|
|||
|
||||
private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
|
||||
|
||||
configReloaded = new Subject<void>()
|
||||
configReloaded = new Subject<ServerConfig>()
|
||||
|
||||
private localeObservable: Observable<any>
|
||||
private videoLicensesObservable: Observable<VideoConstant<number>[]>
|
||||
|
@ -139,6 +139,12 @@ export class ServerService {
|
|||
indexUrl: 'https://instances.joinpeertube.org'
|
||||
}
|
||||
}
|
||||
},
|
||||
broadcastMessage: {
|
||||
enabled: false,
|
||||
message: '',
|
||||
level: 'info',
|
||||
dismissable: false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,6 +168,11 @@ export class ServerService {
|
|||
resetConfig () {
|
||||
this.configLoaded = false
|
||||
this.configReset = true
|
||||
|
||||
// Notify config update
|
||||
this.getConfig().subscribe(() => {
|
||||
// empty, to fire a reset config event
|
||||
})
|
||||
}
|
||||
|
||||
getConfig () {
|
||||
|
@ -175,9 +186,9 @@ export class ServerService {
|
|||
this.config = config
|
||||
this.configLoaded = true
|
||||
}),
|
||||
tap(() => {
|
||||
tap(config => {
|
||||
if (this.configReset) {
|
||||
this.configReloaded.next()
|
||||
this.configReloaded.next(config)
|
||||
this.configReset = false
|
||||
}
|
||||
}),
|
||||
|
|
|
@ -372,3 +372,9 @@ followings:
|
|||
|
||||
theme:
|
||||
default: 'default'
|
||||
|
||||
broadcast_message:
|
||||
enabled: false
|
||||
message: '' # Support markdown
|
||||
level: 'info' # 'info' | 'warning' | 'error'
|
||||
dismissable: false
|
||||
|
|
|
@ -386,3 +386,9 @@ followings:
|
|||
|
||||
theme:
|
||||
default: 'default'
|
||||
|
||||
broadcast_message:
|
||||
enabled: false
|
||||
message: '' # Support markdown
|
||||
level: 'info' # 'info' | 'warning' | 'error'
|
||||
dismissable: false
|
||||
|
|
|
@ -172,6 +172,13 @@ async function getConfig (req: express.Request, res: express.Response) {
|
|||
indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
broadcastMessage: {
|
||||
enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
|
||||
message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
|
||||
level: CONFIG.BROADCAST_MESSAGE.LEVEL,
|
||||
dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -432,6 +439,12 @@ function customConfig (): CustomConfig {
|
|||
indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
|
||||
}
|
||||
}
|
||||
},
|
||||
broadcastMessage: {
|
||||
enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
|
||||
message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
|
||||
level: CONFIG.BROADCAST_MESSAGE.LEVEL,
|
||||
dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,6 +107,10 @@ function checkConfig () {
|
|||
}
|
||||
}
|
||||
|
||||
if (CONFIG.STORAGE.VIDEOS_DIR === CONFIG.STORAGE.REDUNDANCY_DIR) {
|
||||
logger.warn('Redundancy directory should be different than the videos folder.')
|
||||
}
|
||||
|
||||
// Transcoding
|
||||
if (CONFIG.TRANSCODING.ENABLED) {
|
||||
if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false && CONFIG.TRANSCODING.HLS.ENABLED === false) {
|
||||
|
@ -114,8 +118,14 @@ function checkConfig () {
|
|||
}
|
||||
}
|
||||
|
||||
if (CONFIG.STORAGE.VIDEOS_DIR === CONFIG.STORAGE.REDUNDANCY_DIR) {
|
||||
logger.warn('Redundancy directory should be different than the videos folder.')
|
||||
// Broadcast message
|
||||
if (CONFIG.BROADCAST_MESSAGE.ENABLED) {
|
||||
const currentLevel = CONFIG.BROADCAST_MESSAGE.LEVEL
|
||||
const available = [ 'info', 'warning', 'error' ]
|
||||
|
||||
if (available.includes(currentLevel) === false) {
|
||||
return 'Broadcast message level should be ' + available.join(' or ') + ' instead of ' + currentLevel
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
|
|
|
@ -6,6 +6,7 @@ import { buildPath, parseBytes, parseDurationToMs, root } from '../helpers/core-
|
|||
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
|
||||
import * as bytes from 'bytes'
|
||||
import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
|
||||
import { BroadcastMessageLevel } from '@shared/models/server'
|
||||
|
||||
// Use a variable to reload the configuration if we need
|
||||
let config: IConfig = require('config')
|
||||
|
@ -285,6 +286,12 @@ const CONFIG = {
|
|||
},
|
||||
THEME: {
|
||||
get DEFAULT () { return config.get<string>('theme.default') }
|
||||
},
|
||||
BROADCAST_MESSAGE: {
|
||||
get ENABLED () { return config.get<boolean>('broadcast_message.enabled') },
|
||||
get MESSAGE () { return config.get<string>('broadcast_message.message') },
|
||||
get LEVEL () { return config.get<BroadcastMessageLevel>('broadcast_message.level') },
|
||||
get DISMISSABLE () { return config.get<boolean>('broadcast_message.dismissable') }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -55,6 +55,11 @@ const customConfigUpdateValidator = [
|
|||
|
||||
body('theme.default').custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
|
||||
|
||||
body('broadcastMessage.enabled').isBoolean().withMessage('Should have a valid broadcast message enabled boolean'),
|
||||
body('broadcastMessage.message').exists().withMessage('Should have a valid broadcast message'),
|
||||
body('broadcastMessage.level').exists().withMessage('Should have a valid broadcast level'),
|
||||
body('broadcastMessage.dismissable').exists().withMessage('Should have a valid broadcast dismissable boolean'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body })
|
||||
|
||||
|
|
|
@ -133,6 +133,12 @@ describe('Test config API validators', function () {
|
|||
indexUrl: 'https://index.example.com'
|
||||
}
|
||||
}
|
||||
},
|
||||
broadcastMessage: {
|
||||
enabled: true,
|
||||
dismissable: true,
|
||||
message: 'super message',
|
||||
level: 'warning'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -87,6 +87,11 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
|
|||
expect(data.followings.instance.autoFollowBack.enabled).to.be.false
|
||||
expect(data.followings.instance.autoFollowIndex.enabled).to.be.false
|
||||
expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('')
|
||||
|
||||
expect(data.broadcastMessage.enabled).to.be.false
|
||||
expect(data.broadcastMessage.level).to.equal('info')
|
||||
expect(data.broadcastMessage.message).to.equal('')
|
||||
expect(data.broadcastMessage.dismissable).to.be.false
|
||||
}
|
||||
|
||||
function checkUpdatedConfig (data: CustomConfig) {
|
||||
|
@ -155,6 +160,11 @@ function checkUpdatedConfig (data: CustomConfig) {
|
|||
expect(data.followings.instance.autoFollowBack.enabled).to.be.true
|
||||
expect(data.followings.instance.autoFollowIndex.enabled).to.be.true
|
||||
expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://updated.example.com')
|
||||
|
||||
expect(data.broadcastMessage.enabled).to.be.true
|
||||
expect(data.broadcastMessage.level).to.equal('error')
|
||||
expect(data.broadcastMessage.message).to.equal('super bad message')
|
||||
expect(data.broadcastMessage.dismissable).to.be.true
|
||||
}
|
||||
|
||||
describe('Test config', function () {
|
||||
|
@ -324,6 +334,12 @@ describe('Test config', function () {
|
|||
indexUrl: 'https://updated.example.com'
|
||||
}
|
||||
}
|
||||
},
|
||||
broadcastMessage: {
|
||||
enabled: true,
|
||||
level: 'error',
|
||||
message: 'super bad message',
|
||||
dismissable: true
|
||||
}
|
||||
}
|
||||
await updateCustomConfig(server.url, server.accessToken, newCustomConfig)
|
||||
|
|
|
@ -159,6 +159,12 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
|
|||
enabled: false
|
||||
}
|
||||
}
|
||||
},
|
||||
broadcastMessage: {
|
||||
enabled: true,
|
||||
level: 'warning',
|
||||
message: 'hello',
|
||||
dismissable: true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export type BroadcastMessageLevel = 'info' | 'warning' | 'error'
|
|
@ -1,4 +1,5 @@
|
|||
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
|
||||
import { BroadcastMessageLevel } from './broadcast-message-level.type'
|
||||
|
||||
export interface CustomConfig {
|
||||
instance: {
|
||||
|
@ -131,4 +132,11 @@ export interface CustomConfig {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
broadcastMessage: {
|
||||
enabled: boolean
|
||||
message: string
|
||||
level: BroadcastMessageLevel
|
||||
dismissable: boolean
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export * from './about.model'
|
||||
export * from './broadcast-message-level.type'
|
||||
export * from './contact-form.model'
|
||||
export * from './custom-config.model'
|
||||
export * from './debug.model'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
|
||||
import { ClientScript } from '../plugins/plugin-package-json.model'
|
||||
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
|
||||
import { BroadcastMessageLevel } from './broadcast-message-level.type'
|
||||
|
||||
export interface ServerConfigPlugin {
|
||||
name: string
|
||||
|
@ -161,4 +162,11 @@ export interface ServerConfig {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
broadcastMessage: {
|
||||
enabled: boolean
|
||||
message: string
|
||||
level: BroadcastMessageLevel
|
||||
dismissable: boolean
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue