aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-02-11 14:09:23 +0100
committerChocobozzz <me@florianbigard.com>2019-02-11 14:09:23 +0100
commitb718fd22374d64534bcfe69932cf562894abed6a (patch)
tree311d3c67e2a4d1f33ebdd1dc163527de9d33d0f7 /client/src/app
parentadb115f5522bea4d52456a9fc5eb4140bb064476 (diff)
parent501e961199578129629cf0567033d13efced9904 (diff)
downloadPeerTube-b718fd22374d64534bcfe69932cf562894abed6a.tar.gz
PeerTube-b718fd22374d64534bcfe69932cf562894abed6a.tar.zst
PeerTube-b718fd22374d64534bcfe69932cf562894abed6a.zip
Merge branch 'develop' into pr/1285
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.ts2
-rw-r--r--client/src/app/+about/about-instance/contact-admin-modal.component.html2
-rw-r--r--client/src/app/+accounts/account-about/account-about.component.ts4
-rw-r--r--client/src/app/+accounts/accounts.component.ts3
-rw-r--r--client/src/app/+admin/admin.module.ts3
-rw-r--r--client/src/app/+admin/moderation/moderation.component.scss1
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html9
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts4
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html4
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts8
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html8
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts12
-rw-r--r--client/src/app/+admin/users/user-edit/index.ts1
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html14
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.scss22
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.ts11
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.html21
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.scss22
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.ts64
-rw-r--r--client/src/app/+admin/users/user-edit/user-update.component.ts14
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html5
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.scss4
-rw-r--r--client/src/app/+my-account/my-account-history/my-account-history.component.scss4
-rw-r--r--client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html10
-rw-r--r--client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss4
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html3
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html4
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html2
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss2
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts10
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.html6
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.scss11
-rw-r--r--client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html3
-rw-r--r--client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts2
-rw-r--r--client/src/app/app.component.html4
-rw-r--r--client/src/app/core/auth/auth.service.ts4
-rw-r--r--client/src/app/core/confirm/index.ts1
-rw-r--r--client/src/app/core/core.module.ts8
-rw-r--r--client/src/app/core/notification/index.ts1
-rw-r--r--client/src/app/core/notification/user-notification-socket.service.ts41
-rw-r--r--client/src/app/core/server/server.service.ts10
-rw-r--r--client/src/app/header/header.component.html2
-rw-r--r--client/src/app/header/header.component.scss11
-rw-r--r--client/src/app/login/login.component.html3
-rw-r--r--client/src/app/menu/avatar-notification.component.html2
-rw-r--r--client/src/app/menu/avatar-notification.component.scss15
-rw-r--r--client/src/app/menu/avatar-notification.component.ts5
-rw-r--r--client/src/app/menu/language-chooser.component.html2
-rw-r--r--client/src/app/menu/menu.component.scss4
-rw-r--r--client/src/app/search/search.component.html2
-rw-r--r--client/src/app/search/search.component.scss4
-rw-r--r--client/src/app/search/search.component.ts9
-rw-r--r--client/src/app/shared/actor/actor.model.ts2
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.html2
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.scss9
-rw-r--r--client/src/app/shared/buttons/button.component.html2
-rw-r--r--client/src/app/shared/buttons/button.component.scss35
-rw-r--r--client/src/app/shared/buttons/button.component.ts3
-rw-r--r--client/src/app/shared/buttons/delete-button.component.html2
-rw-r--r--client/src/app/shared/buttons/edit-button.component.html2
-rw-r--r--client/src/app/shared/confirm/confirm.component.html (renamed from client/src/app/core/confirm/confirm.component.html)3
-rw-r--r--client/src/app/shared/confirm/confirm.component.scss (renamed from client/src/app/core/confirm/confirm.component.scss)0
-rw-r--r--client/src/app/shared/confirm/confirm.component.ts (renamed from client/src/app/core/confirm/confirm.component.ts)2
-rw-r--r--client/src/app/shared/forms/form-validators/video-abuse-validators.service.ts8
-rw-r--r--client/src/app/shared/forms/markdown-textarea.component.ts2
-rw-r--r--client/src/app/shared/forms/reactive-file.component.ts11
-rw-r--r--client/src/app/shared/icons/global-icon.component.html0
-rw-r--r--client/src/app/shared/icons/global-icon.component.scss4
-rw-r--r--client/src/app/shared/icons/global-icon.component.ts48
-rw-r--r--client/src/app/shared/misc/help.component.html4
-rw-r--r--client/src/app/shared/misc/help.component.scss27
-rw-r--r--client/src/app/shared/misc/help.component.ts2
-rw-r--r--client/src/app/shared/misc/utils.ts13
-rw-r--r--client/src/app/shared/moderation/user-ban-modal.component.html7
-rw-r--r--client/src/app/shared/moderation/user-ban-modal.component.ts4
-rw-r--r--client/src/app/shared/renderer/html-renderer.service.ts35
-rw-r--r--client/src/app/shared/renderer/index.ts3
-rw-r--r--client/src/app/shared/renderer/linkifier.service.ts (renamed from client/src/app/videos/+video-watch/comment/linkifier.service.ts)0
-rw-r--r--client/src/app/shared/renderer/markdown.service.ts (renamed from client/src/app/videos/shared/markdown.service.ts)0
-rw-r--r--client/src/app/shared/shared.module.ts17
-rw-r--r--client/src/app/shared/users/user-notification.model.ts42
-rw-r--r--client/src/app/shared/users/user-notification.service.ts44
-rw-r--r--client/src/app/shared/users/user-notifications.component.html82
-rw-r--r--client/src/app/shared/users/user-notifications.component.scss53
-rw-r--r--client/src/app/shared/users/user-notifications.component.ts15
-rw-r--r--client/src/app/shared/video-abuse/video-abuse.service.ts4
-rw-r--r--client/src/app/shared/video-blacklist/video-blacklist.service.ts7
-rw-r--r--client/src/app/shared/video-import/video-import.service.ts1
-rw-r--r--client/src/app/shared/video/abstract-video-list.html5
-rw-r--r--client/src/app/shared/video/abstract-video-list.scss2
-rw-r--r--client/src/app/shared/video/abstract-video-list.ts1
-rw-r--r--client/src/app/shared/video/feed.component.html9
-rw-r--r--client/src/app/shared/video/feed.component.scss17
-rw-r--r--client/src/app/shared/video/video-details.model.ts15
-rw-r--r--client/src/app/shared/video/video-edit.model.ts13
-rw-r--r--client/src/app/shared/video/video-miniature.component.scss4
-rw-r--r--client/src/app/shared/video/video.model.ts4
-rw-r--r--client/src/app/shared/video/video.service.ts1
-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-edit.component.html17
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.scss28
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.ts2
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html6
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss53
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts5
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html6
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts4
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-send.scss (renamed from client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss)29
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.html10
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss44
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts5
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.ts14
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.html2
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.ts20
-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.ts27
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.service.ts6
-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.ts10
-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-report.component.html2
-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-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.html35
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss80
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts119
-rw-r--r--client/src/app/videos/+video-watch/video-watch.module.ts4
-rw-r--r--client/src/app/videos/shared/index.ts1
-rw-r--r--client/src/app/videos/video-list/video-trending.component.ts21
130 files changed, 970 insertions, 599 deletions
diff --git a/client/src/app/+about/about-instance/about-instance.component.ts b/client/src/app/+about/about-instance/about-instance.component.ts
index d3ee8a1e4..a1b30fa8c 100644
--- a/client/src/app/+about/about-instance/about-instance.component.ts
+++ b/client/src/app/+about/about-instance/about-instance.component.ts
@@ -1,9 +1,9 @@
1import { Component, OnInit, ViewChild } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { Notifier, ServerService } from '@app/core' 2import { Notifier, ServerService } from '@app/core'
3import { MarkdownService } from '@app/videos/shared'
4import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
5import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' 4import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
6import { InstanceService } from '@app/shared/instance/instance.service' 5import { InstanceService } from '@app/shared/instance/instance.service'
6import { MarkdownService } from '@app/shared/renderer'
7 7
8@Component({ 8@Component({
9 selector: 'my-about-instance', 9 selector: 'my-about-instance',
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.html b/client/src/app/+about/about-instance/contact-admin-modal.component.html
index 2b3fb32f3..b2cbd0873 100644
--- a/client/src/app/+about/about-instance/contact-admin-modal.component.html
+++ b/client/src/app/+about/about-instance/contact-admin-modal.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">Contact {{ instanceName }} administrator</h4> 3 <h4 i18n class="modal-title">Contact {{ instanceName }} administrator</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">
diff --git a/client/src/app/+accounts/account-about/account-about.component.ts b/client/src/app/+accounts/account-about/account-about.component.ts
index 6f3e6caa0..13890a0ee 100644
--- a/client/src/app/+accounts/account-about/account-about.component.ts
+++ b/client/src/app/+accounts/account-about/account-about.component.ts
@@ -1,9 +1,9 @@
1import { Component, OnInit, OnDestroy } from '@angular/core' 1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { Account } from '@app/shared/account/account.model' 2import { Account } from '@app/shared/account/account.model'
3import { AccountService } from '@app/shared/account/account.service' 3import { AccountService } from '@app/shared/account/account.service'
4import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { Subscription } from 'rxjs' 5import { Subscription } from 'rxjs'
6import { MarkdownService } from '@app/videos/shared' 6import { MarkdownService } from '@app/shared/renderer'
7 7
8@Component({ 8@Component({
9 selector: 'my-account-about', 9 selector: 'my-account-about',
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts
index 036264602..e8339b78b 100644
--- a/client/src/app/+accounts/accounts.component.ts
+++ b/client/src/app/+accounts/accounts.component.ts
@@ -26,8 +26,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
26 private notifier: Notifier, 26 private notifier: Notifier,
27 private restExtractor: RestExtractor, 27 private restExtractor: RestExtractor,
28 private redirectService: RedirectService, 28 private redirectService: RedirectService,
29 private authService: AuthService, 29 private authService: AuthService
30 private i18n: I18n
31 ) {} 30 ) {}
32 31
33 ngOnInit () { 32 ngOnInit () {
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index c06ae1d60..f7f347105 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -10,7 +10,7 @@ import { FollowingListComponent } from './follows/following-list/following-list.
10import { JobsComponent } from './jobs/job.component' 10import { JobsComponent } from './jobs/job.component'
11import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' 11import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
12import { JobService } from './jobs/shared/job.service' 12import { JobService } from './jobs/shared/job.service'
13import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent } from './users' 13import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users'
14import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' 14import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation'
15import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 15import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
16import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' 16import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
@@ -36,6 +36,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f
36 UsersComponent, 36 UsersComponent,
37 UserCreateComponent, 37 UserCreateComponent,
38 UserUpdateComponent, 38 UserUpdateComponent,
39 UserPasswordComponent,
39 UserListComponent, 40 UserListComponent,
40 41
41 ModerationComponent, 42 ModerationComponent,
diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/+admin/moderation/moderation.component.scss
index 02ccfc8ca..13b019c5b 100644
--- a/client/src/app/+admin/moderation/moderation.component.scss
+++ b/client/src/app/+admin/moderation/moderation.component.scss
@@ -10,6 +10,7 @@
10 font-weight: $font-semibold; 10 font-weight: $font-semibold;
11 min-width: 200px; 11 min-width: 200px;
12 display: inline-block; 12 display: inline-block;
13 vertical-align: top;
13} 14}
14 15
15.moderation-expanded-text { 16.moderation-expanded-text {
diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html
index 3a8424f68..303a788d2 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html
+++ b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html
@@ -1,7 +1,8 @@
1<ng-template #modal> 1<ng-template #modal>
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Moderation comment</h4> 3 <h4 i18n class="modal-title">Moderation comment</h4>
4 <span class="close" aria-hidden="true" (click)="hideModerationCommentModal()"></span> 4
5 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 6 </div>
6 7
7 <div class="modal-body"> 8 <div class="modal-body">
@@ -14,12 +15,12 @@
14 </div> 15 </div>
15 </div> 16 </div>
16 17
17 <div i18n> 18 <div class="form-group" i18n>
18 This comment can only be seen by you or the other moderators. 19 This comment can only be seen by you or the other moderators.
19 </div> 20 </div>
20 21
21 <div class="form-group inputs"> 22 <div class="form-group inputs">
22 <span i18n class="action-button action-button-cancel" (click)="hideModerationCommentModal()">Cancel</span> 23 <span i18n class="action-button action-button-cancel" (click)="hide()">Cancel</span>
23 24
24 <input 25 <input
25 type="submit" i18n-value value="Update this comment" class="action-button-submit" 26 type="submit" i18n-value value="Update this comment" class="action-button-submit"
@@ -29,4 +30,4 @@
29 </form> 30 </form>
30 </div> 31 </div>
31 32
32</ng-template> \ No newline at end of file 33</ng-template>
diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts
index bebcb4207..f915978ee 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts
+++ b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts
@@ -45,7 +45,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
45 }) 45 })
46 } 46 }
47 47
48 hideModerationCommentModal () { 48 hide () {
49 this.abuseToComment = undefined 49 this.abuseToComment = undefined
50 this.openedModal.close() 50 this.openedModal.close()
51 this.form.reset() 51 this.form.reset()
@@ -60,7 +60,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
60 this.notifier.success(this.i18n('Comment updated.')) 60 this.notifier.success(this.i18n('Comment updated.'))
61 61
62 this.commentUpdated.emit(moderationComment) 62 this.commentUpdated.emit(moderationComment)
63 this.hideModerationCommentModal() 63 this.hide()
64 }, 64 },
65 65
66 err => this.notifier.error(err.message) 66 err => this.notifier.error(err.message)
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
index e862d5162..05b549de6 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
+++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
@@ -51,11 +51,11 @@
51 <td class="moderation-expanded" colspan="6"> 51 <td class="moderation-expanded" colspan="6">
52 <div> 52 <div>
53 <span i18n class="moderation-expanded-label">Reason:</span> 53 <span i18n class="moderation-expanded-label">Reason:</span>
54 <span class="moderation-expanded-text">{{ videoAbuse.reason }}</span> 54 <span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.reason)"></span>
55 </div> 55 </div>
56 <div *ngIf="videoAbuse.moderationComment"> 56 <div *ngIf="videoAbuse.moderationComment">
57 <span i18n class="moderation-expanded-label">Moderation comment:</span> 57 <span i18n class="moderation-expanded-label">Moderation comment:</span>
58 <span class="moderation-expanded-text">{{ videoAbuse.moderationComment }}</span> 58 <span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.moderationComment)"></span>
59 </div> 59 </div>
60 </td> 60 </td>
61 </tr> 61 </tr>
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
index f64234b74..00c871659 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
+++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
@@ -9,6 +9,7 @@ import { DropdownAction } from '../../../shared/buttons/action-dropdown.componen
9import { ConfirmService } from '../../../core/index' 9import { ConfirmService } from '../../../core/index'
10import { ModerationCommentModalComponent } from './moderation-comment-modal.component' 10import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
11import { Video } from '../../../shared/video/video.model' 11import { Video } from '../../../shared/video/video.model'
12import { MarkdownService } from '@app/shared/renderer'
12 13
13@Component({ 14@Component({
14 selector: 'my-video-abuse-list', 15 selector: 'my-video-abuse-list',
@@ -30,7 +31,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
30 private notifier: Notifier, 31 private notifier: Notifier,
31 private videoAbuseService: VideoAbuseService, 32 private videoAbuseService: VideoAbuseService,
32 private confirmService: ConfirmService, 33 private confirmService: ConfirmService,
33 private i18n: I18n 34 private i18n: I18n,
35 private markdownRenderer: MarkdownService
34 ) { 36 ) {
35 super() 37 super()
36 38
@@ -108,6 +110,10 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
108 110
109 } 111 }
110 112
113 toHtml (text: string) {
114 return this.markdownRenderer.textMarkdownToHTML(text)
115 }
116
111 protected loadData () { 117 protected loadData () {
112 return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort) 118 return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort)
113 .subscribe( 119 .subscribe(
diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
index 7cef787d2..247f441c1 100644
--- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
+++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
@@ -7,6 +7,7 @@
7 <th style="width: 40px"></th> 7 <th style="width: 40px"></th>
8 <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th> 8 <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
9 <th i18n>Sensitive</th> 9 <th i18n>Sensitive</th>
10 <th i18n>Unfederated</th>
10 <th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th> 11 <th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
11 <th style="width: 120px;"></th> 12 <th style="width: 120px;"></th>
12 </tr> 13 </tr>
@@ -26,7 +27,8 @@
26 </a> 27 </a>
27 </td> 28 </td>
28 29
29 <td>{{ videoBlacklist.video.nsfw }}</td> 30 <td>{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
31 <td>{{ booleanToText(videoBlacklist.unfederated) }}</td>
30 <td>{{ videoBlacklist.createdAt }}</td> 32 <td>{{ videoBlacklist.createdAt }}</td>
31 33
32 <td class="action-cell"> 34 <td class="action-cell">
@@ -37,9 +39,9 @@
37 39
38 <ng-template pTemplate="rowexpansion" let-videoBlacklist> 40 <ng-template pTemplate="rowexpansion" let-videoBlacklist>
39 <tr> 41 <tr>
40 <td class="moderation-expanded" colspan="5"> 42 <td class="moderation-expanded" colspan="6">
41 <span i18n class="moderation-expanded-label">Blacklist reason:</span> 43 <span i18n class="moderation-expanded-label">Blacklist reason:</span>
42 <span class="moderation-expanded-text">{{ videoBlacklist.reason }}</span> 44 <span class="moderation-expanded-text" [innerHTML]="toHtml(videoBlacklist.reason)"></span>
43 </td> 45 </td>
44 </tr> 46 </tr>
45 </ng-template> 47 </ng-template>
diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts
index a02e84f67..b27bbbfef 100644
--- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts
+++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts
@@ -7,6 +7,7 @@ import { VideoBlacklist } from '../../../../../../shared'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { DropdownAction } from '../../../shared/buttons/action-dropdown.component' 8import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
9import { Video } from '../../../shared/video/video.model' 9import { Video } from '../../../shared/video/video.model'
10import { MarkdownService } from '@app/shared/renderer'
10 11
11@Component({ 12@Component({
12 selector: 'my-video-blacklist-list', 13 selector: 'my-video-blacklist-list',
@@ -26,6 +27,7 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
26 private notifier: Notifier, 27 private notifier: Notifier,
27 private confirmService: ConfirmService, 28 private confirmService: ConfirmService,
28 private videoBlacklistService: VideoBlacklistService, 29 private videoBlacklistService: VideoBlacklistService,
30 private markdownRenderer: MarkdownService,
29 private i18n: I18n 31 private i18n: I18n
30 ) { 32 ) {
31 super() 33 super()
@@ -46,6 +48,16 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
46 return Video.buildClientUrl(videoBlacklist.video.uuid) 48 return Video.buildClientUrl(videoBlacklist.video.uuid)
47 } 49 }
48 50
51 booleanToText (value: boolean) {
52 if (value === true) return this.i18n('yes')
53
54 return this.i18n('no')
55 }
56
57 toHtml (text: string) {
58 return this.markdownRenderer.textMarkdownToHTML(text)
59 }
60
49 async removeVideoFromBlacklist (entry: VideoBlacklist) { 61 async removeVideoFromBlacklist (entry: VideoBlacklist) {
50 const confirmMessage = this.i18n( 62 const confirmMessage = this.i18n(
51 'Do you really want to remove this video from the blacklist? It will be available again in the videos list.' 63 'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
diff --git a/client/src/app/+admin/users/user-edit/index.ts b/client/src/app/+admin/users/user-edit/index.ts
index fd80a02e0..ec734ef92 100644
--- a/client/src/app/+admin/users/user-edit/index.ts
+++ b/client/src/app/+admin/users/user-edit/index.ts
@@ -1,2 +1,3 @@
1export * from './user-create.component' 1export * from './user-create.component'
2export * from './user-update.component' 2export * from './user-update.component'
3export * from './user-password.component'
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html
index 56cf7d17d..c6566da24 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.html
@@ -81,3 +81,17 @@
81 81
82 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> 82 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
83</form> 83</form>
84
85<div *ngIf="!isCreation()" class="danger-zone">
86 <div class="account-title" i18n>Danger Zone</div>
87
88 <div class="form-group reset-password-email">
89 <label i18n>Send a link to reset the password by email to the user</label>
90 <button (click)="resetPassword()" i18n>Ask for new password</button>
91 </div>
92
93 <div class="form-group">
94 <label i18n>Manually set the user password</label>
95 <my-user-password [userId]="userId"></my-user-password>
96 </div>
97</div>
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.scss b/client/src/app/+admin/users/user-edit/user-edit.component.scss
index 6675f65cc..c1cc4ca45 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.scss
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.scss
@@ -14,7 +14,7 @@ input:not([type=submit]) {
14 @include peertube-select-container(340px); 14 @include peertube-select-container(340px);
15} 15}
16 16
17input[type=submit] { 17input[type=submit], button {
18 @include peertube-button; 18 @include peertube-button;
19 @include orange-button; 19 @include orange-button;
20 20
@@ -25,3 +25,23 @@ input[type=submit] {
25 margin-top: 5px; 25 margin-top: 5px;
26 font-size: 11px; 26 font-size: 11px;
27} 27}
28
29.account-title {
30 @include in-content-small-title;
31
32 margin-top: 55px;
33 margin-bottom: 30px;
34}
35
36.danger-zone {
37 .reset-password-email {
38 margin-bottom: 30px;
39 padding-bottom: 30px;
40 border-bottom: 1px solid rgba(0, 0, 0, 0.1);
41
42 button {
43 display: block;
44 margin-top: 0;
45 }
46 }
47}
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts
index 0b3511e8e..649b35b0c 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.ts
+++ b/client/src/app/+admin/users/user-edit/user-edit.ts
@@ -8,6 +8,7 @@ export abstract class UserEdit extends FormReactive {
8 videoQuotaDailyOptions: { value: string, label: string }[] = [] 8 videoQuotaDailyOptions: { value: string, label: string }[] = []
9 roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) 9 roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] }))
10 username: string 10 username: string
11 userId: number
11 12
12 protected abstract serverService: ServerService 13 protected abstract serverService: ServerService
13 protected abstract configService: ConfigService 14 protected abstract configService: ConfigService
@@ -22,7 +23,9 @@ export abstract class UserEdit extends FormReactive {
22 } 23 }
23 24
24 computeQuotaWithTranscoding () { 25 computeQuotaWithTranscoding () {
25 const resolutions = this.serverService.getConfig().transcoding.enabledResolutions 26 const transcodingConfig = this.serverService.getConfig().transcoding
27
28 const resolutions = transcodingConfig.enabledResolutions
26 const higherResolution = VideoResolution.H_1080P 29 const higherResolution = VideoResolution.H_1080P
27 let multiplier = 0 30 let multiplier = 0
28 31
@@ -30,9 +33,15 @@ export abstract class UserEdit extends FormReactive {
30 multiplier += resolution / higherResolution 33 multiplier += resolution / higherResolution
31 } 34 }
32 35
36 if (transcodingConfig.hls.enabled) multiplier *= 2
37
33 return multiplier * parseInt(this.form.value['videoQuota'], 10) 38 return multiplier * parseInt(this.form.value['videoQuota'], 10)
34 } 39 }
35 40
41 resetPassword () {
42 return
43 }
44
36 protected buildQuotaOptions () { 45 protected buildQuotaOptions () {
37 // These are used by a HTML select, so convert key into strings 46 // These are used by a HTML select, so convert key into strings
38 this.videoQuotaOptions = this.configService 47 this.videoQuotaOptions = this.configService
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.html b/client/src/app/+admin/users/user-edit/user-password.component.html
new file mode 100644
index 000000000..a1e1f6216
--- /dev/null
+++ b/client/src/app/+admin/users/user-edit/user-password.component.html
@@ -0,0 +1,21 @@
1<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
2 <div class="form-group">
3
4 <div class="input-group">
5 <input id="password" [attr.type]="showPassword ? 'text' : 'password'"
6 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
7 >
8 <div class="input-group-append">
9 <button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button">
10 <ng-container *ngIf="!showPassword" i18n>Show</ng-container>
11 <ng-container *ngIf="!!showPassword" i18n>Hide</ng-container>
12 </button>
13 </div>
14 </div>
15 <div *ngIf="formErrors.password" class="form-error">
16 {{ formErrors.password }}
17 </div>
18 </div>
19
20 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
21</form>
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.scss b/client/src/app/+admin/users/user-edit/user-password.component.scss
new file mode 100644
index 000000000..217d585af
--- /dev/null
+++ b/client/src/app/+admin/users/user-edit/user-password.component.scss
@@ -0,0 +1,22 @@
1@import '_variables';
2@import '_mixins';
3
4input:not([type=submit]):not([type=checkbox]) {
5 @include peertube-input-text(340px);
6
7 display: block;
8 border-top-right-radius: 0;
9 border-bottom-right-radius: 0;
10 border-right: none;
11}
12
13input[type=submit] {
14 @include peertube-button;
15 @include orange-button;
16
17 margin-top: 10px;
18}
19
20.input-group-append {
21 height: 30px;
22}
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.ts b/client/src/app/+admin/users/user-edit/user-password.component.ts
new file mode 100644
index 000000000..5b3040440
--- /dev/null
+++ b/client/src/app/+admin/users/user-edit/user-password.component.ts
@@ -0,0 +1,64 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { UserService } from '@app/shared/users/user.service'
4import { Notifier } from '../../../core'
5import { User, UserUpdate } from '../../../../../../shared'
6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
8import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
9import { FormReactive } from '../../../shared'
10
11@Component({
12 selector: 'my-user-password',
13 templateUrl: './user-password.component.html',
14 styleUrls: [ './user-password.component.scss' ]
15})
16export class UserPasswordComponent extends FormReactive implements OnInit {
17 error: string
18 username: string
19 showPassword = false
20
21 @Input() userId: number
22
23 constructor (
24 protected formValidatorService: FormValidatorService,
25 private userValidatorsService: UserValidatorsService,
26 private route: ActivatedRoute,
27 private router: Router,
28 private notifier: Notifier,
29 private userService: UserService,
30 private i18n: I18n
31 ) {
32 super()
33 }
34
35 ngOnInit () {
36 this.buildForm({
37 password: this.userValidatorsService.USER_PASSWORD
38 })
39 }
40
41 formValidated () {
42 this.error = undefined
43
44 const userUpdate: UserUpdate = this.form.value
45
46 this.userService.updateUser(this.userId, userUpdate).subscribe(
47 () => {
48 this.notifier.success(
49 this.i18n('Password changed for user {{username}}.', { username: this.username })
50 )
51 },
52
53 err => this.error = err.message
54 )
55 }
56
57 togglePasswordVisibility () {
58 this.showPassword = !this.showPassword
59 }
60
61 getFormButtonTitle () {
62 return this.i18n('Update user password')
63 }
64}
diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts
index 61e641823..94ef87b08 100644
--- a/client/src/app/+admin/users/user-edit/user-update.component.ts
+++ b/client/src/app/+admin/users/user-edit/user-update.component.ts
@@ -19,6 +19,7 @@ import { UserService } from '@app/shared'
19export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { 19export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
20 error: string 20 error: string
21 userId: number 21 userId: number
22 userEmail: string
22 username: string 23 username: string
23 24
24 private paramsSub: Subscription 25 private paramsSub: Subscription
@@ -89,9 +90,22 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
89 return this.i18n('Update user') 90 return this.i18n('Update user')
90 } 91 }
91 92
93 resetPassword () {
94 this.userService.askResetPassword(this.userEmail).subscribe(
95 () => {
96 this.notifier.success(
97 this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username })
98 )
99 },
100
101 err => this.error = err.message
102 )
103 }
104
92 private onUserFetched (userJson: User) { 105 private onUserFetched (userJson: User) {
93 this.userId = userJson.id 106 this.userId = userJson.id
94 this.username = userJson.username 107 this.username = userJson.username
108 this.userEmail = userJson.email
95 109
96 this.form.patchValue({ 110 this.form.patchValue({
97 email: userJson.email, 111 email: userJson.email,
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html
index 556ab3c5d..69a4616a3 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/users/user-list/user-list.component.html
@@ -2,7 +2,7 @@
2 <div i18n class="form-sub-title">Users list</div> 2 <div i18n class="form-sub-title">Users list</div>
3 3
4 <a class="add-button" routerLink="/admin/users/create"> 4 <a class="add-button" routerLink="/admin/users/create">
5 <span class="icon icon-add"></span> 5 <my-global-icon iconName="add"></my-global-icon>
6 <ng-container i18n>Create user</ng-container> 6 <ng-container i18n>Create user</ng-container>
7 </a> 7 </a>
8</div> 8</div>
@@ -65,7 +65,9 @@
65 <span i18n *ngIf="user.blocked" class="banned-info">(banned)</span> 65 <span i18n *ngIf="user.blocked" class="banned-info">(banned)</span>
66 </a> 66 </a>
67 </td> 67 </td>
68
68 <td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">{{ user.email }}</td> 69 <td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">{{ user.email }}</td>
70
69 <ng-template #emailWithVerificationStatus> 71 <ng-template #emailWithVerificationStatus>
70 <td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login"> 72 <td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login">
71 <em>? {{ user.email }}</em> 73 <em>? {{ user.email }}</em>
@@ -76,6 +78,7 @@
76 </td> 78 </td>
77 </ng-template> 79 </ng-template>
78 </ng-template> 80 </ng-template>
81
79 <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td> 82 <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td>
80 <td>{{ user.roleLabel }}</td> 83 <td>{{ user.roleLabel }}</td>
81 <td>{{ user.createdAt }}</td> 84 <td>{{ user.createdAt }}</td>
diff --git a/client/src/app/+admin/users/user-list/user-list.component.scss b/client/src/app/+admin/users/user-list/user-list.component.scss
index f235769f0..5274be01c 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.scss
+++ b/client/src/app/+admin/users/user-list/user-list.component.scss
@@ -2,7 +2,7 @@
2@import '_mixins'; 2@import '_mixins';
3 3
4.add-button { 4.add-button {
5 @include create-button('../../../../assets/images/global/add.svg'); 5 @include create-button;
6} 6}
7 7
8tr.banned { 8tr.banned {
@@ -23,4 +23,4 @@ tr.banned {
23 input { 23 input {
24 @include peertube-input-text(250px); 24 @include peertube-input-text(250px);
25 } 25 }
26} \ No newline at end of file 26}
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.scss b/client/src/app/+my-account/my-account-history/my-account-history.component.scss
index 82150cbe3..e7c6863f1 100644
--- a/client/src/app/+my-account/my-account-history/my-account-history.component.scss
+++ b/client/src/app/+my-account/my-account-history/my-account-history.component.scss
@@ -65,10 +65,10 @@
65 text-overflow: ellipsis; 65 text-overflow: ellipsis;
66 white-space: nowrap; 66 white-space: nowrap;
67 font-size: 14px; 67 font-size: 14px;
68 color: #585858; 68 color: $grey-foreground-color;
69 69
70 &:hover { 70 &:hover {
71 color: #303030; 71 color: $grey-foreground-hover-color;
72 } 72 }
73 } 73 }
74 } 74 }
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html
index b98a1087e..d518b22ec 100644
--- a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html
+++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html
@@ -1,7 +1,13 @@
1<div class="header"> 1<div class="header">
2 <a routerLink="/my-account/settings" fragment="notifications" i18n>Notification preferences</a> 2 <a routerLink="/my-account/settings" fragment="notifications" i18n>
3 <my-global-icon iconName="cog"></my-global-icon>
4 Notification preferences
5 </a>
3 6
4 <button (click)="markAllAsRead()" i18n>Mark all as read</button> 7 <button (click)="markAllAsRead()" i18n>
8 <my-global-icon iconName="circle-tick"></my-global-icon>
9 Mark all as read
10 </button>
5</div> 11</div>
6 12
7<my-user-notifications #userNotification></my-user-notifications> 13<my-user-notifications #userNotification></my-user-notifications>
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss
index 86ac094c5..43d1f82ab 100644
--- a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss
+++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss
@@ -5,16 +5,18 @@
5 display: flex; 5 display: flex;
6 justify-content: space-between; 6 justify-content: space-between;
7 font-size: 15px; 7 font-size: 15px;
8 margin-bottom: 10px; 8 margin-bottom: 20px;
9 9
10 a { 10 a {
11 @include peertube-button-link; 11 @include peertube-button-link;
12 @include grey-button; 12 @include grey-button;
13 @include button-with-icon(18px, 3px, -1px);
13 } 14 }
14 15
15 button { 16 button {
16 @include peertube-button; 17 @include peertube-button;
17 @include grey-button; 18 @include grey-button;
19 @include button-with-icon(20px, 3px, -1px);
18 } 20 }
19} 21}
20 22
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html
index fd7d7d23b..674a4e8a2 100644
--- a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html
+++ b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html
@@ -1,7 +1,8 @@
1<ng-template #modal let-close="close" let-dismiss="dismiss"> 1<ng-template #modal let-close="close" let-dismiss="dismiss">
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Accept ownership</h4> 3 <h4 i18n class="modal-title">Accept ownership</h4>
4 <span class="close" aria-label="Close" role="button" (click)="dismiss()"></span> 4
5 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon>
5 </div> 6 </div>
6 7
7 <div class="modal-body" [formGroup]="form"> 8 <div class="modal-body" [formGroup]="form">
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html
index 379fd8bb1..5709e9f54 100644
--- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html
+++ b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html
@@ -40,10 +40,10 @@
40 <td class="action-cell"> 40 <td class="action-cell">
41 <ng-container *ngIf="videoChangeOwnership.status === 'WAITING'"> 41 <ng-container *ngIf="videoChangeOwnership.status === 'WAITING'">
42 <my-button i18n label="Accept" 42 <my-button i18n label="Accept"
43 icon="icon-tick" 43 icon="tick"
44 (click)="openAcceptModal(videoChangeOwnership)"></my-button> 44 (click)="openAcceptModal(videoChangeOwnership)"></my-button>
45 <my-button i18n label="Refuse" 45 <my-button i18n label="Refuse"
46 icon="icon-cross" 46 icon="cross"
47 (click)="refuse(videoChangeOwnership)">Refuse</my-button> 47 (click)="refuse(videoChangeOwnership)">Refuse</my-button>
48 </ng-container> 48 </ng-container>
49 </td> 49 </td>
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html
index df74b19b6..51db2e75d 100644
--- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html
+++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html
@@ -1,6 +1,6 @@
1<div class="video-channels-header"> 1<div class="video-channels-header">
2 <a class="create-button" routerLink="create"> 2 <a class="create-button" routerLink="create">
3 <span class="icon icon-add"></span> 3 <my-global-icon iconName="add"></my-global-icon>
4 <ng-container i18n>Create another video channel</ng-container> 4 <ng-container i18n>Create another video channel</ng-container>
5 </a> 5 </a>
6</div> 6</div>
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss
index 472cbb723..77fce138b 100644
--- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss
+++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss
@@ -2,7 +2,7 @@
2@import '_mixins'; 2@import '_mixins';
3 3
4.create-button { 4.create-button {
5 @include create-button('../../../assets/images/global/add.svg'); 5 @include create-button;
6} 6}
7 7
8/deep/ .action-button { 8/deep/ .action-button {
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts
index 0ab3e2067..da2c5bcd3 100644
--- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts
+++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts
@@ -35,10 +35,14 @@ export class MyAccountVideoChannelsComponent implements OnInit {
35 async deleteVideoChannel (videoChannel: VideoChannel) { 35 async deleteVideoChannel (videoChannel: VideoChannel) {
36 const res = await this.confirmService.confirmWithInput( 36 const res = await this.confirmService.confirmWithInput(
37 this.i18n( 37 this.i18n(
38 'Do you really want to delete {{videoChannelName}}? It will delete all videos uploaded in this channel too.', 38 'Do you really want to delete {{channelDisplayName}}? It will delete all videos uploaded in this channel, ' +
39 { videoChannelName: videoChannel.displayName } 39 'and you will not be able to create another channel with the same name ({{channelName}})!',
40 { channelDisplayName: videoChannel.displayName, channelName: videoChannel.name }
41 ),
42 this.i18n(
43 'Please type the display name of the video channel ({{displayName}}) to confirm',
44 { displayName: videoChannel.displayName }
40 ), 45 ),
41 this.i18n('Please type the name of the video channel to confirm'),
42 videoChannel.displayName, 46 videoChannel.displayName,
43 this.i18n('Delete') 47 this.i18n('Delete')
44 ) 48 )
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
index a6911e4bf..69748ef37 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
@@ -32,7 +32,7 @@
32 </span> 32 </span>
33 33
34 <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()"> 34 <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
35 <span class="icon icon-delete-white"></span> 35 <my-global-icon iconName="delete"></my-global-icon>
36 <ng-container i18n>Delete</ng-container> 36 <ng-container i18n>Delete</ng-container>
37 </span> 37 </span>
38 </div> 38 </div>
@@ -45,7 +45,7 @@
45 45
46 <my-button i18n-label label="Change ownership" 46 <my-button i18n-label label="Change ownership"
47 className="action-button-change-ownership" 47 className="action-button-change-ownership"
48 icon="icon-im-with-her" 48 icon="im-with-her"
49 (click)="changeOwnership($event, video)" 49 (click)="changeOwnership($event, video)"
50 ></my-button> 50 ></my-button>
51 </div> 51 </div>
@@ -53,4 +53,4 @@
53 </div> 53 </div>
54</div> 54</div>
55 55
56<my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership> \ No newline at end of file 56<my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership>
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
index a735562f8..39d0cf2f7 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
@@ -23,14 +23,11 @@
23 .action-button-delete-selection { 23 .action-button-delete-selection {
24 @include peertube-button; 24 @include peertube-button;
25 @include orange-button; 25 @include orange-button;
26 } 26 @include button-with-icon(21px);
27
28 .icon.icon-delete-white {
29 @include icon(21px);
30 27
31 position: relative; 28 my-global-icon {
32 top: -2px; 29 @include apply-svg-color(#fff);
33 background-image: url('../../../assets/images/global/delete-white.svg'); 30 }
34 } 31 }
35 } 32 }
36} 33}
diff --git a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html
index 7c0df850d..22f127904 100644
--- a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html
+++ b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html
@@ -1,7 +1,8 @@
1<ng-template #modal let-close="close" let-dismiss="dismiss"> 1<ng-template #modal let-close="close" let-dismiss="dismiss">
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Change ownership</h4> 3 <h4 i18n class="modal-title">Change ownership</h4>
4 <span class="close" aria-label="Close" role="button" (click)="dismiss()"></span> 4
5 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon>
5 </div> 6 </div>
6 7
7 <div class="modal-body" [formGroup]="form"> 8 <div class="modal-body" [formGroup]="form">
diff --git a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts
index ea7b0e118..895b19064 100644
--- a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts
+++ b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts
@@ -3,7 +3,7 @@ import { VideoChannelService } from '@app/shared/video-channel/video-channel.ser
3import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 3import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
4import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { Subscription } from 'rxjs' 5import { Subscription } from 'rxjs'
6import { MarkdownService } from '@app/videos/shared' 6import { MarkdownService } from '@app/shared/renderer'
7 7
8@Component({ 8@Component({
9 selector: 'my-video-channel-about', 9 selector: 'my-video-channel-about',
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html
index 3a60139e1..d398d4f35 100644
--- a/client/src/app/app.component.html
+++ b/client/src/app/app.component.html
@@ -30,14 +30,16 @@
30 30
31 <footer class="row"> 31 <footer class="row">
32 <a href="https://joinpeertube.org" title="PeerTube website" target="_blank" rel="noopener noreferrer">PeerTube v{{ serverVersion }}{{ serverCommit }}</a>&nbsp;-&nbsp; 32 <a href="https://joinpeertube.org" title="PeerTube website" target="_blank" rel="noopener noreferrer">PeerTube v{{ serverVersion }}{{ serverCommit }}</a>&nbsp;-&nbsp;
33 <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" title="PeerTube license" target="_blank" rel="noopener noreferrer">CopyLeft 2015-2018</a> 33 <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" title="PeerTube license" target="_blank" rel="noopener noreferrer">CopyLeft 2015-2019</a>
34 </footer> 34 </footer>
35 </div> 35 </div>
36 </div> 36 </div>
37</div> 37</div>
38 38
39<ngx-loading-bar [includeSpinner]="false"></ngx-loading-bar> 39<ngx-loading-bar [includeSpinner]="false"></ngx-loading-bar>
40
40<my-confirm></my-confirm> 41<my-confirm></my-confirm>
42
41<p-toast position="bottom-right"> 43<p-toast position="bottom-right">
42 <ng-template let-message pTemplate="message"> 44 <ng-template let-message pTemplate="message">
43 <div class="notification-block"> 45 <div class="notification-block">
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts
index 79ea32ced..eaa822e0f 100644
--- a/client/src/app/core/auth/auth.service.ts
+++ b/client/src/app/core/auth/auth.service.ts
@@ -3,12 +3,12 @@ import { catchError, map, mergeMap, share, tap } from 'rxjs/operators'
3import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' 3import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
5import { Router } from '@angular/router' 5import { Router } from '@angular/router'
6import { Notifier } from '@app/core/notification' 6import { Notifier } from '@app/core/notification/notifier.service'
7import { OAuthClientLocal, User as UserServerModel, UserRefreshToken } from '../../../../../shared' 7import { OAuthClientLocal, User as UserServerModel, UserRefreshToken } from '../../../../../shared'
8import { User } from '../../../../../shared/models/users' 8import { User } from '../../../../../shared/models/users'
9import { UserLogin } from '../../../../../shared/models/users/user-login.model' 9import { UserLogin } from '../../../../../shared/models/users/user-login.model'
10import { environment } from '../../../environments/environment' 10import { environment } from '../../../environments/environment'
11import { RestExtractor } from '../../shared/rest' 11import { RestExtractor } from '../../shared/rest/rest-extractor.service'
12import { AuthStatus } from './auth-status.model' 12import { AuthStatus } from './auth-status.model'
13import { AuthUser } from './auth-user.model' 13import { AuthUser } from './auth-user.model'
14import { objectToUrlEncoded } from '@app/shared/misc/utils' 14import { objectToUrlEncoded } from '@app/shared/misc/utils'
diff --git a/client/src/app/core/confirm/index.ts b/client/src/app/core/confirm/index.ts
index 44aabfc13..aca591e1a 100644
--- a/client/src/app/core/confirm/index.ts
+++ b/client/src/app/core/confirm/index.ts
@@ -1,2 +1 @@
1export * from './confirm.component'
2export * from './confirm.service' export * from './confirm.service'
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts
index 7c0d4ac8f..4ef3b1e73 100644
--- a/client/src/app/core/core.module.ts
+++ b/client/src/app/core/core.module.ts
@@ -8,7 +8,7 @@ import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
8import { LoadingBarRouterModule } from '@ngx-loading-bar/router' 8import { LoadingBarRouterModule } from '@ngx-loading-bar/router'
9 9
10import { AuthService } from './auth' 10import { AuthService } from './auth'
11import { ConfirmComponent, ConfirmService } from './confirm' 11import { ConfirmService } from './confirm'
12import { throwIfAlreadyLoaded } from './module-import-guard' 12import { throwIfAlreadyLoaded } from './module-import-guard'
13import { LoginGuard, RedirectService, UserRightGuard } from './routing' 13import { LoginGuard, RedirectService, UserRightGuard } from './routing'
14import { ServerService } from './server' 14import { ServerService } from './server'
@@ -18,6 +18,7 @@ import { CheatSheetComponent } from './hotkeys'
18import { ToastModule } from 'primeng/toast' 18import { ToastModule } from 'primeng/toast'
19import { Notifier } from './notification' 19import { Notifier } from './notification'
20import { MessageService } from 'primeng/api' 20import { MessageService } from 'primeng/api'
21import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service'
21 22
22@NgModule({ 23@NgModule({
23 imports: [ 24 imports: [
@@ -37,7 +38,6 @@ import { MessageService } from 'primeng/api'
37 ], 38 ],
38 39
39 declarations: [ 40 declarations: [
40 ConfirmComponent,
41 CheatSheetComponent 41 CheatSheetComponent
42 ], 42 ],
43 43
@@ -47,7 +47,6 @@ import { MessageService } from 'primeng/api'
47 47
48 ToastModule, 48 ToastModule,
49 49
50 ConfirmComponent,
51 CheatSheetComponent 50 CheatSheetComponent
52 ], 51 ],
53 52
@@ -60,7 +59,8 @@ import { MessageService } from 'primeng/api'
60 UserRightGuard, 59 UserRightGuard,
61 RedirectService, 60 RedirectService,
62 Notifier, 61 Notifier,
63 MessageService 62 MessageService,
63 UserNotificationSocket
64 ] 64 ]
65}) 65})
66export class CoreModule { 66export class CoreModule {
diff --git a/client/src/app/core/notification/index.ts b/client/src/app/core/notification/index.ts
index 8b0cfde5f..3e8d9ea65 100644
--- a/client/src/app/core/notification/index.ts
+++ b/client/src/app/core/notification/index.ts
@@ -1 +1,2 @@
1export * from './notifier.service' 1export * from './notifier.service'
2export * from './user-notification-socket.service'
diff --git a/client/src/app/core/notification/user-notification-socket.service.ts b/client/src/app/core/notification/user-notification-socket.service.ts
new file mode 100644
index 000000000..f367d9ae4
--- /dev/null
+++ b/client/src/app/core/notification/user-notification-socket.service.ts
@@ -0,0 +1,41 @@
1import { Injectable } from '@angular/core'
2import { environment } from '../../../environments/environment'
3import { UserNotification as UserNotificationServer } from '../../../../../shared'
4import { Subject } from 'rxjs'
5import * as io from 'socket.io-client'
6import { AuthService } from '../auth'
7
8export type NotificationEvent = 'new' | 'read' | 'read-all'
9
10@Injectable()
11export class UserNotificationSocket {
12 private notificationSubject = new Subject<{ type: NotificationEvent, notification?: UserNotificationServer }>()
13
14 private socket: SocketIOClient.Socket
15
16 constructor (
17 private auth: AuthService
18 ) {}
19
20 dispatch (type: NotificationEvent, notification?: UserNotificationServer) {
21 this.notificationSubject.next({ type, notification })
22 }
23
24 getMyNotificationsSocket () {
25 const socket = this.getSocket()
26
27 socket.on('new-notification', (n: UserNotificationServer) => this.dispatch('new', n))
28
29 return this.notificationSubject.asObservable()
30 }
31
32 private getSocket () {
33 if (this.socket) return this.socket
34
35 this.socket = io(environment.apiUrl + '/user-notifications', {
36 query: { accessToken: this.auth.getAccessToken() }
37 })
38
39 return this.socket
40 }
41}
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index f33e6f20c..c868ccdcc 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -51,7 +51,10 @@ export class ServerService {
51 requiresEmailVerification: false 51 requiresEmailVerification: false
52 }, 52 },
53 transcoding: { 53 transcoding: {
54 enabledResolutions: [] 54 enabledResolutions: [],
55 hls: {
56 enabled: false
57 }
55 }, 58 },
56 avatar: { 59 avatar: {
57 file: { 60 file: {
@@ -87,6 +90,11 @@ export class ServerService {
87 enabled: false 90 enabled: false
88 } 91 }
89 } 92 }
93 },
94 trending: {
95 videos: {
96 intervalDays: 0
97 }
90 } 98 }
91 } 99 }
92 private videoCategories: Array<VideoConstant<number>> = [] 100 private videoCategories: Array<VideoConstant<number>> = []
diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html
index c23e0c55d..46a87c79c 100644
--- a/client/src/app/header/header.component.html
+++ b/client/src/app/header/header.component.html
@@ -5,6 +5,6 @@
5<span (click)="doSearch()" class="icon icon-search"></span> 5<span (click)="doSearch()" class="icon icon-search"></span>
6 6
7<a class="upload-button" routerLink="/videos/upload"> 7<a class="upload-button" routerLink="/videos/upload">
8 <span class="icon icon-upload"></span> 8 <my-global-icon iconName="upload"></my-global-icon>
9 <span i18n class="upload-button-label">Upload</span> 9 <span i18n class="upload-button-label">Upload</span>
10</a> 10</a>
diff --git a/client/src/app/header/header.component.scss b/client/src/app/header/header.component.scss
index 2f9820665..cea415d9b 100644
--- a/client/src/app/header/header.component.scss
+++ b/client/src/app/header/header.component.scss
@@ -6,6 +6,7 @@
6 padding-left: 10px; 6 padding-left: 10px;
7 margin-right: 15px; 7 margin-right: 15px;
8 padding-right: 40px; // For the search icon 8 padding-right: 40px; // For the search icon
9 font-size: 14px;
9 10
10 &::placeholder { 11 &::placeholder {
11 color: var(--inputPlaceholderColor); 12 color: var(--inputPlaceholderColor);
@@ -40,6 +41,7 @@
40.upload-button { 41.upload-button {
41 @include peertube-button-link; 42 @include peertube-button-link;
42 @include orange-button; 43 @include orange-button;
44 @include button-with-icon(22px, 3px, -1px);
43 45
44 margin-right: 25px; 46 margin-right: 25px;
45 47
@@ -47,15 +49,6 @@
47 margin-right: 0; 49 margin-right: 0;
48 } 50 }
49 51
50 .icon.icon-upload {
51 @include icon(22px);
52
53 background-image: url('../../assets/images/header/upload-white.svg');
54 height: 24px;
55 vertical-align: middle;
56 margin-right: 6px;
57 }
58
59 @media screen and (max-width: 600px) { 52 @media screen and (max-width: 600px) {
60 margin-right: 10px; 53 margin-right: 10px;
61 padding: 0 10px; 54 padding: 0 10px;
diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html
index 9b8146624..4efe3fb22 100644
--- a/client/src/app/login/login.component.html
+++ b/client/src/app/login/login.component.html
@@ -55,7 +55,8 @@
55<ng-template #forgotPasswordModal> 55<ng-template #forgotPasswordModal>
56 <div class="modal-header"> 56 <div class="modal-header">
57 <h4 i18n class="modal-title">Forgot your password</h4> 57 <h4 i18n class="modal-title">Forgot your password</h4>
58 <span class="close" aria-hidden="true" (click)="hideForgotPasswordModal()"></span> 58
59 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hideForgotPasswordModal()"></my-global-icon>
59 </div> 60 </div>
60 61
61 <div class="modal-body"> 62 <div class="modal-body">
diff --git a/client/src/app/menu/avatar-notification.component.html b/client/src/app/menu/avatar-notification.component.html
index 2f0b7c669..4ef3f0e89 100644
--- a/client/src/app/menu/avatar-notification.component.html
+++ b/client/src/app/menu/avatar-notification.component.html
@@ -17,7 +17,7 @@
17 ></a> 17 ></a>
18 </div> 18 </div>
19 19
20 <my-user-notifications [ignoreLoadingBar]="true" [infiniteScroll]="false"></my-user-notifications> 20 <my-user-notifications [ignoreLoadingBar]="true" [infiniteScroll]="false" itemsPerPage="10"></my-user-notifications>
21 21
22 <a class="all-notifications" routerLink="/my-account/notifications" i18n>See all your notifications</a> 22 <a class="all-notifications" routerLink="/my-account/notifications" i18n>See all your notifications</a>
23</ng-template> 23</ng-template>
diff --git a/client/src/app/menu/avatar-notification.component.scss b/client/src/app/menu/avatar-notification.component.scss
index c86667469..e785db788 100644
--- a/client/src/app/menu/avatar-notification.component.scss
+++ b/client/src/app/menu/avatar-notification.component.scss
@@ -3,7 +3,7 @@
3 3
4/deep/ { 4/deep/ {
5 .popover-notifications.popover { 5 .popover-notifications.popover {
6 max-width: 400px; 6 max-width: none;
7 7
8 .popover-body { 8 .popover-body {
9 padding: 0; 9 padding: 0;
@@ -11,6 +11,7 @@
11 font-family: $main-fonts; 11 font-family: $main-fonts;
12 overflow-y: auto; 12 overflow-y: auto;
13 max-height: 500px; 13 max-height: 500px;
14 width: 400px;
14 box-shadow: 0 6px 14px rgba(0, 0, 0, 0.30); 15 box-shadow: 0 6px 14px rgba(0, 0, 0, 0.30);
15 16
16 .notifications-header { 17 .notifications-header {
@@ -40,7 +41,7 @@
40 justify-content: center; 41 justify-content: center;
41 font-weight: $font-semibold; 42 font-weight: $font-semibold;
42 color: var(--mainForegroundColor); 43 color: var(--mainForegroundColor);
43 height: 30px; 44 padding: 7px 0;
44 } 45 }
45 } 46 }
46 } 47 }
@@ -71,7 +72,7 @@
71 justify-content: center; 72 justify-content: center;
72 73
73 background-color: var(--mainColor); 74 background-color: var(--mainColor);
74 color: var(--mainBackgroundColor); 75 color: var(#fff);
75 font-size: 10px; 76 font-size: 10px;
76 font-weight: $font-semibold; 77 font-weight: $font-semibold;
77 78
@@ -80,3 +81,11 @@
80 height: 15px; 81 height: 15px;
81 } 82 }
82} 83}
84
85@media screen and (max-width: $mobile-view) {
86 /deep/ {
87 .popover-notifications.popover .popover-body {
88 width: 400px;
89 }
90 }
91}
diff --git a/client/src/app/menu/avatar-notification.component.ts b/client/src/app/menu/avatar-notification.component.ts
index 60e090726..f1af08096 100644
--- a/client/src/app/menu/avatar-notification.component.ts
+++ b/client/src/app/menu/avatar-notification.component.ts
@@ -2,7 +2,7 @@ import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'
2import { User } from '../shared/users/user.model' 2import { User } from '../shared/users/user.model'
3import { UserNotificationService } from '@app/shared/users/user-notification.service' 3import { UserNotificationService } from '@app/shared/users/user-notification.service'
4import { Subscription } from 'rxjs' 4import { Subscription } from 'rxjs'
5import { Notifier } from '@app/core' 5import { Notifier, UserNotificationSocket } from '@app/core'
6import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' 6import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
7import { NavigationEnd, Router } from '@angular/router' 7import { NavigationEnd, Router } from '@angular/router'
8import { filter } from 'rxjs/operators' 8import { filter } from 'rxjs/operators'
@@ -23,6 +23,7 @@ export class AvatarNotificationComponent implements OnInit, OnDestroy {
23 23
24 constructor ( 24 constructor (
25 private userNotificationService: UserNotificationService, 25 private userNotificationService: UserNotificationService,
26 private userNotificationSocket: UserNotificationSocket,
26 private notifier: Notifier, 27 private notifier: Notifier,
27 private router: Router 28 private router: Router
28 ) {} 29 ) {}
@@ -53,7 +54,7 @@ export class AvatarNotificationComponent implements OnInit, OnDestroy {
53 } 54 }
54 55
55 private subscribeToNotifications () { 56 private subscribeToNotifications () {
56 this.notificationSub = this.userNotificationService.getMyNotificationsSocket() 57 this.notificationSub = this.userNotificationSocket.getMyNotificationsSocket()
57 .subscribe(data => { 58 .subscribe(data => {
58 if (data.type === 'new') return this.unreadNotifications++ 59 if (data.type === 'new') return this.unreadNotifications++
59 if (data.type === 'read') return this.unreadNotifications-- 60 if (data.type === 'read') return this.unreadNotifications--
diff --git a/client/src/app/menu/language-chooser.component.html b/client/src/app/menu/language-chooser.component.html
index c79609898..a62b33dda 100644
--- a/client/src/app/menu/language-chooser.component.html
+++ b/client/src/app/menu/language-chooser.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">Change the language</h4> 3 <h4 i18n class="modal-title">Change the language</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 7
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss
index a4aaadc7f..f30b89413 100644
--- a/client/src/app/menu/menu.component.scss
+++ b/client/src/app/menu/menu.component.scss
@@ -16,7 +16,7 @@ menu {
16 height: 100%; 16 height: 100%;
17 white-space: nowrap; 17 white-space: nowrap;
18 text-overflow: ellipsis; 18 text-overflow: ellipsis;
19 overflow: hidden; 19 overflow: auto;
20 color: var(--menuForegroundColor); 20 color: var(--menuForegroundColor);
21 display: flex; 21 display: flex;
22 flex-direction: column; 22 flex-direction: column;
@@ -243,7 +243,7 @@ menu {
243 } 243 }
244} 244}
245 245
246@media screen and (max-width: 400px) { 246@media screen and (max-width: $mobile-view) {
247 .menu-wrapper { 247 .menu-wrapper {
248 width: 100% !important; 248 width: 100% !important;
249 } 249 }
diff --git a/client/src/app/search/search.component.html b/client/src/app/search/search.component.html
index 3a87ea1de..82a5f0f26 100644
--- a/client/src/app/search/search.component.html
+++ b/client/src/app/search/search.component.html
@@ -48,7 +48,7 @@
48 </div> 48 </div>
49 49
50 <div *ngIf="isVideo(result)" class="entry video"> 50 <div *ngIf="isVideo(result)" class="entry video">
51 <my-video-thumbnail [video]="result"></my-video-thumbnail> 51 <my-video-thumbnail [video]="result" [nsfw]="isVideoBlur(result)"></my-video-thumbnail>
52 52
53 <div class="video-info"> 53 <div class="video-info">
54 <a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', result.uuid]" [attr.title]="result.name">{{ result.name }}</a> 54 <a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', result.uuid]" [attr.title]="result.name">{{ result.name }}</a>
diff --git a/client/src/app/search/search.component.scss b/client/src/app/search/search.component.scss
index 3e074621b..6de13d276 100644
--- a/client/src/app/search/search.component.scss
+++ b/client/src/app/search/search.component.scss
@@ -87,10 +87,10 @@
87 text-overflow: ellipsis; 87 text-overflow: ellipsis;
88 white-space: nowrap; 88 white-space: nowrap;
89 font-size: 14px; 89 font-size: 14px;
90 color: #585858; 90 color: $grey-foreground-color;
91 91
92 &:hover { 92 &:hover {
93 color: #303030; 93 color: $grey-foreground-hover-color;
94 } 94 }
95 } 95 }
96 } 96 }
diff --git a/client/src/app/search/search.component.ts b/client/src/app/search/search.component.ts
index 474b72824..c4a4b1fde 100644
--- a/client/src/app/search/search.component.ts
+++ b/client/src/app/search/search.component.ts
@@ -1,6 +1,6 @@
1import { Component, OnDestroy, OnInit } from '@angular/core' 1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { AuthService, Notifier } from '@app/core' 3import { AuthService, Notifier, ServerService } from '@app/core'
4import { forkJoin, Subscription } from 'rxjs' 4import { forkJoin, Subscription } from 'rxjs'
5import { SearchService } from '@app/search/search.service' 5import { SearchService } from '@app/search/search.service'
6import { ComponentPagination } from '@app/shared/rest/component-pagination.model' 6import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
@@ -41,7 +41,8 @@ export class SearchComponent implements OnInit, OnDestroy {
41 private metaService: MetaService, 41 private metaService: MetaService,
42 private notifier: Notifier, 42 private notifier: Notifier,
43 private searchService: SearchService, 43 private searchService: SearchService,
44 private authService: AuthService 44 private authService: AuthService,
45 private serverService: ServerService
45 ) { } 46 ) { }
46 47
47 ngOnInit () { 48 ngOnInit () {
@@ -75,6 +76,10 @@ export class SearchComponent implements OnInit, OnDestroy {
75 if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe() 76 if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe()
76 } 77 }
77 78
79 isVideoBlur (video: Video) {
80 return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig())
81 }
82
78 isVideoChannel (d: VideoChannel | Video): d is VideoChannel { 83 isVideoChannel (d: VideoChannel | Video): d is VideoChannel {
79 return d instanceof VideoChannel 84 return d instanceof VideoChannel
80 } 85 }
diff --git a/client/src/app/shared/actor/actor.model.ts b/client/src/app/shared/actor/actor.model.ts
index 811afb449..adecec1fc 100644
--- a/client/src/app/shared/actor/actor.model.ts
+++ b/client/src/app/shared/actor/actor.model.ts
@@ -16,7 +16,7 @@ export abstract class Actor implements ActorServer {
16 16
17 avatarUrl: string 17 avatarUrl: string
18 18
19 static GET_ACTOR_AVATAR_URL (actor: { avatar: Avatar }) { 19 static GET_ACTOR_AVATAR_URL (actor: { avatar?: { path: string } }) {
20 const absoluteAPIUrl = getAbsoluteAPIUrl() 20 const absoluteAPIUrl = getAbsoluteAPIUrl()
21 21
22 if (actor && actor.avatar) return absoluteAPIUrl + actor.avatar.path 22 if (actor && actor.avatar) return absoluteAPIUrl + actor.avatar.path
diff --git a/client/src/app/shared/buttons/action-dropdown.component.html b/client/src/app/shared/buttons/action-dropdown.component.html
index 90651f217..114b1d71f 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.html
+++ b/client/src/app/shared/buttons/action-dropdown.component.html
@@ -3,7 +3,7 @@
3 class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange' }" 3 class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange' }"
4 ngbDropdownToggle role="button" 4 ngbDropdownToggle role="button"
5 > 5 >
6 <span *ngIf="!label" class="icon icon-action"></span> 6 <my-global-icon *ngIf="!label" class="more-icon" iconName="more"></my-global-icon>
7 <span *ngIf="label" class="dropdown-toggle">{{ label }}</span> 7 <span *ngIf="label" class="dropdown-toggle">{{ label }}</span>
8 </div> 8 </div>
9 9
diff --git a/client/src/app/shared/buttons/action-dropdown.component.scss b/client/src/app/shared/buttons/action-dropdown.component.scss
index a4fcceeee..985b2ca88 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.scss
+++ b/client/src/app/shared/buttons/action-dropdown.component.scss
@@ -24,14 +24,11 @@
24 } 24 }
25 25
26 &:hover, &:active, &:focus { 26 &:hover, &:active, &:focus {
27 background-color: $grey-color; 27 background-color: $grey-background-color;
28 } 28 }
29 29
30 .icon-action { 30 .more-icon {
31 @include icon(21px); 31 width: 21px;
32
33 background-image: url('../../../assets/images/video/more.svg');
34 top: -1px;
35 } 32 }
36 33
37 &.small { 34 &.small {
diff --git a/client/src/app/shared/buttons/button.component.html b/client/src/app/shared/buttons/button.component.html
index 87a8daccf..b6df67102 100644
--- a/client/src/app/shared/buttons/button.component.html
+++ b/client/src/app/shared/buttons/button.component.html
@@ -1,4 +1,4 @@
1<span class="action-button" [ngClass]="className" [title]="getTitle()"> 1<span class="action-button" [ngClass]="className" [title]="getTitle()">
2 <span class="icon" [ngClass]="icon"></span> 2 <my-global-icon [iconName]="icon"></my-global-icon>
3 <span class="button-label">{{ label }}</span> 3 <span class="button-label">{{ label }}</span>
4</span> 4</span>
diff --git a/client/src/app/shared/buttons/button.component.scss b/client/src/app/shared/buttons/button.component.scss
index 168102f09..04199a2a9 100644
--- a/client/src/app/shared/buttons/button.component.scss
+++ b/client/src/app/shared/buttons/button.component.scss
@@ -3,41 +3,18 @@
3 3
4.action-button { 4.action-button {
5 @include peertube-button-link; 5 @include peertube-button-link;
6 @include button-with-icon(21px, 0, -2px);
6 7
7 font-size: 15px;
8 font-weight: $font-semibold; 8 font-weight: $font-semibold;
9 color: #585858; 9 color: $grey-foreground-color;
10 background-color: #E5E5E5; 10 background-color: $grey-background-color;
11 11
12 &:hover { 12 &:hover {
13 background-color: #EFEFEF; 13 background-color: $grey-background-hover-color;
14 } 14 }
15 15
16 .icon { 16 my-global-icon {
17 @include icon(21px); 17 @include apply-svg-color($grey-foreground-color);
18
19 position: relative;
20 top: -2px;
21
22 &.icon-edit {
23 background-image: url('../../../assets/images/global/edit-grey.svg');
24 }
25
26 &.icon-delete-grey {
27 background-image: url('../../../assets/images/global/delete-grey.svg');
28 }
29
30 &.icon-im-with-her {
31 background-image: url('../../../assets/images/global/im-with-her.svg');
32 }
33
34 &.icon-tick {
35 background-image: url('../../../assets/images/global/tick.svg');
36 }
37
38 &.icon-cross {
39 background-image: url('../../../assets/images/global/cross.svg');
40 }
41 } 18 }
42} 19}
43 20
diff --git a/client/src/app/shared/buttons/button.component.ts b/client/src/app/shared/buttons/button.component.ts
index 1a1162f09..a91e9c7eb 100644
--- a/client/src/app/shared/buttons/button.component.ts
+++ b/client/src/app/shared/buttons/button.component.ts
@@ -1,4 +1,5 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { GlobalIconName } from '@app/shared/icons/global-icon.component'
2 3
3@Component({ 4@Component({
4 selector: 'my-button', 5 selector: 'my-button',
@@ -9,7 +10,7 @@ import { Component, Input } from '@angular/core'
9export class ButtonComponent { 10export class ButtonComponent {
10 @Input() label = '' 11 @Input() label = ''
11 @Input() className: string = undefined 12 @Input() className: string = undefined
12 @Input() icon: string = undefined 13 @Input() icon: GlobalIconName = undefined
13 @Input() title: string = undefined 14 @Input() title: string = undefined
14 15
15 getTitle () { 16 getTitle () {
diff --git a/client/src/app/shared/buttons/delete-button.component.html b/client/src/app/shared/buttons/delete-button.component.html
index 6c55d8104..4d12a84c0 100644
--- a/client/src/app/shared/buttons/delete-button.component.html
+++ b/client/src/app/shared/buttons/delete-button.component.html
@@ -1,5 +1,5 @@
1<span class="action-button action-button-delete" [title]="getTitle()" role="button"> 1<span class="action-button action-button-delete" [title]="getTitle()" role="button">
2 <span class="icon icon-delete-grey"></span> 2 <my-global-icon iconName="delete"></my-global-icon>
3 3
4 <span class="button-label" *ngIf="label">{{ label }}</span> 4 <span class="button-label" *ngIf="label">{{ label }}</span>
5 <span class="button-label" i18n *ngIf="!label">Delete</span> 5 <span class="button-label" i18n *ngIf="!label">Delete</span>
diff --git a/client/src/app/shared/buttons/edit-button.component.html b/client/src/app/shared/buttons/edit-button.component.html
index cecb780f3..da3addbae 100644
--- a/client/src/app/shared/buttons/edit-button.component.html
+++ b/client/src/app/shared/buttons/edit-button.component.html
@@ -1,5 +1,5 @@
1<a class="action-button action-button-edit" [routerLink]="routerLink" i18n-title title="Edit"> 1<a class="action-button action-button-edit" [routerLink]="routerLink" i18n-title title="Edit">
2 <span class="icon icon-edit"></span> 2 <my-global-icon iconName="edit"></my-global-icon>
3 3
4 <span class="button-label" *ngIf="label">{{ label }}</span> 4 <span class="button-label" *ngIf="label">{{ label }}</span>
5 <span i18n class="button-label" *ngIf="!label">Edit</span> 5 <span i18n class="button-label" *ngIf="!label">Edit</span>
diff --git a/client/src/app/core/confirm/confirm.component.html b/client/src/app/shared/confirm/confirm.component.html
index 43f0c6190..65df1cd4d 100644
--- a/client/src/app/core/confirm/confirm.component.html
+++ b/client/src/app/shared/confirm/confirm.component.html
@@ -2,7 +2,8 @@
2 2
3 <div class="modal-header"> 3 <div class="modal-header">
4 <h4 class="modal-title">{{ title }}</h4> 4 <h4 class="modal-title">{{ title }}</h4>
5 <span class="close" aria-label="Close" role="button" (click)="dismiss()"></span> 5
6 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon>
6 </div> 7 </div>
7 8
8 <div class="modal-body" > 9 <div class="modal-body" >
diff --git a/client/src/app/core/confirm/confirm.component.scss b/client/src/app/shared/confirm/confirm.component.scss
index 93dd7926b..93dd7926b 100644
--- a/client/src/app/core/confirm/confirm.component.scss
+++ b/client/src/app/shared/confirm/confirm.component.scss
diff --git a/client/src/app/core/confirm/confirm.component.ts b/client/src/app/shared/confirm/confirm.component.ts
index 5138b7848..63c163da6 100644
--- a/client/src/app/core/confirm/confirm.component.ts
+++ b/client/src/app/shared/confirm/confirm.component.ts
@@ -1,5 +1,5 @@
1import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core' 1import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core'
2import { ConfirmService } from './confirm.service' 2import { ConfirmService } from '@app/core/confirm/confirm.service'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
diff --git a/client/src/app/shared/forms/form-validators/video-abuse-validators.service.ts b/client/src/app/shared/forms/form-validators/video-abuse-validators.service.ts
index 6e9806611..fcc966b84 100644
--- a/client/src/app/shared/forms/form-validators/video-abuse-validators.service.ts
+++ b/client/src/app/shared/forms/form-validators/video-abuse-validators.service.ts
@@ -10,20 +10,20 @@ export class VideoAbuseValidatorsService {
10 10
11 constructor (private i18n: I18n) { 11 constructor (private i18n: I18n) {
12 this.VIDEO_ABUSE_REASON = { 12 this.VIDEO_ABUSE_REASON = {
13 VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ], 13 VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
14 MESSAGES: { 14 MESSAGES: {
15 'required': this.i18n('Report reason is required.'), 15 'required': this.i18n('Report reason is required.'),
16 'minlength': this.i18n('Report reason must be at least 2 characters long.'), 16 'minlength': this.i18n('Report reason must be at least 2 characters long.'),
17 'maxlength': this.i18n('Report reason cannot be more than 300 characters long.') 17 'maxlength': this.i18n('Report reason cannot be more than 3000 characters long.')
18 } 18 }
19 } 19 }
20 20
21 this.VIDEO_ABUSE_MODERATION_COMMENT = { 21 this.VIDEO_ABUSE_MODERATION_COMMENT = {
22 VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ], 22 VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
23 MESSAGES: { 23 MESSAGES: {
24 'required': this.i18n('Moderation comment is required.'), 24 'required': this.i18n('Moderation comment is required.'),
25 'minlength': this.i18n('Moderation comment must be at least 2 characters long.'), 25 'minlength': this.i18n('Moderation comment must be at least 2 characters long.'),
26 'maxlength': this.i18n('Moderation comment cannot be more than 300 characters long.') 26 'maxlength': this.i18n('Moderation comment cannot be more than 3000 characters long.')
27 } 27 }
28 } 28 }
29 } 29 }
diff --git a/client/src/app/shared/forms/markdown-textarea.component.ts b/client/src/app/shared/forms/markdown-textarea.component.ts
index b99169ed2..e87aca0d4 100644
--- a/client/src/app/shared/forms/markdown-textarea.component.ts
+++ b/client/src/app/shared/forms/markdown-textarea.component.ts
@@ -1,10 +1,10 @@
1import { debounceTime, distinctUntilChanged } from 'rxjs/operators' 1import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
2import { Component, forwardRef, Input, OnInit } from '@angular/core' 2import { Component, forwardRef, Input, OnInit } from '@angular/core'
3import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 3import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
4import { MarkdownService } from '@app/videos/shared'
5import { Subject } from 'rxjs' 4import { Subject } from 'rxjs'
6import truncate from 'lodash-es/truncate' 5import truncate from 'lodash-es/truncate'
7import { ScreenService } from '@app/shared/misc/screen.service' 6import { ScreenService } from '@app/shared/misc/screen.service'
7import { MarkdownService } from '@app/shared/renderer'
8 8
9@Component({ 9@Component({
10 selector: 'my-markdown-textarea', 10 selector: 'my-markdown-textarea',
diff --git a/client/src/app/shared/forms/reactive-file.component.ts b/client/src/app/shared/forms/reactive-file.component.ts
index c3986838f..f60c38e8d 100644
--- a/client/src/app/shared/forms/reactive-file.component.ts
+++ b/client/src/app/shared/forms/reactive-file.component.ts
@@ -53,6 +53,17 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor {
53 return 53 return
54 } 54 }
55 55
56 const extension = '.' + file.name.split('.').pop()
57 if (this.extensions.includes(extension) === false) {
58 const message = this.i18n(
59 'PeerTube cannot handle this kind of file. Accepted extensions are {{extensions}}.',
60 { extensions: this.allowedExtensionsMessage }
61 )
62 this.notifier.error(message)
63
64 return
65 }
66
56 this.file = file 67 this.file = file
57 68
58 this.propagateChange(this.file) 69 this.propagateChange(this.file)
diff --git a/client/src/app/shared/icons/global-icon.component.html b/client/src/app/shared/icons/global-icon.component.html
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/client/src/app/shared/icons/global-icon.component.html
diff --git a/client/src/app/shared/icons/global-icon.component.scss b/client/src/app/shared/icons/global-icon.component.scss
new file mode 100644
index 000000000..6805fb6f7
--- /dev/null
+++ b/client/src/app/shared/icons/global-icon.component.scss
@@ -0,0 +1,4 @@
1/deep/ svg {
2 width: inherit;
3 height: inherit;
4}
diff --git a/client/src/app/shared/icons/global-icon.component.ts b/client/src/app/shared/icons/global-icon.component.ts
new file mode 100644
index 000000000..e8ada0324
--- /dev/null
+++ b/client/src/app/shared/icons/global-icon.component.ts
@@ -0,0 +1,48 @@
1import { Component, ElementRef, Input, OnInit } from '@angular/core'
2
3const icons = {
4 'add': require('../../../assets/images/global/add.html'),
5 'syndication': require('../../../assets/images/global/syndication.html'),
6 'help': require('../../../assets/images/global/help.html'),
7 'sparkle': require('../../../assets/images/global/sparkle.html'),
8 'alert': require('../../../assets/images/global/alert.html'),
9 'cloud-error': require('../../../assets/images/global/cloud-error.html'),
10 'user-add': require('../../../assets/images/global/user-add.html'),
11 'no': require('../../../assets/images/global/no.html'),
12 'cloud-download': require('../../../assets/images/global/cloud-download.html'),
13 'undo': require('../../../assets/images/global/undo.html'),
14 'circle-tick': require('../../../assets/images/global/circle-tick.html'),
15 'cog': require('../../../assets/images/global/cog.html'),
16 'download': require('../../../assets/images/global/download.html'),
17 'edit': require('../../../assets/images/global/edit.html'),
18 'im-with-her': require('../../../assets/images/global/im-with-her.html'),
19 'delete': require('../../../assets/images/global/delete.html'),
20 'cross': require('../../../assets/images/global/cross.html'),
21 'validate': require('../../../assets/images/global/validate.html'),
22 'tick': require('../../../assets/images/global/tick.html'),
23 'dislike': require('../../../assets/images/video/dislike.html'),
24 'heart': require('../../../assets/images/video/heart.html'),
25 'like': require('../../../assets/images/video/like.html'),
26 'more': require('../../../assets/images/video/more.html'),
27 'share': require('../../../assets/images/video/share.html'),
28 'upload': require('../../../assets/images/video/upload.html')
29}
30
31export type GlobalIconName = keyof typeof icons
32
33@Component({
34 selector: 'my-global-icon',
35 template: '',
36 styleUrls: [ './global-icon.component.scss' ]
37})
38export class GlobalIconComponent implements OnInit {
39 @Input() iconName: GlobalIconName
40
41 constructor (private el: ElementRef) {}
42
43 ngOnInit () {
44 const nativeElement = this.el.nativeElement
45
46 nativeElement.innerHTML = icons[this.iconName]
47 }
48}
diff --git a/client/src/app/shared/misc/help.component.html b/client/src/app/shared/misc/help.component.html
index 08a2fc367..444425c9f 100644
--- a/client/src/app/shared/misc/help.component.html
+++ b/client/src/app/shared/misc/help.component.html
@@ -25,4 +25,6 @@
25 [autoClose]="true" 25 [autoClose]="true"
26 (onHidden)="onPopoverHidden()" 26 (onHidden)="onPopoverHidden()"
27 (onShown)="onPopoverShown()" 27 (onShown)="onPopoverShown()"
28></span> 28>
29 <my-global-icon iconName="help"></my-global-icon>
30</span>
diff --git a/client/src/app/shared/misc/help.component.scss b/client/src/app/shared/misc/help.component.scss
index 047e53fab..3898f3cda 100644
--- a/client/src/app/shared/misc/help.component.scss
+++ b/client/src/app/shared/misc/help.component.scss
@@ -2,13 +2,17 @@
2@import '_mixins'; 2@import '_mixins';
3 3
4.help-tooltip-button { 4.help-tooltip-button {
5 @include icon(17px); 5 cursor: pointer;
6
7 position: relative;
8 top: -2px;
9 background-image: url('../../../assets/images/global/help.svg');
10 border: none; 6 border: none;
11 margin: 5px; 7
8 my-global-icon {
9 width: 17px;
10 position: relative;
11 top: -2px;
12 margin: 5px;
13
14 @include apply-svg-color(var(--mainForegroundColor))
15 }
12} 16}
13 17
14/deep/ { 18/deep/ {
@@ -16,16 +20,21 @@
16 max-width: 300px; 20 max-width: 300px;
17 21
18 .popover-body { 22 .popover-body {
23 font-family: $main-fonts;
19 text-align: left; 24 text-align: left;
20 padding: 10px; 25 padding: 10px;
21 font-size: 13px; 26 font-size: 13px;
22 font-family: $main-fonts; 27 background-color: var(--mainBackgroundColor);
23 background-color: #fff; 28 color: var(--mainForegroundColor);
24 color: #000;
25 box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); 29 box-shadow: 0 0 6px rgba(0, 0, 0, 0.5);
26 30
31 p {
32 margin-bottom: 0;
33 }
34
27 ul { 35 ul {
28 padding-left: 20px; 36 padding-left: 20px;
37 margin-bottom: 0;
29 } 38 }
30 } 39 }
31 } 40 }
diff --git a/client/src/app/shared/misc/help.component.ts b/client/src/app/shared/misc/help.component.ts
index ba0452e77..f3426f70f 100644
--- a/client/src/app/shared/misc/help.component.ts
+++ b/client/src/app/shared/misc/help.component.ts
@@ -1,6 +1,6 @@
1import { Component, Input, OnChanges, OnInit } from '@angular/core' 1import { Component, Input, OnChanges, OnInit } from '@angular/core'
2import { MarkdownService } from '@app/videos/shared'
3import { I18n } from '@ngx-translate/i18n-polyfill' 2import { I18n } from '@ngx-translate/i18n-polyfill'
3import { MarkdownService } from '@app/shared/renderer'
4 4
5@Component({ 5@Component({
6 selector: 'my-help', 6 selector: 'my-help',
diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts
index 78e8e9682..7cc6055c2 100644
--- a/client/src/app/shared/misc/utils.ts
+++ b/client/src/app/shared/misc/utils.ts
@@ -102,12 +102,18 @@ function objectToFormData (obj: any, form?: FormData, namespace?: string) {
102 return fd 102 return fd
103} 103}
104 104
105function lineFeedToHtml (obj: any, keyToNormalize: string) { 105function objectLineFeedToHtml (obj: any, keyToNormalize: string) {
106 return immutableAssign(obj, { 106 return immutableAssign(obj, {
107 [keyToNormalize]: obj[keyToNormalize].replace(/\r?\n|\r/g, '<br />') 107 [keyToNormalize]: lineFeedToHtml(obj[keyToNormalize])
108 }) 108 })
109} 109}
110 110
111function lineFeedToHtml (text: string) {
112 if (!text) return text
113
114 return text.replace(/\r?\n|\r/g, '<br />')
115}
116
111function removeElementFromArray <T> (arr: T[], elem: T) { 117function removeElementFromArray <T> (arr: T[], elem: T) {
112 const index = arr.indexOf(elem) 118 const index = arr.indexOf(elem)
113 if (index !== -1) arr.splice(index, 1) 119 if (index !== -1) arr.splice(index, 1)
@@ -131,6 +137,7 @@ function scrollToTop () {
131export { 137export {
132 sortBy, 138 sortBy,
133 durationToString, 139 durationToString,
140 lineFeedToHtml,
134 objectToUrlEncoded, 141 objectToUrlEncoded,
135 getParameterByName, 142 getParameterByName,
136 populateAsyncUserVideoChannels, 143 populateAsyncUserVideoChannels,
@@ -138,7 +145,7 @@ export {
138 dateToHuman, 145 dateToHuman,
139 immutableAssign, 146 immutableAssign,
140 objectToFormData, 147 objectToFormData,
141 lineFeedToHtml, 148 objectLineFeedToHtml,
142 removeElementFromArray, 149 removeElementFromArray,
143 scrollToTop 150 scrollToTop
144} 151}
diff --git a/client/src/app/shared/moderation/user-ban-modal.component.html b/client/src/app/shared/moderation/user-ban-modal.component.html
index fa5cb7404..f38ea543d 100644
--- a/client/src/app/shared/moderation/user-ban-modal.component.html
+++ b/client/src/app/shared/moderation/user-ban-modal.component.html
@@ -1,7 +1,8 @@
1<ng-template #modal> 1<ng-template #modal>
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Ban</h4> 3 <h4 i18n class="modal-title">Ban</h4>
4 <span class="close" aria-hidden="true" (click)="hideBanUserModal()"></span> 4
5 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 6 </div>
6 7
7 <div class="modal-body"> 8 <div class="modal-body">
@@ -19,7 +20,7 @@
19 </div> 20 </div>
20 21
21 <div class="form-group inputs"> 22 <div class="form-group inputs">
22 <span i18n class="action-button action-button-cancel" (click)="hideBanUserModal()">Cancel</span> 23 <span i18n class="action-button action-button-cancel" (click)="hide()">Cancel</span>
23 24
24 <input 25 <input
25 type="submit" i18n-value value="Ban this user" class="action-button-submit" 26 type="submit" i18n-value value="Ban this user" class="action-button-submit"
@@ -29,4 +30,4 @@
29 </form> 30 </form>
30 </div> 31 </div>
31 32
32</ng-template> \ No newline at end of file 33</ng-template>
diff --git a/client/src/app/shared/moderation/user-ban-modal.component.ts b/client/src/app/shared/moderation/user-ban-modal.component.ts
index f755ba0e8..942765301 100644
--- a/client/src/app/shared/moderation/user-ban-modal.component.ts
+++ b/client/src/app/shared/moderation/user-ban-modal.component.ts
@@ -42,7 +42,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
42 this.openedModal = this.modalService.open(this.modal) 42 this.openedModal = this.modalService.open(this.modal)
43 } 43 }
44 44
45 hideBanUserModal () { 45 hide () {
46 this.usersToBan = undefined 46 this.usersToBan = undefined
47 this.openedModal.close() 47 this.openedModal.close()
48 } 48 }
@@ -60,7 +60,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
60 this.notifier.success(message) 60 this.notifier.success(message)
61 61
62 this.userBanned.emit(this.usersToBan) 62 this.userBanned.emit(this.usersToBan)
63 this.hideBanUserModal() 63 this.hide()
64 }, 64 },
65 65
66 err => this.notifier.error(err.message) 66 err => this.notifier.error(err.message)
diff --git a/client/src/app/shared/renderer/html-renderer.service.ts b/client/src/app/shared/renderer/html-renderer.service.ts
new file mode 100644
index 000000000..d49df9b6d
--- /dev/null
+++ b/client/src/app/shared/renderer/html-renderer.service.ts
@@ -0,0 +1,35 @@
1import { Injectable } from '@angular/core'
2import { LinkifierService } from '@app/shared/renderer/linkifier.service'
3import * as sanitizeHtml from 'sanitize-html'
4
5@Injectable()
6export class HtmlRendererService {
7
8 constructor (private linkifier: LinkifierService) {
9
10 }
11
12 toSafeHtml (text: string) {
13 // Convert possible markdown to html
14 const html = this.linkifier.linkify(text)
15
16 return sanitizeHtml(html, {
17 allowedTags: [ 'a', 'p', 'span', 'br' ],
18 allowedSchemes: [ 'http', 'https' ],
19 allowedAttributes: {
20 'a': [ 'href', 'class', 'target' ]
21 },
22 transformTags: {
23 a: (tagName, attribs) => {
24 return {
25 tagName,
26 attribs: Object.assign(attribs, {
27 target: '_blank',
28 rel: 'noopener noreferrer'
29 })
30 }
31 }
32 }
33 })
34 }
35}
diff --git a/client/src/app/shared/renderer/index.ts b/client/src/app/shared/renderer/index.ts
new file mode 100644
index 000000000..39202b385
--- /dev/null
+++ b/client/src/app/shared/renderer/index.ts
@@ -0,0 +1,3 @@
1export * from './html-renderer.service'
2export * from './linkifier.service'
3export * from './markdown.service'
diff --git a/client/src/app/videos/+video-watch/comment/linkifier.service.ts b/client/src/app/shared/renderer/linkifier.service.ts
index 2529c9eaf..2529c9eaf 100644
--- a/client/src/app/videos/+video-watch/comment/linkifier.service.ts
+++ b/client/src/app/shared/renderer/linkifier.service.ts
diff --git a/client/src/app/videos/shared/markdown.service.ts b/client/src/app/shared/renderer/markdown.service.ts
index 07017eca5..07017eca5 100644
--- a/client/src/app/videos/shared/markdown.service.ts
+++ b/client/src/app/shared/renderer/markdown.service.ts
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index d1320aeec..6f8625c7e 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -6,7 +6,6 @@ import { RouterModule } from '@angular/router'
6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' 6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
7import { HelpComponent } from '@app/shared/misc/help.component' 7import { HelpComponent } from '@app/shared/misc/help.component'
8import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' 8import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
9import { MarkdownService } from '@app/videos/shared'
10 9
11import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' 10import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
12import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' 11import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
@@ -34,10 +33,10 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
34import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 33import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
35import { 34import {
36 CustomConfigValidatorsService, 35 CustomConfigValidatorsService,
36 InstanceValidatorsService,
37 LoginValidatorsService, 37 LoginValidatorsService,
38 ReactiveFileComponent, 38 ReactiveFileComponent,
39 ResetPasswordValidatorsService, 39 ResetPasswordValidatorsService,
40 InstanceValidatorsService,
41 TextareaAutoResizeDirective, 40 TextareaAutoResizeDirective,
42 UserValidatorsService, 41 UserValidatorsService,
43 VideoAbuseValidatorsService, 42 VideoAbuseValidatorsService,
@@ -67,6 +66,9 @@ import { UserHistoryService } from '@app/shared/users/user-history.service'
67import { UserNotificationService } from '@app/shared/users/user-notification.service' 66import { UserNotificationService } from '@app/shared/users/user-notification.service'
68import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component' 67import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component'
69import { InstanceService } from '@app/shared/instance/instance.service' 68import { InstanceService } from '@app/shared/instance/instance.service'
69import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer'
70import { ConfirmComponent } from '@app/shared/confirm/confirm.component'
71import { GlobalIconComponent } from '@app/shared/icons/global-icon.component'
70 72
71@NgModule({ 73@NgModule({
72 imports: [ 74 imports: [
@@ -110,7 +112,9 @@ import { InstanceService } from '@app/shared/instance/instance.service'
110 UserBanModalComponent, 112 UserBanModalComponent,
111 UserModerationDropdownComponent, 113 UserModerationDropdownComponent,
112 TopMenuDropdownComponent, 114 TopMenuDropdownComponent,
113 UserNotificationsComponent 115 UserNotificationsComponent,
116 ConfirmComponent,
117 GlobalIconComponent
114 ], 118 ],
115 119
116 exports: [ 120 exports: [
@@ -151,6 +155,8 @@ import { InstanceService } from '@app/shared/instance/instance.service'
151 UserModerationDropdownComponent, 155 UserModerationDropdownComponent,
152 TopMenuDropdownComponent, 156 TopMenuDropdownComponent,
153 UserNotificationsComponent, 157 UserNotificationsComponent,
158 ConfirmComponent,
159 GlobalIconComponent,
154 160
155 NumberFormatterPipe, 161 NumberFormatterPipe,
156 ObjectLengthPipe, 162 ObjectLengthPipe,
@@ -167,7 +173,6 @@ import { InstanceService } from '@app/shared/instance/instance.service'
167 UserService, 173 UserService,
168 VideoService, 174 VideoService,
169 AccountService, 175 AccountService,
170 MarkdownService,
171 VideoChannelService, 176 VideoChannelService,
172 VideoCaptionService, 177 VideoCaptionService,
173 VideoImportService, 178 VideoImportService,
@@ -192,6 +197,10 @@ import { InstanceService } from '@app/shared/instance/instance.service'
192 UserHistoryService, 197 UserHistoryService,
193 InstanceService, 198 InstanceService,
194 199
200 MarkdownService,
201 LinkifierService,
202 HtmlRendererService,
203
195 I18nPrimengCalendarService, 204 I18nPrimengCalendarService,
196 ScreenService, 205 ScreenService,
197 206
diff --git a/client/src/app/shared/users/user-notification.model.ts b/client/src/app/shared/users/user-notification.model.ts
index 5ff816fb8..125d2120c 100644
--- a/client/src/app/shared/users/user-notification.model.ts
+++ b/client/src/app/shared/users/user-notification.model.ts
@@ -1,4 +1,5 @@
1import { UserNotification as UserNotificationServer, UserNotificationType, VideoInfo } from '../../../../../shared' 1import { UserNotification as UserNotificationServer, UserNotificationType, VideoInfo, ActorInfo } from '../../../../../shared'
2import { Actor } from '@app/shared/actor/actor.model'
2 3
3export class UserNotification implements UserNotificationServer { 4export class UserNotification implements UserNotificationServer {
4 id: number 5 id: number
@@ -6,10 +7,7 @@ export class UserNotification implements UserNotificationServer {
6 read: boolean 7 read: boolean
7 8
8 video?: VideoInfo & { 9 video?: VideoInfo & {
9 channel: { 10 channel: ActorInfo & { avatarUrl?: string }
10 id: number
11 displayName: string
12 }
13 } 11 }
14 12
15 videoImport?: { 13 videoImport?: {
@@ -23,10 +21,7 @@ export class UserNotification implements UserNotificationServer {
23 comment?: { 21 comment?: {
24 id: number 22 id: number
25 threadId: number 23 threadId: number
26 account: { 24 account: ActorInfo & { avatarUrl?: string }
27 id: number
28 displayName: string
29 }
30 video: VideoInfo 25 video: VideoInfo
31 } 26 }
32 27
@@ -40,18 +35,11 @@ export class UserNotification implements UserNotificationServer {
40 video: VideoInfo 35 video: VideoInfo
41 } 36 }
42 37
43 account?: { 38 account?: ActorInfo & { avatarUrl?: string }
44 id: number
45 displayName: string
46 name: string
47 }
48 39
49 actorFollow?: { 40 actorFollow?: {
50 id: number 41 id: number
51 follower: { 42 follower: ActorInfo & { avatarUrl?: string }
52 name: string
53 displayName: string
54 }
55 following: { 43 following: {
56 type: 'account' | 'channel' 44 type: 'account' | 'channel'
57 name: string 45 name: string
@@ -76,12 +64,22 @@ export class UserNotification implements UserNotificationServer {
76 this.read = hash.read 64 this.read = hash.read
77 65
78 this.video = hash.video 66 this.video = hash.video
67 if (this.video) this.setAvatarUrl(this.video.channel)
68
79 this.videoImport = hash.videoImport 69 this.videoImport = hash.videoImport
70
80 this.comment = hash.comment 71 this.comment = hash.comment
72 if (this.comment) this.setAvatarUrl(this.comment.account)
73
81 this.videoAbuse = hash.videoAbuse 74 this.videoAbuse = hash.videoAbuse
75
82 this.videoBlacklist = hash.videoBlacklist 76 this.videoBlacklist = hash.videoBlacklist
77
83 this.account = hash.account 78 this.account = hash.account
79 if (this.account) this.setAvatarUrl(this.account)
80
84 this.actorFollow = hash.actorFollow 81 this.actorFollow = hash.actorFollow
82 if (this.actorFollow) this.setAvatarUrl(this.actorFollow.follower)
85 83
86 this.createdAt = hash.createdAt 84 this.createdAt = hash.createdAt
87 this.updatedAt = hash.updatedAt 85 this.updatedAt = hash.updatedAt
@@ -97,6 +95,7 @@ export class UserNotification implements UserNotificationServer {
97 95
98 case UserNotificationType.NEW_COMMENT_ON_MY_VIDEO: 96 case UserNotificationType.NEW_COMMENT_ON_MY_VIDEO:
99 case UserNotificationType.COMMENT_MENTION: 97 case UserNotificationType.COMMENT_MENTION:
98 this.accountUrl = this.buildAccountUrl(this.comment.account)
100 this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ] 99 this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ]
101 break 100 break
102 101
@@ -138,8 +137,8 @@ export class UserNotification implements UserNotificationServer {
138 return '/videos/watch/' + video.uuid 137 return '/videos/watch/' + video.uuid
139 } 138 }
140 139
141 private buildAccountUrl (account: { name: string }) { 140 private buildAccountUrl (account: { name: string, host: string }) {
142 return '/accounts/' + account.name 141 return '/accounts/' + Actor.CREATE_BY_STRING(account.name, account.host)
143 } 142 }
144 143
145 private buildVideoImportUrl () { 144 private buildVideoImportUrl () {
@@ -150,4 +149,7 @@ export class UserNotification implements UserNotificationServer {
150 return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName 149 return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName
151 } 150 }
152 151
152 private setAvatarUrl (actor: { avatarUrl?: string, avatar?: { path: string } }) {
153 actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor)
154 }
153} 155}
diff --git a/client/src/app/shared/users/user-notification.service.ts b/client/src/app/shared/users/user-notification.service.ts
index 2dfee8060..f8a30955d 100644
--- a/client/src/app/shared/users/user-notification.service.ts
+++ b/client/src/app/shared/users/user-notification.service.ts
@@ -1,30 +1,26 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { HttpClient, HttpParams } from '@angular/common/http' 2import { HttpClient, HttpParams } from '@angular/common/http'
3import { RestExtractor, RestService } from '@app/shared/rest' 3import { RestExtractor, RestService } from '../rest'
4import { catchError, map, tap } from 'rxjs/operators' 4import { catchError, map, tap } from 'rxjs/operators'
5import { environment } from '../../../environments/environment' 5import { environment } from '../../../environments/environment'
6import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '../../../../../shared' 6import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '../../../../../shared'
7import { UserNotification } from '@app/shared/users/user-notification.model' 7import { UserNotification } from './user-notification.model'
8import { Subject } from 'rxjs' 8import { AuthService } from '../../core'
9import * as io from 'socket.io-client' 9import { ComponentPagination } from '../rest/component-pagination.model'
10import { AuthService } from '@app/core' 10import { User } from '..'
11import { ComponentPagination } from '@app/shared/rest/component-pagination.model' 11import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service'
12import { User } from '@app/shared'
13 12
14@Injectable() 13@Injectable()
15export class UserNotificationService { 14export class UserNotificationService {
16 static BASE_NOTIFICATIONS_URL = environment.apiUrl + '/api/v1/users/me/notifications' 15 static BASE_NOTIFICATIONS_URL = environment.apiUrl + '/api/v1/users/me/notifications'
17 static BASE_NOTIFICATION_SETTINGS = environment.apiUrl + '/api/v1/users/me/notification-settings' 16 static BASE_NOTIFICATION_SETTINGS = environment.apiUrl + '/api/v1/users/me/notification-settings'
18 17
19 private notificationSubject = new Subject<{ type: 'new' | 'read' | 'read-all', notification?: UserNotification }>()
20
21 private socket: SocketIOClient.Socket
22
23 constructor ( 18 constructor (
24 private auth: AuthService, 19 private auth: AuthService,
25 private authHttp: HttpClient, 20 private authHttp: HttpClient,
26 private restExtractor: RestExtractor, 21 private restExtractor: RestExtractor,
27 private restService: RestService 22 private restService: RestService,
23 private userNotificationSocket: UserNotificationSocket
28 ) {} 24 ) {}
29 25
30 listMyNotifications (pagination: ComponentPagination, unread?: boolean, ignoreLoadingBar = false) { 26 listMyNotifications (pagination: ComponentPagination, unread?: boolean, ignoreLoadingBar = false) {
@@ -48,16 +44,6 @@ export class UserNotificationService {
48 .pipe(map(n => n.total)) 44 .pipe(map(n => n.total))
49 } 45 }
50 46
51 getMyNotificationsSocket () {
52 const socket = this.getSocket()
53
54 socket.on('new-notification', (n: UserNotificationServer) => {
55 this.notificationSubject.next({ type: 'new', notification: new UserNotification(n) })
56 })
57
58 return this.notificationSubject.asObservable()
59 }
60
61 markAsRead (notification: UserNotification) { 47 markAsRead (notification: UserNotification) {
62 const url = UserNotificationService.BASE_NOTIFICATIONS_URL + '/read' 48 const url = UserNotificationService.BASE_NOTIFICATIONS_URL + '/read'
63 49
@@ -67,7 +53,7 @@ export class UserNotificationService {
67 return this.authHttp.post(url, body, { headers }) 53 return this.authHttp.post(url, body, { headers })
68 .pipe( 54 .pipe(
69 map(this.restExtractor.extractDataBool), 55 map(this.restExtractor.extractDataBool),
70 tap(() => this.notificationSubject.next({ type: 'read' })), 56 tap(() => this.userNotificationSocket.dispatch('read')),
71 catchError(res => this.restExtractor.handleError(res)) 57 catchError(res => this.restExtractor.handleError(res))
72 ) 58 )
73 } 59 }
@@ -79,7 +65,7 @@ export class UserNotificationService {
79 return this.authHttp.post(url, {}, { headers }) 65 return this.authHttp.post(url, {}, { headers })
80 .pipe( 66 .pipe(
81 map(this.restExtractor.extractDataBool), 67 map(this.restExtractor.extractDataBool),
82 tap(() => this.notificationSubject.next({ type: 'read-all' })), 68 tap(() => this.userNotificationSocket.dispatch('read-all')),
83 catchError(res => this.restExtractor.handleError(res)) 69 catchError(res => this.restExtractor.handleError(res))
84 ) 70 )
85 } 71 }
@@ -94,16 +80,6 @@ export class UserNotificationService {
94 ) 80 )
95 } 81 }
96 82
97 private getSocket () {
98 if (this.socket) return this.socket
99
100 this.socket = io(environment.apiUrl + '/user-notifications', {
101 query: { accessToken: this.auth.getAccessToken() }
102 })
103
104 return this.socket
105 }
106
107 private formatNotification (notification: UserNotificationServer) { 83 private formatNotification (notification: UserNotificationServer) {
108 return new UserNotification(notification) 84 return new UserNotification(notification)
109 } 85 }
diff --git a/client/src/app/shared/users/user-notifications.component.html b/client/src/app/shared/users/user-notifications.component.html
index 86379d941..0d69e0feb 100644
--- a/client/src/app/shared/users/user-notifications.component.html
+++ b/client/src/app/shared/users/user-notifications.component.html
@@ -1,61 +1,101 @@
1<div *ngIf="componentPagination.totalItems === 0" class="no-notification" i18n>You don't have notifications.</div> 1<div *ngIf="componentPagination.totalItems === 0" class="no-notification" i18n>You don't have notifications.</div>
2 2
3<div class="notifications" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()"> 3<div class="notifications" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()">
4 <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }"> 4 <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)">
5 5
6 <div [ngSwitch]="notification.type"> 6 <ng-container [ngSwitch]="notification.type">
7 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION"> 7 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION">
8 {{ notification.video.channel.displayName }} published a <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">new video</a> 8 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.video.channel.avatarUrl" />
9
10 <div class="message">
11 {{ notification.video.channel.displayName }} published a <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">new video</a>
12 </div>
9 </ng-container> 13 </ng-container>
10 14
11 <ng-container i18n *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO"> 15 <ng-container i18n *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO">
12 Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been unblacklisted 16 <my-global-icon iconName="undo"></my-global-icon>
17
18 <div class="message">
19 Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been unblacklisted
20 </div>
13 </ng-container> 21 </ng-container>
14 22
15 <ng-container i18n *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO"> 23 <ng-container i18n *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO">
16 Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoBlacklist.video.name }}</a> has been blacklisted 24 <my-global-icon iconName="no"></my-global-icon>
25
26 <div class="message">
27 Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoBlacklist.video.name }}</a> has been blacklisted
28 </div>
17 </ng-container> 29 </ng-container>
18 30
19 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS"> 31 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS">
20 <a (click)="markAsRead(notification)" [routerLink]="notification.videoAbuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoAbuse.video.name }}</a> 32 <my-global-icon iconName="alert"></my-global-icon>
33
34 <div class="message">
35 <a (click)="markAsRead(notification)" [routerLink]="notification.videoAbuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoAbuse.video.name }}</a>
36 </div>
21 </ng-container> 37 </ng-container>
22 38
23 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO"> 39 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO">
24 {{ notification.comment.account.displayName }} commented your video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.comment.video.name }}</a> 40 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
41
42 <div class="message">
43 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.comment.account.displayName }}</a> commented your video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.comment.video.name }}</a>
44 </div>
25 </ng-container> 45 </ng-container>
26 46
27 <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED"> 47 <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED">
28 Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been published 48 <my-global-icon iconName="sparkle"></my-global-icon>
49
50 <div class="message">
51 Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been published
52 </div>
29 </ng-container> 53 </ng-container>
30 54
31 <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS"> 55 <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS">
32 <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">Your video import</a> {{ notification.videoImportIdentifier }} succeeded 56 <my-global-icon iconName="cloud-download"></my-global-icon>
57
58 <div class="message">
59 <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">Your video import</a> {{ notification.videoImportIdentifier }} succeeded
60 </div>
33 </ng-container> 61 </ng-container>
34 62
35 <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR"> 63 <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR">
36 <a (click)="markAsRead(notification)" [routerLink]="notification.videoImportUrl">Your video import</a> {{ notification.videoImportIdentifier }} failed 64 <my-global-icon iconName="cloud-error"></my-global-icon>
65
66 <div class="message">
67 <a (click)="markAsRead(notification)" [routerLink]="notification.videoImportUrl">Your video import</a> {{ notification.videoImportIdentifier }} failed
68 </div>
37 </ng-container> 69 </ng-container>
38 70
39 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION"> 71 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION">
40 User <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.account.name }} registered</a> on your instance 72 <my-global-icon iconName="user-add"></my-global-icon>
73
74 <div class="message">
75 User <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.account.name }} registered</a> on your instance
76 </div>
41 </ng-container> 77 </ng-container>
42 78
43 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_FOLLOW"> 79 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_FOLLOW">
44 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.actorFollow.follower.displayName }}</a> is following 80 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" />
45 81
46 <ng-container *ngIf="notification.actorFollow.following.type === 'channel'"> 82 <div class="message">
47 your channel {{ notification.actorFollow.following.displayName }} 83 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.actorFollow.follower.displayName }}</a> is following
48 </ng-container> 84
49 <ng-container *ngIf="notification.actorFollow.following.type === 'account'">your account</ng-container> 85 <ng-container *ngIf="notification.actorFollow.following.type === 'channel'">your channel {{ notification.actorFollow.following.displayName }}</ng-container>
86 <ng-container *ngIf="notification.actorFollow.following.type === 'account'">your account</ng-container>
87 </div>
50 </ng-container> 88 </ng-container>
51 89
52 <ng-container i18n *ngSwitchCase="UserNotificationType.COMMENT_MENTION"> 90 <ng-container i18n *ngSwitchCase="UserNotificationType.COMMENT_MENTION">
53 {{ notification.comment.account.displayName }} mentioned you on <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">video {{ notification.comment.video.name }}</a> 91 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
92
93 <div class="message">
94 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.comment.account.displayName }}</a> mentioned you on <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">video {{ notification.comment.video.name }}</a>
95 </div>
54 </ng-container> 96 </ng-container>
55 </div> 97 </ng-container>
56 98
57 <div i18n title="Mark as read" class="mark-as-read"> 99 <div class="from-date">{{ notification.createdAt | myFromNow }}</div>
58 <div class="glyphicon glyphicon-ok" (click)="markAsRead(notification)"></div>
59 </div>
60 </div> 100 </div>
61</div> 101</div>
diff --git a/client/src/app/shared/users/user-notifications.component.scss b/client/src/app/shared/users/user-notifications.component.scss
index 0493b10d9..315d504c9 100644
--- a/client/src/app/shared/users/user-notifications.component.scss
+++ b/client/src/app/shared/users/user-notifications.component.scss
@@ -1,30 +1,51 @@
1@import '_variables';
2@import '_mixins';
3
4.no-notification {
5 display: flex;
6 justify-content: center;
7 align-items: center;
8 padding: 20px 0;
9}
10
1.notification { 11.notification {
2 display: flex; 12 display: flex;
3 justify-content: space-between;
4 align-items: center; 13 align-items: center;
5 font-size: inherit; 14 font-size: inherit;
6 padding: 15px 10px; 15 padding: 15px 5px 15px 10px;
7 border-bottom: 1px solid rgba(0, 0, 0, 0.10); 16 border-bottom: 1px solid rgba(0, 0, 0, 0.10);
8 17
9 .mark-as-read { 18 &.unread {
10 min-width: 35px; 19 background-color: rgba(0, 0, 0, 0.05);
20 }
21
22 my-global-icon {
23 width: 24px;
24 margin-right: 11px;
25 margin-left: 3px;
11 26
12 .glyphicon { 27 @include apply-svg-color(#333);
13 display: none;
14 cursor: pointer;
15 color: rgba(20, 20, 20, 0.5)
16 }
17 } 28 }
18 29
19 &.unread { 30 .avatar {
20 background-color: rgba(0, 0, 0, 0.05); 31 @include avatar(30px);
21 32
22 &:hover .mark-as-read .glyphicon { 33 margin-right: 10px;
23 display: block; 34 }
35
36 .message {
37 flex-grow: 1;
24 38
25 &:hover { 39 a {
26 color: rgba(20, 20, 20, 0.8); 40 font-weight: $font-semibold;
27 }
28 } 41 }
29 } 42 }
43
44 .from-date {
45 font-size: 0.85em;
46 color: $grey-foreground-color;
47 padding-left: 5px;
48 min-width: 70px;
49 text-align: right;
50 }
30} 51}
diff --git a/client/src/app/shared/users/user-notifications.component.ts b/client/src/app/shared/users/user-notifications.component.ts
index 682116226..b5f9fd399 100644
--- a/client/src/app/shared/users/user-notifications.component.ts
+++ b/client/src/app/shared/users/user-notifications.component.ts
@@ -13,17 +13,14 @@ import { UserNotification } from '@app/shared/users/user-notification.model'
13export class UserNotificationsComponent implements OnInit { 13export class UserNotificationsComponent implements OnInit {
14 @Input() ignoreLoadingBar = false 14 @Input() ignoreLoadingBar = false
15 @Input() infiniteScroll = true 15 @Input() infiniteScroll = true
16 @Input() itemsPerPage = 20
16 17
17 notifications: UserNotification[] = [] 18 notifications: UserNotification[] = []
18 19
19 // So we can access it in the template 20 // So we can access it in the template
20 UserNotificationType = UserNotificationType 21 UserNotificationType = UserNotificationType
21 22
22 componentPagination: ComponentPagination = { 23 componentPagination: ComponentPagination
23 currentPage: 1,
24 itemsPerPage: 10,
25 totalItems: null
26 }
27 24
28 constructor ( 25 constructor (
29 private userNotificationService: UserNotificationService, 26 private userNotificationService: UserNotificationService,
@@ -31,6 +28,12 @@ export class UserNotificationsComponent implements OnInit {
31 ) { } 28 ) { }
32 29
33 ngOnInit () { 30 ngOnInit () {
31 this.componentPagination = {
32 currentPage: 1,
33 itemsPerPage: this.itemsPerPage, // Reset items per page, because of the @Input() variable
34 totalItems: null
35 }
36
34 this.loadMoreNotifications() 37 this.loadMoreNotifications()
35 } 38 }
36 39
@@ -57,6 +60,8 @@ export class UserNotificationsComponent implements OnInit {
57 } 60 }
58 61
59 markAsRead (notification: UserNotification) { 62 markAsRead (notification: UserNotification) {
63 if (notification.read) return
64
60 this.userNotificationService.markAsRead(notification) 65 this.userNotificationService.markAsRead(notification)
61 .subscribe( 66 .subscribe(
62 () => { 67 () => {
diff --git a/client/src/app/shared/video-abuse/video-abuse.service.ts b/client/src/app/shared/video-abuse/video-abuse.service.ts
index 61b7e1b98..b0b59ea0c 100644
--- a/client/src/app/shared/video-abuse/video-abuse.service.ts
+++ b/client/src/app/shared/video-abuse/video-abuse.service.ts
@@ -32,9 +32,7 @@ export class VideoAbuseService {
32 32
33 reportVideo (id: number, reason: string) { 33 reportVideo (id: number, reason: string) {
34 const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse' 34 const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse'
35 const body = { 35 const body = { reason }
36 reason
37 }
38 36
39 return this.authHttp.post(url, body) 37 return this.authHttp.post(url, body)
40 .pipe( 38 .pipe(
diff --git a/client/src/app/shared/video-blacklist/video-blacklist.service.ts b/client/src/app/shared/video-blacklist/video-blacklist.service.ts
index 7d39fd4f2..94e46d7c2 100644
--- a/client/src/app/shared/video-blacklist/video-blacklist.service.ts
+++ b/client/src/app/shared/video-blacklist/video-blacklist.service.ts
@@ -36,8 +36,11 @@ export class VideoBlacklistService {
36 ) 36 )
37 } 37 }
38 38
39 blacklistVideo (videoId: number, reason?: string) { 39 blacklistVideo (videoId: number, reason: string, unfederate: boolean) {
40 const body = reason ? { reason } : {} 40 const body = {
41 unfederate,
42 reason
43 }
41 44
42 return this.authHttp.post(VideoBlacklistService.BASE_VIDEOS_URL + videoId + '/blacklist', body) 45 return this.authHttp.post(VideoBlacklistService.BASE_VIDEOS_URL + videoId + '/blacklist', body)
43 .pipe( 46 .pipe(
diff --git a/client/src/app/shared/video-import/video-import.service.ts b/client/src/app/shared/video-import/video-import.service.ts
index 11a7694c8..7ae13154d 100644
--- a/client/src/app/shared/video-import/video-import.service.ts
+++ b/client/src/app/shared/video-import/video-import.service.ts
@@ -82,6 +82,7 @@ export class VideoImportService {
82 nsfw: video.nsfw, 82 nsfw: video.nsfw,
83 waitTranscoding: video.waitTranscoding, 83 waitTranscoding: video.waitTranscoding,
84 commentsEnabled: video.commentsEnabled, 84 commentsEnabled: video.commentsEnabled,
85 downloadEnabled: video.downloadEnabled,
85 thumbnailfile: video.thumbnailfile, 86 thumbnailfile: video.thumbnailfile,
86 previewfile: video.previewfile, 87 previewfile: video.previewfile,
87 scheduleUpdate, 88 scheduleUpdate,
diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html
index 29492351b..1f97bc389 100644
--- a/client/src/app/shared/video/abstract-video-list.html
+++ b/client/src/app/shared/video/abstract-video-list.html
@@ -1,8 +1,11 @@
1<div [ngClass]="{ 'margin-content': marginContent }"> 1<div [ngClass]="{ 'margin-content': marginContent }">
2 <div class="videos-header"> 2 <div class="videos-header">
3 <div *ngIf="titlePage" class="title-page title-page-single"> 3 <div *ngIf="titlePage" class="title-page title-page-single">
4 {{ titlePage }} 4 <div placement="bottom" [ngbTooltip]="titleTooltip" container="body">
5 {{ titlePage }}
6 </div>
5 </div> 7 </div>
8
6 <my-feed [syndicationItems]="syndicationItems"></my-feed> 9 <my-feed [syndicationItems]="syndicationItems"></my-feed>
7 10
8 <div class="moderation-block" *ngIf="displayModerationBlock"> 11 <div class="moderation-block" *ngIf="displayModerationBlock">
diff --git a/client/src/app/shared/video/abstract-video-list.scss b/client/src/app/shared/video/abstract-video-list.scss
index 9fb3fd4d6..292ede698 100644
--- a/client/src/app/shared/video/abstract-video-list.scss
+++ b/client/src/app/shared/video/abstract-video-list.scss
@@ -19,8 +19,8 @@
19 19
20 my-feed { 20 my-feed {
21 display: inline-block; 21 display: inline-block;
22 position: relative;
23 top: 1px; 22 top: 1px;
23 min-width: 60px;
24 } 24 }
25 25
26 .moderation-block { 26 .moderation-block {
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts
index d234c8bfa..b0633be4a 100644
--- a/client/src/app/shared/video/abstract-video-list.ts
+++ b/client/src/app/shared/video/abstract-video-list.ts
@@ -39,6 +39,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
39 ownerDisplayType: OwnerDisplayType = 'account' 39 ownerDisplayType: OwnerDisplayType = 'account'
40 firstLoadedPage: number 40 firstLoadedPage: number
41 displayModerationBlock = false 41 displayModerationBlock = false
42 titleTooltip: string
42 43
43 protected baseVideoWidth = 215 44 protected baseVideoWidth = 215
44 protected baseVideoHeight = 205 45 protected baseVideoHeight = 205
diff --git a/client/src/app/shared/video/feed.component.html b/client/src/app/shared/video/feed.component.html
index 16116ba88..f7624ec01 100644
--- a/client/src/app/shared/video/feed.component.html
+++ b/client/src/app/shared/video/feed.component.html
@@ -1,10 +1,11 @@
1<div class="video-feed"> 1<div class="video-feed">
2 <span 2 <my-global-icon
3 *ngIf="syndicationItems.length !== 0" [ngbPopover]="feedsList" [autoClose]="true" placement="bottom" 3 *ngIf="syndicationItems.length !== 0" [ngbPopover]="feedsList" [autoClose]="true" placement="bottom"
4 class="icon icon-syndication" role="button" 4 class="icon-syndication" role="button" iconName="syndication"
5 ></span> 5 >
6 </my-global-icon>
6 7
7 <ng-template #feedsList> 8 <ng-template #feedsList>
8 <a *ngFor="let item of syndicationItems" [href]="item.url" target="_blank" rel="noopener noreferrer">{{ item.label }}</a> 9 <a *ngFor="let item of syndicationItems" [href]="item.url" target="_blank" rel="noopener noreferrer">{{ item.label }}</a>
9 </ng-template> 10 </ng-template>
10</div> \ No newline at end of file 11</div>
diff --git a/client/src/app/shared/video/feed.component.scss b/client/src/app/shared/video/feed.component.scss
index 385764be0..ed1dc17d3 100644
--- a/client/src/app/shared/video/feed.component.scss
+++ b/client/src/app/shared/video/feed.component.scss
@@ -1,3 +1,4 @@
1@import '_variables';
1@import '_mixins'; 2@import '_mixins';
2 3
3.video-feed { 4.video-feed {
@@ -6,14 +7,12 @@
6 display: block; 7 display: block;
7 } 8 }
8 9
9 .icon { 10 my-global-icon {
10 @include icon(12px); 11 cursor: pointer;
12 width: 12px;
13 position: relative;
14 top: -2px;
11 15
12 &.icon-syndication { 16 @include apply-svg-color(var(--mainForegroundColor))
13 position: relative;
14 top: -2px;
15 background-color: var(--mainForegroundColor);
16 mask-image: url('../../../assets/images/global/syndication.svg');
17 }
18 } 17 }
19} \ No newline at end of file 18}
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts
index fa4ca7f93..388357343 100644
--- a/client/src/app/shared/video/video-details.model.ts
+++ b/client/src/app/shared/video/video-details.model.ts
@@ -3,6 +3,8 @@ import { AuthUser } from '../../core'
3import { Video } from '../../shared/video/video.model' 3import { Video } from '../../shared/video/video.model'
4import { Account } from '@app/shared/account/account.model' 4import { Account } from '@app/shared/account/account.model'
5import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 5import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
6import { VideoStreamingPlaylist } from '../../../../../shared/models/videos/video-streaming-playlist.model'
7import { VideoStreamingPlaylistType } from '../../../../../shared/models/videos/video-streaming-playlist.type'
6 8
7export class VideoDetails extends Video implements VideoDetailsServerModel { 9export class VideoDetails extends Video implements VideoDetailsServerModel {
8 descriptionPath: string 10 descriptionPath: string
@@ -12,6 +14,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
12 files: VideoFile[] 14 files: VideoFile[]
13 account: Account 15 account: Account
14 commentsEnabled: boolean 16 commentsEnabled: boolean
17 downloadEnabled: boolean
15 18
16 waitTranscoding: boolean 19 waitTranscoding: boolean
17 state: VideoConstant<VideoState> 20 state: VideoConstant<VideoState>
@@ -19,6 +22,10 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
19 likesPercent: number 22 likesPercent: number
20 dislikesPercent: number 23 dislikesPercent: number
21 24
25 trackerUrls: string[]
26
27 streamingPlaylists: VideoStreamingPlaylist[]
28
22 constructor (hash: VideoDetailsServerModel, translations = {}) { 29 constructor (hash: VideoDetailsServerModel, translations = {}) {
23 super(hash, translations) 30 super(hash, translations)
24 31
@@ -29,6 +36,10 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
29 this.tags = hash.tags 36 this.tags = hash.tags
30 this.support = hash.support 37 this.support = hash.support
31 this.commentsEnabled = hash.commentsEnabled 38 this.commentsEnabled = hash.commentsEnabled
39 this.downloadEnabled = hash.downloadEnabled
40
41 this.trackerUrls = hash.trackerUrls
42 this.streamingPlaylists = hash.streamingPlaylists
32 43
33 this.buildLikeAndDislikePercents() 44 this.buildLikeAndDislikePercents()
34 } 45 }
@@ -53,4 +64,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
53 this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 64 this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100
54 this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 65 this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100
55 } 66 }
67
68 getHlsPlaylist () {
69 return this.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
70 }
56} 71}
diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts
index 9078bb5d2..c5d5bb406 100644
--- a/client/src/app/shared/video/video-edit.model.ts
+++ b/client/src/app/shared/video/video-edit.model.ts
@@ -14,6 +14,7 @@ export class VideoEdit implements VideoUpdate {
14 tags: string[] 14 tags: string[]
15 nsfw: boolean 15 nsfw: boolean
16 commentsEnabled: boolean 16 commentsEnabled: boolean
17 downloadEnabled: boolean
17 waitTranscoding: boolean 18 waitTranscoding: boolean
18 channelId: number 19 channelId: number
19 privacy: VideoPrivacy 20 privacy: VideoPrivacy
@@ -27,7 +28,15 @@ export class VideoEdit implements VideoUpdate {
27 scheduleUpdate?: VideoScheduleUpdate 28 scheduleUpdate?: VideoScheduleUpdate
28 originallyPublishedAt?: Date | string 29 originallyPublishedAt?: Date | string
29 30
30 constructor (video?: Video & { tags: string[], commentsEnabled: boolean, support: string, thumbnailUrl: string, previewUrl: string }) { 31 constructor (
32 video?: Video & {
33 tags: string[],
34 commentsEnabled: boolean,
35 downloadEnabled: boolean,
36 support: string,
37 thumbnailUrl: string,
38 previewUrl: string
39 }) {
31 if (video) { 40 if (video) {
32 this.id = video.id 41 this.id = video.id
33 this.uuid = video.uuid 42 this.uuid = video.uuid
@@ -39,6 +48,7 @@ export class VideoEdit implements VideoUpdate {
39 this.tags = video.tags 48 this.tags = video.tags
40 this.nsfw = video.nsfw 49 this.nsfw = video.nsfw
41 this.commentsEnabled = video.commentsEnabled 50 this.commentsEnabled = video.commentsEnabled
51 this.downloadEnabled = video.downloadEnabled
42 this.waitTranscoding = video.waitTranscoding 52 this.waitTranscoding = video.waitTranscoding
43 this.channelId = video.channel.id 53 this.channelId = video.channel.id
44 this.privacy = video.privacy.id 54 this.privacy = video.privacy.id
@@ -88,6 +98,7 @@ export class VideoEdit implements VideoUpdate {
88 tags: this.tags, 98 tags: this.tags,
89 nsfw: this.nsfw, 99 nsfw: this.nsfw,
90 commentsEnabled: this.commentsEnabled, 100 commentsEnabled: this.commentsEnabled,
101 downloadEnabled: this.downloadEnabled,
91 waitTranscoding: this.waitTranscoding, 102 waitTranscoding: this.waitTranscoding,
92 channelId: this.channelId, 103 channelId: this.channelId,
93 privacy: this.privacy, 104 privacy: this.privacy,
diff --git a/client/src/app/shared/video/video-miniature.component.scss b/client/src/app/shared/video/video-miniature.component.scss
index 895879adc..f44bdf9a9 100644
--- a/client/src/app/shared/video/video-miniature.component.scss
+++ b/client/src/app/shared/video/video-miniature.component.scss
@@ -50,10 +50,10 @@
50 text-overflow: ellipsis; 50 text-overflow: ellipsis;
51 white-space: nowrap; 51 white-space: nowrap;
52 font-size: 13px; 52 font-size: 13px;
53 color: #585858; 53 color: $grey-foreground-color;
54 54
55 &:hover { 55 &:hover {
56 color: #303030; 56 color: $grey-foreground-hover-color;
57 } 57 }
58 } 58 }
59 } 59 }
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts
index c9b052951..460c09258 100644
--- a/client/src/app/shared/video/video.model.ts
+++ b/client/src/app/shared/video/video.model.ts
@@ -54,7 +54,7 @@ export class Video implements VideoServerModel {
54 displayName: string 54 displayName: string
55 url: string 55 url: string
56 host: string 56 host: string
57 avatar: Avatar 57 avatar?: Avatar
58 } 58 }
59 59
60 channel: { 60 channel: {
@@ -64,7 +64,7 @@ export class Video implements VideoServerModel {
64 displayName: string 64 displayName: string
65 url: string 65 url: string
66 host: string 66 host: string
67 avatar: Avatar 67 avatar?: Avatar
68 } 68 }
69 69
70 userHistory?: { 70 userHistory?: {
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts
index 5d258891f..960846e21 100644
--- a/client/src/app/shared/video/video.service.ts
+++ b/client/src/app/shared/video/video.service.ts
@@ -96,6 +96,7 @@ export class VideoService implements VideosProvider {
96 nsfw: video.nsfw, 96 nsfw: video.nsfw,
97 waitTranscoding: video.waitTranscoding, 97 waitTranscoding: video.waitTranscoding,
98 commentsEnabled: video.commentsEnabled, 98 commentsEnabled: video.commentsEnabled,
99 downloadEnabled: video.downloadEnabled,
99 thumbnailfile: video.thumbnailfile, 100 thumbnailfile: video.thumbnailfile,
100 previewfile: video.previewfile, 101 previewfile: video.previewfile,
101 scheduleUpdate, 102 scheduleUpdate,
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-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html
index 23a71a068..2fb540170 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
@@ -135,11 +135,6 @@
135 ></my-peertube-checkbox> 135 ></my-peertube-checkbox>
136 136
137 <my-peertube-checkbox 137 <my-peertube-checkbox
138 inputName="commentsEnabled" formControlName="commentsEnabled"
139 i18n-labelText labelText="Enable video comments"
140 ></my-peertube-checkbox>
141
142 <my-peertube-checkbox
143 *ngIf="waitTranscodingEnabled" 138 *ngIf="waitTranscodingEnabled"
144 inputName="waitTranscoding" formControlName="waitTranscoding" 139 inputName="waitTranscoding" formControlName="waitTranscoding"
145 i18n-labelText labelText="Wait transcoding before publishing the video" 140 i18n-labelText labelText="Wait transcoding before publishing the video"
@@ -157,7 +152,7 @@
157 152
158 <div class="captions-header"> 153 <div class="captions-header">
159 <a (click)="openAddCaptionModal()" class="create-caption"> 154 <a (click)="openAddCaptionModal()" class="create-caption">
160 <span class="icon icon-add"></span> 155 <my-global-icon iconName="add"></my-global-icon>
161 <ng-container i18n>Add another caption</ng-container> 156 <ng-container i18n>Add another caption</ng-container>
162 </a> 157 </a>
163 </div> 158 </div>
@@ -230,6 +225,16 @@
230 {{ formErrors.support }} 225 {{ formErrors.support }}
231 </div> 226 </div>
232 </div> 227 </div>
228
229 <my-peertube-checkbox
230 inputName="commentsEnabled" formControlName="commentsEnabled"
231 i18n-labelText labelText="Enable video comments"
232 ></my-peertube-checkbox>
233
234 <my-peertube-checkbox
235 inputName="downloadEnabled" formControlName="downloadEnabled"
236 i18n-labelText labelText="Enable download"
237 ></my-peertube-checkbox>
233 </div> 238 </div>
234 </ng-template> 239 </ng-template>
235 </ngb-tab> 240 </ngb-tab>
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 25db8e8ed..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
@@ -23,10 +23,6 @@ my-peertube-checkbox {
23 display: block; 23 display: block;
24 } 24 }
25 25
26 input, select {
27 font-size: 15px
28 }
29
30 .label-tags + span { 26 .label-tags + span {
31 font-size: 15px; 27 font-size: 15px;
32 } 28 }
@@ -42,7 +38,7 @@ my-peertube-checkbox {
42 text-align: right; 38 text-align: right;
43 39
44 .create-caption { 40 .create-caption {
45 @include create-button('../../../../assets/images/global/add.svg'); 41 @include create-button;
46 } 42 }
47 } 43 }
48 44
@@ -100,13 +96,14 @@ my-peertube-checkbox {
100 display: inline-block; 96 display: inline-block;
101 margin-right: 25px; 97 margin-right: 25px;
102 98
103 color: #585858; 99 color: $grey-foreground-color;
104 font-size: 15px; 100 font-size: 15px;
105 } 101 }
106 102
107 .submit-button { 103 .submit-button {
108 @include peertube-button; 104 @include peertube-button;
109 @include orange-button; 105 @include orange-button;
106 @include button-with-icon(20px, 1px);
110 107
111 display: inline-block; 108 display: inline-block;
112 109
@@ -119,16 +116,6 @@ my-peertube-checkbox {
119 color: inherit; 116 color: inherit;
120 font-weight: $font-semibold; 117 font-weight: $font-semibold;
121 } 118 }
122
123 .icon.icon-validate {
124 @include icon(20px);
125
126 cursor: inherit;
127 position: relative;
128 top: -1px;
129 margin-right: 4px;
130 background-image: url('../../../../assets/images/global/validate.svg');
131 }
132 } 119 }
133} 120}
134 121
@@ -176,10 +163,10 @@ p-calendar {
176 } 163 }
177 164
178 tag { 165 tag {
179 background-color: var(--inputColor) !important; 166 background-color: $grey-background-color !important;
167 color: #000 !important;
180 border-radius: 3px !important; 168 border-radius: 3px !important;
181 font-size: 15px !important; 169 font-size: 15px !important;
182 color: var(--mainForegroundColor) !important;
183 height: 30px !important; 170 height: 30px !important;
184 line-height: 30px !important; 171 line-height: 30px !important;
185 margin: 0 5px 0 0 !important; 172 margin: 0 5px 0 0 !important;
@@ -202,7 +189,10 @@ p-calendar {
202 top: -1px; 189 top: -1px;
203 height: auto !important; 190 height: auto !important;
204 vertical-align: middle !important; 191 vertical-align: middle !important;
205 fill: #585858 !important; 192
193 path {
194 fill: $grey-foreground-color !important;
195 }
206 } 196 }
207 197
208 &: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 d02f18ee9..836452948 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
@@ -82,6 +82,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
82 const defaultValues: any = { 82 const defaultValues: any = {
83 nsfw: 'false', 83 nsfw: 'false',
84 commentsEnabled: 'true', 84 commentsEnabled: 'true',
85 downloadEnabled: 'true',
85 waitTranscoding: 'true', 86 waitTranscoding: 'true',
86 tags: [] 87 tags: []
87 } 88 }
@@ -91,6 +92,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
91 channelId: this.videoValidatorsService.VIDEO_CHANNEL, 92 channelId: this.videoValidatorsService.VIDEO_CHANNEL,
92 nsfw: null, 93 nsfw: null,
93 commentsEnabled: null, 94 commentsEnabled: null,
95 downloadEnabled: null,
94 waitTranscoding: null, 96 waitTranscoding: null,
95 category: this.videoValidatorsService.VIDEO_CATEGORY, 97 category: this.videoValidatorsService.VIDEO_CATEGORY,
96 licence: this.videoValidatorsService.VIDEO_LICENCE, 98 licence: this.videoValidatorsService.VIDEO_LICENCE,
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 11a81ad66..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>
@@ -66,7 +66,7 @@
66 (click)="updateSecondStep()" 66 (click)="updateSecondStep()"
67 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" 67 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"
68 > 68 >
69 <span class="icon icon-validate"></span> 69 <my-global-icon iconName="validate"></my-global-icon>
70 <input type="button" i18n-value value="Update" /> 70 <input type="button" i18n-value value="Update" />
71 </div> 71 </div>
72 </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 00626cd7b..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,45 +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.alert.alert-danger {
11 text-align: center;
12
13 & > div {
14 font-weight: $font-semibold;
15 }
16}
17
18.import-video-torrent {
19 display: flex;
20 flex-direction: column;
21 align-items: center;
22
23 .icon.icon-upload {
24 @include icon(90px);
25 margin-bottom: 25px;
26 cursor: default;
27
28 background-image: url('../../../../assets/images/video/upload.svg');
29 }
30
31 .button-file {
32 @include peertube-button-file(auto);
33
34 min-width: 190px;
35 }
36
37 .button-file-extension {
38 display: block;
39 font-size: 12px;
40 margin-top: 5px;
41 }
42
43 .torrent-or-magnet { 5 .torrent-or-magnet {
44 margin: 10px 0; 6 margin: 10px 0;
45 } 7 }
@@ -47,19 +9,6 @@ $width-size: 190px;
47 .form-group-magnet-uri { 9 .form-group-magnet-uri {
48 margin-bottom: 40px; 10 margin-bottom: 40px;
49 } 11 }
50
51 input[type=text] {
52 @include peertube-input-text($width-size);
53 display: block;
54 }
55
56 input[type=button] {
57 @include peertube-button;
58 @include orange-button;
59
60 width: $width-size;
61 margin-top: 30px;
62 }
63} 12}
64 13
65 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 63db06919..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
@@ -18,7 +18,8 @@ import { scrollToTop } from '@app/shared/misc/utils'
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 {
@@ -78,6 +79,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
78 privacy: this.firstStepPrivacyId, 79 privacy: this.firstStepPrivacyId,
79 waitTranscoding: false, 80 waitTranscoding: false,
80 commentsEnabled: true, 81 commentsEnabled: true,
82 downloadEnabled: true,
81 channelId: this.firstStepChannelId 83 channelId: this.firstStepChannelId
82 } 84 }
83 85
@@ -92,6 +94,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
92 94
93 this.video = new VideoEdit(Object.assign(res.video, { 95 this.video = new VideoEdit(Object.assign(res.video, {
94 commentsEnabled: videoUpdate.commentsEnabled, 96 commentsEnabled: videoUpdate.commentsEnabled,
97 downloadEnabled: videoUpdate.downloadEnabled,
95 support: null, 98 support: null,
96 thumbnailUrl: null, 99 thumbnailUrl: null,
97 previewUrl: null 100 previewUrl: null
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 533446672..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>
@@ -59,7 +59,7 @@
59 (click)="updateSecondStep()" 59 (click)="updateSecondStep()"
60 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" 60 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"
61 > 61 >
62 <span class="icon icon-validate"></span> 62 <my-global-icon iconName="validate"></my-global-icon>
63 <input type="button" i18n-value value="Update" /> 63 <input type="button" i18n-value value="Update" />
64 </div> 64 </div>
65 </div> 65 </div>
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 a1810b7a0..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
@@ -18,7 +18,7 @@ import { scrollToTop } from '@app/shared/misc/utils'
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 {
@@ -70,6 +70,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
70 privacy: this.firstStepPrivacyId, 70 privacy: this.firstStepPrivacyId,
71 waitTranscoding: false, 71 waitTranscoding: false,
72 commentsEnabled: true, 72 commentsEnabled: true,
73 downloadEnabled: true,
73 channelId: this.firstStepChannelId 74 channelId: this.firstStepChannelId
74 } 75 }
75 76
@@ -84,6 +85,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
84 85
85 this.video = new VideoEdit(Object.assign(res.video, { 86 this.video = new VideoEdit(Object.assign(res.video, {
86 commentsEnabled: videoUpdate.commentsEnabled, 87 commentsEnabled: videoUpdate.commentsEnabled,
88 downloadEnabled: videoUpdate.downloadEnabled,
87 support: null, 89 support: null,
88 thumbnailUrl: null, 90 thumbnailUrl: null,
89 previewUrl: null 91 previewUrl: null
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-send.scss
index e907edc70..8769dd302 100644
--- 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-send.scss
@@ -3,10 +3,6 @@
3 3
4$width-size: 190px; 4$width-size: 190px;
5 5
6.peertube-select-container {
7 @include peertube-select-container($width-size);
8}
9
10.alert.alert-danger { 6.alert.alert-danger {
11 text-align: center; 7 text-align: center;
12 8
@@ -15,17 +11,20 @@ $width-size: 190px;
15 } 11 }
16} 12}
17 13
18.import-video-url { 14.first-step-block {
19 display: flex; 15 display: flex;
20 flex-direction: column; 16 flex-direction: column;
21 align-items: center; 17 align-items: center;
22 18
23 .icon.icon-upload { 19 .upload-icon {
24 @include icon(90px); 20 width: 90px;
25 margin-bottom: 25px; 21 margin-bottom: 25px;
26 cursor: default;
27 22
28 background-image: url('../../../../assets/images/video/upload.svg'); 23 @include apply-svg-color(#C6C6C6);
24 }
25
26 .peertube-select-container {
27 @include peertube-select-container($width-size);
29 } 28 }
30 29
31 input[type=text] { 30 input[type=text] {
@@ -40,6 +39,16 @@ $width-size: 190px;
40 width: $width-size; 39 width: $width-size;
41 margin-top: 30px; 40 margin-top: 30px;
42 } 41 }
43}
44 42
43 .button-file {
44 @include peertube-button-file(auto);
45 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-upload.component.html b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
index 289a28c66..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,6 +1,6 @@
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>
@@ -42,6 +42,10 @@
42 {{ error }} 42 {{ error }}
43</div> 43</div>
44 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
45<!-- Hidden because we want to load the component --> 49<!-- Hidden because we want to load the component -->
46<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form"> 50<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form">
47 <my-video-edit 51 <my-video-edit
@@ -57,7 +61,7 @@
57 (click)="updateSecondStep()" 61 (click)="updateSecondStep()"
58 [ngClass]="{ disabled: isPublishingButtonDisabled() }" 62 [ngClass]="{ disabled: isPublishingButtonDisabled() }"
59 > 63 >
60 <span class="icon icon-validate"></span> 64 <my-global-icon iconName="validate"></my-global-icon>
61 <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" /> 65 <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" />
62 </div> 66 </div>
63 </div> 67 </div>
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 4b2c86ae9..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,47 +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.alert.alert-danger {
9 text-align: center;
10
11 & > div {
12 font-weight: $font-semibold;
13 }
14}
15
16.upload-video {
17 display: flex;
18 flex-direction: column;
19 align-items: center;
20
21 .form-group-channel {
22 margin-bottom: 20px;
23 margin-top: 35px;
24 }
25
26 .icon.icon-upload {
27 @include icon(90px);
28 margin-bottom: 25px;
29 cursor: default;
30
31 background-image: url('../../../../assets/images/video/upload.svg');
32 }
33
34 .button-file {
35 @include peertube-button-file(auto);
36
37 min-width: 190px;
38 }
39
40 .button-file-extension {
41 display: block;
42 font-size: 12px;
43 margin-top: 5px;
44 }
45} 7}
46 8
47.upload-progress-cancel { 9.upload-progress-cancel {
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 aa40f8781..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
@@ -20,7 +20,8 @@ import { scrollToTop } from '@app/shared/misc/utils'
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 {
@@ -165,6 +166,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
165 const nsfw = false 166 const nsfw = false
166 const waitTranscoding = true 167 const waitTranscoding = true
167 const commentsEnabled = true 168 const commentsEnabled = true
169 const downloadEnabled = true
168 const channelId = this.firstStepChannelId.toString() 170 const channelId = this.firstStepChannelId.toString()
169 171
170 const formData = new FormData() 172 const formData = new FormData()
@@ -173,6 +175,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
173 formData.append('privacy', VideoPrivacy.PRIVATE.toString()) 175 formData.append('privacy', VideoPrivacy.PRIVATE.toString())
174 formData.append('nsfw', '' + nsfw) 176 formData.append('nsfw', '' + nsfw)
175 formData.append('commentsEnabled', '' + commentsEnabled) 177 formData.append('commentsEnabled', '' + commentsEnabled)
178 formData.append('downloadEnabled', '' + downloadEnabled)
176 formData.append('waitTranscoding', '' + waitTranscoding) 179 formData.append('waitTranscoding', '' + waitTranscoding)
177 formData.append('channelId', '' + channelId) 180 formData.append('channelId', '' + channelId)
178 formData.append('videofile', videofile) 181 formData.append('videofile', videofile)
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 57a9d0ca7..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'
@@ -32,7 +32,17 @@ export class VideoAddComponent implements CanComponentDeactivate {
32 this.secondStepType = undefined 32 this.secondStepType = undefined
33 } 33 }
34 34
35 canDeactivate () { 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} {
36 if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate() 46 if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate()
37 if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate() 47 if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate()
38 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 0457778c0..4992bb369 100644
--- a/client/src/app/videos/+video-edit/video-update.component.html
+++ b/client/src/app/videos/+video-edit/video-update.component.html
@@ -13,7 +13,7 @@
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 d22ee540a..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,5 +1,5 @@
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 { Notifier } from '@app/core' 5import { Notifier } from '@app/core'
@@ -83,14 +83,26 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
83 ) 83 )
84 } 84 }
85 85
86 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 } {
87 if (this.updateDone === true) return { canDeactivate: true } 97 if (this.updateDone === true) return { canDeactivate: true }
88 98
99 const text = this.i18n('You have unsaved changes! If you leave, your changes will be lost.')
100
89 for (const caption of this.videoCaptions) { 101 for (const caption of this.videoCaptions) {
90 if (caption.action) return { canDeactivate: false } 102 if (caption.action) return { canDeactivate: false, text }
91 } 103 }
92 104
93 return { canDeactivate: this.formChanged === false } 105 return { canDeactivate: this.formChanged === false, text }
94 } 106 }
95 107
96 checkForm () { 108 checkForm () {
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 00f0460a1..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',
@@ -29,7 +28,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
29 newParentComments: VideoComment[] = [] 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.service.ts b/client/src/app/videos/+video-watch/comment/video-comment.service.ts
index 921447d5b..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,7 +28,7 @@ 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<{ comment: VideoCommentServerModel }>(url, normalizedComment) 33 return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment)
34 .pipe( 34 .pipe(
@@ -39,7 +39,7 @@ export class VideoCommentService {
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<{ comment: VideoCommentServerModel }>(url, normalizedComment) 44 return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment)
45 .pipe( 45 .pipe(
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 357ce39ce..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
@@ -34,9 +34,12 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
34 } 34 }
35 35
36 ngOnInit () { 36 ngOnInit () {
37 const defaultValues = { unfederate: 'true' }
38
37 this.buildForm({ 39 this.buildForm({
38 reason: this.videoBlacklistValidatorsService.VIDEO_BLACKLIST_REASON 40 reason: this.videoBlacklistValidatorsService.VIDEO_BLACKLIST_REASON,
39 }) 41 unfederate: null
42 }, defaultValues)
40 } 43 }
41 44
42 show () { 45 show () {
@@ -50,8 +53,9 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
50 53
51 blacklist () { 54 blacklist () {
52 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
53 57
54 this.videoBlacklistService.blacklistVideo(this.video.id, reason) 58 this.videoBlacklistService.blacklistVideo(this.video.id, reason, unfederate)
55 .subscribe( 59 .subscribe(
56 () => { 60 () => {
57 this.notifier.success(this.i18n('Video blacklisted.')) 61 this.notifier.success(this.i18n('Video blacklisted.'))
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-report.component.html b/client/src/app/videos/+video-watch/modal/video-report.component.html
index 733c01be0..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,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">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">
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-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 2cdbc7aa6..6e18ab6a6 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -52,55 +52,57 @@
52 <div 52 <div
53 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()" 53 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()"
54 class="action-button action-button-like" role="button" [attr.aria-pressed]="userRating === 'like'" 54 class="action-button action-button-like" role="button" [attr.aria-pressed]="userRating === 'like'"
55 i18n-title title="Like this video"
55 > 56 >
56 <span class="icon icon-like" i18n-title title="Like this video" ></span> 57 <my-global-icon iconName="like"></my-global-icon>
57 </div> 58 </div>
58 59
59 <div 60 <div
60 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()" 61 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()"
61 class="action-button action-button-dislike" role="button" [attr.aria-pressed]="userRating === 'dislike'" 62 class="action-button action-button-dislike" role="button" [attr.aria-pressed]="userRating === 'dislike'"
63 i18n-title title="Dislike this video"
62 > 64 >
63 <span class="icon icon-dislike" i18n-title title="Dislike this video"></span> 65 <my-global-icon iconName="dislike"></my-global-icon>
64 </div> 66 </div>
65 67
66 <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support"> 68 <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support">
67 <span class="icon icon-support"></span> 69 <my-global-icon iconName="heart"></my-global-icon>
68 <span class="icon-text" i18n>Support</span> 70 <span class="icon-text" i18n>Support</span>
69 </div> 71 </div>
70 72
71 <div (click)="showShareModal()" class="action-button action-button-share" role="button"> 73 <div (click)="showShareModal()" class="action-button action-button-share" role="button">
72 <span class="icon icon-share"></span> 74 <my-global-icon iconName="share"></my-global-icon>
73 <span class="icon-text" i18n>Share</span> 75 <span class="icon-text" i18n>Share</span>
74 </div> 76 </div>
75 77
76 <div class="action-more" ngbDropdown placement="top" role="button"> 78 <div class="action-more" ngbDropdown placement="top" role="button">
77 <div class="action-button" ngbDropdownToggle role="button"> 79 <div class="action-button" ngbDropdownToggle role="button">
78 <span class="icon icon-more"></span> 80 <my-global-icon class="more-icon" iconName="more"></my-global-icon>
79 </div> 81 </div>
80 82
81 <div ngbDropdownMenu> 83 <div ngbDropdownMenu>
82 <a class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)"> 84 <a *ngIf="isVideoDownloadable()" class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)">
83 <span class="icon icon-download"></span> <ng-container i18n>Download</ng-container> 85 <my-global-icon iconName="download"></my-global-icon> <ng-container i18n>Download</ng-container>
84 </a> 86 </a>
85 87
86 <a *ngIf="isUserLoggedIn()" class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)"> 88 <a *ngIf="isUserLoggedIn()" class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)">
87 <span class="icon icon-alert"></span> <ng-container i18n>Report</ng-container> 89 <my-global-icon iconName="alert"></my-global-icon> <ng-container i18n>Report</ng-container>
88 </a> 90 </a>
89 91
90 <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]"> 92 <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]">
91 <span class="icon icon-edit"></span> <ng-container i18n>Update</ng-container> 93 <my-global-icon iconName="edit"></my-global-icon> <ng-container i18n>Update</ng-container>
92 </a> 94 </a>
93 95
94 <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)"> 96 <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)">
95 <span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container> 97 <my-global-icon iconName="no"></my-global-icon> <ng-container i18n>Blacklist</ng-container>
96 </a> 98 </a>
97 99
98 <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)"> 100 <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)">
99 <span class="icon icon-unblacklist"></span> <ng-container i18n>Unblacklist</ng-container> 101 <my-global-icon iconName="undo"></my-global-icon> <ng-container i18n>Unblacklist</ng-container>
100 </a> 102 </a>
101 103
102 <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)"> 104 <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)">
103 <span class="icon icon-delete"></span> <ng-container i18n>Delete</ng-container> 105 <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete</ng-container>
104 </a> 106 </a>
105 </div> 107 </div>
106 </div> 108 </div>
@@ -159,12 +161,9 @@
159 <span class="video-attribute-value">{{ video.privacy.label }}</span> 161 <span class="video-attribute-value">{{ video.privacy.label }}</span>
160 </div> 162 </div>
161 163
162 <div class="video-attribute"> 164 <div *ngIf="!!video.originallyPublishedAt" class="video-attribute">
163 <span i18n class="video-attribute-label">Originally published on</span> 165 <span i18n class="video-attribute-label">Originally published</span>
164 <span *ngIf="!video.originallyPublishedAt" class="video-attribute-value">Unknown</span> 166 <span class="video-attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span>
165 <span *ngIf="video.originallyPublishedAt" class="video-attribute-value">
166 {{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}
167 </span>
168 </div> 167 </div>
169 168
170 <div class="video-attribute"> 169 <div class="video-attribute">
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 f96ce8b8f..cfe3533b6 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -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: 142px; 289 min-width: 142px;
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 67c5254b3..4dbfa41e5 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -7,29 +7,27 @@ import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-supp
7import { MetaService } from '@ngx-meta/core' 7import { MetaService } from '@ngx-meta/core'
8import { Notifier, ServerService } from '@app/core' 8import { Notifier, ServerService } from '@app/core'
9import { forkJoin, Subscription } from 'rxjs' 9import { forkJoin, Subscription } from 'rxjs'
10// FIXME: something weird with our path definition in tsconfig and typings
11// @ts-ignore
12import videojs from 'video.js'
13import 'videojs-hotkeys'
14import { Hotkey, HotkeysService } from 'angular2-hotkeys' 10import { Hotkey, HotkeysService } from 'angular2-hotkeys'
15import * as WebTorrent from 'webtorrent'
16import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' 11import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared'
17import '../../../assets/player/peertube-videojs-plugin'
18import { AuthService, ConfirmService } from '../../core' 12import { AuthService, ConfirmService } from '../../core'
19import { RestExtractor, VideoBlacklistService } from '../../shared' 13import { RestExtractor, VideoBlacklistService } from '../../shared'
20import { VideoDetails } from '../../shared/video/video-details.model' 14import { VideoDetails } from '../../shared/video/video-details.model'
21import { VideoService } from '../../shared/video/video.service' 15import { VideoService } from '../../shared/video/video.service'
22import { MarkdownService } from '../shared'
23import { VideoDownloadComponent } from './modal/video-download.component' 16import { VideoDownloadComponent } from './modal/video-download.component'
24import { VideoReportComponent } from './modal/video-report.component' 17import { VideoReportComponent } from './modal/video-report.component'
25import { VideoShareComponent } from './modal/video-share.component' 18import { VideoShareComponent } from './modal/video-share.component'
26import { VideoBlacklistComponent } from './modal/video-blacklist.component' 19import { VideoBlacklistComponent } from './modal/video-blacklist.component'
27import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' 20import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component'
28import { addContextMenu, getVideojsOptions, loadLocaleInVideoJS } from '../../../assets/player/peertube-player'
29import { I18n } from '@ngx-translate/i18n-polyfill' 21import { I18n } from '@ngx-translate/i18n-polyfill'
30import { environment } from '../../../environments/environment' 22import { environment } from '../../../environments/environment'
31import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
32import { 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'
33 31
34@Component({ 32@Component({
35 selector: 'my-video-watch', 33 selector: 'my-video-watch',
@@ -46,7 +44,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
46 @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent 44 @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent
47 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent 45 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
48 46
49 player: videojs.Player 47 player: any
50 playerElement: HTMLVideoElement 48 playerElement: HTMLVideoElement
51 userRating: UserVideoRateType = null 49 userRating: UserVideoRateType = null
52 video: VideoDetails = null 50 video: VideoDetails = null
@@ -61,7 +59,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
61 remoteServerDown = false 59 remoteServerDown = false
62 hotkeys: Hotkey[] 60 hotkeys: Hotkey[]
63 61
64 private videojsLocaleLoaded = false
65 private paramsSub: Subscription 62 private paramsSub: Subscription
66 63
67 constructor ( 64 constructor (
@@ -92,7 +89,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
92 89
93 ngOnInit () { 90 ngOnInit () {
94 if ( 91 if (
95 WebTorrent.WEBRTC_SUPPORT === false || 92 !!((window as any).RTCPeerConnection || (window as any).mozRTCPeerConnection || (window as any).webkitRTCPeerConnection) === false ||
96 peertubeLocalStorage.getItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true' 93 peertubeLocalStorage.getItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true'
97 ) { 94 ) {
98 this.hasAlreadyAcceptedPrivacyConcern = true 95 this.hasAlreadyAcceptedPrivacyConcern = true
@@ -118,8 +115,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
118 .subscribe(([ video, captionsResult ]) => { 115 .subscribe(([ video, captionsResult ]) => {
119 const startTime = this.route.snapshot.queryParams.start 116 const startTime = this.route.snapshot.queryParams.start
120 const subtitle = this.route.snapshot.queryParams.subtitle 117 const subtitle = this.route.snapshot.queryParams.subtitle
118 const playerMode = this.route.snapshot.queryParams.mode
121 119
122 this.onVideoFetched(video, captionsResult.data, { startTime, subtitle }) 120 this.onVideoFetched(video, captionsResult.data, { startTime, subtitle, playerMode })
123 .catch(err => this.handleError(err)) 121 .catch(err => this.handleError(err))
124 }) 122 })
125 }) 123 })
@@ -310,6 +308,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
310 return this.video && this.video.state.id === VideoState.TO_TRANSCODE 308 return this.video && this.video.state.id === VideoState.TO_TRANSCODE
311 } 309 }
312 310
311 isVideoDownloadable () {
312 return this.video && this.video.downloadEnabled
313 }
314
313 isVideoToImport () { 315 isVideoToImport () {
314 return this.video && this.video.state.id === VideoState.TO_IMPORT 316 return this.video && this.video.state.id === VideoState.TO_IMPORT
315 } 317 }
@@ -366,7 +368,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
366 ) 368 )
367 } 369 }
368 370
369 private async onVideoFetched (video: VideoDetails, videoCaptions: VideoCaption[], urlOptions: { startTime: number, subtitle: string }) { 371 private async onVideoFetched (
372 video: VideoDetails,
373 videoCaptions: VideoCaption[],
374 urlOptions: { startTime?: number, subtitle?: string, playerMode?: string }
375 ) {
370 this.video = video 376 this.video = video
371 377
372 // Re init attributes 378 // Re init attributes
@@ -402,41 +408,64 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
402 src: environment.apiUrl + c.captionPath 408 src: environment.apiUrl + c.captionPath
403 })) 409 }))
404 410
405 const videojsOptions = getVideojsOptions({ 411 const options: PeertubePlayerManagerOptions = {
406 autoplay: this.isAutoplay(), 412 common: {
407 inactivityTimeout: 2500, 413 autoplay: this.isAutoplay(),
408 videoFiles: this.video.files, 414
409 videoCaptions: playerCaptions, 415 playerElement: this.playerElement,
410 playerElement: this.playerElement, 416 onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element,
411 videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null, 417
412 videoDuration: this.video.duration, 418 videoDuration: this.video.duration,
413 enableHotkeys: true, 419 enableHotkeys: true,
414 peertubeLink: false, 420 inactivityTimeout: 2500,
415 poster: this.video.previewUrl, 421 poster: this.video.previewUrl,
416 startTime, 422 startTime,
417 subtitle: urlOptions.subtitle, 423
418 theaterMode: true, 424 theaterMode: true,
419 language: this.localeId, 425 captions: videoCaptions.length !== 0,
420 426 peertubeLink: false,
421 userWatching: this.user && this.user.videosHistoryEnabled === true ? { 427
422 url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), 428 videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null,
423 authorizationHeader: this.authService.getRequestHeaderValue() 429 embedUrl: this.video.embedUrl,
424 } : undefined 430
425 }) 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,
426 439
427 if (this.videojsLocaleLoaded === false) { 440 serverUrl: environment.apiUrl,
428 await loadLocaleInVideoJS(environment.apiUrl, videojs, isOnDevLocale() ? getDevLocale() : this.localeId) 441
429 this.videojsLocaleLoaded = true 442 videoCaptions: playerCaptions
443 },
444
445 webtorrent: {
446 videoFiles: this.video.files
447 }
430 } 448 }
431 449
432 const self = this 450 const mode: PlayerMode = urlOptions.playerMode === 'p2p-media-loader' ? 'p2p-media-loader' : 'webtorrent'
433 this.zone.runOutsideAngular(async () => { 451
434 videojs(this.playerElement, videojsOptions, function (this: videojs.Player) { 452 if (mode === 'p2p-media-loader') {
435 self.player = this 453 const hlsPlaylist = this.video.getHlsPlaylist()
436 this.on('customError', ({ err }: { err: any }) => self.handleError(err))
437 454
438 addContextMenu(self.player, self.video.embedUrl) 455 const p2pMediaLoader = {
439 }) 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))
440 }) 469 })
441 470
442 this.setVideoDescriptionHTML() 471 this.setVideoDescriptionHTML()
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 54a12c126..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'
@@ -46,8 +44,6 @@ import { RecommendationsModule } from '@app/videos/recommendations/recommendatio
46 ], 44 ],
47 45
48 providers: [ 46 providers: [
49 MarkdownService,
50 LinkifierService,
51 VideoCommentService 47 VideoCommentService
52 ] 48 ]
53}) 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/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts
index accc5bfe5..6fd74e67a 100644
--- a/client/src/app/videos/video-list/video-trending.component.ts
+++ b/client/src/app/videos/video-list/video-trending.component.ts
@@ -8,7 +8,7 @@ import { VideoSortField } from '../../shared/video/sort-field.type'
8import { VideoService } from '../../shared/video/video.service' 8import { VideoService } from '../../shared/video/video.service'
9import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
10import { ScreenService } from '@app/shared/misc/screen.service' 10import { ScreenService } from '@app/shared/misc/screen.service'
11import { Notifier } from '@app/core' 11import { Notifier, ServerService } from '@app/core'
12 12
13@Component({ 13@Component({
14 selector: 'my-videos-trending', 14 selector: 'my-videos-trending',
@@ -27,18 +27,33 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
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 () {