NEW USERS
@@ -801,8 +853,9 @@
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
+ }
}