diff options
author | Chocobozzz <me@florianbigard.com> | 2020-05-28 11:15:38 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-05-29 09:32:12 +0200 |
commit | 72c33e716fecd1826dcf645957f8669821f91ff3 (patch) | |
tree | 31d270c2afc2f07303fa491189d6b6b1c0ea8bb1 /client | |
parent | 8adf0a767f0816465ac3a8f4a6c63f53dd05fe3d (diff) | |
download | PeerTube-72c33e716fecd1826dcf645957f8669821f91ff3.tar.gz PeerTube-72c33e716fecd1826dcf645957f8669821f91ff3.tar.zst PeerTube-72c33e716fecd1826dcf645957f8669821f91ff3.zip |
Support broadcast messages
Diffstat (limited to 'client')
7 files changed, 186 insertions, 17 deletions
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 @@ | |||
276 | </div> | 276 | </div> |
277 | </div> | 277 | </div> |
278 | 278 | ||
279 | <div class="form-row mt-4"> <!-- broadcast grid --> | ||
280 | <div class="form-group col-12 col-lg-4 col-xl-3"> | ||
281 | <div i18n class="inner-form-title">BROADCAST MESSAGE</div> | ||
282 | <div i18n class="inner-for-description"> | ||
283 | Display a message on your instance | ||
284 | </div> | ||
285 | </div> | ||
286 | |||
287 | <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> | ||
288 | |||
289 | <ng-container formGroupName="broadcastMessage"> | ||
290 | |||
291 | <div class="form-group"> | ||
292 | <my-peertube-checkbox | ||
293 | inputName="broadcastMessageEnabled" formControlName="enabled" | ||
294 | i18n-labelText labelText="Enable broadcast message" | ||
295 | ></my-peertube-checkbox> | ||
296 | </div> | ||
297 | |||
298 | <div class="form-group"> | ||
299 | <my-peertube-checkbox | ||
300 | inputName="broadcastMessageDismissable" formControlName="dismissable" | ||
301 | i18n-labelText labelText="Allow users to dismiss the broadcast message " | ||
302 | ></my-peertube-checkbox> | ||
303 | </div> | ||
304 | |||
305 | <div class="form-group"> | ||
306 | <label i18n for="broadcastMessageLevel">Broadcast message level</label> | ||
307 | <div class="peertube-select-container"> | ||
308 | <select id="broadcastMessageLevel" formControlName="level" class="form-control"> | ||
309 | <option value="info">info</option> | ||
310 | <option value="warning">warning</option> | ||
311 | <option value="error">error</option> | ||
312 | </select> | ||
313 | </div> | ||
314 | <div *ngIf="formErrors.broadcastMessage.level" class="form-error">{{ formErrors.broadcastMessage.level }}</div> | ||
315 | </div> | ||
316 | |||
317 | <div class="form-group"> | ||
318 | <label i18n for="broadcastMessageMessage">Message</label><my-help helpType="markdownText"></my-help> | ||
319 | <my-markdown-textarea | ||
320 | name="broadcastMessageMessage" formControlName="message" textareaMaxWidth="500px" | ||
321 | [classes]="{ 'input-error': formErrors['broadcastMessage.message'] }" | ||
322 | ></my-markdown-textarea> | ||
323 | <div *ngIf="formErrors.broadcastMessage.message" class="form-error">{{ formErrors.broadcastMessage.message }}</div> | ||
324 | </div> | ||
325 | |||
326 | </ng-container> | ||
327 | |||
328 | </div> | ||
329 | </div> | ||
330 | |||
279 | <div class="form-row mt-4"> <!-- new users grid --> | 331 | <div class="form-row mt-4"> <!-- new users grid --> |
280 | <div class="form-group col-12 col-lg-4 col-xl-3"> | 332 | <div class="form-group col-12 col-lg-4 col-xl-3"> |
281 | <div i18n class="inner-form-title">NEW USERS</div> | 333 | <div i18n class="inner-form-title">NEW USERS</div> |
@@ -801,8 +853,9 @@ | |||
801 | <div class="form-row mt-4"> <!-- submit placement block --> | 853 | <div class="form-row mt-4"> <!-- submit placement block --> |
802 | <div class="col-md-7 col-xl-5"></div> | 854 | <div class="col-md-7 col-xl-5"></div> |
803 | <div class="col-md-5 col-xl-5"> | 855 | <div class="col-md-5 col-xl-5"> |
856 | <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> | ||
857 | |||
804 | <input (click)="formValidated()" type="submit" i18n-value value="Update configuration" [disabled]="!form.valid"> | 858 | <input (click)="formValidated()" type="submit" i18n-value value="Update configuration" [disabled]="!form.valid"> |
805 | <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> | ||
806 | </div> | 859 | </div> |
807 | </div> | 860 | </div> |
808 | </form> | 861 | </form> |
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 { | |||
76 | .nav-link { | 76 | .nav-link { |
77 | font-size: 105%; | 77 | font-size: 105%; |
78 | } | 78 | } |
79 | } \ No newline at end of file | 79 | } |
80 | |||
81 | .submit-error { | ||
82 | margin-bottom: 20px; | ||
83 | } | ||
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 | |||
215 | indexUrl: this.customConfigValidatorsService.INDEX_URL | 215 | indexUrl: this.customConfigValidatorsService.INDEX_URL |
216 | } | 216 | } |
217 | } | 217 | } |
218 | }, | ||
219 | broadcastMessage: { | ||
220 | enabled: null, | ||
221 | level: null, | ||
222 | dismissable: null, | ||
223 | message: null | ||
218 | } | 224 | } |
219 | } | 225 | } |
220 | 226 | ||
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 @@ | |||
25 | <div id="content" tabindex="-1" class="main-col" [ngClass]="{ expanded: menu.isMenuDisplayed === false }"> | 25 | <div id="content" tabindex="-1" class="main-col" [ngClass]="{ expanded: menu.isMenuDisplayed === false }"> |
26 | 26 | ||
27 | <div class="main-row"> | 27 | <div class="main-row"> |
28 | |||
29 | <div *ngIf="broadcastMessage" class="broadcast-message alert" [ngClass]="broadcastMessage.class"> | ||
30 | <div [innerHTML]="broadcastMessage.message"></div> | ||
31 | |||
32 | <my-global-icon | ||
33 | *ngIf="broadcastMessage.dismissable" (click)="hideBroadcastMessage()" | ||
34 | iconName="cross" role="button" title="Close this message" i18n-title | ||
35 | ></my-global-icon> | ||
36 | </div> | ||
37 | |||
28 | <router-outlet></router-outlet> | 38 | <router-outlet></router-outlet> |
29 | </div> | 39 | </div> |
30 | </div> | 40 | </div> |
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 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | @import '~bootstrap/scss/functions'; | ||
4 | @import '~bootstrap/scss/variables'; | ||
3 | 5 | ||
4 | .peertube-container { | 6 | .peertube-container { |
5 | padding-bottom: 20px; | 7 | padding-bottom: 20px; |
@@ -88,3 +90,39 @@ | |||
88 | flex: 1; | 90 | flex: 1; |
89 | } | 91 | } |
90 | } | 92 | } |
93 | |||
94 | .broadcast-message { | ||
95 | min-height: 50px; | ||
96 | text-align: center; | ||
97 | margin-bottom: 0; | ||
98 | border-radius: 0; | ||
99 | display: grid; | ||
100 | grid-template-columns: 1fr 30px; | ||
101 | column-gap: 10px; | ||
102 | |||
103 | my-global-icon { | ||
104 | justify-self: center; | ||
105 | align-self: center; | ||
106 | cursor: pointer; | ||
107 | |||
108 | width: 20px; | ||
109 | } | ||
110 | |||
111 | @each $color, $value in $theme-colors { | ||
112 | &.alert-#{$color} { | ||
113 | my-global-icon { | ||
114 | @include apply-svg-color(theme-color-level($color, $alert-color-level)); | ||
115 | } | ||
116 | } | ||
117 | } | ||
118 | |||
119 | ::ng-deep { | ||
120 | p { | ||
121 | font-size: 16px; | ||
122 | } | ||
123 | |||
124 | p:last-child { | ||
125 | margin-bottom: 0; | ||
126 | } | ||
127 | } | ||
128 | } | ||
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 | |||
4 | import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core' | 4 | import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core' |
5 | import { is18nPath } from '../../../shared/models/i18n' | 5 | import { is18nPath } from '../../../shared/models/i18n' |
6 | import { ScreenService } from '@app/shared/misc/screen.service' | 6 | import { ScreenService } from '@app/shared/misc/screen.service' |
7 | import { filter, map, pairwise } from 'rxjs/operators' | 7 | import { filter, map, pairwise, first } from 'rxjs/operators' |
8 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | 8 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
9 | import { I18n } from '@ngx-translate/i18n-polyfill' | 9 | import { I18n } from '@ngx-translate/i18n-polyfill' |
10 | import { PlatformLocation, ViewportScroller } from '@angular/common' | 10 | import { PlatformLocation, ViewportScroller } from '@angular/common' |
@@ -19,6 +19,10 @@ import { ServerConfig, UserRole } from '@shared/models' | |||
19 | import { User } from '@app/shared' | 19 | import { User } from '@app/shared' |
20 | import { InstanceService } from '@app/shared/instance/instance.service' | 20 | import { InstanceService } from '@app/shared/instance/instance.service' |
21 | import { MenuService } from './core/menu/menu.service' | 21 | import { MenuService } from './core/menu/menu.service' |
22 | import { BroadcastMessageLevel } from '@shared/models/server' | ||
23 | import { MarkdownService } from './shared/renderer' | ||
24 | import { concat } from 'rxjs' | ||
25 | import { peertubeLocalStorage } from './shared/misc/peertube-web-storage' | ||
22 | 26 | ||
23 | @Component({ | 27 | @Component({ |
24 | selector: 'my-app', | 28 | selector: 'my-app', |
@@ -26,11 +30,14 @@ import { MenuService } from './core/menu/menu.service' | |||
26 | styleUrls: [ './app.component.scss' ] | 30 | styleUrls: [ './app.component.scss' ] |
27 | }) | 31 | }) |
28 | export class AppComponent implements OnInit, AfterViewInit { | 32 | export class AppComponent implements OnInit, AfterViewInit { |
33 | private static BROADCAST_MESSAGE_KEY = 'app-broadcast-message-dismissed' | ||
34 | |||
29 | @ViewChild('welcomeModal') welcomeModal: WelcomeModalComponent | 35 | @ViewChild('welcomeModal') welcomeModal: WelcomeModalComponent |
30 | @ViewChild('instanceConfigWarningModal') instanceConfigWarningModal: InstanceConfigWarningModalComponent | 36 | @ViewChild('instanceConfigWarningModal') instanceConfigWarningModal: InstanceConfigWarningModalComponent |
31 | @ViewChild('customModal') customModal: CustomModalComponent | 37 | @ViewChild('customModal') customModal: CustomModalComponent |
32 | 38 | ||
33 | customCSS: SafeHtml | 39 | customCSS: SafeHtml |
40 | broadcastMessage: { message: string, dismissable: boolean, class: string } | null = null | ||
34 | 41 | ||
35 | private serverConfig: ServerConfig | 42 | private serverConfig: ServerConfig |
36 | 43 | ||
@@ -50,6 +57,7 @@ export class AppComponent implements OnInit, AfterViewInit { | |||
50 | private hooks: HooksService, | 57 | private hooks: HooksService, |
51 | private location: PlatformLocation, | 58 | private location: PlatformLocation, |
52 | private modalService: NgbModal, | 59 | private modalService: NgbModal, |
60 | private markdownService: MarkdownService, | ||
53 | public menu: MenuService | 61 | public menu: MenuService |
54 | ) { } | 62 | ) { } |
55 | 63 | ||
@@ -81,6 +89,7 @@ export class AppComponent implements OnInit, AfterViewInit { | |||
81 | this.initRouteEvents() | 89 | this.initRouteEvents() |
82 | this.injectJS() | 90 | this.injectJS() |
83 | this.injectCSS() | 91 | this.injectCSS() |
92 | this.injectBroadcastMessage() | ||
84 | 93 | ||
85 | this.initHotkeys() | 94 | this.initHotkeys() |
86 | 95 | ||
@@ -97,6 +106,12 @@ export class AppComponent implements OnInit, AfterViewInit { | |||
97 | return this.authService.isLoggedIn() | 106 | return this.authService.isLoggedIn() |
98 | } | 107 | } |
99 | 108 | ||
109 | hideBroadcastMessage () { | ||
110 | peertubeLocalStorage.setItem(AppComponent.BROADCAST_MESSAGE_KEY, this.serverConfig.broadcastMessage.message) | ||
111 | |||
112 | this.broadcastMessage = null | ||
113 | } | ||
114 | |||
100 | private initRouteEvents () { | 115 | private initRouteEvents () { |
101 | let resetScroll = true | 116 | let resetScroll = true |
102 | const eventsObs = this.router.events | 117 | const eventsObs = this.router.events |
@@ -165,6 +180,36 @@ export class AppComponent implements OnInit, AfterViewInit { | |||
165 | ).subscribe(() => this.menu.isMenuDisplayed = false) // User clicked on a link in the menu, change the page | 180 | ).subscribe(() => this.menu.isMenuDisplayed = false) // User clicked on a link in the menu, change the page |
166 | } | 181 | } |
167 | 182 | ||
183 | private injectBroadcastMessage () { | ||
184 | concat( | ||
185 | this.serverService.getConfig().pipe(first()), | ||
186 | this.serverService.configReloaded | ||
187 | ).subscribe(async config => { | ||
188 | this.broadcastMessage = null | ||
189 | |||
190 | const messageConfig = config.broadcastMessage | ||
191 | |||
192 | if (messageConfig.enabled) { | ||
193 | // Already dismissed this message? | ||
194 | if (messageConfig.dismissable && localStorage.getItem(AppComponent.BROADCAST_MESSAGE_KEY) === messageConfig.message) { | ||
195 | return | ||
196 | } | ||
197 | |||
198 | const classes: { [id in BroadcastMessageLevel]: string } = { | ||
199 | info: 'alert-info', | ||
200 | warning: 'alert-warning', | ||
201 | error: 'alert-danger' | ||
202 | } | ||
203 | |||
204 | this.broadcastMessage = { | ||
205 | message: await this.markdownService.completeMarkdownToHTML(messageConfig.message), | ||
206 | dismissable: messageConfig.dismissable, | ||
207 | class: classes[messageConfig.level] | ||
208 | } | ||
209 | } | ||
210 | }) | ||
211 | } | ||
212 | |||
168 | private injectJS () { | 213 | private injectJS () { |
169 | // Inject JS | 214 | // Inject JS |
170 | this.serverService.getConfig() | 215 | this.serverService.getConfig() |
@@ -182,17 +227,19 @@ export class AppComponent implements OnInit, AfterViewInit { | |||
182 | 227 | ||
183 | private injectCSS () { | 228 | private injectCSS () { |
184 | // Inject CSS if modified (admin config settings) | 229 | // Inject CSS if modified (admin config settings) |
185 | this.serverService.configReloaded | 230 | concat( |
186 | .subscribe(() => { | 231 | this.serverService.getConfig().pipe(first()), |
187 | const headStyle = document.querySelector('style.custom-css-style') | 232 | this.serverService.configReloaded |
188 | if (headStyle) headStyle.parentNode.removeChild(headStyle) | 233 | ).subscribe(config => { |
189 | 234 | const headStyle = document.querySelector('style.custom-css-style') | |
190 | // We test customCSS if the admin removed the css | 235 | if (headStyle) headStyle.parentNode.removeChild(headStyle) |
191 | if (this.customCSS || this.serverConfig.instance.customizations.css) { | 236 | |
192 | const styleTag = '<style>' + this.serverConfig.instance.customizations.css + '</style>' | 237 | // We test customCSS if the admin removed the css |
193 | this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag) | 238 | if (this.customCSS || config.instance.customizations.css) { |
194 | } | 239 | const styleTag = '<style>' + config.instance.customizations.css + '</style>' |
195 | }) | 240 | this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag) |
241 | } | ||
242 | }) | ||
196 | } | 243 | } |
197 | 244 | ||
198 | private async loadPlugins () { | 245 | 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 { | |||
21 | 21 | ||
22 | private static CONFIG_LOCAL_STORAGE_KEY = 'server-config' | 22 | private static CONFIG_LOCAL_STORAGE_KEY = 'server-config' |
23 | 23 | ||
24 | configReloaded = new Subject<void>() | 24 | configReloaded = new Subject<ServerConfig>() |
25 | 25 | ||
26 | private localeObservable: Observable<any> | 26 | private localeObservable: Observable<any> |
27 | private videoLicensesObservable: Observable<VideoConstant<number>[]> | 27 | private videoLicensesObservable: Observable<VideoConstant<number>[]> |
@@ -139,6 +139,12 @@ export class ServerService { | |||
139 | indexUrl: 'https://instances.joinpeertube.org' | 139 | indexUrl: 'https://instances.joinpeertube.org' |
140 | } | 140 | } |
141 | } | 141 | } |
142 | }, | ||
143 | broadcastMessage: { | ||
144 | enabled: false, | ||
145 | message: '', | ||
146 | level: 'info', | ||
147 | dismissable: false | ||
142 | } | 148 | } |
143 | } | 149 | } |
144 | 150 | ||
@@ -162,6 +168,11 @@ export class ServerService { | |||
162 | resetConfig () { | 168 | resetConfig () { |
163 | this.configLoaded = false | 169 | this.configLoaded = false |
164 | this.configReset = true | 170 | this.configReset = true |
171 | |||
172 | // Notify config update | ||
173 | this.getConfig().subscribe(() => { | ||
174 | // empty, to fire a reset config event | ||
175 | }) | ||
165 | } | 176 | } |
166 | 177 | ||
167 | getConfig () { | 178 | getConfig () { |
@@ -175,9 +186,9 @@ export class ServerService { | |||
175 | this.config = config | 186 | this.config = config |
176 | this.configLoaded = true | 187 | this.configLoaded = true |
177 | }), | 188 | }), |
178 | tap(() => { | 189 | tap(config => { |
179 | if (this.configReset) { | 190 | if (this.configReset) { |
180 | this.configReloaded.next() | 191 | this.configReloaded.next(config) |
181 | this.configReset = false | 192 | this.configReset = false |
182 | } | 193 | } |
183 | }), | 194 | }), |