aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-02-06 12:26:58 +0100
committerChocobozzz <me@florianbigard.com>2019-02-06 12:26:58 +0100
commit73471b1a52f242e86364ffb077ea6cadb3b07ae2 (patch)
tree43dbb7748e281f8d80f15326f489cdea10ec857d /client/src/app
parentc22419dd265c0c7185bf4197a1cb286eb3d8ebc0 (diff)
parentf5305c04aae14467d6f957b713c5a902275cbb89 (diff)
downloadPeerTube-73471b1a52f242e86364ffb077ea6cadb3b07ae2.tar.gz
PeerTube-73471b1a52f242e86364ffb077ea6cadb3b07ae2.tar.zst
PeerTube-73471b1a52f242e86364ffb077ea6cadb3b07ae2.zip
Merge branch 'release/v1.2.0'
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.html67
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.scss16
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.ts27
-rw-r--r--client/src/app/+about/about-instance/contact-admin-modal.component.html50
-rw-r--r--client/src/app/+about/about-instance/contact-admin-modal.component.scss11
-rw-r--r--client/src/app/+about/about-instance/contact-admin-modal.component.ts77
-rw-r--r--client/src/app/+about/about.module.ts4
-rw-r--r--client/src/app/+accounts/account-about/account-about.component.ts4
-rw-r--r--client/src/app/+accounts/account-videos/account-videos.component.ts4
-rw-r--r--client/src/app/+accounts/accounts.component.ts12
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html493
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts210
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.ts8
-rw-r--r--client/src/app/+admin/follows/following-add/following-add.component.ts8
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.ts13
-rw-r--r--client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts21
-rw-r--r--client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts6
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts11
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts11
-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.ts27
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html8
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts23
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html10
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts25
-rw-r--r--client/src/app/+admin/users/user-edit/user-create.component.ts10
-rw-r--r--client/src/app/+admin/users/user-edit/user-update.component.ts9
-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/+admin/users/user-list/user-list.component.ts42
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts13
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts11
-rw-r--r--client/src/app/+my-account/my-account-history/my-account-history.component.html27
-rw-r--r--client/src/app/+my-account/my-account-history/my-account-history.component.scss99
-rw-r--r--client/src/app/+my-account/my-account-history/my-account-history.component.ts107
-rw-r--r--client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html13
-rw-r--r--client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss25
-rw-r--r--client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts14
-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-accept-ownership/my-account-accept-ownership.component.ts9
-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-ownership/my-account-ownership.component.ts15
-rw-r--r--client/src/app/+my-account/my-account-routing.module.ts20
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts7
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts8
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/index.ts1
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html19
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss25
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts99
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts6
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.html11
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.ts8
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts8
-rw-r--r--client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts6
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channel-create.component.ts9
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts14
-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.ts21
-rw-r--r--client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts6
-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.scss13
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.ts20
-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/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts14
-rw-r--r--client/src/app/+my-account/my-account.component.html37
-rw-r--r--client/src/app/+my-account/my-account.component.scss15
-rw-r--r--client/src/app/+my-account/my-account.component.ts105
-rw-r--r--client/src/app/+my-account/my-account.module.ts12
-rw-r--r--client/src/app/+my-account/shared/actor-avatar-info.component.ts6
-rw-r--r--client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts11
-rw-r--r--client/src/app/+verify-account/verify-account-email/verify-account-email.component.html2
-rw-r--r--client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts8
-rw-r--r--client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts2
-rw-r--r--client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts6
-rw-r--r--client/src/app/+video-channels/video-channels-routing.module.ts2
-rw-r--r--client/src/app/+video-channels/video-channels.component.ts4
-rw-r--r--client/src/app/app-routing.module.ts3
-rw-r--r--client/src/app/app.component.html19
-rw-r--r--client/src/app/app.component.scss5
-rw-r--r--client/src/app/app.component.ts13
-rw-r--r--client/src/app/app.module.ts4
-rw-r--r--client/src/app/core/auth/auth-user.model.ts10
-rw-r--r--client/src/app/core/auth/auth.service.ts16
-rw-r--r--client/src/app/core/auth/index.ts1
-rw-r--r--client/src/app/core/confirm/index.ts1
-rw-r--r--client/src/app/core/core.module.ts25
-rw-r--r--client/src/app/core/index.ts1
-rw-r--r--client/src/app/core/notification/index.ts2
-rw-r--r--client/src/app/core/notification/notifier.service.ts41
-rw-r--r--client/src/app/core/notification/user-notification-socket.service.ts41
-rw-r--r--client/src/app/core/routing/login-guard.service.ts10
-rw-r--r--client/src/app/core/routing/redirect.service.ts21
-rw-r--r--client/src/app/core/routing/user-right-guard.service.ts2
-rw-r--r--client/src/app/core/server/server.service.ts16
-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.html10
-rw-r--r--client/src/app/login/login.component.ts29
-rw-r--r--client/src/app/menu/avatar-notification.component.html23
-rw-r--r--client/src/app/menu/avatar-notification.component.scss91
-rw-r--r--client/src/app/menu/avatar-notification.component.ts65
-rw-r--r--client/src/app/menu/index.ts2
-rw-r--r--client/src/app/menu/language-chooser.component.html7
-rw-r--r--client/src/app/menu/language-chooser.component.scss7
-rw-r--r--client/src/app/menu/menu.component.html6
-rw-r--r--client/src/app/menu/menu.component.scss11
-rw-r--r--client/src/app/reset-password/reset-password.component.ts13
-rw-r--r--client/src/app/search/search-filters.component.ts6
-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.ts18
-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-reactive.ts48
-rw-r--r--client/src/app/shared/forms/form-validators/form-validator.service.ts33
-rw-r--r--client/src/app/shared/forms/form-validators/index.ts1
-rw-r--r--client/src/app/shared/forms/form-validators/instance-validators.service.ts48
-rw-r--r--client/src/app/shared/forms/form-validators/user-validators.service.ts20
-rw-r--r--client/src/app/shared/forms/form-validators/video-abuse-validators.service.ts8
-rw-r--r--client/src/app/shared/forms/form-validators/video-channel-validators.service.ts20
-rw-r--r--client/src/app/shared/forms/markdown-textarea.component.ts2
-rw-r--r--client/src/app/shared/forms/peertube-checkbox.component.html4
-rw-r--r--client/src/app/shared/forms/peertube-checkbox.component.ts5
-rw-r--r--client/src/app/shared/forms/reactive-file.component.ts17
-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/instance/instance.service.ts36
-rw-r--r--client/src/app/shared/menu/top-menu-dropdown.component.html21
-rw-r--r--client/src/app/shared/menu/top-menu-dropdown.component.scss18
-rw-r--r--client/src/app/shared/menu/top-menu-dropdown.component.ts83
-rw-r--r--client/src/app/shared/misc/help.component.html5
-rw-r--r--client/src/app/shared/misc/help.component.scss43
-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.ts12
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.ts130
-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/rest/component-pagination.model.ts11
-rw-r--r--client/src/app/shared/rest/rest-extractor.service.ts1
-rw-r--r--client/src/app/shared/shared.module.ts30
-rw-r--r--client/src/app/shared/user-subscription/remote-subscribe.component.ts23
-rw-r--r--client/src/app/shared/user-subscription/subscribe-button.component.ts30
-rw-r--r--client/src/app/shared/users/index.ts1
-rw-r--r--client/src/app/shared/users/user-history.service.ts45
-rw-r--r--client/src/app/shared/users/user-notification.model.ts155
-rw-r--r--client/src/app/shared/users/user-notification.service.ts86
-rw-r--r--client/src/app/shared/users/user-notifications.component.html101
-rw-r--r--client/src/app/shared/users/user-notifications.component.scss51
-rw-r--r--client/src/app/shared/users/user-notifications.component.ts87
-rw-r--r--client/src/app/shared/users/user.model.ts37
-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/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.ts7
-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-miniature.component.scss4
-rw-r--r--client/src/app/shared/video/video.model.ts4
-rw-r--r--client/src/app/signup/signup.component.html2
-rw-r--r--client/src/app/signup/signup.component.ts10
-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.html3
-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.ts5
-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.ts12
-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.ts11
-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-send.ts5
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.html13
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss48
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts33
-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.html4
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.ts37
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.ts9
-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/comment/video-comments.component.ts34
-rw-r--r--client/src/app/videos/+video-watch/modal/video-blacklist.component.html9
-rw-r--r--client/src/app/videos/+video-watch/modal/video-blacklist.component.ts19
-rw-r--r--client/src/app/videos/+video-watch/modal/video-download.component.html2
-rw-r--r--client/src/app/videos/+video-watch/modal/video-download.component.ts6
-rw-r--r--client/src/app/videos/+video-watch/modal/video-report.component.html7
-rw-r--r--client/src/app/videos/+video-watch/modal/video-report.component.scss4
-rw-r--r--client/src/app/videos/+video-watch/modal/video-report.component.ts24
-rw-r--r--client/src/app/videos/+video-watch/modal/video-share.component.html2
-rw-r--r--client/src/app/videos/+video-watch/modal/video-share.component.ts6
-rw-r--r--client/src/app/videos/+video-watch/modal/video-support.component.html2
-rw-r--r--client/src/app/videos/+video-watch/modal/video-support.component.ts3
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html24
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss80
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts46
-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-local.component.ts4
-rw-r--r--client/src/app/videos/video-list/video-overview.component.ts10
-rw-r--r--client/src/app/videos/video-list/video-recently-added.component.ts4
-rw-r--r--client/src/app/videos/video-list/video-trending.component.ts23
-rw-r--r--client/src/app/videos/video-list/video-user-subscriptions.component.ts4
220 files changed, 3173 insertions, 1601 deletions
diff --git a/client/src/app/+about/about-instance/about-instance.component.html b/client/src/app/+about/about-instance/about-instance.component.html
index 5970cac01..8c700752e 100644
--- a/client/src/app/+about/about-instance/about-instance.component.html
+++ b/client/src/app/+about/about-instance/about-instance.component.html
@@ -1,39 +1,52 @@
1<div i18n class="about-instance-title"> 1<div class="row">
2 About {{ instanceName }} instance 2 <div class="col-md-12 col-xl-6">
3</div> 3 <div class="about-instance-title">
4 <div i18n>About {{ instanceName }} instance</div>
4 5
5<div class="short-description"> 6 <div *ngIf="isContactFormEnabled" (click)="openContactModal()" i18n role="button" class="contact-admin">Contact administrator</div>
6 <div>{{ shortDescription }}</div> 7 </div>
7</div>
8 8
9<div class="description"> 9 <div class="short-description">
10 <div i18n class="section-title">Description</div> 10 <div>{{ shortDescription }}</div>
11 </div>
11 12
12 <div [innerHTML]="descriptionHTML"></div> 13 <div class="description">
13</div> 14 <div i18n class="section-title">Description</div>
14 15
15<div class="terms" id="terms-section"> 16 <div [innerHTML]="descriptionHTML"></div>
16 <div i18n class="section-title">Terms</div> 17 </div>
17 18
18 <div [innerHTML]="termsHTML"></div> 19 <div class="terms" id="terms-section">
19</div> 20 <div i18n class="section-title">Terms</div>
20 21
21<div class="signup"> 22 <div [innerHTML]="termsHTML"></div>
22 <div i18n class="section-title">Signup</div> 23 </div>
23 24
24 <div *ngIf="isSignupAllowed"> 25 <div class="signup">
25 <ng-container i18n>User registration is allowed and</ng-container> 26 <div i18n class="section-title">Signup</div>
26 27
27 <ng-container i18n *ngIf="userVideoQuota !== -1"> 28 <div *ngIf="isSignupAllowed">
28 this instance provides a baseline quota of {{ userVideoQuota | bytes: 0 }} space for the videos of its users. 29 <ng-container i18n>User registration is allowed and</ng-container>
29 </ng-container>
30 30
31 <ng-container i18n *ngIf="userVideoQuota === -1"> 31 <ng-container i18n *ngIf="userVideoQuota !== -1">
32 this instance provides unlimited space for the videos of its users. 32 this instance provides a baseline quota of {{ userVideoQuota | bytes: 0 }} space for the videos of its users.
33 </ng-container> 33 </ng-container>
34
35 <ng-container i18n *ngIf="userVideoQuota === -1">
36 this instance provides unlimited space for the videos of its users.
37 </ng-container>
38 </div>
39
40 <div i18n *ngIf="isSignupAllowed === false">
41 User registration is currently not allowed.
42 </div>
43 </div>
34 </div> 44 </div>
35 45
36 <div i18n *ngIf="isSignupAllowed === false"> 46 <div class="col-md-12 col-xl-6">
37 User registration is currently not allowed. 47 <label>Features found on this instance</label>
48 <my-instance-features-table></my-instance-features-table>
38 </div> 49 </div>
39</div> \ No newline at end of file 50</div>
51
52<my-contact-admin-modal #contactAdminModal></my-contact-admin-modal>
diff --git a/client/src/app/+about/about-instance/about-instance.component.scss b/client/src/app/+about/about-instance/about-instance.component.scss
index b451e85aa..75cf57322 100644
--- a/client/src/app/+about/about-instance/about-instance.component.scss
+++ b/client/src/app/+about/about-instance/about-instance.component.scss
@@ -2,9 +2,19 @@
2@import '_mixins'; 2@import '_mixins';
3 3
4.about-instance-title { 4.about-instance-title {
5 font-size: 20px; 5 display: flex;
6 font-weight: bold; 6 justify-content: space-between;
7 margin-bottom: 15px; 7
8 & > div {
9 font-size: 20px;
10 font-weight: bold;
11 margin-bottom: 15px;
12 }
13
14 & > .contact-admin {
15 @include peertube-button;
16 @include orange-button;
17 }
8} 18}
9 19
10.section-title { 20.section-title {
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 354f52ce7..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,23 +1,26 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { ServerService } from '@app/core' 2import { Notifier, ServerService } from '@app/core'
3import { MarkdownService } from '@app/videos/shared'
4import { NotificationsService } from 'angular2-notifications'
5import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
5import { InstanceService } from '@app/shared/instance/instance.service'
6import { MarkdownService } from '@app/shared/renderer'
6 7
7@Component({ 8@Component({
8 selector: 'my-about-instance', 9 selector: 'my-about-instance',
9 templateUrl: './about-instance.component.html', 10 templateUrl: './about-instance.component.html',
10 styleUrls: [ './about-instance.component.scss' ] 11 styleUrls: [ './about-instance.component.scss' ]
11}) 12})
12
13export class AboutInstanceComponent implements OnInit { 13export class AboutInstanceComponent implements OnInit {
14 @ViewChild('contactAdminModal') contactAdminModal: ContactAdminModalComponent
15
14 shortDescription = '' 16 shortDescription = ''
15 descriptionHTML = '' 17 descriptionHTML = ''
16 termsHTML = '' 18 termsHTML = ''
17 19
18 constructor ( 20 constructor (
19 private notificationsService: NotificationsService, 21 private notifier: Notifier,
20 private serverService: ServerService, 22 private serverService: ServerService,
23 private instanceService: InstanceService,
21 private markdownService: MarkdownService, 24 private markdownService: MarkdownService,
22 private i18n: I18n 25 private i18n: I18n
23 ) {} 26 ) {}
@@ -34,8 +37,12 @@ export class AboutInstanceComponent implements OnInit {
34 return this.serverService.getConfig().signup.allowed 37 return this.serverService.getConfig().signup.allowed
35 } 38 }
36 39
40 get isContactFormEnabled () {
41 return this.serverService.getConfig().email.enabled && this.serverService.getConfig().contactForm.enabled
42 }
43
37 ngOnInit () { 44 ngOnInit () {
38 this.serverService.getAbout() 45 this.instanceService.getAbout()
39 .subscribe( 46 .subscribe(
40 res => { 47 res => {
41 this.shortDescription = res.instance.shortDescription 48 this.shortDescription = res.instance.shortDescription
@@ -43,8 +50,12 @@ export class AboutInstanceComponent implements OnInit {
43 this.termsHTML = this.markdownService.textMarkdownToHTML(res.instance.terms) 50 this.termsHTML = this.markdownService.textMarkdownToHTML(res.instance.terms)
44 }, 51 },
45 52
46 err => this.notificationsService.error(this.i18n('Error getting about from server'), err) 53 () => this.notifier.error(this.i18n('Cannot get about information from server'))
47 ) 54 )
48 } 55 }
49 56
57 openContactModal () {
58 return this.contactAdminModal.show()
59 }
60
50} 61}
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
new file mode 100644
index 000000000..b2cbd0873
--- /dev/null
+++ b/client/src/app/+about/about-instance/contact-admin-modal.component.html
@@ -0,0 +1,50 @@
1<ng-template #modal>
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Contact {{ instanceName }} administrator</h4>
4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div>
6
7 <div class="modal-body">
8
9 <form novalidate [formGroup]="form" (ngSubmit)="sendForm()">
10 <div class="form-group">
11 <label i18n for="fromName">Your name</label>
12 <input
13 type="text" id="fromName"
14 formControlName="fromName" [ngClass]="{ 'input-error': formErrors.fromName }"
15 >
16 <div *ngIf="formErrors.fromName" class="form-error">{{ formErrors.fromName }}</div>
17 </div>
18
19 <div class="form-group">
20 <label i18n for="fromEmail">Your email</label>
21 <input
22 type="text" id="fromEmail"
23 formControlName="fromEmail" [ngClass]="{ 'input-error': formErrors['fromEmail'] }"
24 >
25 <div *ngIf="formErrors.fromEmail" class="form-error">{{ formErrors.fromEmail }}</div>
26 </div>
27
28 <div class="form-group">
29 <label i18n for="body">Your message</label>
30 <textarea id="body" formControlName="body" [ngClass]="{ 'input-error': formErrors['body'] }">
31 </textarea>
32 <div *ngIf="formErrors.body" class="form-error">{{ formErrors.body }}</div>
33 </div>
34
35 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
36
37 <div class="form-group inputs">
38 <span i18n class="action-button action-button-cancel" (click)="hide()">
39 Cancel
40 </span>
41
42 <input
43 type="submit" i18n-value value="Submit" class="action-button-submit"
44 [disabled]="!form.valid"
45 >
46 </div>
47 </form>
48
49 </div>
50</ng-template>
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.scss b/client/src/app/+about/about-instance/contact-admin-modal.component.scss
new file mode 100644
index 000000000..260d77888
--- /dev/null
+++ b/client/src/app/+about/about-instance/contact-admin-modal.component.scss
@@ -0,0 +1,11 @@
1@import 'variables';
2@import 'mixins';
3
4input[type=text] {
5 @include peertube-input-text(340px);
6 display: block;
7}
8
9textarea {
10 @include peertube-textarea(100%, 200px);
11}
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.ts b/client/src/app/+about/about-instance/contact-admin-modal.component.ts
new file mode 100644
index 000000000..7d79c2215
--- /dev/null
+++ b/client/src/app/+about/about-instance/contact-admin-modal.component.ts
@@ -0,0 +1,77 @@
1import { Component, OnInit, ViewChild } from '@angular/core'
2import { Notifier, ServerService } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
7import { FormReactive, InstanceValidatorsService } from '@app/shared'
8import { InstanceService } from '@app/shared/instance/instance.service'
9
10@Component({
11 selector: 'my-contact-admin-modal',
12 templateUrl: './contact-admin-modal.component.html',
13 styleUrls: [ './contact-admin-modal.component.scss' ]
14})
15export class ContactAdminModalComponent extends FormReactive implements OnInit {
16 @ViewChild('modal') modal: NgbModal
17
18 error: string
19
20 private openedModal: NgbModalRef
21
22 constructor (
23 protected formValidatorService: FormValidatorService,
24 private modalService: NgbModal,
25 private instanceValidatorsService: InstanceValidatorsService,
26 private instanceService: InstanceService,
27 private serverService: ServerService,
28 private notifier: Notifier,
29 private i18n: I18n
30 ) {
31 super()
32 }
33
34 get instanceName () {
35 return this.serverService.getConfig().instance.name
36 }
37
38 ngOnInit () {
39 this.buildForm({
40 fromName: this.instanceValidatorsService.FROM_NAME,
41 fromEmail: this.instanceValidatorsService.FROM_EMAIL,
42 body: this.instanceValidatorsService.BODY
43 })
44 }
45
46 show () {
47 this.openedModal = this.modalService.open(this.modal, { keyboard: false })
48 }
49
50 hide () {
51 this.form.reset()
52 this.error = undefined
53
54 this.openedModal.close()
55 this.openedModal = null
56 }
57
58 sendForm () {
59 const fromName = this.form.value['fromName']
60 const fromEmail = this.form.value[ 'fromEmail' ]
61 const body = this.form.value[ 'body' ]
62
63 this.instanceService.contactAdministrator(fromEmail, fromName, body)
64 .subscribe(
65 () => {
66 this.notifier.success(this.i18n('Your message has been sent.'))
67 this.hide()
68 },
69
70 err => {
71 this.error = err.status === 403
72 ? this.i18n('You already sent this form recently')
73 : err.message
74 }
75 )
76 }
77}
diff --git a/client/src/app/+about/about.module.ts b/client/src/app/+about/about.module.ts
index ff6e8ef41..9c6b29740 100644
--- a/client/src/app/+about/about.module.ts
+++ b/client/src/app/+about/about.module.ts
@@ -5,6 +5,7 @@ import { AboutComponent } from './about.component'
5import { SharedModule } from '../shared' 5import { SharedModule } from '../shared'
6import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' 6import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
7import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' 7import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
8import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
8 9
9@NgModule({ 10@NgModule({
10 imports: [ 11 imports: [
@@ -15,7 +16,8 @@ import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertub
15 declarations: [ 16 declarations: [
16 AboutComponent, 17 AboutComponent,
17 AboutInstanceComponent, 18 AboutInstanceComponent,
18 AboutPeertubeComponent 19 AboutPeertubeComponent,
20 ContactAdminModalComponent
19 ], 21 ],
20 22
21 exports: [ 23 exports: [
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/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts
index e5c1f58b0..13b634a01 100644
--- a/client/src/app/+accounts/account-videos/account-videos.component.ts
+++ b/client/src/app/+accounts/account-videos/account-videos.component.ts
@@ -2,7 +2,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { Location } from '@angular/common' 3import { Location } from '@angular/common'
4import { immutableAssign } from '@app/shared/misc/utils' 4import { immutableAssign } from '@app/shared/misc/utils'
5import { NotificationsService } from 'angular2-notifications'
6import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
7import { ConfirmService } from '../../core/confirm' 6import { ConfirmService } from '../../core/confirm'
8import { AbstractVideoList } from '../../shared/video/abstract-video-list' 7import { AbstractVideoList } from '../../shared/video/abstract-video-list'
@@ -13,6 +12,7 @@ import { tap } from 'rxjs/operators'
13import { I18n } from '@ngx-translate/i18n-polyfill' 12import { I18n } from '@ngx-translate/i18n-polyfill'
14import { Subscription } from 'rxjs' 13import { Subscription } from 'rxjs'
15import { ScreenService } from '@app/shared/misc/screen.service' 14import { ScreenService } from '@app/shared/misc/screen.service'
15import { Notifier } from '@app/core'
16 16
17@Component({ 17@Component({
18 selector: 'my-account-videos', 18 selector: 'my-account-videos',
@@ -35,7 +35,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
35 protected router: Router, 35 protected router: Router,
36 protected route: ActivatedRoute, 36 protected route: ActivatedRoute,
37 protected authService: AuthService, 37 protected authService: AuthService,
38 protected notificationsService: NotificationsService, 38 protected notifier: Notifier,
39 protected confirmService: ConfirmService, 39 protected confirmService: ConfirmService,
40 protected location: Location, 40 protected location: Location,
41 protected screenService: ScreenService, 41 protected screenService: ScreenService,
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts
index e19927d6b..e8339b78b 100644
--- a/client/src/app/+accounts/accounts.component.ts
+++ b/client/src/app/+accounts/accounts.component.ts
@@ -5,10 +5,9 @@ import { Account } from '@app/shared/account/account.model'
5import { RestExtractor, UserService } from '@app/shared' 5import { RestExtractor, UserService } from '@app/shared'
6import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators' 6import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'
7import { Subscription } from 'rxjs' 7import { Subscription } from 'rxjs'
8import { NotificationsService } from 'angular2-notifications' 8import { AuthService, Notifier, RedirectService } from '@app/core'
9import { User, UserRight } from '../../../../shared' 9import { User, UserRight } from '../../../../shared'
10import { I18n } from '@ngx-translate/i18n-polyfill' 10import { I18n } from '@ngx-translate/i18n-polyfill'
11import { AuthService, RedirectService } from '@app/core'
12 11
13@Component({ 12@Component({
14 templateUrl: './accounts.component.html', 13 templateUrl: './accounts.component.html',
@@ -24,11 +23,10 @@ export class AccountsComponent implements OnInit, OnDestroy {
24 private route: ActivatedRoute, 23 private route: ActivatedRoute,
25 private userService: UserService, 24 private userService: UserService,
26 private accountService: AccountService, 25 private accountService: AccountService,
27 private notificationsService: NotificationsService, 26 private notifier: Notifier,
28 private restExtractor: RestExtractor, 27 private restExtractor: RestExtractor,
29 private redirectService: RedirectService, 28 private redirectService: RedirectService,
30 private authService: AuthService, 29 private authService: AuthService
31 private i18n: I18n
32 ) {} 30 ) {}
33 31
34 ngOnInit () { 32 ngOnInit () {
@@ -43,7 +41,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
43 .subscribe( 41 .subscribe(
44 account => this.account = account, 42 account => this.account = account,
45 43
46 err => this.notificationsService.error(this.i18n('Error'), err.message) 44 err => this.notifier.error(err.message)
47 ) 45 )
48 } 46 }
49 47
@@ -69,7 +67,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
69 .subscribe( 67 .subscribe(
70 user => this.user = user, 68 user => this.user = user,
71 69
72 err => this.notificationsService.error(this.i18n('Error'), err.message) 70 err => this.notifier.error(err.message)
73 ) 71 )
74 } 72 }
75 } 73 }
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index fd4d3d9c9..52eb00d93 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -7,169 +7,169 @@
7 7
8 <div i18n class="inner-form-title">Instance</div> 8 <div i18n class="inner-form-title">Instance</div>
9 9
10 <div class="form-group"> 10 <ng-container formGroupName="instance">
11 <label i18n for="instanceName">Name</label> 11 <div class="form-group">
12 <input 12 <label i18n for="instanceName">Name</label>
13 type="text" id="instanceName" 13 <input
14 formControlName="instanceName" [ngClass]="{ 'input-error': formErrors['instanceName'] }" 14 type="text" id="instanceName"
15 > 15 formControlName="name" [ngClass]="{ 'input-error': formErrors.instance.name }"
16 <div *ngIf="formErrors.instanceName" class="form-error"> 16 >
17 {{ formErrors.instanceName }} 17 <div *ngIf="formErrors.instance.name" class="form-error">{{ formErrors.instance.name }}</div>
18 </div> 18 </div>
19 </div>
20 19
21 <div class="form-group"> 20 <div class="form-group">
22 <label i18n for="instanceShortDescription">Short description</label> 21 <label i18n for="instanceShortDescription">Short description</label>
23 <textarea 22 <textarea
24 id="instanceShortDescription" formControlName="instanceShortDescription" 23 id="instanceShortDescription" formControlName="shortDescription"
25 [ngClass]="{ 'input-error': formErrors['instanceShortDescription'] }" 24 [ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
26 ></textarea> 25 ></textarea>
27 <div *ngIf="formErrors.instanceShortDescription" class="form-error"> 26 <div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div>
28 {{ formErrors.instanceShortDescription }}
29 </div> 27 </div>
30 </div>
31 28
32 <div class="form-group"> 29 <div class="form-group">
33 <label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help> 30 <label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help>
34 <my-markdown-textarea 31 <my-markdown-textarea
35 id="instanceDescription" formControlName="instanceDescription" textareaWidth="500px" [previewColumn]="true" 32 id="instanceDescription" formControlName="description" textareaWidth="500px" [previewColumn]="true"
36 [classes]="{ 'input-error': formErrors['instanceDescription'] }" 33 [classes]="{ 'input-error': formErrors['instance.description'] }"
37 ></my-markdown-textarea> 34 ></my-markdown-textarea>
38 <div *ngIf="formErrors.instanceDescription" class="form-error"> 35 <div *ngIf="formErrors.instance.description" class="form-error">{{ formErrors.instance.description }}</div>
39 {{ formErrors.instanceDescription }}
40 </div> 36 </div>
41 </div>
42 37
43 <div class="form-group"> 38 <div class="form-group">
44 <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help> 39 <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
45 <my-markdown-textarea 40 <my-markdown-textarea
46 id="instanceTerms" formControlName="instanceTerms" textareaWidth="500px" [previewColumn]="true" 41 id="instanceTerms" formControlName="terms" textareaWidth="500px" [previewColumn]="true"
47 [ngClass]="{ 'input-error': formErrors['instanceTerms'] }" 42 [ngClass]="{ 'input-error': formErrors['instance.terms'] }"
48 ></my-markdown-textarea> 43 ></my-markdown-textarea>
49 <div *ngIf="formErrors.instanceTerms" class="form-error"> 44 <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
50 {{ formErrors.instanceTerms }}
51 </div> 45 </div>
52 </div>
53 46
54 <div class="form-group"> 47 <div class="form-group">
55 <label i18n for="instanceDefaultClientRoute">Default client route</label> 48 <label i18n for="instanceDefaultClientRoute">Default client route</label>
56 <div class="peertube-select-container"> 49 <div class="peertube-select-container">
57 <select id="instanceDefaultClientRoute" formControlName="instanceDefaultClientRoute"> 50 <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute">
58 <option i18n value="/videos/overview">Videos Overview</option> 51 <option i18n value="/videos/overview">Videos Overview</option>
59 <option i18n value="/videos/trending">Videos Trending</option> 52 <option i18n value="/videos/trending">Videos Trending</option>
60 <option i18n value="/videos/recently-added">Videos Recently Added</option> 53 <option i18n value="/videos/recently-added">Videos Recently Added</option>
61 <option i18n value="/videos/local">Local videos</option> 54 <option i18n value="/videos/local">Local videos</option>
62 </select> 55 </select>
56 </div>
57 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
63 </div> 58 </div>
64 <div *ngIf="formErrors.instanceDefaultClientRoute" class="form-error"> 59
65 {{ formErrors.instanceDefaultClientRoute }} 60 <div class="form-group">
61 <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
62 <my-help
63 helpType="custom" i18n-customHtml
64 customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."
65 ></my-help>
66
67 <div class="peertube-select-container">
68 <select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy">
69 <option i18n value="do_not_list">Do not list</option>
70 <option i18n value="blur">Blur thumbnails</option>
71 <option i18n value="display">Display</option>
72 </select>
73 </div>
74 <div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div>
66 </div> 75 </div>
67 </div> 76 </ng-container>
68 77
69 <div class="form-group"> 78 <div i18n class="inner-form-title">Signup</div>
70 <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
71 <my-help
72 helpType="custom" i18n-customHtml
73 customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."
74 ></my-help>
75 79
76 <div class="peertube-select-container"> 80 <ng-container formGroupName="signup">
77 <select id="instanceDefaultNSFWPolicy" formControlName="instanceDefaultNSFWPolicy"> 81 <div class="form-group">
78 <option i18n value="do_not_list">Do not list</option> 82 <my-peertube-checkbox
79 <option i18n value="blur">Blur thumbnails</option> 83 inputName="signupEnabled" formControlName="enabled"
80 <option i18n value="display">Display</option> 84 i18n-labelText labelText="Signup enabled"
81 </select> 85 ></my-peertube-checkbox>
82 </div> 86 </div>
83 <div *ngIf="formErrors.instanceDefaultNSFWPolicy" class="form-error"> 87
84 {{ formErrors.instanceDefaultNSFWPolicy }} 88 <div class="form-group">
89 <my-peertube-checkbox *ngIf="isSignupEnabled()"
90 inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
91 i18n-labelText labelText="Signup requires email verification"
92 ></my-peertube-checkbox>
85 </div> 93 </div>
86 </div>
87 94
88 <div i18n class="inner-form-title">Signup</div> 95 <div *ngIf="isSignupEnabled()" class="form-group">
96 <label i18n for="signupLimit">Signup limit</label>
97 <input
98 type="text" id="signupLimit"
99 formControlName="limit" [ngClass]="{ 'input-error': formErrors['signup.limit'] }"
100 >
101 <div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div>
102 </div>
103 </ng-container>
89 104
90 <div class="form-group"> 105 <div i18n class="inner-form-title">Users</div>
91 <my-peertube-checkbox
92 inputName="signupEnabled" formControlName="signupEnabled"
93 i18n-labelText labelText="Signup enabled"
94 ></my-peertube-checkbox>
95 </div>
96 106
97 <div class="form-group"> 107 <ng-container formGroupName="user">
98 <my-peertube-checkbox *ngIf="isSignupEnabled()" 108 <div class="form-group">
99 inputName="signupRequiresEmailVerification" formControlName="signupRequiresEmailVerification" 109 <label i18n for="userVideoQuota">User default video quota</label>
100 i18n-labelText labelText="Signup requires email verification" 110 <div class="peertube-select-container">
101 ></my-peertube-checkbox> 111 <select id="userVideoQuota" formControlName="videoQuota">
102 </div> 112 <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
113 {{ videoQuotaOption.label }}
114 </option>
115 </select>
116 </div>
117 <div *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</div>
118 </div>
103 119
104 <div *ngIf="isSignupEnabled()" class="form-group"> 120 <div class="form-group">
105 <label i18n for="signupLimit">Signup limit</label> 121 <label i18n for="userVideoQuotaDaily">User default daily upload limit</label>
106 <input 122 <div class="peertube-select-container">
107 type="text" id="signupLimit" 123 <select id="userVideoQuotaDaily" formControlName="videoQuotaDaily">
108 formControlName="signupLimit" [ngClass]="{ 'input-error': formErrors['signupLimit'] }" 124 <option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
109 > 125 {{ videoQuotaDailyOption.label }}
110 <div *ngIf="formErrors.signupLimit" class="form-error"> 126 </option>
111 {{ formErrors.signupLimit }} 127 </select>
128 </div>
129 <div *ngIf="formErrors.user.videoQuotaDaily" class="form-error">{{ formErrors.user.videoQuotaDaily }}</div>
112 </div> 130 </div>
113 </div> 131 </ng-container>
114 132
115 <div i18n class="inner-form-title">Import</div> 133 <div i18n class="inner-form-title">Import</div>
116 134
117 <div class="form-group"> 135 <ng-container formGroupName="import">
118 <my-peertube-checkbox 136 <ng-container formGroupName="videos">
119 inputName="importVideosHttpEnabled" formControlName="importVideosHttpEnabled"
120 i18n-labelText labelText="Video import with HTTP URL (i.e. YouTube) enabled"
121 ></my-peertube-checkbox>
122 </div>
123 137
124 <div class="form-group"> 138 <div class="form-group" formGroupName="http">
125 <my-peertube-checkbox 139 <my-peertube-checkbox
126 inputName="importVideosTorrentEnabled" formControlName="importVideosTorrentEnabled" 140 inputName="importVideosHttpEnabled" formControlName="enabled"
127 i18n-labelText labelText="Video import with a torrent file or a magnet URI enabled" 141 i18n-labelText labelText="Video import with HTTP URL (i.e. YouTube) enabled"
128 ></my-peertube-checkbox> 142 ></my-peertube-checkbox>
129 </div> 143 </div>
144
145 <div class="form-group" formGroupName="torrent">
146 <my-peertube-checkbox
147 inputName="importVideosTorrentEnabled" formControlName="enabled"
148 i18n-labelText labelText="Video import with a torrent file or a magnet URI enabled"
149 ></my-peertube-checkbox>
150 </div>
151
152 </ng-container>
153 </ng-container>
130 154
131 <div i18n class="inner-form-title">Administrator</div> 155 <div i18n class="inner-form-title">Administrator</div>
132 156
133 <div class="form-group"> 157 <div class="form-group" formGroupName="admin">
134 <label i18n for="adminEmail">Admin email</label> 158 <label i18n for="adminEmail">Admin email</label>
135 <input 159 <input
136 type="text" id="adminEmail" 160 type="text" id="adminEmail"
137 formControlName="adminEmail" [ngClass]="{ 'input-error': formErrors['adminEmail'] }" 161 formControlName="email" [ngClass]="{ 'input-error': formErrors['admin.email'] }"
138 > 162 >
139 <div *ngIf="formErrors.adminEmail" class="form-error"> 163 <div *ngIf="formErrors.admin.email" class="form-error">{{ formErrors.admin.email }}</div>
140 {{ formErrors.adminEmail }}
141 </div>
142 </div> 164 </div>
143 165
144 <div i18n class="inner-form-title">Users</div> 166 <div class="form-group" formGroupName="contactForm">
145 167 <my-peertube-checkbox
146 <div class="form-group"> 168 inputName="enableContactForm" formControlName="enabled"
147 <label i18n for="userVideoQuota">User default video quota</label> 169 i18n-labelText labelText="Enable contact form"
148 <div class="peertube-select-container"> 170 ></my-peertube-checkbox>
149 <select id="userVideoQuota" formControlName="userVideoQuota">
150 <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
151 {{ videoQuotaOption.label }}
152 </option>
153 </select>
154 </div>
155 <div *ngIf="formErrors.userVideoQuota" class="form-error">
156 {{ formErrors.userVideoQuota }}
157 </div>
158 </div> 171 </div>
159 172
160 <div class="form-group">
161 <label i18n for="userVideoQuotaDaily">User default daily upload limit</label>
162 <div class="peertube-select-container">
163 <select id="userVideoQuotaDaily" formControlName="userVideoQuotaDaily">
164 <option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
165 {{ videoQuotaDailyOption.label }}
166 </option>
167 </select>
168 </div>
169 <div *ngIf="formErrors.userVideoQuotaDaily" class="form-error">
170 {{ formErrors.userVideoQuotaDaily }}
171 </div>
172 </div>
173 </ng-template> 173 </ng-template>
174 </ngb-tab> 174 </ngb-tab>
175 175
@@ -177,30 +177,35 @@
177 <ng-template ngbTabContent> 177 <ng-template ngbTabContent>
178 <div i18n class="inner-form-title">Twitter</div> 178 <div i18n class="inner-form-title">Twitter</div>
179 179
180 <div class="form-group"> 180 <ng-container formGroupName="services">
181 <label i18n for="signupLimit">Your Twitter username</label> 181 <ng-container formGroupName="twitter">
182 <my-help 182
183 helpType="custom" i18n-customHtml 183 <div class="form-group">
184 customHtml="Indicates the Twitter account for the website or platform on which the content was published." 184 <label i18n for="signupLimit">Your Twitter username</label>
185 ></my-help> 185 <my-help
186 <input 186 helpType="custom" i18n-customHtml
187 type="text" id="servicesTwitterUsername" 187 customHtml="Indicates the Twitter account for the website or platform on which the content was published."
188 formControlName="servicesTwitterUsername" [ngClass]="{ 'input-error': formErrors['servicesTwitterUsername'] }" 188 ></my-help>
189 > 189 <input
190 <div *ngIf="formErrors.servicesTwitterUsername" class="form-error"> 190 type="text" id="servicesTwitterUsername"
191 {{ formErrors.servicesTwitterUsername }} 191 formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
192 </div> 192 >
193 </div> 193 <div *ngIf="formErrors.services.twitter.username" class="form-error">{{ formErrors.services.twitter.username }}</div>
194 </div>
195
196 <div class="form-group">
197 <my-peertube-checkbox
198 inputName="servicesTwitterWhitelisted" formControlName="whitelisted"
199 i18n-labelText labelText="Instance whitelisted by Twitter"
200 i18n-helpHtml helpHtml="If your instance is whitelisted by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
201 If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
202 Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a> to see if you instance is whitelisted."
203 ></my-peertube-checkbox>
204 </div>
205
206 </ng-container>
207 </ng-container>
194 208
195 <div class="form-group">
196 <my-peertube-checkbox
197 inputName="servicesTwitterWhitelisted" formControlName="servicesTwitterWhitelisted"
198 i18n-labelText labelText="Instance whitelisted by Twitter"
199 i18n-helpHtml helpHtml="If your instance is whitelisted by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
200 If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
201 Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a> to see if you instance is whitelisted."
202 ></my-peertube-checkbox>
203 </div>
204 </ng-template> 209 </ng-template>
205 </ngb-tab> 210 </ngb-tab>
206 211
@@ -209,37 +214,48 @@
209 214
210 <div i18n class="inner-form-title">Transcoding</div> 215 <div i18n class="inner-form-title">Transcoding</div>
211 216
212 <div class="form-group"> 217 <ng-container formGroupName="transcoding">
213 <my-peertube-checkbox 218 <div class="form-group">
214 inputName="transcodingEnabled" formControlName="transcodingEnabled" 219 <my-peertube-checkbox
215 i18n-labelText labelText="Transcoding enabled" 220 inputName="transcodingEnabled" formControlName="enabled"
216 i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!" 221 i18n-labelText labelText="Transcoding enabled"
217 ></my-peertube-checkbox> 222 i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!"
218 </div> 223 ></my-peertube-checkbox>
224 </div>
219 225
220 <ng-template [ngIf]="isTranscodingEnabled()"> 226 <ng-container *ngIf="isTranscodingEnabled()">
221 227
222 <div class="form-group"> 228 <div class="form-group">
223 <label i18n for="transcodingThreads">Transcoding threads</label> 229 <my-peertube-checkbox
224 <div class="peertube-select-container"> 230 inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions"
225 <select id="transcodingThreads" formControlName="transcodingThreads"> 231 i18n-labelText labelText="Allow additional extensions"
226 <option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value"> 232 i18n-helpHtml helpHtml="Allow your users to upload .mkv, .mov, .avi, .flv videos"
227 {{ transcodingThreadOption.label }} 233 ></my-peertube-checkbox>
228 </option>
229 </select>
230 </div> 234 </div>
231 <div *ngIf="formErrors.transcodingThreads" class="form-error"> 235
232 {{ formErrors.transcodingThreads }} 236 <div class="form-group">
237 <label i18n for="transcodingThreads">Transcoding threads</label>
238 <div class="peertube-select-container">
239 <select id="transcodingThreads" formControlName="threads">
240 <option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value">
241 {{ transcodingThreadOption.label }}
242 </option>
243 </select>
244 </div>
245 <div *ngIf="formErrors.transcoding.threads" class="form-error">{{ formErrors.transcoding.threads }}</div>
233 </div> 246 </div>
234 </div>
235 247
236 <div class="form-group" *ngFor="let resolution of resolutions"> 248 <ng-container formGroupName="resolutions">
237 <my-peertube-checkbox 249 <div class="form-group" *ngFor="let resolution of resolutions">
238 [inputName]="getResolutionKey(resolution)" [formControlName]="getResolutionKey(resolution)" 250 <my-peertube-checkbox
239 i18n-labelText labelText="Resolution {{resolution}} enabled" 251 [inputName]="getResolutionKey(resolution)" [formControlName]="resolution"
240 ></my-peertube-checkbox> 252 i18n-labelText labelText="Resolution {{resolution}} enabled"
241 </div> 253 ></my-peertube-checkbox>
242 </ng-template> 254 </div>
255 </ng-container>
256
257 </ng-container>
258 </ng-container>
243 259
244 <div i18n class="inner-form-title"> 260 <div i18n class="inner-form-title">
245 Cache 261 Cache
@@ -250,74 +266,73 @@
250 ></my-help> 266 ></my-help>
251 </div> 267 </div>
252 268
253 <div class="form-group"> 269 <ng-container formGroupName="cache">
254 <label i18n for="cachePreviewsSize">Previews cache size</label> 270 <div class="form-group" formGroupName="previews">
255 <input 271 <label i18n for="cachePreviewsSize">Previews cache size</label>
256 type="text" id="cachePreviewsSize" 272 <input
257 formControlName="cachePreviewsSize" [ngClass]="{ 'input-error': formErrors['cachePreviewsSize'] }" 273 type="text" id="cachePreviewsSize"
258 > 274 formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.previews.size'] }"
259 <div *ngIf="formErrors.cachePreviewsSize" class="form-error"> 275 >
260 {{ formErrors.cachePreviewsSize }} 276 <div *ngIf="formErrors.cache.previews.size" class="form-error">{{ formErrors.cache.previews.size }}</div>
261 </div> 277 </div>
262 </div>
263 278
264 <div class="form-group"> 279 <div class="form-group" formGroupName="captions">
265 <label i18n for="cachePreviewsSize">Video captions cache size</label> 280 <label i18n for="cacheCaptionsSize">Video captions cache size</label>
266 <input 281 <input
267 type="text" id="cacheCaptionsSize" 282 type="text" id="cacheCaptionsSize"
268 formControlName="cacheCaptionsSize" [ngClass]="{ 'input-error': formErrors['cacheCaptionsSize'] }" 283 formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.captions.size'] }"
269 > 284 >
270 <div *ngIf="formErrors.cacheCaptionsSize" class="form-error"> 285 <div *ngIf="formErrors.cache.captions.size" class="form-error">{{ formErrors.cache.captions.size }}</div>
271 {{ formErrors.cacheCaptionsSize }}
272 </div> 286 </div>
273 </div> 287 </ng-container>
274 288
275 <div i18n class="inner-form-title">Customizations</div> 289 <div i18n class="inner-form-title">Customizations</div>
276 290
277 <div class="form-group"> 291 <ng-container formGroupName="instance">
278 <label i18n for="customizationJavascript">JavaScript</label> 292 <ng-container formGroupName="customizations">
279 <my-help 293 <div class="form-group">
280 helpType="custom" i18n-customHtml 294 <label i18n for="customizationJavascript">JavaScript</label>
281 customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>" 295 <my-help
282 ></my-help> 296 helpType="custom" i18n-customHtml
283 <textarea 297 customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>"
284 id="customizationJavascript" formControlName="customizationJavascript" 298 ></my-help>
285 [ngClass]="{ 'input-error': formErrors['customizationJavascript'] }" 299 <textarea
286 ></textarea> 300 id="customizationJavascript" formControlName="javascript"
287 <div *ngIf="formErrors.customizationJavascript" class="form-error"> 301 [ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
288 {{ formErrors.customizationJavascript }} 302 ></textarea>
289 </div> 303 <div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div>
290 </div> 304 </div>
305
306 <div class="form-group">
307 <label for="customizationCSS">CSS</label>
308 <my-help
309 helpType="custom"
310 i18n-customHtml
311 customHtml="
312 Write directly CSS code. Example:<br />
313 <pre>
314 body {{ '{' }}
315 background-color: red;
316 {{ '}' }}
317 </pre>
318
319 Prepend with <em>#custom-css</em> to override styles. Example:
320 <pre>
321 #custom-css .logged-in-email {{ '{' }}
322 color: red;
323 {{ '}' }}
324 </pre>
325 "
326 ></my-help>
327 <textarea
328 id="customizationCSS" formControlName="css"
329 [ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
330 ></textarea>
331 <div *ngIf="formErrors.instance.customizations.css" class="form-error">{{ formErrors.instance.customizations.css }}</div>
332 </div>
333 </ng-container>
334 </ng-container>
291 335
292 <div class="form-group">
293 <label for="customizationCSS">CSS</label>
294 <my-help
295 helpType="custom"
296 i18n-customHtml
297 customHtml="
298 Write directly CSS code. Example:<br />
299 <pre>
300 body {{ '{' }}
301 background-color: red;
302 {{ '}' }}
303 </pre>
304
305 Prepend with <em>#custom-css</em> to override styles. Example:
306 <pre>
307 #custom-css .logged-in-email {{ '{' }}
308 color: red;
309 {{ '}' }}
310 </pre>
311 "
312 ></my-help>
313 <textarea
314 id="customizationCSS" formControlName="customizationCSS"
315 [ngClass]="{ 'input-error': formErrors['customizationCSS'] }"
316 ></textarea>
317 <div *ngIf="formErrors.customizationCSS" class="form-error">
318 {{ formErrors.customizationCSS }}
319 </div>
320 </div>
321 </ng-template> 336 </ng-template>
322 </ngb-tab> 337 </ngb-tab>
323 </ngb-tabset> 338 </ngb-tabset>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index f48b6fc1a..654a076b0 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'
2import { ConfigService } from '@app/+admin/config/shared/config.service' 2import { ConfigService } from '@app/+admin/config/shared/config.service'
3import { ServerService } from '@app/core/server/server.service' 3import { ServerService } from '@app/core/server/server.service'
4import { CustomConfigValidatorsService, FormReactive, UserValidatorsService } from '@app/shared' 4import { CustomConfigValidatorsService, FormReactive, UserValidatorsService } from '@app/shared'
5import { NotificationsService } from 'angular2-notifications' 5import { Notifier } from '@app/core'
6import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model' 6import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 8import { BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
@@ -18,14 +18,11 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
18 resolutions: string[] = [] 18 resolutions: string[] = []
19 transcodingThreadOptions: { label: string, value: number }[] = [] 19 transcodingThreadOptions: { label: string, value: number }[] = []
20 20
21 private oldCustomJavascript: string
22 private oldCustomCSS: string
23
24 constructor ( 21 constructor (
25 protected formValidatorService: FormValidatorService, 22 protected formValidatorService: FormValidatorService,
26 private customConfigValidatorsService: CustomConfigValidatorsService, 23 private customConfigValidatorsService: CustomConfigValidatorsService,
27 private userValidatorsService: UserValidatorsService, 24 private userValidatorsService: UserValidatorsService,
28 private notificationsService: NotificationsService, 25 private notifier: Notifier,
29 private configService: ConfigService, 26 private configService: ConfigService,
30 private serverService: ServerService, 27 private serverService: ServerService,
31 private i18n: I18n 28 private i18n: I18n
@@ -58,40 +55,78 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
58 } 55 }
59 56
60 getResolutionKey (resolution: string) { 57 getResolutionKey (resolution: string) {
61 return 'transcodingResolution' + resolution 58 return 'transcoding.resolutions.' + resolution
62 } 59 }
63 60
64 ngOnInit () { 61 ngOnInit () {
65 const formGroupData: { [key: string]: any } = { 62 const formGroupData: { [key in keyof CustomConfig ]: any } = {
66 instanceName: this.customConfigValidatorsService.INSTANCE_NAME, 63 instance: {
67 instanceShortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION, 64 name: this.customConfigValidatorsService.INSTANCE_NAME,
68 instanceDescription: null, 65 shortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION,
69 instanceTerms: null, 66 description: null,
70 instanceDefaultClientRoute: null, 67 terms: null,
71 instanceDefaultNSFWPolicy: null, 68 defaultClientRoute: null,
72 servicesTwitterUsername: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME, 69 defaultNSFWPolicy: null,
73 servicesTwitterWhitelisted: null, 70 customizations: {
74 cachePreviewsSize: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE, 71 javascript: null,
75 cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE, 72 css: null
76 signupEnabled: null, 73 }
77 signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT, 74 },
78 signupRequiresEmailVerification: null, 75 services: {
79 importVideosHttpEnabled: null, 76 twitter: {
80 importVideosTorrentEnabled: null, 77 username: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME,
81 adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL, 78 whitelisted: null
82 userVideoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, 79 }
83 userVideoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY, 80 },
84 transcodingThreads: this.customConfigValidatorsService.TRANSCODING_THREADS, 81 cache: {
85 transcodingEnabled: null, 82 previews: {
86 customizationJavascript: null, 83 size: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE
87 customizationCSS: null 84 },
85 captions: {
86 size: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE
87 }
88 },
89 signup: {
90 enabled: null,
91 limit: this.customConfigValidatorsService.SIGNUP_LIMIT,
92 requiresEmailVerification: null
93 },
94 import: {
95 videos: {
96 http: {
97 enabled: null
98 },
99 torrent: {
100 enabled: null
101 }
102 }
103 },
104 admin: {
105 email: this.customConfigValidatorsService.ADMIN_EMAIL
106 },
107 contactForm: {
108 enabled: null
109 },
110 user: {
111 videoQuota: this.userValidatorsService.USER_VIDEO_QUOTA,
112 videoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY
113 },
114 transcoding: {
115 enabled: null,
116 threads: this.customConfigValidatorsService.TRANSCODING_THREADS,
117 allowAdditionalExtensions: null,
118 resolutions: {}
119 }
88 } 120 }
89 121
90 const defaultValues: BuildFormDefaultValues = {} 122 const defaultValues = {
123 transcoding: {
124 resolutions: {}
125 }
126 }
91 for (const resolution of this.resolutions) { 127 for (const resolution of this.resolutions) {
92 const key = this.getResolutionKey(resolution) 128 defaultValues.transcoding.resolutions[resolution] = 'false'
93 defaultValues[key] = 'false' 129 formGroupData.transcoding.resolutions[resolution] = null
94 formGroupData[key] = null
95 } 130 }
96 131
97 this.buildForm(formGroupData) 132 this.buildForm(formGroupData)
@@ -101,90 +136,25 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
101 res => { 136 res => {
102 this.customConfig = res 137 this.customConfig = res
103 138
104 this.oldCustomCSS = this.customConfig.instance.customizations.css
105 this.oldCustomJavascript = this.customConfig.instance.customizations.javascript
106
107 this.updateForm() 139 this.updateForm()
108 // Force form validation 140 // Force form validation
109 this.forceCheck() 141 this.forceCheck()
110 }, 142 },
111 143
112 err => this.notificationsService.error(this.i18n('Error'), err.message) 144 err => this.notifier.error(err.message)
113 ) 145 )
114 } 146 }
115 147
116 isTranscodingEnabled () { 148 isTranscodingEnabled () {
117 return this.form.value['transcodingEnabled'] === true 149 return this.form.value['transcoding']['enabled'] === true
118 } 150 }
119 151
120 isSignupEnabled () { 152 isSignupEnabled () {
121 return this.form.value['signupEnabled'] === true 153 return this.form.value['signup']['enabled'] === true
122 } 154 }
123 155
124 async formValidated () { 156 async formValidated () {
125 const data: CustomConfig = { 157 this.configService.updateCustomConfig(this.form.value)
126 instance: {
127 name: this.form.value['instanceName'],
128 shortDescription: this.form.value['instanceShortDescription'],
129 description: this.form.value['instanceDescription'],
130 terms: this.form.value['instanceTerms'],
131 defaultClientRoute: this.form.value['instanceDefaultClientRoute'],
132 defaultNSFWPolicy: this.form.value['instanceDefaultNSFWPolicy'],
133 customizations: {
134 javascript: this.form.value['customizationJavascript'],
135 css: this.form.value['customizationCSS']
136 }
137 },
138 services: {
139 twitter: {
140 username: this.form.value['servicesTwitterUsername'],
141 whitelisted: this.form.value['servicesTwitterWhitelisted']
142 }
143 },
144 cache: {
145 previews: {
146 size: this.form.value['cachePreviewsSize']
147 },
148 captions: {
149 size: this.form.value['cacheCaptionsSize']
150 }
151 },
152 signup: {
153 enabled: this.form.value['signupEnabled'],
154 limit: this.form.value['signupLimit'],
155 requiresEmailVerification: this.form.value['signupRequiresEmailVerification']
156 },
157 admin: {
158 email: this.form.value['adminEmail']
159 },
160 user: {
161 videoQuota: this.form.value['userVideoQuota'],
162 videoQuotaDaily: this.form.value['userVideoQuotaDaily']
163 },
164 transcoding: {
165 enabled: this.form.value['transcodingEnabled'],
166 threads: this.form.value['transcodingThreads'],
167 resolutions: {
168 '240p': this.form.value[this.getResolutionKey('240p')],
169 '360p': this.form.value[this.getResolutionKey('360p')],
170 '480p': this.form.value[this.getResolutionKey('480p')],
171 '720p': this.form.value[this.getResolutionKey('720p')],
172 '1080p': this.form.value[this.getResolutionKey('1080p')]
173 }
174 },
175 import: {
176 videos: {
177 http: {
178 enabled: this.form.value['importVideosHttpEnabled']
179 },
180 torrent: {
181 enabled: this.form.value['importVideosTorrentEnabled']
182 }
183 }
184 }
185 }
186
187 this.configService.updateCustomConfig(data)
188 .subscribe( 158 .subscribe(
189 res => { 159 res => {
190 this.customConfig = res 160 this.customConfig = res
@@ -194,45 +164,15 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
194 164
195 this.updateForm() 165 this.updateForm()
196 166
197 this.notificationsService.success(this.i18n('Success'), this.i18n('Configuration updated.')) 167 this.notifier.success(this.i18n('Configuration updated.'))
198 }, 168 },
199 169
200 err => this.notificationsService.error(this.i18n('Error'), err.message) 170 err => this.notifier.error(err.message)
201 ) 171 )
202 } 172 }
203 173
204 private updateForm () { 174 private updateForm () {
205 const data: { [key: string]: any } = { 175 this.form.patchValue(this.customConfig)
206 instanceName: this.customConfig.instance.name,
207 instanceShortDescription: this.customConfig.instance.shortDescription,
208 instanceDescription: this.customConfig.instance.description,
209 instanceTerms: this.customConfig.instance.terms,
210 instanceDefaultClientRoute: this.customConfig.instance.defaultClientRoute,
211 instanceDefaultNSFWPolicy: this.customConfig.instance.defaultNSFWPolicy,
212 servicesTwitterUsername: this.customConfig.services.twitter.username,
213 servicesTwitterWhitelisted: this.customConfig.services.twitter.whitelisted,
214 cachePreviewsSize: this.customConfig.cache.previews.size,
215 cacheCaptionsSize: this.customConfig.cache.captions.size,
216 signupEnabled: this.customConfig.signup.enabled,
217 signupLimit: this.customConfig.signup.limit,
218 signupRequiresEmailVerification: this.customConfig.signup.requiresEmailVerification,
219 adminEmail: this.customConfig.admin.email,
220 userVideoQuota: this.customConfig.user.videoQuota,
221 userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily,
222 transcodingThreads: this.customConfig.transcoding.threads,
223 transcodingEnabled: this.customConfig.transcoding.enabled,
224 customizationJavascript: this.customConfig.instance.customizations.javascript,
225 customizationCSS: this.customConfig.instance.customizations.css,
226 importVideosHttpEnabled: this.customConfig.import.videos.http.enabled,
227 importVideosTorrentEnabled: this.customConfig.import.videos.torrent.enabled
228 }
229
230 for (const resolution of this.resolutions) {
231 const key = this.getResolutionKey(resolution)
232 data[key] = this.customConfig.transcoding.resolutions[resolution]
233 }
234
235 this.form.patchValue(data)
236 } 176 }
237 177
238} 178}
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.ts b/client/src/app/+admin/follows/followers-list/followers-list.component.ts
index 4a25b7ff3..9a8848bfb 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.ts
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.ts
@@ -1,6 +1,6 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2 2
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier } from '@app/core'
4import { SortMeta } from 'primeng/primeng' 4import { SortMeta } from 'primeng/primeng'
5import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' 5import { ActorFollow } from '../../../../../../shared/models/actors/follow.model'
6import { RestPagination, RestTable } from '../../../shared' 6import { RestPagination, RestTable } from '../../../shared'
@@ -20,7 +20,7 @@ export class FollowersListComponent extends RestTable implements OnInit {
20 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 20 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
21 21
22 constructor ( 22 constructor (
23 private notificationsService: NotificationsService, 23 private notifier: Notifier,
24 private followService: FollowService, 24 private followService: FollowService,
25 private i18n: I18n 25 private i18n: I18n
26 ) { 26 ) {
@@ -32,14 +32,14 @@ export class FollowersListComponent extends RestTable implements OnInit {
32 } 32 }
33 33
34 protected loadData () { 34 protected loadData () {
35 this.followService.getFollowers(this.pagination, this.sort) 35 this.followService.getFollowers(this.pagination, this.sort, this.search)
36 .subscribe( 36 .subscribe(
37 resultList => { 37 resultList => {
38 this.followers = resultList.data 38 this.followers = resultList.data
39 this.totalRecords = resultList.total 39 this.totalRecords = resultList.total
40 }, 40 },
41 41
42 err => this.notificationsService.error(this.i18n('Error'), err.message) 42 err => this.notifier.error(err.message)
43 ) 43 )
44 } 44 }
45} 45}
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.ts b/client/src/app/+admin/follows/following-add/following-add.component.ts
index bd9cc022b..2bb249746 100644
--- a/client/src/app/+admin/follows/following-add/following-add.component.ts
+++ b/client/src/app/+admin/follows/following-add/following-add.component.ts
@@ -1,6 +1,6 @@
1import { Component } from '@angular/core' 1import { Component } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier } from '@app/core'
4import { ConfirmService } from '../../../core' 4import { ConfirmService } from '../../../core'
5import { validateHost } from '../../../shared' 5import { validateHost } from '../../../shared'
6import { FollowService } from '../shared' 6import { FollowService } from '../shared'
@@ -18,7 +18,7 @@ export class FollowingAddComponent {
18 18
19 constructor ( 19 constructor (
20 private router: Router, 20 private router: Router,
21 private notificationsService: NotificationsService, 21 private notifier: Notifier,
22 private confirmService: ConfirmService, 22 private confirmService: ConfirmService,
23 private followService: FollowService, 23 private followService: FollowService,
24 private i18n: I18n 24 private i18n: I18n
@@ -64,12 +64,12 @@ export class FollowingAddComponent {
64 64
65 this.followService.follow(hosts).subscribe( 65 this.followService.follow(hosts).subscribe(
66 () => { 66 () => {
67 this.notificationsService.success(this.i18n('Success'), this.i18n('Follow request(s) sent!')) 67 this.notifier.success(this.i18n('Follow request(s) sent!'))
68 68
69 setTimeout(() => this.router.navigate([ '/admin/follows/following-list' ]), 500) 69 setTimeout(() => this.router.navigate([ '/admin/follows/following-list' ]), 500)
70 }, 70 },
71 71
72 err => this.notificationsService.error(this.i18n('Error'), err.message) 72 err => this.notifier.error(err.message)
73 ) 73 )
74 } 74 }
75 75
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.ts b/client/src/app/+admin/follows/following-list/following-list.component.ts
index 9b7029f75..4517a721e 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.ts
+++ b/client/src/app/+admin/follows/following-list/following-list.component.ts
@@ -1,5 +1,5 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { SortMeta } from 'primeng/primeng' 3import { SortMeta } from 'primeng/primeng'
4import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' 4import { ActorFollow } from '../../../../../../shared/models/actors/follow.model'
5import { ConfirmService } from '../../../core/confirm/confirm.service' 5import { ConfirmService } from '../../../core/confirm/confirm.service'
@@ -20,7 +20,7 @@ export class FollowingListComponent extends RestTable implements OnInit {
20 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 20 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
21 21
22 constructor ( 22 constructor (
23 private notificationsService: NotificationsService, 23 private notifier: Notifier,
24 private confirmService: ConfirmService, 24 private confirmService: ConfirmService,
25 private followService: FollowService, 25 private followService: FollowService,
26 private i18n: I18n 26 private i18n: I18n
@@ -41,14 +41,11 @@ export class FollowingListComponent extends RestTable implements OnInit {
41 41
42 this.followService.unfollow(follow).subscribe( 42 this.followService.unfollow(follow).subscribe(
43 () => { 43 () => {
44 this.notificationsService.success( 44 this.notifier.success(this.i18n('You are not following {{host}} anymore.', { host: follow.following.host }))
45 this.i18n('Success'),
46 this.i18n('You are not following {{host}} anymore.', { host: follow.following.host })
47 )
48 this.loadData() 45 this.loadData()
49 }, 46 },
50 47
51 err => this.notificationsService.error(this.i18n('Error'), err.message) 48 err => this.notifier.error(err.message)
52 ) 49 )
53 } 50 }
54 51
@@ -60,7 +57,7 @@ export class FollowingListComponent extends RestTable implements OnInit {
60 this.totalRecords = resultList.total 57 this.totalRecords = resultList.total
61 }, 58 },
62 59
63 err => this.notificationsService.error(this.i18n('Error'), err.message) 60 err => this.notifier.error(err.message)
64 ) 61 )
65 } 62 }
66} 63}
diff --git a/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts b/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts
index 6d77a0eb4..fa1da26bf 100644
--- a/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts
+++ b/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts
@@ -1,5 +1,5 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' 4import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
5 5
@@ -13,24 +13,21 @@ export class RedundancyCheckboxComponent {
13 @Input() host: string 13 @Input() host: string
14 14
15 constructor ( 15 constructor (
16 private notificationsService: NotificationsService, 16 private notifier: Notifier,
17 private redundancyService: RedundancyService, 17 private redundancyService: RedundancyService,
18 private i18n: I18n 18 private i18n: I18n
19 ) { } 19 ) { }
20 20
21 updateRedundancyState () { 21 updateRedundancyState () {
22 this.redundancyService.updateRedundancy(this.host, this.redundancyAllowed) 22 this.redundancyService.updateRedundancy(this.host, this.redundancyAllowed)
23 .subscribe( 23 .subscribe(
24 () => { 24 () => {
25 const stateLabel = this.redundancyAllowed ? this.i18n('enabled') : this.i18n('disabled') 25 const stateLabel = this.redundancyAllowed ? this.i18n('enabled') : this.i18n('disabled')
26 26
27 this.notificationsService.success( 27 this.notifier.success(this.i18n('Redundancy for {{host}} is {{stateLabel}}', { host: this.host, stateLabel }))
28 this.i18n('Success'), 28 },
29 this.i18n('Redundancy for {{host}} is {{stateLabel}}', { host: this.host, stateLabel })
30 )
31 },
32 29
33 err => this.notificationsService.error(this.i18n('Error'), err.message) 30 err => this.notifier.error(err.message)
34 ) 31 )
35 } 32 }
36} 33}
diff --git a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts
index 44778ab56..b265e1dd6 100644
--- a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts
+++ b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts
@@ -1,6 +1,6 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' 2import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier } from '@app/core'
4import { SortMeta } from 'primeng/primeng' 4import { SortMeta } from 'primeng/primeng'
5import { Job } from '../../../../../../shared/index' 5import { Job } from '../../../../../../shared/index'
6import { JobState } from '../../../../../../shared/models' 6import { JobState } from '../../../../../../shared/models'
@@ -25,7 +25,7 @@ export class JobsListComponent extends RestTable implements OnInit {
25 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 25 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
26 26
27 constructor ( 27 constructor (
28 private notificationsService: NotificationsService, 28 private notifier: Notifier,
29 private jobsService: JobService, 29 private jobsService: JobService,
30 private i18n: I18n 30 private i18n: I18n
31 ) { 31 ) {
@@ -53,7 +53,7 @@ export class JobsListComponent extends RestTable implements OnInit {
53 this.totalRecords = resultList.total 53 this.totalRecords = resultList.total
54 }, 54 },
55 55
56 err => this.notificationsService.error(this.i18n('Error'), err.message) 56 err => this.notifier.error(err.message)
57 ) 57 )
58 } 58 }
59 59
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts
index 3f243aee4..032bf745a 100644
--- a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts
@@ -1,9 +1,9 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared' 4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta' 5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { BlocklistService, AccountBlock } from '@app/shared/blocklist' 6import { AccountBlock, BlocklistService } from '@app/shared/blocklist'
7 7
8@Component({ 8@Component({
9 selector: 'my-instance-account-blocklist', 9 selector: 'my-instance-account-blocklist',
@@ -18,7 +18,7 @@ export class InstanceAccountBlocklistComponent extends RestTable implements OnIn
18 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 18 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
19 19
20 constructor ( 20 constructor (
21 private notificationsService: NotificationsService, 21 private notifier: Notifier,
22 private blocklistService: BlocklistService, 22 private blocklistService: BlocklistService,
23 private i18n: I18n 23 private i18n: I18n
24 ) { 24 ) {
@@ -35,8 +35,7 @@ export class InstanceAccountBlocklistComponent extends RestTable implements OnIn
35 this.blocklistService.unblockAccountByInstance(blockedAccount) 35 this.blocklistService.unblockAccountByInstance(blockedAccount)
36 .subscribe( 36 .subscribe(
37 () => { 37 () => {
38 this.notificationsService.success( 38 this.notifier.success(
39 this.i18n('Success'),
40 this.i18n('Account {{nameWithHost}} unmuted by your instance.', { nameWithHost: blockedAccount.nameWithHost }) 39 this.i18n('Account {{nameWithHost}} unmuted by your instance.', { nameWithHost: blockedAccount.nameWithHost })
41 ) 40 )
42 41
@@ -53,7 +52,7 @@ export class InstanceAccountBlocklistComponent extends RestTable implements OnIn
53 this.totalRecords = resultList.total 52 this.totalRecords = resultList.total
54 }, 53 },
55 54
56 err => this.notificationsService.error(this.i18n('Error'), err.message) 55 err => this.notifier.error(err.message)
57 ) 56 )
58 } 57 }
59} 58}
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts
index 130009dc7..db3dfcd1c 100644
--- a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts
@@ -1,5 +1,5 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared' 4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta' 5import { SortMeta } from 'primeng/components/common/sortmeta'
@@ -19,7 +19,7 @@ export class InstanceServerBlocklistComponent extends RestTable implements OnIni
19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
20 20
21 constructor ( 21 constructor (
22 private notificationsService: NotificationsService, 22 private notifier: Notifier,
23 private blocklistService: BlocklistService, 23 private blocklistService: BlocklistService,
24 private i18n: I18n 24 private i18n: I18n
25 ) { 25 ) {
@@ -36,10 +36,7 @@ export class InstanceServerBlocklistComponent extends RestTable implements OnIni
36 this.blocklistService.unblockServerByInstance(host) 36 this.blocklistService.unblockServerByInstance(host)
37 .subscribe( 37 .subscribe(
38 () => { 38 () => {
39 this.notificationsService.success( 39 this.notifier.success(this.i18n('Instance {{host}} unmuted by your instance.', { host }))
40 this.i18n('Success'),
41 this.i18n('Instance {{host}} unmuted by your instance.', { host })
42 )
43 40
44 this.loadData() 41 this.loadData()
45 } 42 }
@@ -54,7 +51,7 @@ export class InstanceServerBlocklistComponent extends RestTable implements OnIni
54 this.totalRecords = resultList.total 51 this.totalRecords = resultList.total
55 }, 52 },
56 53
57 err => this.notificationsService.error(this.i18n('Error'), err.message) 54 err => this.notifier.error(err.message)
58 ) 55 )
59 } 56 }
60} 57}
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 34ab384d1..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
@@ -1,5 +1,5 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { FormReactive, VideoAbuseService, VideoAbuseValidatorsService } from '../../../shared' 3import { FormReactive, VideoAbuseService, VideoAbuseValidatorsService } from '../../../shared'
4import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 5import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
@@ -22,7 +22,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
22 constructor ( 22 constructor (
23 protected formValidatorService: FormValidatorService, 23 protected formValidatorService: FormValidatorService,
24 private modalService: NgbModal, 24 private modalService: NgbModal,
25 private notificationsService: NotificationsService, 25 private notifier: Notifier,
26 private videoAbuseService: VideoAbuseService, 26 private videoAbuseService: VideoAbuseService,
27 private videoAbuseValidatorsService: VideoAbuseValidatorsService, 27 private videoAbuseValidatorsService: VideoAbuseValidatorsService,
28 private i18n: I18n 28 private i18n: I18n
@@ -45,29 +45,26 @@ 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()
52 } 52 }
53 53
54 async banUser () { 54 async banUser () {
55 const moderationComment: string = this.form.value['moderationComment'] 55 const moderationComment: string = this.form.value[ 'moderationComment' ]
56 56
57 this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment }) 57 this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment })
58 .subscribe( 58 .subscribe(
59 () => { 59 () => {
60 this.notificationsService.success( 60 this.notifier.success(this.i18n('Comment updated.'))
61 this.i18n('Success'),
62 this.i18n('Comment updated.')
63 )
64 61
65 this.commentUpdated.emit(moderationComment) 62 this.commentUpdated.emit(moderationComment)
66 this.hideModerationCommentModal() 63 this.hide()
67 }, 64 },
68 65
69 err => this.notificationsService.error(this.i18n('Error'), err.message) 66 err => this.notifier.error(err.message)
70 ) 67 )
71 } 68 }
72 69
73} 70}
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 0374b70ef..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
@@ -41,7 +41,7 @@
41 </td> 41 </td>
42 42
43 <td class="action-cell"> 43 <td class="action-cell">
44 <my-action-dropdown i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown> 44 <my-action-dropdown placement="bottom-right" i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown>
45 </td> 45 </td>
46 </tr> 46 </tr>
47 </ng-template> 47 </ng-template>
@@ -51,15 +51,15 @@
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>
62 </ng-template> 62 </ng-template>
63</p-table> 63</p-table>
64 64
65<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal> \ No newline at end of file 65<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal>
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 7a219c846..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
@@ -1,6 +1,6 @@
1import { Component, OnInit, ViewChild } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { Account } from '../../../shared/account/account.model' 2import { Account } from '../../../shared/account/account.model'
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier } from '@app/core'
4import { SortMeta } from 'primeng/components/common/sortmeta' 4import { SortMeta } from 'primeng/components/common/sortmeta'
5import { VideoAbuse, VideoAbuseState } from '../../../../../../shared' 5import { VideoAbuse, VideoAbuseState } from '../../../../../../shared'
6import { RestPagination, RestTable, VideoAbuseService } from '../../../shared' 6import { RestPagination, RestTable, VideoAbuseService } from '../../../shared'
@@ -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',
@@ -27,10 +28,11 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
27 videoAbuseActions: DropdownAction<VideoAbuse>[] = [] 28 videoAbuseActions: DropdownAction<VideoAbuse>[] = []
28 29
29 constructor ( 30 constructor (
30 private notificationsService: NotificationsService, 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
@@ -90,14 +92,11 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
90 92
91 this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe( 93 this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe(
92 () => { 94 () => {
93 this.notificationsService.success( 95 this.notifier.success(this.i18n('Abuse deleted.'))
94 this.i18n('Success'),
95 this.i18n('Abuse deleted.')
96 )
97 this.loadData() 96 this.loadData()
98 }, 97 },
99 98
100 err => this.notificationsService.error(this.i18n('Error'), err.message) 99 err => this.notifier.error(err.message)
101 ) 100 )
102 } 101 }
103 102
@@ -106,11 +105,15 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
106 .subscribe( 105 .subscribe(
107 () => this.loadData(), 106 () => this.loadData(),
108 107
109 err => this.notificationsService.error(this.i18n('Error'), err.message) 108 err => this.notifier.error(err.message)
110 ) 109 )
111 110
112 } 111 }
113 112
113 toHtml (text: string) {
114 return this.markdownRenderer.textMarkdownToHTML(text)
115 }
116
114 protected loadData () { 117 protected loadData () {
115 return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort) 118 return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort)
116 .subscribe( 119 .subscribe(
@@ -119,7 +122,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
119 this.totalRecords = resultList.total 122 this.totalRecords = resultList.total
120 }, 123 },
121 124
122 err => this.notificationsService.error(this.i18n('Error'), err.message) 125 err => this.notifier.error(err.message)
123 ) 126 )
124 } 127 }
125} 128}
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 ff4543b97..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,20 +27,21 @@
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">
33 <my-action-dropdown i18n-label label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown> 35 <my-action-dropdown i18n-label placement="bottom-right" label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown>
34 </td> 36 </td>
35 </tr> 37 </tr>
36 </ng-template> 38 </ng-template>
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 e491edaca..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
@@ -1,12 +1,13 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { SortMeta } from 'primeng/components/common/sortmeta' 2import { SortMeta } from 'primeng/components/common/sortmeta'
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier } from '@app/core'
4import { ConfirmService } from '../../../core' 4import { ConfirmService } from '../../../core'
5import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared' 5import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared'
6import { VideoBlacklist } from '../../../../../../shared' 6import { 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',
@@ -23,9 +24,10 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
23 videoBlacklistActions: DropdownAction<VideoBlacklist>[] = [] 24 videoBlacklistActions: DropdownAction<VideoBlacklist>[] = []
24 25
25 constructor ( 26 constructor (
26 private notificationsService: NotificationsService, 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.'
@@ -56,14 +68,11 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
56 68
57 this.videoBlacklistService.removeVideoFromBlacklist(entry.video.id).subscribe( 69 this.videoBlacklistService.removeVideoFromBlacklist(entry.video.id).subscribe(
58 () => { 70 () => {
59 this.notificationsService.success( 71 this.notifier.success(this.i18n('Video {{name}} removed from the blacklist.', { name: entry.video.name }))
60 this.i18n('Success'),
61 this.i18n('Video {{name}} removed from the blacklist.', { name: entry.video.name })
62 )
63 this.loadData() 72 this.loadData()
64 }, 73 },
65 74
66 err => this.notificationsService.error(this.i18n('Error'), err.message) 75 err => this.notifier.error(err.message)
67 ) 76 )
68 } 77 }
69 78
@@ -75,7 +84,7 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
75 this.totalRecords = resultList.total 84 this.totalRecords = resultList.total
76 }, 85 },
77 86
78 err => this.notificationsService.error(this.i18n('Error'), err.message) 87 err => this.notifier.error(err.message)
79 ) 88 )
80 } 89 }
81} 90}
diff --git a/client/src/app/+admin/users/user-edit/user-create.component.ts b/client/src/app/+admin/users/user-edit/user-create.component.ts
index dd8e4efd5..137ecfcbd 100644
--- a/client/src/app/+admin/users/user-edit/user-create.component.ts
+++ b/client/src/app/+admin/users/user-edit/user-create.component.ts
@@ -1,7 +1,6 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier, ServerService } from '@app/core'
4import { ServerService } from '../../../core'
5import { UserCreate, UserRole } from '../../../../../../shared' 4import { UserCreate, UserRole } from '../../../../../../shared'
6import { UserEdit } from './user-edit' 5import { UserEdit } from './user-edit'
7import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -24,7 +23,7 @@ export class UserCreateComponent extends UserEdit implements OnInit {
24 protected configService: ConfigService, 23 protected configService: ConfigService,
25 private userValidatorsService: UserValidatorsService, 24 private userValidatorsService: UserValidatorsService,
26 private router: Router, 25 private router: Router,
27 private notificationsService: NotificationsService, 26 private notifier: Notifier,
28 private userService: UserService, 27 private userService: UserService,
29 private i18n: I18n 28 private i18n: I18n
30 ) { 29 ) {
@@ -60,10 +59,7 @@ export class UserCreateComponent extends UserEdit implements OnInit {
60 59
61 this.userService.addUser(userCreate).subscribe( 60 this.userService.addUser(userCreate).subscribe(
62 () => { 61 () => {
63 this.notificationsService.success( 62 this.notifier.success(this.i18n('User {{username}} created.', { username: userCreate.username }))
64 this.i18n('Success'),
65 this.i18n('User {{username}} created.', { username: userCreate.username })
66 )
67 this.router.navigate([ '/admin/users/list' ]) 63 this.router.navigate([ '/admin/users/list' ])
68 }, 64 },
69 65
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 cd3885a99..61e641823 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
@@ -1,7 +1,7 @@
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 { Subscription } from 'rxjs' 3import { Subscription } from 'rxjs'
4import { NotificationsService } from 'angular2-notifications' 4import { Notifier } from '@app/core'
5import { ServerService } from '../../../core' 5import { ServerService } from '../../../core'
6import { UserEdit } from './user-edit' 6import { UserEdit } from './user-edit'
7import { User, UserUpdate } from '../../../../../../shared' 7import { User, UserUpdate } from '../../../../../../shared'
@@ -30,7 +30,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
30 private userValidatorsService: UserValidatorsService, 30 private userValidatorsService: UserValidatorsService,
31 private route: ActivatedRoute, 31 private route: ActivatedRoute,
32 private router: Router, 32 private router: Router,
33 private notificationsService: NotificationsService, 33 private notifier: Notifier,
34 private userService: UserService, 34 private userService: UserService,
35 private i18n: I18n 35 private i18n: I18n
36 ) { 36 ) {
@@ -73,10 +73,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
73 73
74 this.userService.updateUser(this.userId, userUpdate).subscribe( 74 this.userService.updateUser(this.userId, userUpdate).subscribe(
75 () => { 75 () => {
76 this.notificationsService.success( 76 this.notifier.success(this.i18n('User {{username}} updated.', { username: this.username }))
77 this.i18n('Success'),
78 this.i18n('User {{username}} updated.', { username: this.username })
79 )
80 this.router.navigate([ '/admin/users/list' ]) 77 this.router.navigate([ '/admin/users/list' ])
81 }, 78 },
82 79
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/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts
index fb085c133..66ab796f9 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.ts
+++ b/client/src/app/+admin/users/user-list/user-list.component.ts
@@ -1,5 +1,5 @@
1import { Component, OnInit, ViewChild } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { SortMeta } from 'primeng/components/common/sortmeta' 3import { SortMeta } from 'primeng/components/common/sortmeta'
4import { ConfirmService, ServerService } from '../../../core' 4import { ConfirmService, ServerService } from '../../../core'
5import { RestPagination, RestTable, UserService } from '../../../shared' 5import { RestPagination, RestTable, UserService } from '../../../shared'
@@ -26,7 +26,7 @@ export class UserListComponent extends RestTable implements OnInit {
26 bulkUserActions: DropdownAction<User[]>[] = [] 26 bulkUserActions: DropdownAction<User[]>[] = []
27 27
28 constructor ( 28 constructor (
29 private notificationsService: NotificationsService, 29 private notifier: Notifier,
30 private confirmService: ConfirmService, 30 private confirmService: ConfirmService,
31 private serverService: ServerService, 31 private serverService: ServerService,
32 private userService: UserService, 32 private userService: UserService,
@@ -68,7 +68,7 @@ export class UserListComponent extends RestTable implements OnInit {
68 openBanUserModal (users: User[]) { 68 openBanUserModal (users: User[]) {
69 for (const user of users) { 69 for (const user of users) {
70 if (user.username === 'root') { 70 if (user.username === 'root') {
71 this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot ban root.')) 71 this.notifier.error(this.i18n('You cannot ban root.'))
72 return 72 return
73 } 73 }
74 } 74 }
@@ -91,18 +91,18 @@ export class UserListComponent extends RestTable implements OnInit {
91 () => { 91 () => {
92 const message = this.i18n('{{num}} users unbanned.', { num: users.length }) 92 const message = this.i18n('{{num}} users unbanned.', { num: users.length })
93 93
94 this.notificationsService.success(this.i18n('Success'), message) 94 this.notifier.success(message)
95 this.loadData() 95 this.loadData()
96 }, 96 },
97 97
98 err => this.notificationsService.error(this.i18n('Error'), err.message) 98 err => this.notifier.error(err.message)
99 ) 99 )
100 } 100 }
101 101
102 async removeUsers (users: User[]) { 102 async removeUsers (users: User[]) {
103 for (const user of users) { 103 for (const user of users) {
104 if (user.username === 'root') { 104 if (user.username === 'root') {
105 this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot delete root.')) 105 this.notifier.error(this.i18n('You cannot delete root.'))
106 return 106 return
107 } 107 }
108 } 108 }
@@ -113,28 +113,22 @@ export class UserListComponent extends RestTable implements OnInit {
113 113
114 this.userService.removeUser(users).subscribe( 114 this.userService.removeUser(users).subscribe(
115 () => { 115 () => {
116 this.notificationsService.success( 116 this.notifier.success(this.i18n('{{num}} users deleted.', { num: users.length }))
117 this.i18n('Success'),
118 this.i18n('{{num}} users deleted.', { num: users.length })
119 )
120 this.loadData() 117 this.loadData()
121 }, 118 },
122 119
123 err => this.notificationsService.error(this.i18n('Error'), err.message) 120 err => this.notifier.error(err.message)
124 ) 121 )
125 } 122 }
126 123
127 async setEmailsAsVerified (users: User[]) { 124 async setEmailsAsVerified (users: User[]) {
128 this.userService.updateUsers(users, { emailVerified: true }).subscribe( 125 this.userService.updateUsers(users, { emailVerified: true }).subscribe(
129 () => { 126 () => {
130 this.notificationsService.success( 127 this.notifier.success(this.i18n('{{num}} users email set as verified.', { num: users.length }))
131 this.i18n('Success'),
132 this.i18n('{{num}} users email set as verified.', { num: users.length })
133 )
134 this.loadData() 128 this.loadData()
135 }, 129 },
136 130
137 err => this.notificationsService.error(this.i18n('Error'), err.message) 131 err => this.notifier.error(err.message)
138 ) 132 )
139 } 133 }
140 134
@@ -146,13 +140,13 @@ export class UserListComponent extends RestTable implements OnInit {
146 this.selectedUsers = [] 140 this.selectedUsers = []
147 141
148 this.userService.getUsers(this.pagination, this.sort, this.search) 142 this.userService.getUsers(this.pagination, this.sort, this.search)
149 .subscribe( 143 .subscribe(
150 resultList => { 144 resultList => {
151 this.users = resultList.data 145 this.users = resultList.data
152 this.totalRecords = resultList.total 146 this.totalRecords = resultList.total
153 }, 147 },
154 148
155 err => this.notificationsService.error(this.i18n('Error'), err.message) 149 err => this.notifier.error(err.message)
156 ) 150 )
157 } 151 }
158} 152}
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts
index fbad28410..e3025dec4 100644
--- a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts
@@ -1,9 +1,9 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared' 4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta' 5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { BlocklistService, AccountBlock } from '@app/shared/blocklist' 6import { AccountBlock, BlocklistService } from '@app/shared/blocklist'
7 7
8@Component({ 8@Component({
9 selector: 'my-account-blocklist', 9 selector: 'my-account-blocklist',
@@ -18,7 +18,7 @@ export class MyAccountBlocklistComponent extends RestTable implements OnInit {
18 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 18 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
19 19
20 constructor ( 20 constructor (
21 private notificationsService: NotificationsService, 21 private notifier: Notifier,
22 private blocklistService: BlocklistService, 22 private blocklistService: BlocklistService,
23 private i18n: I18n 23 private i18n: I18n
24 ) { 24 ) {
@@ -35,10 +35,7 @@ export class MyAccountBlocklistComponent extends RestTable implements OnInit {
35 this.blocklistService.unblockAccountByUser(blockedAccount) 35 this.blocklistService.unblockAccountByUser(blockedAccount)
36 .subscribe( 36 .subscribe(
37 () => { 37 () => {
38 this.notificationsService.success( 38 this.notifier.success(this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: blockedAccount.nameWithHost }))
39 this.i18n('Success'),
40 this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: blockedAccount.nameWithHost })
41 )
42 39
43 this.loadData() 40 this.loadData()
44 } 41 }
@@ -53,7 +50,7 @@ export class MyAccountBlocklistComponent extends RestTable implements OnInit {
53 this.totalRecords = resultList.total 50 this.totalRecords = resultList.total
54 }, 51 },
55 52
56 err => this.notificationsService.error(this.i18n('Error'), err.message) 53 err => this.notifier.error(err.message)
57 ) 54 )
58 } 55 }
59} 56}
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts
index b411d6926..4c5cc28b8 100644
--- a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts
@@ -1,5 +1,5 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared' 4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta' 5import { SortMeta } from 'primeng/components/common/sortmeta'
@@ -19,7 +19,7 @@ export class MyAccountServerBlocklistComponent extends RestTable implements OnIn
19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
20 20
21 constructor ( 21 constructor (
22 private notificationsService: NotificationsService, 22 private notifier: Notifier,
23 private blocklistService: BlocklistService, 23 private blocklistService: BlocklistService,
24 private i18n: I18n 24 private i18n: I18n
25 ) { 25 ) {
@@ -36,10 +36,7 @@ export class MyAccountServerBlocklistComponent extends RestTable implements OnIn
36 this.blocklistService.unblockServerByUser(host) 36 this.blocklistService.unblockServerByUser(host)
37 .subscribe( 37 .subscribe(
38 () => { 38 () => {
39 this.notificationsService.success( 39 this.notifier.success(this.i18n('Instance {{host}} unmuted.', { host }))
40 this.i18n('Success'),
41 this.i18n('Instance {{host}} unmuted.', { host })
42 )
43 40
44 this.loadData() 41 this.loadData()
45 } 42 }
@@ -54,7 +51,7 @@ export class MyAccountServerBlocklistComponent extends RestTable implements OnIn
54 this.totalRecords = resultList.total 51 this.totalRecords = resultList.total
55 }, 52 },
56 53
57 err => this.notificationsService.error(this.i18n('Error'), err.message) 54 err => this.notifier.error(err.message)
58 ) 55 )
59 } 56 }
60} 57}
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.html b/client/src/app/+my-account/my-account-history/my-account-history.component.html
new file mode 100644
index 000000000..d42af37d4
--- /dev/null
+++ b/client/src/app/+my-account/my-account-history/my-account-history.component.html
@@ -0,0 +1,27 @@
1<div class="top-buttons">
2 <div class="history-switch">
3 <p-inputSwitch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></p-inputSwitch>
4 <label i18n>History enabled</label>
5 </div>
6
7 <div class="delete-history">
8 <button (click)="deleteHistory()" i18n>Delete history</button>
9 </div>
10</div>
11
12
13<div class="no-history" i18n *ngIf="pagination.totalItems === 0">You don't have videos history yet.</div>
14
15<div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" class="videos" #videosElement>
16 <div *ngFor="let videos of videoPages;" class="videos-page">
17 <div class="video" *ngFor="let video of videos">
18 <my-video-thumbnail [video]="video"></my-video-thumbnail>
19
20 <div class="video-info">
21 <a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
22 <span i18n class="video-info-date-views">{{ video.views | myNumberFormatter }} views</span>
23 <a tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a>
24 </div>
25 </div>
26 </div>
27</div>
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
new file mode 100644
index 000000000..e7c6863f1
--- /dev/null
+++ b/client/src/app/+my-account/my-account-history/my-account-history.component.scss
@@ -0,0 +1,99 @@
1@import '_variables';
2@import '_mixins';
3
4.no-history {
5 display: flex;
6 justify-content: center;
7 margin-top: 50px;
8 font-weight: $font-semibold;
9 font-size: 16px;
10}
11
12.top-buttons {
13 margin-bottom: 20px;
14 display: flex;
15
16 .history-switch {
17 display: flex;
18 flex-grow: 1;
19
20 label {
21 margin: 0 0 0 5px;
22 }
23 }
24
25 .delete-history {
26 font-size: 15px;
27
28 button {
29 @include peertube-button;
30 @include grey-button;
31 }
32 }
33}
34
35.video {
36 @include row-blocks;
37
38 my-video-thumbnail {
39 margin-right: 10px;
40 }
41
42 .video-info {
43 flex-grow: 1;
44
45 .video-info-name {
46 @include disable-default-a-behaviour;
47
48 color: var(--mainForegroundColor);
49 display: block;
50 width: fit-content;
51 font-size: 18px;
52 font-weight: $font-semibold;
53 }
54
55 .video-info-date-views {
56 font-size: 14px;
57 }
58
59 .video-info-account {
60 @include disable-default-a-behaviour;
61
62 display: block;
63 width: fit-content;
64 overflow: hidden;
65 text-overflow: ellipsis;
66 white-space: nowrap;
67 font-size: 14px;
68 color: $grey-foreground-color;
69
70 &:hover {
71 color: $grey-foreground-hover-color;
72 }
73 }
74 }
75}
76
77@media screen and (max-width: $small-view) {
78 .video {
79 flex-direction: column;
80 height: auto;
81 text-align: center;
82
83 .video-info-name {
84 margin: auto;
85 }
86
87 input[type=checkbox] {
88 display: none;
89 }
90
91 my-video-thumbnail {
92 margin-right: 0;
93 }
94
95 .video-buttons {
96 margin-top: 10px;
97 }
98 }
99}
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.ts b/client/src/app/+my-account/my-account-history/my-account-history.component.ts
new file mode 100644
index 000000000..394091bad
--- /dev/null
+++ b/client/src/app/+my-account/my-account-history/my-account-history.component.ts
@@ -0,0 +1,107 @@
1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { Location } from '@angular/common'
4import { immutableAssign } from '@app/shared/misc/utils'
5import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
6import { AuthService } from '../../core/auth'
7import { ConfirmService } from '../../core/confirm'
8import { AbstractVideoList } from '../../shared/video/abstract-video-list'
9import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill'
11import { ScreenService } from '@app/shared/misc/screen.service'
12import { UserHistoryService } from '@app/shared/users/user-history.service'
13import { UserService } from '@app/shared'
14import { Notifier } from '@app/core'
15
16@Component({
17 selector: 'my-account-history',
18 templateUrl: './my-account-history.component.html',
19 styleUrls: [ './my-account-history.component.scss' ]
20})
21export class MyAccountHistoryComponent extends AbstractVideoList implements OnInit, OnDestroy {
22 titlePage: string
23 currentRoute = '/my-account/history/videos'
24 pagination: ComponentPagination = {
25 currentPage: 1,
26 itemsPerPage: 5,
27 totalItems: null
28 }
29 videosHistoryEnabled: boolean
30
31 protected baseVideoWidth = -1
32 protected baseVideoHeight = 155
33
34 constructor (
35 protected router: Router,
36 protected route: ActivatedRoute,
37 protected authService: AuthService,
38 protected userService: UserService,
39 protected notifier: Notifier,
40 protected location: Location,
41 protected screenService: ScreenService,
42 protected i18n: I18n,
43 private confirmService: ConfirmService,
44 private videoService: VideoService,
45 private userHistoryService: UserHistoryService
46 ) {
47 super()
48
49 this.titlePage = this.i18n('My videos history')
50 }
51
52 ngOnInit () {
53 super.ngOnInit()
54
55 this.videosHistoryEnabled = this.authService.getUser().videosHistoryEnabled
56 }
57
58 ngOnDestroy () {
59 super.ngOnDestroy()
60 }
61
62 getVideosObservable (page: number) {
63 const newPagination = immutableAssign(this.pagination, { currentPage: page })
64
65 return this.userHistoryService.getUserVideosHistory(newPagination)
66 }
67
68 generateSyndicationList () {
69 throw new Error('Method not implemented.')
70 }
71
72 onVideosHistoryChange () {
73 this.userService.updateMyProfile({ videosHistoryEnabled: this.videosHistoryEnabled })
74 .subscribe(
75 () => {
76 const message = this.videosHistoryEnabled === true ?
77 this.i18n('Videos history is enabled') :
78 this.i18n('Videos history is disabled')
79
80 this.notifier.success(message)
81
82 this.authService.refreshUserInformation()
83 },
84
85 err => this.notifier.error(err.message)
86 )
87 }
88
89 async deleteHistory () {
90 const title = this.i18n('Delete videos history')
91 const message = this.i18n('Are you sure you want to delete all your videos history?')
92
93 const res = await this.confirmService.confirm(message, title)
94 if (res !== true) return
95
96 this.userHistoryService.deleteUserVideosHistory()
97 .subscribe(
98 () => {
99 this.notifier.success(this.i18n('Videos history deleted'))
100
101 this.reloadVideos()
102 },
103
104 err => this.notifier.error(err.message)
105 )
106 }
107}
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
new file mode 100644
index 000000000..d518b22ec
--- /dev/null
+++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html
@@ -0,0 +1,13 @@
1<div class="header">
2 <a routerLink="/my-account/settings" fragment="notifications" i18n>
3 <my-global-icon iconName="cog"></my-global-icon>
4 Notification preferences
5 </a>
6
7 <button (click)="markAllAsRead()" i18n>
8 <my-global-icon iconName="circle-tick"></my-global-icon>
9 Mark all as read
10 </button>
11</div>
12
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
new file mode 100644
index 000000000..43d1f82ab
--- /dev/null
+++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss
@@ -0,0 +1,25 @@
1@import '_variables';
2@import '_mixins';
3
4.header {
5 display: flex;
6 justify-content: space-between;
7 font-size: 15px;
8 margin-bottom: 20px;
9
10 a {
11 @include peertube-button-link;
12 @include grey-button;
13 @include button-with-icon(18px, 3px, -1px);
14 }
15
16 button {
17 @include peertube-button;
18 @include grey-button;
19 @include button-with-icon(20px, 3px, -1px);
20 }
21}
22
23my-user-notifications {
24 font-size: 15px;
25}
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts
new file mode 100644
index 000000000..3e197088d
--- /dev/null
+++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts
@@ -0,0 +1,14 @@
1import { Component, ViewChild } from '@angular/core'
2import { UserNotificationsComponent } from '@app/shared'
3
4@Component({
5 templateUrl: './my-account-notifications.component.html',
6 styleUrls: [ './my-account-notifications.component.scss' ]
7})
8export class MyAccountNotificationsComponent {
9 @ViewChild('userNotification') userNotification: UserNotificationsComponent
10
11 markAllAsRead () {
12 this.userNotification.markAllAsRead()
13 }
14}
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-accept-ownership/my-account-accept-ownership.component.ts b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts
index a68b452ec..79d29b139 100644
--- a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts
+++ b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts
@@ -1,5 +1,5 @@
1import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { AuthService, Notifier } from '@app/core'
3import { FormReactive } from '@app/shared' 3import { FormReactive } from '@app/shared'
4import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 4import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
5import { VideoOwnershipService } from '@app/shared/video-ownership' 5import { VideoOwnershipService } from '@app/shared/video-ownership'
@@ -8,7 +8,6 @@ import { VideoAcceptOwnershipValidatorsService } from '@app/shared/forms/form-va
8import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 8import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
9import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' 9import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
10import { I18n } from '@ngx-translate/i18n-polyfill' 10import { I18n } from '@ngx-translate/i18n-polyfill'
11import { AuthService } from '@app/core'
12import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 11import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
13 12
14@Component({ 13@Component({
@@ -31,7 +30,7 @@ export class MyAccountAcceptOwnershipComponent extends FormReactive implements O
31 protected formValidatorService: FormValidatorService, 30 protected formValidatorService: FormValidatorService,
32 private videoChangeOwnershipValidatorsService: VideoAcceptOwnershipValidatorsService, 31 private videoChangeOwnershipValidatorsService: VideoAcceptOwnershipValidatorsService,
33 private videoOwnershipService: VideoOwnershipService, 32 private videoOwnershipService: VideoOwnershipService,
34 private notificationsService: NotificationsService, 33 private notifier: Notifier,
35 private authService: AuthService, 34 private authService: AuthService,
36 private videoChannelService: VideoChannelService, 35 private videoChannelService: VideoChannelService,
37 private modalService: NgbModal, 36 private modalService: NgbModal,
@@ -68,12 +67,12 @@ export class MyAccountAcceptOwnershipComponent extends FormReactive implements O
68 .acceptOwnership(videoChangeOwnership.id, { channelId: channel }) 67 .acceptOwnership(videoChangeOwnership.id, { channelId: channel })
69 .subscribe( 68 .subscribe(
70 () => { 69 () => {
71 this.notificationsService.success(this.i18n('Success'), this.i18n('Ownership accepted')) 70 this.notifier.success(this.i18n('Ownership accepted'))
72 if (this.accepted) this.accepted.emit() 71 if (this.accepted) this.accepted.emit()
73 this.videoChangeOwnership = undefined 72 this.videoChangeOwnership = undefined
74 }, 73 },
75 74
76 err => this.notificationsService.error(this.i18n('Error'), err.message) 75 err => this.notifier.error(err.message)
77 ) 76 )
78 } 77 }
79} 78}
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-ownership/my-account-ownership.component.ts b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts
index 0b51ac13c..77857c4fd 100644
--- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts
+++ b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts
@@ -1,13 +1,11 @@
1import { Component, OnInit, ViewChild } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared' 3import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta' 4import { SortMeta } from 'primeng/components/common/sortmeta'
6import { VideoChangeOwnership } from '../../../../../shared' 5import { VideoChangeOwnership } from '../../../../../shared'
7import { VideoOwnershipService } from '@app/shared/video-ownership' 6import { VideoOwnershipService } from '@app/shared/video-ownership'
8import { Account } from '@app/shared/account/account.model' 7import { Account } from '@app/shared/account/account.model'
9import { MyAccountAcceptOwnershipComponent } 8import { MyAccountAcceptOwnershipComponent } from './my-account-accept-ownership/my-account-accept-ownership.component'
10from '@app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component'
11 9
12@Component({ 10@Component({
13 selector: 'my-account-ownership', 11 selector: 'my-account-ownership',
@@ -23,9 +21,8 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit {
23 @ViewChild('myAccountAcceptOwnershipComponent') myAccountAcceptOwnershipComponent: MyAccountAcceptOwnershipComponent 21 @ViewChild('myAccountAcceptOwnershipComponent') myAccountAcceptOwnershipComponent: MyAccountAcceptOwnershipComponent
24 22
25 constructor ( 23 constructor (
26 private notificationsService: NotificationsService, 24 private notifier: Notifier,
27 private videoOwnershipService: VideoOwnershipService, 25 private videoOwnershipService: VideoOwnershipService
28 private i18n: I18n
29 ) { 26 ) {
30 super() 27 super()
31 } 28 }
@@ -50,7 +47,7 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit {
50 this.videoOwnershipService.refuseOwnership(videoChangeOwnership.id) 47 this.videoOwnershipService.refuseOwnership(videoChangeOwnership.id)
51 .subscribe( 48 .subscribe(
52 () => this.loadData(), 49 () => this.loadData(),
53 err => this.notificationsService.error(this.i18n('Error'), err.message) 50 err => this.notifier.error(err.message)
54 ) 51 )
55 } 52 }
56 53
@@ -62,7 +59,7 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit {
62 this.totalRecords = resultList.total 59 this.totalRecords = resultList.total
63 }, 60 },
64 61
65 err => this.notificationsService.error(this.i18n('Error'), err.message) 62 err => this.notifier.error(err.message)
66 ) 63 )
67 } 64 }
68} 65}
diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts
index 601e517b4..9996218ca 100644
--- a/client/src/app/+my-account/my-account-routing.module.ts
+++ b/client/src/app/+my-account/my-account-routing.module.ts
@@ -13,6 +13,8 @@ import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-sub
13import { MyAccountOwnershipComponent } from '@app/+my-account/my-account-ownership/my-account-ownership.component' 13import { MyAccountOwnershipComponent } from '@app/+my-account/my-account-ownership/my-account-ownership.component'
14import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component' 14import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component'
15import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component' 15import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component'
16import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component'
17import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component'
16 18
17const myAccountRoutes: Routes = [ 19const myAccountRoutes: Routes = [
18 { 20 {
@@ -114,6 +116,24 @@ const myAccountRoutes: Routes = [
114 title: 'Muted instances' 116 title: 'Muted instances'
115 } 117 }
116 } 118 }
119 },
120 {
121 path: 'history/videos',
122 component: MyAccountHistoryComponent,
123 data: {
124 meta: {
125 title: 'Videos history'
126 }
127 }
128 },
129 {
130 path: 'notifications',
131 component: MyAccountNotificationsComponent,
132 data: {
133 meta: {
134 title: 'Notifications'
135 }
136 }
117 } 137 }
118 ] 138 ]
119 } 139 }
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts
index e5343b33d..cbb068c7c 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts
@@ -1,11 +1,10 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { AuthService, Notifier } from '@app/core'
3import { FormReactive, UserService } from '../../../shared' 3import { FormReactive, UserService } from '../../../shared'
4import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 5import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
6import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' 6import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
7import { filter } from 'rxjs/operators' 7import { filter } from 'rxjs/operators'
8import { AuthService } from '@app/core'
9import { User } from '../../../../../../shared' 8import { User } from '../../../../../../shared'
10 9
11@Component({ 10@Component({
@@ -20,7 +19,7 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On
20 constructor ( 19 constructor (
21 protected formValidatorService: FormValidatorService, 20 protected formValidatorService: FormValidatorService,
22 private userValidatorsService: UserValidatorsService, 21 private userValidatorsService: UserValidatorsService,
23 private notificationsService: NotificationsService, 22 private notifier: Notifier,
24 private authService: AuthService, 23 private authService: AuthService,
25 private userService: UserService, 24 private userService: UserService,
26 private i18n: I18n 25 private i18n: I18n
@@ -50,7 +49,7 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On
50 49
51 this.userService.changePassword(currentPassword, newPassword).subscribe( 50 this.userService.changePassword(currentPassword, newPassword).subscribe(
52 () => { 51 () => {
53 this.notificationsService.success(this.i18n('Success'), this.i18n('Password updated.')) 52 this.notifier.success(this.i18n('Password updated.'))
54 53
55 this.form.reset() 54 this.form.reset()
56 this.error = null 55 this.error = null
diff --git a/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts b/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts
index 63a121f64..3f79efe20 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts
@@ -1,5 +1,5 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { AuthService, ConfirmService, RedirectService } from '../../../core' 3import { AuthService, ConfirmService, RedirectService } from '../../../core'
4import { UserService } from '../../../shared' 4import { UserService } from '../../../shared'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -15,7 +15,7 @@ export class MyAccountDangerZoneComponent {
15 15
16 constructor ( 16 constructor (
17 private authService: AuthService, 17 private authService: AuthService,
18 private notificationsService: NotificationsService, 18 private notifier: Notifier,
19 private userService: UserService, 19 private userService: UserService,
20 private confirmService: ConfirmService, 20 private confirmService: ConfirmService,
21 private redirectService: RedirectService, 21 private redirectService: RedirectService,
@@ -34,13 +34,13 @@ export class MyAccountDangerZoneComponent {
34 34
35 this.userService.deleteMe().subscribe( 35 this.userService.deleteMe().subscribe(
36 () => { 36 () => {
37 this.notificationsService.success(this.i18n('Success'), this.i18n('Your account is deleted.')) 37 this.notifier.success(this.i18n('Your account is deleted.'))
38 38
39 this.authService.logout() 39 this.authService.logout()
40 this.redirectService.redirectToHomepage() 40 this.redirectService.redirectToHomepage()
41 }, 41 },
42 42
43 err => this.notificationsService.error(this.i18n('Error'), err.message) 43 err => this.notifier.error(err.message)
44 ) 44 )
45 } 45 }
46} 46}
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/index.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/index.ts
new file mode 100644
index 000000000..5e1d51339
--- /dev/null
+++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/index.ts
@@ -0,0 +1 @@
export * from './my-account-notification-preferences.component'
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html
new file mode 100644
index 000000000..59422d682
--- /dev/null
+++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html
@@ -0,0 +1,19 @@
1<div class="custom-row">
2 <div i18n>Activities</div>
3 <div i18n>Web</div>
4 <div i18n *ngIf="emailEnabled">Email</div>
5</div>
6
7<div class="custom-row" *ngFor="let notificationType of notificationSettingKeys">
8 <ng-container *ngIf="hasUserRight(notificationType)">
9 <div>{{ labelNotifications[notificationType] }}</div>
10
11 <div>
12 <p-inputSwitch [(ngModel)]="webNotifications[notificationType]" (onChange)="updateWebSetting(notificationType, $event.checked)"></p-inputSwitch>
13 </div>
14
15 <div *ngIf="emailEnabled">
16 <p-inputSwitch [(ngModel)]="emailNotifications[notificationType]" (onChange)="updateEmailSetting(notificationType, $event.checked)"></p-inputSwitch>
17 </div>
18 </ng-container>
19</div>
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss
new file mode 100644
index 000000000..6feb16ab1
--- /dev/null
+++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss
@@ -0,0 +1,25 @@
1@import '_variables';
2@import '_mixins';
3
4.custom-row {
5 display: flex;
6 align-items: center;
7 border-bottom: 1px solid rgba(0, 0, 0, 0.10);
8
9 &:first-child {
10 font-size: 16px;
11
12 & > div {
13 font-weight: $font-semibold;
14 }
15 }
16
17 & > div {
18 width: 350px;
19 }
20
21 & > div {
22 padding: 10px
23 }
24}
25
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
new file mode 100644
index 000000000..519bdfab4
--- /dev/null
+++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
@@ -0,0 +1,99 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { User } from '@app/shared'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { Subject } from 'rxjs'
5import { UserNotificationSetting, UserNotificationSettingValue, UserRight } from '../../../../../../shared'
6import { Notifier, ServerService } from '@app/core'
7import { debounce } from 'lodash-es'
8import { UserNotificationService } from '@app/shared/users/user-notification.service'
9
10@Component({
11 selector: 'my-account-notification-preferences',
12 templateUrl: './my-account-notification-preferences.component.html',
13 styleUrls: [ './my-account-notification-preferences.component.scss' ]
14})
15export class MyAccountNotificationPreferencesComponent implements OnInit {
16 @Input() user: User = null
17 @Input() userInformationLoaded: Subject<any>
18
19 notificationSettingKeys: (keyof UserNotificationSetting)[] = []
20 emailNotifications: { [ id in keyof UserNotificationSetting ]: boolean } = {} as any
21 webNotifications: { [ id in keyof UserNotificationSetting ]: boolean } = {} as any
22 labelNotifications: { [ id in keyof UserNotificationSetting ]: string } = {} as any
23 rightNotifications: { [ id in keyof Partial<UserNotificationSetting> ]: UserRight } = {} as any
24 emailEnabled: boolean
25
26 private savePreferences = debounce(this.savePreferencesImpl.bind(this), 500)
27
28 constructor (
29 private i18n: I18n,
30 private userNotificationService: UserNotificationService,
31 private serverService: ServerService,
32 private notifier: Notifier
33 ) {
34 this.labelNotifications = {
35 newVideoFromSubscription: this.i18n('New video from your subscriptions'),
36 newCommentOnMyVideo: this.i18n('New comment on your video'),
37 videoAbuseAsModerator: this.i18n('New video abuse on local video'),
38 blacklistOnMyVideo: this.i18n('One of your video is blacklisted/unblacklisted'),
39 myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'),
40 myVideoImportFinished: this.i18n('Video import finished'),
41 newUserRegistration: this.i18n('A new user registered on your instance'),
42 newFollow: this.i18n('You or your channel(s) has a new follower'),
43 commentMention: this.i18n('Someone mentioned you in video comments')
44 }
45 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
46
47 this.rightNotifications = {
48 videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES,
49 newUserRegistration: UserRight.MANAGE_USERS
50 }
51
52 this.emailEnabled = this.serverService.getConfig().email.enabled
53 }
54
55 ngOnInit () {
56 this.userInformationLoaded.subscribe(() => this.loadNotificationSettings())
57 }
58
59 hasUserRight (field: keyof UserNotificationSetting) {
60 const rightToHave = this.rightNotifications[field]
61 if (!rightToHave) return true // No rights needed
62
63 return this.user.hasRight(rightToHave)
64 }
65
66 updateEmailSetting (field: keyof UserNotificationSetting, value: boolean) {
67 if (value === true) this.user.notificationSettings[field] |= UserNotificationSettingValue.EMAIL
68 else this.user.notificationSettings[field] &= ~UserNotificationSettingValue.EMAIL
69
70 this.savePreferences()
71 }
72
73 updateWebSetting (field: keyof UserNotificationSetting, value: boolean) {
74 if (value === true) this.user.notificationSettings[field] |= UserNotificationSettingValue.WEB
75 else this.user.notificationSettings[field] &= ~UserNotificationSettingValue.WEB
76
77 this.savePreferences()
78 }
79
80 private savePreferencesImpl () {
81 this.userNotificationService.updateNotificationSettings(this.user, this.user.notificationSettings)
82 .subscribe(
83 () => {
84 this.notifier.success(this.i18n('Preferences saved'), undefined, 2000)
85 },
86
87 err => this.notifier.error(err.message)
88 )
89 }
90
91 private loadNotificationSettings () {
92 for (const key of Object.keys(this.user.notificationSettings)) {
93 const value = this.user.notificationSettings[key]
94 this.emailNotifications[key] = value & UserNotificationSettingValue.EMAIL
95
96 this.webNotifications[key] = value & UserNotificationSettingValue.WEB
97 }
98 }
99}
diff --git a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts
index 967e21f0b..a9503ed1b 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts
@@ -1,5 +1,5 @@
1import { Component, Input, OnInit } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { FormReactive, UserService } from '../../../shared' 3import { FormReactive, UserService } from '../../../shared'
4import { User } from '@app/shared' 4import { User } from '@app/shared'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -21,7 +21,7 @@ export class MyAccountProfileComponent extends FormReactive implements OnInit {
21 constructor ( 21 constructor (
22 protected formValidatorService: FormValidatorService, 22 protected formValidatorService: FormValidatorService,
23 private userValidatorsService: UserValidatorsService, 23 private userValidatorsService: UserValidatorsService,
24 private notificationsService: NotificationsService, 24 private notifier: Notifier,
25 private userService: UserService, 25 private userService: UserService,
26 private i18n: I18n 26 private i18n: I18n
27 ) { 27 ) {
@@ -53,7 +53,7 @@ export class MyAccountProfileComponent extends FormReactive implements OnInit {
53 this.user.account.displayName = displayName 53 this.user.account.displayName = displayName
54 this.user.account.description = description 54 this.user.account.description = description
55 55
56 this.notificationsService.success(this.i18n('Success'), this.i18n('Profile updated.')) 56 this.notifier.success(this.i18n('Profile updated.'))
57 }, 57 },
58 58
59 err => this.error = err.message 59 err => this.error = err.message
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
index c7e23cd1f..ad64f28fe 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
+++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
@@ -4,10 +4,11 @@
4 <span i18n class="user-quota-label">Video quota:</span> {{ userVideoQuotaUsed | bytes: 0 }} / {{ userVideoQuota }} 4 <span i18n class="user-quota-label">Video quota:</span> {{ userVideoQuotaUsed | bytes: 0 }} / {{ userVideoQuota }}
5</div> 5</div>
6 6
7<ng-template [ngIf]="user && user.account"> 7<div i18n class="account-title">Profile</div>
8 <div i18n class="account-title">Profile</div> 8<my-account-profile [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-profile>
9 <my-account-profile [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-profile> 9
10</ng-template> 10<div i18n class="account-title" id="notifications">Notifications</div>
11<my-account-notification-preferences [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-notification-preferences>
11 12
12<div i18n class="account-title">Password</div> 13<div i18n class="account-title">Password</div>
13<my-account-change-password></my-account-change-password> 14<my-account-change-password></my-account-change-password>
@@ -16,4 +17,4 @@
16<my-account-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-video-settings> 17<my-account-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-video-settings>
17 18
18<div i18n class="account-title">Danger zone</div> 19<div i18n class="account-title">Danger zone</div>
19<my-account-danger-zone [user]="user"></my-account-danger-zone> \ No newline at end of file 20<my-account-danger-zone [user]="user"></my-account-danger-zone>
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts
index 62053d97b..f4b954e54 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts
@@ -1,5 +1,5 @@
1import { Component, OnInit, ViewChild } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { BytesPipe } from 'ngx-pipes' 3import { BytesPipe } from 'ngx-pipes'
4import { AuthService } from '../../core' 4import { AuthService } from '../../core'
5import { User } from '../../shared' 5import { User } from '../../shared'
@@ -19,7 +19,7 @@ export class MyAccountSettingsComponent implements OnInit {
19 constructor ( 19 constructor (
20 private userService: UserService, 20 private userService: UserService,
21 private authService: AuthService, 21 private authService: AuthService,
22 private notificationsService: NotificationsService, 22 private notifier: Notifier,
23 private i18n: I18n 23 private i18n: I18n
24 ) {} 24 ) {}
25 25
@@ -48,12 +48,12 @@ export class MyAccountSettingsComponent implements OnInit {
48 this.userService.changeAvatar(formData) 48 this.userService.changeAvatar(formData)
49 .subscribe( 49 .subscribe(
50 data => { 50 data => {
51 this.notificationsService.success(this.i18n('Success'), this.i18n('Avatar changed.')) 51 this.notifier.success(this.i18n('Avatar changed.'))
52 52
53 this.user.updateAccountAvatar(data.avatar) 53 this.user.updateAccountAvatar(data.avatar)
54 }, 54 },
55 55
56 err => this.notificationsService.error(this.i18n('Error'), err.message) 56 err => this.notifier.error(err.message)
57 ) 57 )
58 } 58 }
59} 59}
diff --git a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts
index 6c9a7ce75..b8f80bc1a 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts
@@ -1,5 +1,5 @@
1import { Component, Input, OnInit } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { UserUpdateMe } from '../../../../../../shared' 3import { UserUpdateMe } from '../../../../../../shared'
4import { AuthService } from '../../../core' 4import { AuthService } from '../../../core'
5import { FormReactive, User, UserService } from '../../../shared' 5import { FormReactive, User, UserService } from '../../../shared'
@@ -19,7 +19,7 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
19 constructor ( 19 constructor (
20 protected formValidatorService: FormValidatorService, 20 protected formValidatorService: FormValidatorService,
21 private authService: AuthService, 21 private authService: AuthService,
22 private notificationsService: NotificationsService, 22 private notifier: Notifier,
23 private userService: UserService, 23 private userService: UserService,
24 private i18n: I18n 24 private i18n: I18n
25 ) { 25 ) {
@@ -54,12 +54,12 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
54 54
55 this.userService.updateMyProfile(details).subscribe( 55 this.userService.updateMyProfile(details).subscribe(
56 () => { 56 () => {
57 this.notificationsService.success(this.i18n('Success'), this.i18n('Information updated.')) 57 this.notifier.success(this.i18n('Information updated.'))
58 58
59 this.authService.refreshUserInformation() 59 this.authService.refreshUserInformation()
60 }, 60 },
61 61
62 err => this.notificationsService.error(this.i18n('Error'), err.message) 62 err => this.notifier.error(err.message)
63 ) 63 )
64 } 64 }
65} 65}
diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts
index 9517a3705..9d2dccdf0 100644
--- a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts
+++ b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts
@@ -1,5 +1,5 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
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 { UserSubscriptionService } from '@app/shared/user-subscription' 5import { UserSubscriptionService } from '@app/shared/user-subscription'
@@ -21,7 +21,7 @@ export class MyAccountSubscriptionsComponent implements OnInit {
21 21
22 constructor ( 22 constructor (
23 private userSubscriptionService: UserSubscriptionService, 23 private userSubscriptionService: UserSubscriptionService,
24 private notificationsService: NotificationsService, 24 private notifier: Notifier,
25 private i18n: I18n 25 private i18n: I18n
26 ) {} 26 ) {}
27 27
@@ -37,7 +37,7 @@ export class MyAccountSubscriptionsComponent implements OnInit {
37 this.pagination.totalItems = res.total 37 this.pagination.totalItems = res.total
38 }, 38 },
39 39
40 error => this.notificationsService.error(this.i18n('Error'), error.message) 40 error => this.notifier.error(error.message)
41 ) 41 )
42 } 42 }
43 43
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-create.component.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-create.component.ts
index 81608d837..a68f79b47 100644
--- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-create.component.ts
+++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-create.component.ts
@@ -1,10 +1,9 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications' 3import { AuthService, Notifier } from '@app/core'
4import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit' 4import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit'
5import { VideoChannelCreate } from '../../../../../shared/models/videos' 5import { VideoChannelCreate } from '../../../../../shared/models/videos'
6import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' 6import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
7import { AuthService } from '@app/core'
8import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
9import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 8import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
10import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators/video-channel-validators.service' 9import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators/video-channel-validators.service'
@@ -21,7 +20,7 @@ export class MyAccountVideoChannelCreateComponent extends MyAccountVideoChannelE
21 protected formValidatorService: FormValidatorService, 20 protected formValidatorService: FormValidatorService,
22 private authService: AuthService, 21 private authService: AuthService,
23 private videoChannelValidatorsService: VideoChannelValidatorsService, 22 private videoChannelValidatorsService: VideoChannelValidatorsService,
24 private notificationsService: NotificationsService, 23 private notifier: Notifier,
25 private router: Router, 24 private router: Router,
26 private videoChannelService: VideoChannelService, 25 private videoChannelService: VideoChannelService,
27 private i18n: I18n 26 private i18n: I18n
@@ -56,8 +55,8 @@ export class MyAccountVideoChannelCreateComponent extends MyAccountVideoChannelE
56 this.videoChannelService.createVideoChannel(videoChannelCreate).subscribe( 55 this.videoChannelService.createVideoChannel(videoChannelCreate).subscribe(
57 () => { 56 () => {
58 this.authService.refreshUserInformation() 57 this.authService.refreshUserInformation()
59 this.notificationsService.success( 58
60 this.i18n('Success'), 59 this.notifier.success(
61 this.i18n('Video channel {{videoChannelName}} created.', { videoChannelName: videoChannelCreate.displayName }) 60 this.i18n('Video channel {{videoChannelName}} created.', { videoChannelName: videoChannelCreate.displayName })
62 ) 61 )
63 this.router.navigate([ '/my-account', 'video-channels' ]) 62 this.router.navigate([ '/my-account', 'video-channels' ])
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts
index 5d43956f2..da4fb645a 100644
--- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts
+++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts
@@ -1,12 +1,11 @@
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 { NotificationsService } from 'angular2-notifications' 3import { AuthService, Notifier, ServerService } from '@app/core'
4import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit' 4import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit'
5import { VideoChannelUpdate } from '../../../../../shared/models/videos' 5import { VideoChannelUpdate } from '../../../../../shared/models/videos'
6import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' 6import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
7import { Subscription } from 'rxjs' 7import { Subscription } from 'rxjs'
8import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 8import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
9import { AuthService, ServerService } from '@app/core'
10import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
11import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 10import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
12import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators/video-channel-validators.service' 11import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators/video-channel-validators.service'
@@ -26,7 +25,7 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
26 protected formValidatorService: FormValidatorService, 25 protected formValidatorService: FormValidatorService,
27 private authService: AuthService, 26 private authService: AuthService,
28 private videoChannelValidatorsService: VideoChannelValidatorsService, 27 private videoChannelValidatorsService: VideoChannelValidatorsService,
29 private notificationsService: NotificationsService, 28 private notifier: Notifier,
30 private router: Router, 29 private router: Router,
31 private route: ActivatedRoute, 30 private route: ActivatedRoute,
32 private videoChannelService: VideoChannelService, 31 private videoChannelService: VideoChannelService,
@@ -79,10 +78,11 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
79 this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe( 78 this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe(
80 () => { 79 () => {
81 this.authService.refreshUserInformation() 80 this.authService.refreshUserInformation()
82 this.notificationsService.success( 81
83 this.i18n('Success'), 82 this.notifier.success(
84 this.i18n('Video channel {{videoChannelName}} updated.', { videoChannelName: videoChannelUpdate.displayName }) 83 this.i18n('Video channel {{videoChannelName}} updated.', { videoChannelName: videoChannelUpdate.displayName })
85 ) 84 )
85
86 this.router.navigate([ '/my-account', 'video-channels' ]) 86 this.router.navigate([ '/my-account', 'video-channels' ])
87 }, 87 },
88 88
@@ -94,12 +94,12 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
94 this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData) 94 this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData)
95 .subscribe( 95 .subscribe(
96 data => { 96 data => {
97 this.notificationsService.success(this.i18n('Success'), this.i18n('Avatar changed.')) 97 this.notifier.success(this.i18n('Avatar changed.'))
98 98
99 this.videoChannelToUpdate.updateAvatar(data.avatar) 99 this.videoChannelToUpdate.updateAvatar(data.avatar)
100 }, 100 },
101 101
102 err => this.notificationsService.error(this.i18n('Error'), err.message) 102 err => this.notifier.error(err.message)
103 ) 103 )
104 } 104 }
105 105
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 6d1098865..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
@@ -1,5 +1,5 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { AuthService } from '../../core/auth' 3import { AuthService } from '../../core/auth'
4import { ConfirmService } from '../../core/confirm' 4import { ConfirmService } from '../../core/confirm'
5import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 5import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
@@ -20,7 +20,7 @@ export class MyAccountVideoChannelsComponent implements OnInit {
20 20
21 constructor ( 21 constructor (
22 private authService: AuthService, 22 private authService: AuthService,
23 private notificationsService: NotificationsService, 23 private notifier: Notifier,
24 private confirmService: ConfirmService, 24 private confirmService: ConfirmService,
25 private videoChannelService: VideoChannelService, 25 private videoChannelService: VideoChannelService,
26 private i18n: I18n 26 private i18n: I18n
@@ -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 )
@@ -46,15 +50,14 @@ export class MyAccountVideoChannelsComponent implements OnInit {
46 50
47 this.videoChannelService.removeVideoChannel(videoChannel) 51 this.videoChannelService.removeVideoChannel(videoChannel)
48 .subscribe( 52 .subscribe(
49 status => { 53 () => {
50 this.loadVideoChannels() 54 this.loadVideoChannels()
51 this.notificationsService.success( 55 this.notifier.success(
52 this.i18n('Success'),
53 this.i18n('Video channel {{videoChannelName}} deleted.', { videoChannelName: videoChannel.displayName }) 56 this.i18n('Video channel {{videoChannelName}} deleted.', { videoChannelName: videoChannel.displayName })
54 ) 57 )
55 }, 58 },
56 59
57 error => this.notificationsService.error(this.i18n('Error'), error.message) 60 error => this.notifier.error(error.message)
58 ) 61 )
59 } 62 }
60 63
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts
index 5b920c98d..21a10c8ff 100644
--- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts
+++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts
@@ -1,7 +1,7 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { RestPagination, RestTable } from '@app/shared' 2import { RestPagination, RestTable } from '@app/shared'
3import { SortMeta } from 'primeng/components/common/sortmeta' 3import { SortMeta } from 'primeng/components/common/sortmeta'
4import { NotificationsService } from 'angular2-notifications' 4import { Notifier } from '@app/core'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { VideoImport, VideoImportState } from '../../../../../shared/models/videos' 6import { VideoImport, VideoImportState } from '../../../../../shared/models/videos'
7import { VideoImportService } from '@app/shared/video-import' 7import { VideoImportService } from '@app/shared/video-import'
@@ -19,7 +19,7 @@ export class MyAccountVideoImportsComponent extends RestTable implements OnInit
19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
20 20
21 constructor ( 21 constructor (
22 private notificationsService: NotificationsService, 22 private notifier: Notifier,
23 private videoImportService: VideoImportService, 23 private videoImportService: VideoImportService,
24 private i18n: I18n 24 private i18n: I18n
25 ) { 25 ) {
@@ -58,7 +58,7 @@ export class MyAccountVideoImportsComponent extends RestTable implements OnInit
58 this.totalRecords = resultList.total 58 this.totalRecords = resultList.total
59 }, 59 },
60 60
61 err => this.notificationsService.error(this.i18n('Error'), err.message) 61 err => this.notifier.error(err.message)
62 ) 62 )
63 } 63 }
64} 64}
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 2db81a3fe..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}
@@ -97,7 +94,7 @@
97 } 94 }
98} 95}
99 96
100@media screen and (max-width: 800px) { 97@media screen and (max-width: $small-view) {
101 .video { 98 .video {
102 flex-direction: column; 99 flex-direction: column;
103 height: auto; 100 height: auto;
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
index 2d88ac760..41608f796 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
@@ -5,7 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router'
5import { Location } from '@angular/common' 5import { Location } from '@angular/common'
6import { immutableAssign } from '@app/shared/misc/utils' 6import { immutableAssign } from '@app/shared/misc/utils'
7import { ComponentPagination } from '@app/shared/rest/component-pagination.model' 7import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
8import { NotificationsService } from 'angular2-notifications' 8import { Notifier } from '@app/core'
9import { AuthService } from '../../core/auth' 9import { AuthService } from '../../core/auth'
10import { ConfirmService } from '../../core/confirm' 10import { ConfirmService } from '../../core/confirm'
11import { AbstractVideoList } from '../../shared/video/abstract-video-list' 11import { AbstractVideoList } from '../../shared/video/abstract-video-list'
@@ -40,7 +40,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
40 protected router: Router, 40 protected router: Router,
41 protected route: ActivatedRoute, 41 protected route: ActivatedRoute,
42 protected authService: AuthService, 42 protected authService: AuthService,
43 protected notificationsService: NotificationsService, 43 protected notifier: Notifier,
44 protected location: Location, 44 protected location: Location,
45 protected screenService: ScreenService, 45 protected screenService: ScreenService,
46 protected i18n: I18n, 46 protected i18n: I18n,
@@ -102,16 +102,13 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
102 .pipe(concatAll()) 102 .pipe(concatAll())
103 .subscribe( 103 .subscribe(
104 res => { 104 res => {
105 this.notificationsService.success( 105 this.notifier.success(this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length }))
106 this.i18n('Success'),
107 this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length })
108 )
109 106
110 this.abortSelectionMode() 107 this.abortSelectionMode()
111 this.reloadVideos() 108 this.reloadVideos()
112 }, 109 },
113 110
114 err => this.notificationsService.error(this.i18n('Error'), err.message) 111 err => this.notifier.error(err.message)
115 ) 112 )
116 } 113 }
117 114
@@ -124,15 +121,12 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
124 121
125 this.videoService.removeVideo(video.id) 122 this.videoService.removeVideo(video.id)
126 .subscribe( 123 .subscribe(
127 status => { 124 () => {
128 this.notificationsService.success( 125 this.notifier.success(this.i18n('Video {{videoName}} deleted.', { videoName: video.name }))
129 this.i18n('Success'),
130 this.i18n('Video {{videoName}} deleted.', { videoName: video.name })
131 )
132 this.reloadVideos() 126 this.reloadVideos()
133 }, 127 },
134 128
135 error => this.notificationsService.error(this.i18n('Error'), error.message) 129 error => this.notifier.error(error.message)
136 ) 130 )
137 } 131 }
138 132
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/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts
index 9f94f3c13..37d7cf2a4 100644
--- a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts
+++ b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts
@@ -1,5 +1,5 @@
1import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' 1import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { FormReactive, UserService } from '../../../shared/index' 4import { FormReactive, UserService } from '../../../shared/index'
5import { Video } from '@app/shared/video/video.model' 5import { Video } from '@app/shared/video/video.model'
@@ -25,7 +25,7 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni
25 protected formValidatorService: FormValidatorService, 25 protected formValidatorService: FormValidatorService,
26 private videoChangeOwnershipValidatorsService: VideoChangeOwnershipValidatorsService, 26 private videoChangeOwnershipValidatorsService: VideoChangeOwnershipValidatorsService,
27 private videoOwnershipService: VideoOwnershipService, 27 private videoOwnershipService: VideoOwnershipService,
28 private notificationsService: NotificationsService, 28 private notifier: Notifier,
29 private userService: UserService, 29 private userService: UserService,
30 private modalService: NgbModal, 30 private modalService: NgbModal,
31 private i18n: I18n 31 private i18n: I18n
@@ -53,11 +53,9 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni
53 const query = event.query 53 const query = event.query
54 this.userService.autocomplete(query) 54 this.userService.autocomplete(query)
55 .subscribe( 55 .subscribe(
56 usernames => { 56 usernames => this.usernamePropositions = usernames,
57 this.usernamePropositions = usernames
58 },
59 57
60 err => this.notificationsService.error('Error', err.message) 58 err => this.notifier.error(err.message)
61 ) 59 )
62 } 60 }
63 61
@@ -67,9 +65,9 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni
67 this.videoOwnershipService 65 this.videoOwnershipService
68 .changeOwnership(this.video.id, username) 66 .changeOwnership(this.video.id, username)
69 .subscribe( 67 .subscribe(
70 () => this.notificationsService.success(this.i18n('Success'), this.i18n('Ownership change request sent.')), 68 () => this.notifier.success(this.i18n('Ownership change request sent.')),
71 69
72 err => this.notificationsService.error(this.i18n('Error'), err.message) 70 err => this.notifier.error(err.message)
73 ) 71 )
74 } 72 }
75} 73}
diff --git a/client/src/app/+my-account/my-account.component.html b/client/src/app/+my-account/my-account.component.html
index 41333c25a..3999252be 100644
--- a/client/src/app/+my-account/my-account.component.html
+++ b/client/src/app/+my-account/my-account.component.html
@@ -1,40 +1,5 @@
1<div class="row"> 1<div class="row">
2 <div class="sub-menu"> 2 <my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown>
3 <a i18n routerLink="/my-account/settings" routerLinkActive="active" class="title-page">My settings</a>
4
5 <div ngbDropdown class="my-library">
6 <span role="button" class="title-page" [ngClass]="{ active: libraryLabel !== '' }" ngbDropdownToggle>
7 <ng-container i18n>My library</ng-container>
8 <ng-container *ngIf="libraryLabel"> - {{ libraryLabel }}</ng-container>
9 </span>
10
11 <div ngbDropdownMenu>
12 <a class="dropdown-item" i18n routerLink="/my-account/video-channels">My channels</a>
13
14 <a class="dropdown-item" i18n routerLink="/my-account/videos">My videos</a>
15
16 <a class="dropdown-item" i18n routerLink="/my-account/subscriptions">My subscriptions</a>
17
18 <a class="dropdown-item" *ngIf="isVideoImportEnabled()" i18n routerLink="/my-account/video-imports">My imports</a>
19 </div>
20 </div>
21
22 <div ngbDropdown class="misc">
23 <span role="button" class="title-page" [ngClass]="{ active: miscLabel !== '' }" ngbDropdownToggle>
24 <ng-container i18n>Misc</ng-container>
25 <ng-container *ngIf="miscLabel"> - {{ miscLabel }}</ng-container>
26 </span>
27
28 <div ngbDropdownMenu>
29 <a class="dropdown-item" i18n routerLink="/my-account/blocklist/accounts">Muted accounts</a>
30
31 <a class="dropdown-item" i18n routerLink="/my-account/blocklist/servers">Muted instances</a>
32
33 <a class="dropdown-item" i18n routerLink="/my-account/ownership">Ownership changes</a>
34 </div>
35 </div>
36
37 </div>
38 3
39 <div class="margin-content"> 4 <div class="margin-content">
40 <router-outlet></router-outlet> 5 <router-outlet></router-outlet>
diff --git a/client/src/app/+my-account/my-account.component.scss b/client/src/app/+my-account/my-account.component.scss
index 6243c6dcf..4f111efdf 100644
--- a/client/src/app/+my-account/my-account.component.scss
+++ b/client/src/app/+my-account/my-account.component.scss
@@ -1,14 +1,3 @@
1.my-library, .misc { 1.row {
2 span[role=button] { 2 flex-direction: column;
3 cursor: pointer;
4 }
5
6 a {
7 display: block;
8 }
9} 3}
10
11/deep/ .dropdown-toggle::after {
12 position: relative;
13 top: 2px;
14} \ No newline at end of file
diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts
index d728caf07..8a4102d80 100644
--- a/client/src/app/+my-account/my-account.component.ts
+++ b/client/src/app/+my-account/my-account.component.ts
@@ -1,38 +1,80 @@
1import { Component, OnDestroy, OnInit } from '@angular/core' 1import { Component } from '@angular/core'
2import { ServerService } from '@app/core' 2import { ServerService } from '@app/core'
3import { NavigationStart, Router } from '@angular/router'
4import { filter } from 'rxjs/operators'
5import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
6import { Subscription } from 'rxjs' 4import { TopMenuDropdownParam } from '@app/shared/menu/top-menu-dropdown.component'
7 5
8@Component({ 6@Component({
9 selector: 'my-my-account', 7 selector: 'my-my-account',
10 templateUrl: './my-account.component.html', 8 templateUrl: './my-account.component.html',
11 styleUrls: [ './my-account.component.scss' ] 9 styleUrls: [ './my-account.component.scss' ]
12}) 10})
13export class MyAccountComponent implements OnInit, OnDestroy { 11export class MyAccountComponent {
14 12 menuEntries: TopMenuDropdownParam[] = []
15 libraryLabel = ''
16 miscLabel = ''
17
18 private routeSub: Subscription
19 13
20 constructor ( 14 constructor (
21 private serverService: ServerService, 15 private serverService: ServerService,
22 private router: Router,
23 private i18n: I18n 16 private i18n: I18n
24 ) {} 17 ) {
18
19 const libraryEntries: TopMenuDropdownParam = {
20 label: this.i18n('My library'),
21 children: [
22 {
23 label: this.i18n('My channels'),
24 routerLink: '/my-account/video-channels'
25 },
26 {
27 label: this.i18n('My videos'),
28 routerLink: '/my-account/videos'
29 },
30 {
31 label: this.i18n('My subscriptions'),
32 routerLink: '/my-account/subscriptions'
33 },
34 {
35 label: this.i18n('My history'),
36 routerLink: '/my-account/history/videos'
37 }
38 ]
39 }
25 40
26 ngOnInit () { 41 if (this.isVideoImportEnabled()) {
27 this.updateLabels(this.router.url) 42 libraryEntries.children.push({
43 label: 'My imports',
44 routerLink: '/my-account/video-imports'
45 })
46 }
28 47
29 this.routeSub = this.router.events 48 const miscEntries: TopMenuDropdownParam = {
30 .pipe(filter(event => event instanceof NavigationStart)) 49 label: this.i18n('Misc'),
31 .subscribe((event: NavigationStart) => this.updateLabels(event.url)) 50 children: [
32 } 51 {
52 label: this.i18n('Muted accounts'),
53 routerLink: '/my-account/blocklist/accounts'
54 },
55 {
56 label: this.i18n('Muted instances'),
57 routerLink: '/my-account/blocklist/servers'
58 },
59 {
60 label: this.i18n('Ownership changes'),
61 routerLink: '/my-account/ownership'
62 }
63 ]
64 }
33 65
34 ngOnDestroy () { 66 this.menuEntries = [
35 if (this.routeSub) this.routeSub.unsubscribe() 67 {
68 label: this.i18n('My settings'),
69 routerLink: '/my-account/settings'
70 },
71 {
72 label: this.i18n('My notifications'),
73 routerLink: '/my-account/notifications'
74 },
75 libraryEntries,
76 miscEntries
77 ]
36 } 78 }
37 79
38 isVideoImportEnabled () { 80 isVideoImportEnabled () {
@@ -41,27 +83,4 @@ export class MyAccountComponent implements OnInit, OnDestroy {
41 return importConfig.http.enabled || importConfig.torrent.enabled 83 return importConfig.http.enabled || importConfig.torrent.enabled
42 } 84 }
43 85
44 private updateLabels (url: string) {
45 const [ path ] = url.split('?')
46
47 if (path.startsWith('/my-account/video-channels')) {
48 this.libraryLabel = this.i18n('Channels')
49 } else if (path.startsWith('/my-account/videos')) {
50 this.libraryLabel = this.i18n('Videos')
51 } else if (path.startsWith('/my-account/subscriptions')) {
52 this.libraryLabel = this.i18n('Subscriptions')
53 } else if (path.startsWith('/my-account/video-imports')) {
54 this.libraryLabel = this.i18n('Video imports')
55 } else {
56 this.libraryLabel = ''
57 }
58
59 if (path.startsWith('/my-account/blocklist/accounts')) {
60 this.miscLabel = this.i18n('Muted accounts')
61 } else if (path.startsWith('/my-account/blocklist/servers')) {
62 this.miscLabel = this.i18n('Muted instances')
63 } else {
64 this.miscLabel = ''
65 }
66 }
67} 86}
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index 017ebd57d..18f51f171 100644
--- a/client/src/app/+my-account/my-account.module.ts
+++ b/client/src/app/+my-account/my-account.module.ts
@@ -1,6 +1,7 @@
1import { TableModule } from 'primeng/table' 1import { TableModule } from 'primeng/table'
2import { NgModule } from '@angular/core' 2import { NgModule } from '@angular/core'
3import { AutoCompleteModule } from 'primeng/autocomplete' 3import { AutoCompleteModule } from 'primeng/autocomplete'
4import { InputSwitchModule } from 'primeng/inputswitch'
4import { SharedModule } from '../shared' 5import { SharedModule } from '../shared'
5import { MyAccountRoutingModule } from './my-account-routing.module' 6import { MyAccountRoutingModule } from './my-account-routing.module'
6import { MyAccountChangePasswordComponent } from './my-account-settings/my-account-change-password/my-account-change-password.component' 7import { MyAccountChangePasswordComponent } from './my-account-settings/my-account-change-password/my-account-change-password.component'
@@ -21,6 +22,9 @@ import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settin
21import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' 22import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
22import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component' 23import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component'
23import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component' 24import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component'
25import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component'
26import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component'
27import { MyAccountNotificationPreferencesComponent } from '@app/+my-account/my-account-settings/my-account-notification-preferences'
24 28
25@NgModule({ 29@NgModule({
26 imports: [ 30 imports: [
@@ -28,7 +32,8 @@ import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-b
28 MyAccountRoutingModule, 32 MyAccountRoutingModule,
29 AutoCompleteModule, 33 AutoCompleteModule,
30 SharedModule, 34 SharedModule,
31 TableModule 35 TableModule,
36 InputSwitchModule
32 ], 37 ],
33 38
34 declarations: [ 39 declarations: [
@@ -49,7 +54,10 @@ import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-b
49 MyAccountDangerZoneComponent, 54 MyAccountDangerZoneComponent,
50 MyAccountSubscriptionsComponent, 55 MyAccountSubscriptionsComponent,
51 MyAccountBlocklistComponent, 56 MyAccountBlocklistComponent,
52 MyAccountServerBlocklistComponent 57 MyAccountServerBlocklistComponent,
58 MyAccountHistoryComponent,
59 MyAccountNotificationsComponent,
60 MyAccountNotificationPreferencesComponent
53 ], 61 ],
54 62
55 exports: [ 63 exports: [
diff --git a/client/src/app/+my-account/shared/actor-avatar-info.component.ts b/client/src/app/+my-account/shared/actor-avatar-info.component.ts
index 54bacc212..72c815a0c 100644
--- a/client/src/app/+my-account/shared/actor-avatar-info.component.ts
+++ b/client/src/app/+my-account/shared/actor-avatar-info.component.ts
@@ -1,8 +1,8 @@
1import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'
2import { ServerService } from '../../core/server' 2import { ServerService } from '../../core/server'
3import { NotificationsService } from 'angular2-notifications'
4import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 3import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
5import { Account } from '@app/shared/account/account.model' 4import { Account } from '@app/shared/account/account.model'
5import { Notifier } from '@app/core'
6 6
7@Component({ 7@Component({
8 selector: 'my-actor-avatar-info', 8 selector: 'my-actor-avatar-info',
@@ -18,13 +18,13 @@ export class ActorAvatarInfoComponent {
18 18
19 constructor ( 19 constructor (
20 private serverService: ServerService, 20 private serverService: ServerService,
21 private notificationsService: NotificationsService 21 private notifier: Notifier
22 ) {} 22 ) {}
23 23
24 onAvatarChange () { 24 onAvatarChange () {
25 const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ] 25 const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ]
26 if (avatarfile.size > this.maxAvatarSize) { 26 if (avatarfile.size > this.maxAvatarSize) {
27 this.notificationsService.error('Error', 'This image is too large.') 27 this.notifier.error('Error', 'This image is too large.')
28 return 28 return
29 } 29 }
30 30
diff --git a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts b/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts
index 995f42ffc..cfd471fa4 100644
--- a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts
+++ b/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts
@@ -1,9 +1,8 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { I18n } from '@ngx-translate/i18n-polyfill' 2import { I18n } from '@ngx-translate/i18n-polyfill'
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier, RedirectService } from '@app/core'
4import { ServerService } from '@app/core/server' 4import { ServerService } from '@app/core/server'
5import { RedirectService } from '@app/core' 5import { FormReactive, UserService } from '@app/shared'
6import { UserService, FormReactive } from '@app/shared'
7import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
8import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' 7import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
9 8
@@ -20,7 +19,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements
20 private userValidatorsService: UserValidatorsService, 19 private userValidatorsService: UserValidatorsService,
21 private userService: UserService, 20 private userService: UserService,
22 private serverService: ServerService, 21 private serverService: ServerService,
23 private notificationsService: NotificationsService, 22 private notifier: Notifier,
24 private redirectService: RedirectService, 23 private redirectService: RedirectService,
25 private i18n: I18n 24 private i18n: I18n
26 ) { 25 ) {
@@ -46,12 +45,12 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements
46 'An email with verification link will be sent to {{email}}.', 45 'An email with verification link will be sent to {{email}}.',
47 { email } 46 { email }
48 ) 47 )
49 this.notificationsService.success(this.i18n('Success'), message) 48 this.notifier.success(message)
50 this.redirectService.redirectToHomepage() 49 this.redirectService.redirectToHomepage()
51 }, 50 },
52 51
53 err => { 52 err => {
54 this.notificationsService.error(this.i18n('Error'), err.message) 53 this.notifier.error(err.message)
55 } 54 }
56 ) 55 )
57 } 56 }
diff --git a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html
index 30ace5e10..a83d4a3c2 100644
--- a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html
+++ b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html
@@ -9,7 +9,7 @@
9 <ng-template #verificationError> 9 <ng-template #verificationError>
10 <div> 10 <div>
11 <span i18n>An error occurred. </span> 11 <span i18n>An error occurred. </span>
12 <a i18n routerLink="/verify-account/ask-email">Request new verification email.</a> 12 <a i18n routerLink="/verify-account/ask-send-email">Request new verification email.</a>
13 </div> 13 </div>
14 </ng-template> 14 </ng-template>
15</div> 15</div>
diff --git a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts
index e4a5522c8..f9ecf664b 100644
--- a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts
+++ b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts
@@ -1,7 +1,7 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { NotificationsService } from 'angular2-notifications' 4import { Notifier } from '@app/core'
5import { UserService } from '@app/shared' 5import { UserService } from '@app/shared'
6 6
7@Component({ 7@Component({
@@ -17,7 +17,7 @@ export class VerifyAccountEmailComponent implements OnInit {
17 17
18 constructor ( 18 constructor (
19 private userService: UserService, 19 private userService: UserService,
20 private notificationsService: NotificationsService, 20 private notifier: Notifier,
21 private router: Router, 21 private router: Router,
22 private route: ActivatedRoute, 22 private route: ActivatedRoute,
23 private i18n: I18n 23 private i18n: I18n
@@ -29,7 +29,7 @@ export class VerifyAccountEmailComponent implements OnInit {
29 this.verificationString = this.route.snapshot.queryParams['verificationString'] 29 this.verificationString = this.route.snapshot.queryParams['verificationString']
30 30
31 if (!this.userId || !this.verificationString) { 31 if (!this.userId || !this.verificationString) {
32 this.notificationsService.error(this.i18n('Error'), this.i18n('Unable to find user id or verification string.')) 32 this.notifier.error(this.i18n('Unable to find user id or verification string.'))
33 } else { 33 } else {
34 this.verifyEmail() 34 this.verifyEmail()
35 } 35 }
@@ -46,7 +46,7 @@ export class VerifyAccountEmailComponent implements OnInit {
46 }, 46 },
47 47
48 err => { 48 err => {
49 this.notificationsService.error(this.i18n('Error'), err.message) 49 this.notifier.error(err.message)
50 } 50 }
51 ) 51 )
52 } 52 }
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/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
index 1f0744fb1..dea378a6e 100644
--- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
+++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
@@ -2,7 +2,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { Location } from '@angular/common' 3import { Location } from '@angular/common'
4import { immutableAssign } from '@app/shared/misc/utils' 4import { immutableAssign } from '@app/shared/misc/utils'
5import { NotificationsService } from 'angular2-notifications'
6import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
7import { ConfirmService } from '../../core/confirm' 6import { ConfirmService } from '../../core/confirm'
8import { AbstractVideoList } from '../../shared/video/abstract-video-list' 7import { AbstractVideoList } from '../../shared/video/abstract-video-list'
@@ -13,6 +12,7 @@ import { tap } from 'rxjs/operators'
13import { I18n } from '@ngx-translate/i18n-polyfill' 12import { I18n } from '@ngx-translate/i18n-polyfill'
14import { Subscription } from 'rxjs' 13import { Subscription } from 'rxjs'
15import { ScreenService } from '@app/shared/misc/screen.service' 14import { ScreenService } from '@app/shared/misc/screen.service'
15import { Notifier } from '@app/core'
16 16
17@Component({ 17@Component({
18 selector: 'my-video-channel-videos', 18 selector: 'my-video-channel-videos',
@@ -35,7 +35,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
35 protected router: Router, 35 protected router: Router,
36 protected route: ActivatedRoute, 36 protected route: ActivatedRoute,
37 protected authService: AuthService, 37 protected authService: AuthService,
38 protected notificationsService: NotificationsService, 38 protected notifier: Notifier,
39 protected confirmService: ConfirmService, 39 protected confirmService: ConfirmService,
40 protected location: Location, 40 protected location: Location,
41 protected screenService: ScreenService, 41 protected screenService: ScreenService,
@@ -55,7 +55,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
55 this.videoChannelSub = this.videoChannelService.videoChannelLoaded 55 this.videoChannelSub = this.videoChannelService.videoChannelLoaded
56 .subscribe(videoChannel => { 56 .subscribe(videoChannel => {
57 this.videoChannel = videoChannel 57 this.videoChannel = videoChannel
58 this.currentRoute = '/video-channels/' + this.videoChannel.uuid + '/videos' 58 this.currentRoute = '/video-channels/' + this.videoChannel.nameWithHost + '/videos'
59 59
60 this.reloadVideos() 60 this.reloadVideos()
61 this.generateSyndicationList() 61 this.generateSyndicationList()
diff --git a/client/src/app/+video-channels/video-channels-routing.module.ts b/client/src/app/+video-channels/video-channels-routing.module.ts
index 935578d2a..3ac3533d9 100644
--- a/client/src/app/+video-channels/video-channels-routing.module.ts
+++ b/client/src/app/+video-channels/video-channels-routing.module.ts
@@ -7,7 +7,7 @@ import { VideoChannelAboutComponent } from './video-channel-about/video-channel-
7 7
8const videoChannelsRoutes: Routes = [ 8const videoChannelsRoutes: Routes = [
9 { 9 {
10 path: ':videoChannelId', 10 path: ':videoChannelName',
11 component: VideoChannelsComponent, 11 component: VideoChannelsComponent,
12 canActivateChild: [ MetaGuard ], 12 canActivateChild: [ MetaGuard ],
13 children: [ 13 children: [
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts
index 0c5c814c7..41ff82e98 100644
--- a/client/src/app/+video-channels/video-channels.component.ts
+++ b/client/src/app/+video-channels/video-channels.component.ts
@@ -34,9 +34,9 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
34 ngOnInit () { 34 ngOnInit () {
35 this.routeSub = this.route.params 35 this.routeSub = this.route.params
36 .pipe( 36 .pipe(
37 map(params => params[ 'videoChannelId' ]), 37 map(params => params[ 'videoChannelName' ]),
38 distinctUntilChanged(), 38 distinctUntilChanged(),
39 switchMap(videoChannelId => this.videoChannelService.getVideoChannel(videoChannelId)), 39 switchMap(videoChannelName => this.videoChannelService.getVideoChannel(videoChannelName)),
40 catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])) 40 catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ]))
41 ) 41 )
42 .subscribe(videoChannel => this.videoChannel = videoChannel) 42 .subscribe(videoChannel => this.videoChannel = videoChannel)
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts
index 545d6aeda..cff37a7d6 100644
--- a/client/src/app/app-routing.module.ts
+++ b/client/src/app/app-routing.module.ts
@@ -43,7 +43,8 @@ const routes: Routes = [
43 imports: [ 43 imports: [
44 RouterModule.forRoot(routes, { 44 RouterModule.forRoot(routes, {
45 useHash: Boolean(history.pushState) === false, 45 useHash: Boolean(history.pushState) === false,
46 preloadingStrategy: PreloadSelectedModulesList 46 preloadingStrategy: PreloadSelectedModulesList,
47 anchorScrolling: 'enabled'
47 }) 48 })
48 ], 49 ],
49 providers: [ 50 providers: [
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html
index 03f7e88ed..d398d4f35 100644
--- a/client/src/app/app.component.html
+++ b/client/src/app/app.component.html
@@ -30,12 +30,27 @@
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>
41<simple-notifications [options]="notificationOptions"></simple-notifications> 42
43<p-toast position="bottom-right">
44 <ng-template let-message pTemplate="message">
45 <div class="notification-block">
46 <div class="message">
47 <h3>{{ message.summary }}</h3>
48 <p>{{ message.detail }}</p>
49 </div>
50
51 <span *ngIf="message.severity === 'success'" class="glyphicon glyphicon-ok"></span>
52 <span *ngIf="message.severity === 'info'" class="glyphicon glyphicon-info-sign"></span>
53 <span *ngIf="message.severity === 'error'" class="glyphicon glyphicon-remove"></span>
54 </div>
55 </ng-template>
56</p-toast>
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss
index b51a81eb1..881f3ff31 100644
--- a/client/src/app/app.component.scss
+++ b/client/src/app/app.component.scss
@@ -91,8 +91,3 @@ footer {
91 height: $footer-height; 91 height: $footer-height;
92 justify-content: center; 92 justify-content: center;
93} 93}
94
95simple-notifications {
96 position: relative;
97 z-index: 1500;
98}
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index dc4d0bf6a..7583fdee8 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -15,19 +15,6 @@ import { fromEvent } from 'rxjs'
15 styleUrls: [ './app.component.scss' ] 15 styleUrls: [ './app.component.scss' ]
16}) 16})
17export class AppComponent implements OnInit { 17export class AppComponent implements OnInit {
18 notificationOptions = {
19 timeOut: 5000,
20 lastOnBottom: true,
21 clickToClose: true,
22 maxLength: 0,
23 maxStack: 7,
24 showProgressBar: false,
25 pauseOnHover: false,
26 preventDuplicates: false,
27 preventLastDuplicates: 'visible',
28 rtl: false
29 }
30
31 isMenuDisplayed = true 18 isMenuDisplayed = true
32 isMenuChangedByUser = false 19 isMenuChangedByUser = false
33 20
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index 371199442..0bbc2e08b 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -12,13 +12,12 @@ import { AppComponent } from './app.component'
12import { CoreModule } from './core' 12import { CoreModule } from './core'
13import { HeaderComponent } from './header' 13import { HeaderComponent } from './header'
14import { LoginModule } from './login' 14import { LoginModule } from './login'
15import { MenuComponent } from './menu' 15import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu'
16import { SharedModule } from './shared' 16import { SharedModule } from './shared'
17import { SignupModule } from './signup' 17import { SignupModule } from './signup'
18import { VideosModule } from './videos' 18import { VideosModule } from './videos'
19import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' 19import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n'
20import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' 20import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
21import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
22import { SearchModule } from '@app/search' 21import { SearchModule } from '@app/search'
23 22
24export function metaFactory (serverService: ServerService): MetaLoader { 23export function metaFactory (serverService: ServerService): MetaLoader {
@@ -40,6 +39,7 @@ export function metaFactory (serverService: ServerService): MetaLoader {
40 39
41 MenuComponent, 40 MenuComponent,
42 LanguageChooserComponent, 41 LanguageChooserComponent,
42 AvatarNotificationComponent,
43 HeaderComponent 43 HeaderComponent
44 ], 44 ],
45 imports: [ 45 imports: [
diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts
index acd13d9c5..abb11fdc2 100644
--- a/client/src/app/core/auth/auth-user.model.ts
+++ b/client/src/app/core/auth/auth-user.model.ts
@@ -1,8 +1,9 @@
1import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' 1import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
2import { UserRight } from '../../../../../shared/models/users/user-right.enum' 2import { UserRight } from '../../../../../shared/models/users/user-right.enum'
3import { User as ServerUserModel } from '../../../../../shared/models/users/user.model'
3// Do not use the barrel (dependency loop) 4// Do not use the barrel (dependency loop)
4import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role' 5import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role'
5import { User, UserConstructorHash } from '../../shared/users/user.model' 6import { User } from '../../shared/users/user.model'
6import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' 7import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type'
7 8
8export type TokenOptions = { 9export type TokenOptions = {
@@ -70,6 +71,7 @@ export class AuthUser extends User {
70 ID: 'id', 71 ID: 'id',
71 ROLE: 'role', 72 ROLE: 'role',
72 EMAIL: 'email', 73 EMAIL: 'email',
74 VIDEOS_HISTORY_ENABLED: 'videos-history-enabled',
73 USERNAME: 'username', 75 USERNAME: 'username',
74 NSFW_POLICY: 'nsfw_policy', 76 NSFW_POLICY: 'nsfw_policy',
75 WEBTORRENT_ENABLED: 'peertube-videojs-' + 'webtorrent_enabled', 77 WEBTORRENT_ENABLED: 'peertube-videojs-' + 'webtorrent_enabled',
@@ -89,7 +91,8 @@ export class AuthUser extends User {
89 role: parseInt(peertubeLocalStorage.getItem(this.KEYS.ROLE), 10) as UserRole, 91 role: parseInt(peertubeLocalStorage.getItem(this.KEYS.ROLE), 10) as UserRole,
90 nsfwPolicy: peertubeLocalStorage.getItem(this.KEYS.NSFW_POLICY) as NSFWPolicyType, 92 nsfwPolicy: peertubeLocalStorage.getItem(this.KEYS.NSFW_POLICY) as NSFWPolicyType,
91 webTorrentEnabled: peertubeLocalStorage.getItem(this.KEYS.WEBTORRENT_ENABLED) === 'true', 93 webTorrentEnabled: peertubeLocalStorage.getItem(this.KEYS.WEBTORRENT_ENABLED) === 'true',
92 autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true' 94 autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true',
95 videosHistoryEnabled: peertubeLocalStorage.getItem(this.KEYS.VIDEOS_HISTORY_ENABLED) === 'true'
93 }, 96 },
94 Tokens.load() 97 Tokens.load()
95 ) 98 )
@@ -104,12 +107,13 @@ export class AuthUser extends User {
104 peertubeLocalStorage.removeItem(this.KEYS.ROLE) 107 peertubeLocalStorage.removeItem(this.KEYS.ROLE)
105 peertubeLocalStorage.removeItem(this.KEYS.NSFW_POLICY) 108 peertubeLocalStorage.removeItem(this.KEYS.NSFW_POLICY)
106 peertubeLocalStorage.removeItem(this.KEYS.WEBTORRENT_ENABLED) 109 peertubeLocalStorage.removeItem(this.KEYS.WEBTORRENT_ENABLED)
110 peertubeLocalStorage.removeItem(this.KEYS.VIDEOS_HISTORY_ENABLED)
107 peertubeLocalStorage.removeItem(this.KEYS.AUTO_PLAY_VIDEO) 111 peertubeLocalStorage.removeItem(this.KEYS.AUTO_PLAY_VIDEO)
108 peertubeLocalStorage.removeItem(this.KEYS.EMAIL) 112 peertubeLocalStorage.removeItem(this.KEYS.EMAIL)
109 Tokens.flush() 113 Tokens.flush()
110 } 114 }
111 115
112 constructor (userHash: UserConstructorHash, hashTokens: TokenOptions) { 116 constructor (userHash: Partial<ServerUserModel>, hashTokens: TokenOptions) {
113 super(userHash) 117 super(userHash)
114 this.tokens = new Tokens(hashTokens) 118 this.tokens = new Tokens(hashTokens)
115 } 119 }
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts
index 443772c9e..eaa822e0f 100644
--- a/client/src/app/core/auth/auth.service.ts
+++ b/client/src/app/core/auth/auth.service.ts
@@ -3,18 +3,18 @@ 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 { NotificationsService } from 'angular2-notifications' 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'
15import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' 15import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
16import { I18n } from '@ngx-translate/i18n-polyfill' 16import { I18n } from '@ngx-translate/i18n-polyfill'
17import { HotkeysService, Hotkey } from 'angular2-hotkeys' 17import { Hotkey, HotkeysService } from 'angular2-hotkeys'
18 18
19interface UserLoginWithUsername extends UserLogin { 19interface UserLoginWithUsername extends UserLogin {
20 access_token: string 20 access_token: string
@@ -38,7 +38,6 @@ export class AuthService {
38 loginChangedSource: Observable<AuthStatus> 38 loginChangedSource: Observable<AuthStatus>
39 userInformationLoaded = new ReplaySubject<boolean>(1) 39 userInformationLoaded = new ReplaySubject<boolean>(1)
40 hotkeys: Hotkey[] 40 hotkeys: Hotkey[]
41 redirectUrl: string
42 41
43 private clientId: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID) 42 private clientId: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID)
44 private clientSecret: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET) 43 private clientSecret: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET)
@@ -48,7 +47,7 @@ export class AuthService {
48 47
49 constructor ( 48 constructor (
50 private http: HttpClient, 49 private http: HttpClient,
51 private notificationsService: NotificationsService, 50 private notifier: Notifier,
52 private hotkeysService: HotkeysService, 51 private hotkeysService: HotkeysService,
53 private restExtractor: RestExtractor, 52 private restExtractor: RestExtractor,
54 private router: Router, 53 private router: Router,
@@ -106,9 +105,8 @@ export class AuthService {
106 ) 105 )
107 } 106 }
108 107
109 // We put a bigger timeout 108 // We put a bigger timeout: this is an important message
110 // This is an important message 109 this.notifier.error(errorMessage, this.i18n('Error'), 7000)
111 this.notificationsService.error(this.i18n('Error'), errorMessage, { timeOut: 7000 })
112 } 110 }
113 ) 111 )
114 } 112 }
@@ -178,8 +176,6 @@ export class AuthService {
178 this.setStatus(AuthStatus.LoggedOut) 176 this.setStatus(AuthStatus.LoggedOut)
179 177
180 this.hotkeysService.remove(this.hotkeys) 178 this.hotkeysService.remove(this.hotkeys)
181
182 this.redirectUrl = null
183 } 179 }
184 180
185 refreshAccessToken () { 181 refreshAccessToken () {
diff --git a/client/src/app/core/auth/index.ts b/client/src/app/core/auth/index.ts
index bc7bfec0e..8e5caa7ed 100644
--- a/client/src/app/core/auth/index.ts
+++ b/client/src/app/core/auth/index.ts
@@ -1,4 +1,3 @@
1export * from './auth-status.model' 1export * from './auth-status.model'
2export * from './auth-user.model' 2export * from './auth-user.model'
3export * from './auth.service' 3export * from './auth.service'
4export * from '../routing/login-guard.service'
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 df2ec696d..4ef3b1e73 100644
--- a/client/src/app/core/core.module.ts
+++ b/client/src/app/core/core.module.ts
@@ -7,16 +7,18 @@ import { LoadingBarModule } from '@ngx-loading-bar/core'
7import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' 7import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
8import { LoadingBarRouterModule } from '@ngx-loading-bar/router' 8import { LoadingBarRouterModule } from '@ngx-loading-bar/router'
9 9
10import { SimpleNotificationsModule } from 'angular2-notifications'
11
12import { AuthService } from './auth' 10import { AuthService } from './auth'
13import { ConfirmComponent, ConfirmService } from './confirm' 11import { ConfirmService } from './confirm'
14import { throwIfAlreadyLoaded } from './module-import-guard' 12import { throwIfAlreadyLoaded } from './module-import-guard'
15import { LoginGuard, RedirectService, UserRightGuard } from './routing' 13import { LoginGuard, RedirectService, UserRightGuard } from './routing'
16import { ServerService } from './server' 14import { ServerService } from './server'
17import { ThemeService } from './theme' 15import { ThemeService } from './theme'
18import { HotkeyModule } from 'angular2-hotkeys' 16import { HotkeyModule } from 'angular2-hotkeys'
19import { CheatSheetComponent } from '@app/core/hotkeys' 17import { CheatSheetComponent } from './hotkeys'
18import { ToastModule } from 'primeng/toast'
19import { Notifier } from './notification'
20import { MessageService } from 'primeng/api'
21import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service'
20 22
21@NgModule({ 23@NgModule({
22 imports: [ 24 imports: [
@@ -25,11 +27,10 @@ import { CheatSheetComponent } from '@app/core/hotkeys'
25 FormsModule, 27 FormsModule,
26 BrowserAnimationsModule, 28 BrowserAnimationsModule,
27 29
28 SimpleNotificationsModule.forRoot(),
29
30 LoadingBarHttpClientModule, 30 LoadingBarHttpClientModule,
31 LoadingBarRouterModule, 31 LoadingBarRouterModule,
32 LoadingBarModule.forRoot(), 32 LoadingBarModule,
33 ToastModule,
33 34
34 HotkeyModule.forRoot({ 35 HotkeyModule.forRoot({
35 cheatSheetCloseEsc: true 36 cheatSheetCloseEsc: true
@@ -37,16 +38,15 @@ import { CheatSheetComponent } from '@app/core/hotkeys'
37 ], 38 ],
38 39
39 declarations: [ 40 declarations: [
40 ConfirmComponent,
41 CheatSheetComponent 41 CheatSheetComponent
42 ], 42 ],
43 43
44 exports: [ 44 exports: [
45 SimpleNotificationsModule,
46 LoadingBarHttpClientModule, 45 LoadingBarHttpClientModule,
47 LoadingBarModule, 46 LoadingBarModule,
48 47
49 ConfirmComponent, 48 ToastModule,
49
50 CheatSheetComponent 50 CheatSheetComponent
51 ], 51 ],
52 52
@@ -57,7 +57,10 @@ import { CheatSheetComponent } from '@app/core/hotkeys'
57 ThemeService, 57 ThemeService,
58 LoginGuard, 58 LoginGuard,
59 UserRightGuard, 59 UserRightGuard,
60 RedirectService 60 RedirectService,
61 Notifier,
62 MessageService,
63 UserNotificationSocket
61 ] 64 ]
62}) 65})
63export class CoreModule { 66export class CoreModule {
diff --git a/client/src/app/core/index.ts b/client/src/app/core/index.ts
index 524589d74..f664aff41 100644
--- a/client/src/app/core/index.ts
+++ b/client/src/app/core/index.ts
@@ -2,6 +2,7 @@ export * from './auth'
2export * from './confirm' 2export * from './confirm'
3export * from './routing' 3export * from './routing'
4export * from './server' 4export * from './server'
5export * from './notification'
5export * from './theme' 6export * from './theme'
6 7
7export * from './core.module' 8export * from './core.module'
diff --git a/client/src/app/core/notification/index.ts b/client/src/app/core/notification/index.ts
new file mode 100644
index 000000000..3e8d9ea65
--- /dev/null
+++ b/client/src/app/core/notification/index.ts
@@ -0,0 +1,2 @@
1export * from './notifier.service'
2export * from './user-notification-socket.service'
diff --git a/client/src/app/core/notification/notifier.service.ts b/client/src/app/core/notification/notifier.service.ts
new file mode 100644
index 000000000..9833c65a0
--- /dev/null
+++ b/client/src/app/core/notification/notifier.service.ts
@@ -0,0 +1,41 @@
1import { Injectable } from '@angular/core'
2import { MessageService } from 'primeng/api'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4
5@Injectable()
6export class Notifier {
7 readonly TIMEOUT = 5000
8
9 constructor (
10 private i18n: I18n,
11 private messageService: MessageService) {
12 }
13
14 info (text: string, title?: string, timeout?: number) {
15 if (!title) title = this.i18n('Info')
16
17 return this.notify('info', text, title, timeout)
18 }
19
20 error (text: string, title?: string, timeout?: number) {
21 if (!title) title = this.i18n('Error')
22
23 return this.notify('error', text, title, timeout)
24 }
25
26 success (text: string, title?: string, timeout?: number) {
27 if (!title) title = this.i18n('Success')
28
29 return this.notify('success', text, title, timeout)
30 }
31
32 private notify (severity: 'success' | 'info' | 'warn' | 'error', text: string, title: string, timeout?: number) {
33 this.messageService.add({
34 severity,
35 summary: title,
36 detail: text,
37 closable: true,
38 life: timeout || this.TIMEOUT
39 })
40 }
41}
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/routing/login-guard.service.ts b/client/src/app/core/routing/login-guard.service.ts
index 40ff8f505..7b1c37ee8 100644
--- a/client/src/app/core/routing/login-guard.service.ts
+++ b/client/src/app/core/routing/login-guard.service.ts
@@ -1,11 +1,5 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { 2import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router'
3 ActivatedRouteSnapshot,
4 CanActivateChild,
5 RouterStateSnapshot,
6 CanActivate,
7 Router
8} from '@angular/router'
9 3
10import { AuthService } from '../auth/auth.service' 4import { AuthService } from '../auth/auth.service'
11 5
@@ -20,8 +14,6 @@ export class LoginGuard implements CanActivate, CanActivateChild {
20 canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 14 canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
21 if (this.auth.isLoggedIn() === true) return true 15 if (this.auth.isLoggedIn() === true) return true
22 16
23 this.auth.redirectUrl = state.url
24
25 this.router.navigate([ '/login' ]) 17 this.router.navigate([ '/login' ])
26 return false 18 return false
27 } 19 }
diff --git a/client/src/app/core/routing/redirect.service.ts b/client/src/app/core/routing/redirect.service.ts
index 1881be117..e1db4097b 100644
--- a/client/src/app/core/routing/redirect.service.ts
+++ b/client/src/app/core/routing/redirect.service.ts
@@ -1,5 +1,5 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { Router } from '@angular/router' 2import { NavigationEnd, Router } from '@angular/router'
3import { ServerService } from '../server' 3import { ServerService } from '../server'
4 4
5@Injectable() 5@Injectable()
@@ -8,6 +8,9 @@ export class RedirectService {
8 static INIT_DEFAULT_ROUTE = '/videos/trending' 8 static INIT_DEFAULT_ROUTE = '/videos/trending'
9 static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE 9 static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
10 10
11 private previousUrl: string
12 private currentUrl: string
13
11 constructor ( 14 constructor (
12 private router: Router, 15 private router: Router,
13 private serverService: ServerService 16 private serverService: ServerService
@@ -18,6 +21,7 @@ export class RedirectService {
18 RedirectService.DEFAULT_ROUTE = config.instance.defaultClientRoute 21 RedirectService.DEFAULT_ROUTE = config.instance.defaultClientRoute
19 } 22 }
20 23
24 // Load default route
21 this.serverService.configLoaded 25 this.serverService.configLoaded
22 .subscribe(() => { 26 .subscribe(() => {
23 const defaultRouteConfig = this.serverService.getConfig().instance.defaultClientRoute 27 const defaultRouteConfig = this.serverService.getConfig().instance.defaultClientRoute
@@ -26,6 +30,21 @@ export class RedirectService {
26 RedirectService.DEFAULT_ROUTE = defaultRouteConfig 30 RedirectService.DEFAULT_ROUTE = defaultRouteConfig
27 } 31 }
28 }) 32 })
33
34 // Track previous url
35 this.currentUrl = this.router.url
36 router.events.subscribe(event => {
37 if (event instanceof NavigationEnd) {
38 this.previousUrl = this.currentUrl
39 this.currentUrl = event.url
40 }
41 })
42 }
43
44 redirectToPreviousRoute () {
45 if (this.previousUrl) return this.router.navigateByUrl(this.previousUrl)
46
47 return this.redirectToHomepage()
29 } 48 }
30 49
31 redirectToHomepage (skipLocationChange = false) { 50 redirectToHomepage (skipLocationChange = false) {
diff --git a/client/src/app/core/routing/user-right-guard.service.ts b/client/src/app/core/routing/user-right-guard.service.ts
index 65d029977..50c3d8c19 100644
--- a/client/src/app/core/routing/user-right-guard.service.ts
+++ b/client/src/app/core/routing/user-right-guard.service.ts
@@ -7,7 +7,7 @@ import {
7 Router 7 Router
8} from '@angular/router' 8} from '@angular/router'
9 9
10import { AuthService } from '../auth' 10import { AuthService } from '../auth/auth.service'
11 11
12@Injectable() 12@Injectable()
13export class UserRightGuard implements CanActivate, CanActivateChild { 13export class UserRightGuard implements CanActivate, CanActivateChild {
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index da8bd26db..4ae72427b 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -13,6 +13,7 @@ import { sortBy } from '@app/shared/misc/utils'
13 13
14@Injectable() 14@Injectable()
15export class ServerService { 15export class ServerService {
16 private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server/'
16 private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/' 17 private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/'
17 private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' 18 private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
18 private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' 19 private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
@@ -37,6 +38,12 @@ export class ServerService {
37 css: '' 38 css: ''
38 } 39 }
39 }, 40 },
41 email: {
42 enabled: false
43 },
44 contactForm: {
45 enabled: false
46 },
40 serverVersion: 'Unknown', 47 serverVersion: 'Unknown',
41 signup: { 48 signup: {
42 allowed: false, 49 allowed: false,
@@ -80,6 +87,11 @@ export class ServerService {
80 enabled: false 87 enabled: false
81 } 88 }
82 } 89 }
90 },
91 trending: {
92 videos: {
93 intervalDays: 0
94 }
83 } 95 }
84 } 96 }
85 private videoCategories: Array<VideoConstant<number>> = [] 97 private videoCategories: Array<VideoConstant<number>> = []
@@ -141,10 +153,6 @@ export class ServerService {
141 return this.videoPrivacies 153 return this.videoPrivacies
142 } 154 }
143 155
144 getAbout () {
145 return this.http.get<About>(ServerService.BASE_CONFIG_URL + '/about')
146 }
147
148 private loadVideoAttributeEnum ( 156 private loadVideoAttributeEnum (
149 attributeName: 'categories' | 'licences' | 'languages' | 'privacies', 157 attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
150 hashToPopulate: VideoConstant<string | number>[], 158 hashToPopulate: VideoConstant<string | 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 93dbed525..4efe3fb22 100644
--- a/client/src/app/login/login.component.html
+++ b/client/src/app/login/login.component.html
@@ -55,11 +55,17 @@
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">
62 <div class="form-group"> 63
64 <div *ngIf="isEmailDisabled()" class="alert alert-danger" i18n>
65 We are sorry, you cannot recover you password because your instance administrator did not configure the PeerTube email system.
66 </div>
67
68 <div class="form-group" [hidden]="isEmailDisabled()">
63 <label i18n for="forgot-password-email">Email</label> 69 <label i18n for="forgot-password-email">Email</label>
64 <input 70 <input
65 type="email" id="forgot-password-email" i18n-placeholder placeholder="Email address" required 71 type="email" id="forgot-password-email" i18n-placeholder placeholder="Email address" required
diff --git a/client/src/app/login/login.component.ts b/client/src/app/login/login.component.ts
index 7553e6456..fc2442c0e 100644
--- a/client/src/app/login/login.component.ts
+++ b/client/src/app/login/login.component.ts
@@ -1,7 +1,6 @@
1import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' 1import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
2import { RedirectService, ServerService } from '@app/core' 2import { Notifier, RedirectService, ServerService } from '@app/core'
3import { UserService } from '@app/shared' 3import { UserService } from '@app/shared'
4import { NotificationsService } from 'angular2-notifications'
5import { AuthService } from '../core' 4import { AuthService } from '../core'
6import { FormReactive } from '../shared' 5import { FormReactive } from '../shared'
7import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -19,7 +18,6 @@ import { Router } from '@angular/router'
19export class LoginComponent extends FormReactive implements OnInit { 18export class LoginComponent extends FormReactive implements OnInit {
20 @ViewChild('emailInput') input: ElementRef 19 @ViewChild('emailInput') input: ElementRef
21 @ViewChild('forgotPasswordModal') forgotPasswordModal: ElementRef 20 @ViewChild('forgotPasswordModal') forgotPasswordModal: ElementRef
22 @ViewChild('forgotPasswordEmailInput') forgotPasswordEmailInput: ElementRef
23 21
24 error: string = null 22 error: string = null
25 forgotPasswordEmail = '' 23 forgotPasswordEmail = ''
@@ -35,7 +33,7 @@ export class LoginComponent extends FormReactive implements OnInit {
35 private userService: UserService, 33 private userService: UserService,
36 private serverService: ServerService, 34 private serverService: ServerService,
37 private redirectService: RedirectService, 35 private redirectService: RedirectService,
38 private notificationsService: NotificationsService, 36 private notifier: Notifier,
39 private i18n: I18n 37 private i18n: I18n
40 ) { 38 ) {
41 super() 39 super()
@@ -45,6 +43,10 @@ export class LoginComponent extends FormReactive implements OnInit {
45 return this.serverService.getConfig().signup.allowed === true 43 return this.serverService.getConfig().signup.allowed === true
46 } 44 }
47 45
46 isEmailDisabled () {
47 return this.serverService.getConfig().email.enabled === false
48 }
49
48 ngOnInit () { 50 ngOnInit () {
49 this.buildForm({ 51 this.buildForm({
50 username: this.loginValidatorsService.LOGIN_USERNAME, 52 username: this.loginValidatorsService.LOGIN_USERNAME,
@@ -61,7 +63,7 @@ export class LoginComponent extends FormReactive implements OnInit {
61 63
62 this.authService.login(username, password) 64 this.authService.login(username, password)
63 .subscribe( 65 .subscribe(
64 () => this.redirect(), 66 () => this.redirectService.redirectToPreviousRoute(),
65 67
66 err => { 68 err => {
67 if (err.message.indexOf('credentials are invalid') !== -1) this.error = this.i18n('Incorrect username or password.') 69 if (err.message.indexOf('credentials are invalid') !== -1) this.error = this.i18n('Incorrect username or password.')
@@ -71,15 +73,6 @@ export class LoginComponent extends FormReactive implements OnInit {
71 ) 73 )
72 } 74 }
73 75
74 redirect () {
75 const redirect = this.authService.redirectUrl
76 if (redirect) {
77 this.router.navigate([ redirect ])
78 } else {
79 this.redirectService.redirectToHomepage()
80 }
81 }
82
83 askResetPassword () { 76 askResetPassword () {
84 this.userService.askResetPassword(this.forgotPasswordEmail) 77 this.userService.askResetPassword(this.forgotPasswordEmail)
85 .subscribe( 78 .subscribe(
@@ -88,18 +81,14 @@ export class LoginComponent extends FormReactive implements OnInit {
88 'An email with the reset password instructions will be sent to {{email}}.', 81 'An email with the reset password instructions will be sent to {{email}}.',
89 { email: this.forgotPasswordEmail } 82 { email: this.forgotPasswordEmail }
90 ) 83 )
91 this.notificationsService.success(this.i18n('Success'), message) 84 this.notifier.success(message)
92 this.hideForgotPasswordModal() 85 this.hideForgotPasswordModal()
93 }, 86 },
94 87
95 err => this.notificationsService.error(this.i18n('Error'), err.message) 88 err => this.notifier.error(err.message)
96 ) 89 )
97 } 90 }
98 91
99 onForgotPasswordModalShown () {
100 this.forgotPasswordEmailInput.nativeElement.focus()
101 }
102
103 openForgotPasswordModal () { 92 openForgotPasswordModal () {
104 this.openedForgotPasswordModal = this.modalService.open(this.forgotPasswordModal) 93 this.openedForgotPasswordModal = this.modalService.open(this.forgotPasswordModal)
105 } 94 }
diff --git a/client/src/app/menu/avatar-notification.component.html b/client/src/app/menu/avatar-notification.component.html
new file mode 100644
index 000000000..4ef3f0e89
--- /dev/null
+++ b/client/src/app/menu/avatar-notification.component.html
@@ -0,0 +1,23 @@
1<div
2 [ngbPopover]="popContent" autoClose="outside" placement="bottom-left" container="body" popoverClass="popover-notifications"
3 i18n-title title="View your notifications" class="notification-avatar" #popover="ngbPopover"
4>
5 <div *ngIf="unreadNotifications > 0" class="unread-notifications">{{ unreadNotifications }}</div>
6
7 <img [src]="user.accountAvatarUrl" alt="Avatar" />
8</div>
9
10<ng-template #popContent>
11 <div class="notifications-header">
12 <div i18n>Notifications</div>
13
14 <a
15 i18n-title title="Update your notification preferences" class="glyphicon glyphicon-cog"
16 routerLink="/my-account/settings" fragment="notifications"
17 ></a>
18 </div>
19
20 <my-user-notifications [ignoreLoadingBar]="true" [infiniteScroll]="false" itemsPerPage="10"></my-user-notifications>
21
22 <a class="all-notifications" routerLink="/my-account/notifications" i18n>See all your notifications</a>
23</ng-template>
diff --git a/client/src/app/menu/avatar-notification.component.scss b/client/src/app/menu/avatar-notification.component.scss
new file mode 100644
index 000000000..e785db788
--- /dev/null
+++ b/client/src/app/menu/avatar-notification.component.scss
@@ -0,0 +1,91 @@
1@import '_variables';
2@import '_mixins';
3
4/deep/ {
5 .popover-notifications.popover {
6 max-width: none;
7
8 .popover-body {
9 padding: 0;
10 font-size: 14px;
11 font-family: $main-fonts;
12 overflow-y: auto;
13 max-height: 500px;
14 width: 400px;
15 box-shadow: 0 6px 14px rgba(0, 0, 0, 0.30);
16
17 .notifications-header {
18 display: flex;
19 justify-content: space-between;
20
21 background-color: rgba(0, 0, 0, 0.10);
22 align-items: center;
23 padding: 0 10px;
24 font-size: 16px;
25 height: 50px;
26
27 a {
28 @include disable-default-a-behaviour;
29
30 color: rgba(20, 20, 20, 0.5);
31
32 &:hover {
33 color: rgba(20, 20, 20, 0.8);
34 }
35 }
36 }
37
38 .all-notifications {
39 display: flex;
40 align-items: center;
41 justify-content: center;
42 font-weight: $font-semibold;
43 color: var(--mainForegroundColor);
44 padding: 7px 0;
45 }
46 }
47 }
48}
49
50.notification-avatar {
51 cursor: pointer;
52 position: relative;
53
54 img,
55 .unread-notifications {
56 margin-left: 20px;
57 }
58
59 img {
60 @include avatar(34px);
61
62 margin-right: 10px;
63 }
64
65 .unread-notifications {
66 position: absolute;
67 top: -5px;
68 left: -5px;
69
70 display: flex;
71 align-items: center;
72 justify-content: center;
73
74 background-color: var(--mainColor);
75 color: var(#fff);
76 font-size: 10px;
77 font-weight: $font-semibold;
78
79 border-radius: 15px;
80 width: 15px;
81 height: 15px;
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
new file mode 100644
index 000000000..f1af08096
--- /dev/null
+++ b/client/src/app/menu/avatar-notification.component.ts
@@ -0,0 +1,65 @@
1import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'
2import { User } from '../shared/users/user.model'
3import { UserNotificationService } from '@app/shared/users/user-notification.service'
4import { Subscription } from 'rxjs'
5import { Notifier, UserNotificationSocket } from '@app/core'
6import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
7import { NavigationEnd, Router } from '@angular/router'
8import { filter } from 'rxjs/operators'
9
10@Component({
11 selector: 'my-avatar-notification',
12 templateUrl: './avatar-notification.component.html',
13 styleUrls: [ './avatar-notification.component.scss' ]
14})
15export class AvatarNotificationComponent implements OnInit, OnDestroy {
16 @ViewChild('popover') popover: NgbPopover
17 @Input() user: User
18
19 unreadNotifications = 0
20
21 private notificationSub: Subscription
22 private routeSub: Subscription
23
24 constructor (
25 private userNotificationService: UserNotificationService,
26 private userNotificationSocket: UserNotificationSocket,
27 private notifier: Notifier,
28 private router: Router
29 ) {}
30
31 ngOnInit () {
32 this.userNotificationService.countUnreadNotifications()
33 .subscribe(
34 result => {
35 this.unreadNotifications = Math.min(result, 99) // Limit number to 99
36 this.subscribeToNotifications()
37 },
38
39 err => this.notifier.error(err.message)
40 )
41
42 this.routeSub = this.router.events
43 .pipe(filter(event => event instanceof NavigationEnd))
44 .subscribe(() => this.closePopover())
45 }
46
47 ngOnDestroy () {
48 if (this.notificationSub) this.notificationSub.unsubscribe()
49 if (this.routeSub) this.routeSub.unsubscribe()
50 }
51
52 closePopover () {
53 this.popover.close()
54 }
55
56 private subscribeToNotifications () {
57 this.notificationSub = this.userNotificationSocket.getMyNotificationsSocket()
58 .subscribe(data => {
59 if (data.type === 'new') return this.unreadNotifications++
60 if (data.type === 'read') return this.unreadNotifications--
61 if (data.type === 'read-all') return this.unreadNotifications = 0
62 })
63 }
64
65}
diff --git a/client/src/app/menu/index.ts b/client/src/app/menu/index.ts
index 421271c12..39dbde750 100644
--- a/client/src/app/menu/index.ts
+++ b/client/src/app/menu/index.ts
@@ -1 +1,3 @@
1export * from './language-chooser.component'
2export * from './avatar-notification.component'
1export * from './menu.component' 3export * from './menu.component'
diff --git a/client/src/app/menu/language-chooser.component.html b/client/src/app/menu/language-chooser.component.html
index c37bf2826..a62b33dda 100644
--- a/client/src/app/menu/language-chooser.component.html
+++ b/client/src/app/menu/language-chooser.component.html
@@ -1,9 +1,14 @@
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
8 <a i18n class="help-to-translate" target="_blank" rel="noreferrer noopener" href="https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/translation.md">
9 Help to translate PeerTube!
10 </a>
11
7 <div class="modal-body"> 12 <div class="modal-body">
8 <a *ngFor="let lang of languages" [href]="buildLanguageLink(lang)">{{ lang.label }}</a> 13 <a *ngFor="let lang of languages" [href]="buildLanguageLink(lang)">{{ lang.label }}</a>
9 </div> 14 </div>
diff --git a/client/src/app/menu/language-chooser.component.scss b/client/src/app/menu/language-chooser.component.scss
index 944e86f46..72deb3952 100644
--- a/client/src/app/menu/language-chooser.component.scss
+++ b/client/src/app/menu/language-chooser.component.scss
@@ -1,6 +1,11 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4.help-to-translate {
5 @include peertube-button-link;
6 @include orange-button;
7}
8
4.modal-body { 9.modal-body {
5 text-align: center; 10 text-align: center;
6 11
@@ -9,4 +14,4 @@
9 font-size: 16px; 14 font-size: 16px;
10 margin: 15px; 15 margin: 15px;
11 } 16 }
12} \ No newline at end of file 17}
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index e04bdf3d6..aa5bfa9c9 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -2,9 +2,7 @@
2 <menu> 2 <menu>
3 <div class="top-menu"> 3 <div class="top-menu">
4 <div *ngIf="isLoggedIn" class="logged-in-block"> 4 <div *ngIf="isLoggedIn" class="logged-in-block">
5 <a routerLink="/my-account/settings"> 5 <my-avatar-notification [user]="user"></my-avatar-notification>
6 <img [src]="user.accountAvatarUrl" alt="Avatar" />
7 </a>
8 6
9 <div class="logged-in-info"> 7 <div class="logged-in-info">
10 <a routerLink="/my-account/settings" class="logged-in-username">{{ user.account?.displayName }}</a> 8 <a routerLink="/my-account/settings" class="logged-in-username">{{ user.account?.displayName }}</a>
@@ -97,4 +95,4 @@
97 </menu> 95 </menu>
98</div> 96</div>
99 97
100<my-language-chooser #languageChooserModal></my-language-chooser> \ No newline at end of file 98<my-language-chooser #languageChooserModal></my-language-chooser>
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss
index b271ebfd2..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;
@@ -39,13 +39,6 @@ menu {
39 justify-content: center; 39 justify-content: center;
40 margin-bottom: 35px; 40 margin-bottom: 35px;
41 41
42 img {
43 @include avatar(34px);
44
45 margin-left: 20px;
46 margin-right: 10px;
47 }
48
49 .logged-in-info { 42 .logged-in-info {
50 flex-grow: 1; 43 flex-grow: 1;
51 44
@@ -250,7 +243,7 @@ menu {
250 } 243 }
251} 244}
252 245
253@media screen and (max-width: 400px) { 246@media screen and (max-width: $mobile-view) {
254 .menu-wrapper { 247 .menu-wrapper {
255 width: 100% !important; 248 width: 100% !important;
256 } 249 }
diff --git a/client/src/app/reset-password/reset-password.component.ts b/client/src/app/reset-password/reset-password.component.ts
index af1298de6..07b93ee73 100644
--- a/client/src/app/reset-password/reset-password.component.ts
+++ b/client/src/app/reset-password/reset-password.component.ts
@@ -1,8 +1,7 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { UserService, UserValidatorsService } from '@app/shared' 3import { UserService, UserValidatorsService, FormReactive } from '@app/shared'
4import { NotificationsService } from 'angular2-notifications' 4import { Notifier } from '@app/core'
5import { FormReactive } from '../shared'
6import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
7import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
8import { ResetPasswordValidatorsService } from '@app/shared/forms/form-validators/reset-password-validators.service' 7import { ResetPasswordValidatorsService } from '@app/shared/forms/form-validators/reset-password-validators.service'
@@ -22,7 +21,7 @@ export class ResetPasswordComponent extends FormReactive implements OnInit {
22 private resetPasswordValidatorsService: ResetPasswordValidatorsService, 21 private resetPasswordValidatorsService: ResetPasswordValidatorsService,
23 private userValidatorsService: UserValidatorsService, 22 private userValidatorsService: UserValidatorsService,
24 private userService: UserService, 23 private userService: UserService,
25 private notificationsService: NotificationsService, 24 private notifier: Notifier,
26 private router: Router, 25 private router: Router,
27 private route: ActivatedRoute, 26 private route: ActivatedRoute,
28 private i18n: I18n 27 private i18n: I18n
@@ -40,7 +39,7 @@ export class ResetPasswordComponent extends FormReactive implements OnInit {
40 this.verificationString = this.route.snapshot.queryParams['verificationString'] 39 this.verificationString = this.route.snapshot.queryParams['verificationString']
41 40
42 if (!this.userId || !this.verificationString) { 41 if (!this.userId || !this.verificationString) {
43 this.notificationsService.error(this.i18n('Error'), this.i18n('Unable to find user id or verification string.')) 42 this.notifier.error(this.i18n('Unable to find user id or verification string.'))
44 this.router.navigate([ '/' ]) 43 this.router.navigate([ '/' ])
45 } 44 }
46 } 45 }
@@ -49,11 +48,11 @@ export class ResetPasswordComponent extends FormReactive implements OnInit {
49 this.userService.resetPassword(this.userId, this.verificationString, this.form.value.password) 48 this.userService.resetPassword(this.userId, this.verificationString, this.form.value.password)
50 .subscribe( 49 .subscribe(
51 () => { 50 () => {
52 this.notificationsService.success(this.i18n('Success'), this.i18n('Your password has been successfully reset!')) 51 this.notifier.success(this.i18n('Your password has been successfully reset!'))
53 this.router.navigate([ '/login' ]) 52 this.router.navigate([ '/login' ])
54 }, 53 },
55 54
56 err => this.notificationsService.error('Error', err.message) 55 err => this.notifier.error(err.message)
57 ) 56 )
58 } 57 }
59 58
diff --git a/client/src/app/search/search-filters.component.ts b/client/src/app/search/search-filters.component.ts
index 8d7f84ac1..3fdc6df35 100644
--- a/client/src/app/search/search-filters.component.ts
+++ b/client/src/app/search/search-filters.component.ts
@@ -1,10 +1,6 @@
1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
2import { ActivatedRoute } from '@angular/router' 2import { ServerService } from '@app/core'
3import { RedirectService, ServerService } from '@app/core'
4import { NotificationsService } from 'angular2-notifications'
5import { SearchService } from '@app/search/search.service'
6import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
7import { MetaService } from '@ngx-meta/core'
8import { AdvancedSearch } from '@app/search/advanced-search.model' 4import { AdvancedSearch } from '@app/search/advanced-search.model'
9import { VideoConstant } from '../../../../shared' 5import { VideoConstant } from '../../../../shared'
10 6
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 3d17e6d96..c4a4b1fde 100644
--- a/client/src/app/search/search.component.ts
+++ b/client/src/app/search/search.component.ts
@@ -1,7 +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 } from '@app/core' 3import { AuthService, Notifier, ServerService } from '@app/core'
4import { NotificationsService } from 'angular2-notifications'
5import { forkJoin, Subscription } from 'rxjs' 4import { forkJoin, Subscription } from 'rxjs'
6import { SearchService } from '@app/search/search.service' 5import { SearchService } from '@app/search/search.service'
7import { ComponentPagination } from '@app/shared/rest/component-pagination.model' 6import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
@@ -40,9 +39,10 @@ export class SearchComponent implements OnInit, OnDestroy {
40 private route: ActivatedRoute, 39 private route: ActivatedRoute,
41 private router: Router, 40 private router: Router,
42 private metaService: MetaService, 41 private metaService: MetaService,
43 private notificationsService: NotificationsService, 42 private notifier: Notifier,
44 private searchService: SearchService, 43 private searchService: SearchService,
45 private authService: AuthService 44 private authService: AuthService,
45 private serverService: ServerService
46 ) { } 46 ) { }
47 47
48 ngOnInit () { 48 ngOnInit () {
@@ -68,7 +68,7 @@ export class SearchComponent implements OnInit, OnDestroy {
68 this.search() 68 this.search()
69 }, 69 },
70 70
71 err => this.notificationsService.error('Error', err.text) 71 err => this.notifier.error(err.text)
72 ) 72 )
73 } 73 }
74 74
@@ -76,6 +76,10 @@ export class SearchComponent implements OnInit, OnDestroy {
76 if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe() 76 if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe()
77 } 77 }
78 78
79 isVideoBlur (video: Video) {
80 return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig())
81 }
82
79 isVideoChannel (d: VideoChannel | Video): d is VideoChannel { 83 isVideoChannel (d: VideoChannel | Video): d is VideoChannel {
80 return d instanceof VideoChannel 84 return d instanceof VideoChannel
81 } 85 }
@@ -112,9 +116,7 @@ export class SearchComponent implements OnInit, OnDestroy {
112 this.firstSearch = false 116 this.firstSearch = false
113 }, 117 },
114 118
115 error => { 119 err => this.notifier.error(err.message)
116 this.notificationsService.error(this.i18n('Error'), error.message)
117 }
118 ) 120 )
119 121
120 } 122 }
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-reactive.ts b/client/src/app/shared/forms/form-reactive.ts
index 0bb7d25e6..b9873af2c 100644
--- a/client/src/app/shared/forms/form-reactive.ts
+++ b/client/src/app/shared/forms/form-reactive.ts
@@ -1,11 +1,9 @@
1import { FormGroup } from '@angular/forms' 1import { FormGroup } from '@angular/forms'
2import { BuildFormArgument, BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 2import { BuildFormArgument, BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
3 3
4export type FormReactiveErrors = { [ id: string ]: string } 4export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors }
5export type FormReactiveValidationMessages = { 5export type FormReactiveValidationMessages = {
6 [ id: string ]: { 6 [ id: string ]: { [ name: string ]: string } | FormReactiveValidationMessages
7 [ name: string ]: string
8 }
9} 7}
10 8
11export abstract class FormReactive { 9export abstract class FormReactive {
@@ -13,7 +11,7 @@ export abstract class FormReactive {
13 protected formChanged = false 11 protected formChanged = false
14 12
15 form: FormGroup 13 form: FormGroup
16 formErrors: FormReactiveErrors 14 formErrors: any // To avoid casting in template because of string | FormReactiveErrors
17 validationMessages: FormReactiveValidationMessages 15 validationMessages: FormReactiveValidationMessages
18 16
19 buildForm (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) { 17 buildForm (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) {
@@ -23,29 +21,49 @@ export abstract class FormReactive {
23 this.formErrors = formErrors 21 this.formErrors = formErrors
24 this.validationMessages = validationMessages 22 this.validationMessages = validationMessages
25 23
26 this.form.valueChanges.subscribe(() => this.onValueChanged(false)) 24 this.form.valueChanges.subscribe(() => this.onValueChanged(this.form, this.formErrors, this.validationMessages, false))
25 }
26
27 protected forceCheck () {
28 return this.onValueChanged(this.form, this.formErrors, this.validationMessages, true)
29 }
30
31 protected check () {
32 return this.onValueChanged(this.form, this.formErrors, this.validationMessages, false)
27 } 33 }
28 34
29 protected onValueChanged (forceCheck = false) { 35 private onValueChanged (
30 for (const field in this.formErrors) { 36 form: FormGroup,
37 formErrors: FormReactiveErrors,
38 validationMessages: FormReactiveValidationMessages,
39 forceCheck = false
40 ) {
41 for (const field of Object.keys(formErrors)) {
42 if (formErrors[field] && typeof formErrors[field] === 'object') {
43 this.onValueChanged(
44 form.controls[field] as FormGroup,
45 formErrors[field] as FormReactiveErrors,
46 validationMessages[field] as FormReactiveValidationMessages,
47 forceCheck
48 )
49 continue
50 }
51
31 // clear previous error message (if any) 52 // clear previous error message (if any)
32 this.formErrors[ field ] = '' 53 formErrors[ field ] = ''
33 const control = this.form.get(field) 54 const control = form.get(field)
34 55
35 if (control.dirty) this.formChanged = true 56 if (control.dirty) this.formChanged = true
36 57
37 // Don't care if dirty on force check 58 // Don't care if dirty on force check
38 const isDirty = control.dirty || forceCheck === true 59 const isDirty = control.dirty || forceCheck === true
39 if (control && isDirty && !control.valid) { 60 if (control && isDirty && !control.valid) {
40 const messages = this.validationMessages[ field ] 61 const messages = validationMessages[ field ]
41 for (const key in control.errors) { 62 for (const key in control.errors) {
42 this.formErrors[ field ] += messages[ key ] + ' ' 63 formErrors[ field ] += messages[ key ] + ' '
43 } 64 }
44 } 65 }
45 } 66 }
46 } 67 }
47 68
48 protected forceCheck () {
49 return this.onValueChanged(true)
50 }
51} 69}
diff --git a/client/src/app/shared/forms/form-validators/form-validator.service.ts b/client/src/app/shared/forms/form-validators/form-validator.service.ts
index 19a8bef25..249fdf119 100644
--- a/client/src/app/shared/forms/form-validators/form-validator.service.ts
+++ b/client/src/app/shared/forms/form-validators/form-validator.service.ts
@@ -7,10 +7,10 @@ export type BuildFormValidator = {
7 MESSAGES: { [ name: string ]: string } 7 MESSAGES: { [ name: string ]: string }
8} 8}
9export type BuildFormArgument = { 9export type BuildFormArgument = {
10 [ id: string ]: BuildFormValidator 10 [ id: string ]: BuildFormValidator | BuildFormArgument
11} 11}
12export type BuildFormDefaultValues = { 12export type BuildFormDefaultValues = {
13 [ name: string ]: string | string[] 13 [ name: string ]: string | string[] | BuildFormDefaultValues
14} 14}
15 15
16@Injectable() 16@Injectable()
@@ -29,7 +29,16 @@ export class FormValidatorService {
29 formErrors[name] = '' 29 formErrors[name] = ''
30 30
31 const field = obj[name] 31 const field = obj[name]
32 if (field && field.MESSAGES) validationMessages[name] = field.MESSAGES 32 if (this.isRecursiveField(field)) {
33 const result = this.buildForm(field as BuildFormArgument, defaultValues[name] as BuildFormDefaultValues)
34 group[name] = result.form
35 formErrors[name] = result.formErrors
36 validationMessages[name] = result.validationMessages
37
38 continue
39 }
40
41 if (field && field.MESSAGES) validationMessages[name] = field.MESSAGES as { [ name: string ]: string }
33 42
34 const defaultValue = defaultValues[name] || '' 43 const defaultValue = defaultValues[name] || ''
35 44
@@ -52,13 +61,27 @@ export class FormValidatorService {
52 formErrors[name] = '' 61 formErrors[name] = ''
53 62
54 const field = obj[name] 63 const field = obj[name]
55 if (field && field.MESSAGES) validationMessages[name] = field.MESSAGES 64 if (this.isRecursiveField(field)) {
65 this.updateForm(
66 form[name],
67 formErrors[name] as FormReactiveErrors,
68 validationMessages[name] as FormReactiveValidationMessages,
69 obj[name] as BuildFormArgument,
70 defaultValues[name] as BuildFormDefaultValues
71 )
72 continue
73 }
74
75 if (field && field.MESSAGES) validationMessages[name] = field.MESSAGES as { [ name: string ]: string }
56 76
57 const defaultValue = defaultValues[name] || '' 77 const defaultValue = defaultValues[name] || ''
58 78
59 if (field && field.VALIDATORS) form.addControl(name, new FormControl(defaultValue, field.VALIDATORS)) 79 if (field && field.VALIDATORS) form.addControl(name, new FormControl(defaultValue, field.VALIDATORS as ValidatorFn[]))
60 else form.addControl(name, new FormControl(defaultValue)) 80 else form.addControl(name, new FormControl(defaultValue))
61 } 81 }
62 } 82 }
63 83
84 private isRecursiveField (field: any) {
85 return field && typeof field === 'object' && !field.MESSAGES && !field.VALIDATORS
86 }
64} 87}
diff --git a/client/src/app/shared/forms/form-validators/index.ts b/client/src/app/shared/forms/form-validators/index.ts
index 74e385b3d..fdcbedb71 100644
--- a/client/src/app/shared/forms/form-validators/index.ts
+++ b/client/src/app/shared/forms/form-validators/index.ts
@@ -1,6 +1,7 @@
1export * from './custom-config-validators.service' 1export * from './custom-config-validators.service'
2export * from './form-validator.service' 2export * from './form-validator.service'
3export * from './host' 3export * from './host'
4export * from './instance-validators.service'
4export * from './login-validators.service' 5export * from './login-validators.service'
5export * from './reset-password-validators.service' 6export * from './reset-password-validators.service'
6export * from './user-validators.service' 7export * from './user-validators.service'
diff --git a/client/src/app/shared/forms/form-validators/instance-validators.service.ts b/client/src/app/shared/forms/form-validators/instance-validators.service.ts
new file mode 100644
index 000000000..5bb852858
--- /dev/null
+++ b/client/src/app/shared/forms/form-validators/instance-validators.service.ts
@@ -0,0 +1,48 @@
1import { I18n } from '@ngx-translate/i18n-polyfill'
2import { Validators } from '@angular/forms'
3import { BuildFormValidator } from '@app/shared'
4import { Injectable } from '@angular/core'
5
6@Injectable()
7export class InstanceValidatorsService {
8 readonly FROM_EMAIL: BuildFormValidator
9 readonly FROM_NAME: BuildFormValidator
10 readonly BODY: BuildFormValidator
11
12 constructor (private i18n: I18n) {
13
14 this.FROM_EMAIL = {
15 VALIDATORS: [ Validators.required, Validators.email ],
16 MESSAGES: {
17 'required': this.i18n('Email is required.'),
18 'email': this.i18n('Email must be valid.')
19 }
20 }
21
22 this.FROM_NAME = {
23 VALIDATORS: [
24 Validators.required,
25 Validators.minLength(1),
26 Validators.maxLength(120)
27 ],
28 MESSAGES: {
29 'required': this.i18n('Your name is required.'),
30 'minlength': this.i18n('Your name must be at least 1 character long.'),
31 'maxlength': this.i18n('Your name cannot be more than 120 characters long.')
32 }
33 }
34
35 this.BODY = {
36 VALIDATORS: [
37 Validators.required,
38 Validators.minLength(3),
39 Validators.maxLength(5000)
40 ],
41 MESSAGES: {
42 'required': this.i18n('A message is required.'),
43 'minlength': this.i18n('The message must be at least 3 characters long.'),
44 'maxlength': this.i18n('The message cannot be more than 5000 characters long.')
45 }
46 }
47 }
48}
diff --git a/client/src/app/shared/forms/form-validators/user-validators.service.ts b/client/src/app/shared/forms/form-validators/user-validators.service.ts
index d14fa4777..6589b2580 100644
--- a/client/src/app/shared/forms/form-validators/user-validators.service.ts
+++ b/client/src/app/shared/forms/form-validators/user-validators.service.ts
@@ -23,15 +23,15 @@ export class UserValidatorsService {
23 this.USER_USERNAME = { 23 this.USER_USERNAME = {
24 VALIDATORS: [ 24 VALIDATORS: [
25 Validators.required, 25 Validators.required,
26 Validators.minLength(3), 26 Validators.minLength(1),
27 Validators.maxLength(20), 27 Validators.maxLength(50),
28 Validators.pattern(/^[a-z0-9._]+$/) 28 Validators.pattern(/^[a-z0-9][a-z0-9._]*$/)
29 ], 29 ],
30 MESSAGES: { 30 MESSAGES: {
31 'required': this.i18n('Username is required.'), 31 'required': this.i18n('Username is required.'),
32 'minlength': this.i18n('Username must be at least 3 characters long.'), 32 'minlength': this.i18n('Username must be at least 1 character long.'),
33 'maxlength': this.i18n('Username cannot be more than 20 characters long.'), 33 'maxlength': this.i18n('Username cannot be more than 50 characters long.'),
34 'pattern': this.i18n('Username should be only lowercase alphanumeric characters.') 34 'pattern': this.i18n('Username should be lowercase alphanumeric; dots and underscores are allowed.')
35 } 35 }
36 } 36 }
37 37
@@ -88,13 +88,13 @@ export class UserValidatorsService {
88 this.USER_DISPLAY_NAME = { 88 this.USER_DISPLAY_NAME = {
89 VALIDATORS: [ 89 VALIDATORS: [
90 Validators.required, 90 Validators.required,
91 Validators.minLength(3), 91 Validators.minLength(1),
92 Validators.maxLength(120) 92 Validators.maxLength(50)
93 ], 93 ],
94 MESSAGES: { 94 MESSAGES: {
95 'required': this.i18n('Display name is required.'), 95 'required': this.i18n('Display name is required.'),
96 'minlength': this.i18n('Display name must be at least 3 characters long.'), 96 'minlength': this.i18n('Display name must be at least 1 character long.'),
97 'maxlength': this.i18n('Display name cannot be more than 120 characters long.') 97 'maxlength': this.i18n('Display name cannot be more than 50 characters long.')
98 } 98 }
99 } 99 }
100 100
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/form-validators/video-channel-validators.service.ts b/client/src/app/shared/forms/form-validators/video-channel-validators.service.ts
index f62ff65f7..1c519c10a 100644
--- a/client/src/app/shared/forms/form-validators/video-channel-validators.service.ts
+++ b/client/src/app/shared/forms/form-validators/video-channel-validators.service.ts
@@ -14,28 +14,28 @@ export class VideoChannelValidatorsService {
14 this.VIDEO_CHANNEL_NAME = { 14 this.VIDEO_CHANNEL_NAME = {
15 VALIDATORS: [ 15 VALIDATORS: [
16 Validators.required, 16 Validators.required,
17 Validators.minLength(3), 17 Validators.minLength(1),
18 Validators.maxLength(20), 18 Validators.maxLength(50),
19 Validators.pattern(/^[a-z0-9._]+$/) 19 Validators.pattern(/^[a-z0-9][a-z0-9._]*$/)
20 ], 20 ],
21 MESSAGES: { 21 MESSAGES: {
22 'required': this.i18n('Name is required.'), 22 'required': this.i18n('Name is required.'),
23 'minlength': this.i18n('Name must be at least 3 characters long.'), 23 'minlength': this.i18n('Name must be at least 1 character long.'),
24 'maxlength': this.i18n('Name cannot be more than 20 characters long.'), 24 'maxlength': this.i18n('Name cannot be more than 50 characters long.'),
25 'pattern': this.i18n('Name should be only lowercase alphanumeric characters.') 25 'pattern': this.i18n('Name should be lowercase alphanumeric; dots and underscores are allowed.')
26 } 26 }
27 } 27 }
28 28
29 this.VIDEO_CHANNEL_DISPLAY_NAME = { 29 this.VIDEO_CHANNEL_DISPLAY_NAME = {
30 VALIDATORS: [ 30 VALIDATORS: [
31 Validators.required, 31 Validators.required,
32 Validators.minLength(3), 32 Validators.minLength(1),
33 Validators.maxLength(120) 33 Validators.maxLength(50)
34 ], 34 ],
35 MESSAGES: { 35 MESSAGES: {
36 'required': i18n('Display name is required.'), 36 'required': i18n('Display name is required.'),
37 'minlength': i18n('Display name must be at least 3 characters long.'), 37 'minlength': i18n('Display name must be at least 1 character long.'),
38 'maxlength': i18n('Display name cannot be more than 120 characters long.') 38 'maxlength': i18n('Display name cannot be more than 50 characters long.')
39 } 39 }
40 } 40 }
41 41
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/peertube-checkbox.component.html b/client/src/app/shared/forms/peertube-checkbox.component.html
index fb3006b53..7b8bcf601 100644
--- a/client/src/app/shared/forms/peertube-checkbox.component.html
+++ b/client/src/app/shared/forms/peertube-checkbox.component.html
@@ -1,10 +1,10 @@
1<div class="root"> 1<div class="root">
2 <label class="form-group-checkbox"> 2 <label class="form-group-checkbox">
3 <input type="checkbox" [(ngModel)]="checked" (ngModelChange)="onModelChange()" [id]="inputName" [disabled]="isDisabled" /> 3 <input type="checkbox" [(ngModel)]="checked" (ngModelChange)="onModelChange()" [id]="inputName" [disabled]="disabled" />
4 <span role="checkbox" [attr.aria-checked]="checked"></span> 4 <span role="checkbox" [attr.aria-checked]="checked"></span>
5 <span *ngIf="labelText">{{ labelText }}</span> 5 <span *ngIf="labelText">{{ labelText }}</span>
6 <span *ngIf="labelHtml" [innerHTML]="labelHtml"></span> 6 <span *ngIf="labelHtml" [innerHTML]="labelHtml"></span>
7 </label> 7 </label>
8 8
9 <my-help *ngIf="helpHtml" tooltipPlacement="top" helpType="custom" i18n-customHtml [customHtml]="helpHtml"></my-help> 9 <my-help *ngIf="helpHtml" tooltipPlacement="top" helpType="custom" i18n-customHtml [customHtml]="helpHtml"></my-help>
10</div> \ No newline at end of file 10</div>
diff --git a/client/src/app/shared/forms/peertube-checkbox.component.ts b/client/src/app/shared/forms/peertube-checkbox.component.ts
index bbc9904df..c1a6915e8 100644
--- a/client/src/app/shared/forms/peertube-checkbox.component.ts
+++ b/client/src/app/shared/forms/peertube-checkbox.component.ts
@@ -19,8 +19,7 @@ export class PeertubeCheckboxComponent implements ControlValueAccessor {
19 @Input() labelText: string 19 @Input() labelText: string
20 @Input() labelHtml: string 20 @Input() labelHtml: string
21 @Input() helpHtml: string 21 @Input() helpHtml: string
22 22 @Input() disabled = false
23 isDisabled = false
24 23
25 propagateChange = (_: any) => { /* empty */ } 24 propagateChange = (_: any) => { /* empty */ }
26 25
@@ -41,6 +40,6 @@ export class PeertubeCheckboxComponent implements ControlValueAccessor {
41 } 40 }
42 41
43 setDisabledState (isDisabled: boolean) { 42 setDisabledState (isDisabled: boolean) {
44 this.isDisabled = isDisabled 43 this.disabled = isDisabled
45 } 44 }
46} 45}
diff --git a/client/src/app/shared/forms/reactive-file.component.ts b/client/src/app/shared/forms/reactive-file.component.ts
index 8d22aa56c..f60c38e8d 100644
--- a/client/src/app/shared/forms/reactive-file.component.ts
+++ b/client/src/app/shared/forms/reactive-file.component.ts
@@ -1,6 +1,6 @@
1import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier } from '@app/core'
4import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
5 5
6@Component({ 6@Component({
@@ -30,7 +30,7 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor {
30 private file: File 30 private file: File
31 31
32 constructor ( 32 constructor (
33 private notificationsService: NotificationsService, 33 private notifier: Notifier,
34 private i18n: I18n 34 private i18n: I18n
35 ) {} 35 ) {}
36 36
@@ -49,7 +49,18 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor {
49 const [ file ] = event.target.files 49 const [ file ] = event.target.files
50 50
51 if (file.size > this.maxFileSize) { 51 if (file.size > this.maxFileSize) {
52 this.notificationsService.error(this.i18n('Error'), this.i18n('This file is too large.')) 52 this.notifier.error(this.i18n('This file is too large.'))
53 return
54 }
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
53 return 64 return
54 } 65 }
55 66
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/instance/instance.service.ts b/client/src/app/shared/instance/instance.service.ts
new file mode 100644
index 000000000..61321ecce
--- /dev/null
+++ b/client/src/app/shared/instance/instance.service.ts
@@ -0,0 +1,36 @@
1import { catchError } from 'rxjs/operators'
2import { HttpClient } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { environment } from '../../../environments/environment'
5import { RestExtractor, RestService } from '../rest'
6import { About } from '../../../../../shared/models/server'
7
8@Injectable()
9export class InstanceService {
10 private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config'
11 private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server'
12
13 constructor (
14 private authHttp: HttpClient,
15 private restService: RestService,
16 private restExtractor: RestExtractor
17 ) {
18 }
19
20 getAbout () {
21 return this.authHttp.get<About>(InstanceService.BASE_CONFIG_URL + '/about')
22 .pipe(catchError(res => this.restExtractor.handleError(res)))
23 }
24
25 contactAdministrator (fromEmail: string, fromName: string, message: string) {
26 const body = {
27 fromEmail,
28 fromName,
29 body: message
30 }
31
32 return this.authHttp.post(InstanceService.BASE_SERVER_URL + '/contact', body)
33 .pipe(catchError(res => this.restExtractor.handleError(res)))
34
35 }
36}
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.html b/client/src/app/shared/menu/top-menu-dropdown.component.html
new file mode 100644
index 000000000..d3c896019
--- /dev/null
+++ b/client/src/app/shared/menu/top-menu-dropdown.component.html
@@ -0,0 +1,21 @@
1<div class="sub-menu">
2 <ng-container *ngFor="let menuEntry of menuEntries">
3
4 <a *ngIf="menuEntry.routerLink" [routerLink]="menuEntry.routerLink" routerLinkActive="active" class="title-page">{{ menuEntry.label }}</a>
5
6 <div *ngIf="!menuEntry.routerLink" ngbDropdown class="parent-entry" #dropdown="ngbDropdown" (mouseleave)="closeDropdownIfHovered(dropdown)">
7 <span
8 (mouseenter)="openDropdownOnHover(dropdown)" [ngClass]="{ active: !!suffixLabels[menuEntry.label] }" ngbDropdownAnchor
9 (click)="dropdownAnchorClicked(dropdown)" role="button" class="title-page"
10 >
11 <ng-container i18n>{{ menuEntry.label }}</ng-container>
12 <ng-container *ngIf="!!suffixLabels[menuEntry.label]"> - {{ suffixLabels[menuEntry.label] }}</ng-container>
13 </span>
14
15 <div ngbDropdownMenu>
16 <a *ngFor="let menuChild of menuEntry.children" class="dropdown-item" [routerLink]="menuChild.routerLink">{{ menuChild.label }}</a>
17 </div>
18 </div>
19
20 </ng-container>
21</div>
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.scss b/client/src/app/shared/menu/top-menu-dropdown.component.scss
new file mode 100644
index 000000000..77159532f
--- /dev/null
+++ b/client/src/app/shared/menu/top-menu-dropdown.component.scss
@@ -0,0 +1,18 @@
1.parent-entry {
2 span[role=button] {
3 cursor: pointer;
4 }
5
6 a {
7 display: block;
8 }
9}
10
11/deep/ .dropdown-toggle::after {
12 position: relative;
13 top: 2px;
14}
15
16/deep/ .dropdown-menu {
17 margin-top: 0 !important;
18}
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.ts b/client/src/app/shared/menu/top-menu-dropdown.component.ts
new file mode 100644
index 000000000..e859c30dd
--- /dev/null
+++ b/client/src/app/shared/menu/top-menu-dropdown.component.ts
@@ -0,0 +1,83 @@
1import { Component, Input, OnDestroy, OnInit } from '@angular/core'
2import { filter, take } from 'rxjs/operators'
3import { NavigationEnd, Router } from '@angular/router'
4import { Subscription } from 'rxjs'
5import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
6
7export type TopMenuDropdownParam = {
8 label: string
9 routerLink?: string
10
11 children?: {
12 label: string
13 routerLink: string
14 }[]
15}
16
17@Component({
18 selector: 'my-top-menu-dropdown',
19 templateUrl: './top-menu-dropdown.component.html',
20 styleUrls: [ './top-menu-dropdown.component.scss' ]
21})
22export class TopMenuDropdownComponent implements OnInit, OnDestroy {
23 @Input() menuEntries: TopMenuDropdownParam[] = []
24
25 suffixLabels: { [ parentLabel: string ]: string }
26
27 private openedOnHover = false
28 private routeSub: Subscription
29
30 constructor (private router: Router) {}
31
32 ngOnInit () {
33 this.updateChildLabels(window.location.pathname)
34
35 this.routeSub = this.router.events
36 .pipe(filter(event => event instanceof NavigationEnd))
37 .subscribe(() => this.updateChildLabels(window.location.pathname))
38 }
39
40 ngOnDestroy () {
41 if (this.routeSub) this.routeSub.unsubscribe()
42 }
43
44 openDropdownOnHover (dropdown: NgbDropdown) {
45 this.openedOnHover = true
46 dropdown.open()
47
48 // Menu was closed
49 dropdown.openChange
50 .pipe(take(1))
51 .subscribe(e => this.openedOnHover = false)
52 }
53
54 dropdownAnchorClicked (dropdown: NgbDropdown) {
55 if (this.openedOnHover) {
56 this.openedOnHover = false
57 return
58 }
59
60 return dropdown.toggle()
61 }
62
63 closeDropdownIfHovered (dropdown: NgbDropdown) {
64 if (this.openedOnHover === false) return
65
66 dropdown.close()
67 this.openedOnHover = false
68 }
69
70 private updateChildLabels (path: string) {
71 this.suffixLabels = {}
72
73 for (const entry of this.menuEntries) {
74 if (!entry.children) continue
75
76 for (const child of entry.children) {
77 if (path.startsWith(child.routerLink)) {
78 this.suffixLabels[entry.label] = child.label
79 }
80 }
81 }
82 }
83}
diff --git a/client/src/app/shared/misc/help.component.html b/client/src/app/shared/misc/help.component.html
index 28ccb1e26..444425c9f 100644
--- a/client/src/app/shared/misc/help.component.html
+++ b/client/src/app/shared/misc/help.component.html
@@ -18,10 +18,13 @@
18 container="body" 18 container="body"
19 title="Get help" 19 title="Get help"
20 i18n-title 20 i18n-title
21 popoverClass="help-popover"
21 [attr.aria-pressed]="isPopoverOpened" 22 [attr.aria-pressed]="isPopoverOpened"
22 [ngbPopover]="tooltipTemplate" 23 [ngbPopover]="tooltipTemplate"
23 [placement]="tooltipPlacement" 24 [placement]="tooltipPlacement"
24 [autoClose]="true" 25 [autoClose]="true"
25 (onHidden)="onPopoverHidden()" 26 (onHidden)="onPopoverHidden()"
26 (onShown)="onPopoverShown()" 27 (onShown)="onPopoverShown()"
27></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 5c73a8031..3898f3cda 100644
--- a/client/src/app/shared/misc/help.component.scss
+++ b/client/src/app/shared/misc/help.component.scss
@@ -2,29 +2,40 @@
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/ {
15 .popover-body { 19 .help-popover {
16 text-align: left;
17 padding: 10px;
18 max-width: 300px; 20 max-width: 300px;
19 21
20 font-size: 13px; 22 .popover-body {
21 font-family: $main-fonts; 23 font-family: $main-fonts;
22 background-color: #fff; 24 text-align: left;
23 color: #000; 25 padding: 10px;
24 box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); 26 font-size: 13px;
27 background-color: var(--mainBackgroundColor);
28 color: var(--mainForegroundColor);
29 box-shadow: 0 0 6px rgba(0, 0, 0, 0.5);
30
31 p {
32 margin-bottom: 0;
33 }
25 34
26 ul { 35 ul {
27 padding-left: 20px; 36 padding-left: 20px;
37 margin-bottom: 0;
38 }
28 } 39 }
29 } 40 }
30} 41}
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 60bd442dd..942765301 100644
--- a/client/src/app/shared/moderation/user-ban-modal.component.ts
+++ b/client/src/app/shared/moderation/user-ban-modal.component.ts
@@ -1,5 +1,5 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
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'
@@ -23,7 +23,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
23 constructor ( 23 constructor (
24 protected formValidatorService: FormValidatorService, 24 protected formValidatorService: FormValidatorService,
25 private modalService: NgbModal, 25 private modalService: NgbModal,
26 private notificationsService: NotificationsService, 26 private notifier: Notifier,
27 private userService: UserService, 27 private userService: UserService,
28 private userValidatorsService: UserValidatorsService, 28 private userValidatorsService: UserValidatorsService,
29 private i18n: I18n 29 private i18n: I18n
@@ -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 }
@@ -57,13 +57,13 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
57 ? this.i18n('{{num}} users banned.', { num: this.usersToBan.length }) 57 ? this.i18n('{{num}} users banned.', { num: this.usersToBan.length })
58 : this.i18n('User {{username}} banned.', { username: this.usersToBan.username }) 58 : this.i18n('User {{username}} banned.', { username: this.usersToBan.username })
59 59
60 this.notificationsService.success(this.i18n('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.notificationsService.error(this.i18n('Error'), err.message) 66 err => this.notifier.error(err.message)
67 ) 67 )
68 } 68 }
69 69
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
index d391246e0..9a2461ebf 100644
--- a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
+++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
@@ -1,10 +1,9 @@
1import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications'
3import { I18n } from '@ngx-translate/i18n-polyfill' 2import { I18n } from '@ngx-translate/i18n-polyfill'
4import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' 3import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
5import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component' 4import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component'
6import { UserService } from '@app/shared/users' 5import { UserService } from '@app/shared/users'
7import { AuthService, ConfirmService, ServerService } from '@app/core' 6import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core'
8import { User, UserRight } from '../../../../../shared/models/users' 7import { User, UserRight } from '../../../../../shared/models/users'
9import { Account } from '@app/shared/account/account.model' 8import { Account } from '@app/shared/account/account.model'
10import { BlocklistService } from '@app/shared/blocklist' 9import { BlocklistService } from '@app/shared/blocklist'
@@ -30,7 +29,7 @@ export class UserModerationDropdownComponent implements OnChanges {
30 29
31 constructor ( 30 constructor (
32 private authService: AuthService, 31 private authService: AuthService,
33 private notificationsService: NotificationsService, 32 private notifier: Notifier,
34 private confirmService: ConfirmService, 33 private confirmService: ConfirmService,
35 private serverService: ServerService, 34 private serverService: ServerService,
36 private userService: UserService, 35 private userService: UserService,
@@ -48,7 +47,7 @@ export class UserModerationDropdownComponent implements OnChanges {
48 47
49 openBanUserModal (user: User) { 48 openBanUserModal (user: User) {
50 if (user.username === 'root') { 49 if (user.username === 'root') {
51 this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot ban root.')) 50 this.notifier.error(this.i18n('You cannot ban root.'))
52 return 51 return
53 } 52 }
54 53
@@ -67,21 +66,18 @@ export class UserModerationDropdownComponent implements OnChanges {
67 this.userService.unbanUsers(user) 66 this.userService.unbanUsers(user)
68 .subscribe( 67 .subscribe(
69 () => { 68 () => {
70 this.notificationsService.success( 69 this.notifier.success(this.i18n('User {{username}} unbanned.', { username: user.username }))
71 this.i18n('Success'),
72 this.i18n('User {{username}} unbanned.', { username: user.username })
73 )
74 70
75 this.userChanged.emit() 71 this.userChanged.emit()
76 }, 72 },
77 73
78 err => this.notificationsService.error(this.i18n('Error'), err.message) 74 err => this.notifier.error(err.message)
79 ) 75 )
80 } 76 }
81 77
82 async removeUser (user: User) { 78 async removeUser (user: User) {
83 if (user.username === 'root') { 79 if (user.username === 'root') {
84 this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot delete root.')) 80 this.notifier.error(this.i18n('You cannot delete root.'))
85 return 81 return
86 } 82 }
87 83
@@ -91,29 +87,23 @@ export class UserModerationDropdownComponent implements OnChanges {
91 87
92 this.userService.removeUser(user).subscribe( 88 this.userService.removeUser(user).subscribe(
93 () => { 89 () => {
94 this.notificationsService.success( 90 this.notifier.success(this.i18n('User {{username}} deleted.', { username: user.username }))
95 this.i18n('Success'),
96 this.i18n('User {{username}} deleted.', { username: user.username })
97 )
98 this.userDeleted.emit() 91 this.userDeleted.emit()
99 }, 92 },
100 93
101 err => this.notificationsService.error(this.i18n('Error'), err.message) 94 err => this.notifier.error(err.message)
102 ) 95 )
103 } 96 }
104 97
105 setEmailAsVerified (user: User) { 98 setEmailAsVerified (user: User) {
106 this.userService.updateUser(user.id, { emailVerified: true }).subscribe( 99 this.userService.updateUser(user.id, { emailVerified: true }).subscribe(
107 () => { 100 () => {
108 this.notificationsService.success( 101 this.notifier.success(this.i18n('User {{username}} email set as verified', { username: user.username }))
109 this.i18n('Success'),
110 this.i18n('User {{username}} email set as verified', { username: user.username })
111 )
112 102
113 this.userChanged.emit() 103 this.userChanged.emit()
114 }, 104 },
115 105
116 err => this.notificationsService.error(this.i18n('Error'), err.message) 106 err => this.notifier.error(err.message)
117 ) 107 )
118 } 108 }
119 109
@@ -121,16 +111,13 @@ export class UserModerationDropdownComponent implements OnChanges {
121 this.blocklistService.blockAccountByUser(account) 111 this.blocklistService.blockAccountByUser(account)
122 .subscribe( 112 .subscribe(
123 () => { 113 () => {
124 this.notificationsService.success( 114 this.notifier.success(this.i18n('Account {{nameWithHost}} muted.', { nameWithHost: account.nameWithHost }))
125 this.i18n('Success'),
126 this.i18n('Account {{nameWithHost}} muted.', { nameWithHost: account.nameWithHost })
127 )
128 115
129 this.account.mutedByUser = true 116 this.account.mutedByUser = true
130 this.userChanged.emit() 117 this.userChanged.emit()
131 }, 118 },
132 119
133 err => this.notificationsService.error(this.i18n('Error'), err.message) 120 err => this.notifier.error(err.message)
134 ) 121 )
135 } 122 }
136 123
@@ -138,16 +125,13 @@ export class UserModerationDropdownComponent implements OnChanges {
138 this.blocklistService.unblockAccountByUser(account) 125 this.blocklistService.unblockAccountByUser(account)
139 .subscribe( 126 .subscribe(
140 () => { 127 () => {
141 this.notificationsService.success( 128 this.notifier.success(this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: account.nameWithHost }))
142 this.i18n('Success'),
143 this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: account.nameWithHost })
144 )
145 129
146 this.account.mutedByUser = false 130 this.account.mutedByUser = false
147 this.userChanged.emit() 131 this.userChanged.emit()
148 }, 132 },
149 133
150 err => this.notificationsService.error(this.i18n('Error'), err.message) 134 err => this.notifier.error(err.message)
151 ) 135 )
152 } 136 }
153 137
@@ -155,16 +139,13 @@ export class UserModerationDropdownComponent implements OnChanges {
155 this.blocklistService.blockServerByUser(host) 139 this.blocklistService.blockServerByUser(host)
156 .subscribe( 140 .subscribe(
157 () => { 141 () => {
158 this.notificationsService.success( 142 this.notifier.success(this.i18n('Instance {{host}} muted.', { host }))
159 this.i18n('Success'),
160 this.i18n('Instance {{host}} muted.', { host })
161 )
162 143
163 this.account.mutedServerByUser = true 144 this.account.mutedServerByUser = true
164 this.userChanged.emit() 145 this.userChanged.emit()
165 }, 146 },
166 147
167 err => this.notificationsService.error(this.i18n('Error'), err.message) 148 err => this.notifier.error(err.message)
168 ) 149 )
169 } 150 }
170 151
@@ -172,16 +153,13 @@ export class UserModerationDropdownComponent implements OnChanges {
172 this.blocklistService.unblockServerByUser(host) 153 this.blocklistService.unblockServerByUser(host)
173 .subscribe( 154 .subscribe(
174 () => { 155 () => {
175 this.notificationsService.success( 156 this.notifier.success(this.i18n('Instance {{host}} unmuted.', { host }))
176 this.i18n('Success'),
177 this.i18n('Instance {{host}} unmuted.', { host })
178 )
179 157
180 this.account.mutedServerByUser = false 158 this.account.mutedServerByUser = false
181 this.userChanged.emit() 159 this.userChanged.emit()
182 }, 160 },
183 161
184 err => this.notificationsService.error(this.i18n('Error'), err.message) 162 err => this.notifier.error(err.message)
185 ) 163 )
186 } 164 }
187 165
@@ -189,16 +167,13 @@ export class UserModerationDropdownComponent implements OnChanges {
189 this.blocklistService.blockAccountByInstance(account) 167 this.blocklistService.blockAccountByInstance(account)
190 .subscribe( 168 .subscribe(
191 () => { 169 () => {
192 this.notificationsService.success( 170 this.notifier.success(this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost }))
193 this.i18n('Success'),
194 this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })
195 )
196 171
197 this.account.mutedByInstance = true 172 this.account.mutedByInstance = true
198 this.userChanged.emit() 173 this.userChanged.emit()
199 }, 174 },
200 175
201 err => this.notificationsService.error(this.i18n('Error'), err.message) 176 err => this.notifier.error(err.message)
202 ) 177 )
203 } 178 }
204 179
@@ -206,16 +181,13 @@ export class UserModerationDropdownComponent implements OnChanges {
206 this.blocklistService.unblockAccountByInstance(account) 181 this.blocklistService.unblockAccountByInstance(account)
207 .subscribe( 182 .subscribe(
208 () => { 183 () => {
209 this.notificationsService.success( 184 this.notifier.success(this.i18n('Account {{nameWithHost}} unmuted by the instance.', { nameWithHost: account.nameWithHost }))
210 this.i18n('Success'),
211 this.i18n('Account {{nameWithHost}} unmuted by the instance.', { nameWithHost: account.nameWithHost })
212 )
213 185
214 this.account.mutedByInstance = false 186 this.account.mutedByInstance = false
215 this.userChanged.emit() 187 this.userChanged.emit()
216 }, 188 },
217 189
218 err => this.notificationsService.error(this.i18n('Error'), err.message) 190 err => this.notifier.error(err.message)
219 ) 191 )
220 } 192 }
221 193
@@ -223,16 +195,13 @@ export class UserModerationDropdownComponent implements OnChanges {
223 this.blocklistService.blockServerByInstance(host) 195 this.blocklistService.blockServerByInstance(host)
224 .subscribe( 196 .subscribe(
225 () => { 197 () => {
226 this.notificationsService.success( 198 this.notifier.success(this.i18n('Instance {{host}} muted by the instance.', { host }))
227 this.i18n('Success'),
228 this.i18n('Instance {{host}} muted by the instance.', { host })
229 )
230 199
231 this.account.mutedServerByInstance = true 200 this.account.mutedServerByInstance = true
232 this.userChanged.emit() 201 this.userChanged.emit()
233 }, 202 },
234 203
235 err => this.notificationsService.error(this.i18n('Error'), err.message) 204 err => this.notifier.error(err.message)
236 ) 205 )
237 } 206 }
238 207
@@ -240,16 +209,13 @@ export class UserModerationDropdownComponent implements OnChanges {
240 this.blocklistService.unblockServerByInstance(host) 209 this.blocklistService.unblockServerByInstance(host)
241 .subscribe( 210 .subscribe(
242 () => { 211 () => {
243 this.notificationsService.success( 212 this.notifier.success(this.i18n('Instance {{host}} unmuted by the instance.', { host }))
244 this.i18n('Success'),
245 this.i18n('Instance {{host}} unmuted by the instance.', { host })
246 )
247 213
248 this.account.mutedServerByInstance = false 214 this.account.mutedServerByInstance = false
249 this.userChanged.emit() 215 this.userChanged.emit()
250 }, 216 },
251 217
252 err => this.notificationsService.error(this.i18n('Error'), err.message) 218 err => this.notifier.error(err.message)
253 ) 219 )
254 } 220 }
255 221
@@ -277,18 +243,18 @@ export class UserModerationDropdownComponent implements OnChanges {
277 }, 243 },
278 { 244 {
279 label: this.i18n('Ban'), 245 label: this.i18n('Ban'),
280 handler: ({ user }: { user: User }) => this.openBanUserModal(user), 246 handler: ({ user }) => this.openBanUserModal(user),
281 isDisplayed: ({ user }: { user: User }) => !user.blocked 247 isDisplayed: ({ user }) => !user.blocked
282 }, 248 },
283 { 249 {
284 label: this.i18n('Unban'), 250 label: this.i18n('Unban'),
285 handler: ({ user }: { user: User }) => this.unbanUser(user), 251 handler: ({ user }) => this.unbanUser(user),
286 isDisplayed: ({ user }: { user: User }) => user.blocked 252 isDisplayed: ({ user }) => user.blocked
287 }, 253 },
288 { 254 {
289 label: this.i18n('Set Email as Verified'), 255 label: this.i18n('Set Email as Verified'),
290 handler: ({ user }: { user: User }) => this.setEmailAsVerified(user), 256 handler: ({ user }) => this.setEmailAsVerified(user),
291 isDisplayed: ({ user }: { user: User }) => this.requiresEmailVerification && !user.blocked && user.emailVerified === false 257 isDisplayed: ({ user }) => this.requiresEmailVerification && !user.blocked && user.emailVerified === false
292 } 258 }
293 ]) 259 ])
294 } 260 }
@@ -299,23 +265,23 @@ export class UserModerationDropdownComponent implements OnChanges {
299 this.userActions.push([ 265 this.userActions.push([
300 { 266 {
301 label: this.i18n('Mute this account'), 267 label: this.i18n('Mute this account'),
302 isDisplayed: ({ account }: { account: Account }) => account.mutedByUser === false, 268 isDisplayed: ({ account }) => account.mutedByUser === false,
303 handler: ({ account }: { account: Account }) => this.blockAccountByUser(account) 269 handler: ({ account }) => this.blockAccountByUser(account)
304 }, 270 },
305 { 271 {
306 label: this.i18n('Unmute this account'), 272 label: this.i18n('Unmute this account'),
307 isDisplayed: ({ account }: { account: Account }) => account.mutedByUser === true, 273 isDisplayed: ({ account }) => account.mutedByUser === true,
308 handler: ({ account }: { account: Account }) => this.unblockAccountByUser(account) 274 handler: ({ account }) => this.unblockAccountByUser(account)
309 }, 275 },
310 { 276 {
311 label: this.i18n('Mute the instance'), 277 label: this.i18n('Mute the instance'),
312 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === false, 278 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false,
313 handler: ({ account }: { account: Account }) => this.blockServerByUser(account.host) 279 handler: ({ account }) => this.blockServerByUser(account.host)
314 }, 280 },
315 { 281 {
316 label: this.i18n('Unmute the instance'), 282 label: this.i18n('Unmute the instance'),
317 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === true, 283 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true,
318 handler: ({ account }: { account: Account }) => this.unblockServerByUser(account.host) 284 handler: ({ account }) => this.unblockServerByUser(account.host)
319 } 285 }
320 ]) 286 ])
321 287
@@ -326,13 +292,13 @@ export class UserModerationDropdownComponent implements OnChanges {
326 instanceActions = instanceActions.concat([ 292 instanceActions = instanceActions.concat([
327 { 293 {
328 label: this.i18n('Mute this account by your instance'), 294 label: this.i18n('Mute this account by your instance'),
329 isDisplayed: ({ account }: { account: Account }) => account.mutedByInstance === false, 295 isDisplayed: ({ account }) => account.mutedByInstance === false,
330 handler: ({ account }: { account: Account }) => this.blockAccountByInstance(account) 296 handler: ({ account }) => this.blockAccountByInstance(account)
331 }, 297 },
332 { 298 {
333 label: this.i18n('Unmute this account by your instance'), 299 label: this.i18n('Unmute this account by your instance'),
334 isDisplayed: ({ account }: { account: Account }) => account.mutedByInstance === true, 300 isDisplayed: ({ account }) => account.mutedByInstance === true,
335 handler: ({ account }: { account: Account }) => this.unblockAccountByInstance(account) 301 handler: ({ account }) => this.unblockAccountByInstance(account)
336 } 302 }
337 ]) 303 ])
338 } 304 }
@@ -342,13 +308,13 @@ export class UserModerationDropdownComponent implements OnChanges {
342 instanceActions = instanceActions.concat([ 308 instanceActions = instanceActions.concat([
343 { 309 {
344 label: this.i18n('Mute the instance by your instance'), 310 label: this.i18n('Mute the instance by your instance'),
345 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === false, 311 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false,
346 handler: ({ account }: { account: Account }) => this.blockServerByInstance(account.host) 312 handler: ({ account }) => this.blockServerByInstance(account.host)
347 }, 313 },
348 { 314 {
349 label: this.i18n('Unmute the instance by your instance'), 315 label: this.i18n('Unmute the instance by your instance'),
350 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === true, 316 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true,
351 handler: ({ account }: { account: Account }) => this.unblockServerByInstance(account.host) 317 handler: ({ account }) => this.unblockServerByInstance(account.host)
352 } 318 }
353 ]) 319 ])
354 } 320 }
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/rest/component-pagination.model.ts b/client/src/app/shared/rest/component-pagination.model.ts
index 0b8ecc318..85160d445 100644
--- a/client/src/app/shared/rest/component-pagination.model.ts
+++ b/client/src/app/shared/rest/component-pagination.model.ts
@@ -3,3 +3,14 @@ export interface ComponentPagination {
3 itemsPerPage: number 3 itemsPerPage: number
4 totalItems?: number 4 totalItems?: number
5} 5}
6
7export function hasMoreItems (componentPagination: ComponentPagination) {
8 // No results
9 if (componentPagination.totalItems === 0) return false
10
11 // Not loaded yet
12 if (!componentPagination.totalItems) return true
13
14 const maxPage = componentPagination.totalItems / componentPagination.itemsPerPage
15 return maxPage > componentPagination.currentPage
16}
diff --git a/client/src/app/shared/rest/rest-extractor.service.ts b/client/src/app/shared/rest/rest-extractor.service.ts
index f149569ef..e6518dd1d 100644
--- a/client/src/app/shared/rest/rest-extractor.service.ts
+++ b/client/src/app/shared/rest/rest-extractor.service.ts
@@ -80,6 +80,7 @@ export class RestExtractor {
80 errorMessage = errorMessage ? errorMessage : 'Unknown error.' 80 errorMessage = errorMessage ? errorMessage : 'Unknown error.'
81 console.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`) 81 console.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`)
82 } else { 82 } else {
83 console.error(err)
83 errorMessage = err 84 errorMessage = err
84 } 85 }
85 86
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index a2fa27b72..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,6 +33,7 @@ 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,
@@ -61,6 +61,14 @@ import { OverviewService } from '@app/shared/overview'
61import { UserBanModalComponent } from '@app/shared/moderation' 61import { UserBanModalComponent } from '@app/shared/moderation'
62import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component' 62import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component'
63import { BlocklistService } from '@app/shared/blocklist' 63import { BlocklistService } from '@app/shared/blocklist'
64import { TopMenuDropdownComponent } from '@app/shared/menu/top-menu-dropdown.component'
65import { UserHistoryService } from '@app/shared/users/user-history.service'
66import { UserNotificationService } from '@app/shared/users/user-notification.service'
67import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component'
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'
64 72
65@NgModule({ 73@NgModule({
66 imports: [ 74 imports: [
@@ -102,7 +110,11 @@ import { BlocklistService } from '@app/shared/blocklist'
102 RemoteSubscribeComponent, 110 RemoteSubscribeComponent,
103 InstanceFeaturesTableComponent, 111 InstanceFeaturesTableComponent,
104 UserBanModalComponent, 112 UserBanModalComponent,
105 UserModerationDropdownComponent 113 UserModerationDropdownComponent,
114 TopMenuDropdownComponent,
115 UserNotificationsComponent,
116 ConfirmComponent,
117 GlobalIconComponent
106 ], 118 ],
107 119
108 exports: [ 120 exports: [
@@ -141,6 +153,10 @@ import { BlocklistService } from '@app/shared/blocklist'
141 InstanceFeaturesTableComponent, 153 InstanceFeaturesTableComponent,
142 UserBanModalComponent, 154 UserBanModalComponent,
143 UserModerationDropdownComponent, 155 UserModerationDropdownComponent,
156 TopMenuDropdownComponent,
157 UserNotificationsComponent,
158 ConfirmComponent,
159 GlobalIconComponent,
144 160
145 NumberFormatterPipe, 161 NumberFormatterPipe,
146 ObjectLengthPipe, 162 ObjectLengthPipe,
@@ -157,7 +173,6 @@ import { BlocklistService } from '@app/shared/blocklist'
157 UserService, 173 UserService,
158 VideoService, 174 VideoService,
159 AccountService, 175 AccountService,
160 MarkdownService,
161 VideoChannelService, 176 VideoChannelService,
162 VideoCaptionService, 177 VideoCaptionService,
163 VideoImportService, 178 VideoImportService,
@@ -177,11 +192,20 @@ import { BlocklistService } from '@app/shared/blocklist'
177 OverviewService, 192 OverviewService,
178 VideoChangeOwnershipValidatorsService, 193 VideoChangeOwnershipValidatorsService,
179 VideoAcceptOwnershipValidatorsService, 194 VideoAcceptOwnershipValidatorsService,
195 InstanceValidatorsService,
180 BlocklistService, 196 BlocklistService,
197 UserHistoryService,
198 InstanceService,
199
200 MarkdownService,
201 LinkifierService,
202 HtmlRendererService,
181 203
182 I18nPrimengCalendarService, 204 I18nPrimengCalendarService,
183 ScreenService, 205 ScreenService,
184 206
207 UserNotificationService,
208
185 I18n 209 I18n
186 ] 210 ]
187}) 211})
diff --git a/client/src/app/shared/user-subscription/remote-subscribe.component.ts b/client/src/app/shared/user-subscription/remote-subscribe.component.ts
index 7a81108cd..ba2a45df1 100644
--- a/client/src/app/shared/user-subscription/remote-subscribe.component.ts
+++ b/client/src/app/shared/user-subscription/remote-subscribe.component.ts
@@ -29,7 +29,7 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit {
29 } 29 }
30 30
31 onValidKey () { 31 onValidKey () {
32 this.onValueChanged() 32 this.check()
33 if (!this.form.valid) return 33 if (!this.form.valid) return
34 34
35 this.formValidated() 35 this.formValidated()
@@ -37,7 +37,24 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit {
37 37
38 formValidated () { 38 formValidated () {
39 const address = this.form.value['text'] 39 const address = this.form.value['text']
40 const [ , hostname ] = address.split('@') 40 const [ username, hostname ] = address.split('@')
41 window.open(`https://${hostname}/authorize_interaction?acct=${this.account}`) 41
42 fetch(`https://${hostname}/.well-known/webfinger?resource=acct:${username}@${hostname}`)
43 .then(response => response.json())
44 .then(data => new Promise((resolve, reject) => {
45 if (data && Array.isArray(data.links)) {
46 const link: {
47 template: string
48 } = data.links.find((link: any) =>
49 link && typeof link.template === 'string' && link.rel === 'http://ostatus.org/schema/1.0/subscribe')
50
51 if (link && link.template.includes('{uri}')) {
52 resolve(link.template.replace('{uri}', `acct:${this.account}`))
53 }
54 }
55 reject()
56 }))
57 .then(window.open)
58 .catch(() => window.open(`https://${hostname}/authorize_interaction?acct=${this.account}`))
42 } 59 }
43} 60}
diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.ts b/client/src/app/shared/user-subscription/subscribe-button.component.ts
index 315ea5037..8f1754c7f 100644
--- a/client/src/app/shared/user-subscription/subscribe-button.component.ts
+++ b/client/src/app/shared/user-subscription/subscribe-button.component.ts
@@ -1,9 +1,8 @@
1import { Component, Input, OnInit } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { AuthService } from '@app/core' 3import { AuthService, Notifier } from '@app/core'
4import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service' 4import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service'
5import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 5import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
6import { NotificationsService } from 'angular2-notifications'
7import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
8import { VideoService } from '@app/shared/video/video.service' 7import { VideoService } from '@app/shared/video/video.service'
9import { FeedFormat } from '../../../../../shared/models/feeds' 8import { FeedFormat } from '../../../../../shared/models/feeds'
@@ -23,7 +22,7 @@ export class SubscribeButtonComponent implements OnInit {
23 constructor ( 22 constructor (
24 private authService: AuthService, 23 private authService: AuthService,
25 private router: Router, 24 private router: Router,
26 private notificationsService: NotificationsService, 25 private notifier: Notifier,
27 private userSubscriptionService: UserSubscriptionService, 26 private userSubscriptionService: UserSubscriptionService,
28 private i18n: I18n, 27 private i18n: I18n,
29 private videoService: VideoService 28 private videoService: VideoService
@@ -43,18 +42,17 @@ export class SubscribeButtonComponent implements OnInit {
43 .subscribe( 42 .subscribe(
44 res => this.subscribed = res[this.uri], 43 res => this.subscribed = res[this.uri],
45 44
46 err => this.notificationsService.error(this.i18n('Error'), err.message) 45 err => this.notifier.error(err.message)
47 ) 46 )
48 } 47 }
49 } 48 }
50 49
51 subscribe () { 50 subscribe () {
52 if (this.isUserLoggedIn()) { 51 if (this.isUserLoggedIn()) {
53 this.localSubscribe() 52 return this.localSubscribe()
54 } else {
55 this.authService.redirectUrl = this.router.url
56 this.gotoLogin()
57 } 53 }
54
55 return this.gotoLogin()
58 } 56 }
59 57
60 localSubscribe () { 58 localSubscribe () {
@@ -63,13 +61,13 @@ export class SubscribeButtonComponent implements OnInit {
63 () => { 61 () => {
64 this.subscribed = true 62 this.subscribed = true
65 63
66 this.notificationsService.success( 64 this.notifier.success(
67 this.i18n('Subscribed'), 65 this.i18n('Subscribed to {{nameWithHost}}', { nameWithHost: this.videoChannel.displayName }),
68 this.i18n('Subscribed to {{nameWithHost}}', { nameWithHost: this.videoChannel.displayName }) 66 this.i18n('Subscribed')
69 ) 67 )
70 }, 68 },
71 69
72 err => this.notificationsService.error(this.i18n('Error'), err.message) 70 err => this.notifier.error(err.message)
73 ) 71 )
74 } 72 }
75 73
@@ -85,13 +83,13 @@ export class SubscribeButtonComponent implements OnInit {
85 () => { 83 () => {
86 this.subscribed = false 84 this.subscribed = false
87 85
88 this.notificationsService.success( 86 this.notifier.success(
89 this.i18n('Unsubscribed'), 87 this.i18n('Unsubscribed from {{nameWithHost}}', { nameWithHost: this.videoChannel.displayName }),
90 this.i18n('Unsubscribed from {{nameWithHost}}', { nameWithHost: this.videoChannel.displayName }) 88 this.i18n('Unsubscribed')
91 ) 89 )
92 }, 90 },
93 91
94 err => this.notificationsService.error(this.i18n('Error'), err.message) 92 err => this.notifier.error(err.message)
95 ) 93 )
96 } 94 }
97 95
diff --git a/client/src/app/shared/users/index.ts b/client/src/app/shared/users/index.ts
index 7b5a67bc7..ebd715fb1 100644
--- a/client/src/app/shared/users/index.ts
+++ b/client/src/app/shared/users/index.ts
@@ -1,2 +1,3 @@
1export * from './user.model' 1export * from './user.model'
2export * from './user.service' 2export * from './user.service'
3export * from './user-notifications.component'
diff --git a/client/src/app/shared/users/user-history.service.ts b/client/src/app/shared/users/user-history.service.ts
new file mode 100644
index 000000000..9ed25bfc7
--- /dev/null
+++ b/client/src/app/shared/users/user-history.service.ts
@@ -0,0 +1,45 @@
1import { HttpClient, HttpParams } from '@angular/common/http'
2import { Injectable } from '@angular/core'
3import { environment } from '../../../environments/environment'
4import { RestExtractor } from '../rest/rest-extractor.service'
5import { RestService } from '../rest/rest.service'
6import { Video } from '../video/video.model'
7import { catchError, map, switchMap } from 'rxjs/operators'
8import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
9import { VideoService } from '@app/shared/video/video.service'
10import { ResultList } from '../../../../../shared'
11
12@Injectable()
13export class UserHistoryService {
14 static BASE_USER_VIDEOS_HISTORY_URL = environment.apiUrl + '/api/v1/users/me/history/videos'
15
16 constructor (
17 private authHttp: HttpClient,
18 private restExtractor: RestExtractor,
19 private restService: RestService,
20 private videoService: VideoService
21 ) {}
22
23 getUserVideosHistory (historyPagination: ComponentPagination) {
24 const pagination = this.restService.componentPaginationToRestPagination(historyPagination)
25
26 let params = new HttpParams()
27 params = this.restService.addRestGetParams(params, pagination)
28
29 return this.authHttp
30 .get<ResultList<Video>>(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL, { params })
31 .pipe(
32 switchMap(res => this.videoService.extractVideos(res)),
33 catchError(err => this.restExtractor.handleError(err))
34 )
35 }
36
37 deleteUserVideosHistory () {
38 return this.authHttp
39 .post(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL + '/remove', {})
40 .pipe(
41 map(() => this.restExtractor.extractDataBool()),
42 catchError(err => this.restExtractor.handleError(err))
43 )
44 }
45}
diff --git a/client/src/app/shared/users/user-notification.model.ts b/client/src/app/shared/users/user-notification.model.ts
new file mode 100644
index 000000000..125d2120c
--- /dev/null
+++ b/client/src/app/shared/users/user-notification.model.ts
@@ -0,0 +1,155 @@
1import { UserNotification as UserNotificationServer, UserNotificationType, VideoInfo, ActorInfo } from '../../../../../shared'
2import { Actor } from '@app/shared/actor/actor.model'
3
4export class UserNotification implements UserNotificationServer {
5 id: number
6 type: UserNotificationType
7 read: boolean
8
9 video?: VideoInfo & {
10 channel: ActorInfo & { avatarUrl?: string }
11 }
12
13 videoImport?: {
14 id: number
15 video?: VideoInfo
16 torrentName?: string
17 magnetUri?: string
18 targetUrl?: string
19 }
20
21 comment?: {
22 id: number
23 threadId: number
24 account: ActorInfo & { avatarUrl?: string }
25 video: VideoInfo
26 }
27
28 videoAbuse?: {
29 id: number
30 video: VideoInfo
31 }
32
33 videoBlacklist?: {
34 id: number
35 video: VideoInfo
36 }
37
38 account?: ActorInfo & { avatarUrl?: string }
39
40 actorFollow?: {
41 id: number
42 follower: ActorInfo & { avatarUrl?: string }
43 following: {
44 type: 'account' | 'channel'
45 name: string
46 displayName: string
47 }
48 }
49
50 createdAt: string
51 updatedAt: string
52
53 // Additional fields
54 videoUrl?: string
55 commentUrl?: any[]
56 videoAbuseUrl?: string
57 accountUrl?: string
58 videoImportIdentifier?: string
59 videoImportUrl?: string
60
61 constructor (hash: UserNotificationServer) {
62 this.id = hash.id
63 this.type = hash.type
64 this.read = hash.read
65
66 this.video = hash.video
67 if (this.video) this.setAvatarUrl(this.video.channel)
68
69 this.videoImport = hash.videoImport
70
71 this.comment = hash.comment
72 if (this.comment) this.setAvatarUrl(this.comment.account)
73
74 this.videoAbuse = hash.videoAbuse
75
76 this.videoBlacklist = hash.videoBlacklist
77
78 this.account = hash.account
79 if (this.account) this.setAvatarUrl(this.account)
80
81 this.actorFollow = hash.actorFollow
82 if (this.actorFollow) this.setAvatarUrl(this.actorFollow.follower)
83
84 this.createdAt = hash.createdAt
85 this.updatedAt = hash.updatedAt
86
87 switch (this.type) {
88 case UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION:
89 this.videoUrl = this.buildVideoUrl(this.video)
90 break
91
92 case UserNotificationType.UNBLACKLIST_ON_MY_VIDEO:
93 this.videoUrl = this.buildVideoUrl(this.video)
94 break
95
96 case UserNotificationType.NEW_COMMENT_ON_MY_VIDEO:
97 case UserNotificationType.COMMENT_MENTION:
98 this.accountUrl = this.buildAccountUrl(this.comment.account)
99 this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ]
100 break
101
102 case UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS:
103 this.videoAbuseUrl = '/admin/moderation/video-abuses/list'
104 this.videoUrl = this.buildVideoUrl(this.videoAbuse.video)
105 break
106
107 case UserNotificationType.BLACKLIST_ON_MY_VIDEO:
108 this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video)
109 break
110
111 case UserNotificationType.MY_VIDEO_PUBLISHED:
112 this.videoUrl = this.buildVideoUrl(this.video)
113 break
114
115 case UserNotificationType.MY_VIDEO_IMPORT_SUCCESS:
116 this.videoImportUrl = this.buildVideoImportUrl()
117 this.videoImportIdentifier = this.buildVideoImportIdentifier(this.videoImport)
118 this.videoUrl = this.buildVideoUrl(this.videoImport.video)
119 break
120
121 case UserNotificationType.MY_VIDEO_IMPORT_ERROR:
122 this.videoImportUrl = this.buildVideoImportUrl()
123 this.videoImportIdentifier = this.buildVideoImportIdentifier(this.videoImport)
124 break
125
126 case UserNotificationType.NEW_USER_REGISTRATION:
127 this.accountUrl = this.buildAccountUrl(this.account)
128 break
129
130 case UserNotificationType.NEW_FOLLOW:
131 this.accountUrl = this.buildAccountUrl(this.actorFollow.follower)
132 break
133 }
134 }
135
136 private buildVideoUrl (video: { uuid: string }) {
137 return '/videos/watch/' + video.uuid
138 }
139
140 private buildAccountUrl (account: { name: string, host: string }) {
141 return '/accounts/' + Actor.CREATE_BY_STRING(account.name, account.host)
142 }
143
144 private buildVideoImportUrl () {
145 return '/my-account/video-imports'
146 }
147
148 private buildVideoImportIdentifier (videoImport: { targetUrl?: string, magnetUri?: string, torrentName?: string }) {
149 return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName
150 }
151
152 private setAvatarUrl (actor: { avatarUrl?: string, avatar?: { path: string } }) {
153 actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor)
154 }
155}
diff --git a/client/src/app/shared/users/user-notification.service.ts b/client/src/app/shared/users/user-notification.service.ts
new file mode 100644
index 000000000..f8a30955d
--- /dev/null
+++ b/client/src/app/shared/users/user-notification.service.ts
@@ -0,0 +1,86 @@
1import { Injectable } from '@angular/core'
2import { HttpClient, HttpParams } from '@angular/common/http'
3import { RestExtractor, RestService } from '../rest'
4import { catchError, map, tap } from 'rxjs/operators'
5import { environment } from '../../../environments/environment'
6import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '../../../../../shared'
7import { UserNotification } from './user-notification.model'
8import { AuthService } from '../../core'
9import { ComponentPagination } from '../rest/component-pagination.model'
10import { User } from '..'
11import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service'
12
13@Injectable()
14export class UserNotificationService {
15 static BASE_NOTIFICATIONS_URL = environment.apiUrl + '/api/v1/users/me/notifications'
16 static BASE_NOTIFICATION_SETTINGS = environment.apiUrl + '/api/v1/users/me/notification-settings'
17
18 constructor (
19 private auth: AuthService,
20 private authHttp: HttpClient,
21 private restExtractor: RestExtractor,
22 private restService: RestService,
23 private userNotificationSocket: UserNotificationSocket
24 ) {}
25
26 listMyNotifications (pagination: ComponentPagination, unread?: boolean, ignoreLoadingBar = false) {
27 let params = new HttpParams()
28 params = this.restService.addRestGetParams(params, this.restService.componentPaginationToRestPagination(pagination))
29
30 if (unread) params = params.append('unread', `${unread}`)
31
32 const headers = ignoreLoadingBar ? { ignoreLoadingBar: '' } : undefined
33
34 return this.authHttp.get<ResultList<UserNotification>>(UserNotificationService.BASE_NOTIFICATIONS_URL, { params, headers })
35 .pipe(
36 map(res => this.restExtractor.convertResultListDateToHuman(res)),
37 map(res => this.restExtractor.applyToResultListData(res, this.formatNotification.bind(this))),
38 catchError(err => this.restExtractor.handleError(err))
39 )
40 }
41
42 countUnreadNotifications () {
43 return this.listMyNotifications({ currentPage: 1, itemsPerPage: 0 }, true)
44 .pipe(map(n => n.total))
45 }
46
47 markAsRead (notification: UserNotification) {
48 const url = UserNotificationService.BASE_NOTIFICATIONS_URL + '/read'
49
50 const body = { ids: [ notification.id ] }
51 const headers = { ignoreLoadingBar: '' }
52
53 return this.authHttp.post(url, body, { headers })
54 .pipe(
55 map(this.restExtractor.extractDataBool),
56 tap(() => this.userNotificationSocket.dispatch('read')),
57 catchError(res => this.restExtractor.handleError(res))
58 )
59 }
60
61 markAllAsRead () {
62 const url = UserNotificationService.BASE_NOTIFICATIONS_URL + '/read-all'
63 const headers = { ignoreLoadingBar: '' }
64
65 return this.authHttp.post(url, {}, { headers })
66 .pipe(
67 map(this.restExtractor.extractDataBool),
68 tap(() => this.userNotificationSocket.dispatch('read-all')),
69 catchError(res => this.restExtractor.handleError(res))
70 )
71 }
72
73 updateNotificationSettings (user: User, settings: UserNotificationSetting) {
74 const url = UserNotificationService.BASE_NOTIFICATION_SETTINGS
75
76 return this.authHttp.put(url, settings)
77 .pipe(
78 map(this.restExtractor.extractDataBool),
79 catchError(res => this.restExtractor.handleError(res))
80 )
81 }
82
83 private formatNotification (notification: UserNotificationServer) {
84 return new UserNotification(notification)
85 }
86}
diff --git a/client/src/app/shared/users/user-notifications.component.html b/client/src/app/shared/users/user-notifications.component.html
new file mode 100644
index 000000000..0d69e0feb
--- /dev/null
+++ b/client/src/app/shared/users/user-notifications.component.html
@@ -0,0 +1,101 @@
1<div *ngIf="componentPagination.totalItems === 0" class="no-notification" i18n>You don't have notifications.</div>
2
3<div class="notifications" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()">
4 <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)">
5
6 <ng-container [ngSwitch]="notification.type">
7 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION">
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>
13 </ng-container>
14
15 <ng-container i18n *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO">
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>
21 </ng-container>
22
23 <ng-container i18n *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO">
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>
29 </ng-container>
30
31 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS">
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>
37 </ng-container>
38
39 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO">
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>
45 </ng-container>
46
47 <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_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>
53 </ng-container>
54
55 <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS">
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>
61 </ng-container>
62
63 <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR">
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>
69 </ng-container>
70
71 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION">
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>
77 </ng-container>
78
79 <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_FOLLOW">
80 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" />
81
82 <div class="message">
83 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.actorFollow.follower.displayName }}</a> is following
84
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>
88 </ng-container>
89
90 <ng-container i18n *ngSwitchCase="UserNotificationType.COMMENT_MENTION">
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>
96 </ng-container>
97 </ng-container>
98
99 <div class="from-date">{{ notification.createdAt | myFromNow }}</div>
100 </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
new file mode 100644
index 000000000..315d504c9
--- /dev/null
+++ b/client/src/app/shared/users/user-notifications.component.scss
@@ -0,0 +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
11.notification {
12 display: flex;
13 align-items: center;
14 font-size: inherit;
15 padding: 15px 5px 15px 10px;
16 border-bottom: 1px solid rgba(0, 0, 0, 0.10);
17
18 &.unread {
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;
26
27 @include apply-svg-color(#333);
28 }
29
30 .avatar {
31 @include avatar(30px);
32
33 margin-right: 10px;
34 }
35
36 .message {
37 flex-grow: 1;
38
39 a {
40 font-weight: $font-semibold;
41 }
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 }
51}
diff --git a/client/src/app/shared/users/user-notifications.component.ts b/client/src/app/shared/users/user-notifications.component.ts
new file mode 100644
index 000000000..b5f9fd399
--- /dev/null
+++ b/client/src/app/shared/users/user-notifications.component.ts
@@ -0,0 +1,87 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { UserNotificationService } from '@app/shared/users/user-notification.service'
3import { UserNotificationType } from '../../../../../shared'
4import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model'
5import { Notifier } from '@app/core'
6import { UserNotification } from '@app/shared/users/user-notification.model'
7
8@Component({
9 selector: 'my-user-notifications',
10 templateUrl: 'user-notifications.component.html',
11 styleUrls: [ 'user-notifications.component.scss' ]
12})
13export class UserNotificationsComponent implements OnInit {
14 @Input() ignoreLoadingBar = false
15 @Input() infiniteScroll = true
16 @Input() itemsPerPage = 20
17
18 notifications: UserNotification[] = []
19
20 // So we can access it in the template
21 UserNotificationType = UserNotificationType
22
23 componentPagination: ComponentPagination
24
25 constructor (
26 private userNotificationService: UserNotificationService,
27 private notifier: Notifier
28 ) { }
29
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
37 this.loadMoreNotifications()
38 }
39
40 loadMoreNotifications () {
41 this.userNotificationService.listMyNotifications(this.componentPagination, undefined, this.ignoreLoadingBar)
42 .subscribe(
43 result => {
44 this.notifications = this.notifications.concat(result.data)
45 this.componentPagination.totalItems = result.total
46 },
47
48 err => this.notifier.error(err.message)
49 )
50 }
51
52 onNearOfBottom () {
53 if (this.infiniteScroll === false) return
54
55 this.componentPagination.currentPage++
56
57 if (hasMoreItems(this.componentPagination)) {
58 this.loadMoreNotifications()
59 }
60 }
61
62 markAsRead (notification: UserNotification) {
63 if (notification.read) return
64
65 this.userNotificationService.markAsRead(notification)
66 .subscribe(
67 () => {
68 notification.read = true
69 },
70
71 err => this.notifier.error(err.message)
72 )
73 }
74
75 markAllAsRead () {
76 this.userNotificationService.markAllAsRead()
77 .subscribe(
78 () => {
79 for (const notification of this.notifications) {
80 notification.read = true
81 }
82 },
83
84 err => this.notifier.error(err.message)
85 )
86 }
87}
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts
index 9819829fd..c15f1de8c 100644
--- a/client/src/app/shared/users/user.model.ts
+++ b/client/src/app/shared/users/user.model.ts
@@ -1,33 +1,8 @@
1import { 1import { hasUserRight, User as UserServerModel, UserNotificationSetting, UserRight, UserRole, VideoChannel } from '../../../../../shared'
2 Account as AccountServerModel,
3 hasUserRight,
4 User as UserServerModel,
5 UserRight,
6 UserRole,
7 VideoChannel
8} from '../../../../../shared'
9import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' 2import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type'
10import { Account } from '@app/shared/account/account.model' 3import { Account } from '@app/shared/account/account.model'
11import { Avatar } from '../../../../../shared/models/avatars/avatar.model' 4import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
12 5
13export type UserConstructorHash = {
14 id: number,
15 username: string,
16 email: string,
17 role: UserRole,
18 emailVerified?: boolean,
19 videoQuota?: number,
20 videoQuotaDaily?: number,
21 nsfwPolicy?: NSFWPolicyType,
22 webTorrentEnabled?: boolean,
23 autoPlayVideo?: boolean,
24 createdAt?: Date,
25 account?: AccountServerModel,
26 videoChannels?: VideoChannel[]
27
28 blocked?: boolean
29 blockedReason?: string
30}
31export class User implements UserServerModel { 6export class User implements UserServerModel {
32 id: number 7 id: number
33 username: string 8 username: string
@@ -35,8 +10,11 @@ export class User implements UserServerModel {
35 emailVerified: boolean 10 emailVerified: boolean
36 role: UserRole 11 role: UserRole
37 nsfwPolicy: NSFWPolicyType 12 nsfwPolicy: NSFWPolicyType
13
38 webTorrentEnabled: boolean 14 webTorrentEnabled: boolean
39 autoPlayVideo: boolean 15 autoPlayVideo: boolean
16 videosHistoryEnabled: boolean
17
40 videoQuota: number 18 videoQuota: number
41 videoQuotaDaily: number 19 videoQuotaDaily: number
42 account: Account 20 account: Account
@@ -46,7 +24,9 @@ export class User implements UserServerModel {
46 blocked: boolean 24 blocked: boolean
47 blockedReason?: string 25 blockedReason?: string
48 26
49 constructor (hash: UserConstructorHash) { 27 notificationSettings?: UserNotificationSetting
28
29 constructor (hash: Partial<UserServerModel>) {
50 this.id = hash.id 30 this.id = hash.id
51 this.username = hash.username 31 this.username = hash.username
52 this.email = hash.email 32 this.email = hash.email
@@ -57,11 +37,14 @@ export class User implements UserServerModel {
57 this.videoQuotaDaily = hash.videoQuotaDaily 37 this.videoQuotaDaily = hash.videoQuotaDaily
58 this.nsfwPolicy = hash.nsfwPolicy 38 this.nsfwPolicy = hash.nsfwPolicy
59 this.webTorrentEnabled = hash.webTorrentEnabled 39 this.webTorrentEnabled = hash.webTorrentEnabled
40 this.videosHistoryEnabled = hash.videosHistoryEnabled
60 this.autoPlayVideo = hash.autoPlayVideo 41 this.autoPlayVideo = hash.autoPlayVideo
61 this.createdAt = hash.createdAt 42 this.createdAt = hash.createdAt
62 this.blocked = hash.blocked 43 this.blocked = hash.blocked
63 this.blockedReason = hash.blockedReason 44 this.blockedReason = hash.blockedReason
64 45
46 this.notificationSettings = hash.notificationSettings
47
65 if (hash.account !== undefined) { 48 if (hash.account !== undefined) {
66 this.account = new Account(hash.account) 49 this.account = new Account(hash.account)
67 } 50 }
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/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 2d32dd6ad..b0633be4a 100644
--- a/client/src/app/shared/video/abstract-video-list.ts
+++ b/client/src/app/shared/video/abstract-video-list.ts
@@ -3,7 +3,6 @@ import { ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { Location } from '@angular/common' 4import { Location } from '@angular/common'
5import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' 5import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
6import { NotificationsService } from 'angular2-notifications'
7import { fromEvent, Observable, Subscription } from 'rxjs' 6import { fromEvent, Observable, Subscription } from 'rxjs'
8import { AuthService } from '../../core/auth' 7import { AuthService } from '../../core/auth'
9import { ComponentPagination } from '../rest/component-pagination.model' 8import { ComponentPagination } from '../rest/component-pagination.model'
@@ -13,6 +12,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
13import { ScreenService } from '@app/shared/misc/screen.service' 12import { ScreenService } from '@app/shared/misc/screen.service'
14import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' 13import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
15import { Syndication } from '@app/shared/video/syndication.model' 14import { Syndication } from '@app/shared/video/syndication.model'
15import { Notifier } from '@app/core'
16 16
17export abstract class AbstractVideoList implements OnInit, OnDestroy { 17export abstract class AbstractVideoList implements OnInit, OnDestroy {
18 private static LINES_PER_PAGE = 4 18 private static LINES_PER_PAGE = 4
@@ -39,11 +39,12 @@ 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
45 46
46 protected abstract notificationsService: NotificationsService 47 protected abstract notifier: Notifier
47 protected abstract authService: AuthService 48 protected abstract authService: AuthService
48 protected abstract router: Router 49 protected abstract router: Router
49 protected abstract route: ActivatedRoute 50 protected abstract route: ActivatedRoute
@@ -157,7 +158,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
157 }, 158 },
158 error => { 159 error => {
159 this.loadingPage[page] = false 160 this.loadingPage[page] = false
160 this.notificationsService.error(this.i18n('Error'), error.message) 161 this.notifier.error(error.message)
161 } 162 }
162 ) 163 )
163 } 164 }
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-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 b92c96450..6ea83d13b 100644
--- a/client/src/app/shared/video/video.model.ts
+++ b/client/src/app/shared/video/video.model.ts
@@ -53,7 +53,7 @@ export class Video implements VideoServerModel {
53 displayName: string 53 displayName: string
54 url: string 54 url: string
55 host: string 55 host: string
56 avatar: Avatar 56 avatar?: Avatar
57 } 57 }
58 58
59 channel: { 59 channel: {
@@ -63,7 +63,7 @@ export class Video implements VideoServerModel {
63 displayName: string 63 displayName: string
64 url: string 64 url: string
65 host: string 65 host: string
66 avatar: Avatar 66 avatar?: Avatar
67 } 67 }
68 68
69 userHistory?: { 69 userHistory?: {
diff --git a/client/src/app/signup/signup.component.html b/client/src/app/signup/signup.component.html
index 0207a166e..07d24b381 100644
--- a/client/src/app/signup/signup.component.html
+++ b/client/src/app/signup/signup.component.html
@@ -64,7 +64,7 @@
64 </form> 64 </form>
65 65
66 <div> 66 <div>
67 <label for="email" i18n>Features found on this instance</label> 67 <label i18n>Features found on this instance</label>
68 <my-instance-features-table></my-instance-features-table> 68 <my-instance-features-table></my-instance-features-table>
69 </div> 69 </div>
70 </div> 70 </div>
diff --git a/client/src/app/signup/signup.component.ts b/client/src/app/signup/signup.component.ts
index 3341d4e09..13941ec79 100644
--- a/client/src/app/signup/signup.component.ts
+++ b/client/src/app/signup/signup.component.ts
@@ -1,8 +1,7 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { AuthService, Notifier, RedirectService, ServerService } from '@app/core'
3import { UserCreate } from '../../../../shared' 3import { UserCreate } from '../../../../shared'
4import { FormReactive, UserService, UserValidatorsService } from '../shared' 4import { FormReactive, UserService, UserValidatorsService } from '../shared'
5import { AuthService, RedirectService, ServerService } from '@app/core'
6import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
7import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
8 7
@@ -20,7 +19,7 @@ export class SignupComponent extends FormReactive implements OnInit {
20 protected formValidatorService: FormValidatorService, 19 protected formValidatorService: FormValidatorService,
21 private authService: AuthService, 20 private authService: AuthService,
22 private userValidatorsService: UserValidatorsService, 21 private userValidatorsService: UserValidatorsService,
23 private notificationsService: NotificationsService, 22 private notifier: Notifier,
24 private userService: UserService, 23 private userService: UserService,
25 private serverService: ServerService, 24 private serverService: ServerService,
26 private redirectService: RedirectService, 25 private redirectService: RedirectService,
@@ -64,10 +63,7 @@ export class SignupComponent extends FormReactive implements OnInit {
64 this.authService.login(userCreate.username, userCreate.password) 63 this.authService.login(userCreate.username, userCreate.password)
65 .subscribe( 64 .subscribe(
66 () => { 65 () => {
67 this.notificationsService.success( 66 this.notifier.success(this.i18n('You are now logged in as {{username}}!', { username: userCreate.username }))
68 this.i18n('Success'),
69 this.i18n('You are now logged in as {{username}}!', { username: userCreate.username })
70 )
71 67
72 this.redirectService.redirectToHomepage() 68 this.redirectService.redirectToHomepage()
73 }, 69 },
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 33c766d87..092c0e862 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
@@ -126,6 +126,7 @@
126 ></my-peertube-checkbox> 126 ></my-peertube-checkbox>
127 127
128 <my-peertube-checkbox 128 <my-peertube-checkbox
129 *ngIf="waitTranscodingEnabled"
129 inputName="waitTranscoding" formControlName="waitTranscoding" 130 inputName="waitTranscoding" formControlName="waitTranscoding"
130 i18n-labelText labelText="Wait transcoding before publishing the video" 131 i18n-labelText labelText="Wait transcoding before publishing the video"
131 i18n-helpHtml helpHtml="If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends." 132 i18n-helpHtml helpHtml="If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends."
@@ -142,7 +143,7 @@
142 143
143 <div class="captions-header"> 144 <div class="captions-header">
144 <a (click)="openAddCaptionModal()" class="create-caption"> 145 <a (click)="openAddCaptionModal()" class="create-caption">
145 <span class="icon icon-add"></span> 146 <my-global-icon iconName="add"></my-global-icon>
146 <ng-container i18n>Add another caption</ng-container> 147 <ng-container i18n>Add another caption</ng-container>
147 </a> 148 </a>
148 </div> 149 </div>
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.scss b/client/src/app/videos/+video-edit/shared/video-edit.component.scss
index 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 a56733e57..85e015901 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.ts
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts
@@ -2,7 +2,7 @@ import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'
2import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms' 2import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared' 4import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared'
5import { NotificationsService } from 'angular2-notifications' 5import { Notifier } from '@app/core'
6import { ServerService } from '../../../core/server' 6import { ServerService } from '../../../core/server'
7import { VideoEdit } from '../../../shared/video/video-edit.model' 7import { VideoEdit } from '../../../shared/video/video-edit.model'
8import { map } from 'rxjs/operators' 8import { map } from 'rxjs/operators'
@@ -27,6 +27,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
27 @Input() userVideoChannels: { id: number, label: string, support: string }[] = [] 27 @Input() userVideoChannels: { id: number, label: string, support: string }[] = []
28 @Input() schedulePublicationPossible = true 28 @Input() schedulePublicationPossible = true
29 @Input() videoCaptions: VideoCaptionEdit[] = [] 29 @Input() videoCaptions: VideoCaptionEdit[] = []
30 @Input() waitTranscodingEnabled = true
30 31
31 @ViewChild('videoCaptionAddModal') videoCaptionAddModal: VideoCaptionAddModalComponent 32 @ViewChild('videoCaptionAddModal') videoCaptionAddModal: VideoCaptionAddModalComponent
32 33
@@ -58,7 +59,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
58 private videoCaptionService: VideoCaptionService, 59 private videoCaptionService: VideoCaptionService,
59 private route: ActivatedRoute, 60 private route: ActivatedRoute,
60 private router: Router, 61 private router: Router,
61 private notificationsService: NotificationsService, 62 private notifier: Notifier,
62 private serverService: ServerService, 63 private serverService: ServerService,
63 private i18nPrimengCalendarService: I18nPrimengCalendarService 64 private i18nPrimengCalendarService: I18nPrimengCalendarService
64 ) { 65 ) {
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 13776ae36..307806bb9 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
@@ -1,8 +1,7 @@
1import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications'
4import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos' 3import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos'
5import { AuthService, ServerService } from '../../../core' 4import { AuthService, Notifier, ServerService } from '../../../core'
6import { VideoService } from '../../../shared/video/video.service' 5import { VideoService } from '../../../shared/video/video.service'
7import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
8import { LoadingBarService } from '@ngx-loading-bar/core' 7import { LoadingBarService } from '@ngx-loading-bar/core'
@@ -19,7 +18,8 @@ import { scrollToTop } from '@app/shared/misc/utils'
19 templateUrl: './video-import-torrent.component.html', 18 templateUrl: './video-import-torrent.component.html',
20 styleUrls: [ 19 styleUrls: [
21 '../shared/video-edit.component.scss', 20 '../shared/video-edit.component.scss',
22 './video-import-torrent.component.scss' 21 './video-import-torrent.component.scss',
22 './video-send.scss'
23 ] 23 ]
24}) 24})
25export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { 25export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate {
@@ -41,7 +41,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
41 constructor ( 41 constructor (
42 protected formValidatorService: FormValidatorService, 42 protected formValidatorService: FormValidatorService,
43 protected loadingBar: LoadingBarService, 43 protected loadingBar: LoadingBarService,
44 protected notificationsService: NotificationsService, 44 protected notifier: Notifier,
45 protected authService: AuthService, 45 protected authService: AuthService,
46 protected serverService: ServerService, 46 protected serverService: ServerService,
47 protected videoService: VideoService, 47 protected videoService: VideoService,
@@ -107,7 +107,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
107 this.loadingBar.complete() 107 this.loadingBar.complete()
108 this.isImportingVideo = false 108 this.isImportingVideo = false
109 this.firstStepError.emit() 109 this.firstStepError.emit()
110 this.notificationsService.error(this.i18n('Error'), err.message) 110 this.notifier.error(err.message)
111 } 111 }
112 ) 112 )
113 } 113 }
@@ -126,7 +126,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
126 .subscribe( 126 .subscribe(
127 () => { 127 () => {
128 this.isUpdatingVideo = false 128 this.isUpdatingVideo = false
129 this.notificationsService.success(this.i18n('Success'), this.i18n('Video to import updated.')) 129 this.notifier.success(this.i18n('Video to import updated.'))
130 130
131 this.router.navigate([ '/my-account', 'video-imports' ]) 131 this.router.navigate([ '/my-account', 'video-imports' ])
132 }, 132 },
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 9cdface75..257c6e5db 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
@@ -1,8 +1,7 @@
1import { Component, EventEmitter, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications'
4import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos' 3import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos'
5import { AuthService, ServerService } from '../../../core' 4import { AuthService, Notifier, ServerService } from '../../../core'
6import { VideoService } from '../../../shared/video/video.service' 5import { VideoService } from '../../../shared/video/video.service'
7import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
8import { LoadingBarService } from '@ngx-loading-bar/core' 7import { LoadingBarService } from '@ngx-loading-bar/core'
@@ -19,7 +18,7 @@ import { scrollToTop } from '@app/shared/misc/utils'
19 templateUrl: './video-import-url.component.html', 18 templateUrl: './video-import-url.component.html',
20 styleUrls: [ 19 styleUrls: [
21 '../shared/video-edit.component.scss', 20 '../shared/video-edit.component.scss',
22 './video-import-url.component.scss' 21 './video-send.scss'
23 ] 22 ]
24}) 23})
25export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { 24export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate {
@@ -40,7 +39,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
40 constructor ( 39 constructor (
41 protected formValidatorService: FormValidatorService, 40 protected formValidatorService: FormValidatorService,
42 protected loadingBar: LoadingBarService, 41 protected loadingBar: LoadingBarService,
43 protected notificationsService: NotificationsService, 42 protected notifier: Notifier,
44 protected authService: AuthService, 43 protected authService: AuthService,
45 protected serverService: ServerService, 44 protected serverService: ServerService,
46 protected videoService: VideoService, 45 protected videoService: VideoService,
@@ -99,7 +98,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
99 this.loadingBar.complete() 98 this.loadingBar.complete()
100 this.isImportingVideo = false 99 this.isImportingVideo = false
101 this.firstStepError.emit() 100 this.firstStepError.emit()
102 this.notificationsService.error(this.i18n('Error'), err.message) 101 this.notifier.error(err.message)
103 } 102 }
104 ) 103 )
105 } 104 }
@@ -118,7 +117,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
118 .subscribe( 117 .subscribe(
119 () => { 118 () => {
120 this.isUpdatingVideo = false 119 this.isUpdatingVideo = false
121 this.notificationsService.success(this.i18n('Success'), this.i18n('Video to import updated.')) 120 this.notifier.success(this.i18n('Video to import updated.'))
122 121
123 this.router.navigate([ '/my-account', 'video-imports' ]) 122 this.router.navigate([ '/my-account', 'video-imports' ])
124 }, 123 },
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-send.ts b/client/src/app/videos/+video-edit/video-add-components/video-send.ts
index 71d2544d8..580c123a0 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-send.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-send.ts
@@ -1,10 +1,9 @@
1import { EventEmitter, OnInit } from '@angular/core' 1import { EventEmitter, OnInit } from '@angular/core'
2import { LoadingBarService } from '@ngx-loading-bar/core' 2import { LoadingBarService } from '@ngx-loading-bar/core'
3import { NotificationsService } from 'angular2-notifications' 3import { AuthService, Notifier, ServerService } from '@app/core'
4import { catchError, switchMap, tap } from 'rxjs/operators' 4import { catchError, switchMap, tap } from 'rxjs/operators'
5import { FormReactive } from '@app/shared' 5import { FormReactive } from '@app/shared'
6import { VideoConstant, VideoPrivacy } from '../../../../../../shared' 6import { VideoConstant, VideoPrivacy } from '../../../../../../shared'
7import { AuthService, ServerService } from '@app/core'
8import { VideoService } from '@app/shared/video/video.service' 7import { VideoService } from '@app/shared/video/video.service'
9import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' 8import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
10import { VideoCaptionService } from '@app/shared/video-caption' 9import { VideoCaptionService } from '@app/shared/video-caption'
@@ -25,7 +24,7 @@ export abstract class VideoSend extends FormReactive implements OnInit {
25 protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy 24 protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy
26 25
27 protected loadingBar: LoadingBarService 26 protected loadingBar: LoadingBarService
28 protected notificationsService: NotificationsService 27 protected notifier: Notifier
29 protected authService: AuthService 28 protected authService: AuthService
30 protected serverService: ServerService 29 protected serverService: ServerService
31 protected videoService: VideoService 30 protected videoService: VideoService
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 a09f54dfc..b252cd60a 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
@@ -1,12 +1,12 @@
1<div *ngIf="!isUploadingVideo" class="upload-video-container"> 1<div *ngIf="!isUploadingVideo" class="upload-video-container">
2 <div class="upload-video"> 2 <div class="first-step-block">
3 <div class="icon icon-upload"></div> 3 <my-global-icon class="upload-icon" iconName="upload"></my-global-icon>
4 4
5 <div class="button-file"> 5 <div class="button-file">
6 <span i18n>Select the file to upload</span> 6 <span i18n>Select the file to upload</span>
7 <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" /> 7 <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" />
8 </div> 8 </div>
9 <span class="button-file-extension">(.mp4, .webm, .ogv)</span> 9 <span class="button-file-extension">({{ videoExtensions }})</span>
10 10
11 <div class="form-group form-group-channel"> 11 <div class="form-group form-group-channel">
12 <label i18n for="first-step-channel">Channel</label> 12 <label i18n for="first-step-channel">Channel</label>
@@ -42,11 +42,16 @@
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
48 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" 52 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
49 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" 53 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
54 [waitTranscodingEnabled]="waitTranscodingEnabled"
50 ></my-video-edit> 55 ></my-video-edit>
51 56
52 <div class="submit-container"> 57 <div class="submit-container">
@@ -56,7 +61,7 @@
56 (click)="updateSecondStep()" 61 (click)="updateSecondStep()"
57 [ngClass]="{ disabled: isPublishingButtonDisabled() }" 62 [ngClass]="{ disabled: isPublishingButtonDisabled() }"
58 > 63 >
59 <span class="icon icon-validate"></span> 64 <my-global-icon iconName="validate"></my-global-icon>
60 <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" /> 65 <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" />
61 </div> 66 </div>
62 </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 cf1725ef9..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 {
@@ -54,9 +16,7 @@
54 16
55 /deep/ .ui-progressbar { 17 /deep/ .ui-progressbar {
56 font-size: 15px !important; 18 font-size: 15px !important;
57 color: #fff !important;
58 height: 30px !important; 19 height: 30px !important;
59 line-height: 30px !important;
60 border-radius: 3px !important; 20 border-radius: 3px !important;
61 background-color: rgba(11, 204, 41, 0.16) !important; 21 background-color: rgba(11, 204, 41, 0.16) !important;
62 22
@@ -68,6 +28,8 @@
68 text-align: left; 28 text-align: left;
69 padding-left: 18px; 29 padding-left: 18px;
70 margin-top: 0 !important; 30 margin-top: 0 !important;
31 color: #fff !important;
32 line-height: 30px !important;
71 } 33 }
72 } 34 }
73 35
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 3fcb71ac3..e4d54b654 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
@@ -2,11 +2,10 @@ import { HttpEventType, HttpResponse } from '@angular/common/http'
2import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' 2import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
3import { Router } from '@angular/router' 3import { Router } from '@angular/router'
4import { LoadingBarService } from '@ngx-loading-bar/core' 4import { LoadingBarService } from '@ngx-loading-bar/core'
5import { NotificationsService } from 'angular2-notifications'
6import { BytesPipe } from 'ngx-pipes' 5import { BytesPipe } from 'ngx-pipes'
7import { Subscription } from 'rxjs' 6import { Subscription } from 'rxjs'
8import { VideoPrivacy } from '../../../../../../shared/models/videos' 7import { VideoPrivacy } from '../../../../../../shared/models/videos'
9import { AuthService, ServerService } from '../../../core' 8import { AuthService, Notifier, ServerService } from '../../../core'
10import { VideoEdit } from '../../../shared/video/video-edit.model' 9import { VideoEdit } from '../../../shared/video/video-edit.model'
11import { VideoService } from '../../../shared/video/video.service' 10import { VideoService } from '../../../shared/video/video.service'
12import { I18n } from '@ngx-translate/i18n-polyfill' 11import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -21,7 +20,8 @@ import { scrollToTop } from '@app/shared/misc/utils'
21 templateUrl: './video-upload.component.html', 20 templateUrl: './video-upload.component.html',
22 styleUrls: [ 21 styleUrls: [
23 '../shared/video-edit.component.scss', 22 '../shared/video-edit.component.scss',
24 './video-upload.component.scss' 23 './video-upload.component.scss',
24 './video-send.scss'
25 ] 25 ]
26}) 26})
27export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { 27export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate {
@@ -44,6 +44,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
44 id: 0, 44 id: 0,
45 uuid: '' 45 uuid: ''
46 } 46 }
47 waitTranscodingEnabled = true
47 48
48 error: string 49 error: string
49 50
@@ -52,7 +53,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
52 constructor ( 53 constructor (
53 protected formValidatorService: FormValidatorService, 54 protected formValidatorService: FormValidatorService,
54 protected loadingBar: LoadingBarService, 55 protected loadingBar: LoadingBarService,
55 protected notificationsService: NotificationsService, 56 protected notifier: Notifier,
56 protected authService: AuthService, 57 protected authService: AuthService,
57 protected serverService: ServerService, 58 protected serverService: ServerService,
58 protected videoService: VideoService, 59 protected videoService: VideoService,
@@ -109,7 +110,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
109 this.isUploadingVideo = false 110 this.isUploadingVideo = false
110 this.videoUploadPercents = 0 111 this.videoUploadPercents = 0
111 this.videoUploadObservable = null 112 this.videoUploadObservable = null
112 this.notificationsService.info(this.i18n('Info'), this.i18n('Upload cancelled')) 113 this.notifier.info(this.i18n('Upload cancelled'))
113 } 114 }
114 } 115 }
115 116
@@ -117,12 +118,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
117 const videofile = this.videofileInput.nativeElement.files[0] 118 const videofile = this.videofileInput.nativeElement.files[0]
118 if (!videofile) return 119 if (!videofile) return
119 120
120 // Cannot upload videos > 8GB for now 121 // Check global user quota
121 if (videofile.size > 8 * 1024 * 1024 * 1024) {
122 this.notificationsService.error(this.i18n('Error'), this.i18n('We are sorry but PeerTube cannot handle videos > 8GB'))
123 return
124 }
125
126 const bytePipes = new BytesPipe() 122 const bytePipes = new BytesPipe()
127 const videoQuota = this.authService.getUser().videoQuota 123 const videoQuota = this.authService.getUser().videoQuota
128 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) { 124 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
@@ -134,10 +130,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
134 videoQuota: bytePipes.transform(videoQuota, 0) 130 videoQuota: bytePipes.transform(videoQuota, 0)
135 } 131 }
136 ) 132 )
137 this.notificationsService.error(this.i18n('Error'), msg) 133 this.notifier.error(msg)
138 return 134 return
139 } 135 }
140 136
137 // Check daily user quota
141 const videoQuotaDaily = this.authService.getUser().videoQuotaDaily 138 const videoQuotaDaily = this.authService.getUser().videoQuotaDaily
142 if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) { 139 if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) {
143 const msg = this.i18n( 140 const msg = this.i18n(
@@ -148,10 +145,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
148 quotaDaily: bytePipes.transform(videoQuotaDaily, 0) 145 quotaDaily: bytePipes.transform(videoQuotaDaily, 0)
149 } 146 }
150 ) 147 )
151 this.notificationsService.error(this.i18n('Error'), msg) 148 this.notifier.error(msg)
152 return 149 return
153 } 150 }
154 151
152 // Build name field
155 const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '') 153 const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '')
156 let name: string 154 let name: string
157 155
@@ -159,6 +157,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
159 if (nameWithoutExtension.length < 3) name = videofile.name 157 if (nameWithoutExtension.length < 3) name = videofile.name
160 else name = nameWithoutExtension 158 else name = nameWithoutExtension
161 159
160 // Force user to wait transcoding for unsupported video types in web browsers
161 if (!videofile.name.endsWith('.mp4') && !videofile.name.endsWith('.webm') && !videofile.name.endsWith('.ogv')) {
162 this.waitTranscodingEnabled = false
163 }
164
162 const privacy = this.firstStepPrivacyId.toString() 165 const privacy = this.firstStepPrivacyId.toString()
163 const nsfw = false 166 const nsfw = false
164 const waitTranscoding = true 167 const waitTranscoding = true
@@ -206,7 +209,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
206 this.videoUploadPercents = 0 209 this.videoUploadPercents = 0
207 this.videoUploadObservable = null 210 this.videoUploadObservable = null
208 this.firstStepError.emit() 211 this.firstStepError.emit()
209 this.notificationsService.error(this.i18n('Error'), err.message) 212 this.notifier.error(err.message)
210 } 213 }
211 ) 214 )
212 } 215 }
@@ -235,7 +238,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
235 this.isUpdatingVideo = false 238 this.isUpdatingVideo = false
236 this.isUploadingVideo = false 239 this.isUploadingVideo = false
237 240
238 this.notificationsService.success(this.i18n('Success'), this.i18n('Video published.')) 241 this.notifier.success(this.i18n('Video published.'))
239 this.router.navigate([ '/videos/watch', video.uuid ]) 242 this.router.navigate([ '/videos/watch', video.uuid ])
240 }, 243 },
241 244
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 9242c30a0..4992bb369 100644
--- a/client/src/app/videos/+video-edit/video-update.component.html
+++ b/client/src/app/videos/+video-edit/video-update.component.html
@@ -8,12 +8,12 @@
8 <my-video-edit 8 <my-video-edit
9 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible" 9 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible"
10 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" 10 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
11 [videoCaptions]="videoCaptions" 11 [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled"
12 ></my-video-edit> 12 ></my-video-edit>
13 13
14 <div class="submit-container"> 14 <div class="submit-container">
15 <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"> 15 <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }">
16 <span class="icon icon-validate"></span> 16 <my-global-icon iconName="validate"></my-global-icon>
17 <input type="button" i18n-value value="Update" /> 17 <input type="button" i18n-value value="Update" />
18 </div> 18 </div>
19 </div> 19 </div>
diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts
index 3a0f3a39a..9e849014e 100644
--- a/client/src/app/videos/+video-edit/video-update.component.ts
+++ b/client/src/app/videos/+video-edit/video-update.component.ts
@@ -1,8 +1,8 @@
1import { map, switchMap } from 'rxjs/operators' 1import { map, switchMap } from 'rxjs/operators'
2import { Component, OnInit } from '@angular/core' 2import { Component, HostListener, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { LoadingBarService } from '@ngx-loading-bar/core' 4import { LoadingBarService } from '@ngx-loading-bar/core'
5import { NotificationsService } from 'angular2-notifications' 5import { Notifier } from '@app/core'
6import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos' 6import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos'
7import { ServerService } from '../../core' 7import { ServerService } from '../../core'
8import { FormReactive } from '../../shared' 8import { FormReactive } from '../../shared'
@@ -12,6 +12,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
12import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 12import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
13import { VideoCaptionService } from '@app/shared/video-caption' 13import { VideoCaptionService } from '@app/shared/video-caption'
14import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' 14import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
15import { VideoDetails } from '@app/shared/video/video-details.model'
15 16
16@Component({ 17@Component({
17 selector: 'my-videos-update', 18 selector: 'my-videos-update',
@@ -26,6 +27,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
26 userVideoChannels: { id: number, label: string, support: string }[] = [] 27 userVideoChannels: { id: number, label: string, support: string }[] = []
27 schedulePublicationPossible = false 28 schedulePublicationPossible = false
28 videoCaptions: VideoCaptionEdit[] = [] 29 videoCaptions: VideoCaptionEdit[] = []
30 waitTranscodingEnabled = true
29 31
30 private updateDone = false 32 private updateDone = false
31 33
@@ -33,7 +35,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
33 protected formValidatorService: FormValidatorService, 35 protected formValidatorService: FormValidatorService,
34 private route: ActivatedRoute, 36 private route: ActivatedRoute,
35 private router: Router, 37 private router: Router,
36 private notificationsService: NotificationsService, 38 private notifier: Notifier,
37 private serverService: ServerService, 39 private serverService: ServerService,
38 private videoService: VideoService, 40 private videoService: VideoService,
39 private loadingBar: LoadingBarService, 41 private loadingBar: LoadingBarService,
@@ -65,25 +67,42 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
65 67
66 this.videoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies) 68 this.videoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies)
67 69
70 const videoFiles = (video as VideoDetails).files
71 if (videoFiles.length > 1) { // Already transcoded
72 this.waitTranscodingEnabled = false
73 }
74
68 // FIXME: Angular does not detect the change inside this subscription, so use the patched setTimeout 75 // FIXME: Angular does not detect the change inside this subscription, so use the patched setTimeout
69 setTimeout(() => this.hydrateFormFromVideo()) 76 setTimeout(() => this.hydrateFormFromVideo())
70 }, 77 },
71 78
72 err => { 79 err => {
73 console.error(err) 80 console.error(err)
74 this.notificationsService.error(this.i18n('Error'), err.message) 81 this.notifier.error(err.message)
75 } 82 }
76 ) 83 )
77 } 84 }
78 85
79 canDeactivate () { 86 @HostListener('window:beforeunload', [ '$event' ])
87 onUnload (event: any) {
88 const { text, canDeactivate } = this.canDeactivate()
89
90 if (canDeactivate) return
91
92 event.returnValue = text
93 return text
94 }
95
96 canDeactivate (): { canDeactivate: boolean, text?: string } {
80 if (this.updateDone === true) return { canDeactivate: true } 97 if (this.updateDone === true) return { canDeactivate: true }
81 98
99 const text = this.i18n('You have unsaved changes! If you leave, your changes will be lost.')
100
82 for (const caption of this.videoCaptions) { 101 for (const caption of this.videoCaptions) {
83 if (caption.action) return { canDeactivate: false } 102 if (caption.action) return { canDeactivate: false, text }
84 } 103 }
85 104
86 return { canDeactivate: this.formChanged === false } 105 return { canDeactivate: this.formChanged === false, text }
87 } 106 }
88 107
89 checkForm () { 108 checkForm () {
@@ -114,14 +133,14 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
114 this.updateDone = true 133 this.updateDone = true
115 this.isUpdatingVideo = false 134 this.isUpdatingVideo = false
116 this.loadingBar.complete() 135 this.loadingBar.complete()
117 this.notificationsService.success(this.i18n('Success'), this.i18n('Video updated.')) 136 this.notifier.success(this.i18n('Video updated.'))
118 this.router.navigate([ '/videos/watch', this.video.uuid ]) 137 this.router.navigate([ '/videos/watch', this.video.uuid ])
119 }, 138 },
120 139
121 err => { 140 err => {
122 this.loadingBar.complete() 141 this.loadingBar.complete()
123 this.isUpdatingVideo = false 142 this.isUpdatingVideo = false
124 this.notificationsService.error(this.i18n('Error'), err.message) 143 this.notifier.error(err.message)
125 console.error(err) 144 console.error(err)
126 } 145 }
127 ) 146 )
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
index 6db0eb55d..fd85c28f2 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
@@ -1,6 +1,6 @@
1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier } from '@app/core'
4import { Observable } from 'rxjs' 4import { Observable } from 'rxjs'
5import { VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model' 5import { VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model'
6import { FormReactive } from '../../../shared' 6import { FormReactive } from '../../../shared'
@@ -36,7 +36,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
36 constructor ( 36 constructor (
37 protected formValidatorService: FormValidatorService, 37 protected formValidatorService: FormValidatorService,
38 private videoCommentValidatorsService: VideoCommentValidatorsService, 38 private videoCommentValidatorsService: VideoCommentValidatorsService,
39 private notificationsService: NotificationsService, 39 private notifier: Notifier,
40 private videoCommentService: VideoCommentService, 40 private videoCommentService: VideoCommentService,
41 private authService: AuthService, 41 private authService: AuthService,
42 private modalService: NgbModal, 42 private modalService: NgbModal,
@@ -70,7 +70,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
70 } 70 }
71 71
72 onValidKey () { 72 onValidKey () {
73 this.onValueChanged() 73 this.check()
74 if (!this.form.valid) return 74 if (!this.form.valid) return
75 75
76 this.formValidated() 76 this.formValidated()
@@ -115,7 +115,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
115 err => { 115 err => {
116 this.addingComment = false 116 this.addingComment = false
117 117
118 this.notificationsService.error(this.i18n('Error'), err.text) 118 this.notifier.error(err.text)
119 } 119 }
120 ) 120 )
121 } 121 }
@@ -135,7 +135,6 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
135 135
136 gotoLogin () { 136 gotoLogin () {
137 this.hideVisitorModal() 137 this.hideVisitorModal()
138 this.authService.redirectUrl = this.router.url
139 this.router.navigate([ '/login' ]) 138 this.router.navigate([ '/login' ])
140 } 139 }
141 140
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.scss b/client/src/app/videos/+video-watch/comment/video-comment.component.scss
index 84da5727e..731ecbf8f 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.component.scss
+++ b/client/src/app/videos/+video-watch/comment/video-comment.component.scss
@@ -41,7 +41,7 @@
41 } 41 }
42 42
43 .comment-date { 43 .comment-date {
44 color: #585858; 44 color: $grey-foreground-color;
45 margin-left: 10px; 45 margin-left: 10px;
46 } 46 }
47 } 47 }
@@ -69,7 +69,7 @@
69 69
70 .comment-action-reply, 70 .comment-action-reply,
71 .comment-action-delete { 71 .comment-action-delete {
72 color: #585858; 72 color: $grey-foreground-color;
73 cursor: pointer; 73 cursor: pointer;
74 margin-right: 10px; 74 margin-right: 10px;
75 75
@@ -108,4 +108,4 @@
108 .root-comment { 108 .root-comment {
109 font-size: 14px; 109 font-size: 14px;
110 } 110 }
111} \ No newline at end of file 111}
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.ts b/client/src/app/videos/+video-watch/comment/video-comment.component.ts
index 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/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts
index 8850eccd8..2616820d2 100644
--- a/client/src/app/videos/+video-watch/comment/video-comments.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comments.component.ts
@@ -1,11 +1,10 @@
1import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild, ElementRef } from '@angular/core' 1import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core'
2import { ActivatedRoute } from '@angular/router' 2import { ActivatedRoute } from '@angular/router'
3import { ConfirmService } from '@app/core' 3import { ConfirmService, Notifier } from '@app/core'
4import { NotificationsService } from 'angular2-notifications'
5import { Subscription } from 'rxjs' 4import { Subscription } from 'rxjs'
6import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' 5import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model'
7import { AuthService } from '../../../core/auth' 6import { AuthService } from '../../../core/auth'
8import { ComponentPagination } from '../../../shared/rest/component-pagination.model' 7import { ComponentPagination, hasMoreItems } from '../../../shared/rest/component-pagination.model'
9import { User } from '../../../shared/users' 8import { User } from '../../../shared/users'
10import { VideoSortField } from '../../../shared/video/sort-field.type' 9import { VideoSortField } from '../../../shared/video/sort-field.type'
11import { VideoDetails } from '../../../shared/video/video-details.model' 10import { VideoDetails } from '../../../shared/video/video-details.model'
@@ -42,7 +41,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
42 41
43 constructor ( 42 constructor (
44 private authService: AuthService, 43 private authService: AuthService,
45 private notificationsService: NotificationsService, 44 private notifier: Notifier,
46 private confirmService: ConfirmService, 45 private confirmService: ConfirmService,
47 private videoCommentService: VideoCommentService, 46 private videoCommentService: VideoCommentService,
48 private activatedRoute: ActivatedRoute, 47 private activatedRoute: ActivatedRoute,
@@ -84,15 +83,11 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
84 this.highlightedThread = new VideoComment(res.comment) 83 this.highlightedThread = new VideoComment(res.comment)
85 84
86 // Scroll to the highlighted thread 85 // Scroll to the highlighted thread
87 setTimeout(() => { 86 setTimeout(() => this.commentHighlightBlock.nativeElement.scrollIntoView(), 0)
88 // -60 because of the fixed header
89 const scrollY = this.commentHighlightBlock.nativeElement.offsetTop - 60
90 window.scroll(0, scrollY)
91 }, 500)
92 } 87 }
93 }, 88 },
94 89
95 err => this.notificationsService.error(this.i18n('Error'), err.message) 90 err => this.notifier.error(err.message)
96 ) 91 )
97 } 92 }
98 93
@@ -104,7 +99,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
104 this.componentPagination.totalItems = res.totalComments 99 this.componentPagination.totalItems = res.totalComments
105 }, 100 },
106 101
107 err => this.notificationsService.error(this.i18n('Error'), err.message) 102 err => this.notifier.error(err.message)
108 ) 103 )
109 } 104 }
110 105
@@ -155,7 +150,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
155 if (this.highlightedThread.id === commentToDelete.id) this.highlightedThread = undefined 150 if (this.highlightedThread.id === commentToDelete.id) this.highlightedThread = undefined
156 }, 151 },
157 152
158 err => this.notificationsService.error(this.i18n('Error'), err.message) 153 err => this.notifier.error(err.message)
159 ) 154 )
160 } 155 }
161 156
@@ -166,22 +161,11 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
166 onNearOfBottom () { 161 onNearOfBottom () {
167 this.componentPagination.currentPage++ 162 this.componentPagination.currentPage++
168 163
169 if (this.hasMoreComments()) { 164 if (hasMoreItems(this.componentPagination)) {
170 this.loadMoreComments() 165 this.loadMoreComments()
171 } 166 }
172 } 167 }
173 168
174 private hasMoreComments () {
175 // No results
176 if (this.componentPagination.totalItems === 0) return false
177
178 // Not loaded yet
179 if (!this.componentPagination.totalItems) return true
180
181 const maxPage = this.componentPagination.totalItems / this.componentPagination.itemsPerPage
182 return maxPage > this.componentPagination.currentPage
183 }
184
185 private deleteLocalCommentThread (parentComment: VideoCommentThreadTree, commentToDelete: VideoComment) { 169 private deleteLocalCommentThread (parentComment: VideoCommentThreadTree, commentToDelete: VideoComment) {
186 for (const commentChild of parentComment.children) { 170 for (const commentChild of parentComment.children) {
187 if (commentChild.comment.id === commentToDelete.id) { 171 if (commentChild.comment.id === commentToDelete.id) {
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html b/client/src/app/videos/+video-watch/modal/video-blacklist.component.html
index c436501b4..1a87bdcd4 100644
--- a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-blacklist.component.html
@@ -1,7 +1,7 @@
1<ng-template #modal> 1<ng-template #modal>
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Blacklist video</h4> 3 <h4 i18n class="modal-title">Blacklist video</h4>
4 <span class="close" aria-label="Close" role="button" (click)="hide()"></span> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body"> 7 <div class="modal-body">
@@ -15,6 +15,13 @@
15 </div> 15 </div>
16 </div> 16 </div>
17 17
18 <div class="form-group" *ngIf="video.isLocal">
19 <my-peertube-checkbox
20 inputName="unfederate" formControlName="unfederate"
21 i18n-labelText labelText="Unfederate the video (ask for its deletion from the remote instances)"
22 ></my-peertube-checkbox>
23 </div>
24
18 <div class="form-group inputs"> 25 <div class="form-group inputs">
19 <span i18n class="action-button action-button-cancel" (click)="hide()"> 26 <span i18n class="action-button action-button-cancel" (click)="hide()">
20 Cancel 27 Cancel
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts b/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts
index 2c123ebed..50a7cadd1 100644
--- a/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts
@@ -1,12 +1,11 @@
1import { Component, Input, OnInit, ViewChild } from '@angular/core' 1import { Component, Input, OnInit, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier, RedirectService } from '@app/core'
3import { FormReactive, VideoBlacklistService, VideoBlacklistValidatorsService } from '../../../shared/index' 3import { FormReactive, VideoBlacklistService, VideoBlacklistValidatorsService } from '../../../shared/index'
4import { VideoDetails } from '../../../shared/video/video-details.model' 4import { VideoDetails } from '../../../shared/video/video-details.model'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
9import { RedirectService } from '@app/core'
10 9
11@Component({ 10@Component({
12 selector: 'my-video-blacklist', 11 selector: 'my-video-blacklist',
@@ -27,7 +26,7 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
27 private modalService: NgbModal, 26 private modalService: NgbModal,
28 private videoBlacklistValidatorsService: VideoBlacklistValidatorsService, 27 private videoBlacklistValidatorsService: VideoBlacklistValidatorsService,
29 private videoBlacklistService: VideoBlacklistService, 28 private videoBlacklistService: VideoBlacklistService,
30 private notificationsService: NotificationsService, 29 private notifier: Notifier,
31 private redirectService: RedirectService, 30 private redirectService: RedirectService,
32 private i18n: I18n 31 private i18n: I18n
33 ) { 32 ) {
@@ -35,9 +34,12 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
35 } 34 }
36 35
37 ngOnInit () { 36 ngOnInit () {
37 const defaultValues = { unfederate: 'true' }
38
38 this.buildForm({ 39 this.buildForm({
39 reason: this.videoBlacklistValidatorsService.VIDEO_BLACKLIST_REASON 40 reason: this.videoBlacklistValidatorsService.VIDEO_BLACKLIST_REASON,
40 }) 41 unfederate: null
42 }, defaultValues)
41 } 43 }
42 44
43 show () { 45 show () {
@@ -51,16 +53,17 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
51 53
52 blacklist () { 54 blacklist () {
53 const reason = this.form.value[ 'reason' ] || undefined 55 const reason = this.form.value[ 'reason' ] || undefined
56 const unfederate = this.video.isLocal ? this.form.value[ 'unfederate' ] : undefined
54 57
55 this.videoBlacklistService.blacklistVideo(this.video.id, reason) 58 this.videoBlacklistService.blacklistVideo(this.video.id, reason, unfederate)
56 .subscribe( 59 .subscribe(
57 () => { 60 () => {
58 this.notificationsService.success(this.i18n('Success'), this.i18n('Video blacklisted.')) 61 this.notifier.success(this.i18n('Video blacklisted.'))
59 this.hide() 62 this.hide()
60 this.redirectService.redirectToHomepage() 63 this.redirectService.redirectToHomepage()
61 }, 64 },
62 65
63 err => this.notificationsService.error(this.i18n('Error'), err.message) 66 err => this.notifier.error(err.message)
64 ) 67 )
65 } 68 }
66} 69}
diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.html b/client/src/app/videos/+video-watch/modal/video-download.component.html
index f46f92a17..2bb5d6d37 100644
--- a/client/src/app/videos/+video-watch/modal/video-download.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-download.component.html
@@ -1,7 +1,7 @@
1<ng-template #modal let-hide="close"> 1<ng-template #modal let-hide="close">
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Download video</h4> 3 <h4 i18n class="modal-title">Download video</h4>
4 <span class="close" aria-hidden="true" (click)="hide()"></span> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body"> 7 <div class="modal-body">
diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.ts b/client/src/app/videos/+video-watch/modal/video-download.component.ts
index b1b2c0623..834385771 100644
--- a/client/src/app/videos/+video-watch/modal/video-download.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-download.component.ts
@@ -2,7 +2,7 @@ import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'
2import { VideoDetails } from '../../../shared/video/video-details.model' 2import { VideoDetails } from '../../../shared/video/video-details.model'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { NotificationsService } from 'angular2-notifications' 5import { Notifier } from '@app/core'
6 6
7@Component({ 7@Component({
8 selector: 'my-video-download', 8 selector: 'my-video-download',
@@ -18,7 +18,7 @@ export class VideoDownloadComponent implements OnInit {
18 resolutionId: number | string = -1 18 resolutionId: number | string = -1
19 19
20 constructor ( 20 constructor (
21 private notificationsService: NotificationsService, 21 private notifier: Notifier,
22 private modalService: NgbModal, 22 private modalService: NgbModal,
23 private i18n: I18n 23 private i18n: I18n
24 ) { } 24 ) { }
@@ -63,6 +63,6 @@ export class VideoDownloadComponent implements OnInit {
63 } 63 }
64 64
65 activateCopiedMessage () { 65 activateCopiedMessage () {
66 this.notificationsService.success(this.i18n('Success'), this.i18n('Copied')) 66 this.notifier.success(this.i18n('Copied'))
67 } 67 }
68} 68}
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.html b/client/src/app/videos/+video-watch/modal/video-report.component.html
index 8d9a49276..b9434da26 100644
--- a/client/src/app/videos/+video-watch/modal/video-report.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-report.component.html
@@ -1,11 +1,16 @@
1<ng-template #modal> 1<ng-template #modal>
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Report video</h4> 3 <h4 i18n class="modal-title">Report video</h4>
4 <span class="close" aria-label="Close" role="button" (click)="hide()"></span> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body"> 7 <div class="modal-body">
8 8
9 <div i18n class="information">
10 Your report will be sent to moderators of {{ currentHost }}.
11 <ng-container *ngIf="isRemoteVideo()"> It will be forwarded to origin instance {{ originHost }} too.</ng-container>
12 </div>
13
9 <form novalidate [formGroup]="form" (ngSubmit)="report()"> 14 <form novalidate [formGroup]="form" (ngSubmit)="report()">
10 <div class="form-group"> 15 <div class="form-group">
11 <textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }"> 16 <textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }">
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.scss b/client/src/app/videos/+video-watch/modal/video-report.component.scss
index afcdb9a16..4713660a2 100644
--- a/client/src/app/videos/+video-watch/modal/video-report.component.scss
+++ b/client/src/app/videos/+video-watch/modal/video-report.component.scss
@@ -1,6 +1,10 @@
1@import 'variables'; 1@import 'variables';
2@import 'mixins'; 2@import 'mixins';
3 3
4.information {
5 margin-bottom: 20px;
6}
7
4textarea { 8textarea {
5 @include peertube-textarea(100%, 100px); 9 @include peertube-textarea(100%, 100px);
6} 10}
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.ts b/client/src/app/videos/+video-watch/modal/video-report.component.ts
index 297afb19f..911f3b447 100644
--- a/client/src/app/videos/+video-watch/modal/video-report.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-report.component.ts
@@ -1,5 +1,5 @@
1import { Component, Input, OnInit, ViewChild } from '@angular/core' 1import { Component, Input, OnInit, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { FormReactive, VideoAbuseService } from '../../../shared/index' 3import { FormReactive, VideoAbuseService } from '../../../shared/index'
4import { VideoDetails } from '../../../shared/video/video-details.model' 4import { VideoDetails } from '../../../shared/video/video-details.model'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -27,12 +27,24 @@ export class VideoReportComponent extends FormReactive implements OnInit {
27 private modalService: NgbModal, 27 private modalService: NgbModal,
28 private videoAbuseValidatorsService: VideoAbuseValidatorsService, 28 private videoAbuseValidatorsService: VideoAbuseValidatorsService,
29 private videoAbuseService: VideoAbuseService, 29 private videoAbuseService: VideoAbuseService,
30 private notificationsService: NotificationsService, 30 private notifier: Notifier,
31 private i18n: I18n 31 private i18n: I18n
32 ) { 32 ) {
33 super() 33 super()
34 } 34 }
35 35
36 get currentHost () {
37 return window.location.host
38 }
39
40 get originHost () {
41 if (this.isRemoteVideo()) {
42 return this.video.account.host
43 }
44
45 return ''
46 }
47
36 ngOnInit () { 48 ngOnInit () {
37 this.buildForm({ 49 this.buildForm({
38 reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON 50 reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON
@@ -54,11 +66,15 @@ export class VideoReportComponent extends FormReactive implements OnInit {
54 this.videoAbuseService.reportVideo(this.video.id, reason) 66 this.videoAbuseService.reportVideo(this.video.id, reason)
55 .subscribe( 67 .subscribe(
56 () => { 68 () => {
57 this.notificationsService.success(this.i18n('Success'), this.i18n('Video reported.')) 69 this.notifier.success(this.i18n('Video reported.'))
58 this.hide() 70 this.hide()
59 }, 71 },
60 72
61 err => this.notificationsService.error(this.i18n('Error'), err.message) 73 err => this.notifier.error(err.message)
62 ) 74 )
63 } 75 }
76
77 isRemoteVideo () {
78 return !this.video.isLocal
79 }
64} 80}
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.html b/client/src/app/videos/+video-watch/modal/video-share.component.html
index 301f67f2d..9f3c37fe8 100644
--- a/client/src/app/videos/+video-watch/modal/video-share.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-share.component.html
@@ -1,7 +1,7 @@
1<ng-template #modal let-hide="close"> 1<ng-template #modal let-hide="close">
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Share</h4> 3 <h4 i18n class="modal-title">Share</h4>
4 <span class="close" aria-hidden="true" (click)="hide()"></span> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body"> 7 <div class="modal-body">
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.ts b/client/src/app/videos/+video-watch/modal/video-share.component.ts
index 17e2b31e1..c6205e355 100644
--- a/client/src/app/videos/+video-watch/modal/video-share.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-share.component.ts
@@ -1,5 +1,5 @@
1import { Component, ElementRef, Input, ViewChild } from '@angular/core' 1import { Component, ElementRef, Input, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { VideoDetails } from '../../../shared/video/video-details.model' 3import { VideoDetails } from '../../../shared/video/video-details.model'
4import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils' 4import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -23,7 +23,7 @@ export class VideoShareComponent {
23 23
24 constructor ( 24 constructor (
25 private modalService: NgbModal, 25 private modalService: NgbModal,
26 private notificationsService: NotificationsService, 26 private notifier: Notifier,
27 private i18n: I18n 27 private i18n: I18n
28 ) { } 28 ) { }
29 29
@@ -49,7 +49,7 @@ export class VideoShareComponent {
49 } 49 }
50 50
51 activateCopiedMessage () { 51 activateCopiedMessage () {
52 this.notificationsService.success(this.i18n('Success'), this.i18n('Copied')) 52 this.notifier.success(this.i18n('Copied'))
53 } 53 }
54 54
55 getStartCheckboxLabel () { 55 getStartCheckboxLabel () {
diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.html b/client/src/app/videos/+video-watch/modal/video-support.component.html
index 00c304709..2a05224a8 100644
--- a/client/src/app/videos/+video-watch/modal/video-support.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-support.component.html
@@ -1,7 +1,7 @@
1<ng-template #modal let-hide="close"> 1<ng-template #modal let-hide="close">
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Support</h4> 3 <h4 i18n class="modal-title">Support</h4>
4 <span class="close" aria-label="Close" role="button" (click)="hide()"></span> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body" [innerHTML]="videoHTMLSupport"></div> 7 <div class="modal-body" [innerHTML]="videoHTMLSupport"></div>
diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.ts b/client/src/app/videos/+video-watch/modal/video-support.component.ts
index 154002120..deb8fbc67 100644
--- a/client/src/app/videos/+video-watch/modal/video-support.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-support.component.ts
@@ -1,8 +1,7 @@
1import { Component, Input, ViewChild } from '@angular/core' 1import { Component, Input, ViewChild } from '@angular/core'
2import { MarkdownService } from '@app/videos/shared'
3
4import { VideoDetails } from '../../../shared/video/video-details.model' 2import { VideoDetails } from '../../../shared/video/video-details.model'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { MarkdownService } from '@app/shared/renderer'
6 5
7@Component({ 6@Component({
8 selector: 'my-video-support', 7 selector: 'my-video-support',
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html
index 770785d02..709eb91a8 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -53,55 +53,57 @@
53 <div 53 <div
54 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()" 54 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()"
55 class="action-button action-button-like" role="button" [attr.aria-pressed]="userRating === 'like'" 55 class="action-button action-button-like" role="button" [attr.aria-pressed]="userRating === 'like'"
56 i18n-title title="Like this video"
56 > 57 >
57 <span class="icon icon-like" i18n-title title="Like this video" ></span> 58 <my-global-icon iconName="like"></my-global-icon>
58 </div> 59 </div>
59 60
60 <div 61 <div
61 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()" 62 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()"
62 class="action-button action-button-dislike" role="button" [attr.aria-pressed]="userRating === 'dislike'" 63 class="action-button action-button-dislike" role="button" [attr.aria-pressed]="userRating === 'dislike'"
64 i18n-title title="Dislike this video"
63 > 65 >
64 <span class="icon icon-dislike" i18n-title title="Dislike this video"></span> 66 <my-global-icon iconName="dislike"></my-global-icon>
65 </div> 67 </div>
66 68
67 <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support"> 69 <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support">
68 <span class="icon icon-support"></span> 70 <my-global-icon iconName="heart"></my-global-icon>
69 <span class="icon-text" i18n>Support</span> 71 <span class="icon-text" i18n>Support</span>
70 </div> 72 </div>
71 73
72 <div (click)="showShareModal()" class="action-button action-button-share" role="button"> 74 <div (click)="showShareModal()" class="action-button action-button-share" role="button">
73 <span class="icon icon-share"></span> 75 <my-global-icon iconName="share"></my-global-icon>
74 <span class="icon-text" i18n>Share</span> 76 <span class="icon-text" i18n>Share</span>
75 </div> 77 </div>
76 78
77 <div class="action-more" ngbDropdown placement="top" role="button"> 79 <div class="action-more" ngbDropdown placement="top" role="button">
78 <div class="action-button" ngbDropdownToggle role="button"> 80 <div class="action-button" ngbDropdownToggle role="button">
79 <span class="icon icon-more"></span> 81 <my-global-icon class="more-icon" iconName="more"></my-global-icon>
80 </div> 82 </div>
81 83
82 <div ngbDropdownMenu> 84 <div ngbDropdownMenu>
83 <a class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)"> 85 <a class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)">
84 <span class="icon icon-download"></span> <ng-container i18n>Download</ng-container> 86 <my-global-icon iconName="download"></my-global-icon> <ng-container i18n>Download</ng-container>
85 </a> 87 </a>
86 88
87 <a *ngIf="isUserLoggedIn()" class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)"> 89 <a *ngIf="isUserLoggedIn()" class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)">
88 <span class="icon icon-alert"></span> <ng-container i18n>Report</ng-container> 90 <my-global-icon iconName="alert"></my-global-icon> <ng-container i18n>Report</ng-container>
89 </a> 91 </a>
90 92
91 <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]"> 93 <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]">
92 <span class="icon icon-edit"></span> <ng-container i18n>Update</ng-container> 94 <my-global-icon iconName="edit"></my-global-icon> <ng-container i18n>Update</ng-container>
93 </a> 95 </a>
94 96
95 <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)"> 97 <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)">
96 <span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container> 98 <my-global-icon iconName="no"></my-global-icon> <ng-container i18n>Blacklist</ng-container>
97 </a> 99 </a>
98 100
99 <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)"> 101 <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)">
100 <span class="icon icon-unblacklist"></span> <ng-container i18n>Unblacklist</ng-container> 102 <my-global-icon iconName="undo"></my-global-icon> <ng-container i18n>Unblacklist</ng-container>
101 </a> 103 </a>
102 104
103 <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)"> 105 <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)">
104 <span class="icon icon-delete"></span> <ng-container i18n>Delete</ng-container> 106 <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete</ng-container>
105 </a> 107 </a>
106 </div> 108 </div>
107 </div> 109 </div>
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss
index 2586a2204..b03ed197d 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: 91px; 289 min-width: 91px;
340 padding-right: 5px; 290 padding-right: 5px;
341 display: inline-block; 291 display: inline-block;
342 color: #585858; 292 color: $grey-foreground-color;
343 font-weight: $font-bold; 293 font-weight: $font-bold;
344 } 294 }
345 295
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index 09ee96bdc..ee504bc58 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -5,7 +5,7 @@ import { RedirectService } from '@app/core/routing/redirect.service'
5import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' 5import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
6import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' 6import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
7import { MetaService } from '@ngx-meta/core' 7import { MetaService } from '@ngx-meta/core'
8import { NotificationsService } from 'angular2-notifications' 8import { Notifier, ServerService } from '@app/core'
9import { forkJoin, Subscription } from 'rxjs' 9import { forkJoin, Subscription } from 'rxjs'
10// FIXME: something weird with our path definition in tsconfig and typings 10// FIXME: something weird with our path definition in tsconfig and typings
11// @ts-ignore 11// @ts-ignore
@@ -13,24 +13,23 @@ import videojs from 'video.js'
13import 'videojs-hotkeys' 13import 'videojs-hotkeys'
14import { Hotkey, HotkeysService } from 'angular2-hotkeys' 14import { Hotkey, HotkeysService } from 'angular2-hotkeys'
15import * as WebTorrent from 'webtorrent' 15import * as WebTorrent from 'webtorrent'
16import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoRateType, VideoState } from '../../../../../shared' 16import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared'
17import '../../../assets/player/peertube-videojs-plugin' 17import '../../../assets/player/peertube-videojs-plugin'
18import { AuthService, ConfirmService } from '../../core' 18import { AuthService, ConfirmService } from '../../core'
19import { RestExtractor, VideoBlacklistService } from '../../shared' 19import { RestExtractor, VideoBlacklistService } from '../../shared'
20import { VideoDetails } from '../../shared/video/video-details.model' 20import { VideoDetails } from '../../shared/video/video-details.model'
21import { VideoService } from '../../shared/video/video.service' 21import { VideoService } from '../../shared/video/video.service'
22import { MarkdownService } from '../shared'
23import { VideoDownloadComponent } from './modal/video-download.component' 22import { VideoDownloadComponent } from './modal/video-download.component'
24import { VideoReportComponent } from './modal/video-report.component' 23import { VideoReportComponent } from './modal/video-report.component'
25import { VideoShareComponent } from './modal/video-share.component' 24import { VideoShareComponent } from './modal/video-share.component'
26import { VideoBlacklistComponent } from './modal/video-blacklist.component' 25import { VideoBlacklistComponent } from './modal/video-blacklist.component'
27import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' 26import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component'
28import { addContextMenu, getVideojsOptions, loadLocaleInVideoJS } from '../../../assets/player/peertube-player' 27import { addContextMenu, getVideojsOptions, loadLocaleInVideoJS } from '../../../assets/player/peertube-player'
29import { ServerService } from '@app/core'
30import { I18n } from '@ngx-translate/i18n-polyfill' 28import { I18n } from '@ngx-translate/i18n-polyfill'
31import { environment } from '../../../environments/environment' 29import { environment } from '../../../environments/environment'
32import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' 30import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
33import { VideoCaptionService } from '@app/shared/video-caption' 31import { VideoCaptionService } from '@app/shared/video-caption'
32import { MarkdownService } from '@app/shared/renderer'
34 33
35@Component({ 34@Component({
36 selector: 'my-video-watch', 35 selector: 'my-video-watch',
@@ -77,7 +76,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
77 private authService: AuthService, 76 private authService: AuthService,
78 private serverService: ServerService, 77 private serverService: ServerService,
79 private restExtractor: RestExtractor, 78 private restExtractor: RestExtractor,
80 private notificationsService: NotificationsService, 79 private notifier: Notifier,
81 private markdownService: MarkdownService, 80 private markdownService: MarkdownService,
82 private zone: NgZone, 81 private zone: NgZone,
83 private redirectService: RedirectService, 82 private redirectService: RedirectService,
@@ -118,7 +117,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
118 ) 117 )
119 .subscribe(([ video, captionsResult ]) => { 118 .subscribe(([ video, captionsResult ]) => {
120 const startTime = this.route.snapshot.queryParams.start 119 const startTime = this.route.snapshot.queryParams.start
121 this.onVideoFetched(video, captionsResult.data, startTime) 120 const subtitle = this.route.snapshot.queryParams.subtitle
121
122 this.onVideoFetched(video, captionsResult.data, { startTime, subtitle })
122 .catch(err => this.handleError(err)) 123 .catch(err => this.handleError(err))
123 }) 124 })
124 }) 125 })
@@ -203,7 +204,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
203 204
204 error => { 205 error => {
205 this.descriptionLoading = false 206 this.descriptionLoading = false
206 this.notificationsService.error(this.i18n('Error'), error.message) 207 this.notifier.error(error.message)
207 } 208 }
208 ) 209 )
209 } 210 }
@@ -245,16 +246,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
245 246
246 this.videoBlacklistService.removeVideoFromBlacklist(this.video.id).subscribe( 247 this.videoBlacklistService.removeVideoFromBlacklist(this.video.id).subscribe(
247 () => { 248 () => {
248 this.notificationsService.success( 249 this.notifier.success(this.i18n('Video {{name}} removed from the blacklist.', { name: this.video.name }))
249 this.i18n('Success'),
250 this.i18n('Video {{name}} removed from the blacklist.', { name: this.video.name })
251 )
252 250
253 this.video.blacklisted = false 251 this.video.blacklisted = false
254 this.video.blacklistedReason = null 252 this.video.blacklistedReason = null
255 }, 253 },
256 254
257 err => this.notificationsService.error(this.i18n('Error'), err.message) 255 err => this.notifier.error(err.message)
258 ) 256 )
259 } 257 }
260 258
@@ -292,17 +290,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
292 290
293 this.videoService.removeVideo(this.video.id) 291 this.videoService.removeVideo(this.video.id)
294 .subscribe( 292 .subscribe(
295 status => { 293 () => {
296 this.notificationsService.success( 294 this.notifier.success(this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name }))
297 this.i18n('Success'),
298 this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name })
299 )
300 295
301 // Go back to the video-list. 296 // Go back to the video-list.
302 this.redirectService.redirectToHomepage() 297 this.redirectService.redirectToHomepage()
303 }, 298 },
304 299
305 error => this.notificationsService.error(this.i18n('Error'), error.message) 300 error => this.notifier.error(error.message)
306 ) 301 )
307 } 302 }
308 303
@@ -352,7 +347,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
352 return 347 return
353 } 348 }
354 349
355 this.notificationsService.error(this.i18n('Error'), errorMessage) 350 this.notifier.error(errorMessage)
356 } 351 }
357 352
358 private checkUserRating () { 353 private checkUserRating () {
@@ -367,11 +362,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
367 } 362 }
368 }, 363 },
369 364
370 err => this.notificationsService.error(this.i18n('Error'), err.message) 365 err => this.notifier.error(err.message)
371 ) 366 )
372 } 367 }
373 368
374 private async onVideoFetched (video: VideoDetails, videoCaptions: VideoCaption[], startTimeFromUrl: number) { 369 private async onVideoFetched (video: VideoDetails, videoCaptions: VideoCaption[], urlOptions: { startTime: number, subtitle: string }) {
375 this.video = video 370 this.video = video
376 371
377 // Re init attributes 372 // Re init attributes
@@ -379,8 +374,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
379 this.completeDescriptionShown = false 374 this.completeDescriptionShown = false
380 this.remoteServerDown = false 375 this.remoteServerDown = false
381 376
382 let startTime = startTimeFromUrl || (this.video.userHistory ? this.video.userHistory.currentTime : 0) 377 let startTime = urlOptions.startTime || (this.video.userHistory ? this.video.userHistory.currentTime : 0)
383 // Don't start the video if we are at the end 378 // If we are at the end of the video, reset the timer
384 if (this.video.duration - startTime <= 1) startTime = 0 379 if (this.video.duration - startTime <= 1) startTime = 0
385 380
386 if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) { 381 if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) {
@@ -419,10 +414,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
419 peertubeLink: false, 414 peertubeLink: false,
420 poster: this.video.previewUrl, 415 poster: this.video.previewUrl,
421 startTime, 416 startTime,
417 subtitle: urlOptions.subtitle,
422 theaterMode: true, 418 theaterMode: true,
423 language: this.localeId, 419 language: this.localeId,
424 420
425 userWatching: this.user ? { 421 userWatching: this.user && this.user.videosHistoryEnabled === true ? {
426 url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), 422 url: this.videoService.getUserWatchingVideoUrl(this.video.uuid),
427 authorizationHeader: this.authService.getRequestHeaderValue() 423 authorizationHeader: this.authService.getRequestHeaderValue()
428 } : undefined 424 } : undefined
@@ -472,7 +468,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
472 this.userRating = nextRating 468 this.userRating = nextRating
473 }, 469 },
474 470
475 (err: { message: string }) => this.notificationsService.error(this.i18n('Error'), err.message) 471 (err: { message: string }) => this.notifier.error(err.message)
476 ) 472 )
477 } 473 }
478 474
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-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts
index 9d000cf2e..c0be4b885 100644
--- a/client/src/app/videos/video-list/video-local.component.ts
+++ b/client/src/app/videos/video-list/video-local.component.ts
@@ -2,7 +2,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { immutableAssign } from '@app/shared/misc/utils' 3import { immutableAssign } from '@app/shared/misc/utils'
4import { Location } from '@angular/common' 4import { Location } from '@angular/common'
5import { NotificationsService } from 'angular2-notifications'
6import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
7import { AbstractVideoList } from '../../shared/video/abstract-video-list' 6import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 7import { VideoSortField } from '../../shared/video/sort-field.type'
@@ -11,6 +10,7 @@ import { VideoFilter } from '../../../../../shared/models/videos/video-query.typ
11import { I18n } from '@ngx-translate/i18n-polyfill' 10import { I18n } from '@ngx-translate/i18n-polyfill'
12import { ScreenService } from '@app/shared/misc/screen.service' 11import { ScreenService } from '@app/shared/misc/screen.service'
13import { UserRight } from '../../../../../shared/models/users' 12import { UserRight } from '../../../../../shared/models/users'
13import { Notifier } from '@app/core'
14 14
15@Component({ 15@Component({
16 selector: 'my-videos-local', 16 selector: 'my-videos-local',
@@ -26,7 +26,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
26 constructor ( 26 constructor (
27 protected router: Router, 27 protected router: Router,
28 protected route: ActivatedRoute, 28 protected route: ActivatedRoute,
29 protected notificationsService: NotificationsService, 29 protected notifier: Notifier,
30 protected authService: AuthService, 30 protected authService: AuthService,
31 protected location: Location, 31 protected location: Location,
32 protected i18n: I18n, 32 protected i18n: I18n,
diff --git a/client/src/app/videos/video-list/video-overview.component.ts b/client/src/app/videos/video-list/video-overview.component.ts
index 2c6054721..7ff52b259 100644
--- a/client/src/app/videos/video-list/video-overview.component.ts
+++ b/client/src/app/videos/video-list/video-overview.component.ts
@@ -1,6 +1,5 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { AuthService } from '@app/core' 2import { AuthService, Notifier } from '@app/core'
3import { NotificationsService } from 'angular2-notifications'
4import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
5import { VideosOverview } from '@app/shared/overview/videos-overview.model' 4import { VideosOverview } from '@app/shared/overview/videos-overview.model'
6import { OverviewService } from '@app/shared/overview' 5import { OverviewService } from '@app/shared/overview'
@@ -21,7 +20,7 @@ export class VideoOverviewComponent implements OnInit {
21 20
22 constructor ( 21 constructor (
23 private i18n: I18n, 22 private i18n: I18n,
24 private notificationsService: NotificationsService, 23 private notifier: Notifier,
25 private authService: AuthService, 24 private authService: AuthService,
26 private overviewService: OverviewService 25 private overviewService: OverviewService
27 ) { } 26 ) { }
@@ -43,10 +42,7 @@ export class VideoOverviewComponent implements OnInit {
43 ) this.notResults = true 42 ) this.notResults = true
44 }, 43 },
45 44
46 err => { 45 err => this.notifier.error(err.message)
47 console.log(err)
48 this.notificationsService.error('Error', err.text)
49 }
50 ) 46 )
51 } 47 }
52 48
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts
index ac1fcfff3..f99c8abb6 100644
--- a/client/src/app/videos/video-list/video-recently-added.component.ts
+++ b/client/src/app/videos/video-list/video-recently-added.component.ts
@@ -2,13 +2,13 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { Location } from '@angular/common' 3import { Location } from '@angular/common'
4import { immutableAssign } from '@app/shared/misc/utils' 4import { immutableAssign } from '@app/shared/misc/utils'
5import { NotificationsService } from 'angular2-notifications'
6import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
7import { AbstractVideoList } from '../../shared/video/abstract-video-list' 6import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 7import { VideoSortField } from '../../shared/video/sort-field.type'
9import { VideoService } from '../../shared/video/video.service' 8import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
11import { ScreenService } from '@app/shared/misc/screen.service' 10import { ScreenService } from '@app/shared/misc/screen.service'
11import { Notifier } from '@app/core'
12 12
13@Component({ 13@Component({
14 selector: 'my-videos-recently-added', 14 selector: 'my-videos-recently-added',
@@ -24,7 +24,7 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
24 protected router: Router, 24 protected router: Router,
25 protected route: ActivatedRoute, 25 protected route: ActivatedRoute,
26 protected location: Location, 26 protected location: Location,
27 protected notificationsService: NotificationsService, 27 protected notifier: Notifier,
28 protected authService: AuthService, 28 protected authService: AuthService,
29 protected i18n: I18n, 29 protected i18n: I18n,
30 protected screenService: ScreenService, 30 protected screenService: ScreenService,
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts
index 8f3d3842b..6fd74e67a 100644
--- a/client/src/app/videos/video-list/video-trending.component.ts
+++ b/client/src/app/videos/video-list/video-trending.component.ts
@@ -2,13 +2,13 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { Location } from '@angular/common' 3import { Location } from '@angular/common'
4import { immutableAssign } from '@app/shared/misc/utils' 4import { immutableAssign } from '@app/shared/misc/utils'
5import { NotificationsService } from 'angular2-notifications'
6import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
7import { AbstractVideoList } from '../../shared/video/abstract-video-list' 6import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 7import { VideoSortField } from '../../shared/video/sort-field.type'
9import { VideoService } from '../../shared/video/video.service' 8import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
11import { ScreenService } from '@app/shared/misc/screen.service' 10import { ScreenService } from '@app/shared/misc/screen.service'
11import { Notifier, ServerService } from '@app/core'
12 12
13@Component({ 13@Component({
14 selector: 'my-videos-trending', 14 selector: 'my-videos-trending',
@@ -23,22 +23,37 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
23 constructor ( 23 constructor (
24 protected router: Router, 24 protected router: Router,
25 protected route: ActivatedRoute, 25 protected route: ActivatedRoute,
26 protected notificationsService: NotificationsService, 26 protected notifier: Notifier,
27 protected authService: AuthService, 27 protected authService: AuthService,
28 protected location: Location, 28 protected location: Location,
29 protected screenService: ScreenService, 29 protected screenService: ScreenService,
30 private serverService: ServerService,
30 protected i18n: I18n, 31 protected i18n: I18n,
31 private videoService: VideoService 32 private videoService: VideoService
32 ) { 33 ) {
33 super() 34 super()
34
35 this.titlePage = i18n('Trending')
36 } 35 }
37 36
38 ngOnInit () { 37 ngOnInit () {
39 super.ngOnInit() 38 super.ngOnInit()
40 39
41 this.generateSyndicationList() 40 this.generateSyndicationList()
41
42 this.serverService.configLoaded.subscribe(
43 () => {
44 const trendingDays = this.serverService.getConfig().trending.videos.intervalDays
45
46 if (trendingDays === 1) {
47 this.titlePage = this.i18n('Trending for the last 24 hours')
48 this.titleTooltip = this.i18n('Trending videos are those totalizing the greatest number of views during the last 24 hours.')
49 } else {
50 this.titlePage = this.i18n('Trending for the last {{days}} days', { days: trendingDays })
51 this.titleTooltip = this.i18n(
52 'Trending videos are those totalizing the greatest number of views during the last {{days}} days.',
53 { days: trendingDays }
54 )
55 }
56 })
42 } 57 }
43 58
44 ngOnDestroy () { 59 ngOnDestroy () {
diff --git a/client/src/app/videos/video-list/video-user-subscriptions.component.ts b/client/src/app/videos/video-list/video-user-subscriptions.component.ts
index 6e8959c54..bee828e12 100644
--- a/client/src/app/videos/video-list/video-user-subscriptions.component.ts
+++ b/client/src/app/videos/video-list/video-user-subscriptions.component.ts
@@ -2,7 +2,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { immutableAssign } from '@app/shared/misc/utils' 3import { immutableAssign } from '@app/shared/misc/utils'
4import { Location } from '@angular/common' 4import { Location } from '@angular/common'
5import { NotificationsService } from 'angular2-notifications'
6import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
7import { AbstractVideoList } from '../../shared/video/abstract-video-list' 6import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 7import { VideoSortField } from '../../shared/video/sort-field.type'
@@ -10,6 +9,7 @@ import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
11import { ScreenService } from '@app/shared/misc/screen.service' 10import { ScreenService } from '@app/shared/misc/screen.service'
12import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' 11import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
12import { Notifier } from '@app/core'
13 13
14@Component({ 14@Component({
15 selector: 'my-videos-user-subscriptions', 15 selector: 'my-videos-user-subscriptions',
@@ -25,7 +25,7 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
25 constructor ( 25 constructor (
26 protected router: Router, 26 protected router: Router,
27 protected route: ActivatedRoute, 27 protected route: ActivatedRoute,
28 protected notificationsService: NotificationsService, 28 protected notifier: Notifier,
29 protected authService: AuthService, 29 protected authService: AuthService,
30 protected location: Location, 30 protected location: Location,
31 protected i18n: I18n, 31 protected i18n: I18n,