diff options
Diffstat (limited to 'client/src/app/+admin')
22 files changed, 313 insertions, 169 deletions
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index fdbe70314..16273f6d8 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts | |||
@@ -5,7 +5,7 @@ import { TableModule } from 'primeng/table' | |||
5 | import { SharedModule } from '../shared' | 5 | import { SharedModule } from '../shared' |
6 | import { AdminRoutingModule } from './admin-routing.module' | 6 | import { AdminRoutingModule } from './admin-routing.module' |
7 | import { AdminComponent } from './admin.component' | 7 | import { AdminComponent } from './admin.component' |
8 | import { FollowersListComponent, FollowingAddComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows' | 8 | import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows' |
9 | import { FollowingListComponent } from './follows/following-list/following-list.component' | 9 | import { FollowingListComponent } from './follows/following-list/following-list.component' |
10 | import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' | 10 | import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' |
11 | import { | 11 | import { |
@@ -28,6 +28,7 @@ import { SelectButtonModule } from 'primeng/selectbutton' | |||
28 | import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' | 28 | import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' |
29 | import { VideoRedundancyInformationComponent } from '@app/+admin/follows/video-redundancies-list/video-redundancy-information.component' | 29 | import { VideoRedundancyInformationComponent } from '@app/+admin/follows/video-redundancies-list/video-redundancy-information.component' |
30 | import { ChartModule } from 'primeng/chart' | 30 | import { ChartModule } from 'primeng/chart' |
31 | import { BatchDomainsModalComponent } from './config/shared/batch-domains-modal.component' | ||
31 | 32 | ||
32 | @NgModule({ | 33 | @NgModule({ |
33 | imports: [ | 34 | imports: [ |
@@ -44,7 +45,6 @@ import { ChartModule } from 'primeng/chart' | |||
44 | AdminComponent, | 45 | AdminComponent, |
45 | 46 | ||
46 | FollowsComponent, | 47 | FollowsComponent, |
47 | FollowingAddComponent, | ||
48 | FollowersListComponent, | 48 | FollowersListComponent, |
49 | FollowingListComponent, | 49 | FollowingListComponent, |
50 | RedundancyCheckboxComponent, | 50 | RedundancyCheckboxComponent, |
@@ -76,7 +76,9 @@ import { ChartModule } from 'primeng/chart' | |||
76 | DebugComponent, | 76 | DebugComponent, |
77 | 77 | ||
78 | ConfigComponent, | 78 | ConfigComponent, |
79 | EditCustomConfigComponent | 79 | EditCustomConfigComponent, |
80 | |||
81 | BatchDomainsModalComponent | ||
80 | ], | 82 | ], |
81 | 83 | ||
82 | exports: [ | 84 | exports: [ |
diff --git a/client/src/app/+admin/config/shared/batch-domains-modal.component.html b/client/src/app/+admin/config/shared/batch-domains-modal.component.html new file mode 100644 index 000000000..1b85c8f48 --- /dev/null +++ b/client/src/app/+admin/config/shared/batch-domains-modal.component.html | |||
@@ -0,0 +1,43 @@ | |||
1 | <ng-template #modal> | ||
2 | <div class="modal-header"> | ||
3 | <h4 i18n class="modal-title">{{ action }}</h4> | ||
4 | |||
5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | ||
6 | </div> | ||
7 | |||
8 | <div class="modal-body"> | ||
9 | <form novalidate [formGroup]="form" (ngSubmit)="submit()"> | ||
10 | <div class="form-group"> | ||
11 | <label i18n for="hosts">1 host (without "http://") per line</label> | ||
12 | |||
13 | <textarea | ||
14 | [placeholder]="placeholder" formControlName="domains" type="text" id="hosts" name="hosts" | ||
15 | class="form-control" [ngClass]="{ 'input-error': formErrors['domains'] }" ngbAutofocus | ||
16 | ></textarea> | ||
17 | |||
18 | <div *ngIf="formErrors.domains" class="form-error"> | ||
19 | {{ formErrors.domains }} | ||
20 | |||
21 | <div *ngIf="form.controls['domains'].errors.validDomains"> | ||
22 | {{ form.controls['domains'].errors.validDomains.value }} | ||
23 | </div> | ||
24 | </div> | ||
25 | </div> | ||
26 | |||
27 | <ng-content select="warning"></ng-content> | ||
28 | |||
29 | <div class="form-group inputs"> | ||
30 | <input | ||
31 | type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" | ||
32 | (click)="hide()" (key.enter)="hide()" | ||
33 | > | ||
34 | |||
35 | <input | ||
36 | type="submit" [value]="action" class="action-button-submit" | ||
37 | [disabled]="!form.valid" | ||
38 | > | ||
39 | </div> | ||
40 | </form> | ||
41 | </div> | ||
42 | |||
43 | </ng-template> | ||
diff --git a/client/src/app/+admin/config/shared/batch-domains-modal.component.scss b/client/src/app/+admin/config/shared/batch-domains-modal.component.scss new file mode 100644 index 000000000..9621a566f --- /dev/null +++ b/client/src/app/+admin/config/shared/batch-domains-modal.component.scss | |||
@@ -0,0 +1,3 @@ | |||
1 | textarea { | ||
2 | height: 200px; | ||
3 | } | ||
diff --git a/client/src/app/+admin/config/shared/batch-domains-modal.component.ts b/client/src/app/+admin/config/shared/batch-domains-modal.component.ts new file mode 100644 index 000000000..620f2726b --- /dev/null +++ b/client/src/app/+admin/config/shared/batch-domains-modal.component.ts | |||
@@ -0,0 +1,54 @@ | |||
1 | import { Component, OnInit, ViewChild, Input, Output, EventEmitter } from '@angular/core' | ||
2 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
4 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | ||
5 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
6 | import { FormReactive } from '@app/shared/forms' | ||
7 | import { BatchDomainsValidatorsService } from './batch-domains-validators.service' | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-batch-domains-modal', | ||
11 | templateUrl: './batch-domains-modal.component.html', | ||
12 | styleUrls: [ './batch-domains-modal.component.scss' ] | ||
13 | }) | ||
14 | export class BatchDomainsModalComponent extends FormReactive implements OnInit { | ||
15 | @ViewChild('modal', { static: true }) modal: NgbModal | ||
16 | @Input() placeholder = 'example.com' | ||
17 | @Input() action: string | ||
18 | @Output() domains = new EventEmitter<string[]>() | ||
19 | |||
20 | private openedModal: NgbModalRef | ||
21 | |||
22 | constructor ( | ||
23 | protected formValidatorService: FormValidatorService, | ||
24 | private modalService: NgbModal, | ||
25 | private batchDomainsValidatorsService: BatchDomainsValidatorsService, | ||
26 | private i18n: I18n | ||
27 | ) { | ||
28 | super() | ||
29 | } | ||
30 | |||
31 | ngOnInit () { | ||
32 | if (!this.action) this.action = this.i18n('Process domains') | ||
33 | |||
34 | this.buildForm({ | ||
35 | domains: this.batchDomainsValidatorsService.DOMAINS | ||
36 | }) | ||
37 | } | ||
38 | |||
39 | openModal () { | ||
40 | this.openedModal = this.modalService.open(this.modal, { centered: true }) | ||
41 | } | ||
42 | |||
43 | hide () { | ||
44 | this.openedModal.close() | ||
45 | } | ||
46 | |||
47 | submit () { | ||
48 | this.domains.emit( | ||
49 | this.batchDomainsValidatorsService.getNotEmptyHosts(this.form.controls['domains'].value) | ||
50 | ) | ||
51 | this.form.reset() | ||
52 | this.hide() | ||
53 | } | ||
54 | } | ||
diff --git a/client/src/app/+admin/config/shared/batch-domains-validators.service.ts b/client/src/app/+admin/config/shared/batch-domains-validators.service.ts new file mode 100644 index 000000000..154ef3a23 --- /dev/null +++ b/client/src/app/+admin/config/shared/batch-domains-validators.service.ts | |||
@@ -0,0 +1,68 @@ | |||
1 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
2 | import { Validators, ValidatorFn } from '@angular/forms' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { BuildFormValidator, validateHost } from '@app/shared' | ||
5 | |||
6 | @Injectable() | ||
7 | export class BatchDomainsValidatorsService { | ||
8 | readonly DOMAINS: BuildFormValidator | ||
9 | |||
10 | constructor (private i18n: I18n) { | ||
11 | this.DOMAINS = { | ||
12 | VALIDATORS: [ Validators.required, this.validDomains, this.isHostsUnique ], | ||
13 | MESSAGES: { | ||
14 | 'required': this.i18n('Domain is required.'), | ||
15 | 'validDomains': this.i18n('Domains entered are invalid.'), | ||
16 | 'uniqueDomains': this.i18n('Domains entered contain duplicates.') | ||
17 | } | ||
18 | } | ||
19 | } | ||
20 | |||
21 | getNotEmptyHosts (hosts: string) { | ||
22 | return hosts | ||
23 | .split('\n') | ||
24 | .filter((host: string) => host && host.length !== 0) // Eject empty hosts | ||
25 | } | ||
26 | |||
27 | private validDomains: ValidatorFn = (control) => { | ||
28 | if (!control.value) return null | ||
29 | |||
30 | const newHostsErrors = [] | ||
31 | const hosts = this.getNotEmptyHosts(control.value) | ||
32 | |||
33 | for (const host of hosts) { | ||
34 | if (validateHost(host) === false) { | ||
35 | newHostsErrors.push(this.i18n('{{host}} is not valid', { host })) | ||
36 | } | ||
37 | } | ||
38 | |||
39 | /* Is not valid. */ | ||
40 | if (newHostsErrors.length !== 0) { | ||
41 | return { | ||
42 | 'validDomains': { | ||
43 | reason: 'invalid', | ||
44 | value: newHostsErrors.join('. ') + '.' | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | |||
49 | /* Is valid. */ | ||
50 | return null | ||
51 | } | ||
52 | |||
53 | private isHostsUnique: ValidatorFn = (control) => { | ||
54 | if (!control.value) return null | ||
55 | |||
56 | const hosts = this.getNotEmptyHosts(control.value) | ||
57 | |||
58 | if (hosts.every((host: string) => hosts.indexOf(host) === hosts.lastIndexOf(host))) { | ||
59 | return null | ||
60 | } else { | ||
61 | return { | ||
62 | 'uniqueDomains': { | ||
63 | reason: 'invalid' | ||
64 | } | ||
65 | } | ||
66 | } | ||
67 | } | ||
68 | } | ||
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.ts b/client/src/app/+admin/follows/followers-list/followers-list.component.ts index aff59a691..585902827 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.ts +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.ts | |||
@@ -15,7 +15,7 @@ export class FollowersListComponent extends RestTable implements OnInit { | |||
15 | followers: ActorFollow[] = [] | 15 | followers: ActorFollow[] = [] |
16 | totalRecords = 0 | 16 | totalRecords = 0 |
17 | rowsPerPage = 10 | 17 | rowsPerPage = 10 |
18 | sort: SortMeta = { field: 'createdAt', order: 1 } | 18 | sort: SortMeta = { field: 'createdAt', order: -1 } |
19 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 19 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
20 | 20 | ||
21 | constructor ( | 21 | constructor ( |
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.html b/client/src/app/+admin/follows/following-add/following-add.component.html deleted file mode 100644 index e08decb3f..000000000 --- a/client/src/app/+admin/follows/following-add/following-add.component.html +++ /dev/null | |||
@@ -1,22 +0,0 @@ | |||
1 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | ||
2 | |||
3 | <form (ngSubmit)="addFollowing()"> | ||
4 | <div class="form-group"> | ||
5 | <label i18n for="hosts">1 host (without "http://") per line</label> | ||
6 | |||
7 | <textarea | ||
8 | type="text" class="form-control" placeholder="example.com" id="hosts" name="hosts" | ||
9 | [(ngModel)]="hostsString" (ngModelChange)="onHostsChanged()" [ngClass]="{ 'input-error': hostsError }" | ||
10 | ></textarea> | ||
11 | |||
12 | <div *ngIf="hostsError" class="form-error"> | ||
13 | {{ hostsError }} | ||
14 | </div> | ||
15 | </div> | ||
16 | |||
17 | <div i18n *ngIf="httpEnabled() === false" class="alert alert-warning"> | ||
18 | It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers. | ||
19 | </div> | ||
20 | |||
21 | <input type="submit" i18n-value value="Add following" [disabled]="hostsError || !hostsString" class="btn btn-secondary"> | ||
22 | </form> | ||
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.scss b/client/src/app/+admin/follows/following-add/following-add.component.scss deleted file mode 100644 index 7594b502c..000000000 --- a/client/src/app/+admin/follows/following-add/following-add.component.scss +++ /dev/null | |||
@@ -1,11 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | textarea { | ||
5 | height: 250px; | ||
6 | } | ||
7 | |||
8 | input[type=submit] { | ||
9 | @include peertube-button; | ||
10 | @include orange-button; | ||
11 | } | ||
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.ts b/client/src/app/+admin/follows/following-add/following-add.component.ts deleted file mode 100644 index 308bbb0c5..000000000 --- a/client/src/app/+admin/follows/following-add/following-add.component.ts +++ /dev/null | |||
@@ -1,85 +0,0 @@ | |||
1 | import { Component } from '@angular/core' | ||
2 | import { Router } from '@angular/router' | ||
3 | import { Notifier } from '@app/core' | ||
4 | import { ConfirmService } from '../../../core' | ||
5 | import { validateHost } from '../../../shared' | ||
6 | import { FollowService } from '@app/shared/instance/follow.service' | ||
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-following-add', | ||
11 | templateUrl: './following-add.component.html', | ||
12 | styleUrls: [ './following-add.component.scss' ] | ||
13 | }) | ||
14 | export class FollowingAddComponent { | ||
15 | hostsString = '' | ||
16 | hostsError: string = null | ||
17 | error: string = null | ||
18 | |||
19 | constructor ( | ||
20 | private router: Router, | ||
21 | private notifier: Notifier, | ||
22 | private confirmService: ConfirmService, | ||
23 | private followService: FollowService, | ||
24 | private i18n: I18n | ||
25 | ) {} | ||
26 | |||
27 | httpEnabled () { | ||
28 | return window.location.protocol === 'https:' | ||
29 | } | ||
30 | |||
31 | onHostsChanged () { | ||
32 | this.hostsError = null | ||
33 | |||
34 | const newHostsErrors = [] | ||
35 | const hosts = this.getNotEmptyHosts() | ||
36 | |||
37 | for (const host of hosts) { | ||
38 | if (validateHost(host) === false) { | ||
39 | newHostsErrors.push(this.i18n('{{host}} is not valid', { host })) | ||
40 | } | ||
41 | } | ||
42 | |||
43 | if (newHostsErrors.length !== 0) { | ||
44 | this.hostsError = newHostsErrors.join('. ') | ||
45 | } | ||
46 | } | ||
47 | |||
48 | async addFollowing () { | ||
49 | this.error = '' | ||
50 | |||
51 | const hosts = this.getNotEmptyHosts() | ||
52 | if (hosts.length === 0) { | ||
53 | this.error = this.i18n('You need to specify hosts to follow.') | ||
54 | } | ||
55 | |||
56 | if (!this.isHostsUnique(hosts)) { | ||
57 | this.error = this.i18n('Hosts need to be unique.') | ||
58 | return | ||
59 | } | ||
60 | |||
61 | const confirmMessage = this.i18n('If you confirm, you will send a follow request to:<br /> - ') + hosts.join('<br /> - ') | ||
62 | const res = await this.confirmService.confirm(confirmMessage, this.i18n('Follow new server(s)')) | ||
63 | if (res === false) return | ||
64 | |||
65 | this.followService.follow(hosts).subscribe( | ||
66 | () => { | ||
67 | this.notifier.success(this.i18n('Follow request(s) sent!')) | ||
68 | |||
69 | setTimeout(() => this.router.navigate([ '/admin/follows/following-list' ]), 500) | ||
70 | }, | ||
71 | |||
72 | err => this.notifier.error(err.message) | ||
73 | ) | ||
74 | } | ||
75 | |||
76 | private isHostsUnique (hosts: string[]) { | ||
77 | return hosts.every(host => hosts.indexOf(host) === hosts.lastIndexOf(host)) | ||
78 | } | ||
79 | |||
80 | private getNotEmptyHosts () { | ||
81 | return this.hostsString | ||
82 | .split('\n') | ||
83 | .filter(host => host && host.length !== 0) // Eject empty hosts | ||
84 | } | ||
85 | } | ||
diff --git a/client/src/app/+admin/follows/following-add/index.ts b/client/src/app/+admin/follows/following-add/index.ts deleted file mode 100644 index 1b1897ffa..000000000 --- a/client/src/app/+admin/follows/following-add/index.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export * from './following-add.component' | ||
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html index 01aba0c11..cb62d52dd 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.html +++ b/client/src/app/+admin/follows/following-list/following-list.component.html | |||
@@ -4,12 +4,16 @@ | |||
4 | > | 4 | > |
5 | <ng-template pTemplate="caption"> | 5 | <ng-template pTemplate="caption"> |
6 | <div class="caption"> | 6 | <div class="caption"> |
7 | <div> | 7 | <div class="ml-auto"> |
8 | <input | 8 | <input |
9 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | 9 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." |
10 | (keyup)="onSearch($event)" | 10 | (keyup)="onSearch($event)" |
11 | > | 11 | > |
12 | </div> | 12 | </div> |
13 | <a class="ml-2Â follow-button" (click)="addDomainsToFollow()" (key.enter)="addDomainsToFollow()"> | ||
14 | <my-global-icon iconName="add"></my-global-icon> | ||
15 | <ng-container i18n>Follow domain</ng-container> | ||
16 | </a> | ||
13 | </div> | 17 | </div> |
14 | </ng-template> | 18 | </ng-template> |
15 | 19 | ||
@@ -42,3 +46,5 @@ | |||
42 | </tr> | 46 | </tr> |
43 | </ng-template> | 47 | </ng-template> |
44 | </p-table> | 48 | </p-table> |
49 | |||
50 | <my-batch-domains-modal #batchDomainsModal i18n-action action="Follow domains" (domains)="addFollowing($event)"></my-batch-domains-modal> | ||
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.scss b/client/src/app/+admin/follows/following-list/following-list.component.scss index a6f0656b8..f4656b88d 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.scss +++ b/client/src/app/+admin/follows/following-list/following-list.component.scss | |||
@@ -7,4 +7,8 @@ | |||
7 | input { | 7 | input { |
8 | @include peertube-input-text(250px); | 8 | @include peertube-input-text(250px); |
9 | } | 9 | } |
10 | } \ No newline at end of file | 10 | } |
11 | |||
12 | .follow-button { | ||
13 | @include create-button; | ||
14 | } | ||
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.ts b/client/src/app/+admin/follows/following-list/following-list.component.ts index dd7629ead..477a6c0d7 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.ts +++ b/client/src/app/+admin/follows/following-list/following-list.component.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { SortMeta } from 'primeng/api' | 3 | import { SortMeta } from 'primeng/api' |
4 | import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' | 4 | import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' |
@@ -6,6 +6,7 @@ import { ConfirmService } from '../../../core/confirm/confirm.service' | |||
6 | import { RestPagination, RestTable } from '../../../shared' | 6 | import { RestPagination, RestTable } from '../../../shared' |
7 | import { FollowService } from '@app/shared/instance/follow.service' | 7 | import { FollowService } from '@app/shared/instance/follow.service' |
8 | import { I18n } from '@ngx-translate/i18n-polyfill' | 8 | import { I18n } from '@ngx-translate/i18n-polyfill' |
9 | import { BatchDomainsModalComponent } from '@app/+admin/config/shared/batch-domains-modal.component' | ||
9 | 10 | ||
10 | @Component({ | 11 | @Component({ |
11 | selector: 'my-followers-list', | 12 | selector: 'my-followers-list', |
@@ -13,10 +14,12 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
13 | styleUrls: [ './following-list.component.scss' ] | 14 | styleUrls: [ './following-list.component.scss' ] |
14 | }) | 15 | }) |
15 | export class FollowingListComponent extends RestTable implements OnInit { | 16 | export class FollowingListComponent extends RestTable implements OnInit { |
17 | @ViewChild('batchDomainsModal') batchDomainsModal: BatchDomainsModalComponent | ||
18 | |||
16 | following: ActorFollow[] = [] | 19 | following: ActorFollow[] = [] |
17 | totalRecords = 0 | 20 | totalRecords = 0 |
18 | rowsPerPage = 10 | 21 | rowsPerPage = 10 |
19 | sort: SortMeta = { field: 'createdAt', order: 1 } | 22 | sort: SortMeta = { field: 'createdAt', order: -1 } |
20 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 23 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
21 | 24 | ||
22 | constructor ( | 25 | constructor ( |
@@ -36,6 +39,21 @@ export class FollowingListComponent extends RestTable implements OnInit { | |||
36 | return 'FollowingListComponent' | 39 | return 'FollowingListComponent' |
37 | } | 40 | } |
38 | 41 | ||
42 | addDomainsToFollow () { | ||
43 | this.batchDomainsModal.openModal() | ||
44 | } | ||
45 | |||
46 | async addFollowing (hosts: string[]) { | ||
47 | this.followService.follow(hosts).subscribe( | ||
48 | () => { | ||
49 | this.notifier.success(this.i18n('Follow request(s) sent!')) | ||
50 | this.loadData() | ||
51 | }, | ||
52 | |||
53 | err => this.notifier.error(err.message) | ||
54 | ) | ||
55 | } | ||
56 | |||
39 | async removeFollowing (follow: ActorFollow) { | 57 | async removeFollowing (follow: ActorFollow) { |
40 | const res = await this.confirmService.confirm( | 58 | const res = await this.confirmService.confirm( |
41 | this.i18n('Do you really want to unfollow {{host}}?', { host: follow.following.host }), | 59 | this.i18n('Do you really want to unfollow {{host}}?', { host: follow.following.host }), |
diff --git a/client/src/app/+admin/follows/follows.component.html b/client/src/app/+admin/follows/follows.component.html index 46581daf9..7b5bcc2db 100644 --- a/client/src/app/+admin/follows/follows.component.html +++ b/client/src/app/+admin/follows/follows.component.html | |||
@@ -4,8 +4,6 @@ | |||
4 | <div class="admin-sub-nav"> | 4 | <div class="admin-sub-nav"> |
5 | <a i18n routerLink="following-list" routerLinkActive="active">Following</a> | 5 | <a i18n routerLink="following-list" routerLinkActive="active">Following</a> |
6 | 6 | ||
7 | <a i18n routerLink="following-add" routerLinkActive="active">Follow</a> | ||
8 | |||
9 | <a i18n routerLink="followers-list" routerLinkActive="active">Followers</a> | 7 | <a i18n routerLink="followers-list" routerLinkActive="active">Followers</a> |
10 | 8 | ||
11 | <a i18n routerLink="video-redundancies-list" routerLinkActive="active">Video redundancies</a> | 9 | <a i18n routerLink="video-redundancies-list" routerLinkActive="active">Video redundancies</a> |
diff --git a/client/src/app/+admin/follows/follows.routes.ts b/client/src/app/+admin/follows/follows.routes.ts index 298733eb0..8270ae444 100644 --- a/client/src/app/+admin/follows/follows.routes.ts +++ b/client/src/app/+admin/follows/follows.routes.ts | |||
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router' | |||
2 | 2 | ||
3 | import { UserRightGuard } from '../../core' | 3 | import { UserRightGuard } from '../../core' |
4 | import { FollowsComponent } from './follows.component' | 4 | import { FollowsComponent } from './follows.component' |
5 | import { FollowingAddComponent } from './following-add' | ||
6 | import { FollowersListComponent } from './followers-list' | 5 | import { FollowersListComponent } from './followers-list' |
7 | import { UserRight } from '../../../../../shared' | 6 | import { UserRight } from '../../../../../shared' |
8 | import { FollowingListComponent } from './following-list/following-list.component' | 7 | import { FollowingListComponent } from './following-list/following-list.component' |
@@ -42,12 +41,7 @@ export const FollowsRoutes: Routes = [ | |||
42 | }, | 41 | }, |
43 | { | 42 | { |
44 | path: 'following-add', | 43 | path: 'following-add', |
45 | component: FollowingAddComponent, | 44 | redirectTo: 'following-list' |
46 | data: { | ||
47 | meta: { | ||
48 | title: 'Add follow' | ||
49 | } | ||
50 | } | ||
51 | }, | 45 | }, |
52 | { | 46 | { |
53 | path: 'video-redundancies-list', | 47 | path: 'video-redundancies-list', |
diff --git a/client/src/app/+admin/follows/index.ts b/client/src/app/+admin/follows/index.ts index 4fcb35cb1..285955468 100644 --- a/client/src/app/+admin/follows/index.ts +++ b/client/src/app/+admin/follows/index.ts | |||
@@ -1,4 +1,3 @@ | |||
1 | export * from './following-add' | ||
2 | export * from './followers-list' | 1 | export * from './followers-list' |
3 | export * from './following-list' | 2 | export * from './following-list' |
4 | export * from './video-redundancies-list' | 3 | export * from './video-redundancies-list' |
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html index 44c5c2fb8..0e072d84b 100644 --- a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html | |||
@@ -4,6 +4,14 @@ | |||
4 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | 4 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate |
5 | currentPageReportTemplate="Showing {first} to {last} of {totalRecords} muted instances" | 5 | currentPageReportTemplate="Showing {first} to {last} of {totalRecords} muted instances" |
6 | > | 6 | > |
7 | <ng-template pTemplate="caption"> | ||
8 | <div class="caption"> | ||
9 | <a class="ml-auto block-button" (click)="addServersToBlock()" (key.enter)="addServersToBlock()"> | ||
10 | <my-global-icon iconName="add"></my-global-icon> | ||
11 | <ng-container i18n>Mute domain</ng-container> | ||
12 | </a> | ||
13 | </div> | ||
14 | </ng-template> | ||
7 | 15 | ||
8 | <ng-template pTemplate="header"> | 16 | <ng-template pTemplate="header"> |
9 | <tr> | 17 | <tr> |
@@ -23,3 +31,11 @@ | |||
23 | </tr> | 31 | </tr> |
24 | </ng-template> | 32 | </ng-template> |
25 | </p-table> | 33 | </p-table> |
34 | |||
35 | <my-batch-domains-modal #batchDomainsModal i18n-action action="Mute domains" (domains)="onDomainsToBlock($event)"> | ||
36 | <ng-container ngProjectAs="warning"> | ||
37 | <div i18n *ngIf="httpEnabled() === false" class="alert alert-warning"> | ||
38 | It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers. | ||
39 | </div> | ||
40 | </ng-container> | ||
41 | </my-batch-domains-modal> | ||
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss index 6028b75ea..9d3bedd80 100644 --- a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss | |||
@@ -4,4 +4,8 @@ | |||
4 | .unblock-button { | 4 | .unblock-button { |
5 | @include peertube-button; | 5 | @include peertube-button; |
6 | @include grey-button; | 6 | @include grey-button; |
7 | } \ No newline at end of file | 7 | } |
8 | |||
9 | .block-button { | ||
10 | @include create-button; | ||
11 | } | ||
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts index 5af6d8f76..431729ef2 100644 --- a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts | |||
@@ -1,10 +1,11 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
4 | import { RestPagination, RestTable } from '@app/shared' | 4 | import { RestPagination, RestTable } from '@app/shared' |
5 | import { SortMeta } from 'primeng/api' | 5 | import { SortMeta } from 'primeng/api' |
6 | import { BlocklistService } from '@app/shared/blocklist' | 6 | import { BlocklistService } from '@app/shared/blocklist' |
7 | import { ServerBlock } from '../../../../../../shared' | 7 | import { ServerBlock } from '../../../../../../shared' |
8 | import { BatchDomainsModalComponent } from '@app/+admin/config/shared/batch-domains-modal.component' | ||
8 | 9 | ||
9 | @Component({ | 10 | @Component({ |
10 | selector: 'my-instance-server-blocklist', | 11 | selector: 'my-instance-server-blocklist', |
@@ -12,6 +13,8 @@ import { ServerBlock } from '../../../../../../shared' | |||
12 | templateUrl: './instance-server-blocklist.component.html' | 13 | templateUrl: './instance-server-blocklist.component.html' |
13 | }) | 14 | }) |
14 | export class InstanceServerBlocklistComponent extends RestTable implements OnInit { | 15 | export class InstanceServerBlocklistComponent extends RestTable implements OnInit { |
16 | @ViewChild('batchDomainsModal') batchDomainsModal: BatchDomainsModalComponent | ||
17 | |||
15 | blockedServers: ServerBlock[] = [] | 18 | blockedServers: ServerBlock[] = [] |
16 | totalRecords = 0 | 19 | totalRecords = 0 |
17 | rowsPerPage = 10 | 20 | rowsPerPage = 10 |
@@ -47,6 +50,27 @@ export class InstanceServerBlocklistComponent extends RestTable implements OnIni | |||
47 | ) | 50 | ) |
48 | } | 51 | } |
49 | 52 | ||
53 | httpEnabled () { | ||
54 | return window.location.protocol === 'https:' | ||
55 | } | ||
56 | |||
57 | addServersToBlock () { | ||
58 | this.batchDomainsModal.openModal() | ||
59 | } | ||
60 | |||
61 | onDomainsToBlock (domains: string[]) { | ||
62 | domains.forEach(domain => { | ||
63 | this.blocklistService.blockServerByInstance(domain) | ||
64 | .subscribe( | ||
65 | () => { | ||
66 | this.notifier.success(this.i18n('Instance {{domain}} muted by your instance.', { domain })) | ||
67 | |||
68 | this.loadData() | ||
69 | } | ||
70 | ) | ||
71 | }) | ||
72 | } | ||
73 | |||
50 | protected loadData () { | 74 | protected loadData () { |
51 | return this.blocklistService.getInstanceServerBlocklist(this.pagination, this.sort) | 75 | return this.blocklistService.getInstanceServerBlocklist(this.pagination, this.sort) |
52 | .subscribe( | 76 | .subscribe( |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html index 155d10dda..3899ee07f 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html | |||
@@ -48,13 +48,13 @@ | |||
48 | </a> | 48 | </a> |
49 | </td> | 49 | </td> |
50 | 50 | ||
51 | <td> | 51 | <td class="c-hand" [pRowToggler]="videoAbuse"> |
52 | <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span> | 52 | <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span> |
53 | <span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span> | 53 | <span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span> |
54 | </td> | 54 | </td> |
55 | 55 | ||
56 | <td class="action-cell"> | 56 | <td class="action-cell"> |
57 | <my-action-dropdown placement="bottom-right" i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown> | 57 | <my-action-dropdown placement="bottom-right auto" i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown> |
58 | </td> | 58 | </td> |
59 | </tr> | 59 | </tr> |
60 | </ng-template> | 60 | </ng-template> |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts index b135792a7..5e48cf24f 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import { Component, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { Account } from '../../../shared/account/account.model' | 2 | import { Account } from '@app/shared/account/account.model' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { SortMeta } from 'primeng/api' | 4 | import { SortMeta } from 'primeng/api' |
5 | import { VideoAbuse, VideoAbuseState } from '../../../../../../shared' | 5 | import { VideoAbuse, VideoAbuseState } from '../../../../../../shared' |
6 | import { RestPagination, RestTable, VideoAbuseService } from '../../../shared' | 6 | import { RestPagination, RestTable, VideoAbuseService, VideoBlacklistService } from '../../../shared' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 7 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | import { DropdownAction } from '../../../shared/buttons/action-dropdown.component' | 8 | import { DropdownAction } from '../../../shared/buttons/action-dropdown.component' |
9 | import { ConfirmService } from '../../../core/index' | 9 | import { ConfirmService } from '../../../core/index' |
@@ -14,6 +14,7 @@ import { Actor } from '@app/shared/actor/actor.model' | |||
14 | import { buildVideoLink, buildVideoEmbed } from 'src/assets/player/utils' | 14 | import { buildVideoLink, buildVideoEmbed } from 'src/assets/player/utils' |
15 | import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' | 15 | import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' |
16 | import { DomSanitizer } from '@angular/platform-browser' | 16 | import { DomSanitizer } from '@angular/platform-browser' |
17 | import { BlocklistService } from '@app/shared/blocklist' | ||
17 | 18 | ||
18 | @Component({ | 19 | @Component({ |
19 | selector: 'my-video-abuse-list', | 20 | selector: 'my-video-abuse-list', |
@@ -29,11 +30,13 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { | |||
29 | sort: SortMeta = { field: 'createdAt', order: 1 } | 30 | sort: SortMeta = { field: 'createdAt', order: 1 } |
30 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 31 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
31 | 32 | ||
32 | videoAbuseActions: DropdownAction<VideoAbuse>[] = [] | 33 | videoAbuseActions: DropdownAction<VideoAbuse>[][] = [] |
33 | 34 | ||
34 | constructor ( | 35 | constructor ( |
35 | private notifier: Notifier, | 36 | private notifier: Notifier, |
36 | private videoAbuseService: VideoAbuseService, | 37 | private videoAbuseService: VideoAbuseService, |
38 | private blocklistService: BlocklistService, | ||
39 | private videoBlacklistService: VideoBlacklistService, | ||
37 | private confirmService: ConfirmService, | 40 | private confirmService: ConfirmService, |
38 | private i18n: I18n, | 41 | private i18n: I18n, |
39 | private markdownRenderer: MarkdownService, | 42 | private markdownRenderer: MarkdownService, |
@@ -42,30 +45,57 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { | |||
42 | super() | 45 | super() |
43 | 46 | ||
44 | this.videoAbuseActions = [ | 47 | this.videoAbuseActions = [ |
45 | { | 48 | [ |
46 | label: this.i18n('Delete this report'), | 49 | { |
47 | handler: videoAbuse => this.removeVideoAbuse(videoAbuse) | 50 | label: this.i18n('Internal actions'), |
48 | }, | 51 | isHeader: true |
49 | { | 52 | }, |
50 | label: this.i18n('Add note'), | 53 | { |
51 | handler: videoAbuse => this.openModerationCommentModal(videoAbuse), | 54 | label: this.i18n('Delete report'), |
52 | isDisplayed: videoAbuse => !videoAbuse.moderationComment | 55 | handler: videoAbuse => this.removeVideoAbuse(videoAbuse) |
53 | }, | 56 | }, |
54 | { | 57 | { |
55 | label: this.i18n('Update note'), | 58 | label: this.i18n('Add note'), |
56 | handler: videoAbuse => this.openModerationCommentModal(videoAbuse), | 59 | handler: videoAbuse => this.openModerationCommentModal(videoAbuse), |
57 | isDisplayed: videoAbuse => !!videoAbuse.moderationComment | 60 | isDisplayed: videoAbuse => !videoAbuse.moderationComment |
58 | }, | 61 | }, |
59 | { | 62 | { |
60 | label: this.i18n('Mark as accepted'), | 63 | label: this.i18n('Update note'), |
61 | handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED), | 64 | handler: videoAbuse => this.openModerationCommentModal(videoAbuse), |
62 | isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse) | 65 | isDisplayed: videoAbuse => !!videoAbuse.moderationComment |
63 | }, | 66 | }, |
64 | { | 67 | { |
65 | label: this.i18n('Mark as rejected'), | 68 | label: this.i18n('Mark as accepted'), |
66 | handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED), | 69 | handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED), |
67 | isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse) | 70 | isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse) |
68 | } | 71 | }, |
72 | { | ||
73 | label: this.i18n('Mark as rejected'), | ||
74 | handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED), | ||
75 | isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse) | ||
76 | } | ||
77 | ], | ||
78 | [ | ||
79 | { | ||
80 | label: this.i18n('Actions for the video'), | ||
81 | isHeader: true | ||
82 | }, | ||
83 | { | ||
84 | label: this.i18n('Blacklist video'), | ||
85 | handler: videoAbuse => { | ||
86 | this.videoBlacklistService.blacklistVideo(videoAbuse.video.id, undefined, true) | ||
87 | .subscribe( | ||
88 | () => { | ||
89 | this.notifier.success(this.i18n('Video blacklisted.')) | ||
90 | |||
91 | this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) | ||
92 | }, | ||
93 | |||
94 | err => this.notifier.error(err.message) | ||
95 | ) | ||
96 | } | ||
97 | } | ||
98 | ] | ||
69 | ] | 99 | ] |
70 | } | 100 | } |
71 | 101 | ||
diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts index 43b6863af..4e9965bee 100644 --- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts +++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts | |||
@@ -18,7 +18,7 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit { | |||
18 | blacklist: (VideoBlacklist & { reasonHtml?: string })[] = [] | 18 | blacklist: (VideoBlacklist & { reasonHtml?: string })[] = [] |
19 | totalRecords = 0 | 19 | totalRecords = 0 |
20 | rowsPerPage = 10 | 20 | rowsPerPage = 10 |
21 | sort: SortMeta = { field: 'createdAt', order: 1 } | 21 | sort: SortMeta = { field: 'createdAt', order: -1 } |
22 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 22 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
23 | listBlacklistTypeFilter: VideoBlacklistType = undefined | 23 | listBlacklistTypeFilter: VideoBlacklistType = undefined |
24 | 24 | ||