</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>
<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>
.nav-link {
font-size: 105%;
}
-}
\ No newline at end of file
+}
+
+.submit-error {
+ margin-bottom: 20px;
+}
indexUrl: this.customConfigValidatorsService.INDEX_URL
}
}
+ },
+ broadcastMessage: {
+ enabled: null,
+ level: null,
+ dismissable: null,
+ message: null
}
}
<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>
@import '_variables';
@import '_mixins';
+@import '~bootstrap/scss/functions';
+@import '~bootstrap/scss/variables';
.peertube-container {
padding-bottom: 20px;
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;
+ }
+ }
+}
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'
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',
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
private hooks: HooksService,
private location: PlatformLocation,
private modalService: NgbModal,
+ private markdownService: MarkdownService,
public menu: MenuService
) { }
this.initRouteEvents()
this.injectJS()
this.injectCSS()
+ this.injectBroadcastMessage()
this.initHotkeys()
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
).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()
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)
-
- // 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)
- }
- })
+ 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 || config.instance.customizations.css) {
+ const styleTag = '<style>' + config.instance.customizations.css + '</style>'
+ this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag)
+ }
+ })
}
private async loadPlugins () {
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>[]>
indexUrl: 'https://instances.joinpeertube.org'
}
}
+ },
+ broadcastMessage: {
+ enabled: false,
+ message: '',
+ level: 'info',
+ dismissable: false
}
}
resetConfig () {
this.configLoaded = false
this.configReset = true
+
+ // Notify config update
+ this.getConfig().subscribe(() => {
+ // empty, to fire a reset config event
+ })
}
getConfig () {
this.config = config
this.configLoaded = true
}),
- tap(() => {
+ tap(config => {
if (this.configReset) {
- this.configReloaded.next()
+ this.configReloaded.next(config)
this.configReset = false
}
}),
theme:
default: 'default'
+
+broadcast_message:
+ enabled: false
+ message: '' # Support markdown
+ level: 'info' # 'info' | 'warning' | 'error'
+ dismissable: false
theme:
default: 'default'
+
+broadcast_message:
+ enabled: false
+ message: '' # Support markdown
+ level: 'info' # 'info' | 'warning' | 'error'
+ dismissable: false
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
}
}
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
}
}
}
}
}
+ 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) {
}
}
- 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
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')
},
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') }
}
}
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 })
indexUrl: 'https://index.example.com'
}
}
+ },
+ broadcastMessage: {
+ enabled: true,
+ dismissable: true,
+ message: 'super message',
+ level: 'warning'
}
}
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) {
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 () {
indexUrl: 'https://updated.example.com'
}
}
+ },
+ broadcastMessage: {
+ enabled: true,
+ level: 'error',
+ message: 'super bad message',
+ dismissable: true
}
}
await updateCustomConfig(server.url, server.accessToken, newCustomConfig)
enabled: false
}
}
+ },
+ broadcastMessage: {
+ enabled: true,
+ level: 'warning',
+ message: 'hello',
+ dismissable: true
}
}
--- /dev/null
+export type BroadcastMessageLevel = 'info' | 'warning' | 'error'
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
+import { BroadcastMessageLevel } from './broadcast-message-level.type'
export interface CustomConfig {
instance: {
}
}
}
+
+ broadcastMessage: {
+ enabled: boolean
+ message: string
+ level: BroadcastMessageLevel
+ dismissable: boolean
+ }
}
export * from './about.model'
+export * from './broadcast-message-level.type'
export * from './contact-form.model'
export * from './custom-config.model'
export * from './debug.model'
-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
}
}
}
+
+ broadcastMessage: {
+ enabled: boolean
+ message: string
+ level: BroadcastMessageLevel
+ dismissable: boolean
+ }
}