aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html55
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss6
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts6
-rw-r--r--client/src/app/app.component.html10
-rw-r--r--client/src/app/app.component.scss38
-rw-r--r--client/src/app/app.component.ts71
-rw-r--r--client/src/app/app.module.ts6
-rw-r--r--client/src/app/core/server/server.service.ts17
-rw-r--r--client/src/app/shared/bulk/bulk.service.ts24
-rw-r--r--client/src/app/shared/locale/oc.ts104
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.ts38
-rw-r--r--client/src/app/shared/shared.module.ts145
12 files changed, 426 insertions, 94 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
4import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core' 4import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core'
5import { is18nPath } from '../../../shared/models/i18n' 5import { is18nPath } from '../../../shared/models/i18n'
6import { ScreenService } from '@app/shared/misc/screen.service' 6import { ScreenService } from '@app/shared/misc/screen.service'
7import { filter, map, pairwise } from 'rxjs/operators' 7import { filter, map, pairwise, first } from 'rxjs/operators'
8import { Hotkey, HotkeysService } from 'angular2-hotkeys' 8import { Hotkey, HotkeysService } from 'angular2-hotkeys'
9import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
10import { PlatformLocation, ViewportScroller } from '@angular/common' 10import { PlatformLocation, ViewportScroller } from '@angular/common'
@@ -19,6 +19,10 @@ import { ServerConfig, UserRole } from '@shared/models'
19import { User } from '@app/shared' 19import { User } from '@app/shared'
20import { InstanceService } from '@app/shared/instance/instance.service' 20import { InstanceService } from '@app/shared/instance/instance.service'
21import { MenuService } from './core/menu/menu.service' 21import { MenuService } from './core/menu/menu.service'
22import { BroadcastMessageLevel } from '@shared/models/server'
23import { MarkdownService } from './shared/renderer'
24import { concat } from 'rxjs'
25import { 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})
28export class AppComponent implements OnInit, AfterViewInit { 32export 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/app.module.ts b/client/src/app/app.module.ts
index 6e74cd394..e61346dac 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -2,7 +2,6 @@ import { LOCALE_ID, NgModule, TRANSLATIONS, TRANSLATIONS_FORMAT } from '@angular
2import { BrowserModule } from '@angular/platform-browser' 2import { BrowserModule } from '@angular/platform-browser'
3import { ServerService } from '@app/core' 3import { ServerService } from '@app/core'
4import { ResetPasswordModule } from '@app/reset-password' 4import { ResetPasswordModule } from '@app/reset-password'
5
6import { MetaLoader, MetaModule, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core' 5import { MetaLoader, MetaModule, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core'
7import 'focus-visible' 6import 'focus-visible'
8 7
@@ -18,9 +17,12 @@ import { SearchModule } from '@app/search'
18import { WelcomeModalComponent } from '@app/modal/welcome-modal.component' 17import { WelcomeModalComponent } from '@app/modal/welcome-modal.component'
19import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component' 18import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
20import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '@shared/models' 19import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '@shared/models'
21import { APP_BASE_HREF } from '@angular/common' 20import { APP_BASE_HREF, registerLocaleData } from '@angular/common'
22import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component' 21import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
23import { CustomModalComponent } from '@app/modal/custom-modal.component' 22import { CustomModalComponent } from '@app/modal/custom-modal.component'
23import localeOc from '@app/shared/locale/oc'
24
25registerLocaleData(localeOc, 'oc')
24 26
25@NgModule({ 27@NgModule({
26 bootstrap: [ AppComponent ], 28 bootstrap: [ AppComponent ],
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 }),
diff --git a/client/src/app/shared/bulk/bulk.service.ts b/client/src/app/shared/bulk/bulk.service.ts
new file mode 100644
index 000000000..b00db31ec
--- /dev/null
+++ b/client/src/app/shared/bulk/bulk.service.ts
@@ -0,0 +1,24 @@
1import { HttpClient } from '@angular/common/http'
2import { Injectable } from '@angular/core'
3import { environment } from '../../../environments/environment'
4import { RestExtractor, RestService } from '../rest'
5import { BulkRemoveCommentsOfBody } from '../../../../../shared'
6import { catchError } from 'rxjs/operators'
7
8@Injectable()
9export class BulkService {
10 static BASE_BULK_URL = environment.apiUrl + '/api/v1/bulk'
11
12 constructor (
13 private authHttp: HttpClient,
14 private restExtractor: RestExtractor,
15 private restService: RestService
16 ) { }
17
18 removeCommentsOf (body: BulkRemoveCommentsOfBody) {
19 const url = BulkService.BASE_BULK_URL + '/remove-comments-of'
20
21 return this.authHttp.post(url, body)
22 .pipe(catchError(err => this.restExtractor.handleError(err)))
23 }
24}
diff --git a/client/src/app/shared/locale/oc.ts b/client/src/app/shared/locale/oc.ts
new file mode 100644
index 000000000..d3b2e8407
--- /dev/null
+++ b/client/src/app/shared/locale/oc.ts
@@ -0,0 +1,104 @@
1
2// This code is not generated
3// See angular/tools/gulp-tasks/cldr/extract.js
4
5const u: any = undefined
6
7function plural (n: number): number {
8 const i = Math.floor(Math.abs(n))
9 if (i === 0 || i === 1) return 1
10 return 5
11}
12
13export default [
14 'oc',
15 [['a. m.', 'p. m.'], u, u],
16 u,
17 [
18 ['dg', 'dl', 'dm', 'dc', 'dj', 'dv', 'ds'], ['dg.', 'dl.', 'dm.', 'dc.', 'dj.', 'dv.', 'ds.'],
19 ['dimenge', 'diluns', 'dimars', 'dimècres', 'dijòus', 'divendres', 'dissabte'],
20 ['dg.', 'dl.', 'dm.', 'dc.', 'dj.', 'dv.', 'ds.']
21 ],
22 u,
23 [
24 ['GN', 'FB', 'MÇ', 'AB', 'MA', 'JN', 'JL', 'AG', 'ST', 'OC', 'NV', 'DC'],
25 [
26 'de gen.', 'de febr.', 'de març', 'd’abr.', 'de mai', 'de junh', 'de jul.', 'd’ag.',
27 'de set.', 'd’oct.', 'de nov.', 'de dec.'
28 ],
29 [
30 'de genièr', 'de febrièr', 'de març', 'd’abril', 'de mai', 'de junh', 'de julhet',
31 'd’agòst', 'de setembre', 'd’octòbre', 'de novembre', 'de decembre'
32 ]
33 ],
34 [
35 ['GN', 'FB', 'MÇ', 'AB', 'MA', 'JN', 'JL', 'AG', 'ST', 'OC', 'NV', 'DC'],
36 [
37 'gen.', 'febr.', 'març', 'abr.', 'mai', 'junh', 'jul.', 'ag.', 'set.', 'oct.', 'nov.',
38 'dec.'
39 ],
40 [
41 'genièr', 'febrièr', 'març', 'abril', 'mai', 'junh', 'julhet', 'agòst', 'setembre', 'octòbre',
42 'novembre', 'decembre'
43 ]
44 ],
45 [['aC', 'dC'], u, ['abans Jèsus-Crist', 'aprèp Jèsus-Crist']],
46 1,
47 [6, 0],
48 ['d/M/yy', 'd MMM y', 'd MMMM \'de\' y', 'EEEE, d MMMM \'de\' y'],
49 ['H:mm', 'H:mm:ss', 'H:mm:ss z', 'H:mm:ss zzzz'],
50 ['{1} {0}', '{1}, {0}', '{1} \'a\' \'les\' {0}', u],
51 [',', '.', ';', '%', '+', '-', 'E', '×', '‰', '∞', 'NaN', ':'],
52 ['#,##0.###', '#,##0%', '#,##0.00 ¤', '#E0'],
53 'EUR',
54 '€',
55 'euro',
56 {
57 'ARS': ['$AR', '$'],
58 'AUD': ['$AU', '$'],
59 'BEF': ['FB'],
60 'BMD': ['$BM', '$'],
61 'BND': ['$BN', '$'],
62 'BZD': ['$BZ', '$'],
63 'CAD': ['$CA', '$'],
64 'CLP': ['$CL', '$'],
65 'CNY': [u, 'Â¥'],
66 'COP': ['$CO', '$'],
67 'CYP': ['£CY'],
68 'EGP': [u, '£E'],
69 'FJD': ['$FJ', '$'],
70 'FKP': ['£FK', '£'],
71 'FRF': ['F'],
72 'GBP': ['£GB', '£'],
73 'GIP': ['£GI', '£'],
74 'HKD': [u, '$'],
75 'IEP': ['£IE'],
76 'ILP': ['£IL'],
77 'ITL': ['₤IT'],
78 'JPY': [u, 'Â¥'],
79 'KMF': [u, 'FC'],
80 'LBP': ['£LB', '£L'],
81 'MTP': ['£MT'],
82 'MXN': ['$MX', '$'],
83 'NAD': ['$NA', '$'],
84 'NIO': [u, '$C'],
85 'NZD': ['$NZ', '$'],
86 'RHD': ['$RH'],
87 'RON': [u, 'L'],
88 'RWF': [u, 'FR'],
89 'SBD': ['$SB', '$'],
90 'SGD': ['$SG', '$'],
91 'SRD': ['$SR', '$'],
92 'TOP': [u, '$T'],
93 'TTD': ['$TT', '$'],
94 'TWD': [u, 'NT$'],
95 'USD': ['$US', '$'],
96 'UYU': ['$UY', '$'],
97 'WST': ['$WS'],
98 'XCD': [u, '$'],
99 'XPF': ['FCFP'],
100 'ZMW': [u, 'Kw']
101 },
102 'ltr',
103 plural
104]
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
index f8ad7ce13..82f39050e 100644
--- a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
+++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
@@ -7,7 +7,8 @@ import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core'
7import { User, UserRight } from '../../../../../shared/models/users' 7import { User, UserRight } from '../../../../../shared/models/users'
8import { Account } from '@app/shared/account/account.model' 8import { Account } from '@app/shared/account/account.model'
9import { BlocklistService } from '@app/shared/blocklist' 9import { BlocklistService } from '@app/shared/blocklist'
10import { ServerConfig } from '@shared/models' 10import { ServerConfig, BulkRemoveCommentsOfBody } from '@shared/models'
11import { BulkService } from '../bulk/bulk.service'
11 12
12@Component({ 13@Component({
13 selector: 'my-user-moderation-dropdown', 14 selector: 'my-user-moderation-dropdown',
@@ -38,6 +39,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
38 private serverService: ServerService, 39 private serverService: ServerService,
39 private userService: UserService, 40 private userService: UserService,
40 private blocklistService: BlocklistService, 41 private blocklistService: BlocklistService,
42 private bulkService: BulkService,
41 private i18n: I18n 43 private i18n: I18n
42 ) { } 44 ) { }
43 45
@@ -229,6 +231,21 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
229 ) 231 )
230 } 232 }
231 233
234 async bulkRemoveCommentsOf (body: BulkRemoveCommentsOfBody) {
235 const message = this.i18n('Are you sure you want to remove all the comments of this account?')
236 const res = await this.confirmService.confirm(message, this.i18n('Delete account comments'))
237 if (res === false) return
238
239 this.bulkService.removeCommentsOf(body)
240 .subscribe(
241 () => {
242 this.notifier.success(this.i18n('Will remove comments of this account (may take several minutes).'))
243 },
244
245 err => this.notifier.error(err.message)
246 )
247 }
248
232 getRouterUserEditLink (user: User) { 249 getRouterUserEditLink (user: User) {
233 return [ '/admin', 'users', 'update', user.id ] 250 return [ '/admin', 'users', 'update', user.id ]
234 } 251 }
@@ -300,12 +317,17 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
300 description: this.i18n('Show back content from that instance for you.'), 317 description: this.i18n('Show back content from that instance for you.'),
301 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true, 318 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true,
302 handler: ({ account }) => this.unblockServerByUser(account.host) 319 handler: ({ account }) => this.unblockServerByUser(account.host)
320 },
321 {
322 label: this.i18n('Remove comments from your videos'),
323 description: this.i18n('Remove comments of this account from your videos.'),
324 handler: ({ account }) => this.bulkRemoveCommentsOf({ accountName: account.nameWithHost, scope: 'my-videos' })
303 } 325 }
304 ]) 326 ])
305 327
306 let instanceActions: DropdownAction<{ user: User, account: Account }>[] = [] 328 let instanceActions: DropdownAction<{ user: User, account: Account }>[] = []
307 329
308 // Instance actions 330 // Instance actions on account blocklists
309 if (authUser.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)) { 331 if (authUser.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)) {
310 instanceActions = instanceActions.concat([ 332 instanceActions = instanceActions.concat([
311 { 333 {
@@ -323,7 +345,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
323 ]) 345 ])
324 } 346 }
325 347
326 // Instance actions 348 // Instance actions on server blocklists
327 if (authUser.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) { 349 if (authUser.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) {
328 instanceActions = instanceActions.concat([ 350 instanceActions = instanceActions.concat([
329 { 351 {
@@ -341,6 +363,16 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
341 ]) 363 ])
342 } 364 }
343 365
366 if (authUser.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT)) {
367 instanceActions = instanceActions.concat([
368 {
369 label: this.i18n('Remove comments from your instance'),
370 description: this.i18n('Remove comments of this account from your instance.'),
371 handler: ({ account }) => this.bulkRemoveCommentsOf({ accountName: account.nameWithHost, scope: 'instance' })
372 }
373 ])
374 }
375
344 if (instanceActions.length !== 0) { 376 if (instanceActions.length !== 0) {
345 this.userActions.push(instanceActions) 377 this.userActions.push(instanceActions)
346 } 378 }
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index 01735c187..813f76672 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -1,32 +1,30 @@
1import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
2import { SharedModule as PrimeSharedModule } from 'primeng/api'
3import { InputMaskModule } from 'primeng/inputmask'
4import { InputSwitchModule } from 'primeng/inputswitch'
5import { MultiSelectModule } from 'primeng/multiselect'
6import { ClipboardModule } from '@angular/cdk/clipboard'
1import { CommonModule } from '@angular/common' 7import { CommonModule } from '@angular/common'
2import { HttpClientModule } from '@angular/common/http' 8import { HttpClientModule } from '@angular/common/http'
3import { NgModule } from '@angular/core' 9import { NgModule } from '@angular/core'
4import { FormsModule, ReactiveFormsModule } from '@angular/forms' 10import { FormsModule, ReactiveFormsModule } from '@angular/forms'
5import { RouterModule } from '@angular/router' 11import { RouterModule } from '@angular/router'
6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' 12import { BatchDomainsValidatorsService } from '@app/+admin/config/shared/batch-domains-validators.service'
7import { HelpComponent } from '@app/shared/misc/help.component' 13import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface'
8import { ListOverflowComponent } from '@app/shared/misc/list-overflow.component' 14import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings'
9import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' 15import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component'
10import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
11import { SharedModule as PrimeSharedModule } from 'primeng/api'
12import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
13import { ButtonComponent } from './buttons/button.component'
14import { DeleteButtonComponent } from './buttons/delete-button.component'
15import { EditButtonComponent } from './buttons/edit-button.component'
16import { LoaderComponent } from './misc/loader.component'
17import { RestExtractor, RestService } from './rest'
18import { UserService } from './users'
19import { VideoAbuseService } from './video-abuse'
20import { VideoBlacklistService } from './video-blacklist'
21import { VideoOwnershipService } from './video-ownership'
22import { VideoMiniatureComponent } from './video/video-miniature.component'
23import { FeedComponent } from './video/feed.component'
24import { VideoThumbnailComponent } from './video/video-thumbnail.component'
25import { VideoService } from './video/video.service'
26import { AccountService } from '@app/shared/account/account.service' 16import { AccountService } from '@app/shared/account/account.service'
27import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' 17import { FromNowPipe } from '@app/shared/angular/from-now.pipe'
28import { I18n } from '@ngx-translate/i18n-polyfill' 18import { HighlightPipe } from '@app/shared/angular/highlight.pipe'
29import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 19import { NumberFormatterPipe } from '@app/shared/angular/number-formatter.pipe'
20import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe'
21import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
22import { VideoDurationPipe } from '@app/shared/angular/video-duration-formatter.pipe'
23import { BlocklistService } from '@app/shared/blocklist'
24import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component'
25import { AvatarComponent } from '@app/shared/channel/avatar.component'
26import { ConfirmComponent } from '@app/shared/confirm/confirm.component'
27import { DateToggleComponent } from '@app/shared/date/date-toggle.component'
30import { 28import {
31 CustomConfigValidatorsService, 29 CustomConfigValidatorsService,
32 InstanceValidatorsService, 30 InstanceValidatorsService,
@@ -44,70 +42,72 @@ import {
44 VideoPlaylistValidatorsService, 42 VideoPlaylistValidatorsService,
45 VideoValidatorsService 43 VideoValidatorsService
46} from '@app/shared/forms' 44} from '@app/shared/forms'
47import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' 45import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
48import { InputMaskModule } from 'primeng/inputmask'
49import { ScreenService } from '@app/shared/misc/screen.service'
50import { LocalStorageService, SessionStorageService } from '@app/shared/misc/storage.service'
51import { VideoCaptionsValidatorsService } from '@app/shared/forms/form-validators/video-captions-validators.service' 46import { VideoCaptionsValidatorsService } from '@app/shared/forms/form-validators/video-captions-validators.service'
52import { VideoCaptionService } from '@app/shared/video-caption' 47import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component'
48import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
53import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.component' 49import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.component'
54import { VideoImportService } from '@app/shared/video-import/video-import.service' 50import { TimestampInputComponent } from '@app/shared/forms/timestamp-input.component'
55import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component' 51import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
56import { 52import { GlobalIconComponent } from '@app/shared/images/global-icon.component'
57 NgbCollapseModule, 53import { PreviewUploadComponent } from '@app/shared/images/preview-upload.component'
58 NgbDropdownModule, 54import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component'
59 NgbModalModule, 55import { FollowService } from '@app/shared/instance/follow.service'
60 NgbPopoverModule,
61 NgbNavModule,
62 NgbTooltipModule
63} from '@ng-bootstrap/ng-bootstrap'
64import { RemoteSubscribeComponent, SubscribeButtonComponent, UserSubscriptionService } from '@app/shared/user-subscription'
65import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component' 56import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component'
66import { InstanceStatisticsComponent } from '@app/shared/instance/instance-statistics.component' 57import { InstanceStatisticsComponent } from '@app/shared/instance/instance-statistics.component'
67import { OverviewService } from '@app/shared/overview' 58import { InstanceService } from '@app/shared/instance/instance.service'
59import { TopMenuDropdownComponent } from '@app/shared/menu/top-menu-dropdown.component'
60import { HelpComponent } from '@app/shared/misc/help.component'
61import { ListOverflowComponent } from '@app/shared/misc/list-overflow.component'
62import { ScreenService } from '@app/shared/misc/screen.service'
63import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
64import { LocalStorageService, SessionStorageService } from '@app/shared/misc/storage.service'
68import { UserBanModalComponent } from '@app/shared/moderation' 65import { UserBanModalComponent } from '@app/shared/moderation'
69import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component' 66import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component'
70import { BlocklistService } from '@app/shared/blocklist' 67import { OverviewService } from '@app/shared/overview'
71import { AvatarComponent } from '@app/shared/channel/avatar.component' 68import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer'
72import { TopMenuDropdownComponent } from '@app/shared/menu/top-menu-dropdown.component' 69import { RemoteSubscribeComponent, SubscribeButtonComponent, UserSubscriptionService } from '@app/shared/user-subscription'
73import { UserHistoryService } from '@app/shared/users/user-history.service' 70import { UserHistoryService } from '@app/shared/users/user-history.service'
74import { UserNotificationService } from '@app/shared/users/user-notification.service' 71import { UserNotificationService } from '@app/shared/users/user-notification.service'
75import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component' 72import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component'
76import { InstanceService } from '@app/shared/instance/instance.service' 73import { VideoCaptionService } from '@app/shared/video-caption'
77import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer' 74import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
78import { ConfirmComponent } from '@app/shared/confirm/confirm.component' 75import { VideoImportService } from '@app/shared/video-import/video-import.service'
79import { DateToggleComponent } from '@app/shared/date/date-toggle.component'
80import { SmallLoaderComponent } from '@app/shared/misc/small-loader.component'
81import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
82import { PreviewUploadComponent } from '@app/shared/images/preview-upload.component'
83import { GlobalIconComponent } from '@app/shared/images/global-icon.component'
84import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component'
85import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component' 76import { VideoAddToPlaylistComponent } from '@app/shared/video-playlist/video-add-to-playlist.component'
86import { TimestampInputComponent } from '@app/shared/forms/timestamp-input.component'
87import { VideoPlaylistElementMiniatureComponent } from '@app/shared/video-playlist/video-playlist-element-miniature.component' 77import { VideoPlaylistElementMiniatureComponent } from '@app/shared/video-playlist/video-playlist-element-miniature.component'
88import { VideosSelectionComponent } from '@app/shared/video/videos-selection.component' 78import { VideoPlaylistMiniatureComponent } from '@app/shared/video-playlist/video-playlist-miniature.component'
89import { NumberFormatterPipe } from '@app/shared/angular/number-formatter.pipe' 79import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
90import { VideoDurationPipe } from '@app/shared/angular/video-duration-formatter.pipe' 80import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
91import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe'
92import { FromNowPipe } from '@app/shared/angular/from-now.pipe'
93import { HighlightPipe } from '@app/shared/angular/highlight.pipe'
94import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
95import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component'
96import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component' 81import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component'
97import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component' 82import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.component'
98import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' 83import { VideoReportComponent } from '@app/shared/video/modals/video-report.component'
99import { FollowService } from '@app/shared/instance/follow.service'
100import { MultiSelectModule } from 'primeng/multiselect'
101import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component'
102import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component'
103import { RedundancyService } from '@app/shared/video/redundancy.service' 84import { RedundancyService } from '@app/shared/video/redundancy.service'
104import { ClipboardModule } from '@angular/cdk/clipboard' 85import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component'
105import { InputSwitchModule } from 'primeng/inputswitch' 86import { VideosSelectionComponent } from '@app/shared/video/videos-selection.component'
106 87import {
107import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings' 88 NgbCollapseModule,
108import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface' 89 NgbDropdownModule,
109import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component' 90 NgbModalModule,
110import { BatchDomainsValidatorsService } from '@app/+admin/config/shared/batch-domains-validators.service' 91 NgbNavModule,
92 NgbPopoverModule,
93 NgbTooltipModule
94} from '@ng-bootstrap/ng-bootstrap'
95import { I18n } from '@ngx-translate/i18n-polyfill'
96import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
97import { BulkService } from './bulk/bulk.service'
98import { ButtonComponent } from './buttons/button.component'
99import { DeleteButtonComponent } from './buttons/delete-button.component'
100import { EditButtonComponent } from './buttons/edit-button.component'
101import { LoaderComponent } from './misc/loader.component'
102import { RestExtractor, RestService } from './rest'
103import { UserService } from './users'
104import { VideoAbuseService } from './video-abuse'
105import { VideoBlacklistService } from './video-blacklist'
106import { VideoOwnershipService } from './video-ownership'
107import { FeedComponent } from './video/feed.component'
108import { VideoMiniatureComponent } from './video/video-miniature.component'
109import { VideoThumbnailComponent } from './video/video-thumbnail.component'
110import { VideoService } from './video/video.service'
111 111
112@NgModule({ 112@NgModule({
113 imports: [ 113 imports: [
@@ -313,6 +313,7 @@ import { BatchDomainsValidatorsService } from '@app/+admin/config/shared/batch-d
313 BlocklistService, 313 BlocklistService,
314 UserHistoryService, 314 UserHistoryService,
315 InstanceService, 315 InstanceService,
316 BulkService,
316 317
317 MarkdownService, 318 MarkdownService,
318 LinkifierService, 319 LinkifierService,