aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+about/about-instance/contact-admin-modal.component.ts3
-rw-r--r--client/src/app/+accounts/accounts.component.ts3
-rw-r--r--client/src/app/+admin/admin.component.ts4
-rw-r--r--client/src/app/+admin/admin.module.ts3
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.html4
-rw-r--r--client/src/app/+admin/follows/following-list/follow-modal.component.html42
-rw-r--r--client/src/app/+admin/follows/following-list/follow-modal.component.scss3
-rw-r--r--client/src/app/+admin/follows/following-list/follow-modal.component.ts69
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.html21
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.ts22
-rw-r--r--client/src/app/+admin/follows/following-list/index.ts1
-rw-r--r--client/src/app/+admin/follows/follows.routes.ts4
-rw-r--r--client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts8
-rw-r--r--client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts2
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.ts16
-rw-r--r--client/src/app/+login/login.component.html4
-rw-r--r--client/src/app/+login/login.component.ts4
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts7
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts4
-rw-r--r--client/src/app/+my-library/my-videos/my-videos.component.html2
-rw-r--r--client/src/app/+page-not-found/page-not-found.component.ts3
-rw-r--r--client/src/app/+search/search-filters.component.html16
-rw-r--r--client/src/app/+search/search-filters.component.ts8
-rw-r--r--client/src/app/+search/search.component.html2
-rw-r--r--client/src/app/+search/search.component.scss4
-rw-r--r--client/src/app/+search/search.component.ts32
-rw-r--r--client/src/app/+video-channels/video-channels.component.ts2
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts3
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts2
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html62
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.scss18
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.ts4
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts3
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts14
-rw-r--r--client/src/app/core/auth/auth.service.ts3
-rw-r--r--client/src/app/core/menu/menu.service.ts2
-rw-r--r--client/src/app/core/renderer/markdown.service.ts10
-rw-r--r--client/src/app/core/rest/rest-extractor.service.ts3
-rw-r--r--client/src/app/core/rest/rest.service.ts10
-rw-r--r--client/src/app/helpers/index.ts1
-rw-r--r--client/src/app/helpers/rxjs.ts29
-rw-r--r--client/src/app/helpers/utils.ts2
-rw-r--r--client/src/app/shared/form-validators/batch-domains-validators.ts60
-rw-r--r--client/src/app/shared/form-validators/host-validators.ts105
-rw-r--r--client/src/app/shared/form-validators/host.ts8
-rw-r--r--client/src/app/shared/form-validators/index.ts3
-rw-r--r--client/src/app/shared/form-validators/video-channel-validators.ts10
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts7
-rw-r--r--client/src/app/shared/shared-custom-markup/custom-markup.service.ts20
-rw-r--r--client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts9
-rw-r--r--client/src/app/shared/shared-custom-markup/peertube-custom-tags/embed-markup.component.ts7
-rw-r--r--client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts9
-rw-r--r--client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts7
-rw-r--r--client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts4
-rw-r--r--client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts4
-rw-r--r--client/src/app/shared/shared-forms/markdown-textarea.component.ts5
-rw-r--r--client/src/app/shared/shared-forms/timestamp-input.component.ts2
-rw-r--r--client/src/app/shared/shared-instance/instance-follow.service.ts13
-rw-r--r--client/src/app/shared/shared-main/auth/auth-interceptor.service.ts8
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.model.ts6
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.ts2
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts3
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts34
-rw-r--r--client/src/app/shared/shared-moderation/batch-domains-modal.component.html14
-rw-r--r--client/src/app/shared/shared-moderation/batch-domains-modal.component.ts8
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/video-report.component.ts8
-rw-r--r--client/src/app/shared/shared-search/advanced-search.model.ts34
-rw-r--r--client/src/app/shared/shared-search/find-in-bulk.service.ts123
-rw-r--r--client/src/app/shared/shared-search/index.ts1
-rw-r--r--client/src/app/shared/shared-search/search.service.ts43
-rw-r--r--client/src/app/shared/shared-search/shared-search.module.ts2
-rw-r--r--client/src/app/shared/shared-share-modal/video-share.component.ts33
-rw-r--r--client/src/app/shared/shared-user-subscription/user-subscription.service.ts22
-rw-r--r--client/src/app/shared/shared-video-miniature/abstract-video-list.ts2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist.model.ts3
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist.service.ts23
79 files changed, 750 insertions, 357 deletions
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.ts b/client/src/app/+about/about-instance/contact-admin-modal.component.ts
index a528faa20..37e9feacb 100644
--- a/client/src/app/+about/about-instance/contact-admin-modal.component.ts
+++ b/client/src/app/+about/about-instance/contact-admin-modal.component.ts
@@ -11,8 +11,7 @@ import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
11import { InstanceService } from '@app/shared/shared-instance' 11import { InstanceService } from '@app/shared/shared-instance'
12import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 12import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
13import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 13import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
14import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 14import { HTMLServerConfig, HttpStatusCode } from '@shared/models'
15import { HTMLServerConfig } from '@shared/models'
16 15
17type Prefill = { 16type Prefill = {
18 subject?: string 17 subject?: string
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts
index c69b04a01..5b59f3cd0 100644
--- a/client/src/app/+accounts/accounts.component.ts
+++ b/client/src/app/+accounts/accounts.component.ts
@@ -13,8 +13,7 @@ import {
13 VideoService 13 VideoService
14} from '@app/shared/shared-main' 14} from '@app/shared/shared-main'
15import { AccountReportComponent } from '@app/shared/shared-moderation' 15import { AccountReportComponent } from '@app/shared/shared-moderation'
16import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 16import { HttpStatusCode, User, UserRight } from '@shared/models'
17import { User, UserRight } from '@shared/models'
18import { AccountSearchComponent } from './account-search/account-search.component' 17import { AccountSearchComponent } from './account-search/account-search.component'
19 18
20@Component({ 19@Component({
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts
index dd92ed2ca..4b6fab6ed 100644
--- a/client/src/app/+admin/admin.component.ts
+++ b/client/src/app/+admin/admin.component.ts
@@ -26,12 +26,12 @@ export class AdminComponent implements OnInit {
26 label: $localize`Federation`, 26 label: $localize`Federation`,
27 children: [ 27 children: [
28 { 28 {
29 label: $localize`Instances you follow`, 29 label: $localize`Following`,
30 routerLink: '/admin/follows/following-list', 30 routerLink: '/admin/follows/following-list',
31 iconName: 'following' 31 iconName: 'following'
32 }, 32 },
33 { 33 {
34 label: $localize`Instances following you`, 34 label: $localize`Followers`,
35 routerLink: '/admin/follows/followers-list', 35 routerLink: '/admin/follows/followers-list',
36 iconName: 'follower' 36 iconName: 'follower'
37 }, 37 },
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index a7fe20b07..1ea7b9784 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -25,7 +25,7 @@ import {
25 EditVODTranscodingComponent 25 EditVODTranscodingComponent
26} from './config' 26} from './config'
27import { ConfigService } from './config/shared/config.service' 27import { ConfigService } from './config/shared/config.service'
28import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows' 28import { FollowersListComponent, FollowModalComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows'
29import { FollowingListComponent } from './follows/following-list/following-list.component' 29import { FollowingListComponent } from './follows/following-list/following-list.component'
30import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' 30import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component'
31import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' 31import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component'
@@ -68,6 +68,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
68 FollowsComponent, 68 FollowsComponent,
69 FollowersListComponent, 69 FollowersListComponent,
70 FollowingListComponent, 70 FollowingListComponent,
71 FollowModalComponent,
71 RedundancyCheckboxComponent, 72 RedundancyCheckboxComponent,
72 VideoRedundanciesListComponent, 73 VideoRedundanciesListComponent,
73 VideoRedundancyInformationComponent, 74 VideoRedundancyInformationComponent,
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html
index c2e9a4df6..08459634d 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.html
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html
@@ -1,6 +1,6 @@
1<h1> 1<h1>
2 <my-global-icon iconName="follower" aria-hidden="true"></my-global-icon> 2 <my-global-icon iconName="follower" aria-hidden="true"></my-global-icon>
3 <ng-container i18n>Instances following you</ng-container> 3 <ng-container i18n>Followers of your instance</ng-container>
4</h1> 4</h1>
5 5
6<p-table 6<p-table
@@ -21,7 +21,7 @@
21 <ng-template pTemplate="header"> 21 <ng-template pTemplate="header">
22 <tr> 22 <tr>
23 <th style="width: 150px;" i18n>Actions</th> 23 <th style="width: 150px;" i18n>Actions</th>
24 <th i18n>Follower handle</th> 24 <th i18n>Follower</th>
25 <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th> 25 <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
26 <th style="width: 100px;" i18n pSortableColumn="score">Score <p-sortIcon field="score"></p-sortIcon></th> 26 <th style="width: 100px;" i18n pSortableColumn="score">Score <p-sortIcon field="score"></p-sortIcon></th>
27 <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> 27 <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
diff --git a/client/src/app/+admin/follows/following-list/follow-modal.component.html b/client/src/app/+admin/follows/following-list/follow-modal.component.html
new file mode 100644
index 000000000..d0761b718
--- /dev/null
+++ b/client/src/app/+admin/follows/following-list/follow-modal.component.html
@@ -0,0 +1,42 @@
1<ng-template #modal>
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Follow</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="hostsOrHandles">1 host (without "http://"), account handle or channel handle per line</label>
12
13 <textarea
14 [placeholder]="placeholder" formControlName="hostsOrHandles" type="text" id="hostsOrHandles" name="hostsOrHandles"
15 class="form-control" [ngClass]="{ 'input-error': formErrors['hostsOrHandles'] }" ngbAutofocus
16 ></textarea>
17
18 <div *ngIf="formErrors.hostsOrHandles" class="form-error">
19 {{ formErrors.hostsOrHandles }}
20
21 <div *ngIf="form.controls['hostsOrHandles'].errors.validHostsOrHandles">
22 {{ form.controls['hostsOrHandles'].errors.validHostsOrHandles.value }}
23 </div>
24 </div>
25 </div>
26
27 <div i18n *ngIf="httpEnabled() === false" class="alert alert-warning">
28 It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
29 </div>
30
31 <div class="form-group inputs">
32 <input
33 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
34 (click)="hide()" (key.enter)="hide()"
35 >
36
37 <input type="submit" i18n-value value="Follow" class="peertube-button orange-button" [disabled]="!form.valid" />
38 </div>
39 </form>
40 </div>
41
42</ng-template>
diff --git a/client/src/app/+admin/follows/following-list/follow-modal.component.scss b/client/src/app/+admin/follows/following-list/follow-modal.component.scss
new file mode 100644
index 000000000..9621a566f
--- /dev/null
+++ b/client/src/app/+admin/follows/following-list/follow-modal.component.scss
@@ -0,0 +1,3 @@
1textarea {
2 height: 200px;
3}
diff --git a/client/src/app/+admin/follows/following-list/follow-modal.component.ts b/client/src/app/+admin/follows/following-list/follow-modal.component.ts
new file mode 100644
index 000000000..dc6909200
--- /dev/null
+++ b/client/src/app/+admin/follows/following-list/follow-modal.component.ts
@@ -0,0 +1,69 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core'
3import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5import { InstanceFollowService } from '@app/shared/shared-instance'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
8
9@Component({
10 selector: 'my-follow-modal',
11 templateUrl: './follow-modal.component.html',
12 styleUrls: [ './follow-modal.component.scss' ]
13})
14export class FollowModalComponent extends FormReactive implements OnInit {
15 @ViewChild('modal', { static: true }) modal: NgbModal
16
17 @Output() newFollow = new EventEmitter<void>()
18
19 placeholder = 'example.com\nchocobozzz@example.com\nchocobozzz_channel@example.com'
20
21 private openedModal: NgbModalRef
22
23 constructor (
24 protected formValidatorService: FormValidatorService,
25 private modalService: NgbModal,
26 private followService: InstanceFollowService,
27 private notifier: Notifier
28 ) {
29 super()
30 }
31
32 ngOnInit () {
33 this.buildForm({
34 hostsOrHandles: UNIQUE_HOSTS_OR_HANDLE_VALIDATOR
35 })
36 }
37
38 openModal () {
39 this.openedModal = this.modalService.open(this.modal, { centered: true })
40 }
41
42 hide () {
43 this.openedModal.close()
44 }
45
46 submit () {
47 this.addFollowing()
48
49 this.form.reset()
50 this.hide()
51 }
52
53 httpEnabled () {
54 return window.location.protocol === 'https:'
55 }
56
57 private async addFollowing () {
58 const hostsOrHandles = splitAndGetNotEmpty(this.form.value['hostsOrHandles'])
59
60 this.followService.follow(hostsOrHandles).subscribe(
61 () => {
62 this.notifier.success($localize`Follow request(s) sent!`)
63 this.newFollow.emit()
64 },
65
66 err => this.notifier.error(err.message)
67 )
68 }
69}
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 e7c0c9088..75b0efca8 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
@@ -1,6 +1,6 @@
1<h1> 1<h1>
2 <my-global-icon iconName="following" aria-hidden="true"></my-global-icon> 2 <my-global-icon iconName="following" aria-hidden="true"></my-global-icon>
3 <ng-container i18n>Instances you follow</ng-container> 3 <ng-container i18n>Your instance subscriptions</ng-container>
4</h1> 4</h1>
5 5
6<p-table 6<p-table
@@ -13,9 +13,9 @@
13 <ng-template pTemplate="caption"> 13 <ng-template pTemplate="caption">
14 <div class="caption"> 14 <div class="caption">
15 <div class="left-buttons"> 15 <div class="left-buttons">
16 <a class="follow-button" (click)="addDomainsToFollow()" (key.enter)="addDomainsToFollow()"> 16 <a class="follow-button" (click)="openFollowModal()" (key.enter)="openFollowModal()">
17 <my-global-icon iconName="following" aria-hidden="true"></my-global-icon> 17 <my-global-icon iconName="following" aria-hidden="true"></my-global-icon>
18 <ng-container i18n>Follow instances</ng-container> 18 <ng-container i18n>Follow</ng-container>
19 </a> 19 </a>
20 </div> 20 </div>
21 21
@@ -28,7 +28,7 @@
28 <ng-template pTemplate="header"> 28 <ng-template pTemplate="header">
29 <tr> 29 <tr>
30 <th style="width: 150px;" i18n>Action</th> 30 <th style="width: 150px;" i18n>Action</th>
31 <th i18n>Host</th> 31 <th i18n>Following</th>
32 <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th> 32 <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
33 <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> 33 <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
34 <th style="width: 160px;" i18n pSortableColumn="redundancyAllowed">Redundancy allowed <p-sortIcon field="redundancyAllowed"></p-sortIcon></th> 34 <th style="width: 160px;" i18n pSortableColumn="redundancyAllowed">Redundancy allowed <p-sortIcon field="redundancyAllowed"></p-sortIcon></th>
@@ -41,8 +41,8 @@
41 <my-delete-button label="Unfollow" i18n-label (click)="removeFollowing(follow)"></my-delete-button> 41 <my-delete-button label="Unfollow" i18n-label (click)="removeFollowing(follow)"></my-delete-button>
42 </td> 42 </td>
43 <td> 43 <td>
44 <a [href]="'https://' + follow.following.host" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer"> 44 <a [href]="follow.following.url" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer">
45 {{ follow.following.host }} 45 {{ follow.following.name + '@' + follow.following.host }}
46 <span class="glyphicon glyphicon-new-window"></span> 46 <span class="glyphicon glyphicon-new-window"></span>
47 </a> 47 </a>
48 </td> 48 </td>
@@ -57,6 +57,7 @@
57 <td>{{ follow.createdAt | date: 'short' }}</td> 57 <td>{{ follow.createdAt | date: 'short' }}</td>
58 <td> 58 <td>
59 <my-redundancy-checkbox 59 <my-redundancy-checkbox
60 *ngIf="isInstanceFollowing(follow)"
60 [host]="follow.following.host" [redundancyAllowed]="follow.following.hostRedundancyAllowed" 61 [host]="follow.following.host" [redundancyAllowed]="follow.following.hostRedundancyAllowed"
61 ></my-redundancy-checkbox> 62 ></my-redundancy-checkbox>
62 </td> 63 </td>
@@ -75,10 +76,4 @@
75 </ng-template> 76 </ng-template>
76</p-table> 77</p-table>
77 78
78<my-batch-domains-modal #batchDomainsModal i18n-action action="Follow domains" (domains)="addFollowing($event)"> 79<my-follow-modal #followModal></my-follow-modal>
79 <ng-container ngProjectAs="warning">
80 <div i18n *ngIf="httpEnabled() === false" class="alert alert-warning">
81 It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
82 </div>
83 </ng-container>
84</my-batch-domains-modal>
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 b63fe08c0..ba62dfa23 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
@@ -4,13 +4,14 @@ import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
4import { InstanceFollowService } from '@app/shared/shared-instance' 4import { InstanceFollowService } from '@app/shared/shared-instance'
5import { BatchDomainsModalComponent } from '@app/shared/shared-moderation' 5import { BatchDomainsModalComponent } from '@app/shared/shared-moderation'
6import { ActorFollow } from '@shared/models' 6import { ActorFollow } from '@shared/models'
7import { FollowModalComponent } from './follow-modal.component'
7 8
8@Component({ 9@Component({
9 templateUrl: './following-list.component.html', 10 templateUrl: './following-list.component.html',
10 styleUrls: [ '../follows.component.scss', './following-list.component.scss' ] 11 styleUrls: [ '../follows.component.scss', './following-list.component.scss' ]
11}) 12})
12export class FollowingListComponent extends RestTable implements OnInit { 13export class FollowingListComponent extends RestTable implements OnInit {
13 @ViewChild('batchDomainsModal') batchDomainsModal: BatchDomainsModalComponent 14 @ViewChild('followModal') followModal: FollowModalComponent
14 15
15 following: ActorFollow[] = [] 16 following: ActorFollow[] = []
16 totalRecords = 0 17 totalRecords = 0
@@ -33,23 +34,12 @@ export class FollowingListComponent extends RestTable implements OnInit {
33 return 'FollowingListComponent' 34 return 'FollowingListComponent'
34 } 35 }
35 36
36 addDomainsToFollow () { 37 openFollowModal () {
37 this.batchDomainsModal.openModal() 38 this.followModal.openModal()
38 } 39 }
39 40
40 httpEnabled () { 41 isInstanceFollowing (follow: ActorFollow) {
41 return window.location.protocol === 'https:' 42 return follow.following.name === 'peertube'
42 }
43
44 async addFollowing (hosts: string[]) {
45 this.followService.follow(hosts).subscribe(
46 () => {
47 this.notifier.success($localize`Follow request(s) sent!`)
48 this.reloadData()
49 },
50
51 err => this.notifier.error(err.message)
52 )
53 } 43 }
54 44
55 async removeFollowing (follow: ActorFollow) { 45 async removeFollowing (follow: ActorFollow) {
diff --git a/client/src/app/+admin/follows/following-list/index.ts b/client/src/app/+admin/follows/following-list/index.ts
index a70d46a7e..88be0ed4c 100644
--- a/client/src/app/+admin/follows/following-list/index.ts
+++ b/client/src/app/+admin/follows/following-list/index.ts
@@ -1 +1,2 @@
1export * from './follow-modal.component'
1export * from './following-list.component' 2export * from './following-list.component'
diff --git a/client/src/app/+admin/follows/follows.routes.ts b/client/src/app/+admin/follows/follows.routes.ts
index cd70daf77..3843b42b5 100644
--- a/client/src/app/+admin/follows/follows.routes.ts
+++ b/client/src/app/+admin/follows/follows.routes.ts
@@ -25,7 +25,7 @@ export const FollowsRoutes: Routes = [
25 component: FollowingListComponent, 25 component: FollowingListComponent,
26 data: { 26 data: {
27 meta: { 27 meta: {
28 title: $localize`Following list` 28 title: $localize`Following`
29 } 29 }
30 } 30 }
31 }, 31 },
@@ -34,7 +34,7 @@ export const FollowsRoutes: Routes = [
34 component: FollowersListComponent, 34 component: FollowersListComponent,
35 data: { 35 data: {
36 meta: { 36 meta: {
37 title: $localize`Followers list` 37 title: $localize`Followers`
38 } 38 }
39 } 39 }
40 }, 40 },
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
index 08500ef5c..4fe5ec441 100644
--- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
+++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
@@ -1,6 +1,6 @@
1import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
2import { switchMap } from 'rxjs/operators' 2import { switchMap } from 'rxjs/operators'
3import { buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' 3import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
4import { environment } from 'src/environments/environment' 4import { environment } from 'src/environments/environment'
5import { Component, OnInit } from '@angular/core' 5import { Component, OnInit } from '@angular/core'
6import { DomSanitizer } from '@angular/platform-browser' 6import { DomSanitizer } from '@angular/platform-browser'
@@ -9,6 +9,7 @@ import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, S
9import { AdvancedInputFilter } from '@app/shared/shared-forms' 9import { AdvancedInputFilter } from '@app/shared/shared-forms'
10import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' 10import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
11import { VideoBlockService } from '@app/shared/shared-moderation' 11import { VideoBlockService } from '@app/shared/shared-moderation'
12import { buildVideoEmbedLink, decorateVideoLink } from '@shared/core-utils'
12import { VideoBlacklist, VideoBlacklistType } from '@shared/models' 13import { VideoBlacklist, VideoBlacklistType } from '@shared/models'
13 14
14@Component({ 15@Component({
@@ -147,8 +148,9 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
147 148
148 getVideoEmbed (entry: VideoBlacklist) { 149 getVideoEmbed (entry: VideoBlacklist) {
149 return buildVideoOrPlaylistEmbed( 150 return buildVideoOrPlaylistEmbed(
150 buildVideoLink({ 151 decorateVideoLink({
151 baseUrl: `${environment.originServerUrl}/videos/embed/${entry.video.uuid}`, 152 url: buildVideoEmbedLink(entry.video, environment.originServerUrl),
153
152 title: false, 154 title: false,
153 warningTitle: false 155 warningTitle: false
154 }), 156 }),
diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
index 6af224920..968abcbe5 100644
--- a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
+++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
@@ -4,7 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router'
4import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' 4import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
5import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core' 5import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core'
6import { PluginService } from '@app/core/plugins/plugin.service' 6import { PluginService } from '@app/core/plugins/plugin.service'
7import { compareSemVer } from '@shared/core-utils/miscs/miscs' 7import { compareSemVer } from '@shared/core-utils'
8import { PeerTubePlugin, PluginType } from '@shared/models' 8import { PeerTubePlugin, PluginType } from '@shared/models'
9 9
10@Component({ 10@Component({
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts
index e02d8e1ad..e3ae68a93 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.ts
+++ b/client/src/app/+admin/users/user-list/user-list.component.ts
@@ -108,18 +108,18 @@ export class UserListComponent extends RestTable implements OnInit {
108 ] 108 ]
109 109
110 this.columns = [ 110 this.columns = [
111 { id: 'username', label: 'Username' }, 111 { id: 'username', label: $localize`Username` },
112 { id: 'email', label: 'Email' }, 112 { id: 'email', label: $localize`Email` },
113 { id: 'quota', label: 'Video quota' }, 113 { id: 'quota', label: $localize`Video quota` },
114 { id: 'role', label: 'Role' }, 114 { id: 'role', label: $localize`Role` },
115 { id: 'createdAt', label: 'Created' } 115 { id: 'createdAt', label: $localize`Created` }
116 ] 116 ]
117 117
118 this.selectedColumns = this.columns.map(c => c.id) 118 this.selectedColumns = this.columns.map(c => c.id)
119 119
120 this.columns.push({ id: 'quotaDaily', label: 'Daily quota' }) 120 this.columns.push({ id: 'quotaDaily', label: $localize`Daily quota` })
121 this.columns.push({ id: 'pluginAuth', label: 'Auth plugin' }) 121 this.columns.push({ id: 'pluginAuth', label: $localize`Auth plugin` })
122 this.columns.push({ id: 'lastLoginDate', label: 'Last login' }) 122 this.columns.push({ id: 'lastLoginDate', label: $localize`Last login` })
123 } 123 }
124 124
125 getIdentifier () { 125 getIdentifier () {
diff --git a/client/src/app/+login/login.component.html b/client/src/app/+login/login.component.html
index 5f5b0f565..27793ff0c 100644
--- a/client/src/app/+login/login.component.html
+++ b/client/src/app/+login/login.component.html
@@ -28,6 +28,10 @@
28 <div *ngIf="formErrors.username" class="form-error"> 28 <div *ngIf="formErrors.username" class="form-error">
29 {{ formErrors.username }} 29 {{ formErrors.username }}
30 </div> 30 </div>
31
32 <div *ngIf="hasUsernameUppercase()" i18n class="form-warning">
33 ⚠️ Most email addresses do not include capital letters.
34 </div>
31 </div> 35 </div>
32 36
33 <div class="form-group"> 37 <div class="form-group">
diff --git a/client/src/app/+login/login.component.ts b/client/src/app/+login/login.component.ts
index d8ad49081..9731383af 100644
--- a/client/src/app/+login/login.component.ts
+++ b/client/src/app/+login/login.component.ts
@@ -141,6 +141,10 @@ The link will expire within 1 hour.`
141 this.accordion = instanceAboutAccordion.accordion 141 this.accordion = instanceAboutAccordion.accordion
142 } 142 }
143 143
144 hasUsernameUppercase () {
145 return this.form.value['username'].match(/[A-Z]/)
146 }
147
144 private loadExternalAuthToken (username: string, token: string) { 148 private loadExternalAuthToken (username: string, token: string) {
145 this.isAuthenticatedWithExternalAuth = true 149 this.isAuthenticatedWithExternalAuth = true
146 150
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts
index b3265210f..433475f66 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts
@@ -1,3 +1,5 @@
1import { of } from 'rxjs'
2import { switchMap } from 'rxjs/operators'
1import { Component, OnInit } from '@angular/core' 3import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router' 4import { Router } from '@angular/router'
3import { AuthService, Notifier } from '@app/core' 5import { AuthService, Notifier } from '@app/core'
@@ -9,11 +11,8 @@ import {
9} from '@app/shared/form-validators/video-channel-validators' 11} from '@app/shared/form-validators/video-channel-validators'
10import { FormValidatorService } from '@app/shared/shared-forms' 12import { FormValidatorService } from '@app/shared/shared-forms'
11import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' 13import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
12import { VideoChannelCreate } from '@shared/models' 14import { HttpStatusCode, VideoChannelCreate } from '@shared/models'
13import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
14import { MyVideoChannelEdit } from './my-video-channel-edit' 15import { MyVideoChannelEdit } from './my-video-channel-edit'
15import { switchMap } from 'rxjs/operators'
16import { of } from 'rxjs'
17 16
18@Component({ 17@Component({
19 templateUrl: './my-video-channel-edit.component.html', 18 templateUrl: './my-video-channel-edit.component.html',
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts
index 67b3ee496..b6a2f592d 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts
@@ -45,9 +45,9 @@ export class MyVideoChannelsComponent {
45It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another 45It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another
46channel with the same name (${videoChannel.name})!`, 46channel with the same name (${videoChannel.name})!`,
47 47
48 $localize`Please type the display name of the video channel (${videoChannel.displayName}) to confirm`, 48 $localize`Please type the name of the video channel (${videoChannel.name}) to confirm`,
49 49
50 videoChannel.displayName, 50 videoChannel.name,
51 51
52 $localize`Delete` 52 $localize`Delete`
53 ) 53 )
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.html b/client/src/app/+my-library/my-videos/my-videos.component.html
index 8d8b482ad..0552b8ce4 100644
--- a/client/src/app/+my-library/my-videos/my-videos.component.html
+++ b/client/src/app/+my-library/my-videos/my-videos.component.html
@@ -23,7 +23,7 @@
23 23
24 <div class="peertube-select-container peertube-select-button"> 24 <div class="peertube-select-container peertube-select-button">
25 <select [(ngModel)]="sort" (ngModelChange)="onChangeSortColumn()" class="form-control"> 25 <select [(ngModel)]="sort" (ngModelChange)="onChangeSortColumn()" class="form-control">
26 <option value="undefined" disabled>Sort by</option> 26 <option value="undefined" disabled i18n>Sort by</option>
27 <option value="-publishedAt" i18n>Last published first</option> 27 <option value="-publishedAt" i18n>Last published first</option>
28 <option value="-createdAt" i18n>Last created first</option> 28 <option value="-createdAt" i18n>Last created first</option>
29 <option value="-views" i18n>Most viewed first</option> 29 <option value="-views" i18n>Most viewed first</option>
diff --git a/client/src/app/+page-not-found/page-not-found.component.ts b/client/src/app/+page-not-found/page-not-found.component.ts
index 639e5db78..10645a634 100644
--- a/client/src/app/+page-not-found/page-not-found.component.ts
+++ b/client/src/app/+page-not-found/page-not-found.component.ts
@@ -1,7 +1,8 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { Title } from '@angular/platform-browser' 2import { Title } from '@angular/platform-browser'
3import { Router } from '@angular/router' 3import { Router } from '@angular/router'
4import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 4import { HttpStatusCode } from '@shared/models'
5
5@Component({ 6@Component({
6 selector: 'my-page-not-found', 7 selector: 'my-page-not-found',
7 templateUrl: './page-not-found.component.html', 8 templateUrl: './page-not-found.component.html',
diff --git a/client/src/app/+search/search-filters.component.html b/client/src/app/+search/search-filters.component.html
index 421bc7f6f..4b87a2102 100644
--- a/client/src/app/+search/search-filters.component.html
+++ b/client/src/app/+search/search-filters.component.html
@@ -63,7 +63,7 @@
63 </div> 63 </div>
64 64
65 <div class="peertube-radio-container" *ngFor="let date of publishedDateRanges"> 65 <div class="peertube-radio-container" *ngFor="let date of publishedDateRanges">
66 <input type="radio" (change)="onInputUpdated()" name="publishedDateRange" [id]="date.id" [value]="date.id" [(ngModel)]="publishedDateRange"> 66 <input type="radio" (change)="onDurationOrPublishedUpdated()" name="publishedDateRange" [id]="date.id" [value]="date.id" [(ngModel)]="publishedDateRange">
67 <label [for]="date.id" class="radio">{{ date.label }}</label> 67 <label [for]="date.id" class="radio">{{ date.label }}</label>
68 </div> 68 </div>
69 </div> 69 </div>
@@ -79,7 +79,7 @@
79 <div class="row"> 79 <div class="row">
80 <div class="pl-0 col-sm-6"> 80 <div class="pl-0 col-sm-6">
81 <input 81 <input
82 (change)="onInputUpdated()" 82 (change)="onDurationOrPublishedUpdated()"
83 (keydown.enter)="$event.preventDefault()" 83 (keydown.enter)="$event.preventDefault()"
84 type="text" id="original-publication-after" name="original-publication-after" 84 type="text" id="original-publication-after" name="original-publication-after"
85 i18n-placeholder placeholder="After..." 85 i18n-placeholder placeholder="After..."
@@ -89,7 +89,7 @@
89 </div> 89 </div>
90 <div class="pr-0 col-sm-6"> 90 <div class="pr-0 col-sm-6">
91 <input 91 <input
92 (change)="onInputUpdated()" 92 (change)="onDurationOrPublishedUpdated()"
93 (keydown.enter)="$event.preventDefault()" 93 (keydown.enter)="$event.preventDefault()"
94 type="text" id="original-publication-before" name="original-publication-before" 94 type="text" id="original-publication-before" name="original-publication-before"
95 i18n-placeholder placeholder="Before..." 95 i18n-placeholder placeholder="Before..."
@@ -112,7 +112,7 @@
112 </div> 112 </div>
113 113
114 <div class="peertube-radio-container" *ngFor="let duration of durationRanges"> 114 <div class="peertube-radio-container" *ngFor="let duration of durationRanges">
115 <input type="radio" (change)="onInputUpdated()" name="durationRange" [id]="duration.id" [value]="duration.id" [(ngModel)]="durationRange"> 115 <input type="radio" (change)="onDurationOrPublishedUpdated()" name="durationRange" [id]="duration.id" [value]="duration.id" [(ngModel)]="durationRange">
116 <label [for]="duration.id" class="radio">{{ duration.label }}</label> 116 <label [for]="duration.id" class="radio">{{ duration.label }}</label>
117 </div> 117 </div>
118 </div> 118 </div>
@@ -174,6 +174,14 @@
174 <my-select-tags name="tagsOneOf" labelForId="tagsOneOf" id="tagsOneOf" [(ngModel)]="advancedSearch.tagsOneOf"></my-select-tags> 174 <my-select-tags name="tagsOneOf" labelForId="tagsOneOf" id="tagsOneOf" [(ngModel)]="advancedSearch.tagsOneOf"></my-select-tags>
175 </div> 175 </div>
176 176
177 <div class="form-group">
178 <label i18n for="host">PeerTube instance host</label>
179
180 <input (change)="onDurationOrPublishedUpdated()" (keydown.enter)="$event.preventDefault()" type="text" id="host" name="host"
181 placeholder="example.com" [(ngModel)]="advancedSearch.host" class="form-control"
182 >
183 </div>
184
177 <div class="form-group" *ngIf="isSearchTargetEnabled()"> 185 <div class="form-group" *ngIf="isSearchTargetEnabled()">
178 <div class="radio-label label-container"> 186 <div class="radio-label label-container">
179 <label i18n>Search target</label> 187 <label i18n>Search target</label>
diff --git a/client/src/app/+search/search-filters.component.ts b/client/src/app/+search/search-filters.component.ts
index afa523b91..5972ba553 100644
--- a/client/src/app/+search/search-filters.component.ts
+++ b/client/src/app/+search/search-filters.component.ts
@@ -108,14 +108,14 @@ export class SearchFiltersComponent implements OnInit {
108 this.loadOriginallyPublishedAtYears() 108 this.loadOriginallyPublishedAtYears()
109 } 109 }
110 110
111 onInputUpdated () { 111 onDurationOrPublishedUpdated () {
112 this.updateModelFromDurationRange() 112 this.updateModelFromDurationRange()
113 this.updateModelFromPublishedRange() 113 this.updateModelFromPublishedRange()
114 this.updateModelFromOriginallyPublishedAtYears() 114 this.updateModelFromOriginallyPublishedAtYears()
115 } 115 }
116 116
117 formUpdated () { 117 formUpdated () {
118 this.onInputUpdated() 118 this.onDurationOrPublishedUpdated()
119 this.filtered.emit(this.advancedSearch) 119 this.filtered.emit(this.advancedSearch)
120 } 120 }
121 121
@@ -127,7 +127,7 @@ export class SearchFiltersComponent implements OnInit {
127 this.durationRange = undefined 127 this.durationRange = undefined
128 this.publishedDateRange = undefined 128 this.publishedDateRange = undefined
129 129
130 this.onInputUpdated() 130 this.onDurationOrPublishedUpdated()
131 } 131 }
132 132
133 resetField (fieldName: string, value?: any) { 133 resetField (fieldName: string, value?: any) {
@@ -136,7 +136,7 @@ export class SearchFiltersComponent implements OnInit {
136 136
137 resetLocalField (fieldName: string, value?: any) { 137 resetLocalField (fieldName: string, value?: any) {
138 this[fieldName] = value 138 this[fieldName] = value
139 this.onInputUpdated() 139 this.onDurationOrPublishedUpdated()
140 } 140 }
141 141
142 resetOriginalPublicationYears () { 142 resetOriginalPublicationYears () {
diff --git a/client/src/app/+search/search.component.html b/client/src/app/+search/search.component.html
index b28abca6a..dc8b4d595 100644
--- a/client/src/app/+search/search.component.html
+++ b/client/src/app/+search/search.component.html
@@ -24,6 +24,8 @@
24 24
25 <div class="results-filter collapse-transition" [ngbCollapse]="isSearchFilterCollapsed"> 25 <div class="results-filter collapse-transition" [ngbCollapse]="isSearchFilterCollapsed">
26 <my-search-filters [advancedSearch]="advancedSearch" (filtered)="onFiltered()"></my-search-filters> 26 <my-search-filters [advancedSearch]="advancedSearch" (filtered)="onFiltered()"></my-search-filters>
27
28 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
27 </div> 29 </div>
28 </div> 30 </div>
29 31
diff --git a/client/src/app/+search/search.component.scss b/client/src/app/+search/search.component.scss
index fca704d27..b521825e5 100644
--- a/client/src/app/+search/search.component.scss
+++ b/client/src/app/+search/search.component.scss
@@ -15,6 +15,10 @@
15 padding: 40px; 15 padding: 40px;
16} 16}
17 17
18.alert-danger {
19 margin-top: 10px;
20}
21
18.results-header { 22.results-header {
19 font-size: 16px; 23 font-size: 16px;
20 padding-bottom: 20px; 24 padding-bottom: 20px;
diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts
index 235bbfa4c..7425b7016 100644
--- a/client/src/app/+search/search.component.ts
+++ b/client/src/app/+search/search.component.ts
@@ -1,9 +1,10 @@
1import { forkJoin, of, Subscription } from 'rxjs' 1import { forkJoin, Subscription } from 'rxjs'
2import { LinkType } from 'src/types/link.type' 2import { LinkType } from 'src/types/link.type'
3import { Component, OnDestroy, OnInit } from '@angular/core' 3import { Component, OnDestroy, OnInit } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core' 5import { AuthService, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core'
6import { immutableAssign } from '@app/helpers' 6import { immutableAssign } from '@app/helpers'
7import { validateHost } from '@app/shared/form-validators/host-validators'
7import { Video, VideoChannel } from '@app/shared/shared-main' 8import { Video, VideoChannel } from '@app/shared/shared-main'
8import { AdvancedSearch, SearchService } from '@app/shared/shared-search' 9import { AdvancedSearch, SearchService } from '@app/shared/shared-search'
9import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature' 10import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
@@ -16,7 +17,9 @@ import { HTMLServerConfig, SearchTargetType } from '@shared/models'
16 templateUrl: './search.component.html' 17 templateUrl: './search.component.html'
17}) 18})
18export class SearchComponent implements OnInit, OnDestroy { 19export class SearchComponent implements OnInit, OnDestroy {
19 results: (Video | VideoChannel)[] = [] 20 error: string
21
22 results: (Video | VideoChannel | VideoPlaylist)[] = []
20 23
21 pagination = { 24 pagination = {
22 currentPage: 1, 25 currentPage: 1,
@@ -89,8 +92,10 @@ export class SearchComponent implements OnInit, OnDestroy {
89 this.advancedSearch.searchTarget = this.getDefaultSearchTarget() 92 this.advancedSearch.searchTarget = this.getDefaultSearchTarget()
90 } 93 }
91 94
92 // Don't hide filters if we have some of them AND the user just came on the webpage 95 this.error = this.checkFieldsAndGetError()
93 this.isSearchFilterCollapsed = this.isInitialLoad === false || !this.advancedSearch.containsValues() 96
97 // Don't hide filters if we have some of them AND the user just came on the webpage, or we have an error
98 this.isSearchFilterCollapsed = !this.error && (this.isInitialLoad === false || !this.advancedSearch.containsValues())
94 this.isInitialLoad = false 99 this.isInitialLoad = false
95 100
96 this.search() 101 this.search()
@@ -126,6 +131,9 @@ export class SearchComponent implements OnInit, OnDestroy {
126 } 131 }
127 132
128 search () { 133 search () {
134 this.error = this.checkFieldsAndGetError()
135 if (this.error) return
136
129 this.isSearching = true 137 this.isSearching = true
130 138
131 forkJoin([ 139 forkJoin([
@@ -275,12 +283,10 @@ export class SearchComponent implements OnInit, OnDestroy {
275 } 283 }
276 284
277 private getVideoChannelObs () { 285 private getVideoChannelObs () {
278 if (!this.currentSearch) return of({ data: [], total: 0 })
279
280 const params = { 286 const params = {
281 search: this.currentSearch, 287 search: this.currentSearch,
282 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }), 288 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }),
283 searchTarget: this.advancedSearch.searchTarget 289 advancedSearch: this.advancedSearch
284 } 290 }
285 291
286 return this.hooks.wrapObsFun( 292 return this.hooks.wrapObsFun(
@@ -293,12 +299,10 @@ export class SearchComponent implements OnInit, OnDestroy {
293 } 299 }
294 300
295 private getVideoPlaylistObs () { 301 private getVideoPlaylistObs () {
296 if (!this.currentSearch) return of({ data: [], total: 0 })
297
298 const params = { 302 const params = {
299 search: this.currentSearch, 303 search: this.currentSearch,
300 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.playlistsPerPage }), 304 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.playlistsPerPage }),
301 searchTarget: this.advancedSearch.searchTarget 305 advancedSearch: this.advancedSearch
302 } 306 }
303 307
304 return this.hooks.wrapObsFun( 308 return this.hooks.wrapObsFun(
@@ -319,4 +323,12 @@ export class SearchComponent implements OnInit, OnDestroy {
319 323
320 return 'local' 324 return 'local'
321 } 325 }
326
327 private checkFieldsAndGetError () {
328 if (this.advancedSearch.host && !validateHost(this.advancedSearch.host)) {
329 return $localize`PeerTube instance host filter is invalid`
330 }
331
332 return undefined
333 }
322} 334}
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts
index 3833d9c54..6479644f1 100644
--- a/client/src/app/+video-channels/video-channels.component.ts
+++ b/client/src/app/+video-channels/video-channels.component.ts
@@ -7,7 +7,7 @@ import { AuthService, MarkdownService, Notifier, RestExtractor, ScreenService }
7import { ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' 7import { ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
8import { SupportModalComponent } from '@app/shared/shared-support-modal' 8import { SupportModalComponent } from '@app/shared/shared-support-modal'
9import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' 9import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
10import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 10import { HttpStatusCode } from '@shared/models'
11 11
12@Component({ 12@Component({
13 templateUrl: './video-channels.component.html', 13 templateUrl: './video-channels.component.html',
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
index 50d030ac9..ee5a50611 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
@@ -45,7 +45,7 @@
45 </ng-template> 45 </ng-template>
46 </my-help> 46 </my-help>
47 47
48 <my-markdown-textarea [truncate]="250" formControlName="description" [markdownVideo]="true"></my-markdown-textarea> 48 <my-markdown-textarea [truncate]="250" formControlName="description" [markdownVideo]="videoToUpdate"></my-markdown-textarea>
49 49
50 <div *ngIf="formErrors.description" class="form-error"> 50 <div *ngIf="formErrors.description" class="form-error">
51 {{ formErrors.description }} 51 {{ formErrors.description }}
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
index d8d20a249..189bc9669 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
@@ -7,8 +7,7 @@ import { genericUploadErrorHandler, scrollToTop } from '@app/helpers'
7import { FormValidatorService } from '@app/shared/shared-forms' 7import { FormValidatorService } from '@app/shared/shared-forms'
8import { BytesPipe, Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' 8import { BytesPipe, Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
9import { LoadingBarService } from '@ngx-loading-bar/core' 9import { LoadingBarService } from '@ngx-loading-bar/core'
10import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 10import { HttpStatusCode, VideoPrivacy } from '@shared/models'
11import { VideoPrivacy } from '@shared/models'
12import { UploaderXFormData } from './uploaderx-form-data' 11import { UploaderXFormData } from './uploaderx-form-data'
13import { VideoSend } from './video-send' 12import { VideoSend } from './video-send'
14 13
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts
index 04f8f0d58..0e1c4c207 100644
--- a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts
+++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts
@@ -161,7 +161,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
161 // Before HTML rendering restore line feed for markdown list compatibility 161 // Before HTML rendering restore line feed for markdown list compatibility
162 const commentText = this.comment.text.replace(/<br.?\/?>/g, '\r\n') 162 const commentText = this.comment.text.replace(/<br.?\/?>/g, '\r\n')
163 const html = await this.markdownService.textMarkdownToHTML(commentText, true, true) 163 const html = await this.markdownService.textMarkdownToHTML(commentText, true, true)
164 this.sanitizedCommentHTML = this.markdownService.processVideoTimestamps(html) 164 this.sanitizedCommentHTML = this.markdownService.processVideoTimestamps(this.video.shortUUID, html)
165 this.newParentComments = this.parentComments.concat([ this.comment ]) 165 this.newParentComments = this.parentComments.concat([ this.comment ])
166 166
167 if (this.comment.account) { 167 if (this.comment.account) {
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html b/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html
index 598bc485d..362a21905 100644
--- a/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html
@@ -1,54 +1,62 @@
1<div class="video-attribute"> 1<div class="attribute">
2 <span i18n class="video-attribute-label">Privacy</span> 2 <span i18n class="attribute-label">Privacy</span>
3 <span class="video-attribute-value">{{ video.privacy.label }}</span> 3 <span class="attribute-value">{{ video.privacy.label }}</span>
4</div> 4</div>
5 5
6<div *ngIf="video.isLocal === false" class="video-attribute"> 6<div *ngIf="video.isLocal === false" class="attribute">
7 <span i18n class="video-attribute-label">Origin</span> 7 <span i18n class="attribute-label">Origin</span>
8 <a class="video-attribute-value" target="_blank" rel="noopener noreferrer" [href]="getVideoUrl()">{{ video.originInstanceHost }}</a> 8 <a
9 class="attribute-value" target="_blank" rel="noopener noreferrer"
10 routerLink="/search" [queryParams]="{ host: getVideoHost() }"
11 >{{ video.originInstanceHost }}</a>
12
13 <a
14 i18n-title title="Open the video on the origin instance" class="glyphicon glyphicon-new-window"
15 target="_blank" rel="noopener noreferrer" [href]="getVideoUrl()"
16 ></a>
9</div> 17</div>
10 18
11<div *ngIf="!!video.originallyPublishedAt" class="video-attribute"> 19<div *ngIf="!!video.originallyPublishedAt" class="attribute">
12 <span i18n class="video-attribute-label">Originally published</span> 20 <span i18n class="attribute-label">Originally published</span>
13 <span class="video-attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span> 21 <span class="attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span>
14</div> 22</div>
15 23
16<div class="video-attribute"> 24<div class="attribute">
17 <span i18n class="video-attribute-label">Category</span> 25 <span i18n class="attribute-label">Category</span>
18 <span *ngIf="!video.category.id" class="video-attribute-value">{{ video.category.label }}</span> 26 <span *ngIf="!video.category.id" class="attribute-value">{{ video.category.label }}</span>
19 <a 27 <a
20 *ngIf="video.category.id" class="video-attribute-value" 28 *ngIf="video.category.id" class="attribute-value"
21 [routerLink]="[ '/search' ]" [queryParams]="{ categoryOneOf: [ video.category.id ] }" 29 [routerLink]="[ '/search' ]" [queryParams]="{ categoryOneOf: [ video.category.id ] }"
22 >{{ video.category.label }}</a> 30 >{{ video.category.label }}</a>
23</div> 31</div>
24 32
25<div class="video-attribute"> 33<div class="attribute">
26 <span i18n class="video-attribute-label">Licence</span> 34 <span i18n class="attribute-label">Licence</span>
27 <span *ngIf="!video.licence.id" class="video-attribute-value">{{ video.licence.label }}</span> 35 <span *ngIf="!video.licence.id" class="attribute-value">{{ video.licence.label }}</span>
28 <a 36 <a
29 *ngIf="video.licence.id" class="video-attribute-value" 37 *ngIf="video.licence.id" class="attribute-value"
30 [routerLink]="[ '/search' ]" [queryParams]="{ licenceOneOf: [ video.licence.id ] }" 38 [routerLink]="[ '/search' ]" [queryParams]="{ licenceOneOf: [ video.licence.id ] }"
31 >{{ video.licence.label }}</a> 39 >{{ video.licence.label }}</a>
32</div> 40</div>
33 41
34<div class="video-attribute"> 42<div class="attribute">
35 <span i18n class="video-attribute-label">Language</span> 43 <span i18n class="attribute-label">Language</span>
36 <span *ngIf="!video.language.id" class="video-attribute-value">{{ video.language.label }}</span> 44 <span *ngIf="!video.language.id" class="attribute-value">{{ video.language.label }}</span>
37 <a 45 <a
38 *ngIf="video.language.id" class="video-attribute-value" 46 *ngIf="video.language.id" class="attribute-value"
39 [routerLink]="[ '/search' ]" [queryParams]="{ languageOneOf: [ video.language.id ] }" 47 [routerLink]="[ '/search' ]" [queryParams]="{ languageOneOf: [ video.language.id ] }"
40 >{{ video.language.label }}</a> 48 >{{ video.language.label }}</a>
41</div> 49</div>
42 50
43<div class="video-attribute video-attribute-tags"> 51<div class="attribute attribute-tags">
44 <span i18n class="video-attribute-label">Tags</span> 52 <span i18n class="attribute-label">Tags</span>
45 <a 53 <a
46 *ngFor="let tag of getVideoTags()" 54 *ngFor="let tag of getVideoTags()"
47 class="video-attribute-value" [routerLink]="[ '/search' ]" [queryParams]="{ tagsOneOf: [ tag ] }" 55 class="attribute-value" [routerLink]="[ '/search' ]" [queryParams]="{ tagsOneOf: [ tag ] }"
48 >{{ tag }}</a> 56 >{{ tag }}</a>
49</div> 57</div>
50 58
51<div class="video-attribute" *ngIf="!video.isLive"> 59<div class="attribute" *ngIf="!video.isLive">
52 <span i18n class="video-attribute-label">Duration</span> 60 <span i18n class="attribute-label">Duration</span>
53 <span class="video-attribute-value">{{ video.duration | myDurationFormatter }}</span> 61 <span class="attribute-value">{{ video.duration | myDurationFormatter }}</span>
54</div> 62</div>
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.scss b/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.scss
index 45190a3e3..26bead124 100644
--- a/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.scss
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.scss
@@ -1,13 +1,13 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.video-attribute { 4.attribute {
5 font-size: 13px; 5 font-size: 13px;
6 display: block; 6 display: block;
7 margin-bottom: 12px; 7 margin-bottom: 12px;
8} 8}
9 9
10.video-attribute-label { 10.attribute-label {
11 @include padding-right(5px); 11 @include padding-right(5px);
12 12
13 min-width: 142px; 13 min-width: 142px;
@@ -16,7 +16,7 @@
16 font-weight: $font-bold; 16 font-weight: $font-bold;
17} 17}
18 18
19a.video-attribute-value { 19a.attribute-value {
20 @include disable-default-a-behaviour; 20 @include disable-default-a-behaviour;
21 color: pvar(--mainForegroundColor); 21 color: pvar(--mainForegroundColor);
22 22
@@ -25,16 +25,22 @@ a.video-attribute-value {
25 } 25 }
26} 26}
27 27
28.video-attribute-tags { 28.attribute-tags {
29 .video-attribute-value:not(:nth-child(2)) { 29 .attribute-value:not(:nth-child(2)) {
30 &::before { 30 &::before {
31 content: ', '; 31 content: ', ';
32 } 32 }
33 } 33 }
34} 34}
35 35
36.glyphicon-new-window {
37 color: pvar(--inputPlaceholderColor);
38 margin-left: 5px;
39 font-size: 12px;
40}
41
36@media screen and (max-width: 1600px) { 42@media screen and (max-width: 1600px) {
37 .video-attributes .video-attribute { 43 .attributes .attribute {
38 margin-bottom: 5px; 44 margin-bottom: 5px;
39 } 45 }
40} 46}
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.ts b/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.ts
index 5cb77f0c8..9429581ac 100644
--- a/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.ts
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.ts
@@ -17,6 +17,10 @@ export class VideoAttributesComponent {
17 return this.video.url 17 return this.video.url
18 } 18 }
19 19
20 getVideoHost () {
21 return this.video.channel.host
22 }
23
20 getVideoTags () { 24 getVideoTags () {
21 if (!this.video || Array.isArray(this.video.tags) === false) return [] 25 if (!this.video || Array.isArray(this.video.tags) === false) return []
22 26
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts
index 23d00d31a..870c7ae3f 100644
--- a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts
@@ -80,6 +80,7 @@ export class VideoDescriptionComponent implements OnChanges {
80 80
81 private async setVideoDescriptionHTML () { 81 private async setVideoDescriptionHTML () {
82 const html = await this.markdownService.textMarkdownToHTML(this.video.description) 82 const html = await this.markdownService.textMarkdownToHTML(this.video.description)
83 this.videoHTMLDescription = this.markdownService.processVideoTimestamps(html) 83
84 this.videoHTMLDescription = this.markdownService.processVideoTimestamps(this.video.shortUUID, html)
84 } 85 }
85} 86}
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts
index d078844c3..ccb9c5e71 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -21,8 +21,16 @@ import { isXPercentInViewport, scrollToTop } from '@app/helpers'
21import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' 21import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main'
22import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' 22import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
23import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' 23import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
24import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 24import { timeToInt } from '@shared/core-utils'
25import { HTMLServerConfig, PeerTubeProblemDocument, ServerErrorCode, VideoCaption, VideoPrivacy, VideoState } from '@shared/models' 25import {
26 HTMLServerConfig,
27 HttpStatusCode,
28 PeerTubeProblemDocument,
29 ServerErrorCode,
30 VideoCaption,
31 VideoPrivacy,
32 VideoState
33} from '@shared/models'
26import { cleanupVideoWatch, getStoredTheater, getStoredVideoWatchHistory } from '../../../assets/player/peertube-player-local-storage' 34import { cleanupVideoWatch, getStoredTheater, getStoredVideoWatchHistory } from '../../../assets/player/peertube-player-local-storage'
27import { 35import {
28 CustomizationOptions, 36 CustomizationOptions,
@@ -32,7 +40,6 @@ import {
32 PlayerMode, 40 PlayerMode,
33 videojs 41 videojs
34} from '../../../assets/player/peertube-player-manager' 42} from '../../../assets/player/peertube-player-manager'
35import { timeToInt } from '../../../assets/player/utils'
36import { environment } from '../../../environments/environment' 43import { environment } from '../../../environments/environment'
37import { VideoWatchPlaylistComponent } from './shared' 44import { VideoWatchPlaylistComponent } from './shared'
38 45
@@ -575,6 +582,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
575 582
576 videoCaptions: playerCaptions, 583 videoCaptions: playerCaptions,
577 584
585 videoShortUUID: video.shortUUID,
578 videoUUID: video.uuid 586 videoUUID: video.uuid
579 }, 587 },
580 588
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts
index cdf13186b..60bd72c60 100644
--- a/client/src/app/core/auth/auth.service.ts
+++ b/client/src/app/core/auth/auth.service.ts
@@ -6,12 +6,11 @@ import { Injectable } from '@angular/core'
6import { Router } from '@angular/router' 6import { Router } from '@angular/router'
7import { Notifier } from '@app/core/notification/notifier.service' 7import { Notifier } from '@app/core/notification/notifier.service'
8import { objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index' 8import { objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index'
9import { MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' 9import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models'
10import { environment } from '../../../environments/environment' 10import { environment } from '../../../environments/environment'
11import { RestExtractor } from '../rest/rest-extractor.service' 11import { RestExtractor } from '../rest/rest-extractor.service'
12import { AuthStatus } from './auth-status.model' 12import { AuthStatus } from './auth-status.model'
13import { AuthUser } from './auth-user.model' 13import { AuthUser } from './auth-user.model'
14import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
15 14
16interface UserLoginWithUsername extends UserLogin { 15interface UserLoginWithUsername extends UserLogin {
17 access_token: string 16 access_token: string
diff --git a/client/src/app/core/menu/menu.service.ts b/client/src/app/core/menu/menu.service.ts
index 60130382f..0b8d0191e 100644
--- a/client/src/app/core/menu/menu.service.ts
+++ b/client/src/app/core/menu/menu.service.ts
@@ -101,7 +101,7 @@ export class MenuService {
101 101
102 return { 102 return {
103 key: 'in-my-library', 103 key: 'in-my-library',
104 title: 'In my library', 104 title: $localize`In my library`,
105 links 105 links
106 } 106 }
107 } 107 }
diff --git a/client/src/app/core/renderer/markdown.service.ts b/client/src/app/core/renderer/markdown.service.ts
index ca1bf4eb9..36258ca98 100644
--- a/client/src/app/core/renderer/markdown.service.ts
+++ b/client/src/app/core/renderer/markdown.service.ts
@@ -1,6 +1,6 @@
1import * as MarkdownIt from 'markdown-it' 1import * as MarkdownIt from 'markdown-it'
2import { buildVideoLink } from 'src/assets/player/utils'
3import { Injectable } from '@angular/core' 2import { Injectable } from '@angular/core'
3import { buildVideoLink, decorateVideoLink } from '@shared/core-utils'
4import { 4import {
5 COMPLETE_RULES, 5 COMPLETE_RULES,
6 ENHANCED_RULES, 6 ENHANCED_RULES,
@@ -82,10 +82,14 @@ export class MarkdownService {
82 return this.render({ name: 'customPageMarkdownIt', markdown, withEmoji: true, additionalAllowedTags }) 82 return this.render({ name: 'customPageMarkdownIt', markdown, withEmoji: true, additionalAllowedTags })
83 } 83 }
84 84
85 processVideoTimestamps (html: string) { 85 processVideoTimestamps (videoShortUUID: string, html: string) {
86 return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) { 86 return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) {
87 const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0)) 87 const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0))
88 const url = buildVideoLink({ startTime: t }) 88
89 const url = decorateVideoLink({
90 url: buildVideoLink({ shortUUID: videoShortUUID }),
91 startTime: t
92 })
89 return `<a class="video-timestamp" href="${url}">${str}</a>` 93 return `<a class="video-timestamp" href="${url}">${str}</a>`
90 }) 94 })
91 } 95 }
diff --git a/client/src/app/core/rest/rest-extractor.service.ts b/client/src/app/core/rest/rest-extractor.service.ts
index 08ab49512..2a926e68f 100644
--- a/client/src/app/core/rest/rest-extractor.service.ts
+++ b/client/src/app/core/rest/rest-extractor.service.ts
@@ -2,8 +2,7 @@ import { throwError as observableThrowError } from 'rxjs'
2import { Injectable } from '@angular/core' 2import { Injectable } from '@angular/core'
3import { Router } from '@angular/router' 3import { Router } from '@angular/router'
4import { dateToHuman } from '@app/helpers' 4import { dateToHuman } from '@app/helpers'
5import { ResultList } from '@shared/models' 5import { HttpStatusCode, ResultList } from '@shared/models'
6import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
7 6
8@Injectable() 7@Injectable()
9export class RestExtractor { 8export class RestExtractor {
diff --git a/client/src/app/core/rest/rest.service.ts b/client/src/app/core/rest/rest.service.ts
index 1696e6709..98e45ffc0 100644
--- a/client/src/app/core/rest/rest.service.ts
+++ b/client/src/app/core/rest/rest.service.ts
@@ -44,13 +44,21 @@ export class RestService {
44 return newParams 44 return newParams
45 } 45 }
46 46
47 addArrayParams (params: HttpParams, name: string, values: (string | number)[]) {
48 for (const v of values) {
49 params = params.append(name, v)
50 }
51
52 return params
53 }
54
47 addObjectParams (params: HttpParams, object: { [ name: string ]: any }) { 55 addObjectParams (params: HttpParams, object: { [ name: string ]: any }) {
48 for (const name of Object.keys(object)) { 56 for (const name of Object.keys(object)) {
49 const value = object[name] 57 const value = object[name]
50 if (value === undefined || value === null) continue 58 if (value === undefined || value === null) continue
51 59
52 if (Array.isArray(value)) { 60 if (Array.isArray(value)) {
53 for (const v of value) params = params.append(name, v) 61 params = this.addArrayParams(params, name, value)
54 } else { 62 } else {
55 params = params.append(name, value) 63 params = params.append(name, value)
56 } 64 }
diff --git a/client/src/app/helpers/index.ts b/client/src/app/helpers/index.ts
index cc61255ba..beff749ec 100644
--- a/client/src/app/helpers/index.ts
+++ b/client/src/app/helpers/index.ts
@@ -1,5 +1,6 @@
1export * from './locales' 1export * from './locales'
2export * from './constants' 2export * from './constants'
3export * from './i18n-utils' 3export * from './i18n-utils'
4export * from './rxjs'
4export * from './utils' 5export * from './utils'
5export * from './zone' 6export * from './zone'
diff --git a/client/src/app/helpers/rxjs.ts b/client/src/app/helpers/rxjs.ts
new file mode 100644
index 000000000..eb051f868
--- /dev/null
+++ b/client/src/app/helpers/rxjs.ts
@@ -0,0 +1,29 @@
1import { uniq } from 'lodash-es'
2import { asyncScheduler, Observable } from 'rxjs'
3import { bufferTime, distinctUntilChanged, filter, map, observeOn, share, switchMap } from 'rxjs/operators'
4import { NgZone } from '@angular/core'
5import { enterZone, leaveZone } from './zone'
6
7function buildBulkObservable <T extends number | string, R> (options: {
8 ngZone: NgZone
9 notifierObservable: Observable<T>
10 time: number
11 bulkGet: (params: T[]) => Observable<R>
12}) {
13 const { ngZone, notifierObservable, time, bulkGet } = options
14
15 return notifierObservable.pipe(
16 distinctUntilChanged(),
17 // We leave Angular zone so Protractor does not get stuck
18 bufferTime(time, leaveZone(ngZone, asyncScheduler)),
19 filter(params => params.length !== 0),
20 map(params => uniq(params)),
21 observeOn(enterZone(ngZone, asyncScheduler)),
22 switchMap(params => bulkGet(params)),
23 share()
24 )
25}
26
27export {
28 buildBulkObservable
29}
diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts
index 94f6def26..edcaf50e0 100644
--- a/client/src/app/helpers/utils.ts
+++ b/client/src/app/helpers/utils.ts
@@ -3,7 +3,7 @@ import { SelectChannelItem } from 'src/types/select-options-item.model'
3import { DatePipe } from '@angular/common' 3import { DatePipe } from '@angular/common'
4import { HttpErrorResponse } from '@angular/common/http' 4import { HttpErrorResponse } from '@angular/common/http'
5import { Notifier } from '@app/core' 5import { Notifier } from '@app/core'
6import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '@shared/models'
7import { environment } from '../../environments/environment' 7import { environment } from '../../environments/environment'
8import { AuthService } from '../core/auth' 8import { AuthService } from '../core/auth'
9 9
diff --git a/client/src/app/shared/form-validators/batch-domains-validators.ts b/client/src/app/shared/form-validators/batch-domains-validators.ts
deleted file mode 100644
index 423d1337f..000000000
--- a/client/src/app/shared/form-validators/batch-domains-validators.ts
+++ /dev/null
@@ -1,60 +0,0 @@
1import { AbstractControl, FormControl, ValidatorFn, Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model'
3import { validateHost } from './host'
4
5export function getNotEmptyHosts (hosts: string) {
6 return hosts
7 .split('\n')
8 .filter((host: string) => host && host.length !== 0) // Eject empty hosts
9}
10
11const validDomains: ValidatorFn = (control: FormControl) => {
12 if (!control.value) return null
13
14 const newHostsErrors = []
15 const hosts = getNotEmptyHosts(control.value)
16
17 for (const host of hosts) {
18 if (validateHost(host) === false) {
19 newHostsErrors.push($localize`${host} is not valid`)
20 }
21 }
22
23 /* Is not valid. */
24 if (newHostsErrors.length !== 0) {
25 return {
26 'validDomains': {
27 reason: 'invalid',
28 value: newHostsErrors.join('. ') + '.'
29 }
30 }
31 }
32
33 /* Is valid. */
34 return null
35}
36
37const isHostsUnique: ValidatorFn = (control: AbstractControl) => {
38 if (!control.value) return null
39
40 const hosts = getNotEmptyHosts(control.value)
41
42 if (hosts.every((host: string) => hosts.indexOf(host) === hosts.lastIndexOf(host))) {
43 return null
44 } else {
45 return {
46 'uniqueDomains': {
47 reason: 'invalid'
48 }
49 }
50 }
51}
52
53export const DOMAINS_VALIDATOR: BuildFormValidator = {
54 VALIDATORS: [Validators.required, validDomains, isHostsUnique],
55 MESSAGES: {
56 'required': $localize`Domain is required.`,
57 'validDomains': $localize`Domains entered are invalid.`,
58 'uniqueDomains': $localize`Domains entered contain duplicates.`
59 }
60}
diff --git a/client/src/app/shared/form-validators/host-validators.ts b/client/src/app/shared/form-validators/host-validators.ts
new file mode 100644
index 000000000..6f410a50a
--- /dev/null
+++ b/client/src/app/shared/form-validators/host-validators.ts
@@ -0,0 +1,105 @@
1import { AbstractControl, ValidatorFn, Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model'
3
4export function validateHost (value: string) {
5 // Thanks to http://stackoverflow.com/a/106223
6 const HOST_REGEXP = new RegExp(
7 '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$'
8 )
9
10 return HOST_REGEXP.test(value)
11}
12
13export function validateHandle (value: string) {
14 if (!value) return false
15
16 return value.includes('@')
17}
18
19const validHosts: ValidatorFn = (control: AbstractControl) => {
20 if (!control.value) return null
21
22 const errors = []
23 const hosts = splitAndGetNotEmpty(control.value)
24
25 for (const host of hosts) {
26 if (validateHost(host) === false) {
27 errors.push($localize`${host} is not valid`)
28 }
29 }
30
31 // valid
32 if (errors.length === 0) return null
33
34 return {
35 'validHosts': {
36 reason: 'invalid',
37 value: errors.join('. ') + '.'
38 }
39 }
40}
41
42const validHostsOrHandles: ValidatorFn = (control: AbstractControl) => {
43 if (!control.value) return null
44
45 const errors = []
46 const lines = splitAndGetNotEmpty(control.value)
47
48 for (const line of lines) {
49 if (validateHost(line) === false && validateHandle(line) === false) {
50 errors.push($localize`${line} is not valid`)
51 }
52 }
53
54 // valid
55 if (errors.length === 0) return null
56
57 return {
58 'validHostsOrHandles': {
59 reason: 'invalid',
60 value: errors.join('. ') + '.'
61 }
62 }
63}
64
65// ---------------------------------------------------------------------------
66
67export function splitAndGetNotEmpty (value: string) {
68 return value
69 .split('\n')
70 .filter(line => line && line.length !== 0) // Eject empty hosts
71}
72
73export const unique: ValidatorFn = (control: AbstractControl) => {
74 if (!control.value) return null
75
76 const hosts = splitAndGetNotEmpty(control.value)
77
78 if (hosts.every((host: string) => hosts.indexOf(host) === hosts.lastIndexOf(host))) {
79 return null
80 }
81
82 return {
83 'unique': {
84 reason: 'invalid'
85 }
86 }
87}
88
89export const UNIQUE_HOSTS_VALIDATOR: BuildFormValidator = {
90 VALIDATORS: [ Validators.required, validHosts, unique ],
91 MESSAGES: {
92 'required': $localize`Domain is required.`,
93 'validHosts': $localize`Hosts entered are invalid.`,
94 'unique': $localize`Hosts entered contain duplicates.`
95 }
96}
97
98export const UNIQUE_HOSTS_OR_HANDLE_VALIDATOR: BuildFormValidator = {
99 VALIDATORS: [ Validators.required, validHostsOrHandles, unique ],
100 MESSAGES: {
101 'required': $localize`Domain is required.`,
102 'validHostsOrHandles': $localize`Hosts or handles are invalid.`,
103 'unique': $localize`Hosts or handles contain duplicates.`
104 }
105}
diff --git a/client/src/app/shared/form-validators/host.ts b/client/src/app/shared/form-validators/host.ts
deleted file mode 100644
index c18a35f9b..000000000
--- a/client/src/app/shared/form-validators/host.ts
+++ /dev/null
@@ -1,8 +0,0 @@
1export function validateHost (value: string) {
2 // Thanks to http://stackoverflow.com/a/106223
3 const HOST_REGEXP = new RegExp(
4 '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$'
5 )
6
7 return HOST_REGEXP.test(value)
8}
diff --git a/client/src/app/shared/form-validators/index.ts b/client/src/app/shared/form-validators/index.ts
index f621f03a4..0b605719c 100644
--- a/client/src/app/shared/form-validators/index.ts
+++ b/client/src/app/shared/form-validators/index.ts
@@ -1,7 +1,6 @@
1export * from './form-validator.model' 1export * from './form-validator.model'
2export * from './host'
3 2
4// Don't re export const variables because webpack 4 cannot do tree shaking with them 3// Don't re export const variables because webpack cannot do tree shaking with them
5// export * from './abuse-validators' 4// export * from './abuse-validators'
6// export * from './batch-domains-validators' 5// export * from './batch-domains-validators'
7// export * from './custom-config-validators' 6// export * from './custom-config-validators'
diff --git a/client/src/app/shared/form-validators/video-channel-validators.ts b/client/src/app/shared/form-validators/video-channel-validators.ts
index 0daab22ce..ba502ed01 100644
--- a/client/src/app/shared/form-validators/video-channel-validators.ts
+++ b/client/src/app/shared/form-validators/video-channel-validators.ts
@@ -1,13 +1,11 @@
1import { Validators } from '@angular/forms' 1import { Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model' 2import { BuildFormValidator } from './form-validator.model'
3import { USER_USERNAME_VALIDATOR } from './user-validators'
3 4
4export const VIDEO_CHANNEL_NAME_VALIDATOR: BuildFormValidator = { 5export const VIDEO_CHANNEL_NAME_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [ 6 // Use the same constraints than user usernmae
6 Validators.required, 7 VALIDATORS: USER_USERNAME_VALIDATOR.VALIDATORS,
7 Validators.minLength(1), 8
8 Validators.maxLength(50),
9 Validators.pattern(/^[a-z0-9][a-z0-9._]*$/)
10 ],
11 MESSAGES: { 9 MESSAGES: {
12 'required': $localize`Name is required.`, 10 'required': $localize`Name is required.`,
13 'minlength': $localize`Name must be at least 1 character long.`, 11 'minlength': $localize`Name must be at least 1 character long.`,
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
index 67aa0e399..a7932ebab 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
@@ -1,7 +1,7 @@
1import * as debug from 'debug' 1import * as debug from 'debug'
2import truncate from 'lodash-es/truncate' 2import truncate from 'lodash-es/truncate'
3import { SortMeta } from 'primeng/api' 3import { SortMeta } from 'primeng/api'
4import { buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' 4import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
5import { environment } from 'src/environments/environment' 5import { environment } from 'src/environments/environment'
6import { Component, Input, OnInit, ViewChild } from '@angular/core' 6import { Component, Input, OnInit, ViewChild } from '@angular/core'
7import { DomSanitizer } from '@angular/platform-browser' 7import { DomSanitizer } from '@angular/platform-browser'
@@ -10,6 +10,7 @@ import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable }
10import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' 10import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
11import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation' 11import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation'
12import { VideoCommentService } from '@app/shared/shared-video-comment' 12import { VideoCommentService } from '@app/shared/shared-video-comment'
13import { buildVideoEmbedLink, decorateVideoLink } from '@shared/core-utils'
13import { AbuseState, AdminAbuse } from '@shared/models' 14import { AbuseState, AdminAbuse } from '@shared/models'
14import { AdvancedInputFilter } from '../shared-forms' 15import { AdvancedInputFilter } from '../shared-forms'
15import { AbuseMessageModalComponent } from './abuse-message-modal.component' 16import { AbuseMessageModalComponent } from './abuse-message-modal.component'
@@ -129,8 +130,8 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
129 130
130 getVideoEmbed (abuse: AdminAbuse) { 131 getVideoEmbed (abuse: AdminAbuse) {
131 return buildVideoOrPlaylistEmbed( 132 return buildVideoOrPlaylistEmbed(
132 buildVideoLink({ 133 decorateVideoLink({
133 baseUrl: `${environment.originServerUrl}/videos/embed/${abuse.video.uuid}`, 134 url: buildVideoEmbedLink(abuse.video, environment.originServerUrl),
134 title: false, 135 title: false,
135 warningTitle: false, 136 warningTitle: false,
136 startTime: abuse.video.startAt, 137 startTime: abuse.video.startAt,
diff --git a/client/src/app/shared/shared-custom-markup/custom-markup.service.ts b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts
index 231e52d0a..c9d33980e 100644
--- a/client/src/app/shared/shared-custom-markup/custom-markup.service.ts
+++ b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts
@@ -65,15 +65,15 @@ export class CustomMarkupService {
65 65
66 for (const selector of Object.keys(this.htmlBuilders)) { 66 for (const selector of Object.keys(this.htmlBuilders)) {
67 rootElement.querySelectorAll(selector) 67 rootElement.querySelectorAll(selector)
68 .forEach((e: HTMLElement) => { 68 .forEach((e: HTMLElement) => {
69 try { 69 try {
70 const element = this.execHTMLBuilder(selector, e) 70 const element = this.execHTMLBuilder(selector, e)
71 // Insert as first child 71 // Insert as first child
72 e.insertBefore(element, e.firstChild) 72 e.insertBefore(element, e.firstChild)
73 } catch (err) { 73 } catch (err) {
74 console.error('Cannot inject component %s.', selector, err) 74 console.error('Cannot inject component %s.', selector, err)
75 } 75 }
76 }) 76 })
77 } 77 }
78 78
79 const loadedPromises: Promise<boolean>[] = [] 79 const loadedPromises: Promise<boolean>[] = []
@@ -191,6 +191,8 @@ export class CustomMarkupService {
191 accountHandle: data.accountHandle || undefined, 191 accountHandle: data.accountHandle || undefined,
192 channelHandle: data.channelHandle || undefined, 192 channelHandle: data.channelHandle || undefined,
193 193
194 isLive: this.buildBoolean(data.isLive),
195
194 filter: this.buildBoolean(data.onlyLocal) ? 'local' as VideoFilter : undefined 196 filter: this.buildBoolean(data.onlyLocal) ? 'local' as VideoFilter : undefined
195 } 197 }
196 198
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts
index 7043a7ec9..5bb045a82 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts
@@ -2,8 +2,9 @@ import { from } from 'rxjs'
2import { finalize, map, switchMap, tap } from 'rxjs/operators' 2import { finalize, map, switchMap, tap } from 'rxjs/operators'
3import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' 3import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
4import { MarkdownService, Notifier, UserService } from '@app/core' 4import { MarkdownService, Notifier, UserService } from '@app/core'
5import { FindInBulkService } from '@app/shared/shared-search'
5import { Video, VideoSortField } from '@shared/models/videos' 6import { Video, VideoSortField } from '@shared/models/videos'
6import { VideoChannel, VideoChannelService, VideoService } from '../../shared-main' 7import { VideoChannel, VideoService } from '../../shared-main'
7import { CustomMarkupComponent } from './shared' 8import { CustomMarkupComponent } from './shared'
8 9
9/* 10/*
@@ -29,14 +30,14 @@ export class ChannelMiniatureMarkupComponent implements CustomMarkupComponent, O
29 30
30 constructor ( 31 constructor (
31 private markdown: MarkdownService, 32 private markdown: MarkdownService,
32 private channelService: VideoChannelService, 33 private findInBulk: FindInBulkService,
33 private videoService: VideoService, 34 private videoService: VideoService,
34 private userService: UserService, 35 private userService: UserService,
35 private notifier: Notifier 36 private notifier: Notifier
36 ) { } 37 ) { }
37 38
38 ngOnInit () { 39 ngOnInit () {
39 this.channelService.getVideoChannel(this.name) 40 this.findInBulk.getChannel(this.name)
40 .pipe( 41 .pipe(
41 tap(channel => this.channel = channel), 42 tap(channel => this.channel = channel),
42 switchMap(() => from(this.markdown.textMarkdownToHTML(this.channel.description))), 43 switchMap(() => from(this.markdown.textMarkdownToHTML(this.channel.description))),
@@ -49,7 +50,7 @@ export class ChannelMiniatureMarkupComponent implements CustomMarkupComponent, O
49 this.video = data[0] 50 this.video = data[0]
50 }, 51 },
51 52
52 err => this.notifier.error('Error in channel miniature component: ' + err.message) 53 err => this.notifier.error($localize`Error in channel miniature component: ${err.message}`)
53 ) 54 )
54 } 55 }
55 56
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/embed-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/embed-markup.component.ts
index 4462903db..53b70cc47 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/embed-markup.component.ts
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/embed-markup.component.ts
@@ -1,6 +1,7 @@
1import { buildPlaylistLink, buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' 1import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
2import { environment } from 'src/environments/environment' 2import { environment } from 'src/environments/environment'
3import { Component, ElementRef, Input, OnInit } from '@angular/core' 3import { Component, ElementRef, Input, OnInit } from '@angular/core'
4import { buildPlaylistEmbedLink, buildVideoEmbedLink } from '@shared/core-utils'
4import { CustomMarkupComponent } from './shared' 5import { CustomMarkupComponent } from './shared'
5 6
6@Component({ 7@Component({
@@ -17,8 +18,8 @@ export class EmbedMarkupComponent implements CustomMarkupComponent, OnInit {
17 18
18 ngOnInit () { 19 ngOnInit () {
19 const link = this.type === 'video' 20 const link = this.type === 'video'
20 ? buildVideoLink({ baseUrl: `${environment.originServerUrl}/videos/embed/${this.uuid}` }) 21 ? buildVideoEmbedLink({ uuid: this.uuid }, environment.originServerUrl)
21 : buildPlaylistLink({ baseUrl: `${environment.originServerUrl}/video-playlists/embed/${this.uuid}` }) 22 : buildPlaylistEmbedLink({ uuid: this.uuid }, environment.originServerUrl)
22 23
23 this.el.nativeElement.innerHTML = buildVideoOrPlaylistEmbed(link, this.uuid) 24 this.el.nativeElement.innerHTML = buildVideoOrPlaylistEmbed(link, this.uuid)
24 } 25 }
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts
index ff8cc01db..5a5c34867 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts
@@ -1,8 +1,9 @@
1import { finalize } from 'rxjs/operators' 1import { finalize } from 'rxjs/operators'
2import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' 2import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { FindInBulkService } from '@app/shared/shared-search'
4import { MiniatureDisplayOptions } from '../../shared-video-miniature' 5import { MiniatureDisplayOptions } from '../../shared-video-miniature'
5import { VideoPlaylist, VideoPlaylistService } from '../../shared-video-playlist' 6import { VideoPlaylist } from '../../shared-video-playlist'
6import { CustomMarkupComponent } from './shared' 7import { CustomMarkupComponent } from './shared'
7 8
8/* 9/*
@@ -33,17 +34,17 @@ export class PlaylistMiniatureMarkupComponent implements CustomMarkupComponent,
33 } 34 }
34 35
35 constructor ( 36 constructor (
36 private playlistService: VideoPlaylistService, 37 private findInBulkService: FindInBulkService,
37 private notifier: Notifier 38 private notifier: Notifier
38 ) { } 39 ) { }
39 40
40 ngOnInit () { 41 ngOnInit () {
41 this.playlistService.getVideoPlaylist(this.uuid) 42 this.findInBulkService.getPlaylist(this.uuid)
42 .pipe(finalize(() => this.loaded.emit(true))) 43 .pipe(finalize(() => this.loaded.emit(true)))
43 .subscribe( 44 .subscribe(
44 playlist => this.playlist = playlist, 45 playlist => this.playlist = playlist,
45 46
46 err => this.notifier.error('Error in playlist miniature component: ' + err.message) 47 err => this.notifier.error($localize`Error in playlist miniature component: ${err.message}`)
47 ) 48 )
48 } 49 }
49} 50}
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts
index 47518abfd..84c936ee7 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts
@@ -4,6 +4,7 @@ import { AuthService, Notifier } from '@app/core'
4import { Video, VideoService } from '../../shared-main' 4import { Video, VideoService } from '../../shared-main'
5import { MiniatureDisplayOptions } from '../../shared-video-miniature' 5import { MiniatureDisplayOptions } from '../../shared-video-miniature'
6import { CustomMarkupComponent } from './shared' 6import { CustomMarkupComponent } from './shared'
7import { FindInBulkService } from '@app/shared/shared-search'
7 8
8/* 9/*
9 * Markup component that creates a video miniature only 10 * Markup component that creates a video miniature only
@@ -35,7 +36,7 @@ export class VideoMiniatureMarkupComponent implements CustomMarkupComponent, OnI
35 36
36 constructor ( 37 constructor (
37 private auth: AuthService, 38 private auth: AuthService,
38 private videoService: VideoService, 39 private findInBulk: FindInBulkService,
39 private notifier: Notifier 40 private notifier: Notifier
40 ) { } 41 ) { }
41 42
@@ -50,12 +51,12 @@ export class VideoMiniatureMarkupComponent implements CustomMarkupComponent, OnI
50 } 51 }
51 } 52 }
52 53
53 this.videoService.getVideo({ videoId: this.uuid }) 54 this.findInBulk.getVideo(this.uuid)
54 .pipe(finalize(() => this.loaded.emit(true))) 55 .pipe(finalize(() => this.loaded.emit(true)))
55 .subscribe( 56 .subscribe(
56 video => this.video = video, 57 video => this.video = video,
57 58
58 err => this.notifier.error('Error in video miniature component: ' + err.message) 59 err => this.notifier.error($localize`Error in video miniature component: ${err.message}`)
59 ) 60 )
60 } 61 }
61} 62}
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts
index d9f77802b..6473e9ba0 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts
@@ -22,6 +22,7 @@ export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit
22 @Input() count: number 22 @Input() count: number
23 @Input() onlyDisplayTitle: boolean 23 @Input() onlyDisplayTitle: boolean
24 @Input() filter: VideoFilter 24 @Input() filter: VideoFilter
25 @Input() isLive: boolean
25 @Input() maxRows: number 26 @Input() maxRows: number
26 @Input() channelHandle: string 27 @Input() channelHandle: string
27 @Input() accountHandle: string 28 @Input() accountHandle: string
@@ -73,7 +74,7 @@ export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit
73 .subscribe( 74 .subscribe(
74 ({ data }) => this.videos = data, 75 ({ data }) => this.videos = data,
75 76
76 err => this.notifier.error('Error in videos list component: ' + err.message) 77 err => this.notifier.error($localize`Error in videos list component: ${err.message}`)
77 ) 78 )
78 } 79 }
79 80
@@ -86,6 +87,7 @@ export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit
86 categoryOneOf: this.categoryOneOf, 87 categoryOneOf: this.categoryOneOf,
87 languageOneOf: this.languageOneOf, 88 languageOneOf: this.languageOneOf,
88 filter: this.filter, 89 filter: this.filter,
90 isLive: this.isLive,
89 sort: this.sort as VideoSortField, 91 sort: this.sort as VideoSortField,
90 account: { nameWithHost: this.accountHandle }, 92 account: { nameWithHost: this.accountHandle },
91 videoChannel: { nameWithHost: this.channelHandle } 93 videoChannel: { nameWithHost: this.channelHandle }
diff --git a/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts b/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts
index dccd64709..27e976d13 100644
--- a/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts
+++ b/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts
@@ -3,6 +3,7 @@ import { NgModule } from '@angular/core'
3import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module' 3import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
4import { SharedGlobalIconModule } from '../shared-icons' 4import { SharedGlobalIconModule } from '../shared-icons'
5import { SharedMainModule } from '../shared-main' 5import { SharedMainModule } from '../shared-main'
6import { SharedSearchModule } from '../shared-search'
6import { SharedVideoMiniatureModule } from '../shared-video-miniature' 7import { SharedVideoMiniatureModule } from '../shared-video-miniature'
7import { SharedVideoPlaylistModule } from '../shared-video-playlist' 8import { SharedVideoPlaylistModule } from '../shared-video-playlist'
8import { CustomMarkupContainerComponent } from './custom-markup-container.component' 9import { CustomMarkupContainerComponent } from './custom-markup-container.component'
@@ -26,7 +27,8 @@ import {
26 SharedGlobalIconModule, 27 SharedGlobalIconModule,
27 SharedVideoMiniatureModule, 28 SharedVideoMiniatureModule,
28 SharedVideoPlaylistModule, 29 SharedVideoPlaylistModule,
29 SharedActorImageModule 30 SharedActorImageModule,
31 SharedSearchModule
30 ], 32 ],
31 33
32 declarations: [ 34 declarations: [
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.ts b/client/src/app/shared/shared-forms/markdown-textarea.component.ts
index a233a4205..8f51d47df 100644
--- a/client/src/app/shared/shared-forms/markdown-textarea.component.ts
+++ b/client/src/app/shared/shared-forms/markdown-textarea.component.ts
@@ -6,6 +6,7 @@ import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@an
6import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 6import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
7import { SafeHtml } from '@angular/platform-browser' 7import { SafeHtml } from '@angular/platform-browser'
8import { MarkdownService, ScreenService } from '@app/core' 8import { MarkdownService, ScreenService } from '@app/core'
9import { Video } from '@shared/models'
9 10
10@Component({ 11@Component({
11 selector: 'my-markdown-textarea', 12 selector: 'my-markdown-textarea',
@@ -33,7 +34,7 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
33 @Input() markdownType: 'text' | 'enhanced' = 'text' 34 @Input() markdownType: 'text' | 'enhanced' = 'text'
34 @Input() customMarkdownRenderer?: (text: string) => Promise<string | HTMLElement> 35 @Input() customMarkdownRenderer?: (text: string) => Promise<string | HTMLElement>
35 36
36 @Input() markdownVideo = false 37 @Input() markdownVideo: Video
37 38
38 @Input() name = 'description' 39 @Input() name = 'description'
39 40
@@ -147,7 +148,7 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
147 } 148 }
148 149
149 if (this.markdownVideo) { 150 if (this.markdownVideo) {
150 html = this.markdownService.processVideoTimestamps(html) 151 html = this.markdownService.processVideoTimestamps(this.markdownVideo.shortUUID, html)
151 } 152 }
152 153
153 return html 154 return html
diff --git a/client/src/app/shared/shared-forms/timestamp-input.component.ts b/client/src/app/shared/shared-forms/timestamp-input.component.ts
index 0ffd03d02..3fc705905 100644
--- a/client/src/app/shared/shared-forms/timestamp-input.component.ts
+++ b/client/src/app/shared/shared-forms/timestamp-input.component.ts
@@ -1,6 +1,6 @@
1import { ChangeDetectorRef, Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core' 1import { ChangeDetectorRef, Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { secondsToTime, timeToInt } from '../../../assets/player/utils' 3import { secondsToTime, timeToInt } from '@shared/core-utils'
4 4
5@Component({ 5@Component({
6 selector: 'my-timestamp-input', 6 selector: 'my-timestamp-input',
diff --git a/client/src/app/shared/shared-instance/instance-follow.service.ts b/client/src/app/shared/shared-instance/instance-follow.service.ts
index e52660140..af44020cf 100644
--- a/client/src/app/shared/shared-instance/instance-follow.service.ts
+++ b/client/src/app/shared/shared-instance/instance-follow.service.ts
@@ -4,7 +4,7 @@ import { catchError, map } from 'rxjs/operators'
4import { HttpClient, HttpParams } from '@angular/common/http' 4import { HttpClient, HttpParams } from '@angular/common/http'
5import { Injectable } from '@angular/core' 5import { Injectable } from '@angular/core'
6import { RestExtractor, RestPagination, RestService } from '@app/core' 6import { RestExtractor, RestPagination, RestService } from '@app/core'
7import { ActivityPubActorType, ActorFollow, FollowState, ResultList } from '@shared/models' 7import { ActivityPubActorType, ActorFollow, FollowState, ResultList, ServerFollowCreate } from '@shared/models'
8import { environment } from '../../../environments/environment' 8import { environment } from '../../../environments/environment'
9 9
10@Injectable() 10@Injectable()
@@ -64,9 +64,10 @@ export class InstanceFollowService {
64 ) 64 )
65 } 65 }
66 66
67 follow (notEmptyHosts: string[]) { 67 follow (hostsOrHandles: string[]) {
68 const body = { 68 const body: ServerFollowCreate = {
69 hosts: notEmptyHosts 69 handles: hostsOrHandles.filter(v => v.includes('@')),
70 hosts: hostsOrHandles.filter(v => !v.includes('@'))
70 } 71 }
71 72
72 return this.authHttp.post(InstanceFollowService.BASE_APPLICATION_URL + '/following', body) 73 return this.authHttp.post(InstanceFollowService.BASE_APPLICATION_URL + '/following', body)
@@ -77,7 +78,9 @@ export class InstanceFollowService {
77 } 78 }
78 79
79 unfollow (follow: ActorFollow) { 80 unfollow (follow: ActorFollow) {
80 return this.authHttp.delete(InstanceFollowService.BASE_APPLICATION_URL + '/following/' + follow.following.host) 81 const handle = follow.following.name + '@' + follow.following.host
82
83 return this.authHttp.delete(InstanceFollowService.BASE_APPLICATION_URL + '/following/' + handle)
81 .pipe( 84 .pipe(
82 map(this.restExtractor.extractDataBool), 85 map(this.restExtractor.extractDataBool),
83 catchError(res => this.restExtractor.handleError(res)) 86 catchError(res => this.restExtractor.handleError(res))
diff --git a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
index 5bcad36d0..a75c8a25c 100644
--- a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
+++ b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
@@ -1,11 +1,11 @@
1import { Observable, of, throwError as observableThrowError } from 'rxjs' 1import { Observable, of, throwError as observableThrowError } from 'rxjs'
2import { catchError, switchMap } from 'rxjs/operators' 2import { catchError, switchMap } from 'rxjs/operators'
3import { HTTP_INTERCEPTORS, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpErrorResponse } from '@angular/common/http' 3import { HTTP_INTERCEPTORS, HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'
4import { Injectable, Injector } from '@angular/core' 4import { Injectable, Injector } from '@angular/core'
5import { AuthService } from '@app/core/auth/auth.service'
6import { Router } from '@angular/router' 5import { Router } from '@angular/router'
7import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 6import { AuthService } from '@app/core/auth/auth.service'
8import { OAuth2ErrorCode, PeerTubeProblemDocument, ServerErrorCode } from '@shared/models/server' 7import { HttpStatusCode } from '@shared/models'
8import { OAuth2ErrorCode, PeerTubeProblemDocument } from '@shared/models/server'
9 9
10@Injectable() 10@Injectable()
11export class AuthInterceptor implements HttpInterceptor { 11export class AuthInterceptor implements HttpInterceptor {
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts
index 4c15eb981..439547102 100644
--- a/client/src/app/shared/shared-main/users/user-notification.model.ts
+++ b/client/src/app/shared/shared-main/users/user-notification.model.ts
@@ -47,11 +47,7 @@ export class UserNotification implements UserNotificationServer {
47 comment?: { 47 comment?: {
48 threadId: number 48 threadId: number
49 49
50 video: { 50 video: VideoInfo
51 id: number
52 uuid: string
53 name: string
54 }
55 } 51 }
56 52
57 account?: ActorInfo 53 account?: ActorInfo
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.ts b/client/src/app/shared/shared-main/users/user-notifications.component.ts
index d7c722355..96b141543 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.ts
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.ts
@@ -1,7 +1,7 @@
1import { Subject } from 'rxjs' 1import { Subject } from 'rxjs'
2import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' 2import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
3import { ComponentPagination, hasMoreItems, Notifier } from '@app/core' 3import { ComponentPagination, hasMoreItems, Notifier } from '@app/core'
4import { UserNotificationType, AbuseState } from '@shared/models' 4import { AbuseState } from '@shared/models'
5import { UserNotification } from './user-notification.model' 5import { UserNotification } from './user-notification.model'
6import { UserNotificationService } from './user-notification.service' 6import { UserNotificationService } from './user-notification.service'
7 7
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts
index f0a4a3f37..b7720c8d2 100644
--- a/client/src/app/shared/shared-main/video/video.model.ts
+++ b/client/src/app/shared/shared-main/video/video.model.ts
@@ -2,6 +2,7 @@ import { AuthUser } from '@app/core'
2import { User } from '@app/core/users/user.model' 2import { User } from '@app/core/users/user.model'
3import { durationToString, getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers' 3import { durationToString, getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers'
4import { Actor } from '@app/shared/shared-main/account/actor.model' 4import { Actor } from '@app/shared/shared-main/account/actor.model'
5import { buildVideoWatchPath } from '@shared/core-utils'
5import { peertubeTranslate } from '@shared/core-utils/i18n' 6import { peertubeTranslate } from '@shared/core-utils/i18n'
6import { 7import {
7 ActorImage, 8 ActorImage,
@@ -92,7 +93,7 @@ export class Video implements VideoServerModel {
92 pluginData?: any 93 pluginData?: any
93 94
94 static buildWatchUrl (video: Partial<Pick<Video, 'uuid' | 'shortUUID'>>) { 95 static buildWatchUrl (video: Partial<Pick<Video, 'uuid' | 'shortUUID'>>) {
95 return '/w/' + (video.shortUUID || video.uuid) 96 return buildVideoWatchPath({ shortUUID: video.shortUUID || video.uuid })
96 } 97 }
97 98
98 static buildUpdateUrl (video: Pick<Video, 'uuid'>) { 99 static buildUpdateUrl (video: Pick<Video, 'uuid'>) {
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index 04a39be0e..4a97719fa 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -210,15 +210,16 @@ export class VideoService implements VideosProvider {
210 } 210 }
211 211
212 getVideos (parameters: { 212 getVideos (parameters: {
213 videoPagination: ComponentPaginationLight, 213 videoPagination: ComponentPaginationLight
214 sort: VideoSortField, 214 sort: VideoSortField
215 filter?: VideoFilter, 215 filter?: VideoFilter
216 categoryOneOf?: number[], 216 categoryOneOf?: number[]
217 languageOneOf?: string[], 217 languageOneOf?: string[]
218 skipCount?: boolean, 218 isLive?: boolean
219 skipCount?: boolean
219 nsfwPolicy?: NSFWPolicyType 220 nsfwPolicy?: NSFWPolicyType
220 }): Observable<ResultList<Video>> { 221 }): Observable<ResultList<Video>> {
221 const { videoPagination, sort, filter, categoryOneOf, languageOneOf, skipCount, nsfwPolicy } = parameters 222 const { videoPagination, sort, filter, categoryOneOf, languageOneOf, skipCount, nsfwPolicy, isLive } = parameters
222 223
223 const pagination = this.restService.componentPaginationToRestPagination(videoPagination) 224 const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
224 225
@@ -228,21 +229,10 @@ export class VideoService implements VideosProvider {
228 if (filter) params = params.set('filter', filter) 229 if (filter) params = params.set('filter', filter)
229 if (skipCount) params = params.set('skipCount', skipCount + '') 230 if (skipCount) params = params.set('skipCount', skipCount + '')
230 231
231 if (nsfwPolicy) { 232 if (isLive) params = params.set('isLive', isLive)
232 params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy)) 233 if (nsfwPolicy) params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
233 } 234 if (languageOneOf) this.restService.addArrayParams(params, 'languageOneOf', languageOneOf)
234 235 if (categoryOneOf) this.restService.addArrayParams(params, 'categoryOneOf', categoryOneOf)
235 if (languageOneOf) {
236 for (const l of languageOneOf) {
237 params = params.append('languageOneOf[]', l)
238 }
239 }
240
241 if (categoryOneOf) {
242 for (const c of categoryOneOf) {
243 params = params.append('categoryOneOf[]', c + '')
244 }
245 }
246 236
247 return this.authHttp 237 return this.authHttp
248 .get<ResultList<Video>>(VideoService.BASE_VIDEO_URL, { params }) 238 .get<ResultList<Video>>(VideoService.BASE_VIDEO_URL, { params })
diff --git a/client/src/app/shared/shared-moderation/batch-domains-modal.component.html b/client/src/app/shared/shared-moderation/batch-domains-modal.component.html
index 6a3c65721..8306a96bc 100644
--- a/client/src/app/shared/shared-moderation/batch-domains-modal.component.html
+++ b/client/src/app/shared/shared-moderation/batch-domains-modal.component.html
@@ -1,6 +1,6 @@
1<ng-template #modal> 1<ng-template #modal>
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">{{ action }}</h4> 3 <h4 class="modal-title">{{ action }}</h4>
4 4
5 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> 5 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
6 </div> 6 </div>
@@ -11,15 +11,15 @@
11 <label i18n for="hosts">1 host (without "http://") per line</label> 11 <label i18n for="hosts">1 host (without "http://") per line</label>
12 12
13 <textarea 13 <textarea
14 [placeholder]="placeholder" formControlName="domains" type="text" id="hosts" name="hosts" 14 [placeholder]="placeholder" formControlName="hosts" type="text" id="hosts" name="hosts"
15 class="form-control" [ngClass]="{ 'input-error': formErrors['domains'] }" ngbAutofocus 15 class="form-control" [ngClass]="{ 'input-error': formErrors['hosts'] }" ngbAutofocus
16 ></textarea> 16 ></textarea>
17 17
18 <div *ngIf="formErrors.domains" class="form-error"> 18 <div *ngIf="formErrors.hosts" class="form-error">
19 {{ formErrors.domains }} 19 {{ formErrors.hosts }}
20 20
21 <div *ngIf="form.controls['domains'].errors.validDomains"> 21 <div *ngIf="form.controls['hosts'].errors.validHosts">
22 {{ form.controls['domains'].errors.validDomains.value }} 22 {{ form.controls['hosts'].errors.validHosts.value }}
23 </div> 23 </div>
24 </div> 24 </div>
25 </div> 25 </div>
diff --git a/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts b/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts
index 6edbb6023..20be728f6 100644
--- a/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts
+++ b/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts
@@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angu
2import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 2import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 4import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
5import { DOMAINS_VALIDATOR, getNotEmptyHosts } from '../form-validators/batch-domains-validators' 5import { splitAndGetNotEmpty, UNIQUE_HOSTS_VALIDATOR } from '../form-validators/host-validators'
6 6
7@Component({ 7@Component({
8 selector: 'my-batch-domains-modal', 8 selector: 'my-batch-domains-modal',
@@ -28,7 +28,7 @@ export class BatchDomainsModalComponent extends FormReactive implements OnInit {
28 if (!this.action) this.action = $localize`Process domains` 28 if (!this.action) this.action = $localize`Process domains`
29 29
30 this.buildForm({ 30 this.buildForm({
31 domains: DOMAINS_VALIDATOR 31 hosts: UNIQUE_HOSTS_VALIDATOR
32 }) 32 })
33 } 33 }
34 34
@@ -41,9 +41,7 @@ export class BatchDomainsModalComponent extends FormReactive implements OnInit {
41 } 41 }
42 42
43 submit () { 43 submit () {
44 this.domains.emit( 44 this.domains.emit(splitAndGetNotEmpty(this.form.controls['hosts'].value))
45 getNotEmptyHosts(this.form.controls['domains'].value)
46 )
47 this.form.reset() 45 this.form.reset()
48 this.hide() 46 this.hide()
49 } 47 }
diff --git a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
index 4ca6f52ad..e509ac88f 100644
--- a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
+++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
@@ -1,5 +1,5 @@
1import { mapValues, pickBy } from 'lodash-es' 1import { mapValues, pickBy } from 'lodash-es'
2import { buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' 2import { buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
3import { Component, Input, OnInit, ViewChild } from '@angular/core' 3import { Component, Input, OnInit, ViewChild } from '@angular/core'
4import { DomSanitizer, SafeHtml } from '@angular/platform-browser' 4import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
5import { Notifier } from '@app/core' 5import { Notifier } from '@app/core'
@@ -7,6 +7,7 @@ import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-valida
7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
8import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 8import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
9import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 9import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
10import { decorateVideoLink } from '@shared/core-utils'
10import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' 11import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse'
11import { AbusePredefinedReasonsString } from '@shared/models' 12import { AbusePredefinedReasonsString } from '@shared/models'
12import { Video } from '../../shared-main' 13import { Video } from '../../shared-main'
@@ -57,11 +58,12 @@ export class VideoReportComponent extends FormReactive implements OnInit {
57 getVideoEmbed () { 58 getVideoEmbed () {
58 return this.sanitizer.bypassSecurityTrustHtml( 59 return this.sanitizer.bypassSecurityTrustHtml(
59 buildVideoOrPlaylistEmbed( 60 buildVideoOrPlaylistEmbed(
60 buildVideoLink({ 61 decorateVideoLink({
61 baseUrl: this.video.embedUrl, 62 url: this.video.embedUrl,
62 title: false, 63 title: false,
63 warningTitle: false 64 warningTitle: false
64 }), 65 }),
66
65 this.video.name 67 this.video.name
66 ) 68 )
67 ) 69 )
diff --git a/client/src/app/shared/shared-search/advanced-search.model.ts b/client/src/app/shared/shared-search/advanced-search.model.ts
index 2c83f53b6..9c55f6cd8 100644
--- a/client/src/app/shared/shared-search/advanced-search.model.ts
+++ b/client/src/app/shared/shared-search/advanced-search.model.ts
@@ -1,4 +1,11 @@
1import { BooleanBothQuery, BooleanQuery, SearchTargetType, VideosSearchQuery } from '@shared/models' 1import {
2 BooleanBothQuery,
3 BooleanQuery,
4 SearchTargetType,
5 VideoChannelsSearchQuery,
6 VideoPlaylistsSearchQuery,
7 VideosSearchQuery
8} from '@shared/models'
2 9
3export class AdvancedSearch { 10export class AdvancedSearch {
4 startDate: string // ISO 8601 11 startDate: string // ISO 8601
@@ -23,6 +30,8 @@ export class AdvancedSearch {
23 30
24 isLive: BooleanQuery 31 isLive: BooleanQuery
25 32
33 host: string
34
26 sort: string 35 sort: string
27 36
28 searchTarget: SearchTargetType 37 searchTarget: SearchTargetType
@@ -45,6 +54,8 @@ export class AdvancedSearch {
45 54
46 isLive?: BooleanQuery 55 isLive?: BooleanQuery
47 56
57 host?: string
58
48 durationMin?: string 59 durationMin?: string
49 durationMax?: string 60 durationMax?: string
50 sort?: string 61 sort?: string
@@ -68,6 +79,8 @@ export class AdvancedSearch {
68 this.durationMin = parseInt(options.durationMin, 10) 79 this.durationMin = parseInt(options.durationMin, 10)
69 this.durationMax = parseInt(options.durationMax, 10) 80 this.durationMax = parseInt(options.durationMax, 10)
70 81
82 this.host = options.host || undefined
83
71 this.searchTarget = options.searchTarget || undefined 84 this.searchTarget = options.searchTarget || undefined
72 85
73 if (isNaN(this.durationMin)) this.durationMin = undefined 86 if (isNaN(this.durationMin)) this.durationMin = undefined
@@ -101,6 +114,7 @@ export class AdvancedSearch {
101 this.durationMin = undefined 114 this.durationMin = undefined
102 this.durationMax = undefined 115 this.durationMax = undefined
103 this.isLive = undefined 116 this.isLive = undefined
117 this.host = undefined
104 118
105 this.sort = '-match' 119 this.sort = '-match'
106 } 120 }
@@ -120,12 +134,13 @@ export class AdvancedSearch {
120 durationMin: this.durationMin, 134 durationMin: this.durationMin,
121 durationMax: this.durationMax, 135 durationMax: this.durationMax,
122 isLive: this.isLive, 136 isLive: this.isLive,
137 host: this.host,
123 sort: this.sort, 138 sort: this.sort,
124 searchTarget: this.searchTarget 139 searchTarget: this.searchTarget
125 } 140 }
126 } 141 }
127 142
128 toAPIObject (): VideosSearchQuery { 143 toVideosAPIObject (): VideosSearchQuery {
129 let isLive: boolean 144 let isLive: boolean
130 if (this.isLive) isLive = this.isLive === 'true' 145 if (this.isLive) isLive = this.isLive === 'true'
131 146
@@ -142,12 +157,27 @@ export class AdvancedSearch {
142 tagsAllOf: this.tagsAllOf, 157 tagsAllOf: this.tagsAllOf,
143 durationMin: this.durationMin, 158 durationMin: this.durationMin,
144 durationMax: this.durationMax, 159 durationMax: this.durationMax,
160 host: this.host,
145 isLive, 161 isLive,
146 sort: this.sort, 162 sort: this.sort,
147 searchTarget: this.searchTarget 163 searchTarget: this.searchTarget
148 } 164 }
149 } 165 }
150 166
167 toPlaylistAPIObject (): VideoPlaylistsSearchQuery {
168 return {
169 host: this.host,
170 searchTarget: this.searchTarget
171 }
172 }
173
174 toChannelAPIObject (): VideoChannelsSearchQuery {
175 return {
176 host: this.host,
177 searchTarget: this.searchTarget
178 }
179 }
180
151 size () { 181 size () {
152 let acc = 0 182 let acc = 0
153 183
diff --git a/client/src/app/shared/shared-search/find-in-bulk.service.ts b/client/src/app/shared/shared-search/find-in-bulk.service.ts
new file mode 100644
index 000000000..61dd2cbc5
--- /dev/null
+++ b/client/src/app/shared/shared-search/find-in-bulk.service.ts
@@ -0,0 +1,123 @@
1import * as debug from 'debug'
2import { Observable, Subject, throwError } from 'rxjs'
3import { first, map } from 'rxjs/operators'
4import { Injectable, NgZone } from '@angular/core'
5import { buildBulkObservable } from '@app/helpers'
6import { ResultList } from '@shared/models/common'
7import { Video, VideoChannel } from '../shared-main'
8import { VideoPlaylist } from '../shared-video-playlist'
9import { SearchService } from './search.service'
10
11const logger = debug('peertube:search:FindInBulkService')
12
13type BulkObservables <P extends number | string, R> = {
14 notifier: Subject<P>
15 result: Observable<R>
16}
17
18@Injectable()
19export class FindInBulkService {
20
21 private getVideoInBulk: BulkObservables<string, ResultList<Video>>
22 private getChannelInBulk: BulkObservables<string, ResultList<VideoChannel>>
23 private getPlaylistInBulk: BulkObservables<string, ResultList<VideoPlaylist>>
24
25 constructor (
26 private searchService: SearchService,
27 private ngZone: NgZone
28 ) {
29 this.getVideoInBulk = this.buildBulkObservableObject(this.getVideosInBulk.bind(this))
30 this.getChannelInBulk = this.buildBulkObservableObject(this.getChannelsInBulk.bind(this))
31 this.getPlaylistInBulk = this.buildBulkObservableObject(this.getPlaylistsInBulk.bind(this))
32 }
33
34 getVideo (uuid: string): Observable<Video> {
35 logger('Schedule video fetch for uuid %s.', uuid)
36
37 return this.getData({
38 observableObject: this.getVideoInBulk,
39 finder: v => v.uuid === uuid,
40 param: uuid
41 })
42 }
43
44 getChannel (handle: string): Observable<VideoChannel> {
45 logger('Schedule channel fetch for handle %s.', handle)
46
47 return this.getData({
48 observableObject: this.getChannelInBulk,
49 finder: c => c.nameWithHost === handle || c.nameWithHostForced === handle,
50 param: handle
51 })
52 }
53
54 getPlaylist (uuid: string): Observable<VideoPlaylist> {
55 logger('Schedule playlist fetch for uuid %s.', uuid)
56
57 return this.getData({
58 observableObject: this.getPlaylistInBulk,
59 finder: p => p.uuid === uuid,
60 param: uuid
61 })
62 }
63
64 private getData <P extends number | string, R> (options: {
65 observableObject: BulkObservables<P, ResultList<R>>
66 param: P
67 finder: (d: R) => boolean
68 }) {
69 const { observableObject, param, finder } = options
70
71 return new Observable<R>(obs => {
72 observableObject.result
73 .pipe(
74 first(),
75 map(({ data }) => data),
76 map(data => data.find(finder))
77 )
78 .subscribe(result => {
79 if (!result) {
80 obs.error(new Error($localize`Element ${param} not found`))
81 } else {
82 obs.next(result)
83 obs.complete()
84 }
85 })
86
87 observableObject.notifier.next(param)
88 })
89 }
90
91 private getVideosInBulk (uuids: string[]) {
92 logger('Fetching videos %s.', uuids.join(', '))
93
94 return this.searchService.searchVideos({ uuids })
95 }
96
97 private getChannelsInBulk (handles: string[]) {
98 logger('Fetching channels %s.', handles.join(', '))
99
100 return this.searchService.searchVideoChannels({ handles })
101 }
102
103 private getPlaylistsInBulk (uuids: string[]) {
104 logger('Fetching playlists %s.', uuids.join(', '))
105
106 return this.searchService.searchVideoPlaylists({ uuids })
107 }
108
109 private buildBulkObservableObject <T extends number | string, R> (bulkGet: (params: T[]) => Observable<R>) {
110 const notifier = new Subject<T>()
111
112 return {
113 notifier,
114
115 result: buildBulkObservable({
116 time: 500,
117 bulkGet,
118 ngZone: this.ngZone,
119 notifierObservable: notifier.asObservable()
120 })
121 }
122 }
123}
diff --git a/client/src/app/shared/shared-search/index.ts b/client/src/app/shared/shared-search/index.ts
index f687f6767..0235893c4 100644
--- a/client/src/app/shared/shared-search/index.ts
+++ b/client/src/app/shared/shared-search/index.ts
@@ -1,3 +1,4 @@
1export * from './advanced-search.model' 1export * from './advanced-search.model'
2export * from './find-in-bulk.service'
2export * from './search.service' 3export * from './search.service'
3export * from './shared-search.module' 4export * from './shared-search.module'
diff --git a/client/src/app/shared/shared-search/search.service.ts b/client/src/app/shared/shared-search/search.service.ts
index ad258f5e5..fdfab0e0e 100644
--- a/client/src/app/shared/shared-search/search.service.ts
+++ b/client/src/app/shared/shared-search/search.service.ts
@@ -7,7 +7,6 @@ import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/sha
7import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' 7import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
8import { 8import {
9 ResultList, 9 ResultList,
10 SearchTargetType,
11 Video as VideoServerModel, 10 Video as VideoServerModel,
12 VideoChannel as VideoChannelServerModel, 11 VideoChannel as VideoChannelServerModel,
13 VideoPlaylist as VideoPlaylistServerModel 12 VideoPlaylist as VideoPlaylistServerModel
@@ -33,11 +32,12 @@ export class SearchService {
33 } 32 }
34 33
35 searchVideos (parameters: { 34 searchVideos (parameters: {
36 search: string, 35 search?: string
37 componentPagination?: ComponentPaginationLight, 36 componentPagination?: ComponentPaginationLight
38 advancedSearch?: AdvancedSearch 37 advancedSearch?: AdvancedSearch
38 uuids?: string[]
39 }): Observable<ResultList<Video>> { 39 }): Observable<ResultList<Video>> {
40 const { search, componentPagination, advancedSearch } = parameters 40 const { search, uuids, componentPagination, advancedSearch } = parameters
41 41
42 const url = SearchService.BASE_SEARCH_URL + 'videos' 42 const url = SearchService.BASE_SEARCH_URL + 'videos'
43 let pagination: RestPagination 43 let pagination: RestPagination
@@ -50,9 +50,10 @@ export class SearchService {
50 params = this.restService.addRestGetParams(params, pagination) 50 params = this.restService.addRestGetParams(params, pagination)
51 51
52 if (search) params = params.append('search', search) 52 if (search) params = params.append('search', search)
53 if (uuids) params = this.restService.addArrayParams(params, 'uuids', uuids)
53 54
54 if (advancedSearch) { 55 if (advancedSearch) {
55 const advancedSearchObject = advancedSearch.toAPIObject() 56 const advancedSearchObject = advancedSearch.toVideosAPIObject()
56 params = this.restService.addObjectParams(params, advancedSearchObject) 57 params = this.restService.addObjectParams(params, advancedSearchObject)
57 } 58 }
58 59
@@ -65,11 +66,12 @@ export class SearchService {
65 } 66 }
66 67
67 searchVideoChannels (parameters: { 68 searchVideoChannels (parameters: {
68 search: string, 69 search?: string
69 searchTarget?: SearchTargetType, 70 advancedSearch?: AdvancedSearch
70 componentPagination?: ComponentPaginationLight 71 componentPagination?: ComponentPaginationLight
72 handles?: string[]
71 }): Observable<ResultList<VideoChannel>> { 73 }): Observable<ResultList<VideoChannel>> {
72 const { search, componentPagination, searchTarget } = parameters 74 const { search, advancedSearch, componentPagination, handles } = parameters
73 75
74 const url = SearchService.BASE_SEARCH_URL + 'video-channels' 76 const url = SearchService.BASE_SEARCH_URL + 'video-channels'
75 77
@@ -80,10 +82,13 @@ export class SearchService {
80 82
81 let params = new HttpParams() 83 let params = new HttpParams()
82 params = this.restService.addRestGetParams(params, pagination) 84 params = this.restService.addRestGetParams(params, pagination)
83 params = params.append('search', search)
84 85
85 if (searchTarget) { 86 if (search) params = params.append('search', search)
86 params = params.append('searchTarget', searchTarget as string) 87 if (handles) params = this.restService.addArrayParams(params, 'handles', handles)
88
89 if (advancedSearch) {
90 const advancedSearchObject = advancedSearch.toChannelAPIObject()
91 params = this.restService.addObjectParams(params, advancedSearchObject)
87 } 92 }
88 93
89 return this.authHttp 94 return this.authHttp
@@ -95,11 +100,12 @@ export class SearchService {
95 } 100 }
96 101
97 searchVideoPlaylists (parameters: { 102 searchVideoPlaylists (parameters: {
98 search: string, 103 search?: string
99 searchTarget?: SearchTargetType, 104 advancedSearch?: AdvancedSearch
100 componentPagination?: ComponentPaginationLight 105 componentPagination?: ComponentPaginationLight
106 uuids?: string[]
101 }): Observable<ResultList<VideoPlaylist>> { 107 }): Observable<ResultList<VideoPlaylist>> {
102 const { search, componentPagination, searchTarget } = parameters 108 const { search, advancedSearch, componentPagination, uuids } = parameters
103 109
104 const url = SearchService.BASE_SEARCH_URL + 'video-playlists' 110 const url = SearchService.BASE_SEARCH_URL + 'video-playlists'
105 111
@@ -110,10 +116,13 @@ export class SearchService {
110 116
111 let params = new HttpParams() 117 let params = new HttpParams()
112 params = this.restService.addRestGetParams(params, pagination) 118 params = this.restService.addRestGetParams(params, pagination)
113 params = params.append('search', search)
114 119
115 if (searchTarget) { 120 if (search) params = params.append('search', search)
116 params = params.append('searchTarget', searchTarget as string) 121 if (uuids) params = this.restService.addArrayParams(params, 'uuids', uuids)
122
123 if (advancedSearch) {
124 const advancedSearchObject = advancedSearch.toPlaylistAPIObject()
125 params = this.restService.addObjectParams(params, advancedSearchObject)
117 } 126 }
118 127
119 return this.authHttp 128 return this.authHttp
diff --git a/client/src/app/shared/shared-search/shared-search.module.ts b/client/src/app/shared/shared-search/shared-search.module.ts
index be4ef5e3f..8b5492400 100644
--- a/client/src/app/shared/shared-search/shared-search.module.ts
+++ b/client/src/app/shared/shared-search/shared-search.module.ts
@@ -1,6 +1,7 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { SharedMainModule } from '../shared-main' 2import { SharedMainModule } from '../shared-main'
3import { SharedVideoPlaylistModule } from '../shared-video-playlist' 3import { SharedVideoPlaylistModule } from '../shared-video-playlist'
4import { FindInBulkService } from './find-in-bulk.service'
4import { SearchService } from './search.service' 5import { SearchService } from './search.service'
5 6
6@NgModule({ 7@NgModule({
@@ -16,6 +17,7 @@ import { SearchService } from './search.service'
16 ], 17 ],
17 18
18 providers: [ 19 providers: [
20 FindInBulkService,
19 SearchService 21 SearchService
20 ] 22 ]
21}) 23})
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.ts b/client/src/app/shared/shared-share-modal/video-share.component.ts
index a41ff248b..341abdc2b 100644
--- a/client/src/app/shared/shared-share-modal/video-share.component.ts
+++ b/client/src/app/shared/shared-share-modal/video-share.component.ts
@@ -1,9 +1,10 @@
1import { Component, ElementRef, Input, ViewChild } from '@angular/core' 1import { Component, ElementRef, Input, ViewChild } from '@angular/core'
2import { Video, VideoDetails } from '@app/shared/shared-main' 2import { VideoDetails } from '@app/shared/shared-main'
3import { VideoPlaylist } from '@app/shared/shared-video-playlist' 3import { VideoPlaylist } from '@app/shared/shared-video-playlist'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { buildPlaylistLink, buildVideoLink, decoratePlaylistLink, decorateVideoLink } from '@shared/core-utils'
5import { VideoCaption } from '@shared/models' 6import { VideoCaption } from '@shared/models'
6import { buildPlaylistLink, buildVideoLink, buildVideoOrPlaylistEmbed } from '../../../assets/player/utils' 7import { buildVideoOrPlaylistEmbed } from '../../../assets/player/utils'
7 8
8type Customizations = { 9type Customizations = {
9 startAtCheckbox: boolean 10 startAtCheckbox: boolean
@@ -83,34 +84,34 @@ export class VideoShareComponent {
83 } 84 }
84 85
85 getVideoIframeCode () { 86 getVideoIframeCode () {
86 const options = this.getVideoOptions(this.video.embedUrl) 87 const embedUrl = decorateVideoLink({ url: this.video.embedUrl, ...this.getVideoOptions() })
87 88
88 const embedUrl = buildVideoLink(options)
89 return buildVideoOrPlaylistEmbed(embedUrl, this.video.name) 89 return buildVideoOrPlaylistEmbed(embedUrl, this.video.name)
90 } 90 }
91 91
92 getPlaylistIframeCode () { 92 getPlaylistIframeCode () {
93 const options = this.getPlaylistOptions(this.playlist.embedUrl) 93 const embedUrl = decoratePlaylistLink({ url: this.playlist.embedUrl, ...this.getPlaylistOptions() })
94 94
95 const embedUrl = buildPlaylistLink(options)
96 return buildVideoOrPlaylistEmbed(embedUrl, this.playlist.displayName) 95 return buildVideoOrPlaylistEmbed(embedUrl, this.playlist.displayName)
97 } 96 }
98 97
99 getVideoUrl () { 98 getVideoUrl () {
100 let baseUrl = this.customizations.originUrl ? this.video.originInstanceUrl : window.location.origin 99 const baseUrl = this.customizations.originUrl
101 baseUrl += Video.buildWatchUrl(this.video) 100 ? this.video.originInstanceUrl
101 : window.location.origin
102 102
103 const options = this.getVideoOptions(baseUrl) 103 return decorateVideoLink({
104 url: buildVideoLink(this.video, baseUrl),
104 105
105 return buildVideoLink(options) 106 ...this.getVideoOptions()
107 })
106 } 108 }
107 109
108 getPlaylistUrl () { 110 getPlaylistUrl () {
109 const base = window.location.origin + VideoPlaylist.buildWatchUrl(this.playlist) 111 const url = buildPlaylistLink(this.playlist)
112 if (!this.includeVideoInPlaylist) return url
110 113
111 if (!this.includeVideoInPlaylist) return base 114 return decoratePlaylistLink({ url, playlistPosition: this.playlistPosition })
112
113 return base + '?playlistPosition=' + this.playlistPosition
114 } 115 }
115 116
116 notSecure () { 117 notSecure () {
@@ -133,10 +134,8 @@ export class VideoShareComponent {
133 } 134 }
134 } 135 }
135 136
136 private getVideoOptions (baseUrl?: string) { 137 private getVideoOptions () {
137 return { 138 return {
138 baseUrl,
139
140 startTime: this.customizations.startAtCheckbox ? this.customizations.startAt : undefined, 139 startTime: this.customizations.startAtCheckbox ? this.customizations.startAt : undefined,
141 stopTime: this.customizations.stopAtCheckbox ? this.customizations.stopAt : undefined, 140 stopTime: this.customizations.stopAtCheckbox ? this.customizations.stopAt : undefined,
142 141
diff --git a/client/src/app/shared/shared-user-subscription/user-subscription.service.ts b/client/src/app/shared/shared-user-subscription/user-subscription.service.ts
index eb1fdf91c..bb44660d2 100644
--- a/client/src/app/shared/shared-user-subscription/user-subscription.service.ts
+++ b/client/src/app/shared/shared-user-subscription/user-subscription.service.ts
@@ -1,11 +1,10 @@
1import * as debug from 'debug' 1import * as debug from 'debug'
2import { uniq } from 'lodash-es' 2import { merge, Observable, of, ReplaySubject, Subject } from 'rxjs'
3import { asyncScheduler, merge, Observable, of, ReplaySubject, Subject } from 'rxjs' 3import { catchError, filter, map, switchMap, tap } from 'rxjs/operators'
4import { bufferTime, catchError, filter, map, observeOn, share, switchMap, tap } from 'rxjs/operators'
5import { HttpClient, HttpParams } from '@angular/common/http' 4import { HttpClient, HttpParams } from '@angular/common/http'
6import { Injectable, NgZone } from '@angular/core' 5import { Injectable, NgZone } from '@angular/core'
7import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' 6import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core'
8import { enterZone, leaveZone } from '@app/helpers' 7import { buildBulkObservable } from '@app/helpers'
9import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' 8import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
10import { ResultList, VideoChannel as VideoChannelServer, VideoSortField } from '@shared/models' 9import { ResultList, VideoChannel as VideoChannelServer, VideoSortField } from '@shared/models'
11import { environment } from '../../../environments/environment' 10import { environment } from '../../../environments/environment'
@@ -35,15 +34,12 @@ export class UserSubscriptionService {
35 private ngZone: NgZone 34 private ngZone: NgZone
36 ) { 35 ) {
37 this.existsObservable = merge( 36 this.existsObservable = merge(
38 this.existsSubject.pipe( 37 buildBulkObservable({
39 // We leave Angular zone so Protractor does not get stuck 38 time: 500,
40 bufferTime(500, leaveZone(this.ngZone, asyncScheduler)), 39 ngZone: this.ngZone,
41 filter(uris => uris.length !== 0), 40 notifierObservable: this.existsSubject,
42 map(uris => uniq(uris)), 41 bulkGet: this.doSubscriptionsExist.bind(this)
43 observeOn(enterZone(this.ngZone, asyncScheduler)), 42 }),
44 switchMap(uris => this.doSubscriptionsExist(uris)),
45 share()
46 ),
47 43
48 this.myAccountSubscriptionCacheSubject 44 this.myAccountSubscriptionCacheSubject
49 ) 45 )
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts
index 52e72d35b..33061a837 100644
--- a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts
+++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts
@@ -24,7 +24,7 @@ import {
24} from '@app/core' 24} from '@app/core'
25import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' 25import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
26import { GlobalIconName } from '@app/shared/shared-icons' 26import { GlobalIconName } from '@app/shared/shared-icons'
27import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@shared/core-utils/miscs/date' 27import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@shared/core-utils'
28import { HTMLServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/models' 28import { HTMLServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/models'
29import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' 29import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
30import { Syndication, Video } from '../shared-main' 30import { Syndication, Video } from '../shared-main'
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
index 681e5becd..8b019103c 100644
--- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
@@ -4,6 +4,7 @@ import { debounceTime, filter } from 'rxjs/operators'
4import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core' 4import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'
5import { AuthService, DisableForReuseHook, Notifier } from '@app/core' 5import { AuthService, DisableForReuseHook, Notifier } from '@app/core'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
7import { secondsToTime } from '@shared/core-utils'
7import { 8import {
8 Video, 9 Video,
9 VideoExistInPlaylist, 10 VideoExistInPlaylist,
@@ -12,7 +13,6 @@ import {
12 VideoPlaylistElementUpdate, 13 VideoPlaylistElementUpdate,
13 VideoPlaylistPrivacy 14 VideoPlaylistPrivacy
14} from '@shared/models' 15} from '@shared/models'
15import { secondsToTime } from '../../../assets/player/utils'
16import { VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR } from '../form-validators/video-playlist-validators' 16import { VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR } from '../form-validators/video-playlist-validators'
17import { CachedPlaylist, VideoPlaylistService } from './video-playlist.service' 17import { CachedPlaylist, VideoPlaylistService } from './video-playlist.service'
18 18
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts
index d99170e4e..2e495ec26 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts
@@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, In
2import { AuthService, Notifier, ServerService } from '@app/core' 2import { AuthService, Notifier, ServerService } from '@app/core'
3import { Video } from '@app/shared/shared-main' 3import { Video } from '@app/shared/shared-main'
4import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' 4import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
5import { secondsToTime } from '@shared/core-utils'
5import { HTMLServerConfig, VideoPlaylistElementType, VideoPlaylistElementUpdate } from '@shared/models' 6import { HTMLServerConfig, VideoPlaylistElementType, VideoPlaylistElementUpdate } from '@shared/models'
6import { secondsToTime } from '../../../assets/player/utils'
7import { VideoPlaylistElement } from './video-playlist-element.model' 7import { VideoPlaylistElement } from './video-playlist-element.model'
8import { VideoPlaylist } from './video-playlist.model' 8import { VideoPlaylist } from './video-playlist.model'
9import { VideoPlaylistService } from './video-playlist.service' 9import { VideoPlaylistService } from './video-playlist.service'
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.model.ts b/client/src/app/shared/shared-video-playlist/video-playlist.model.ts
index 55013e4c5..fcc2ce705 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist.model.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist.model.ts
@@ -1,5 +1,6 @@
1import { getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers' 1import { getAbsoluteAPIUrl, getAbsoluteEmbedUrl } from '@app/helpers'
2import { Actor } from '@app/shared/shared-main' 2import { Actor } from '@app/shared/shared-main'
3import { buildPlaylistWatchPath } from '@shared/core-utils'
3import { peertubeTranslate } from '@shared/core-utils/i18n' 4import { peertubeTranslate } from '@shared/core-utils/i18n'
4import { 5import {
5 AccountSummary, 6 AccountSummary,
@@ -44,7 +45,7 @@ export class VideoPlaylist implements ServerVideoPlaylist {
44 videoChannelBy?: string 45 videoChannelBy?: string
45 46
46 static buildWatchUrl (playlist: Pick<VideoPlaylist, 'uuid' | 'shortUUID'>) { 47 static buildWatchUrl (playlist: Pick<VideoPlaylist, 'uuid' | 'shortUUID'>) {
47 return '/w/p/' + (playlist.shortUUID || playlist.uuid) 48 return buildPlaylistWatchPath({ shortUUID: playlist.shortUUID || playlist.uuid })
48 } 49 }
49 50
50 constructor (hash: ServerVideoPlaylist, translations: {}) { 51 constructor (hash: ServerVideoPlaylist, translations: {}) {
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
index 1b87e0b2a..a3f1393ff 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
@@ -1,11 +1,10 @@
1import * as debug from 'debug' 1import * as debug from 'debug'
2import { uniq } from 'lodash-es' 2import { merge, Observable, of, ReplaySubject, Subject } from 'rxjs'
3import { asyncScheduler, merge, Observable, of, ReplaySubject, Subject } from 'rxjs' 3import { catchError, filter, map, share, switchMap, tap } from 'rxjs/operators'
4import { bufferTime, catchError, filter, map, observeOn, share, switchMap, tap, distinctUntilChanged } from 'rxjs/operators'
5import { HttpClient, HttpParams } from '@angular/common/http' 4import { HttpClient, HttpParams } from '@angular/common/http'
6import { Injectable, NgZone } from '@angular/core' 5import { Injectable, NgZone } from '@angular/core'
7import { AuthUser, ComponentPaginationLight, RestExtractor, RestService, ServerService } from '@app/core' 6import { AuthUser, ComponentPaginationLight, RestExtractor, RestService, ServerService } from '@app/core'
8import { enterZone, leaveZone, objectToFormData } from '@app/helpers' 7import { buildBulkObservable, objectToFormData } from '@app/helpers'
9import { Account, AccountService, VideoChannel, VideoChannelService } from '@app/shared/shared-main' 8import { Account, AccountService, VideoChannel, VideoChannelService } from '@app/shared/shared-main'
10import { 9import {
11 ResultList, 10 ResultList,
@@ -52,16 +51,12 @@ export class VideoPlaylistService {
52 private ngZone: NgZone 51 private ngZone: NgZone
53 ) { 52 ) {
54 this.videoExistsInPlaylistObservable = merge( 53 this.videoExistsInPlaylistObservable = merge(
55 this.videoExistsInPlaylistNotifier.pipe( 54 buildBulkObservable({
56 distinctUntilChanged(), 55 time: 500,
57 // We leave Angular zone so Protractor does not get stuck 56 ngZone: this.ngZone,
58 bufferTime(500, leaveZone(this.ngZone, asyncScheduler)), 57 bulkGet: this.doVideosExistInPlaylist.bind(this),
59 filter(videoIds => videoIds.length !== 0), 58 notifierObservable: this.videoExistsInPlaylistNotifier
60 map(videoIds => uniq(videoIds)), 59 }),
61 observeOn(enterZone(this.ngZone, asyncScheduler)),
62 switchMap(videoIds => this.doVideosExistInPlaylist(videoIds)),
63 share()
64 ),
65 60
66 this.videoExistsInPlaylistCacheSubject 61 this.videoExistsInPlaylistCacheSubject
67 ) 62 )