From 72c33e716fecd1826dcf645957f8669821f91ff3 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 28 May 2020 11:15:38 +0200 Subject: [PATCH] Support broadcast messages --- .../edit-custom-config.component.html | 55 ++++++++++++++- .../edit-custom-config.component.scss | 6 +- .../edit-custom-config.component.ts | 6 ++ client/src/app/app.component.html | 10 +++ client/src/app/app.component.scss | 38 ++++++++++ client/src/app/app.component.ts | 69 ++++++++++++++++--- client/src/app/core/server/server.service.ts | 17 ++++- config/default.yaml | 6 ++ config/production.yaml.example | 6 ++ server/controllers/api/config.ts | 13 ++++ server/initializers/checker-after-init.ts | 14 +++- server/initializers/config.ts | 7 ++ server/middlewares/validators/config.ts | 5 ++ server/tests/api/check-params/config.ts | 6 ++ server/tests/api/server/config.ts | 16 +++++ shared/extra-utils/server/config.ts | 6 ++ .../server/broadcast-message-level.type.ts | 1 + shared/models/server/custom-config.model.ts | 8 +++ shared/models/server/index.ts | 1 + shared/models/server/server-config.model.ts | 10 ++- 20 files changed, 281 insertions(+), 19 deletions(-) create mode 100644 shared/models/server/broadcast-message-level.type.ts diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index 5703d5a2e..4ee573696 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html @@ -276,6 +276,58 @@ +
+
+
BROADCAST MESSAGE
+
+ Display a message on your instance +
+
+ +
+ + + +
+ +
+ +
+ +
+ +
+ +
+ +
+
{{ formErrors.broadcastMessage.level }}
+
+ +
+ + +
{{ formErrors.broadcastMessage.message }}
+
+ +
+ +
+
+
NEW USERS
@@ -801,8 +853,9 @@
+ It seems like the configuration is invalid. Please search for potential errors in the different tabs. + - It seems like the configuration is invalid. Please search for potential errors in the different tabs.
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss index 9ee960ad6..2bfa92da4 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss @@ -76,4 +76,8 @@ ngb-tabset:not(.previews) ::ng-deep { .nav-link { font-size: 105%; } -} \ No newline at end of file +} + +.submit-error { + margin-bottom: 20px; +} diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index cea314cea..6d59494c8 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts @@ -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 } } diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index b0d2e5050..b243c129b 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -25,6 +25,16 @@
+ +
+
+ + +
+
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index 0c33dc4a1..27fd69c8d 100644 --- a/client/src/app/app.component.scss +++ b/client/src/app/app.component.scss @@ -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; + } + } +} diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 12c0efd8a..a464e90fa 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -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 = '' - this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag) - } - }) + // We test customCSS if the admin removed the css + if (this.customCSS || config.instance.customizations.css) { + const styleTag = '' + this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag) + } + }) } private async loadPlugins () { diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index eac8f85e4..fdfbe4c02 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts @@ -21,7 +21,7 @@ export class ServerService { private static CONFIG_LOCAL_STORAGE_KEY = 'server-config' - configReloaded = new Subject() + configReloaded = new Subject() private localeObservable: Observable private videoLicensesObservable: Observable[]> @@ -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 } }), diff --git a/config/default.yaml b/config/default.yaml index a0f2eb3a1..34a0a146f 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -372,3 +372,9 @@ followings: theme: default: 'default' + +broadcast_message: + enabled: false + message: '' # Support markdown + level: 'info' # 'info' | 'warning' | 'error' + dismissable: false diff --git a/config/production.yaml.example b/config/production.yaml.example index 8b8c98f8c..0ac05c515 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -386,3 +386,9 @@ followings: theme: default: 'default' + +broadcast_message: + enabled: false + message: '' # Support markdown + level: 'info' # 'info' | 'warning' | 'error' + dismissable: false diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index edcb0b99e..41e5027b9 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts @@ -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 } } } diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts index f111be2ae..b5b854137 100644 --- a/server/initializers/checker-after-init.ts +++ b/server/initializers/checker-after-init.ts @@ -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 diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 6932b41e1..e2920ce9e 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts @@ -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('theme.default') } + }, + BROADCAST_MESSAGE: { + get ENABLED () { return config.get('broadcast_message.enabled') }, + get MESSAGE () { return config.get('broadcast_message.message') }, + get LEVEL () { return config.get('broadcast_message.level') }, + get DISMISSABLE () { return config.get('broadcast_message.dismissable') } } } diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index dfa549e76..6905ac762 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts @@ -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 }) diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index f1a79806b..7c96fa762 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts @@ -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' } } diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index 8580835d6..d18a93082 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts @@ -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) diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts index 743d10316..98cd435f6 100644 --- a/shared/extra-utils/server/config.ts +++ b/shared/extra-utils/server/config.ts @@ -159,6 +159,12 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti enabled: false } } + }, + broadcastMessage: { + enabled: true, + level: 'warning', + message: 'hello', + dismissable: true } } diff --git a/shared/models/server/broadcast-message-level.type.ts b/shared/models/server/broadcast-message-level.type.ts new file mode 100644 index 000000000..bf43e18b5 --- /dev/null +++ b/shared/models/server/broadcast-message-level.type.ts @@ -0,0 +1 @@ +export type BroadcastMessageLevel = 'info' | 'warning' | 'error' diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts index 07e17bda2..851bf1854 100644 --- a/shared/models/server/custom-config.model.ts +++ b/shared/models/server/custom-config.model.ts @@ -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 + } } diff --git a/shared/models/server/index.ts b/shared/models/server/index.ts index b0afb2c66..2bb443d46 100644 --- a/shared/models/server/index.ts +++ b/shared/models/server/index.ts @@ -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' diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts index a1f9b3b5d..9c903b7ee 100644 --- a/shared/models/server/server-config.model.ts +++ b/shared/models/server/server-config.model.ts @@ -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 + } }