aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--README.md17
-rw-r--r--client/src/app/+admin/admin.module.ts8
-rw-r--r--client/src/app/+admin/config/shared/batch-domains-modal.component.html43
-rw-r--r--client/src/app/+admin/config/shared/batch-domains-modal.component.scss3
-rw-r--r--client/src/app/+admin/config/shared/batch-domains-modal.component.ts54
-rw-r--r--client/src/app/+admin/config/shared/batch-domains-validators.service.ts68
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.ts2
-rw-r--r--client/src/app/+admin/follows/following-add/following-add.component.html22
-rw-r--r--client/src/app/+admin/follows/following-add/following-add.component.scss11
-rw-r--r--client/src/app/+admin/follows/following-add/following-add.component.ts85
-rw-r--r--client/src/app/+admin/follows/following-add/index.ts1
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.html8
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.scss6
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.ts22
-rw-r--r--client/src/app/+admin/follows/follows.component.html2
-rw-r--r--client/src/app/+admin/follows/follows.routes.ts8
-rw-r--r--client/src/app/+admin/follows/index.ts1
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html16
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss6
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts26
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html4
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts84
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts2
-rw-r--r--client/src/app/core/plugins/plugin.service.ts9
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.html16
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.scss4
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.ts1
-rw-r--r--client/src/app/shared/shared.module.ts2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.ts2
-rw-r--r--client/src/types/register-client-option.model.ts6
-rw-r--r--server/middlewares/validators/blocklist.ts6
-rw-r--r--server/tests/api/check-params/blocklist.ts12
-rw-r--r--shared/__ngcc_entry_points__.json1
-rw-r--r--support/doc/plugins/guide.md12
34 files changed, 376 insertions, 194 deletions
diff --git a/README.md b/README.md
index 02809db42..fe2ba4883 100644
--- a/README.md
+++ b/README.md
@@ -45,8 +45,8 @@ Be part of a network of multiple small federated, interoperable video hosting pr
45 45
46 <br /> 46 <br />
47 47
48 <a href="https://framagit.org/framasoft/peertube/PeerTube/commits/develop"> 48 <a href="https://travis-ci.com/github/Chocobozzz/PeerTube">
49 <img alt="pipeline status" src="https://framagit.org/framasoft/peertube/PeerTube/badges/develop/pipeline.svg" /> 49 <img alt="pipeline status" src="https://travis-ci.com/Chocobozzz/PeerTube.svg?branch=develop" />
50 </a> 50 </a>
51 51
52 <a href="https://david-dm.org/Chocobozzz/PeerTube"> 52 <a href="https://david-dm.org/Chocobozzz/PeerTube">
@@ -73,7 +73,12 @@ Be part of a network of multiple small federated, interoperable video hosting pr
73Introduction 73Introduction
74---------------------------------------------------------------- 74----------------------------------------------------------------
75 75
76PeerTube is a free, decentralized and federated video platform developed as an alternative to other platforms that centralize our data and attention, such as YouTube, Dailymotion or Vimeo. :clapper: But one organization hosting PeerTube alone may not have enough money to pay for bandwidth and video storage of its servers, all servers of PeerTube are interoperable as a federated network, and non-PeerTube servers can be part of the larger Vidiverse (federated video network) by talking our implementation of ActivityPub. Video load is reduced thanks to P2P (BitTorrent) in the web browser via <a href="https://github.com/webtorrent/webtorrent">WebTorrent</a>. 76PeerTube is a free, decentralized and federated video platform developed as an alternative to other platforms that centralize our data and attention, such as YouTube, Dailymotion or Vimeo. :clapper:
77
78But one organization hosting PeerTube alone may not have enough money to pay for bandwidth and video storage of its servers,
79all servers of PeerTube are interoperable as a federated network, and non-PeerTube servers can be part of the larger Vidiverse
80(federated video network) by talking our implementation of ActivityPub.
81Video load is reduced thanks to P2P in the web browser using <a href="https://github.com/webtorrent/webtorrent">WebTorrent</a> or <a href="https://github.com/novage/p2p-media-loader">p2p-media-loader</a>.
77 82
78To learn more, see: 83To learn more, see:
79* This [two-minute video](https://framatube.org/videos/watch/217eefeb-883d-45be-b7fc-a788ad8507d3) (hosted on PeerTube) explaining what PeerTube is and how it works 84* This [two-minute video](https://framatube.org/videos/watch/217eefeb-883d-45be-b7fc-a788ad8507d3) (hosted on PeerTube) explaining what PeerTube is and how it works
@@ -170,11 +175,9 @@ See [how to create your own instance](https://github.com/Chocobozzz/PeerTube/blo
170 175
171See the more general [admin documentation](https://docs.joinpeertube.org/#/admin-following-instances). 176See the more general [admin documentation](https://docs.joinpeertube.org/#/admin-following-instances).
172 177
173#### Tools 178### Tools documentation
174 179
175 * [Import videos (YouTube, Dailymotion, Vimeo...)](https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/tools.md) 180Learn how to import/upload videos from CLI or admin your PeerTube instance with the [tools documentation](https://docs.joinpeertube.org/#/maintain-tools).
176 * [Upload videos from the CLI](https://github.com/Chocobozzz/PeerTube/blob/support/doc/tools.md)
177 * [Admin server tools (create transcoding jobs, prune storage...)](https://github.com/Chocobozzz/PeerTube/blob/support/doc/tools.md#server-tools)
178 181
179### Technical documentation 182### Technical documentation
180 183
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'
5import { SharedModule } from '../shared' 5import { SharedModule } from '../shared'
6import { AdminRoutingModule } from './admin-routing.module' 6import { AdminRoutingModule } from './admin-routing.module'
7import { AdminComponent } from './admin.component' 7import { AdminComponent } from './admin.component'
8import { FollowersListComponent, FollowingAddComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows' 8import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows'
9import { FollowingListComponent } from './follows/following-list/following-list.component' 9import { FollowingListComponent } from './follows/following-list/following-list.component'
10import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' 10import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users'
11import { 11import {
@@ -28,6 +28,7 @@ import { SelectButtonModule } from 'primeng/selectbutton'
28import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' 28import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
29import { VideoRedundancyInformationComponent } from '@app/+admin/follows/video-redundancies-list/video-redundancy-information.component' 29import { VideoRedundancyInformationComponent } from '@app/+admin/follows/video-redundancies-list/video-redundancy-information.component'
30import { ChartModule } from 'primeng/chart' 30import { ChartModule } from 'primeng/chart'
31import { 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 @@
1textarea {
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 @@
1import { Component, OnInit, ViewChild, Input, Output, EventEmitter } from '@angular/core'
2import { I18n } from '@ngx-translate/i18n-polyfill'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
5import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
6import { FormReactive } from '@app/shared/forms'
7import { 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})
14export 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 @@
1import { I18n } from '@ngx-translate/i18n-polyfill'
2import { Validators, ValidatorFn } from '@angular/forms'
3import { Injectable } from '@angular/core'
4import { BuildFormValidator, validateHost } from '@app/shared'
5
6@Injectable()
7export 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
4textarea {
5 height: 250px;
6}
7
8input[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 @@
1import { Component } from '@angular/core'
2import { Router } from '@angular/router'
3import { Notifier } from '@app/core'
4import { ConfirmService } from '../../../core'
5import { validateHost } from '../../../shared'
6import { FollowService } from '@app/shared/instance/follow.service'
7import { 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})
14export 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 @@
1export * 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 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { SortMeta } from 'primeng/api' 3import { SortMeta } from 'primeng/api'
4import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' 4import { ActorFollow } from '../../../../../../shared/models/actors/follow.model'
@@ -6,6 +6,7 @@ import { ConfirmService } from '../../../core/confirm/confirm.service'
6import { RestPagination, RestTable } from '../../../shared' 6import { RestPagination, RestTable } from '../../../shared'
7import { FollowService } from '@app/shared/instance/follow.service' 7import { FollowService } from '@app/shared/instance/follow.service'
8import { I18n } from '@ngx-translate/i18n-polyfill' 8import { I18n } from '@ngx-translate/i18n-polyfill'
9import { 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})
15export class FollowingListComponent extends RestTable implements OnInit { 16export 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
3import { UserRightGuard } from '../../core' 3import { UserRightGuard } from '../../core'
4import { FollowsComponent } from './follows.component' 4import { FollowsComponent } from './follows.component'
5import { FollowingAddComponent } from './following-add'
6import { FollowersListComponent } from './followers-list' 5import { FollowersListComponent } from './followers-list'
7import { UserRight } from '../../../../../shared' 6import { UserRight } from '../../../../../shared'
8import { FollowingListComponent } from './following-list/following-list.component' 7import { 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 @@
1export * from './following-add'
2export * from './followers-list' 1export * from './followers-list'
3export * from './following-list' 2export * from './following-list'
4export * from './video-redundancies-list' 3export * 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 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared' 4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/api' 5import { SortMeta } from 'primeng/api'
6import { BlocklistService } from '@app/shared/blocklist' 6import { BlocklistService } from '@app/shared/blocklist'
7import { ServerBlock } from '../../../../../../shared' 7import { ServerBlock } from '../../../../../../shared'
8import { 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})
14export class InstanceServerBlocklistComponent extends RestTable implements OnInit { 15export 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 @@
1import { Component, OnInit, ViewChild } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { Account } from '../../../shared/account/account.model' 2import { Account } from '@app/shared/account/account.model'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { SortMeta } from 'primeng/api' 4import { SortMeta } from 'primeng/api'
5import { VideoAbuse, VideoAbuseState } from '../../../../../../shared' 5import { VideoAbuse, VideoAbuseState } from '../../../../../../shared'
6import { RestPagination, RestTable, VideoAbuseService } from '../../../shared' 6import { RestPagination, RestTable, VideoAbuseService, VideoBlacklistService } from '../../../shared'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { DropdownAction } from '../../../shared/buttons/action-dropdown.component' 8import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
9import { ConfirmService } from '../../../core/index' 9import { ConfirmService } from '../../../core/index'
@@ -14,6 +14,7 @@ import { Actor } from '@app/shared/actor/actor.model'
14import { buildVideoLink, buildVideoEmbed } from 'src/assets/player/utils' 14import { buildVideoLink, buildVideoEmbed } from 'src/assets/player/utils'
15import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' 15import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
16import { DomSanitizer } from '@angular/platform-browser' 16import { DomSanitizer } from '@angular/platform-browser'
17import { 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
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts
index da5114048..aa6823060 100644
--- a/client/src/app/core/plugins/plugin.service.ts
+++ b/client/src/app/core/plugins/plugin.service.ts
@@ -12,7 +12,7 @@ import { ClientHook, ClientHookName, clientHookObject } from '@shared/models/plu
12import { PluginClientScope } from '@shared/models/plugins/plugin-client-scope.type' 12import { PluginClientScope } from '@shared/models/plugins/plugin-client-scope.type'
13import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model' 13import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model'
14import { HttpClient } from '@angular/common/http' 14import { HttpClient } from '@angular/common/http'
15import { AuthService } from '@app/core/auth' 15import { AuthService, Notifier } from '@app/core'
16import { RestExtractor } from '@app/shared/rest' 16import { RestExtractor } from '@app/shared/rest'
17import { PluginType } from '@shared/models/plugins/plugin.type' 17import { PluginType } from '@shared/models/plugins/plugin.type'
18import { PublicServerSetting } from '@shared/models/plugins/public-server.setting' 18import { PublicServerSetting } from '@shared/models/plugins/public-server.setting'
@@ -60,6 +60,7 @@ export class PluginService implements ClientHook {
60 constructor ( 60 constructor (
61 private router: Router, 61 private router: Router,
62 private authService: AuthService, 62 private authService: AuthService,
63 private notifier: Notifier,
63 private server: ServerService, 64 private server: ServerService,
64 private zone: NgZone, 65 private zone: NgZone,
65 private authHttp: HttpClient, 66 private authHttp: HttpClient,
@@ -272,6 +273,12 @@ export class PluginService implements ClientHook {
272 return this.authService.isLoggedIn() 273 return this.authService.isLoggedIn()
273 }, 274 },
274 275
276 notifier: {
277 info: (text: string, title?: string, timeout?: number) => this.notifier.info(text, title, timeout),
278 error: (text: string, title?: string, timeout?: number) => this.notifier.error(text, title, timeout),
279 success: (text: string, title?: string, timeout?: number) => this.notifier.success(text, title, timeout)
280 },
281
275 translate: (value: string) => { 282 translate: (value: string) => {
276 return this.translationsObservable 283 return this.translationsObservable
277 .pipe(map(allTranslations => allTranslations[npmName])) 284 .pipe(map(allTranslations => allTranslations[npmName]))
diff --git a/client/src/app/shared/buttons/action-dropdown.component.html b/client/src/app/shared/buttons/action-dropdown.component.html
index cd993db9f..14cfe9a22 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.html
+++ b/client/src/app/shared/buttons/action-dropdown.component.html
@@ -24,17 +24,27 @@
24 </div> 24 </div>
25 </ng-template> 25 </ng-template>
26 26
27 <a *ngIf="action.linkBuilder" [ngClass]="{ 'with-icon': !!action.iconName }" class="dropdown-item" [routerLink]="action.linkBuilder(entry)" [title]="action.title || ''"> 27 <a
28 *ngIf="action.linkBuilder && !action.isHeader" [ngClass]="{ 'with-icon': !!action.iconName }"
29 class="dropdown-item" [routerLink]="action.linkBuilder(entry)" [title]="action.title || ''"
30 >
28 <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container> 31 <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container>
29 </a> 32 </a>
30 33
31 <span 34 <span
32 *ngIf="!action.linkBuilder" [ngClass]="{ 'with-icon': !!action.iconName }" (click)="action.handler(entry)" 35 *ngIf="!action.linkBuilder && !action.isHeader" [ngClass]="{ 'with-icon': !!action.iconName }"
33 class="custom-action dropdown-item" role="button" [title]="action.title || ''" 36 class="custom-action dropdown-item" role="button" [title]="action.title || ''" (click)="action.handler(entry)"
34 > 37 >
35 <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container> 38 <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container>
36 </span> 39 </span>
37 40
41 <h6
42 *ngIf="!action.linkBuilder && action.isHeader" [ngClass]="{ 'with-icon': !!action.iconName }"
43 class="dropdown-header" role="button" [title]="action.title || ''" (click)="action.handler(entry)"
44 >
45 <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container>
46 </h6>
47
38 </ng-container> 48 </ng-container>
39 </ng-container> 49 </ng-container>
40 50
diff --git a/client/src/app/shared/buttons/action-dropdown.component.scss b/client/src/app/shared/buttons/action-dropdown.component.scss
index 442c90984..7a030f32c 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.scss
+++ b/client/src/app/shared/buttons/action-dropdown.component.scss
@@ -51,6 +51,10 @@
51} 51}
52 52
53.dropdown-menu { 53.dropdown-menu {
54 .dropdown-header {
55 padding: 0.2rem 1rem;
56 }
57
54 .dropdown-item { 58 .dropdown-item {
55 display: flex; 59 display: flex;
56 cursor: pointer; 60 cursor: pointer;
diff --git a/client/src/app/shared/buttons/action-dropdown.component.ts b/client/src/app/shared/buttons/action-dropdown.component.ts
index 6649b092a..8fcaa38b9 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.ts
+++ b/client/src/app/shared/buttons/action-dropdown.component.ts
@@ -9,6 +9,7 @@ export type DropdownAction<T> = {
9 handler?: (a: T) => any 9 handler?: (a: T) => any
10 linkBuilder?: (a: T) => (string | number)[] 10 linkBuilder?: (a: T) => (string | number)[]
11 isDisplayed?: (a: T) => boolean 11 isDisplayed?: (a: T) => boolean
12 isHeader?: boolean
12} 13}
13 14
14export type DropdownButtonSize = 'normal' | 'small' 15export type DropdownButtonSize = 'normal' | 'small'
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index a952880a6..01735c187 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -107,6 +107,7 @@ import { InputSwitchModule } from 'primeng/inputswitch'
107import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings' 107import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings'
108import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface' 108import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface'
109import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component' 109import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component'
110import { BatchDomainsValidatorsService } from '@app/+admin/config/shared/batch-domains-validators.service'
110 111
111@NgModule({ 112@NgModule({
112 imports: [ 113 imports: [
@@ -297,6 +298,7 @@ import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-i
297 LoginValidatorsService, 298 LoginValidatorsService,
298 ResetPasswordValidatorsService, 299 ResetPasswordValidatorsService,
299 UserValidatorsService, 300 UserValidatorsService,
301 BatchDomainsValidatorsService,
300 VideoPlaylistValidatorsService, 302 VideoPlaylistValidatorsService,
301 VideoAbuseValidatorsService, 303 VideoAbuseValidatorsService,
302 VideoChannelValidatorsService, 304 VideoChannelValidatorsService,
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
index 0f7c19765..e1a8f6260 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
@@ -11,7 +11,6 @@ import { VideoCommentService } from './video-comment.service'
11import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 11import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
12import { VideoCommentValidatorsService } from '@app/shared/forms/form-validators/video-comment-validators.service' 12import { VideoCommentValidatorsService } from '@app/shared/forms/form-validators/video-comment-validators.service'
13import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 13import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
14import { AuthService } from '@app/core/auth'
15 14
16@Component({ 15@Component({
17 selector: 'my-video-comment-add', 16 selector: 'my-video-comment-add',
@@ -38,7 +37,6 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
38 private videoCommentValidatorsService: VideoCommentValidatorsService, 37 private videoCommentValidatorsService: VideoCommentValidatorsService,
39 private notifier: Notifier, 38 private notifier: Notifier,
40 private videoCommentService: VideoCommentService, 39 private videoCommentService: VideoCommentService,
41 private authService: AuthService,
42 private modalService: NgbModal, 40 private modalService: NgbModal,
43 private router: Router 41 private router: Router
44 ) { 42 ) {
diff --git a/client/src/types/register-client-option.model.ts b/client/src/types/register-client-option.model.ts
index 638b08653..b64652a0f 100644
--- a/client/src/types/register-client-option.model.ts
+++ b/client/src/types/register-client-option.model.ts
@@ -13,5 +13,11 @@ export type RegisterClientHelpers = {
13 13
14 getSettings: () => Promise<{ [ name: string ]: string }> 14 getSettings: () => Promise<{ [ name: string ]: string }>
15 15
16 notifier: {
17 info: (text: string, title?: string, timeout?: number) => void,
18 error: (text: string, title?: string, timeout?: number) => void,
19 success: (text: string, title?: string, timeout?: number) => void
20 }
21
16 translate: (toTranslate: string) => Promise<string> 22 translate: (toTranslate: string) => Promise<string>
17} 23}
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts
index 47a0b1a1c..b2183437c 100644
--- a/server/middlewares/validators/blocklist.ts
+++ b/server/middlewares/validators/blocklist.ts
@@ -84,11 +84,9 @@ const blockServerValidator = [
84 .end() 84 .end()
85 } 85 }
86 86
87 const server = await ServerModel.loadByHost(host) 87 let server = await ServerModel.loadByHost(host)
88 if (!server) { 88 if (!server) {
89 return res.status(404) 89 server = await ServerModel.create({ host })
90 .send({ error: 'Server host not found.' })
91 .end()
92 } 90 }
93 91
94 res.locals.server = server 92 res.locals.server = server
diff --git a/server/tests/api/check-params/blocklist.ts b/server/tests/api/check-params/blocklist.ts
index fb459f756..1219ec9bd 100644
--- a/server/tests/api/check-params/blocklist.ts
+++ b/server/tests/api/check-params/blocklist.ts
@@ -175,13 +175,13 @@ describe('Test blocklist API validators', function () {
175 }) 175 })
176 }) 176 })
177 177
178 it('Should fail with an unknown server', async function () { 178 it('Should succeed with an unknown server', async function () {
179 await makePostBodyRequest({ 179 await makePostBodyRequest({
180 url: server.url, 180 url: server.url,
181 token: server.accessToken, 181 token: server.accessToken,
182 path, 182 path,
183 fields: { host: 'localhost:9003' }, 183 fields: { host: 'localhost:9003' },
184 statusCodeExpected: 404 184 statusCodeExpected: 204
185 }) 185 })
186 }) 186 })
187 187
@@ -218,7 +218,7 @@ describe('Test blocklist API validators', function () {
218 it('Should fail with an unknown server block', async function () { 218 it('Should fail with an unknown server block', async function () {
219 await makeDeleteRequest({ 219 await makeDeleteRequest({
220 url: server.url, 220 url: server.url,
221 path: path + '/localhost:9003', 221 path: path + '/localhost:9004',
222 token: server.accessToken, 222 token: server.accessToken,
223 statusCodeExpected: 404 223 statusCodeExpected: 404
224 }) 224 })
@@ -415,13 +415,13 @@ describe('Test blocklist API validators', function () {
415 }) 415 })
416 }) 416 })
417 417
418 it('Should fail with an unknown server', async function () { 418 it('Should succeed with an unknown server', async function () {
419 await makePostBodyRequest({ 419 await makePostBodyRequest({
420 url: server.url, 420 url: server.url,
421 token: server.accessToken, 421 token: server.accessToken,
422 path, 422 path,
423 fields: { host: 'localhost:9003' }, 423 fields: { host: 'localhost:9003' },
424 statusCodeExpected: 404 424 statusCodeExpected: 204
425 }) 425 })
426 }) 426 })
427 427
@@ -467,7 +467,7 @@ describe('Test blocklist API validators', function () {
467 it('Should fail with an unknown server block', async function () { 467 it('Should fail with an unknown server block', async function () {
468 await makeDeleteRequest({ 468 await makeDeleteRequest({
469 url: server.url, 469 url: server.url,
470 path: path + '/localhost:9003', 470 path: path + '/localhost:9004',
471 token: server.accessToken, 471 token: server.accessToken,
472 statusCodeExpected: 404 472 statusCodeExpected: 404
473 }) 473 })
diff --git a/shared/__ngcc_entry_points__.json b/shared/__ngcc_entry_points__.json
deleted file mode 100644
index 179bdcda3..000000000
--- a/shared/__ngcc_entry_points__.json
+++ /dev/null
@@ -1 +0,0 @@
1{"ngccVersion":"9.1.0","configFileHash":"87c535c3ce0eac2a54c246892e0e21a1","lockFileHash":"d04bf20520f2518af162e882d32081e4","entryPointPaths":[]} \ No newline at end of file
diff --git a/support/doc/plugins/guide.md b/support/doc/plugins/guide.md
index bdc9d2ad8..5251ce48a 100644
--- a/support/doc/plugins/guide.md
+++ b/support/doc/plugins/guide.md
@@ -197,7 +197,7 @@ The `ping` route can be accessed using:
197 197
198### Client helpers (themes & plugins) 198### Client helpers (themes & plugins)
199 199
200### Plugin static route 200#### Plugin static route
201 201
202To get your plugin static route: 202To get your plugin static route:
203 203
@@ -206,6 +206,16 @@ const baseStaticUrl = peertubeHelpers.getBaseStaticRoute()
206const imageUrl = baseStaticUrl + '/images/chocobo.png' 206const imageUrl = baseStaticUrl + '/images/chocobo.png'
207``` 207```
208 208
209#### Notifier
210
211To notify the user with the PeerTube ToastModule:
212
213```js
214const { notifier } = peertubeHelpers
215notifier.success('Success message content.')
216notifier.error('Error message content.')
217```
218
209#### Translate 219#### Translate
210 220
211You can translate some strings of your plugin (PeerTube will use your `translations` object of your `package.json` file): 221You can translate some strings of your plugin (PeerTube will use your `translations` object of your `package.json` file):