aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/videos
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-02-11 11:52:34 +0100
committerChocobozzz <me@florianbigard.com>2019-02-11 11:52:34 +0100
commit88108880bbdba473cfe36ecbebc1c3c4f972e102 (patch)
treeb242efb3b4f0d7e49d88f2d1f2063b5b3b0489c0 /client/src/app/videos
parent53a94c7cfa8368da4cd248d65df8346905938f0c (diff)
parent9b712a2017e4ab3cf12cd6bd58278905520159d0 (diff)
downloadPeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.gz
PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.zst
PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.zip
Merge branch 'develop' into pr/1217
Diffstat (limited to 'client/src/app/videos')
-rw-r--r--client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html2
-rw-r--r--client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts8
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.html5
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.scss33
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.ts11
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html13
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss45
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts27
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html14
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss37
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts20
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-send.scss54
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-send.ts12
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.html22
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss42
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts48
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.html8
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.ts19
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.html4
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.ts37
-rw-r--r--client/src/app/videos/+video-watch/comment/linkifier.service.ts114
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.html2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.ts15
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.component.scss6
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.component.ts29
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.model.ts2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.service.ts14
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.html2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.scss2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.ts37
-rw-r--r--client/src/app/videos/+video-watch/modal/video-blacklist.component.html9
-rw-r--r--client/src/app/videos/+video-watch/modal/video-blacklist.component.ts19
-rw-r--r--client/src/app/videos/+video-watch/modal/video-download.component.html2
-rw-r--r--client/src/app/videos/+video-watch/modal/video-download.component.ts6
-rw-r--r--client/src/app/videos/+video-watch/modal/video-report.component.html7
-rw-r--r--client/src/app/videos/+video-watch/modal/video-report.component.scss4
-rw-r--r--client/src/app/videos/+video-watch/modal/video-report.component.ts24
-rw-r--r--client/src/app/videos/+video-watch/modal/video-share.component.html2
-rw-r--r--client/src/app/videos/+video-watch/modal/video-share.component.ts6
-rw-r--r--client/src/app/videos/+video-watch/modal/video-support.component.html2
-rw-r--r--client/src/app/videos/+video-watch/modal/video-support.component.ts3
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html24
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss82
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts155
-rw-r--r--client/src/app/videos/+video-watch/video-watch.module.ts6
-rw-r--r--client/src/app/videos/shared/index.ts1
-rw-r--r--client/src/app/videos/shared/markdown.service.ts79
-rw-r--r--client/src/app/videos/video-list/video-local.component.ts16
-rw-r--r--client/src/app/videos/video-list/video-overview.component.ts10
-rw-r--r--client/src/app/videos/video-list/video-recently-added.component.ts4
-rw-r--r--client/src/app/videos/video-list/video-trending.component.ts23
-rw-r--r--client/src/app/videos/video-list/video-user-subscriptions.component.ts4
52 files changed, 488 insertions, 684 deletions
diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html
index 30aefdbfc..19043eee6 100644
--- a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html
+++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html
@@ -3,7 +3,7 @@
3 3
4 <div class="modal-header"> 4 <div class="modal-header">
5 <h4 i18n class="modal-title">Add caption</h4> 5 <h4 i18n class="modal-title">Add caption</h4>
6 <span class="close" aria-label="Close" role="button" (click)="hide()"></span> 6 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
7 </div> 7 </div>
8 8
9 <div class="modal-body"> 9 <div class="modal-body">
diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
index 07c33030a..1413e7262 100644
--- a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
+++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
@@ -5,6 +5,7 @@ import { VideoCaptionsValidatorsService } from '@app/shared/forms/form-validator
5import { ServerService } from '@app/core' 5import { ServerService } from '@app/core'
6import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' 6import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
7import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
8import { VideoConstant } from '../../../../../../shared'
8 9
9@Component({ 10@Component({
10 selector: 'my-video-caption-add-modal', 11 selector: 'my-video-caption-add-modal',
@@ -19,7 +20,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
19 20
20 @ViewChild('modal') modal: ElementRef 21 @ViewChild('modal') modal: ElementRef
21 22
22 videoCaptionLanguages = [] 23 videoCaptionLanguages: VideoConstant<string>[] = []
23 24
24 private openedModal: NgbModalRef 25 private openedModal: NgbModalRef
25 private closingModal = false 26 private closingModal = false
@@ -59,6 +60,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
59 hide () { 60 hide () {
60 this.closingModal = true 61 this.closingModal = true
61 this.openedModal.close() 62 this.openedModal.close()
63 this.form.reset()
62 } 64 }
63 65
64 isReplacingExistingCaption () { 66 isReplacingExistingCaption () {
@@ -70,8 +72,6 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
70 } 72 }
71 73
72 async addCaption () { 74 async addCaption () {
73 this.hide()
74
75 const languageId = this.form.value[ 'language' ] 75 const languageId = this.form.value[ 'language' ]
76 const languageObject = this.videoCaptionLanguages.find(l => l.id === languageId) 76 const languageObject = this.videoCaptionLanguages.find(l => l.id === languageId)
77 77
@@ -80,6 +80,6 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
80 captionfile: this.form.value[ 'captionfile' ] 80 captionfile: this.form.value[ 'captionfile' ]
81 }) 81 })
82 82
83 this.form.reset() 83 this.hide()
84 } 84 }
85} 85}
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 8c74a1ca6..7fd9af208 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
@@ -127,10 +127,11 @@
127 127
128 <my-peertube-checkbox 128 <my-peertube-checkbox
129 inputName="downloadEnabled" formControlName="downloadEnabled" 129 inputName="downloadEnabled" formControlName="downloadEnabled"
130 i18n-labelText labelText="Disable downloading" 130 i18n-labelText labelText="Download enabled"
131 ></my-peertube-checkbox> 131 ></my-peertube-checkbox>
132 132
133 <my-peertube-checkbox 133 <my-peertube-checkbox
134 *ngIf="waitTranscodingEnabled"
134 inputName="waitTranscoding" formControlName="waitTranscoding" 135 inputName="waitTranscoding" formControlName="waitTranscoding"
135 i18n-labelText labelText="Wait transcoding before publishing the video" 136 i18n-labelText labelText="Wait transcoding before publishing the video"
136 i18n-helpHtml helpHtml="If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends." 137 i18n-helpHtml helpHtml="If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends."
@@ -147,7 +148,7 @@
147 148
148 <div class="captions-header"> 149 <div class="captions-header">
149 <a (click)="openAddCaptionModal()" class="create-caption"> 150 <a (click)="openAddCaptionModal()" class="create-caption">
150 <span class="icon icon-add"></span> 151 <my-global-icon iconName="add"></my-global-icon>
151 <ng-container i18n>Add another caption</ng-container> 152 <ng-container i18n>Add another caption</ng-container>
152 </a> 153 </a>
153 </div> 154 </div>
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.scss b/client/src/app/videos/+video-edit/shared/video-edit.component.scss
index b039d7ad4..bb775cb0a 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.scss
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.scss
@@ -5,6 +5,11 @@
5 @include peertube-select-container(auto); 5 @include peertube-select-container(auto);
6} 6}
7 7
8my-peertube-checkbox {
9 display: block;
10 margin-bottom: 1rem;
11}
12
8.video-edit { 13.video-edit {
9 height: 100%; 14 height: 100%;
10 min-height: 300px; 15 min-height: 300px;
@@ -18,10 +23,6 @@
18 display: block; 23 display: block;
19 } 24 }
20 25
21 input, select {
22 font-size: 15px
23 }
24
25 .label-tags + span { 26 .label-tags + span {
26 font-size: 15px; 27 font-size: 15px;
27 } 28 }
@@ -37,7 +38,7 @@
37 text-align: right; 38 text-align: right;
38 39
39 .create-caption { 40 .create-caption {
40 @include create-button('../../../../assets/images/global/add.svg'); 41 @include create-button;
41 } 42 }
42 } 43 }
43 44
@@ -95,13 +96,14 @@
95 display: inline-block; 96 display: inline-block;
96 margin-right: 25px; 97 margin-right: 25px;
97 98
98 color: #585858; 99 color: $grey-foreground-color;
99 font-size: 15px; 100 font-size: 15px;
100 } 101 }
101 102
102 .submit-button { 103 .submit-button {
103 @include peertube-button; 104 @include peertube-button;
104 @include orange-button; 105 @include orange-button;
106 @include button-with-icon(20px, 1px);
105 107
106 display: inline-block; 108 display: inline-block;
107 109
@@ -114,16 +116,6 @@
114 color: inherit; 116 color: inherit;
115 font-weight: $font-semibold; 117 font-weight: $font-semibold;
116 } 118 }
117
118 .icon.icon-validate {
119 @include icon(20px);
120
121 cursor: inherit;
122 position: relative;
123 top: -1px;
124 margin-right: 4px;
125 background-image: url('../../../../assets/images/global/validate.svg');
126 }
127 } 119 }
128} 120}
129 121
@@ -171,10 +163,10 @@ p-calendar {
171 } 163 }
172 164
173 tag { 165 tag {
174 background-color: var(--inputColor) !important; 166 background-color: $grey-background-color !important;
167 color: #000 !important;
175 border-radius: 3px !important; 168 border-radius: 3px !important;
176 font-size: 15px !important; 169 font-size: 15px !important;
177 color: var(--mainForegroundColor) !important;
178 height: 30px !important; 170 height: 30px !important;
179 line-height: 30px !important; 171 line-height: 30px !important;
180 margin: 0 5px 0 0 !important; 172 margin: 0 5px 0 0 !important;
@@ -197,7 +189,10 @@ p-calendar {
197 top: -1px; 189 top: -1px;
198 height: auto !important; 190 height: auto !important;
199 vertical-align: middle !important; 191 vertical-align: middle !important;
200 fill: #585858 !important; 192
193 path {
194 fill: $grey-foreground-color !important;
195 }
201 } 196 }
202 197
203 &:hover { 198 &:hover {
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.ts b/client/src/app/videos/+video-edit/shared/video-edit.component.ts
index 1dae931e2..3ed7a4a10 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.ts
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts
@@ -2,7 +2,7 @@ import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'
2import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms' 2import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared' 4import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared'
5import { NotificationsService } from 'angular2-notifications' 5import { Notifier } from '@app/core'
6import { ServerService } from '../../../core/server' 6import { ServerService } from '../../../core/server'
7import { VideoEdit } from '../../../shared/video/video-edit.model' 7import { VideoEdit } from '../../../shared/video/video-edit.model'
8import { map } from 'rxjs/operators' 8import { map } from 'rxjs/operators'
@@ -27,6 +27,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
27 @Input() userVideoChannels: { id: number, label: string, support: string }[] = [] 27 @Input() userVideoChannels: { id: number, label: string, support: string }[] = []
28 @Input() schedulePublicationPossible = true 28 @Input() schedulePublicationPossible = true
29 @Input() videoCaptions: VideoCaptionEdit[] = [] 29 @Input() videoCaptions: VideoCaptionEdit[] = []
30 @Input() waitTranscodingEnabled = true
30 31
31 @ViewChild('videoCaptionAddModal') videoCaptionAddModal: VideoCaptionAddModalComponent 32 @ViewChild('videoCaptionAddModal') videoCaptionAddModal: VideoCaptionAddModalComponent
32 33
@@ -48,7 +49,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
48 calendarTimezone: string 49 calendarTimezone: string
49 calendarDateFormat: string 50 calendarDateFormat: string
50 51
51 private schedulerInterval 52 private schedulerInterval: any
52 private firstPatchDone = false 53 private firstPatchDone = false
53 private initialVideoCaptions: string[] = [] 54 private initialVideoCaptions: string[] = []
54 55
@@ -58,7 +59,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
58 private videoCaptionService: VideoCaptionService, 59 private videoCaptionService: VideoCaptionService,
59 private route: ActivatedRoute, 60 private route: ActivatedRoute,
60 private router: Router, 61 private router: Router,
61 private notificationsService: NotificationsService, 62 private notifier: Notifier,
62 private serverService: ServerService, 63 private serverService: ServerService,
63 private i18nPrimengCalendarService: I18nPrimengCalendarService 64 private i18nPrimengCalendarService: I18nPrimengCalendarService
64 ) { 65 ) {
@@ -77,14 +78,14 @@ export class VideoEditComponent implements OnInit, OnDestroy {
77 } 78 }
78 79
79 updateForm () { 80 updateForm () {
80 const defaultValues = { 81 const defaultValues: any = {
81 nsfw: 'false', 82 nsfw: 'false',
82 commentsEnabled: 'true', 83 commentsEnabled: 'true',
83 downloadEnabled: 'true', 84 downloadEnabled: 'true',
84 waitTranscoding: 'true', 85 waitTranscoding: 'true',
85 tags: [] 86 tags: []
86 } 87 }
87 const obj = { 88 const obj: any = {
88 name: this.videoValidatorsService.VIDEO_NAME, 89 name: this.videoValidatorsService.VIDEO_NAME,
89 privacy: this.videoValidatorsService.VIDEO_PRIVACY, 90 privacy: this.videoValidatorsService.VIDEO_PRIVACY,
90 channelId: this.videoValidatorsService.VIDEO_CHANNEL, 91 channelId: this.videoValidatorsService.VIDEO_CHANNEL,
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
index a933a64f0..28eb143c9 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
@@ -1,6 +1,6 @@
1<div *ngIf="!hasImportedVideo" class="upload-video-container"> 1<div *ngIf="!hasImportedVideo" class="upload-video-container">
2 <div class="import-video-torrent"> 2 <div class="first-step-block">
3 <div class="icon icon-upload"></div> 3 <my-global-icon class="upload-icon" iconName="upload"></my-global-icon>
4 4
5 <div class="button-file"> 5 <div class="button-file">
6 <span i18n>Select the torrent to import</span> 6 <span i18n>Select the torrent to import</span>
@@ -45,7 +45,12 @@
45 </div> 45 </div>
46</div> 46</div>
47 47
48<div *ngIf="hasImportedVideo" class="alert alert-info" i18n> 48<div *ngIf="error" class="alert alert-danger">
49 <div i18n>Sorry, but something went wrong</div>
50 {{ error }}
51</div>
52
53<div *ngIf="hasImportedVideo && !error" class="alert alert-info" i18n>
49 Congratulations, the video will be imported with BitTorrent! You can already add information about this video. 54 Congratulations, the video will be imported with BitTorrent! You can already add information about this video.
50</div> 55</div>
51 56
@@ -61,7 +66,7 @@
61 (click)="updateSecondStep()" 66 (click)="updateSecondStep()"
62 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" 67 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"
63 > 68 >
64 <span class="icon icon-validate"></span> 69 <my-global-icon iconName="validate"></my-global-icon>
65 <input type="button" i18n-value value="Update" /> 70 <input type="button" i18n-value value="Update" />
66 </div> 71 </div>
67 </div> 72 </div>
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss
index 262b0b68e..6d59ed834 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss
@@ -1,37 +1,7 @@
1@import 'variables'; 1@import 'variables';
2@import 'mixins'; 2@import 'mixins';
3 3
4$width-size: 190px; 4.first-step-block {
5
6.peertube-select-container {
7 @include peertube-select-container($width-size);
8}
9
10.import-video-torrent {
11 display: flex;
12 flex-direction: column;
13 align-items: center;
14
15 .icon.icon-upload {
16 @include icon(90px);
17 margin-bottom: 25px;
18 cursor: default;
19
20 background-image: url('../../../../assets/images/video/upload.svg');
21 }
22
23 .button-file {
24 @include peertube-button-file(auto);
25
26 min-width: 190px;
27 }
28
29 .button-file-extension {
30 display: block;
31 font-size: 12px;
32 margin-top: 5px;
33 }
34
35 .torrent-or-magnet { 5 .torrent-or-magnet {
36 margin: 10px 0; 6 margin: 10px 0;
37 } 7 }
@@ -39,19 +9,6 @@ $width-size: 190px;
39 .form-group-magnet-uri { 9 .form-group-magnet-uri {
40 margin-bottom: 40px; 10 margin-bottom: 40px;
41 } 11 }
42
43 input[type=text] {
44 @include peertube-input-text($width-size);
45 display: block;
46 }
47
48 input[type=button] {
49 @include peertube-button;
50 @include orange-button;
51
52 width: $width-size;
53 margin-top: 30px;
54 }
55} 12}
56 13
57 14
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
index 0f6fd17a9..c12a1d653 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
@@ -1,8 +1,7 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications'
4import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos' 3import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos'
5import { AuthService, ServerService } from '../../../core' 4import { AuthService, Notifier, ServerService } from '../../../core'
6import { VideoService } from '../../../shared/video/video.service' 5import { VideoService } from '../../../shared/video/video.service'
7import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
8import { LoadingBarService } from '@ngx-loading-bar/core' 7import { LoadingBarService } from '@ngx-loading-bar/core'
@@ -12,20 +11,22 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
12import { FormValidatorService } from '@app/shared' 11import { FormValidatorService } from '@app/shared'
13import { VideoCaptionService } from '@app/shared/video-caption' 12import { VideoCaptionService } from '@app/shared/video-caption'
14import { VideoImportService } from '@app/shared/video-import' 13import { VideoImportService } from '@app/shared/video-import'
14import { scrollToTop } from '@app/shared/misc/utils'
15 15
16@Component({ 16@Component({
17 selector: 'my-video-import-torrent', 17 selector: 'my-video-import-torrent',
18 templateUrl: './video-import-torrent.component.html', 18 templateUrl: './video-import-torrent.component.html',
19 styleUrls: [ 19 styleUrls: [
20 '../shared/video-edit.component.scss', 20 '../shared/video-edit.component.scss',
21 './video-import-torrent.component.scss' 21 './video-import-torrent.component.scss',
22 './video-send.scss'
22 ] 23 ]
23}) 24})
24export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { 25export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate {
25 @Output() firstStepDone = new EventEmitter<string>() 26 @Output() firstStepDone = new EventEmitter<string>()
26 @ViewChild('torrentfileInput') torrentfileInput 27 @Output() firstStepError = new EventEmitter<void>()
28 @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement>
27 29
28 videoFileName: string
29 magnetUri = '' 30 magnetUri = ''
30 31
31 isImportingVideo = false 32 isImportingVideo = false
@@ -33,13 +34,14 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
33 isUpdatingVideo = false 34 isUpdatingVideo = false
34 35
35 video: VideoEdit 36 video: VideoEdit
37 error: string
36 38
37 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC 39 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
38 40
39 constructor ( 41 constructor (
40 protected formValidatorService: FormValidatorService, 42 protected formValidatorService: FormValidatorService,
41 protected loadingBar: LoadingBarService, 43 protected loadingBar: LoadingBarService,
42 protected notificationsService: NotificationsService, 44 protected notifier: Notifier,
43 protected authService: AuthService, 45 protected authService: AuthService,
44 protected serverService: ServerService, 46 protected serverService: ServerService,
45 protected videoService: VideoService, 47 protected videoService: VideoService,
@@ -64,7 +66,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
64 } 66 }
65 67
66 fileChange () { 68 fileChange () {
67 const torrentfile = this.torrentfileInput.nativeElement.files[0] as File 69 const torrentfile = this.torrentfileInput.nativeElement.files[0]
68 if (!torrentfile) return 70 if (!torrentfile) return
69 71
70 this.importVideo(torrentfile) 72 this.importVideo(torrentfile)
@@ -106,7 +108,8 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
106 err => { 108 err => {
107 this.loadingBar.complete() 109 this.loadingBar.complete()
108 this.isImportingVideo = false 110 this.isImportingVideo = false
109 this.notificationsService.error(this.i18n('Error'), err.message) 111 this.firstStepError.emit()
112 this.notifier.error(err.message)
110 } 113 }
111 ) 114 )
112 } 115 }
@@ -125,14 +128,14 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
125 .subscribe( 128 .subscribe(
126 () => { 129 () => {
127 this.isUpdatingVideo = false 130 this.isUpdatingVideo = false
128 this.notificationsService.success(this.i18n('Success'), this.i18n('Video to import updated.')) 131 this.notifier.success(this.i18n('Video to import updated.'))
129 132
130 this.router.navigate([ '/my-account', 'video-imports' ]) 133 this.router.navigate([ '/my-account', 'video-imports' ])
131 }, 134 },
132 135
133 err => { 136 err => {
134 this.isUpdatingVideo = false 137 this.error = err.message
135 this.notificationsService.error(this.i18n('Error'), err.message) 138 scrollToTop()
136 console.error(err) 139 console.error(err)
137 } 140 }
138 ) 141 )
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
index 9f5fc6d22..3550c3585 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
@@ -1,6 +1,6 @@
1<div *ngIf="!hasImportedVideo" class="upload-video-container"> 1<div *ngIf="!hasImportedVideo" class="upload-video-container">
2 <div class="import-video-url"> 2 <div class="first-step-block">
3 <div class="icon icon-upload"></div> 3 <my-global-icon class="upload-icon" iconName="upload"></my-global-icon>
4 4
5 <div class="form-group"> 5 <div class="form-group">
6 <label i18n for="targetUrl">URL</label> 6 <label i18n for="targetUrl">URL</label>
@@ -37,7 +37,13 @@
37 </div> 37 </div>
38</div> 38</div>
39 39
40<div *ngIf="hasImportedVideo" class="alert alert-info" i18n> 40
41<div *ngIf="error" class="alert alert-danger">
42 <div i18n>Sorry, but something went wrong</div>
43 {{ error }}
44</div>
45
46<div *ngIf="!error && hasImportedVideo" class="alert alert-info" i18n>
41 Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video. 47 Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video.
42</div> 48</div>
43 49
@@ -53,7 +59,7 @@
53 (click)="updateSecondStep()" 59 (click)="updateSecondStep()"
54 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" 60 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"
55 > 61 >
56 <span class="icon icon-validate"></span> 62 <my-global-icon iconName="validate"></my-global-icon>
57 <input type="button" i18n-value value="Update" /> 63 <input type="button" i18n-value value="Update" />
58 </div> 64 </div>
59 </div> 65 </div>
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss
deleted file mode 100644
index 7c6deda1d..000000000
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss
+++ /dev/null
@@ -1,37 +0,0 @@
1@import 'variables';
2@import 'mixins';
3
4$width-size: 190px;
5
6.peertube-select-container {
7 @include peertube-select-container($width-size);
8}
9
10.import-video-url {
11 display: flex;
12 flex-direction: column;
13 align-items: center;
14
15 .icon.icon-upload {
16 @include icon(90px);
17 margin-bottom: 25px;
18 cursor: default;
19
20 background-image: url('../../../../assets/images/video/upload.svg');
21 }
22
23 input[type=text] {
24 @include peertube-input-text($width-size);
25 display: block;
26 }
27
28 input[type=button] {
29 @include peertube-button;
30 @include orange-button;
31
32 width: $width-size;
33 margin-top: 30px;
34 }
35}
36
37
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
index fbc85c74f..d11685916 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
@@ -1,8 +1,7 @@
1import { Component, EventEmitter, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications'
4import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos' 3import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos'
5import { AuthService, ServerService } from '../../../core' 4import { AuthService, Notifier, ServerService } from '../../../core'
6import { VideoService } from '../../../shared/video/video.service' 5import { VideoService } from '../../../shared/video/video.service'
7import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
8import { LoadingBarService } from '@ngx-loading-bar/core' 7import { LoadingBarService } from '@ngx-loading-bar/core'
@@ -12,33 +11,35 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
12import { FormValidatorService } from '@app/shared' 11import { FormValidatorService } from '@app/shared'
13import { VideoCaptionService } from '@app/shared/video-caption' 12import { VideoCaptionService } from '@app/shared/video-caption'
14import { VideoImportService } from '@app/shared/video-import' 13import { VideoImportService } from '@app/shared/video-import'
14import { scrollToTop } from '@app/shared/misc/utils'
15 15
16@Component({ 16@Component({
17 selector: 'my-video-import-url', 17 selector: 'my-video-import-url',
18 templateUrl: './video-import-url.component.html', 18 templateUrl: './video-import-url.component.html',
19 styleUrls: [ 19 styleUrls: [
20 '../shared/video-edit.component.scss', 20 '../shared/video-edit.component.scss',
21 './video-import-url.component.scss' 21 './video-send.scss'
22 ] 22 ]
23}) 23})
24export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { 24export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate {
25 @Output() firstStepDone = new EventEmitter<string>() 25 @Output() firstStepDone = new EventEmitter<string>()
26 @Output() firstStepError = new EventEmitter<void>()
26 27
27 targetUrl = '' 28 targetUrl = ''
28 videoFileName: string
29 29
30 isImportingVideo = false 30 isImportingVideo = false
31 hasImportedVideo = false 31 hasImportedVideo = false
32 isUpdatingVideo = false 32 isUpdatingVideo = false
33 33
34 video: VideoEdit 34 video: VideoEdit
35 error: string
35 36
36 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC 37 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
37 38
38 constructor ( 39 constructor (
39 protected formValidatorService: FormValidatorService, 40 protected formValidatorService: FormValidatorService,
40 protected loadingBar: LoadingBarService, 41 protected loadingBar: LoadingBarService,
41 protected notificationsService: NotificationsService, 42 protected notifier: Notifier,
42 protected authService: AuthService, 43 protected authService: AuthService,
43 protected serverService: ServerService, 44 protected serverService: ServerService,
44 protected videoService: VideoService, 45 protected videoService: VideoService,
@@ -98,7 +99,8 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
98 err => { 99 err => {
99 this.loadingBar.complete() 100 this.loadingBar.complete()
100 this.isImportingVideo = false 101 this.isImportingVideo = false
101 this.notificationsService.error(this.i18n('Error'), err.message) 102 this.firstStepError.emit()
103 this.notifier.error(err.message)
102 } 104 }
103 ) 105 )
104 } 106 }
@@ -117,14 +119,14 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
117 .subscribe( 119 .subscribe(
118 () => { 120 () => {
119 this.isUpdatingVideo = false 121 this.isUpdatingVideo = false
120 this.notificationsService.success(this.i18n('Success'), this.i18n('Video to import updated.')) 122 this.notifier.success(this.i18n('Video to import updated.'))
121 123
122 this.router.navigate([ '/my-account', 'video-imports' ]) 124 this.router.navigate([ '/my-account', 'video-imports' ])
123 }, 125 },
124 126
125 err => { 127 err => {
126 this.isUpdatingVideo = false 128 this.error = err.message
127 this.notificationsService.error(this.i18n('Error'), err.message) 129 scrollToTop()
128 console.error(err) 130 console.error(err)
129 } 131 }
130 ) 132 )
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-send.scss b/client/src/app/videos/+video-edit/video-add-components/video-send.scss
new file mode 100644
index 000000000..8769dd302
--- /dev/null
+++ b/client/src/app/videos/+video-edit/video-add-components/video-send.scss
@@ -0,0 +1,54 @@
1@import 'variables';
2@import 'mixins';
3
4$width-size: 190px;
5
6.alert.alert-danger {
7 text-align: center;
8
9 & > div {
10 font-weight: $font-semibold;
11 }
12}
13
14.first-step-block {
15 display: flex;
16 flex-direction: column;
17 align-items: center;
18
19 .upload-icon {
20 width: 90px;
21 margin-bottom: 25px;
22
23 @include apply-svg-color(#C6C6C6);
24 }
25
26 .peertube-select-container {
27 @include peertube-select-container($width-size);
28 }
29
30 input[type=text] {
31 @include peertube-input-text($width-size);
32 display: block;
33 }
34
35 input[type=button] {
36 @include peertube-button;
37 @include orange-button;
38
39 width: $width-size;
40 margin-top: 30px;
41 }
42
43 .button-file {
44 @include peertube-button-file(auto);
45
46 min-width: 190px;
47 }
48
49 .button-file-extension {
50 display: block;
51 font-size: 12px;
52 margin-top: 5px;
53 }
54}
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-send.ts b/client/src/app/videos/+video-edit/video-add-components/video-send.ts
index 6d1bac3f2..580c123a0 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-send.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-send.ts
@@ -1,18 +1,17 @@
1import { EventEmitter, OnInit } from '@angular/core' 1import { EventEmitter, OnInit } from '@angular/core'
2import { LoadingBarService } from '@ngx-loading-bar/core' 2import { LoadingBarService } from '@ngx-loading-bar/core'
3import { NotificationsService } from 'angular2-notifications' 3import { AuthService, Notifier, ServerService } from '@app/core'
4import { catchError, switchMap, tap } from 'rxjs/operators' 4import { catchError, switchMap, tap } from 'rxjs/operators'
5import { FormReactive } from '@app/shared' 5import { FormReactive } from '@app/shared'
6import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
7import { VideoConstant, VideoPrivacy } from '../../../../../../shared' 6import { VideoConstant, VideoPrivacy } from '../../../../../../shared'
8import { AuthService, ServerService } from '@app/core'
9import { VideoService } from '@app/shared/video/video.service' 7import { VideoService } from '@app/shared/video/video.service'
10import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' 8import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
11import { VideoCaptionService } from '@app/shared/video-caption' 9import { VideoCaptionService } from '@app/shared/video-caption'
12import { VideoEdit } from '@app/shared/video/video-edit.model' 10import { VideoEdit } from '@app/shared/video/video-edit.model'
13import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils' 11import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
12import { CanComponentDeactivateResult } from '@app/shared/guards/can-deactivate-guard.service'
14 13
15export abstract class VideoSend extends FormReactive implements OnInit, CanComponentDeactivate { 14export abstract class VideoSend extends FormReactive implements OnInit {
16 userVideoChannels: { id: number, label: string, support: string }[] = [] 15 userVideoChannels: { id: number, label: string, support: string }[] = []
17 videoPrivacies: VideoConstant<VideoPrivacy>[] = [] 16 videoPrivacies: VideoConstant<VideoPrivacy>[] = []
18 videoCaptions: VideoCaptionEdit[] = [] 17 videoCaptions: VideoCaptionEdit[] = []
@@ -21,16 +20,17 @@ export abstract class VideoSend extends FormReactive implements OnInit, CanCompo
21 firstStepChannelId = 0 20 firstStepChannelId = 0
22 21
23 abstract firstStepDone: EventEmitter<string> 22 abstract firstStepDone: EventEmitter<string>
23 abstract firstStepError: EventEmitter<void>
24 protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy 24 protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy
25 25
26 protected loadingBar: LoadingBarService 26 protected loadingBar: LoadingBarService
27 protected notificationsService: NotificationsService 27 protected notifier: Notifier
28 protected authService: AuthService 28 protected authService: AuthService
29 protected serverService: ServerService 29 protected serverService: ServerService
30 protected videoService: VideoService 30 protected videoService: VideoService
31 protected videoCaptionService: VideoCaptionService 31 protected videoCaptionService: VideoCaptionService
32 32
33 abstract canDeactivate () 33 abstract canDeactivate (): CanComponentDeactivateResult
34 34
35 ngOnInit () { 35 ngOnInit () {
36 this.buildForm({}) 36 this.buildForm({})
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
index fa57c8cb5..b252cd60a 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
@@ -1,12 +1,12 @@
1<div *ngIf="!isUploadingVideo" class="upload-video-container"> 1<div *ngIf="!isUploadingVideo" class="upload-video-container">
2 <div class="upload-video"> 2 <div class="first-step-block">
3 <div class="icon icon-upload"></div> 3 <my-global-icon class="upload-icon" iconName="upload"></my-global-icon>
4 4
5 <div class="button-file"> 5 <div class="button-file">
6 <span i18n>Select the file to upload</span> 6 <span i18n>Select the file to upload</span>
7 <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" /> 7 <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" />
8 </div> 8 </div>
9 <span class="button-file-extension">(.mp4, .webm, .ogv)</span> 9 <span class="button-file-extension">({{ videoExtensions }})</span>
10 10
11 <div class="form-group form-group-channel"> 11 <div class="form-group form-group-channel">
12 <label i18n for="first-step-channel">Channel</label> 12 <label i18n for="first-step-channel">Channel</label>
@@ -29,7 +29,7 @@
29 </div> 29 </div>
30</div> 30</div>
31 31
32<div *ngIf="isUploadingVideo" class="upload-progress-cancel"> 32<div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel">
33 <p-progressBar 33 <p-progressBar
34 [value]="videoUploadPercents" 34 [value]="videoUploadPercents"
35 [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }" 35 [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }"
@@ -37,11 +37,21 @@
37 <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" /> 37 <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" />
38</div> 38</div>
39 39
40<div *ngIf="error" class="alert alert-danger">
41 <div i18n>Sorry, but something went wrong</div>
42 {{ error }}
43</div>
44
45<div *ngIf="videoUploaded && !error" class="alert alert-info" i18n>
46 Congratulations! Your video is now available in your private library.
47</div>
48
40<!-- Hidden because we want to load the component --> 49<!-- Hidden because we want to load the component -->
41<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form"> 50<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form">
42 <my-video-edit 51 <my-video-edit
43 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" 52 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
44 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" 53 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
54 [waitTranscodingEnabled]="waitTranscodingEnabled"
45 ></my-video-edit> 55 ></my-video-edit>
46 56
47 <div class="submit-container"> 57 <div class="submit-container">
@@ -51,8 +61,8 @@
51 (click)="updateSecondStep()" 61 (click)="updateSecondStep()"
52 [ngClass]="{ disabled: isPublishingButtonDisabled() }" 62 [ngClass]="{ disabled: isPublishingButtonDisabled() }"
53 > 63 >
54 <span class="icon icon-validate"></span> 64 <my-global-icon iconName="validate"></my-global-icon>
55 <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" /> 65 <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" />
56 </div> 66 </div>
57 </div> 67 </div>
58</form> \ No newline at end of file 68</form>
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
index dbae5230d..8adf8f169 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
@@ -1,39 +1,9 @@
1@import 'variables'; 1@import 'variables';
2@import 'mixins'; 2@import 'mixins';
3 3
4.peertube-select-container { 4.first-step-block .form-group-channel {
5 @include peertube-select-container(190px); 5 margin-bottom: 20px;
6} 6 margin-top: 35px;
7
8.upload-video {
9 display: flex;
10 flex-direction: column;
11 align-items: center;
12
13 .form-group-channel {
14 margin-bottom: 20px;
15 margin-top: 35px;
16 }
17
18 .icon.icon-upload {
19 @include icon(90px);
20 margin-bottom: 25px;
21 cursor: default;
22
23 background-image: url('../../../../assets/images/video/upload.svg');
24 }
25
26 .button-file {
27 @include peertube-button-file(auto);
28
29 min-width: 190px;
30 }
31
32 .button-file-extension {
33 display: block;
34 font-size: 12px;
35 margin-top: 5px;
36 }
37} 7}
38 8
39.upload-progress-cancel { 9.upload-progress-cancel {
@@ -46,9 +16,7 @@
46 16
47 /deep/ .ui-progressbar { 17 /deep/ .ui-progressbar {
48 font-size: 15px !important; 18 font-size: 15px !important;
49 color: #fff !important;
50 height: 30px !important; 19 height: 30px !important;
51 line-height: 30px !important;
52 border-radius: 3px !important; 20 border-radius: 3px !important;
53 background-color: rgba(11, 204, 41, 0.16) !important; 21 background-color: rgba(11, 204, 41, 0.16) !important;
54 22
@@ -60,6 +28,8 @@
60 text-align: left; 28 text-align: left;
61 padding-left: 18px; 29 padding-left: 18px;
62 margin-top: 0 !important; 30 margin-top: 0 !important;
31 color: #fff !important;
32 line-height: 30px !important;
63 } 33 }
64 } 34 }
65 35
@@ -82,4 +52,4 @@
82 52
83 margin-left: 10px; 53 margin-left: 10px;
84 } 54 }
85} \ No newline at end of file 55}
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 ac6c1786f..9cadf52cb 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
@@ -1,12 +1,11 @@
1import { HttpEventType, HttpResponse } from '@angular/common/http' 1import { HttpEventType, HttpResponse } from '@angular/common/http'
2import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' 2import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
3import { Router } from '@angular/router' 3import { Router } from '@angular/router'
4import { LoadingBarService } from '@ngx-loading-bar/core' 4import { LoadingBarService } from '@ngx-loading-bar/core'
5import { NotificationsService } from 'angular2-notifications'
6import { BytesPipe } from 'ngx-pipes' 5import { BytesPipe } from 'ngx-pipes'
7import { Subscription } from 'rxjs' 6import { Subscription } from 'rxjs'
8import { VideoPrivacy } from '../../../../../../shared/models/videos' 7import { VideoPrivacy } from '../../../../../../shared/models/videos'
9import { AuthService, ServerService } from '../../../core' 8import { AuthService, Notifier, ServerService } from '../../../core'
10import { VideoEdit } from '../../../shared/video/video-edit.model' 9import { VideoEdit } from '../../../shared/video/video-edit.model'
11import { VideoService } from '../../../shared/video/video.service' 10import { VideoService } from '../../../shared/video/video.service'
12import { I18n } from '@ngx-translate/i18n-polyfill' 11import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -14,18 +13,21 @@ import { VideoSend } from '@app/videos/+video-edit/video-add-components/video-se
14import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' 13import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
15import { FormValidatorService, UserService } from '@app/shared' 14import { FormValidatorService, UserService } from '@app/shared'
16import { VideoCaptionService } from '@app/shared/video-caption' 15import { VideoCaptionService } from '@app/shared/video-caption'
16import { scrollToTop } from '@app/shared/misc/utils'
17 17
18@Component({ 18@Component({
19 selector: 'my-video-upload', 19 selector: 'my-video-upload',
20 templateUrl: './video-upload.component.html', 20 templateUrl: './video-upload.component.html',
21 styleUrls: [ 21 styleUrls: [
22 '../shared/video-edit.component.scss', 22 '../shared/video-edit.component.scss',
23 './video-upload.component.scss' 23 './video-upload.component.scss',
24 './video-send.scss'
24 ] 25 ]
25}) 26})
26export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { 27export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate {
27 @Output() firstStepDone = new EventEmitter<string>() 28 @Output() firstStepDone = new EventEmitter<string>()
28 @ViewChild('videofileInput') videofileInput 29 @Output() firstStepError = new EventEmitter<void>()
30 @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
29 31
30 // So that it can be accessed in the template 32 // So that it can be accessed in the template
31 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY 33 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
@@ -42,13 +44,16 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
42 id: 0, 44 id: 0,
43 uuid: '' 45 uuid: ''
44 } 46 }
47 waitTranscodingEnabled = true
48
49 error: string
45 50
46 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC 51 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
47 52
48 constructor ( 53 constructor (
49 protected formValidatorService: FormValidatorService, 54 protected formValidatorService: FormValidatorService,
50 protected loadingBar: LoadingBarService, 55 protected loadingBar: LoadingBarService,
51 protected notificationsService: NotificationsService, 56 protected notifier: Notifier,
52 protected authService: AuthService, 57 protected authService: AuthService,
53 protected serverService: ServerService, 58 protected serverService: ServerService,
54 protected videoService: VideoService, 59 protected videoService: VideoService,
@@ -105,20 +110,15 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
105 this.isUploadingVideo = false 110 this.isUploadingVideo = false
106 this.videoUploadPercents = 0 111 this.videoUploadPercents = 0
107 this.videoUploadObservable = null 112 this.videoUploadObservable = null
108 this.notificationsService.info(this.i18n('Info'), this.i18n('Upload cancelled')) 113 this.notifier.info(this.i18n('Upload cancelled'))
109 } 114 }
110 } 115 }
111 116
112 uploadFirstStep () { 117 uploadFirstStep () {
113 const videofile = this.videofileInput.nativeElement.files[0] as File 118 const videofile = this.videofileInput.nativeElement.files[0]
114 if (!videofile) return 119 if (!videofile) return
115 120
116 // Cannot upload videos > 8GB for now 121 // Check global user quota
117 if (videofile.size > 8 * 1024 * 1024 * 1024) {
118 this.notificationsService.error(this.i18n('Error'), this.i18n('We are sorry but PeerTube cannot handle videos > 8GB'))
119 return
120 }
121
122 const bytePipes = new BytesPipe() 122 const bytePipes = new BytesPipe()
123 const videoQuota = this.authService.getUser().videoQuota 123 const videoQuota = this.authService.getUser().videoQuota
124 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) { 124 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
@@ -130,10 +130,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
130 videoQuota: bytePipes.transform(videoQuota, 0) 130 videoQuota: bytePipes.transform(videoQuota, 0)
131 } 131 }
132 ) 132 )
133 this.notificationsService.error(this.i18n('Error'), msg) 133 this.notifier.error(msg)
134 return 134 return
135 } 135 }
136 136
137 // Check daily user quota
137 const videoQuotaDaily = this.authService.getUser().videoQuotaDaily 138 const videoQuotaDaily = this.authService.getUser().videoQuotaDaily
138 if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) { 139 if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) {
139 const msg = this.i18n( 140 const msg = this.i18n(
@@ -144,10 +145,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
144 quotaDaily: bytePipes.transform(videoQuotaDaily, 0) 145 quotaDaily: bytePipes.transform(videoQuotaDaily, 0)
145 } 146 }
146 ) 147 )
147 this.notificationsService.error(this.i18n('Error'), msg) 148 this.notifier.error(msg)
148 return 149 return
149 } 150 }
150 151
152 // Build name field
151 const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '') 153 const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '')
152 let name: string 154 let name: string
153 155
@@ -155,6 +157,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
155 if (nameWithoutExtension.length < 3) name = videofile.name 157 if (nameWithoutExtension.length < 3) name = videofile.name
156 else name = nameWithoutExtension 158 else name = nameWithoutExtension
157 159
160 // Force user to wait transcoding for unsupported video types in web browsers
161 if (!videofile.name.endsWith('.mp4') && !videofile.name.endsWith('.webm') && !videofile.name.endsWith('.ogv')) {
162 this.waitTranscodingEnabled = false
163 }
164
158 const privacy = this.firstStepPrivacyId.toString() 165 const privacy = this.firstStepPrivacyId.toString()
159 const nsfw = false 166 const nsfw = false
160 const waitTranscoding = true 167 const waitTranscoding = true
@@ -203,7 +210,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
203 this.isUploadingVideo = false 210 this.isUploadingVideo = false
204 this.videoUploadPercents = 0 211 this.videoUploadPercents = 0
205 this.videoUploadObservable = null 212 this.videoUploadObservable = null
206 this.notificationsService.error(this.i18n('Error'), err.message) 213 this.firstStepError.emit()
214 this.notifier.error(err.message)
207 } 215 }
208 ) 216 )
209 } 217 }
@@ -232,13 +240,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
232 this.isUpdatingVideo = false 240 this.isUpdatingVideo = false
233 this.isUploadingVideo = false 241 this.isUploadingVideo = false
234 242
235 this.notificationsService.success(this.i18n('Success'), this.i18n('Video published.')) 243 this.notifier.success(this.i18n('Video published.'))
236 this.router.navigate([ '/videos/watch', video.uuid ]) 244 this.router.navigate([ '/videos/watch', video.uuid ])
237 }, 245 },
238 246
239 err => { 247 err => {
240 this.isUpdatingVideo = false 248 this.error = err.message
241 this.notificationsService.error(this.i18n('Error'), err.message) 249 scrollToTop()
242 console.error(err) 250 console.error(err)
243 } 251 }
244 ) 252 )
diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html
index e14e23aed..72a233b72 100644
--- a/client/src/app/videos/+video-edit/video-add.component.html
+++ b/client/src/app/videos/+video-edit/video-add.component.html
@@ -6,24 +6,24 @@
6 6
7 <ngb-tabset class="video-add-tabset root-tabset bootstrap" [ngClass]="{ 'hide-nav': secondStepType !== undefined }"> 7 <ngb-tabset class="video-add-tabset root-tabset bootstrap" [ngClass]="{ 'hide-nav': secondStepType !== undefined }">
8 8
9 <ngb-tab i18n-title title=""> 9 <ngb-tab>
10 <ng-template ngbTabTitle><span i18n>Upload a file</span></ng-template> 10 <ng-template ngbTabTitle><span i18n>Upload a file</span></ng-template>
11 <ng-template ngbTabContent> 11 <ng-template ngbTabContent>
12 <my-video-upload #videoUpload (firstStepDone)="onFirstStepDone('upload', $event)"></my-video-upload> 12 <my-video-upload #videoUpload (firstStepDone)="onFirstStepDone('upload', $event)" (firstStepError)="onError()"></my-video-upload>
13 </ng-template> 13 </ng-template>
14 </ngb-tab> 14 </ngb-tab>
15 15
16 <ngb-tab *ngIf="isVideoImportHttpEnabled()"> 16 <ngb-tab *ngIf="isVideoImportHttpEnabled()">
17 <ng-template ngbTabTitle><span i18n>Import with URL</span></ng-template> 17 <ng-template ngbTabTitle><span i18n>Import with URL</span></ng-template>
18 <ng-template ngbTabContent> 18 <ng-template ngbTabContent>
19 <my-video-import-url #videoImportUrl (firstStepDone)="onFirstStepDone('import-url', $event)"></my-video-import-url> 19 <my-video-import-url #videoImportUrl (firstStepDone)="onFirstStepDone('import-url', $event)" (firstStepError)="onError()"></my-video-import-url>
20 </ng-template> 20 </ng-template>
21 </ngb-tab> 21 </ngb-tab>
22 22
23 <ngb-tab *ngIf="isVideoImportTorrentEnabled()"> 23 <ngb-tab *ngIf="isVideoImportTorrentEnabled()">
24 <ng-template ngbTabTitle><span i18n>Import with torrent</span></ng-template> 24 <ng-template ngbTabTitle><span i18n>Import with torrent</span></ng-template>
25 <ng-template ngbTabContent> 25 <ng-template ngbTabContent>
26 <my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)"></my-video-import-torrent> 26 <my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)" (firstStepError)="onError()"></my-video-import-torrent>
27 </ng-template> 27 </ng-template>
28 </ngb-tab> 28 </ngb-tab>
29 </ngb-tabset> 29 </ngb-tabset>
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts
index 1a9247dbe..01fdfcb66 100644
--- a/client/src/app/videos/+video-edit/video-add.component.ts
+++ b/client/src/app/videos/+video-edit/video-add.component.ts
@@ -1,4 +1,4 @@
1import { Component, ViewChild } from '@angular/core' 1import { Component, HostListener, ViewChild } from '@angular/core'
2import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' 2import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
3import { VideoImportUrlComponent } from '@app/videos/+video-edit/video-add-components/video-import-url.component' 3import { VideoImportUrlComponent } from '@app/videos/+video-edit/video-add-components/video-import-url.component'
4import { VideoUploadComponent } from '@app/videos/+video-edit/video-add-components/video-upload.component' 4import { VideoUploadComponent } from '@app/videos/+video-edit/video-add-components/video-upload.component'
@@ -27,7 +27,22 @@ export class VideoAddComponent implements CanComponentDeactivate {
27 this.videoName = videoName 27 this.videoName = videoName
28 } 28 }
29 29
30 canDeactivate () { 30 onError () {
31 this.videoName = undefined
32 this.secondStepType = undefined
33 }
34
35 @HostListener('window:beforeunload', [ '$event' ])
36 onUnload (event: any) {
37 const { text, canDeactivate } = this.canDeactivate()
38
39 if (canDeactivate) return
40
41 event.returnValue = text
42 return text
43 }
44
45 canDeactivate (): { canDeactivate: boolean, text?: string} {
31 if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate() 46 if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate()
32 if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate() 47 if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate()
33 if (this.secondStepType === 'import-torrent') return this.videoImportTorrent.canDeactivate() 48 if (this.secondStepType === 'import-torrent') return this.videoImportTorrent.canDeactivate()
diff --git a/client/src/app/videos/+video-edit/video-update.component.html b/client/src/app/videos/+video-edit/video-update.component.html
index 9242c30a0..4992bb369 100644
--- a/client/src/app/videos/+video-edit/video-update.component.html
+++ b/client/src/app/videos/+video-edit/video-update.component.html
@@ -8,12 +8,12 @@
8 <my-video-edit 8 <my-video-edit
9 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible" 9 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible"
10 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" 10 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
11 [videoCaptions]="videoCaptions" 11 [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled"
12 ></my-video-edit> 12 ></my-video-edit>
13 13
14 <div class="submit-container"> 14 <div class="submit-container">
15 <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"> 15 <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }">
16 <span class="icon icon-validate"></span> 16 <my-global-icon iconName="validate"></my-global-icon>
17 <input type="button" i18n-value value="Update" /> 17 <input type="button" i18n-value value="Update" />
18 </div> 18 </div>
19 </div> 19 </div>
diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts
index 3a0f3a39a..9e849014e 100644
--- a/client/src/app/videos/+video-edit/video-update.component.ts
+++ b/client/src/app/videos/+video-edit/video-update.component.ts
@@ -1,8 +1,8 @@
1import { map, switchMap } from 'rxjs/operators' 1import { map, switchMap } from 'rxjs/operators'
2import { Component, OnInit } from '@angular/core' 2import { Component, HostListener, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { LoadingBarService } from '@ngx-loading-bar/core' 4import { LoadingBarService } from '@ngx-loading-bar/core'
5import { NotificationsService } from 'angular2-notifications' 5import { Notifier } from '@app/core'
6import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos' 6import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos'
7import { ServerService } from '../../core' 7import { ServerService } from '../../core'
8import { FormReactive } from '../../shared' 8import { FormReactive } from '../../shared'
@@ -12,6 +12,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
12import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 12import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
13import { VideoCaptionService } from '@app/shared/video-caption' 13import { VideoCaptionService } from '@app/shared/video-caption'
14import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' 14import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
15import { VideoDetails } from '@app/shared/video/video-details.model'
15 16
16@Component({ 17@Component({
17 selector: 'my-videos-update', 18 selector: 'my-videos-update',
@@ -26,6 +27,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
26 userVideoChannels: { id: number, label: string, support: string }[] = [] 27 userVideoChannels: { id: number, label: string, support: string }[] = []
27 schedulePublicationPossible = false 28 schedulePublicationPossible = false
28 videoCaptions: VideoCaptionEdit[] = [] 29 videoCaptions: VideoCaptionEdit[] = []
30 waitTranscodingEnabled = true
29 31
30 private updateDone = false 32 private updateDone = false
31 33
@@ -33,7 +35,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
33 protected formValidatorService: FormValidatorService, 35 protected formValidatorService: FormValidatorService,
34 private route: ActivatedRoute, 36 private route: ActivatedRoute,
35 private router: Router, 37 private router: Router,
36 private notificationsService: NotificationsService, 38 private notifier: Notifier,
37 private serverService: ServerService, 39 private serverService: ServerService,
38 private videoService: VideoService, 40 private videoService: VideoService,
39 private loadingBar: LoadingBarService, 41 private loadingBar: LoadingBarService,
@@ -65,25 +67,42 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
65 67
66 this.videoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies) 68 this.videoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies)
67 69
70 const videoFiles = (video as VideoDetails).files
71 if (videoFiles.length > 1) { // Already transcoded
72 this.waitTranscodingEnabled = false
73 }
74
68 // FIXME: Angular does not detect the change inside this subscription, so use the patched setTimeout 75 // FIXME: Angular does not detect the change inside this subscription, so use the patched setTimeout
69 setTimeout(() => this.hydrateFormFromVideo()) 76 setTimeout(() => this.hydrateFormFromVideo())
70 }, 77 },
71 78
72 err => { 79 err => {
73 console.error(err) 80 console.error(err)
74 this.notificationsService.error(this.i18n('Error'), err.message) 81 this.notifier.error(err.message)
75 } 82 }
76 ) 83 )
77 } 84 }
78 85
79 canDeactivate () { 86 @HostListener('window:beforeunload', [ '$event' ])
87 onUnload (event: any) {
88 const { text, canDeactivate } = this.canDeactivate()
89
90 if (canDeactivate) return
91
92 event.returnValue = text
93 return text
94 }
95
96 canDeactivate (): { canDeactivate: boolean, text?: string } {
80 if (this.updateDone === true) return { canDeactivate: true } 97 if (this.updateDone === true) return { canDeactivate: true }
81 98
99 const text = this.i18n('You have unsaved changes! If you leave, your changes will be lost.')
100
82 for (const caption of this.videoCaptions) { 101 for (const caption of this.videoCaptions) {
83 if (caption.action) return { canDeactivate: false } 102 if (caption.action) return { canDeactivate: false, text }
84 } 103 }
85 104
86 return { canDeactivate: this.formChanged === false } 105 return { canDeactivate: this.formChanged === false, text }
87 } 106 }
88 107
89 checkForm () { 108 checkForm () {
@@ -114,14 +133,14 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
114 this.updateDone = true 133 this.updateDone = true
115 this.isUpdatingVideo = false 134 this.isUpdatingVideo = false
116 this.loadingBar.complete() 135 this.loadingBar.complete()
117 this.notificationsService.success(this.i18n('Success'), this.i18n('Video updated.')) 136 this.notifier.success(this.i18n('Video updated.'))
118 this.router.navigate([ '/videos/watch', this.video.uuid ]) 137 this.router.navigate([ '/videos/watch', this.video.uuid ])
119 }, 138 },
120 139
121 err => { 140 err => {
122 this.loadingBar.complete() 141 this.loadingBar.complete()
123 this.isUpdatingVideo = false 142 this.isUpdatingVideo = false
124 this.notificationsService.error(this.i18n('Error'), err.message) 143 this.notifier.error(err.message)
125 console.error(err) 144 console.error(err)
126 } 145 }
127 ) 146 )
diff --git a/client/src/app/videos/+video-watch/comment/linkifier.service.ts b/client/src/app/videos/+video-watch/comment/linkifier.service.ts
deleted file mode 100644
index 3f4072efd..000000000
--- a/client/src/app/videos/+video-watch/comment/linkifier.service.ts
+++ /dev/null
@@ -1,114 +0,0 @@
1import { Injectable } from '@angular/core'
2import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
3import * as linkify from 'linkifyjs'
4import * as linkifyHtml from 'linkifyjs/html'
5
6@Injectable()
7export class LinkifierService {
8
9 static CLASSNAME = 'linkified'
10
11 private linkifyOptions = {
12 className: {
13 mention: LinkifierService.CLASSNAME + '-mention',
14 url: LinkifierService.CLASSNAME + '-url'
15 }
16 }
17
18 constructor () {
19 // Apply plugin
20 this.mentionWithDomainPlugin(linkify)
21 }
22
23 linkify (text: string) {
24 return linkifyHtml(text, this.linkifyOptions)
25 }
26
27 private mentionWithDomainPlugin (linkify: any) {
28 const TT = linkify.scanner.TOKENS // Text tokens
29 const { TOKENS: MT, State } = linkify.parser // Multi tokens, state
30 const MultiToken = MT.Base
31 const S_START = linkify.parser.start
32
33 const TT_AT = TT.AT
34 const TT_DOMAIN = TT.DOMAIN
35 const TT_LOCALHOST = TT.LOCALHOST
36 const TT_NUM = TT.NUM
37 const TT_COLON = TT.COLON
38 const TT_SLASH = TT.SLASH
39 const TT_TLD = TT.TLD
40 const TT_UNDERSCORE = TT.UNDERSCORE
41 const TT_DOT = TT.DOT
42
43 function MENTION (value) {
44 this.v = value
45 }
46
47 linkify.inherits(MultiToken, MENTION, {
48 type: 'mentionWithDomain',
49 isLink: true,
50 toHref () {
51 return getAbsoluteAPIUrl() + '/services/redirect/accounts/' + this.toString().substr(1)
52 }
53 })
54
55 const S_AT = S_START.jump(TT_AT) // @
56 const S_AT_SYMS = new State()
57 const S_MENTION = new State(MENTION)
58 const S_MENTION_DIVIDER = new State()
59 const S_MENTION_DIVIDER_SYMS = new State()
60
61 // @_,
62 S_AT.on(TT_UNDERSCORE, S_AT_SYMS)
63
64 // @_*
65 S_AT_SYMS
66 .on(TT_UNDERSCORE, S_AT_SYMS)
67 .on(TT_DOT, S_AT_SYMS)
68
69 // Valid mention (not made up entirely of symbols)
70 S_AT
71 .on(TT_DOMAIN, S_MENTION)
72 .on(TT_LOCALHOST, S_MENTION)
73 .on(TT_TLD, S_MENTION)
74 .on(TT_NUM, S_MENTION)
75
76 S_AT_SYMS
77 .on(TT_DOMAIN, S_MENTION)
78 .on(TT_LOCALHOST, S_MENTION)
79 .on(TT_TLD, S_MENTION)
80 .on(TT_NUM, S_MENTION)
81
82 // More valid mentions
83 S_MENTION
84 .on(TT_DOMAIN, S_MENTION)
85 .on(TT_LOCALHOST, S_MENTION)
86 .on(TT_TLD, S_MENTION)
87 .on(TT_COLON, S_MENTION)
88 .on(TT_NUM, S_MENTION)
89 .on(TT_UNDERSCORE, S_MENTION)
90
91 // Mention with a divider
92 S_MENTION
93 .on(TT_AT, S_MENTION_DIVIDER)
94 .on(TT_SLASH, S_MENTION_DIVIDER)
95 .on(TT_DOT, S_MENTION_DIVIDER)
96
97 // Mention _ trailing stash plus syms
98 S_MENTION_DIVIDER.on(TT_UNDERSCORE, S_MENTION_DIVIDER_SYMS)
99 S_MENTION_DIVIDER_SYMS.on(TT_UNDERSCORE, S_MENTION_DIVIDER_SYMS)
100
101 // Once we get a word token, mentions can start up again
102 S_MENTION_DIVIDER
103 .on(TT_DOMAIN, S_MENTION)
104 .on(TT_LOCALHOST, S_MENTION)
105 .on(TT_TLD, S_MENTION)
106 .on(TT_NUM, S_MENTION)
107
108 S_MENTION_DIVIDER_SYMS
109 .on(TT_DOMAIN, S_MENTION)
110 .on(TT_LOCALHOST, S_MENTION)
111 .on(TT_TLD, S_MENTION)
112 .on(TT_NUM, S_MENTION)
113 }
114}
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.html b/client/src/app/videos/+video-watch/comment/video-comment-add.component.html
index b58a56596..d8a7a78c4 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.html
+++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.html
@@ -3,7 +3,7 @@
3 <img [src]="getAvatarUrl()" alt="Avatar" /> 3 <img [src]="getAvatarUrl()" alt="Avatar" />
4 4
5 <div class="form-group"> 5 <div class="form-group">
6 <textarea i18n-placeholder placeholder="Add comment..." autosize 6 <textarea i18n-placeholder placeholder="Add comment..." myAutoResize
7 [readonly]="(user === null) ? true : false" 7 [readonly]="(user === null) ? true : false"
8 (click)="openVisitorModal($event)" 8 (click)="openVisitorModal($event)"
9 formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }" 9 formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }"
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
index fb7de0e04..fd85c28f2 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
@@ -1,6 +1,6 @@
1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier } from '@app/core'
4import { Observable } from 'rxjs' 4import { Observable } from 'rxjs'
5import { VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model' 5import { VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model'
6import { FormReactive } from '../../../shared' 6import { FormReactive } from '../../../shared'
@@ -29,14 +29,14 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
29 @Output() commentCreated = new EventEmitter<VideoCommentCreate>() 29 @Output() commentCreated = new EventEmitter<VideoCommentCreate>()
30 30
31 @ViewChild('visitorModal') visitorModal: NgbModal 31 @ViewChild('visitorModal') visitorModal: NgbModal
32 @ViewChild('textarea') private textareaElement: ElementRef 32 @ViewChild('textarea') textareaElement: ElementRef
33 33
34 private addingComment = false 34 addingComment = false
35 35
36 constructor ( 36 constructor (
37 protected formValidatorService: FormValidatorService, 37 protected formValidatorService: FormValidatorService,
38 private videoCommentValidatorsService: VideoCommentValidatorsService, 38 private videoCommentValidatorsService: VideoCommentValidatorsService,
39 private notificationsService: NotificationsService, 39 private notifier: Notifier,
40 private videoCommentService: VideoCommentService, 40 private videoCommentService: VideoCommentService,
41 private authService: AuthService, 41 private authService: AuthService,
42 private modalService: NgbModal, 42 private modalService: NgbModal,
@@ -70,13 +70,13 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
70 } 70 }
71 71
72 onValidKey () { 72 onValidKey () {
73 this.onValueChanged() 73 this.check()
74 if (!this.form.valid) return 74 if (!this.form.valid) return
75 75
76 this.formValidated() 76 this.formValidated()
77 } 77 }
78 78
79 openVisitorModal (event) { 79 openVisitorModal (event: any) {
80 if (this.user === null) { // we only open it for visitors 80 if (this.user === null) { // we only open it for visitors
81 // fixing ng-bootstrap ModalService and the "Expression Changed After It Has Been Checked" Error 81 // fixing ng-bootstrap ModalService and the "Expression Changed After It Has Been Checked" Error
82 event.srcElement.blur() 82 event.srcElement.blur()
@@ -115,7 +115,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
115 err => { 115 err => {
116 this.addingComment = false 116 this.addingComment = false
117 117
118 this.notificationsService.error(this.i18n('Error'), err.text) 118 this.notifier.error(err.text)
119 } 119 }
120 ) 120 )
121 } 121 }
@@ -135,7 +135,6 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
135 135
136 gotoLogin () { 136 gotoLogin () {
137 this.hideVisitorModal() 137 this.hideVisitorModal()
138 this.authService.redirectUrl = this.router.url
139 this.router.navigate([ '/login' ]) 138 this.router.navigate([ '/login' ])
140 } 139 }
141 140
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.scss b/client/src/app/videos/+video-watch/comment/video-comment.component.scss
index 84da5727e..731ecbf8f 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.component.scss
+++ b/client/src/app/videos/+video-watch/comment/video-comment.component.scss
@@ -41,7 +41,7 @@
41 } 41 }
42 42
43 .comment-date { 43 .comment-date {
44 color: #585858; 44 color: $grey-foreground-color;
45 margin-left: 10px; 45 margin-left: 10px;
46 } 46 }
47 } 47 }
@@ -69,7 +69,7 @@
69 69
70 .comment-action-reply, 70 .comment-action-reply,
71 .comment-action-delete { 71 .comment-action-delete {
72 color: #585858; 72 color: $grey-foreground-color;
73 cursor: pointer; 73 cursor: pointer;
74 margin-right: 10px; 74 margin-right: 10px;
75 75
@@ -108,4 +108,4 @@
108 .root-comment { 108 .root-comment {
109 font-size: 14px; 109 font-size: 14px;
110 } 110 }
111} \ No newline at end of file 111}
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.ts b/client/src/app/videos/+video-watch/comment/video-comment.component.ts
index e90008de9..aba7f9d1c 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment.component.ts
@@ -1,11 +1,10 @@
1import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'
2import { LinkifierService } from '@app/videos/+video-watch/comment/linkifier.service'
3import * as sanitizeHtml from 'sanitize-html'
4import { UserRight } from '../../../../../../shared/models/users' 2import { UserRight } from '../../../../../../shared/models/users'
5import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' 3import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model'
6import { AuthService } from '../../../core/auth' 4import { AuthService } from '../../../core/auth'
7import { Video } from '../../../shared/video/video.model' 5import { Video } from '../../../shared/video/video.model'
8import { VideoComment } from './video-comment.model' 6import { VideoComment } from './video-comment.model'
7import { HtmlRendererService } from '@app/shared/renderer'
9 8
10@Component({ 9@Component({
11 selector: 'my-video-comment', 10 selector: 'my-video-comment',
@@ -26,10 +25,10 @@ export class VideoCommentComponent implements OnInit, OnChanges {
26 @Output() resetReply = new EventEmitter() 25 @Output() resetReply = new EventEmitter()
27 26
28 sanitizedCommentHTML = '' 27 sanitizedCommentHTML = ''
29 newParentComments = [] 28 newParentComments: VideoComment[] = []
30 29
31 constructor ( 30 constructor (
32 private linkifierService: LinkifierService, 31 private htmlRenderer: HtmlRendererService,
33 private authService: AuthService 32 private authService: AuthService
34 ) {} 33 ) {}
35 34
@@ -87,27 +86,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
87 } 86 }
88 87
89 private init () { 88 private init () {
90 // Convert possible markdown to html 89 this.sanitizedCommentHTML = this.htmlRenderer.toSafeHtml(this.comment.text)
91 const html = this.linkifierService.linkify(this.comment.text)
92
93 this.sanitizedCommentHTML = sanitizeHtml(html, {
94 allowedTags: [ 'a', 'p', 'span', 'br' ],
95 allowedSchemes: [ 'http', 'https' ],
96 allowedAttributes: {
97 'a': [ 'href', 'class', 'target' ]
98 },
99 transformTags: {
100 a: (tagName, attribs) => {
101 return {
102 tagName,
103 attribs: Object.assign(attribs, {
104 target: '_blank',
105 rel: 'noopener noreferrer'
106 })
107 }
108 }
109 }
110 })
111 90
112 this.newParentComments = this.parentComments.concat([ this.comment ]) 91 this.newParentComments = this.parentComments.concat([ this.comment ])
113 } 92 }
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.model.ts b/client/src/app/videos/+video-watch/comment/video-comment.model.ts
index fe591811e..824fb24c3 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.model.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment.model.ts
@@ -14,7 +14,7 @@ export class VideoComment implements VideoCommentServerModel {
14 account: AccountInterface 14 account: AccountInterface
15 totalReplies: number 15 totalReplies: number
16 by: string 16 by: string
17 accountAvatarUrl 17 accountAvatarUrl: string
18 18
19 constructor (hash: VideoCommentServerModel) { 19 constructor (hash: VideoCommentServerModel) {
20 this.id = hash.id 20 this.id = hash.id
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.service.ts b/client/src/app/videos/+video-watch/comment/video-comment.service.ts
index 9bcb4b7de..b8e5878c5 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.service.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment.service.ts
@@ -1,7 +1,7 @@
1import { catchError, map } from 'rxjs/operators' 1import { catchError, map } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http' 2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { lineFeedToHtml } from '@app/shared/misc/utils' 4import { objectLineFeedToHtml } from '@app/shared/misc/utils'
5import { Observable } from 'rxjs' 5import { Observable } from 'rxjs'
6import { ResultList, FeedFormat } from '../../../../../../shared/models' 6import { ResultList, FeedFormat } from '../../../../../../shared/models'
7import { 7import {
@@ -28,22 +28,22 @@ export class VideoCommentService {
28 28
29 addCommentThread (videoId: number | string, comment: VideoCommentCreate) { 29 addCommentThread (videoId: number | string, comment: VideoCommentCreate) {
30 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' 30 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads'
31 const normalizedComment = lineFeedToHtml(comment, 'text') 31 const normalizedComment = objectLineFeedToHtml(comment, 'text')
32 32
33 return this.authHttp.post(url, normalizedComment) 33 return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment)
34 .pipe( 34 .pipe(
35 map(data => this.extractVideoComment(data['comment'])), 35 map(data => this.extractVideoComment(data.comment)),
36 catchError(err => this.restExtractor.handleError(err)) 36 catchError(err => this.restExtractor.handleError(err))
37 ) 37 )
38 } 38 }
39 39
40 addCommentReply (videoId: number | string, inReplyToCommentId: number, comment: VideoCommentCreate) { 40 addCommentReply (videoId: number | string, inReplyToCommentId: number, comment: VideoCommentCreate) {
41 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId 41 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId
42 const normalizedComment = lineFeedToHtml(comment, 'text') 42 const normalizedComment = objectLineFeedToHtml(comment, 'text')
43 43
44 return this.authHttp.post(url, normalizedComment) 44 return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment)
45 .pipe( 45 .pipe(
46 map(data => this.extractVideoComment(data[ 'comment' ])), 46 map(data => this.extractVideoComment(data.comment)),
47 catchError(err => this.restExtractor.handleError(err)) 47 catchError(err => this.restExtractor.handleError(err))
48 ) 48 )
49 } 49 }
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.html b/client/src/app/videos/+video-watch/comment/video-comments.component.html
index 42e129d65..44016d8ad 100644
--- a/client/src/app/videos/+video-watch/comment/video-comments.component.html
+++ b/client/src/app/videos/+video-watch/comment/video-comments.component.html
@@ -4,7 +4,7 @@
4 Comments 4 Comments
5 </div> 5 </div>
6 6
7 <my-video-feed [syndicationItems]="syndicationItems"></my-video-feed> 7 <my-feed [syndicationItems]="syndicationItems"></my-feed>
8 </div> 8 </div>
9 9
10 <ng-template [ngIf]="video.commentsEnabled === true"> 10 <ng-template [ngIf]="video.commentsEnabled === true">
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.scss b/client/src/app/videos/+video-watch/comment/video-comments.component.scss
index dbb44c66c..575e331e4 100644
--- a/client/src/app/videos/+video-watch/comment/video-comments.component.scss
+++ b/client/src/app/videos/+video-watch/comment/video-comments.component.scss
@@ -23,7 +23,7 @@
23 margin-right: 0; 23 margin-right: 0;
24} 24}
25 25
26my-video-feed { 26my-feed {
27 display: inline-block; 27 display: inline-block;
28 margin-left: 5px; 28 margin-left: 5px;
29} 29}
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts
index c864d82b7..2616820d2 100644
--- a/client/src/app/videos/+video-watch/comment/video-comments.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comments.component.ts
@@ -1,17 +1,17 @@
1import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild, ElementRef } from '@angular/core' 1import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core'
2import { ActivatedRoute } from '@angular/router' 2import { ActivatedRoute } from '@angular/router'
3import { ConfirmService } from '@app/core' 3import { ConfirmService, Notifier } from '@app/core'
4import { NotificationsService } from 'angular2-notifications'
5import { Subscription } from 'rxjs' 4import { Subscription } from 'rxjs'
6import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' 5import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model'
7import { AuthService } from '../../../core/auth' 6import { AuthService } from '../../../core/auth'
8import { ComponentPagination } from '../../../shared/rest/component-pagination.model' 7import { ComponentPagination, hasMoreItems } from '../../../shared/rest/component-pagination.model'
9import { User } from '../../../shared/users' 8import { User } from '../../../shared/users'
10import { VideoSortField } from '../../../shared/video/sort-field.type' 9import { VideoSortField } from '../../../shared/video/sort-field.type'
11import { VideoDetails } from '../../../shared/video/video-details.model' 10import { VideoDetails } from '../../../shared/video/video-details.model'
12import { VideoComment } from './video-comment.model' 11import { VideoComment } from './video-comment.model'
13import { VideoCommentService } from './video-comment.service' 12import { VideoCommentService } from './video-comment.service'
14import { I18n } from '@ngx-translate/i18n-polyfill' 13import { I18n } from '@ngx-translate/i18n-polyfill'
14import { Syndication } from '@app/shared/video/syndication.model'
15 15
16@Component({ 16@Component({
17 selector: 'my-video-comments', 17 selector: 'my-video-comments',
@@ -35,13 +35,13 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
35 threadComments: { [ id: number ]: VideoCommentThreadTree } = {} 35 threadComments: { [ id: number ]: VideoCommentThreadTree } = {}
36 threadLoading: { [ id: number ]: boolean } = {} 36 threadLoading: { [ id: number ]: boolean } = {}
37 37
38 syndicationItems = [] 38 syndicationItems: Syndication[] = []
39 39
40 private sub: Subscription 40 private sub: Subscription
41 41
42 constructor ( 42 constructor (
43 private authService: AuthService, 43 private authService: AuthService,
44 private notificationsService: NotificationsService, 44 private notifier: Notifier,
45 private confirmService: ConfirmService, 45 private confirmService: ConfirmService,
46 private videoCommentService: VideoCommentService, 46 private videoCommentService: VideoCommentService,
47 private activatedRoute: ActivatedRoute, 47 private activatedRoute: ActivatedRoute,
@@ -83,15 +83,11 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
83 this.highlightedThread = new VideoComment(res.comment) 83 this.highlightedThread = new VideoComment(res.comment)
84 84
85 // Scroll to the highlighted thread 85 // Scroll to the highlighted thread
86 setTimeout(() => { 86 setTimeout(() => this.commentHighlightBlock.nativeElement.scrollIntoView(), 0)
87 // -60 because of the fixed header
88 const scrollY = this.commentHighlightBlock.nativeElement.offsetTop - 60
89 window.scroll(0, scrollY)
90 }, 500)
91 } 87 }
92 }, 88 },
93 89
94 err => this.notificationsService.error(this.i18n('Error'), err.message) 90 err => this.notifier.error(err.message)
95 ) 91 )
96 } 92 }
97 93
@@ -103,7 +99,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
103 this.componentPagination.totalItems = res.totalComments 99 this.componentPagination.totalItems = res.totalComments
104 }, 100 },
105 101
106 err => this.notificationsService.error(this.i18n('Error'), err.message) 102 err => this.notifier.error(err.message)
107 ) 103 )
108 } 104 }
109 105
@@ -154,7 +150,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
154 if (this.highlightedThread.id === commentToDelete.id) this.highlightedThread = undefined 150 if (this.highlightedThread.id === commentToDelete.id) this.highlightedThread = undefined
155 }, 151 },
156 152
157 err => this.notificationsService.error(this.i18n('Error'), err.message) 153 err => this.notifier.error(err.message)
158 ) 154 )
159 } 155 }
160 156
@@ -165,22 +161,11 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
165 onNearOfBottom () { 161 onNearOfBottom () {
166 this.componentPagination.currentPage++ 162 this.componentPagination.currentPage++
167 163
168 if (this.hasMoreComments()) { 164 if (hasMoreItems(this.componentPagination)) {
169 this.loadMoreComments() 165 this.loadMoreComments()
170 } 166 }
171 } 167 }
172 168
173 private hasMoreComments () {
174 // No results
175 if (this.componentPagination.totalItems === 0) return false
176
177 // Not loaded yet
178 if (!this.componentPagination.totalItems) return true
179
180 const maxPage = this.componentPagination.totalItems / this.componentPagination.itemsPerPage
181 return maxPage > this.componentPagination.currentPage
182 }
183
184 private deleteLocalCommentThread (parentComment: VideoCommentThreadTree, commentToDelete: VideoComment) { 169 private deleteLocalCommentThread (parentComment: VideoCommentThreadTree, commentToDelete: VideoComment) {
185 for (const commentChild of parentComment.children) { 170 for (const commentChild of parentComment.children) {
186 if (commentChild.comment.id === commentToDelete.id) { 171 if (commentChild.comment.id === commentToDelete.id) {
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html b/client/src/app/videos/+video-watch/modal/video-blacklist.component.html
index c436501b4..1a87bdcd4 100644
--- a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-blacklist.component.html
@@ -1,7 +1,7 @@
1<ng-template #modal> 1<ng-template #modal>
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Blacklist video</h4> 3 <h4 i18n class="modal-title">Blacklist video</h4>
4 <span class="close" aria-label="Close" role="button" (click)="hide()"></span> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body"> 7 <div class="modal-body">
@@ -15,6 +15,13 @@
15 </div> 15 </div>
16 </div> 16 </div>
17 17
18 <div class="form-group" *ngIf="video.isLocal">
19 <my-peertube-checkbox
20 inputName="unfederate" formControlName="unfederate"
21 i18n-labelText labelText="Unfederate the video (ask for its deletion from the remote instances)"
22 ></my-peertube-checkbox>
23 </div>
24
18 <div class="form-group inputs"> 25 <div class="form-group inputs">
19 <span i18n class="action-button action-button-cancel" (click)="hide()"> 26 <span i18n class="action-button action-button-cancel" (click)="hide()">
20 Cancel 27 Cancel
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts b/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts
index 2c123ebed..50a7cadd1 100644
--- a/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts
@@ -1,12 +1,11 @@
1import { Component, Input, OnInit, ViewChild } from '@angular/core' 1import { Component, Input, OnInit, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier, RedirectService } from '@app/core'
3import { FormReactive, VideoBlacklistService, VideoBlacklistValidatorsService } from '../../../shared/index' 3import { FormReactive, VideoBlacklistService, VideoBlacklistValidatorsService } from '../../../shared/index'
4import { VideoDetails } from '../../../shared/video/video-details.model' 4import { VideoDetails } from '../../../shared/video/video-details.model'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
9import { RedirectService } from '@app/core'
10 9
11@Component({ 10@Component({
12 selector: 'my-video-blacklist', 11 selector: 'my-video-blacklist',
@@ -27,7 +26,7 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
27 private modalService: NgbModal, 26 private modalService: NgbModal,
28 private videoBlacklistValidatorsService: VideoBlacklistValidatorsService, 27 private videoBlacklistValidatorsService: VideoBlacklistValidatorsService,
29 private videoBlacklistService: VideoBlacklistService, 28 private videoBlacklistService: VideoBlacklistService,
30 private notificationsService: NotificationsService, 29 private notifier: Notifier,
31 private redirectService: RedirectService, 30 private redirectService: RedirectService,
32 private i18n: I18n 31 private i18n: I18n
33 ) { 32 ) {
@@ -35,9 +34,12 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
35 } 34 }
36 35
37 ngOnInit () { 36 ngOnInit () {
37 const defaultValues = { unfederate: 'true' }
38
38 this.buildForm({ 39 this.buildForm({
39 reason: this.videoBlacklistValidatorsService.VIDEO_BLACKLIST_REASON 40 reason: this.videoBlacklistValidatorsService.VIDEO_BLACKLIST_REASON,
40 }) 41 unfederate: null
42 }, defaultValues)
41 } 43 }
42 44
43 show () { 45 show () {
@@ -51,16 +53,17 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
51 53
52 blacklist () { 54 blacklist () {
53 const reason = this.form.value[ 'reason' ] || undefined 55 const reason = this.form.value[ 'reason' ] || undefined
56 const unfederate = this.video.isLocal ? this.form.value[ 'unfederate' ] : undefined
54 57
55 this.videoBlacklistService.blacklistVideo(this.video.id, reason) 58 this.videoBlacklistService.blacklistVideo(this.video.id, reason, unfederate)
56 .subscribe( 59 .subscribe(
57 () => { 60 () => {
58 this.notificationsService.success(this.i18n('Success'), this.i18n('Video blacklisted.')) 61 this.notifier.success(this.i18n('Video blacklisted.'))
59 this.hide() 62 this.hide()
60 this.redirectService.redirectToHomepage() 63 this.redirectService.redirectToHomepage()
61 }, 64 },
62 65
63 err => this.notificationsService.error(this.i18n('Error'), err.message) 66 err => this.notifier.error(err.message)
64 ) 67 )
65 } 68 }
66} 69}
diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.html b/client/src/app/videos/+video-watch/modal/video-download.component.html
index f46f92a17..2bb5d6d37 100644
--- a/client/src/app/videos/+video-watch/modal/video-download.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-download.component.html
@@ -1,7 +1,7 @@
1<ng-template #modal let-hide="close"> 1<ng-template #modal let-hide="close">
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Download video</h4> 3 <h4 i18n class="modal-title">Download video</h4>
4 <span class="close" aria-hidden="true" (click)="hide()"></span> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body"> 7 <div class="modal-body">
diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.ts b/client/src/app/videos/+video-watch/modal/video-download.component.ts
index b1b2c0623..834385771 100644
--- a/client/src/app/videos/+video-watch/modal/video-download.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-download.component.ts
@@ -2,7 +2,7 @@ import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'
2import { VideoDetails } from '../../../shared/video/video-details.model' 2import { VideoDetails } from '../../../shared/video/video-details.model'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { NotificationsService } from 'angular2-notifications' 5import { Notifier } from '@app/core'
6 6
7@Component({ 7@Component({
8 selector: 'my-video-download', 8 selector: 'my-video-download',
@@ -18,7 +18,7 @@ export class VideoDownloadComponent implements OnInit {
18 resolutionId: number | string = -1 18 resolutionId: number | string = -1
19 19
20 constructor ( 20 constructor (
21 private notificationsService: NotificationsService, 21 private notifier: Notifier,
22 private modalService: NgbModal, 22 private modalService: NgbModal,
23 private i18n: I18n 23 private i18n: I18n
24 ) { } 24 ) { }
@@ -63,6 +63,6 @@ export class VideoDownloadComponent implements OnInit {
63 } 63 }
64 64
65 activateCopiedMessage () { 65 activateCopiedMessage () {
66 this.notificationsService.success(this.i18n('Success'), this.i18n('Copied')) 66 this.notifier.success(this.i18n('Copied'))
67 } 67 }
68} 68}
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.html b/client/src/app/videos/+video-watch/modal/video-report.component.html
index 8d9a49276..b9434da26 100644
--- a/client/src/app/videos/+video-watch/modal/video-report.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-report.component.html
@@ -1,11 +1,16 @@
1<ng-template #modal> 1<ng-template #modal>
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Report video</h4> 3 <h4 i18n class="modal-title">Report video</h4>
4 <span class="close" aria-label="Close" role="button" (click)="hide()"></span> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body"> 7 <div class="modal-body">
8 8
9 <div i18n class="information">
10 Your report will be sent to moderators of {{ currentHost }}.
11 <ng-container *ngIf="isRemoteVideo()"> It will be forwarded to origin instance {{ originHost }} too.</ng-container>
12 </div>
13
9 <form novalidate [formGroup]="form" (ngSubmit)="report()"> 14 <form novalidate [formGroup]="form" (ngSubmit)="report()">
10 <div class="form-group"> 15 <div class="form-group">
11 <textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }"> 16 <textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }">
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.scss b/client/src/app/videos/+video-watch/modal/video-report.component.scss
index afcdb9a16..4713660a2 100644
--- a/client/src/app/videos/+video-watch/modal/video-report.component.scss
+++ b/client/src/app/videos/+video-watch/modal/video-report.component.scss
@@ -1,6 +1,10 @@
1@import 'variables'; 1@import 'variables';
2@import 'mixins'; 2@import 'mixins';
3 3
4.information {
5 margin-bottom: 20px;
6}
7
4textarea { 8textarea {
5 @include peertube-textarea(100%, 100px); 9 @include peertube-textarea(100%, 100px);
6} 10}
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.ts b/client/src/app/videos/+video-watch/modal/video-report.component.ts
index 297afb19f..911f3b447 100644
--- a/client/src/app/videos/+video-watch/modal/video-report.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-report.component.ts
@@ -1,5 +1,5 @@
1import { Component, Input, OnInit, ViewChild } from '@angular/core' 1import { Component, Input, OnInit, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { FormReactive, VideoAbuseService } from '../../../shared/index' 3import { FormReactive, VideoAbuseService } from '../../../shared/index'
4import { VideoDetails } from '../../../shared/video/video-details.model' 4import { VideoDetails } from '../../../shared/video/video-details.model'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -27,12 +27,24 @@ export class VideoReportComponent extends FormReactive implements OnInit {
27 private modalService: NgbModal, 27 private modalService: NgbModal,
28 private videoAbuseValidatorsService: VideoAbuseValidatorsService, 28 private videoAbuseValidatorsService: VideoAbuseValidatorsService,
29 private videoAbuseService: VideoAbuseService, 29 private videoAbuseService: VideoAbuseService,
30 private notificationsService: NotificationsService, 30 private notifier: Notifier,
31 private i18n: I18n 31 private i18n: I18n
32 ) { 32 ) {
33 super() 33 super()
34 } 34 }
35 35
36 get currentHost () {
37 return window.location.host
38 }
39
40 get originHost () {
41 if (this.isRemoteVideo()) {
42 return this.video.account.host
43 }
44
45 return ''
46 }
47
36 ngOnInit () { 48 ngOnInit () {
37 this.buildForm({ 49 this.buildForm({
38 reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON 50 reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON
@@ -54,11 +66,15 @@ export class VideoReportComponent extends FormReactive implements OnInit {
54 this.videoAbuseService.reportVideo(this.video.id, reason) 66 this.videoAbuseService.reportVideo(this.video.id, reason)
55 .subscribe( 67 .subscribe(
56 () => { 68 () => {
57 this.notificationsService.success(this.i18n('Success'), this.i18n('Video reported.')) 69 this.notifier.success(this.i18n('Video reported.'))
58 this.hide() 70 this.hide()
59 }, 71 },
60 72
61 err => this.notificationsService.error(this.i18n('Error'), err.message) 73 err => this.notifier.error(err.message)
62 ) 74 )
63 } 75 }
76
77 isRemoteVideo () {
78 return !this.video.isLocal
79 }
64} 80}
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.html b/client/src/app/videos/+video-watch/modal/video-share.component.html
index 301f67f2d..9f3c37fe8 100644
--- a/client/src/app/videos/+video-watch/modal/video-share.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-share.component.html
@@ -1,7 +1,7 @@
1<ng-template #modal let-hide="close"> 1<ng-template #modal let-hide="close">
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Share</h4> 3 <h4 i18n class="modal-title">Share</h4>
4 <span class="close" aria-hidden="true" (click)="hide()"></span> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body"> 7 <div class="modal-body">
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.ts b/client/src/app/videos/+video-watch/modal/video-share.component.ts
index 17e2b31e1..c6205e355 100644
--- a/client/src/app/videos/+video-watch/modal/video-share.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-share.component.ts
@@ -1,5 +1,5 @@
1import { Component, ElementRef, Input, ViewChild } from '@angular/core' 1import { Component, ElementRef, Input, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { VideoDetails } from '../../../shared/video/video-details.model' 3import { VideoDetails } from '../../../shared/video/video-details.model'
4import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils' 4import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -23,7 +23,7 @@ export class VideoShareComponent {
23 23
24 constructor ( 24 constructor (
25 private modalService: NgbModal, 25 private modalService: NgbModal,
26 private notificationsService: NotificationsService, 26 private notifier: Notifier,
27 private i18n: I18n 27 private i18n: I18n
28 ) { } 28 ) { }
29 29
@@ -49,7 +49,7 @@ export class VideoShareComponent {
49 } 49 }
50 50
51 activateCopiedMessage () { 51 activateCopiedMessage () {
52 this.notificationsService.success(this.i18n('Success'), this.i18n('Copied')) 52 this.notifier.success(this.i18n('Copied'))
53 } 53 }
54 54
55 getStartCheckboxLabel () { 55 getStartCheckboxLabel () {
diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.html b/client/src/app/videos/+video-watch/modal/video-support.component.html
index 00c304709..2a05224a8 100644
--- a/client/src/app/videos/+video-watch/modal/video-support.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-support.component.html
@@ -1,7 +1,7 @@
1<ng-template #modal let-hide="close"> 1<ng-template #modal let-hide="close">
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Support</h4> 3 <h4 i18n class="modal-title">Support</h4>
4 <span class="close" aria-label="Close" role="button" (click)="hide()"></span> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body" [innerHTML]="videoHTMLSupport"></div> 7 <div class="modal-body" [innerHTML]="videoHTMLSupport"></div>
diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.ts b/client/src/app/videos/+video-watch/modal/video-support.component.ts
index 154002120..deb8fbc67 100644
--- a/client/src/app/videos/+video-watch/modal/video-support.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-support.component.ts
@@ -1,8 +1,7 @@
1import { Component, Input, ViewChild } from '@angular/core' 1import { Component, Input, ViewChild } from '@angular/core'
2import { MarkdownService } from '@app/videos/shared'
3
4import { VideoDetails } from '../../../shared/video/video-details.model' 2import { VideoDetails } from '../../../shared/video/video-details.model'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { MarkdownService } from '@app/shared/renderer'
6 5
7@Component({ 6@Component({
8 selector: 'my-video-support', 7 selector: 'my-video-support',
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html
index ba04d638f..1875230d8 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -53,55 +53,57 @@
53 <div 53 <div
54 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()" 54 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()"
55 class="action-button action-button-like" role="button" [attr.aria-pressed]="userRating === 'like'" 55 class="action-button action-button-like" role="button" [attr.aria-pressed]="userRating === 'like'"
56 i18n-title title="Like this video"
56 > 57 >
57 <span class="icon icon-like" i18n-title title="Like this video" ></span> 58 <my-global-icon iconName="like"></my-global-icon>
58 </div> 59 </div>
59 60
60 <div 61 <div
61 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()" 62 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()"
62 class="action-button action-button-dislike" role="button" [attr.aria-pressed]="userRating === 'dislike'" 63 class="action-button action-button-dislike" role="button" [attr.aria-pressed]="userRating === 'dislike'"
64 i18n-title title="Dislike this video"
63 > 65 >
64 <span class="icon icon-dislike" i18n-title title="Dislike this video"></span> 66 <my-global-icon iconName="dislike"></my-global-icon>
65 </div> 67 </div>
66 68
67 <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support"> 69 <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support">
68 <span class="icon icon-support"></span> 70 <my-global-icon iconName="heart"></my-global-icon>
69 <span class="icon-text" i18n>Support</span> 71 <span class="icon-text" i18n>Support</span>
70 </div> 72 </div>
71 73
72 <div (click)="showShareModal()" class="action-button action-button-share" role="button"> 74 <div (click)="showShareModal()" class="action-button action-button-share" role="button">
73 <span class="icon icon-share"></span> 75 <my-global-icon iconName="share"></my-global-icon>
74 <span class="icon-text" i18n>Share</span> 76 <span class="icon-text" i18n>Share</span>
75 </div> 77 </div>
76 78
77 <div class="action-more" ngbDropdown placement="top" role="button"> 79 <div class="action-more" ngbDropdown placement="top" role="button">
78 <div class="action-button" ngbDropdownToggle role="button"> 80 <div class="action-button" ngbDropdownToggle role="button">
79 <span class="icon icon-more"></span> 81 <my-global-icon class="more-icon" iconName="more"></my-global-icon>
80 </div> 82 </div>
81 83
82 <div ngbDropdownMenu> 84 <div ngbDropdownMenu>
83 <a *ngIf="isVideoDownloadable()" class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)"> 85 <a *ngIf="isVideoDownloadable()" class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)">
84 <span class="icon icon-download"></span> <ng-container i18n>Download</ng-container> 86 <my-global-icon iconName="download"></my-global-icon> <ng-container i18n>Download</ng-container>
85 </a> 87 </a>
86 88
87 <a *ngIf="isUserLoggedIn()" class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)"> 89 <a *ngIf="isUserLoggedIn()" class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)">
88 <span class="icon icon-alert"></span> <ng-container i18n>Report</ng-container> 90 <my-global-icon iconName="alert"></my-global-icon> <ng-container i18n>Report</ng-container>
89 </a> 91 </a>
90 92
91 <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]"> 93 <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]">
92 <span class="icon icon-edit"></span> <ng-container i18n>Update</ng-container> 94 <my-global-icon iconName="edit"></my-global-icon> <ng-container i18n>Update</ng-container>
93 </a> 95 </a>
94 96
95 <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)"> 97 <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)">
96 <span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container> 98 <my-global-icon iconName="no"></my-global-icon> <ng-container i18n>Blacklist</ng-container>
97 </a> 99 </a>
98 100
99 <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)"> 101 <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)">
100 <span class="icon icon-unblacklist"></span> <ng-container i18n>Unblacklist</ng-container> 102 <my-global-icon iconName="undo"></my-global-icon> <ng-container i18n>Unblacklist</ng-container>
101 </a> 103 </a>
102 104
103 <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)"> 105 <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)">
104 <span class="icon icon-delete"></span> <ng-container i18n>Delete</ng-container> 106 <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete</ng-container>
105 </a> 107 </a>
106 </div> 108 </div>
107 </div> 109 </div>
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss
index f31e4694a..b03ed197d 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -162,7 +162,7 @@ $other-videos-width: 260px;
162 } 162 }
163 } 163 }
164 164
165 my-video-feed { 165 my-feed {
166 margin-left: 5px; 166 margin-left: 5px;
167 margin-top: 1px; 167 margin-top: 1px;
168 } 168 }
@@ -183,6 +183,8 @@ $other-videos-width: 260px;
183 .action-button { 183 .action-button {
184 @include peertube-button; 184 @include peertube-button;
185 @include grey-button; 185 @include grey-button;
186 @include button-with-icon(21px, 0, -1px);
187 @include apply-svg-color($grey-foreground-color);
186 188
187 font-size: 15px; 189 font-size: 15px;
188 font-weight: $font-semibold; 190 font-weight: $font-semibold;
@@ -194,53 +196,25 @@ $other-videos-width: 260px;
194 display: none; 196 display: none;
195 } 197 }
196 198
197 .icon {
198 @include icon(21px);
199
200 position: relative;
201 top: -2px;
202
203 &.icon-like {
204 background-image: url('../../../assets/images/video/like-grey.svg');
205 }
206
207 &.icon-dislike {
208 background-image: url('../../../assets/images/video/dislike-grey.svg');
209 }
210
211 &.icon-support {
212 background-image: url('../../../assets/images/video/heart.svg');
213 }
214
215 &.icon-share {
216 background-image: url('../../../assets/images/video/share.svg');
217 }
218
219 &.icon-more {
220 background-image: url('../../../assets/images/video/more.svg');
221 top: -1px;
222 }
223 }
224
225 .icon-text {
226 margin-left: 3px;
227 }
228
229 &.action-button-like.activated { 199 &.action-button-like.activated {
230 background-color: $green; 200 background-color: $green;
231 201
232 .icon-like { 202 my-global-icon {
233 background-image: url('../../../assets/images/video/like-white.svg'); 203 @include apply-svg-color(#fff);
234 } 204 }
235 } 205 }
236 206
237 &.action-button-dislike.activated { 207 &.action-button-dislike.activated {
238 background-color: $red; 208 background-color: $red;
239 209
240 .icon-dislike { 210 my-global-icon {
241 background-image: url('../../../assets/images/video/dislike-white.svg'); 211 @include apply-svg-color(#fff);
242 } 212 }
243 } 213 }
214
215 .icon-text {
216 margin-left: 3px;
217 }
244 } 218 }
245 219
246 .action-more { 220 .action-more {
@@ -249,36 +223,12 @@ $other-videos-width: 260px;
249 .dropdown-menu .dropdown-item { 223 .dropdown-menu .dropdown-item {
250 padding: 6px 24px; 224 padding: 6px 24px;
251 225
252 .icon { 226 my-global-icon {
253 @include icon(24px); 227 width: 24px;
254 228
255 margin-right: 10px; 229 margin-right: 10px;
256 position: relative; 230 position: relative;
257 top: -1px; 231 top: -2px;
258
259 &.icon-download {
260 background-image: url('../../../assets/images/video/download-black.svg');
261 }
262
263 &.icon-edit {
264 background-image: url('../../../assets/images/global/edit-black.svg');
265 }
266
267 &.icon-alert {
268 background-image: url('../../../assets/images/video/alert.svg');
269 }
270
271 &.icon-blacklist {
272 background-image: url('../../../assets/images/video/blacklist.svg');
273 }
274
275 &.icon-unblacklist {
276 background-image: url('../../../assets/images/global/undo.svg');
277 }
278
279 &.icon-delete {
280 background-image: url('../../../assets/images/global/delete-black.svg');
281 }
282 } 232 }
283 } 233 }
284 } 234 }
@@ -320,7 +270,7 @@ $other-videos-width: 260px;
320 .video-info-description-more { 270 .video-info-description-more {
321 cursor: pointer; 271 cursor: pointer;
322 font-weight: $font-semibold; 272 font-weight: $font-semibold;
323 color: #585858; 273 color: $grey-foreground-color;
324 font-size: 14px; 274 font-size: 14px;
325 275
326 .glyphicon { 276 .glyphicon {
@@ -339,7 +289,7 @@ $other-videos-width: 260px;
339 min-width: 91px; 289 min-width: 91px;
340 padding-right: 5px; 290 padding-right: 5px;
341 display: inline-block; 291 display: inline-block;
342 color: #585858; 292 color: $grey-foreground-color;
343 font-weight: $font-bold; 293 font-weight: $font-bold;
344 } 294 }
345 295
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 a7fd45695..4dbfa41e5 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -5,30 +5,29 @@ import { RedirectService } from '@app/core/routing/redirect.service'
5import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' 5import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
6import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' 6import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
7import { MetaService } from '@ngx-meta/core' 7import { MetaService } from '@ngx-meta/core'
8import { NotificationsService } from 'angular2-notifications' 8import { Notifier, ServerService } from '@app/core'
9import { forkJoin, Subscription } from 'rxjs' 9import { forkJoin, Subscription } from 'rxjs'
10import * as videojs from 'video.js'
11import 'videojs-hotkeys'
12import { Hotkey, HotkeysService } from 'angular2-hotkeys' 10import { Hotkey, HotkeysService } from 'angular2-hotkeys'
13import * as WebTorrent from 'webtorrent' 11import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared'
14import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoRateType, VideoState } from '../../../../../shared'
15import '../../../assets/player/peertube-videojs-plugin'
16import { AuthService, ConfirmService } from '../../core' 12import { AuthService, ConfirmService } from '../../core'
17import { RestExtractor, VideoBlacklistService } from '../../shared' 13import { RestExtractor, VideoBlacklistService } from '../../shared'
18import { VideoDetails } from '../../shared/video/video-details.model' 14import { VideoDetails } from '../../shared/video/video-details.model'
19import { VideoService } from '../../shared/video/video.service' 15import { VideoService } from '../../shared/video/video.service'
20import { MarkdownService } from '../shared'
21import { VideoDownloadComponent } from './modal/video-download.component' 16import { VideoDownloadComponent } from './modal/video-download.component'
22import { VideoReportComponent } from './modal/video-report.component' 17import { VideoReportComponent } from './modal/video-report.component'
23import { VideoShareComponent } from './modal/video-share.component' 18import { VideoShareComponent } from './modal/video-share.component'
24import { VideoBlacklistComponent } from './modal/video-blacklist.component' 19import { VideoBlacklistComponent } from './modal/video-blacklist.component'
25import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' 20import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component'
26import { addContextMenu, getVideojsOptions, loadLocaleInVideoJS } from '../../../assets/player/peertube-player'
27import { ServerService } from '@app/core'
28import { I18n } from '@ngx-translate/i18n-polyfill' 21import { I18n } from '@ngx-translate/i18n-polyfill'
29import { environment } from '../../../environments/environment' 22import { environment } from '../../../environments/environment'
30import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
31import { VideoCaptionService } from '@app/shared/video-caption' 23import { VideoCaptionService } from '@app/shared/video-caption'
24import { MarkdownService } from '@app/shared/renderer'
25import {
26 P2PMediaLoaderOptions,
27 PeertubePlayerManager,
28 PeertubePlayerManagerOptions,
29 PlayerMode
30} from '../../../assets/player/peertube-player-manager'
32 31
33@Component({ 32@Component({
34 selector: 'my-video-watch', 33 selector: 'my-video-watch',
@@ -45,7 +44,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
45 @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent 44 @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent
46 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent 45 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
47 46
48 player: videojs.Player 47 player: any
49 playerElement: HTMLVideoElement 48 playerElement: HTMLVideoElement
50 userRating: UserVideoRateType = null 49 userRating: UserVideoRateType = null
51 video: VideoDetails = null 50 video: VideoDetails = null
@@ -60,7 +59,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
60 remoteServerDown = false 59 remoteServerDown = false
61 hotkeys: Hotkey[] 60 hotkeys: Hotkey[]
62 61
63 private videojsLocaleLoaded = false
64 private paramsSub: Subscription 62 private paramsSub: Subscription
65 63
66 constructor ( 64 constructor (
@@ -75,7 +73,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
75 private authService: AuthService, 73 private authService: AuthService,
76 private serverService: ServerService, 74 private serverService: ServerService,
77 private restExtractor: RestExtractor, 75 private restExtractor: RestExtractor,
78 private notificationsService: NotificationsService, 76 private notifier: Notifier,
79 private markdownService: MarkdownService, 77 private markdownService: MarkdownService,
80 private zone: NgZone, 78 private zone: NgZone,
81 private redirectService: RedirectService, 79 private redirectService: RedirectService,
@@ -91,7 +89,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
91 89
92 ngOnInit () { 90 ngOnInit () {
93 if ( 91 if (
94 WebTorrent.WEBRTC_SUPPORT === false || 92 !!((window as any).RTCPeerConnection || (window as any).mozRTCPeerConnection || (window as any).webkitRTCPeerConnection) === false ||
95 peertubeLocalStorage.getItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true' 93 peertubeLocalStorage.getItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true'
96 ) { 94 ) {
97 this.hasAlreadyAcceptedPrivacyConcern = true 95 this.hasAlreadyAcceptedPrivacyConcern = true
@@ -112,11 +110,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
112 ) 110 )
113 .pipe( 111 .pipe(
114 // If 401, the video is private or blacklisted so redirect to 404 112 // If 401, the video is private or blacklisted so redirect to 404
115 catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 404 ])) 113 catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ]))
116 ) 114 )
117 .subscribe(([ video, captionsResult ]) => { 115 .subscribe(([ video, captionsResult ]) => {
118 const startTime = this.route.snapshot.queryParams.start 116 const startTime = this.route.snapshot.queryParams.start
119 this.onVideoFetched(video, captionsResult.data, startTime) 117 const subtitle = this.route.snapshot.queryParams.subtitle
118 const playerMode = this.route.snapshot.queryParams.mode
119
120 this.onVideoFetched(video, captionsResult.data, { startTime, subtitle, playerMode })
120 .catch(err => this.handleError(err)) 121 .catch(err => this.handleError(err))
121 }) 122 })
122 }) 123 })
@@ -201,7 +202,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
201 202
202 error => { 203 error => {
203 this.descriptionLoading = false 204 this.descriptionLoading = false
204 this.notificationsService.error(this.i18n('Error'), error.message) 205 this.notifier.error(error.message)
205 } 206 }
206 ) 207 )
207 } 208 }
@@ -243,16 +244,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
243 244
244 this.videoBlacklistService.removeVideoFromBlacklist(this.video.id).subscribe( 245 this.videoBlacklistService.removeVideoFromBlacklist(this.video.id).subscribe(
245 () => { 246 () => {
246 this.notificationsService.success( 247 this.notifier.success(this.i18n('Video {{name}} removed from the blacklist.', { name: this.video.name }))
247 this.i18n('Success'),
248 this.i18n('Video {{name}} removed from the blacklist.', { name: this.video.name })
249 )
250 248
251 this.video.blacklisted = false 249 this.video.blacklisted = false
252 this.video.blacklistedReason = null 250 this.video.blacklistedReason = null
253 }, 251 },
254 252
255 err => this.notificationsService.error(this.i18n('Error'), err.message) 253 err => this.notifier.error(err.message)
256 ) 254 )
257 } 255 }
258 256
@@ -290,17 +288,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
290 288
291 this.videoService.removeVideo(this.video.id) 289 this.videoService.removeVideo(this.video.id)
292 .subscribe( 290 .subscribe(
293 status => { 291 () => {
294 this.notificationsService.success( 292 this.notifier.success(this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name }))
295 this.i18n('Success'),
296 this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name })
297 )
298 293
299 // Go back to the video-list. 294 // Go back to the video-list.
300 this.redirectService.redirectToHomepage() 295 this.redirectService.redirectToHomepage()
301 }, 296 },
302 297
303 error => this.notificationsService.error(this.i18n('Error'), error.message) 298 error => this.notifier.error(error.message)
304 ) 299 )
305 } 300 }
306 301
@@ -354,7 +349,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
354 return 349 return
355 } 350 }
356 351
357 this.notificationsService.error(this.i18n('Error'), errorMessage) 352 this.notifier.error(errorMessage)
358 } 353 }
359 354
360 private checkUserRating () { 355 private checkUserRating () {
@@ -369,11 +364,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
369 } 364 }
370 }, 365 },
371 366
372 err => this.notificationsService.error(this.i18n('Error'), err.message) 367 err => this.notifier.error(err.message)
373 ) 368 )
374 } 369 }
375 370
376 private async onVideoFetched (video: VideoDetails, videoCaptions: VideoCaption[], startTimeFromUrl: number) { 371 private async onVideoFetched (
372 video: VideoDetails,
373 videoCaptions: VideoCaption[],
374 urlOptions: { startTime?: number, subtitle?: string, playerMode?: string }
375 ) {
377 this.video = video 376 this.video = video
378 377
379 // Re init attributes 378 // Re init attributes
@@ -381,8 +380,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
381 this.completeDescriptionShown = false 380 this.completeDescriptionShown = false
382 this.remoteServerDown = false 381 this.remoteServerDown = false
383 382
384 let startTime = startTimeFromUrl || (this.video.userHistory ? this.video.userHistory.currentTime : 0) 383 let startTime = urlOptions.startTime || (this.video.userHistory ? this.video.userHistory.currentTime : 0)
385 // Don't start the video if we are at the end 384 // If we are at the end of the video, reset the timer
386 if (this.video.duration - startTime <= 1) startTime = 0 385 if (this.video.duration - startTime <= 1) startTime = 0
387 386
388 if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) { 387 if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) {
@@ -409,40 +408,64 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
409 src: environment.apiUrl + c.captionPath 408 src: environment.apiUrl + c.captionPath
410 })) 409 }))
411 410
412 const videojsOptions = getVideojsOptions({ 411 const options: PeertubePlayerManagerOptions = {
413 autoplay: this.isAutoplay(), 412 common: {
414 inactivityTimeout: 2500, 413 autoplay: this.isAutoplay(),
415 videoFiles: this.video.files, 414
416 videoCaptions: playerCaptions, 415 playerElement: this.playerElement,
417 playerElement: this.playerElement, 416 onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element,
418 videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null, 417
419 videoDuration: this.video.duration, 418 videoDuration: this.video.duration,
420 enableHotkeys: true, 419 enableHotkeys: true,
421 peertubeLink: false, 420 inactivityTimeout: 2500,
422 poster: this.video.previewUrl, 421 poster: this.video.previewUrl,
423 startTime, 422 startTime,
424 theaterMode: true, 423
425 language: this.localeId, 424 theaterMode: true,
426 425 captions: videoCaptions.length !== 0,
427 userWatching: this.user ? { 426 peertubeLink: false,
428 url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), 427
429 authorizationHeader: this.authService.getRequestHeaderValue() 428 videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null,
430 } : undefined 429 embedUrl: this.video.embedUrl,
431 }) 430
431 language: this.localeId,
432
433 subtitle: urlOptions.subtitle,
434
435 userWatching: this.user && this.user.videosHistoryEnabled === true ? {
436 url: this.videoService.getUserWatchingVideoUrl(this.video.uuid),
437 authorizationHeader: this.authService.getRequestHeaderValue()
438 } : undefined,
432 439
433 if (this.videojsLocaleLoaded === false) { 440 serverUrl: environment.apiUrl,
434 await loadLocaleInVideoJS(environment.apiUrl, videojs, isOnDevLocale() ? getDevLocale() : this.localeId) 441
435 this.videojsLocaleLoaded = true 442 videoCaptions: playerCaptions
443 },
444
445 webtorrent: {
446 videoFiles: this.video.files
447 }
436 } 448 }
437 449
438 const self = this 450 const mode: PlayerMode = urlOptions.playerMode === 'p2p-media-loader' ? 'p2p-media-loader' : 'webtorrent'
439 this.zone.runOutsideAngular(async () => { 451
440 videojs(this.playerElement, videojsOptions, function () { 452 if (mode === 'p2p-media-loader') {
441 self.player = this 453 const hlsPlaylist = this.video.getHlsPlaylist()
442 this.on('customError', (event, data) => self.handleError(data.err))
443 454
444 addContextMenu(self.player, self.video.embedUrl) 455 const p2pMediaLoader = {
445 }) 456 playlistUrl: hlsPlaylist.playlistUrl,
457 segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
458 redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
459 trackerAnnounce: this.video.trackerUrls,
460 videoFiles: this.video.files
461 } as P2PMediaLoaderOptions
462
463 Object.assign(options, { p2pMediaLoader })
464 }
465
466 this.zone.runOutsideAngular(async () => {
467 this.player = await PeertubePlayerManager.initialize(mode, options)
468 this.player.on('customError', ({ err }: { err: any }) => this.handleError(err))
446 }) 469 })
447 470
448 this.setVideoDescriptionHTML() 471 this.setVideoDescriptionHTML()
@@ -452,7 +475,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
452 this.checkUserRating() 475 this.checkUserRating()
453 } 476 }
454 477
455 private setRating (nextRating) { 478 private setRating (nextRating: UserVideoRateType) {
456 let method 479 let method
457 switch (nextRating) { 480 switch (nextRating) {
458 case 'like': 481 case 'like':
@@ -474,11 +497,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
474 this.userRating = nextRating 497 this.userRating = nextRating
475 }, 498 },
476 499
477 err => this.notificationsService.error(this.i18n('Error'), err.message) 500 (err: { message: string }) => this.notifier.error(err.message)
478 ) 501 )
479 } 502 }
480 503
481 private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) { 504 private updateVideoRating (oldRating: UserVideoRateType, newRating: UserVideoRateType) {
482 let likesToIncrement = 0 505 let likesToIncrement = 0
483 let dislikesToIncrement = 0 506 let dislikesToIncrement = 0
484 507
diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts
index 5582ab40f..2f448db78 100644
--- a/client/src/app/videos/+video-watch/video-watch.module.ts
+++ b/client/src/app/videos/+video-watch/video-watch.module.ts
@@ -1,9 +1,7 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { LinkifierService } from '@app/videos/+video-watch/comment/linkifier.service'
3import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' 2import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
4import { ClipboardModule } from 'ngx-clipboard' 3import { ClipboardModule } from 'ngx-clipboard'
5import { SharedModule } from '../../shared' 4import { SharedModule } from '../../shared'
6import { MarkdownService } from '../shared'
7import { VideoCommentAddComponent } from './comment/video-comment-add.component' 5import { VideoCommentAddComponent } from './comment/video-comment-add.component'
8import { VideoCommentComponent } from './comment/video-comment.component' 6import { VideoCommentComponent } from './comment/video-comment.component'
9import { VideoCommentService } from './comment/video-comment.service' 7import { VideoCommentService } from './comment/video-comment.service'
@@ -17,7 +15,6 @@ import { NgxQRCodeModule } from 'ngx-qrcode2'
17import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' 15import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
18import { VideoBlacklistComponent } from '@app/videos/+video-watch/modal/video-blacklist.component' 16import { VideoBlacklistComponent } from '@app/videos/+video-watch/modal/video-blacklist.component'
19import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module' 17import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module'
20import { TextareaAutosizeModule } from 'ngx-textarea-autosize'
21 18
22@NgModule({ 19@NgModule({
23 imports: [ 20 imports: [
@@ -26,7 +23,6 @@ import { TextareaAutosizeModule } from 'ngx-textarea-autosize'
26 ClipboardModule, 23 ClipboardModule,
27 NgbTooltipModule, 24 NgbTooltipModule,
28 NgxQRCodeModule, 25 NgxQRCodeModule,
29 TextareaAutosizeModule,
30 RecommendationsModule 26 RecommendationsModule
31 ], 27 ],
32 28
@@ -48,8 +44,6 @@ import { TextareaAutosizeModule } from 'ngx-textarea-autosize'
48 ], 44 ],
49 45
50 providers: [ 46 providers: [
51 MarkdownService,
52 LinkifierService,
53 VideoCommentService 47 VideoCommentService
54 ] 48 ]
55}) 49})
diff --git a/client/src/app/videos/shared/index.ts b/client/src/app/videos/shared/index.ts
deleted file mode 100644
index 7a66944b9..000000000
--- a/client/src/app/videos/shared/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './markdown.service'
diff --git a/client/src/app/videos/shared/markdown.service.ts b/client/src/app/videos/shared/markdown.service.ts
deleted file mode 100644
index 07017eca5..000000000
--- a/client/src/app/videos/shared/markdown.service.ts
+++ /dev/null
@@ -1,79 +0,0 @@
1import { Injectable } from '@angular/core'
2
3import * as MarkdownIt from 'markdown-it'
4
5@Injectable()
6export class MarkdownService {
7 static TEXT_RULES = [
8 'linkify',
9 'autolink',
10 'emphasis',
11 'link',
12 'newline',
13 'list'
14 ]
15 static ENHANCED_RULES = MarkdownService.TEXT_RULES.concat([ 'image' ])
16
17 private textMarkdownIt: MarkdownIt.MarkdownIt
18 private enhancedMarkdownIt: MarkdownIt.MarkdownIt
19
20 constructor () {
21 this.textMarkdownIt = this.createMarkdownIt(MarkdownService.TEXT_RULES)
22 this.enhancedMarkdownIt = this.createMarkdownIt(MarkdownService.ENHANCED_RULES)
23 }
24
25 textMarkdownToHTML (markdown: string) {
26 if (!markdown) return ''
27
28 const html = this.textMarkdownIt.render(markdown)
29 return this.avoidTruncatedTags(html)
30 }
31
32 enhancedMarkdownToHTML (markdown: string) {
33 if (!markdown) return ''
34
35 const html = this.enhancedMarkdownIt.render(markdown)
36 return this.avoidTruncatedTags(html)
37 }
38
39 private createMarkdownIt (rules: string[]) {
40 const markdownIt = new MarkdownIt('zero', { linkify: true, breaks: true })
41
42 for (let rule of rules) {
43 markdownIt.enable(rule)
44 }
45
46 this.setTargetToLinks(markdownIt)
47
48 return markdownIt
49 }
50
51 private setTargetToLinks (markdownIt: MarkdownIt.MarkdownIt) {
52 // Snippet from markdown-it documentation: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
53 const defaultRender = markdownIt.renderer.rules.link_open || function (tokens, idx, options, env, self) {
54 return self.renderToken(tokens, idx, options)
55 }
56
57 markdownIt.renderer.rules.link_open = function (tokens, index, options, env, self) {
58 const token = tokens[index]
59
60 const targetIndex = token.attrIndex('target')
61 if (targetIndex < 0) token.attrPush([ 'target', '_blank' ])
62 else token.attrs[targetIndex][1] = '_blank'
63
64 const relIndex = token.attrIndex('rel')
65 if (relIndex < 0) token.attrPush([ 'rel', 'noopener noreferrer' ])
66 else token.attrs[relIndex][1] = 'noopener noreferrer'
67
68 // pass token to default renderer.
69 return defaultRender(tokens, index, options, env, self)
70 }
71 }
72
73 private avoidTruncatedTags (html: string) {
74 return html.replace(/\*\*?([^*]+)$/, '$1')
75 .replace(/<a[^>]+>([^<]+)<\/a>\s*...((<\/p>)|(<\/li>)|(<\/strong>))?$/mi, '$1...')
76 .replace(/\[[^\]]+\]?\(?([^\)]+)$/, '$1')
77
78 }
79}
diff --git a/client/src/app/videos/video-list/video-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts
index c91c639ca..c0be4b885 100644
--- a/client/src/app/videos/video-list/video-local.component.ts
+++ b/client/src/app/videos/video-list/video-local.component.ts
@@ -2,7 +2,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { immutableAssign } from '@app/shared/misc/utils' 3import { immutableAssign } from '@app/shared/misc/utils'
4import { Location } from '@angular/common' 4import { Location } from '@angular/common'
5import { NotificationsService } from 'angular2-notifications'
6import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
7import { AbstractVideoList } from '../../shared/video/abstract-video-list' 6import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 7import { VideoSortField } from '../../shared/video/sort-field.type'
@@ -10,6 +9,8 @@ import { VideoService } from '../../shared/video/video.service'
10import { VideoFilter } from '../../../../../shared/models/videos/video-query.type' 9import { VideoFilter } from '../../../../../shared/models/videos/video-query.type'
11import { I18n } from '@ngx-translate/i18n-polyfill' 10import { I18n } from '@ngx-translate/i18n-polyfill'
12import { ScreenService } from '@app/shared/misc/screen.service' 11import { ScreenService } from '@app/shared/misc/screen.service'
12import { UserRight } from '../../../../../shared/models/users'
13import { Notifier } from '@app/core'
13 14
14@Component({ 15@Component({
15 selector: 'my-videos-local', 16 selector: 'my-videos-local',
@@ -25,7 +26,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
25 constructor ( 26 constructor (
26 protected router: Router, 27 protected router: Router,
27 protected route: ActivatedRoute, 28 protected route: ActivatedRoute,
28 protected notificationsService: NotificationsService, 29 protected notifier: Notifier,
29 protected authService: AuthService, 30 protected authService: AuthService,
30 protected location: Location, 31 protected location: Location,
31 protected i18n: I18n, 32 protected i18n: I18n,
@@ -40,6 +41,11 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
40 ngOnInit () { 41 ngOnInit () {
41 super.ngOnInit() 42 super.ngOnInit()
42 43
44 if (this.authService.isLoggedIn()) {
45 const user = this.authService.getUser()
46 this.displayModerationBlock = user.hasRight(UserRight.SEE_ALL_VIDEOS)
47 }
48
43 this.generateSyndicationList() 49 this.generateSyndicationList()
44 } 50 }
45 51
@@ -56,4 +62,10 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
56 generateSyndicationList () { 62 generateSyndicationList () {
57 this.syndicationItems = this.videoService.getVideoFeedUrls(this.sort, this.filter, this.categoryOneOf) 63 this.syndicationItems = this.videoService.getVideoFeedUrls(this.sort, this.filter, this.categoryOneOf)
58 } 64 }
65
66 toggleModerationDisplay () {
67 this.filter = this.filter === 'local' ? 'all-local' as 'all-local' : 'local' as 'local'
68
69 this.reloadVideos()
70 }
59} 71}
diff --git a/client/src/app/videos/video-list/video-overview.component.ts b/client/src/app/videos/video-list/video-overview.component.ts
index 2c6054721..7ff52b259 100644
--- a/client/src/app/videos/video-list/video-overview.component.ts
+++ b/client/src/app/videos/video-list/video-overview.component.ts
@@ -1,6 +1,5 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { AuthService } from '@app/core' 2import { AuthService, Notifier } from '@app/core'
3import { NotificationsService } from 'angular2-notifications'
4import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
5import { VideosOverview } from '@app/shared/overview/videos-overview.model' 4import { VideosOverview } from '@app/shared/overview/videos-overview.model'
6import { OverviewService } from '@app/shared/overview' 5import { OverviewService } from '@app/shared/overview'
@@ -21,7 +20,7 @@ export class VideoOverviewComponent implements OnInit {
21 20
22 constructor ( 21 constructor (
23 private i18n: I18n, 22 private i18n: I18n,
24 private notificationsService: NotificationsService, 23 private notifier: Notifier,
25 private authService: AuthService, 24 private authService: AuthService,
26 private overviewService: OverviewService 25 private overviewService: OverviewService
27 ) { } 26 ) { }
@@ -43,10 +42,7 @@ export class VideoOverviewComponent implements OnInit {
43 ) this.notResults = true 42 ) this.notResults = true
44 }, 43 },
45 44
46 err => { 45 err => this.notifier.error(err.message)
47 console.log(err)
48 this.notificationsService.error('Error', err.text)
49 }
50 ) 46 )
51 } 47 }
52 48
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts
index ac1fcfff3..f99c8abb6 100644
--- a/client/src/app/videos/video-list/video-recently-added.component.ts
+++ b/client/src/app/videos/video-list/video-recently-added.component.ts
@@ -2,13 +2,13 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { Location } from '@angular/common' 3import { Location } from '@angular/common'
4import { immutableAssign } from '@app/shared/misc/utils' 4import { immutableAssign } from '@app/shared/misc/utils'
5import { NotificationsService } from 'angular2-notifications'
6import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
7import { AbstractVideoList } from '../../shared/video/abstract-video-list' 6import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 7import { VideoSortField } from '../../shared/video/sort-field.type'
9import { VideoService } from '../../shared/video/video.service' 8import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
11import { ScreenService } from '@app/shared/misc/screen.service' 10import { ScreenService } from '@app/shared/misc/screen.service'
11import { Notifier } from '@app/core'
12 12
13@Component({ 13@Component({
14 selector: 'my-videos-recently-added', 14 selector: 'my-videos-recently-added',
@@ -24,7 +24,7 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
24 protected router: Router, 24 protected router: Router,
25 protected route: ActivatedRoute, 25 protected route: ActivatedRoute,
26 protected location: Location, 26 protected location: Location,
27 protected notificationsService: NotificationsService, 27 protected notifier: Notifier,
28 protected authService: AuthService, 28 protected authService: AuthService,
29 protected i18n: I18n, 29 protected i18n: I18n,
30 protected screenService: ScreenService, 30 protected screenService: ScreenService,
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts
index 8f3d3842b..6fd74e67a 100644
--- a/client/src/app/videos/video-list/video-trending.component.ts
+++ b/client/src/app/videos/video-list/video-trending.component.ts
@@ -2,13 +2,13 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { Location } from '@angular/common' 3import { Location } from '@angular/common'
4import { immutableAssign } from '@app/shared/misc/utils' 4import { immutableAssign } from '@app/shared/misc/utils'
5import { NotificationsService } from 'angular2-notifications'
6import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
7import { AbstractVideoList } from '../../shared/video/abstract-video-list' 6import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 7import { VideoSortField } from '../../shared/video/sort-field.type'
9import { VideoService } from '../../shared/video/video.service' 8import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
11import { ScreenService } from '@app/shared/misc/screen.service' 10import { ScreenService } from '@app/shared/misc/screen.service'
11import { Notifier, ServerService } from '@app/core'
12 12
13@Component({ 13@Component({
14 selector: 'my-videos-trending', 14 selector: 'my-videos-trending',
@@ -23,22 +23,37 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
23 constructor ( 23 constructor (
24 protected router: Router, 24 protected router: Router,
25 protected route: ActivatedRoute, 25 protected route: ActivatedRoute,
26 protected notificationsService: NotificationsService, 26 protected notifier: Notifier,
27 protected authService: AuthService, 27 protected authService: AuthService,
28 protected location: Location, 28 protected location: Location,
29 protected screenService: ScreenService, 29 protected screenService: ScreenService,
30 private serverService: ServerService,
30 protected i18n: I18n, 31 protected i18n: I18n,
31 private videoService: VideoService 32 private videoService: VideoService
32 ) { 33 ) {
33 super() 34 super()
34
35 this.titlePage = i18n('Trending')
36 } 35 }
37 36
38 ngOnInit () { 37 ngOnInit () {
39 super.ngOnInit() 38 super.ngOnInit()
40 39
41 this.generateSyndicationList() 40 this.generateSyndicationList()
41
42 this.serverService.configLoaded.subscribe(
43 () => {
44 const trendingDays = this.serverService.getConfig().trending.videos.intervalDays
45
46 if (trendingDays === 1) {
47 this.titlePage = this.i18n('Trending for the last 24 hours')
48 this.titleTooltip = this.i18n('Trending videos are those totalizing the greatest number of views during the last 24 hours.')
49 } else {
50 this.titlePage = this.i18n('Trending for the last {{days}} days', { days: trendingDays })
51 this.titleTooltip = this.i18n(
52 'Trending videos are those totalizing the greatest number of views during the last {{days}} days.',
53 { days: trendingDays }
54 )
55 }
56 })
42 } 57 }
43 58
44 ngOnDestroy () { 59 ngOnDestroy () {
diff --git a/client/src/app/videos/video-list/video-user-subscriptions.component.ts b/client/src/app/videos/video-list/video-user-subscriptions.component.ts
index 6e8959c54..bee828e12 100644
--- a/client/src/app/videos/video-list/video-user-subscriptions.component.ts
+++ b/client/src/app/videos/video-list/video-user-subscriptions.component.ts
@@ -2,7 +2,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { immutableAssign } from '@app/shared/misc/utils' 3import { immutableAssign } from '@app/shared/misc/utils'
4import { Location } from '@angular/common' 4import { Location } from '@angular/common'
5import { NotificationsService } from 'angular2-notifications'
6import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
7import { AbstractVideoList } from '../../shared/video/abstract-video-list' 6import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 7import { VideoSortField } from '../../shared/video/sort-field.type'
@@ -10,6 +9,7 @@ import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
11import { ScreenService } from '@app/shared/misc/screen.service' 10import { ScreenService } from '@app/shared/misc/screen.service'
12import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' 11import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
12import { Notifier } from '@app/core'
13 13
14@Component({ 14@Component({
15 selector: 'my-videos-user-subscriptions', 15 selector: 'my-videos-user-subscriptions',
@@ -25,7 +25,7 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
25 constructor ( 25 constructor (
26 protected router: Router, 26 protected router: Router,
27 protected route: ActivatedRoute, 27 protected route: ActivatedRoute,
28 protected notificationsService: NotificationsService, 28 protected notifier: Notifier,
29 protected authService: AuthService, 29 protected authService: AuthService,
30 protected location: Location, 30 protected location: Location,
31 protected i18n: I18n, 31 protected i18n: I18n,