diff options
34 files changed, 376 insertions, 194 deletions
@@ -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 | |||
73 | Introduction | 73 | Introduction |
74 | ---------------------------------------------------------------- | 74 | ---------------------------------------------------------------- |
75 | 75 | ||
76 | PeerTube 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>. | 76 | PeerTube 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 | |||
78 | But one organization hosting PeerTube alone may not have enough money to pay for bandwidth and video storage of its servers, | ||
79 | all 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. | ||
81 | Video 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 | ||
78 | To learn more, see: | 83 | To 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 | ||
171 | See the more general [admin documentation](https://docs.joinpeertube.org/#/admin-following-instances). | 176 | See 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) | 180 | Learn 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' | |||
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 | ||
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 | |||
12 | import { PluginClientScope } from '@shared/models/plugins/plugin-client-scope.type' | 12 | import { PluginClientScope } from '@shared/models/plugins/plugin-client-scope.type' |
13 | import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model' | 13 | import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model' |
14 | import { HttpClient } from '@angular/common/http' | 14 | import { HttpClient } from '@angular/common/http' |
15 | import { AuthService } from '@app/core/auth' | 15 | import { AuthService, Notifier } from '@app/core' |
16 | import { RestExtractor } from '@app/shared/rest' | 16 | import { RestExtractor } from '@app/shared/rest' |
17 | import { PluginType } from '@shared/models/plugins/plugin.type' | 17 | import { PluginType } from '@shared/models/plugins/plugin.type' |
18 | import { PublicServerSetting } from '@shared/models/plugins/public-server.setting' | 18 | import { 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 | ||
14 | export type DropdownButtonSize = 'normal' | 'small' | 15 | export 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' | |||
107 | import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings' | 107 | import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings' |
108 | import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface' | 108 | import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface' |
109 | import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component' | 109 | import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component' |
110 | import { 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' | |||
11 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 11 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
12 | import { VideoCommentValidatorsService } from '@app/shared/forms/form-validators/video-comment-validators.service' | 12 | import { VideoCommentValidatorsService } from '@app/shared/forms/form-validators/video-comment-validators.service' |
13 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 13 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
14 | import { 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 | ||
202 | To get your plugin static route: | 202 | To get your plugin static route: |
203 | 203 | ||
@@ -206,6 +206,16 @@ const baseStaticUrl = peertubeHelpers.getBaseStaticRoute() | |||
206 | const imageUrl = baseStaticUrl + '/images/chocobo.png' | 206 | const imageUrl = baseStaticUrl + '/images/chocobo.png' |
207 | ``` | 207 | ``` |
208 | 208 | ||
209 | #### Notifier | ||
210 | |||
211 | To notify the user with the PeerTube ToastModule: | ||
212 | |||
213 | ```js | ||
214 | const { notifier } = peertubeHelpers | ||
215 | notifier.success('Success message content.') | ||
216 | notifier.error('Error message content.') | ||
217 | ``` | ||
218 | |||
209 | #### Translate | 219 | #### Translate |
210 | 220 | ||
211 | You can translate some strings of your plugin (PeerTube will use your `translations` object of your `package.json` file): | 221 | You can translate some strings of your plugin (PeerTube will use your `translations` object of your `package.json` file): |