aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+about/about-instance/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-peertube/about-peertube.component.html4
-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.html11
-rw-r--r--client/src/app/+accounts/accounts.component.ts12
-rw-r--r--client/src/app/+admin/admin.module.ts6
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html480
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts234
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.html9
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.scss10
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.ts10
-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.html11
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.scss11
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.ts17
-rw-r--r--client/src/app/+admin/follows/shared/follow.service.ts8
-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.ts8
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/index.ts2
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html22
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss7
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts58
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html23
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss7
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts57
-rw-r--r--client/src/app/+admin/moderation/moderation.component.html4
-rw-r--r--client/src/app/+admin/moderation/moderation.component.scss1
-rw-r--r--client/src/app/+admin/moderation/moderation.component.ts8
-rw-r--r--client/src/app/+admin/moderation/moderation.routes.ts23
-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.html10
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts29
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html12
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts27
-rw-r--r--client/src/app/+admin/users/user-edit/index.ts1
-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-edit.component.html14
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.scss22
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.ts14
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.html21
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.scss22
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.ts64
-rw-r--r--client/src/app/+admin/users/user-edit/user-update.component.ts23
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html56
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.scss12
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.ts131
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html26
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss7
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts56
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html27
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss7
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts57
-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.ts39
-rw-r--r--client/src/app/+my-account/my-account-routing.module.ts40
-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.html17
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts12
-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-edit.ts4
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts20
-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.ts8
-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.ts22
-rw-r--r--client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html9
-rw-r--r--client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts16
-rw-r--r--client/src/app/+my-account/my-account.component.html23
-rw-r--r--client/src/app/+my-account/my-account.component.scss15
-rw-r--r--client/src/app/+my-account/my-account.component.ts96
-rw-r--r--client/src/app/+my-account/my-account.module.ts16
-rw-r--r--client/src/app/+my-account/shared/actor-avatar-info.component.ts10
-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.ts9
-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.ts8
-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.ts6
-rw-r--r--client/src/app/core/auth/auth-user.model.ts14
-rw-r--r--client/src/app/core/auth/auth.service.ts18
-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.ts23
-rw-r--r--client/src/app/core/theme/theme.service.ts4
-rw-r--r--client/src/app/header/header.component.html6
-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.scss17
-rw-r--r--client/src/app/menu/menu.component.ts2
-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.ts22
-rw-r--r--client/src/app/shared/account/account.model.ts9
-rw-r--r--client/src/app/shared/actor/actor.model.ts2
-rw-r--r--client/src/app/shared/blocklist/account-block.model.ts14
-rw-r--r--client/src/app/shared/blocklist/blocklist.service.ts135
-rw-r--r--client/src/app/shared/blocklist/index.ts2
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.html30
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.scss34
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.ts18
-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.ts7
-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/buttons/edit-button.component.ts2
-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.ts24
-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-change-ownership-validators.service.ts15
-rw-r--r--client/src/app/shared/forms/form-validators/video-channel-validators.service.ts28
-rw-r--r--client/src/app/shared/forms/form-validators/video-validators.service.ts4
-rw-r--r--client/src/app/shared/forms/index.ts1
-rw-r--r--client/src/app/shared/forms/markdown-textarea.component.ts2
-rw-r--r--client/src/app/shared/forms/peertube-checkbox.component.html6
-rw-r--r--client/src/app/shared/forms/peertube-checkbox.component.scss6
-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/forms/textarea-autoresize.directive.ts25
-rw-r--r--client/src/app/shared/guards/can-deactivate-guard.service.ts4
-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/from-now.pipe.ts5
-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/peertube-local-storage.ts6
-rw-r--r--client/src/app/shared/misc/utils.ts20
-rw-r--r--client/src/app/shared/moderation/user-ban-modal.component.html9
-rw-r--r--client/src/app/shared/moderation/user-ban-modal.component.ts33
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.html7
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.ts261
-rw-r--r--client/src/app/shared/overview/videos-overview.model.ts1
-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)7
-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.ts5
-rw-r--r--client/src/app/shared/rest/rest-table.ts29
-rw-r--r--client/src/app/shared/rest/rest.service.ts2
-rw-r--r--client/src/app/shared/shared.module.ts48
-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.ts38
-rw-r--r--client/src/app/shared/users/user.service.ts52
-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.html19
-rw-r--r--client/src/app/shared/video/abstract-video-list.scss25
-rw-r--r--client/src/app/shared/video/abstract-video-list.ts21
-rw-r--r--client/src/app/shared/video/feed.component.html (renamed from client/src/app/shared/video/video-feed.component.html)9
-rw-r--r--client/src/app/shared/video/feed.component.scss18
-rw-r--r--client/src/app/shared/video/feed.component.ts11
-rw-r--r--client/src/app/shared/video/syndication.model.ts7
-rw-r--r--client/src/app/shared/video/video-details.model.ts13
-rw-r--r--client/src/app/shared/video/video-edit.model.ts4
-rw-r--r--client/src/app/shared/video/video-feed.component.scss19
-rw-r--r--client/src/app/shared/video/video-feed.component.ts10
-rw-r--r--client/src/app/shared/video/video-miniature.component.html3
-rw-r--r--client/src/app/shared/video/video-miniature.component.scss4
-rw-r--r--client/src/app/shared/video/video-miniature.component.ts9
-rw-r--r--client/src/app/shared/video/video.model.ts4
-rw-r--r--client/src/app/shared/video/video.service.ts8
-rw-r--r--client/src/app/signup/signup.component.html7
-rw-r--r--client/src/app/signup/signup.component.ts34
-rw-r--r--client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html2
-rw-r--r--client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts8
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.html5
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.scss33
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.ts11
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html13
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss45
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts27
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html14
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss37
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts20
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-send.scss54
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-send.ts12
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.html22
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss42
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts48
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.html8
-rw-r--r--client/src/app/videos/+video-edit/video-add.component.ts19
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.html4
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.ts37
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.html2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.ts15
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.component.scss6
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.component.ts29
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.model.ts2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.service.ts14
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.html2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.scss2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.ts37
-rw-r--r--client/src/app/videos/+video-watch/modal/video-blacklist.component.html9
-rw-r--r--client/src/app/videos/+video-watch/modal/video-blacklist.component.ts19
-rw-r--r--client/src/app/videos/+video-watch/modal/video-download.component.html2
-rw-r--r--client/src/app/videos/+video-watch/modal/video-download.component.ts6
-rw-r--r--client/src/app/videos/+video-watch/modal/video-report.component.html7
-rw-r--r--client/src/app/videos/+video-watch/modal/video-report.component.scss4
-rw-r--r--client/src/app/videos/+video-watch/modal/video-report.component.ts24
-rw-r--r--client/src/app/videos/+video-watch/modal/video-share.component.html2
-rw-r--r--client/src/app/videos/+video-watch/modal/video-share.component.ts6
-rw-r--r--client/src/app/videos/+video-watch/modal/video-support.component.html2
-rw-r--r--client/src/app/videos/+video-watch/modal/video-support.component.ts3
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html24
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss82
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts155
-rw-r--r--client/src/app/videos/+video-watch/video-watch.module.ts6
-rw-r--r--client/src/app/videos/shared/index.ts1
-rw-r--r--client/src/app/videos/video-list/video-local.component.ts16
-rw-r--r--client/src/app/videos/video-list/video-overview.component.ts10
-rw-r--r--client/src/app/videos/video-list/video-recently-added.component.ts4
-rw-r--r--client/src/app/videos/video-list/video-trending.component.ts23
-rw-r--r--client/src/app/videos/video-list/video-user-subscriptions.component.ts4
286 files changed, 4918 insertions, 1800 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-peertube/about-peertube.component.html b/client/src/app/+about/about-peertube/about-peertube.component.html
index 13ce89f75..d3fc9a828 100644
--- a/client/src/app/+about/about-peertube/about-peertube.component.html
+++ b/client/src/app/+about/about-peertube/about-peertube.component.html
@@ -83,7 +83,7 @@
83 <h6 i18n class="p2p-privacy-title">What will be done to mitigate this problem?</h6> 83 <h6 i18n class="p2p-privacy-title">What will be done to mitigate this problem?</h6>
84 84
85 <p i18n> 85 <p i18n>
86 PeerTube is only in beta, and want to deliver the best countermeasures possible by the time the stable is released. 86 PeerTube is in its early stages, and want to deliver the best countermeasures possible by the time the stable is released.
87 In the meantime, we want to test different ideas related to this issue: 87 In the meantime, we want to test different ideas related to this issue:
88 </p> 88 </p>
89 89
@@ -94,4 +94,4 @@
94 <li i18n>Disable P2P from the administration interface</li> 94 <li i18n>Disable P2P from the administration interface</li>
95 <li i18n>An automatic video redundancy program: we wouldn't know if the IP downloaded the video on purpose or if it was the automatized program</li> 95 <li i18n>An automatic video redundancy program: we wouldn't know if the IP downloaded the video on purpose or if it was the automatized program</li>
96 </ul> 96 </ul>
97</div> \ No newline at end of file 97</div>
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.html b/client/src/app/+accounts/accounts.component.html
index 036e794d2..c1377c1ea 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -10,8 +10,15 @@
10 <div class="actor-name">{{ account.nameWithHost }}</div> 10 <div class="actor-name">{{ account.nameWithHost }}</div>
11 11
12 <span *ngIf="user?.blocked" [ngbTooltip]="user.blockedReason" class="badge badge-danger" i18n>Banned</span> 12 <span *ngIf="user?.blocked" [ngbTooltip]="user.blockedReason" class="badge badge-danger" i18n>Banned</span>
13 13 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
14 <my-user-moderation-dropdown buttonSize="small" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"> 14 <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Muted by your instance</span>
15 <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Instance muted</span>
16 <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
17
18 <my-user-moderation-dropdown
19 buttonSize="small" [account]="account" [user]="user"
20 (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
21 >
15 </my-user-moderation-dropdown> 22 </my-user-moderation-dropdown>
16 </div> 23 </div>
17 <div i18n class="actor-followers">{{ account.followersCount }} subscribers</div> 24 <div i18n class="actor-followers">{{ account.followersCount }} subscribers</div>
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/admin.module.ts b/client/src/app/+admin/admin.module.ts
index 8c6db98d9..f7f347105 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -10,11 +10,12 @@ import { FollowingListComponent } from './follows/following-list/following-list.
10import { JobsComponent } from './jobs/job.component' 10import { JobsComponent } from './jobs/job.component'
11import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' 11import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
12import { JobService } from './jobs/shared/job.service' 12import { JobService } from './jobs/shared/job.service'
13import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent } from './users' 13import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users'
14import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' 14import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation'
15import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 15import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
16import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' 16import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
17import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' 17import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
18import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
18 19
19@NgModule({ 20@NgModule({
20 imports: [ 21 imports: [
@@ -35,12 +36,15 @@ import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service
35 UsersComponent, 36 UsersComponent,
36 UserCreateComponent, 37 UserCreateComponent,
37 UserUpdateComponent, 38 UserUpdateComponent,
39 UserPasswordComponent,
38 UserListComponent, 40 UserListComponent,
39 41
40 ModerationComponent, 42 ModerationComponent,
41 VideoBlacklistListComponent, 43 VideoBlacklistListComponent,
42 VideoAbuseListComponent, 44 VideoAbuseListComponent,
43 ModerationCommentModalComponent, 45 ModerationCommentModalComponent,
46 InstanceServerBlocklistComponent,
47 InstanceAccountBlocklistComponent,
44 48
45 JobsComponent, 49 JobsComponent,
46 JobsListComponent, 50 JobsListComponent,
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 e2cbd35ca..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,161 +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 <my-peertube-checkbox 105 <div i18n class="inner-form-title">Users</div>
91 inputName="signupEnabled" formControlName="signupEnabled"
92 i18n-labelText labelText="Signup enabled"
93 ></my-peertube-checkbox>
94 106
95 <my-peertube-checkbox *ngIf="isSignupEnabled()" 107 <ng-container formGroupName="user">
96 inputName="signupRequiresEmailVerification" formControlName="signupRequiresEmailVerification" 108 <div class="form-group">
97 i18n-labelText labelText="Signup requires email verification" 109 <label i18n for="userVideoQuota">User default video quota</label>
98 ></my-peertube-checkbox> 110 <div class="peertube-select-container">
111 <select id="userVideoQuota" formControlName="videoQuota">
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>
99 119
100 <div *ngIf="isSignupEnabled()" class="form-group"> 120 <div class="form-group">
101 <label i18n for="signupLimit">Signup limit</label> 121 <label i18n for="userVideoQuotaDaily">User default daily upload limit</label>
102 <input 122 <div class="peertube-select-container">
103 type="text" id="signupLimit" 123 <select id="userVideoQuotaDaily" formControlName="videoQuotaDaily">
104 formControlName="signupLimit" [ngClass]="{ 'input-error': formErrors['signupLimit'] }" 124 <option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
105 > 125 {{ videoQuotaDailyOption.label }}
106 <div *ngIf="formErrors.signupLimit" class="form-error"> 126 </option>
107 {{ formErrors.signupLimit }} 127 </select>
128 </div>
129 <div *ngIf="formErrors.user.videoQuotaDaily" class="form-error">{{ formErrors.user.videoQuotaDaily }}</div>
108 </div> 130 </div>
109 </div> 131 </ng-container>
110 132
111 <div i18n class="inner-form-title">Import</div> 133 <div i18n class="inner-form-title">Import</div>
112 134
113 <my-peertube-checkbox 135 <ng-container formGroupName="import">
114 inputName="importVideosHttpEnabled" formControlName="importVideosHttpEnabled" 136 <ng-container formGroupName="videos">
115 i18n-labelText labelText="Video import with HTTP enabled"
116 ></my-peertube-checkbox>
117 137
118 <my-peertube-checkbox 138 <div class="form-group" formGroupName="http">
119 inputName="importVideosTorrentEnabled" formControlName="importVideosTorrentEnabled" 139 <my-peertube-checkbox
120 i18n-labelText labelText="Video import with a torrent file or a magnet URI enabled" 140 inputName="importVideosHttpEnabled" formControlName="enabled"
121 ></my-peertube-checkbox> 141 i18n-labelText labelText="Video import with HTTP URL (i.e. YouTube) enabled"
142 ></my-peertube-checkbox>
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>
122 154
123 <div i18n class="inner-form-title">Administrator</div> 155 <div i18n class="inner-form-title">Administrator</div>
124 156
125 <div class="form-group"> 157 <div class="form-group" formGroupName="admin">
126 <label i18n for="adminEmail">Admin email</label> 158 <label i18n for="adminEmail">Admin email</label>
127 <input 159 <input
128 type="text" id="adminEmail" 160 type="text" id="adminEmail"
129 formControlName="adminEmail" [ngClass]="{ 'input-error': formErrors['adminEmail'] }" 161 formControlName="email" [ngClass]="{ 'input-error': formErrors['admin.email'] }"
130 > 162 >
131 <div *ngIf="formErrors.adminEmail" class="form-error"> 163 <div *ngIf="formErrors.admin.email" class="form-error">{{ formErrors.admin.email }}</div>
132 {{ formErrors.adminEmail }}
133 </div>
134 </div> 164 </div>
135 165
136 <div i18n class="inner-form-title">Users</div> 166 <div class="form-group" formGroupName="contactForm">
137 167 <my-peertube-checkbox
138 <div class="form-group"> 168 inputName="enableContactForm" formControlName="enabled"
139 <label i18n for="userVideoQuota">User default video quota</label> 169 i18n-labelText labelText="Enable contact form"
140 <div class="peertube-select-container"> 170 ></my-peertube-checkbox>
141 <select id="userVideoQuota" formControlName="userVideoQuota">
142 <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
143 {{ videoQuotaOption.label }}
144 </option>
145 </select>
146 </div>
147 <div *ngIf="formErrors.userVideoQuota" class="form-error">
148 {{ formErrors.userVideoQuota }}
149 </div>
150 </div> 171 </div>
151 172
152 <div class="form-group">
153 <label i18n for="userVideoQuotaDaily">User default daily upload limit</label>
154 <div class="peertube-select-container">
155 <select id="userVideoQuotaDaily" formControlName="userVideoQuotaDaily">
156 <option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
157 {{ videoQuotaDailyOption.label }}
158 </option>
159 </select>
160 </div>
161 <div *ngIf="formErrors.userVideoQuotaDaily" class="form-error">
162 {{ formErrors.userVideoQuotaDaily }}
163 </div>
164 </div>
165 </ng-template> 173 </ng-template>
166 </ngb-tab> 174 </ngb-tab>
167 175
@@ -169,28 +177,35 @@
169 <ng-template ngbTabContent> 177 <ng-template ngbTabContent>
170 <div i18n class="inner-form-title">Twitter</div> 178 <div i18n class="inner-form-title">Twitter</div>
171 179
172 <div class="form-group"> 180 <ng-container formGroupName="services">
173 <label i18n for="signupLimit">Your Twitter username</label> 181 <ng-container formGroupName="twitter">
174 <my-help 182
175 helpType="custom" i18n-customHtml 183 <div class="form-group">
176 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>
177 ></my-help> 185 <my-help
178 <input 186 helpType="custom" i18n-customHtml
179 type="text" id="servicesTwitterUsername" 187 customHtml="Indicates the Twitter account for the website or platform on which the content was published."
180 formControlName="servicesTwitterUsername" [ngClass]="{ 'input-error': formErrors['servicesTwitterUsername'] }" 188 ></my-help>
181 > 189 <input
182 <div *ngIf="formErrors.servicesTwitterUsername" class="form-error"> 190 type="text" id="servicesTwitterUsername"
183 {{ formErrors.servicesTwitterUsername }} 191 formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
184 </div> 192 >
185 </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>
186 208
187 <my-peertube-checkbox
188 inputName="servicesTwitterWhitelisted" formControlName="servicesTwitterWhitelisted"
189 i18n-labelText labelText="Instance whitelisted by Twitter"
190 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 />
191 If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
192 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."
193 ></my-peertube-checkbox>
194 </ng-template> 209 </ng-template>
195 </ngb-tab> 210 </ngb-tab>
196 211
@@ -199,36 +214,48 @@
199 214
200 <div i18n class="inner-form-title">Transcoding</div> 215 <div i18n class="inner-form-title">Transcoding</div>
201 216
202 <my-peertube-checkbox 217 <ng-container formGroupName="transcoding">
203 inputName="transcodingEnabled" formControlName="transcodingEnabled" 218 <div class="form-group">
204 i18n-labelText labelText="Transcoding enabled" 219 <my-peertube-checkbox
205 i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!" 220 inputName="transcodingEnabled" formControlName="enabled"
206 ></my-peertube-checkbox> 221 i18n-labelText labelText="Transcoding enabled"
222 i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!"
223 ></my-peertube-checkbox>
224 </div>
207 225
208 <ng-template [ngIf]="isTranscodingEnabled()"> 226 <ng-container *ngIf="isTranscodingEnabled()">
209 227
210 <div class="form-group"> 228 <div class="form-group">
211 <label i18n for="transcodingThreads">Transcoding threads</label> 229 <my-peertube-checkbox
212 <div class="peertube-select-container"> 230 inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions"
213 <select id="transcodingThreads" formControlName="transcodingThreads"> 231 i18n-labelText labelText="Allow additional extensions"
214 <option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value"> 232 i18n-helpHtml helpHtml="Allow your users to upload .mkv, .mov, .avi, .flv videos"
215 {{ transcodingThreadOption.label }} 233 ></my-peertube-checkbox>
216 </option>
217 </select>
218 </div> 234 </div>
219 <div *ngIf="formErrors.transcodingThreads" class="form-error"> 235
220 {{ 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>
221 </div> 246 </div>
222 </div>
223 247
224 <div class="form-group" *ngFor="let resolution of resolutions"> 248 <ng-container formGroupName="resolutions">
225 <my-peertube-checkbox 249 <div class="form-group" *ngFor="let resolution of resolutions">
226 [inputName]="getResolutionKey(resolution)" [formControlName]="getResolutionKey(resolution)" 250 <my-peertube-checkbox
227 i18n-labelText labelText="Resolution {{resolution}} enabled" 251 [inputName]="getResolutionKey(resolution)" [formControlName]="resolution"
228 ></my-peertube-checkbox> 252 i18n-labelText labelText="Resolution {{resolution}} enabled"
253 ></my-peertube-checkbox>
254 </div>
255 </ng-container>
229 256
230 </div> 257 </ng-container>
231 </ng-template> 258 </ng-container>
232 259
233 <div i18n class="inner-form-title"> 260 <div i18n class="inner-form-title">
234 Cache 261 Cache
@@ -239,74 +266,73 @@
239 ></my-help> 266 ></my-help>
240 </div> 267 </div>
241 268
242 <div class="form-group"> 269 <ng-container formGroupName="cache">
243 <label i18n for="cachePreviewsSize">Previews cache size</label> 270 <div class="form-group" formGroupName="previews">
244 <input 271 <label i18n for="cachePreviewsSize">Previews cache size</label>
245 type="text" id="cachePreviewsSize" 272 <input
246 formControlName="cachePreviewsSize" [ngClass]="{ 'input-error': formErrors['cachePreviewsSize'] }" 273 type="text" id="cachePreviewsSize"
247 > 274 formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.previews.size'] }"
248 <div *ngIf="formErrors.cachePreviewsSize" class="form-error"> 275 >
249 {{ formErrors.cachePreviewsSize }} 276 <div *ngIf="formErrors.cache.previews.size" class="form-error">{{ formErrors.cache.previews.size }}</div>
250 </div> 277 </div>
251 </div>
252 278
253 <div class="form-group"> 279 <div class="form-group" formGroupName="captions">
254 <label i18n for="cachePreviewsSize">Video captions cache size</label> 280 <label i18n for="cacheCaptionsSize">Video captions cache size</label>
255 <input 281 <input
256 type="text" id="cacheCaptionsSize" 282 type="text" id="cacheCaptionsSize"
257 formControlName="cacheCaptionsSize" [ngClass]="{ 'input-error': formErrors['cacheCaptionsSize'] }" 283 formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.captions.size'] }"
258 > 284 >
259 <div *ngIf="formErrors.cacheCaptionsSize" class="form-error"> 285 <div *ngIf="formErrors.cache.captions.size" class="form-error">{{ formErrors.cache.captions.size }}</div>
260 {{ formErrors.cacheCaptionsSize }}
261 </div> 286 </div>
262 </div> 287 </ng-container>
263 288
264 <div i18n class="inner-form-title">Customizations</div> 289 <div i18n class="inner-form-title">Customizations</div>
265 290
266 <div class="form-group"> 291 <ng-container formGroupName="instance">
267 <label i18n for="customizationJavascript">JavaScript</label> 292 <ng-container formGroupName="customizations">
268 <my-help 293 <div class="form-group">
269 helpType="custom" i18n-customHtml 294 <label i18n for="customizationJavascript">JavaScript</label>
270 customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>" 295 <my-help
271 ></my-help> 296 helpType="custom" i18n-customHtml
272 <textarea 297 customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>"
273 id="customizationJavascript" formControlName="customizationJavascript" 298 ></my-help>
274 [ngClass]="{ 'input-error': formErrors['customizationJavascript'] }" 299 <textarea
275 ></textarea> 300 id="customizationJavascript" formControlName="javascript"
276 <div *ngIf="formErrors.customizationJavascript" class="form-error"> 301 [ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
277 {{ formErrors.customizationJavascript }} 302 ></textarea>
278 </div> 303 <div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div>
279 </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>
280 335
281 <div class="form-group">
282 <label for="customizationCSS">CSS</label>
283 <my-help
284 helpType="custom"
285 i18n-customHtml
286 customHtml="
287 Write directly CSS code. Example:<br />
288 <pre>
289 body {{ '{' }}
290 background-color: red;
291 {{ '}' }}
292 </pre>
293
294 Prepend with <em>#custom-css</em> to override styles. Example:
295 <pre>
296 #custom-css .logged-in-email {{ '{' }}
297 color: red;
298 {{ '}' }}
299 </pre>
300 "
301 ></my-help>
302 <textarea
303 id="customizationCSS" formControlName="customizationCSS"
304 [ngClass]="{ 'input-error': formErrors['customizationCSS'] }"
305 ></textarea>
306 <div *ngIf="formErrors.customizationCSS" class="form-error">
307 {{ formErrors.customizationCSS }}
308 </div>
309 </div>
310 </ng-template> 336 </ng-template>
311 </ngb-tab> 337 </ngb-tab>
312 </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 4983b0425..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
@@ -1,9 +1,8 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ConfigService } from '@app/+admin/config/shared/config.service' 2import { ConfigService } from '@app/+admin/config/shared/config.service'
3import { ConfirmService } from '@app/core'
4import { ServerService } from '@app/core/server/server.service' 3import { ServerService } from '@app/core/server/server.service'
5import { CustomConfigValidatorsService, FormReactive, UserValidatorsService } from '@app/shared' 4import { CustomConfigValidatorsService, FormReactive, UserValidatorsService } from '@app/shared'
6import { NotificationsService } from 'angular2-notifications' 5import { Notifier } from '@app/core'
7import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model' 6import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model'
8import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
9import { BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 8import { BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
@@ -19,17 +18,13 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
19 resolutions: string[] = [] 18 resolutions: string[] = []
20 transcodingThreadOptions: { label: string, value: number }[] = [] 19 transcodingThreadOptions: { label: string, value: number }[] = []
21 20
22 private oldCustomJavascript: string
23 private oldCustomCSS: string
24
25 constructor ( 21 constructor (
26 protected formValidatorService: FormValidatorService, 22 protected formValidatorService: FormValidatorService,
27 private customConfigValidatorsService: CustomConfigValidatorsService, 23 private customConfigValidatorsService: CustomConfigValidatorsService,
28 private userValidatorsService: UserValidatorsService, 24 private userValidatorsService: UserValidatorsService,
29 private notificationsService: NotificationsService, 25 private notifier: Notifier,
30 private configService: ConfigService, 26 private configService: ConfigService,
31 private serverService: ServerService, 27 private serverService: ServerService,
32 private confirmService: ConfirmService,
33 private i18n: I18n 28 private i18n: I18n
34 ) { 29 ) {
35 super() 30 super()
@@ -60,40 +55,78 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
60 } 55 }
61 56
62 getResolutionKey (resolution: string) { 57 getResolutionKey (resolution: string) {
63 return 'transcodingResolution' + resolution 58 return 'transcoding.resolutions.' + resolution
64 } 59 }
65 60
66 ngOnInit () { 61 ngOnInit () {
67 const formGroupData = { 62 const formGroupData: { [key in keyof CustomConfig ]: any } = {
68 instanceName: this.customConfigValidatorsService.INSTANCE_NAME, 63 instance: {
69 instanceShortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION, 64 name: this.customConfigValidatorsService.INSTANCE_NAME,
70 instanceDescription: null, 65 shortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION,
71 instanceTerms: null, 66 description: null,
72 instanceDefaultClientRoute: null, 67 terms: null,
73 instanceDefaultNSFWPolicy: null, 68 defaultClientRoute: null,
74 servicesTwitterUsername: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME, 69 defaultNSFWPolicy: null,
75 servicesTwitterWhitelisted: null, 70 customizations: {
76 cachePreviewsSize: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE, 71 javascript: null,
77 cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE, 72 css: null
78 signupEnabled: null, 73 }
79 signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT, 74 },
80 signupRequiresEmailVerification: null, 75 services: {
81 importVideosHttpEnabled: null, 76 twitter: {
82 importVideosTorrentEnabled: null, 77 username: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME,
83 adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL, 78 whitelisted: null
84 userVideoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, 79 }
85 userVideoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY, 80 },
86 transcodingThreads: this.customConfigValidatorsService.TRANSCODING_THREADS, 81 cache: {
87 transcodingEnabled: null, 82 previews: {
88 customizationJavascript: null, 83 size: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE
89 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 }
90 } 120 }
91 121
92 const defaultValues: BuildFormDefaultValues = {} 122 const defaultValues = {
123 transcoding: {
124 resolutions: {}
125 }
126 }
93 for (const resolution of this.resolutions) { 127 for (const resolution of this.resolutions) {
94 const key = this.getResolutionKey(resolution) 128 defaultValues.transcoding.resolutions[resolution] = 'false'
95 defaultValues[key] = 'false' 129 formGroupData.transcoding.resolutions[resolution] = null
96 formGroupData[key] = null
97 } 130 }
98 131
99 this.buildForm(formGroupData) 132 this.buildForm(formGroupData)
@@ -103,112 +136,25 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
103 res => { 136 res => {
104 this.customConfig = res 137 this.customConfig = res
105 138
106 this.oldCustomCSS = this.customConfig.instance.customizations.css
107 this.oldCustomJavascript = this.customConfig.instance.customizations.javascript
108
109 this.updateForm() 139 this.updateForm()
110 // Force form validation 140 // Force form validation
111 this.forceCheck() 141 this.forceCheck()
112 }, 142 },
113 143
114 err => this.notificationsService.error(this.i18n('Error'), err.message) 144 err => this.notifier.error(err.message)
115 ) 145 )
116 } 146 }
117 147
118 isTranscodingEnabled () { 148 isTranscodingEnabled () {
119 return this.form.value['transcodingEnabled'] === true 149 return this.form.value['transcoding']['enabled'] === true
120 } 150 }
121 151
122 isSignupEnabled () { 152 isSignupEnabled () {
123 return this.form.value['signupEnabled'] === true 153 return this.form.value['signup']['enabled'] === true
124 } 154 }
125 155
126 async formValidated () { 156 async formValidated () {
127 const newCustomizationJavascript = this.form.value['customizationJavascript'] 157 this.configService.updateCustomConfig(this.form.value)
128 const newCustomizationCSS = this.form.value['customizationCSS']
129
130 const customizations = []
131 if (newCustomizationJavascript && newCustomizationJavascript !== this.oldCustomJavascript) customizations.push('JavaScript')
132 if (newCustomizationCSS && newCustomizationCSS !== this.oldCustomCSS) customizations.push('CSS')
133
134 if (customizations.length !== 0) {
135 const customizationsText = customizations.join('/')
136
137 // FIXME: i18n service does not support string concatenation
138 const message = this.i18n('You set custom {{customizationsText}}. ', { customizationsText }) +
139 this.i18n('This could lead to security issues or bugs if you do not understand it. ') +
140 this.i18n('Are you sure you want to update the configuration?')
141
142 const label = this.i18n('Please type') + ` "I understand the ${customizationsText} I set" ` + this.i18n('to confirm.')
143 const expectedInputValue = `I understand the ${customizationsText} I set`
144
145 const confirmRes = await this.confirmService.confirmWithInput(message, label, expectedInputValue)
146 if (confirmRes === false) return
147 }
148
149 const data: CustomConfig = {
150 instance: {
151 name: this.form.value['instanceName'],
152 shortDescription: this.form.value['instanceShortDescription'],
153 description: this.form.value['instanceDescription'],
154 terms: this.form.value['instanceTerms'],
155 defaultClientRoute: this.form.value['instanceDefaultClientRoute'],
156 defaultNSFWPolicy: this.form.value['instanceDefaultNSFWPolicy'],
157 customizations: {
158 javascript: this.form.value['customizationJavascript'],
159 css: this.form.value['customizationCSS']
160 }
161 },
162 services: {
163 twitter: {
164 username: this.form.value['servicesTwitterUsername'],
165 whitelisted: this.form.value['servicesTwitterWhitelisted']
166 }
167 },
168 cache: {
169 previews: {
170 size: this.form.value['cachePreviewsSize']
171 },
172 captions: {
173 size: this.form.value['cacheCaptionsSize']
174 }
175 },
176 signup: {
177 enabled: this.form.value['signupEnabled'],
178 limit: this.form.value['signupLimit'],
179 requiresEmailVerification: this.form.value['signupRequiresEmailVerification']
180 },
181 admin: {
182 email: this.form.value['adminEmail']
183 },
184 user: {
185 videoQuota: this.form.value['userVideoQuota'],
186 videoQuotaDaily: this.form.value['userVideoQuotaDaily']
187 },
188 transcoding: {
189 enabled: this.form.value['transcodingEnabled'],
190 threads: this.form.value['transcodingThreads'],
191 resolutions: {
192 '240p': this.form.value[this.getResolutionKey('240p')],
193 '360p': this.form.value[this.getResolutionKey('360p')],
194 '480p': this.form.value[this.getResolutionKey('480p')],
195 '720p': this.form.value[this.getResolutionKey('720p')],
196 '1080p': this.form.value[this.getResolutionKey('1080p')]
197 }
198 },
199 import: {
200 videos: {
201 http: {
202 enabled: this.form.value['importVideosHttpEnabled']
203 },
204 torrent: {
205 enabled: this.form.value['importVideosTorrentEnabled']
206 }
207 }
208 }
209 }
210
211 this.configService.updateCustomConfig(data)
212 .subscribe( 158 .subscribe(
213 res => { 159 res => {
214 this.customConfig = res 160 this.customConfig = res
@@ -218,45 +164,15 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
218 164
219 this.updateForm() 165 this.updateForm()
220 166
221 this.notificationsService.success(this.i18n('Success'), this.i18n('Configuration updated.')) 167 this.notifier.success(this.i18n('Configuration updated.'))
222 }, 168 },
223 169
224 err => this.notificationsService.error(this.i18n('Error'), err.message) 170 err => this.notifier.error(err.message)
225 ) 171 )
226 } 172 }
227 173
228 private updateForm () { 174 private updateForm () {
229 const data = { 175 this.form.patchValue(this.customConfig)
230 instanceName: this.customConfig.instance.name,
231 instanceShortDescription: this.customConfig.instance.shortDescription,
232 instanceDescription: this.customConfig.instance.description,
233 instanceTerms: this.customConfig.instance.terms,
234 instanceDefaultClientRoute: this.customConfig.instance.defaultClientRoute,
235 instanceDefaultNSFWPolicy: this.customConfig.instance.defaultNSFWPolicy,
236 servicesTwitterUsername: this.customConfig.services.twitter.username,
237 servicesTwitterWhitelisted: this.customConfig.services.twitter.whitelisted,
238 cachePreviewsSize: this.customConfig.cache.previews.size,
239 cacheCaptionsSize: this.customConfig.cache.captions.size,
240 signupEnabled: this.customConfig.signup.enabled,
241 signupLimit: this.customConfig.signup.limit,
242 signupRequiresEmailVerification: this.customConfig.signup.requiresEmailVerification,
243 adminEmail: this.customConfig.admin.email,
244 userVideoQuota: this.customConfig.user.videoQuota,
245 userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily,
246 transcodingThreads: this.customConfig.transcoding.threads,
247 transcodingEnabled: this.customConfig.transcoding.enabled,
248 customizationJavascript: this.customConfig.instance.customizations.javascript,
249 customizationCSS: this.customConfig.instance.customizations.css,
250 importVideosHttpEnabled: this.customConfig.import.videos.http.enabled,
251 importVideosTorrentEnabled: this.customConfig.import.videos.torrent.enabled
252 }
253
254 for (const resolution of this.resolutions) {
255 const key = this.getResolutionKey(resolution)
256 data[key] = this.customConfig.transcoding.resolutions[resolution]
257 }
258
259 this.form.patchValue(data)
260 } 176 }
261 177
262} 178}
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html
index 5645a60cc..fc022bdb4 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.html
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html
@@ -2,6 +2,15 @@
2 [value]="followers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" 2 [value]="followers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" 3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
4> 4>
5 <ng-template pTemplate="caption">
6 <div class="caption">
7 <input
8 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
9 (keyup)="onSearch($event.target.value)"
10 >
11 </div>
12 </ng-template>
13
5 <ng-template pTemplate="header"> 14 <ng-template pTemplate="header">
6 <tr> 15 <tr>
7 <th i18n style="width: 60px">ID</th> 16 <th i18n style="width: 60px">ID</th>
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.scss b/client/src/app/+admin/follows/followers-list/followers-list.component.scss
index e69de29bb..a6f0656b8 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.scss
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.scss
@@ -0,0 +1,10 @@
1@import '_variables';
2@import '_mixins';
3
4.caption {
5 justify-content: flex-end;
6
7 input {
8 @include peertube-input-text(250px);
9 }
10} \ No newline at end of file
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 ca993dcd3..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 ) {
@@ -28,18 +28,18 @@ export class FollowersListComponent extends RestTable implements OnInit {
28 } 28 }
29 29
30 ngOnInit () { 30 ngOnInit () {
31 this.loadSort() 31 this.initialize()
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.html b/client/src/app/+admin/follows/following-list/following-list.component.html
index 8af624ac5..5bc8fbc2d 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.html
+++ b/client/src/app/+admin/follows/following-list/following-list.component.html
@@ -2,6 +2,17 @@
2 [value]="following" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" 2 [value]="following" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" 3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
4> 4>
5 <ng-template pTemplate="caption">
6 <div class="caption">
7 <div>
8 <input
9 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
10 (keyup)="onSearch($event.target.value)"
11 >
12 </div>
13 </div>
14 </ng-template>
15
5 <ng-template pTemplate="header"> 16 <ng-template pTemplate="header">
6 <tr> 17 <tr>
7 <th i18n style="width: 60px">ID</th> 18 <th i18n style="width: 60px">ID</th>
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.scss b/client/src/app/+admin/follows/following-list/following-list.component.scss
index bfcdcaa49..a6f0656b8 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.scss
+++ b/client/src/app/+admin/follows/following-list/following-list.component.scss
@@ -1,13 +1,10 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4my-redundancy-checkbox /deep/ my-peertube-checkbox { 4.caption {
5 .form-group { 5 justify-content: flex-end;
6 margin-bottom: 0;
7 align-items: center;
8 }
9 6
10 label { 7 input {
11 margin: 0; 8 @include peertube-input-text(250px);
12 } 9 }
13} \ No newline at end of file 10} \ No newline at end of file
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 dd57884c6..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
@@ -29,7 +29,7 @@ export class FollowingListComponent extends RestTable implements OnInit {
29 } 29 }
30 30
31 ngOnInit () { 31 ngOnInit () {
32 this.loadSort() 32 this.initialize()
33 } 33 }
34 34
35 async removeFollowing (follow: ActorFollow) { 35 async removeFollowing (follow: ActorFollow) {
@@ -41,26 +41,23 @@ 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
55 protected loadData () { 52 protected loadData () {
56 this.followService.getFollowing(this.pagination, this.sort) 53 this.followService.getFollowing(this.pagination, this.sort, this.search)
57 .subscribe( 54 .subscribe(
58 resultList => { 55 resultList => {
59 this.following = resultList.data 56 this.following = resultList.data
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/follow.service.ts b/client/src/app/+admin/follows/shared/follow.service.ts
index 27169a9cd..a2904179e 100644
--- a/client/src/app/+admin/follows/shared/follow.service.ts
+++ b/client/src/app/+admin/follows/shared/follow.service.ts
@@ -18,10 +18,12 @@ export class FollowService {
18 ) { 18 ) {
19 } 19 }
20 20
21 getFollowing (pagination: RestPagination, sort: SortMeta): Observable<ResultList<ActorFollow>> { 21 getFollowing (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<ActorFollow>> {
22 let params = new HttpParams() 22 let params = new HttpParams()
23 params = this.restService.addRestGetParams(params, pagination, sort) 23 params = this.restService.addRestGetParams(params, pagination, sort)
24 24
25 if (search) params = params.append('search', search)
26
25 return this.authHttp.get<ResultList<ActorFollow>>(FollowService.BASE_APPLICATION_URL + '/following', { params }) 27 return this.authHttp.get<ResultList<ActorFollow>>(FollowService.BASE_APPLICATION_URL + '/following', { params })
26 .pipe( 28 .pipe(
27 map(res => this.restExtractor.convertResultListDateToHuman(res)), 29 map(res => this.restExtractor.convertResultListDateToHuman(res)),
@@ -29,10 +31,12 @@ export class FollowService {
29 ) 31 )
30 } 32 }
31 33
32 getFollowers (pagination: RestPagination, sort: SortMeta): Observable<ResultList<ActorFollow>> { 34 getFollowers (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<ActorFollow>> {
33 let params = new HttpParams() 35 let params = new HttpParams()
34 params = this.restService.addRestGetParams(params, pagination, sort) 36 params = this.restService.addRestGetParams(params, pagination, sort)
35 37
38 if (search) params = params.append('search', search)
39
36 return this.authHttp.get<ResultList<ActorFollow>>(FollowService.BASE_APPLICATION_URL + '/followers', { params }) 40 return this.authHttp.get<ResultList<ActorFollow>>(FollowService.BASE_APPLICATION_URL + '/followers', { params })
37 .pipe( 41 .pipe(
38 map(res => this.restExtractor.convertResultListDateToHuman(res)), 42 map(res => this.restExtractor.convertResultListDateToHuman(res)),
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 866ba1b23..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 ) {
@@ -34,7 +34,7 @@ export class JobsListComponent extends RestTable implements OnInit {
34 34
35 ngOnInit () { 35 ngOnInit () {
36 this.loadJobState() 36 this.loadJobState()
37 this.loadSort() 37 this.initialize()
38 } 38 }
39 39
40 onJobStateChanged () { 40 onJobStateChanged () {
@@ -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/index.ts b/client/src/app/+admin/moderation/instance-blocklist/index.ts
new file mode 100644
index 000000000..3e7a344bb
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/index.ts
@@ -0,0 +1,2 @@
1export * from './instance-account-blocklist.component'
2export * from './instance-server-blocklist.component'
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
new file mode 100644
index 000000000..7797bc56e
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
@@ -0,0 +1,22 @@
1<p-table
2 [value]="blockedAccounts" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
4>
5
6 <ng-template pTemplate="header">
7 <tr>
8 <th i18n>Account</th>
9 <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
10 </tr>
11 </ng-template>
12
13 <ng-template pTemplate="body" let-accountBlock>
14 <tr>
15 <td>{{ accountBlock.blockedAccount.nameWithHost }}</td>
16 <td>{{ accountBlock.createdAt }}</td>
17 <td class="action-cell">
18 <button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button>
19 </td>
20 </tr>
21 </ng-template>
22</p-table>
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss
new file mode 100644
index 000000000..6028b75ea
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss
@@ -0,0 +1,7 @@
1@import '_variables';
2@import '_mixins';
3
4.unblock-button {
5 @include peertube-button;
6 @include grey-button;
7} \ No newline at end of file
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
new file mode 100644
index 000000000..032bf745a
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts
@@ -0,0 +1,58 @@
1import { Component, OnInit } from '@angular/core'
2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { AccountBlock, BlocklistService } from '@app/shared/blocklist'
7
8@Component({
9 selector: 'my-instance-account-blocklist',
10 styleUrls: [ './instance-account-blocklist.component.scss' ],
11 templateUrl: './instance-account-blocklist.component.html'
12})
13export class InstanceAccountBlocklistComponent extends RestTable implements OnInit {
14 blockedAccounts: AccountBlock[] = []
15 totalRecords = 0
16 rowsPerPage = 10
17 sort: SortMeta = { field: 'createdAt', order: -1 }
18 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
19
20 constructor (
21 private notifier: Notifier,
22 private blocklistService: BlocklistService,
23 private i18n: I18n
24 ) {
25 super()
26 }
27
28 ngOnInit () {
29 this.initialize()
30 }
31
32 unblockAccount (accountBlock: AccountBlock) {
33 const blockedAccount = accountBlock.blockedAccount
34
35 this.blocklistService.unblockAccountByInstance(blockedAccount)
36 .subscribe(
37 () => {
38 this.notifier.success(
39 this.i18n('Account {{nameWithHost}} unmuted by your instance.', { nameWithHost: blockedAccount.nameWithHost })
40 )
41
42 this.loadData()
43 }
44 )
45 }
46
47 protected loadData () {
48 return this.blocklistService.getInstanceAccountBlocklist(this.pagination, this.sort)
49 .subscribe(
50 resultList => {
51 this.blockedAccounts = resultList.data
52 this.totalRecords = resultList.total
53 },
54
55 err => this.notifier.error(err.message)
56 )
57 }
58}
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
new file mode 100644
index 000000000..f634ba834
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
@@ -0,0 +1,23 @@
1<p-table
2 [value]="blockedServers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
4>
5
6 <ng-template pTemplate="header">
7 <tr>
8 <th i18n>Instance</th>
9 <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
10 <th></th>
11 </tr>
12 </ng-template>
13
14 <ng-template pTemplate="body" let-serverBlock>
15 <tr>
16 <td>{{ serverBlock.blockedServer.host }}</td>
17 <td>{{ serverBlock.createdAt }}</td>
18 <td class="action-cell">
19 <button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button>
20 </td>
21 </tr>
22 </ng-template>
23</p-table>
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss
new file mode 100644
index 000000000..6028b75ea
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss
@@ -0,0 +1,7 @@
1@import '_variables';
2@import '_mixins';
3
4.unblock-button {
5 @include peertube-button;
6 @include grey-button;
7} \ No newline at end of file
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
new file mode 100644
index 000000000..db3dfcd1c
--- /dev/null
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts
@@ -0,0 +1,57 @@
1import { Component, OnInit } from '@angular/core'
2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { BlocklistService } from '@app/shared/blocklist'
7import { ServerBlock } from '../../../../../../shared'
8
9@Component({
10 selector: 'my-instance-server-blocklist',
11 styleUrls: [ './instance-server-blocklist.component.scss' ],
12 templateUrl: './instance-server-blocklist.component.html'
13})
14export class InstanceServerBlocklistComponent extends RestTable implements OnInit {
15 blockedServers: ServerBlock[] = []
16 totalRecords = 0
17 rowsPerPage = 10
18 sort: SortMeta = { field: 'createdAt', order: -1 }
19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
20
21 constructor (
22 private notifier: Notifier,
23 private blocklistService: BlocklistService,
24 private i18n: I18n
25 ) {
26 super()
27 }
28
29 ngOnInit () {
30 this.initialize()
31 }
32
33 unblockServer (serverBlock: ServerBlock) {
34 const host = serverBlock.blockedServer.host
35
36 this.blocklistService.unblockServerByInstance(host)
37 .subscribe(
38 () => {
39 this.notifier.success(this.i18n('Instance {{host}} unmuted by your instance.', { host }))
40
41 this.loadData()
42 }
43 )
44 }
45
46 protected loadData () {
47 return this.blocklistService.getInstanceServerBlocklist(this.pagination, this.sort)
48 .subscribe(
49 resultList => {
50 this.blockedServers = resultList.data
51 this.totalRecords = resultList.total
52 },
53
54 err => this.notifier.error(err.message)
55 )
56 }
57}
diff --git a/client/src/app/+admin/moderation/moderation.component.html b/client/src/app/+admin/moderation/moderation.component.html
index 91e87fcd4..01457936c 100644
--- a/client/src/app/+admin/moderation/moderation.component.html
+++ b/client/src/app/+admin/moderation/moderation.component.html
@@ -5,6 +5,10 @@
5 <a *ngIf="hasVideoAbusesRight()" i18n routerLink="video-abuses/list" routerLinkActive="active">Video abuses</a> 5 <a *ngIf="hasVideoAbusesRight()" i18n routerLink="video-abuses/list" routerLinkActive="active">Video abuses</a>
6 6
7 <a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">Blacklisted videos</a> 7 <a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">Blacklisted videos</a>
8
9 <a *ngIf="hasAccountsBlocklistRight()" i18n routerLink="blocklist/accounts" routerLinkActive="active">Muted accounts</a>
10
11 <a *ngIf="hasServersBlocklistRight()" i18n routerLink="blocklist/servers" routerLinkActive="active">Muted servers</a>
8 </div> 12 </div>
9</div> 13</div>
10 14
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/moderation.component.ts b/client/src/app/+admin/moderation/moderation.component.ts
index 0f4efb970..2b2618933 100644
--- a/client/src/app/+admin/moderation/moderation.component.ts
+++ b/client/src/app/+admin/moderation/moderation.component.ts
@@ -16,4 +16,12 @@ export class ModerationComponent {
16 hasVideoBlacklistRight () { 16 hasVideoBlacklistRight () {
17 return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) 17 return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
18 } 18 }
19
20 hasAccountsBlocklistRight () {
21 return this.auth.getUser().hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)
22 }
23
24 hasServersBlocklistRight () {
25 return this.auth.getUser().hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)
26 }
19} 27}
diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts
index 6d81b9b36..bc6dd49d5 100644
--- a/client/src/app/+admin/moderation/moderation.routes.ts
+++ b/client/src/app/+admin/moderation/moderation.routes.ts
@@ -4,6 +4,7 @@ import { UserRightGuard } from '@app/core'
4import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list' 4import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list'
5import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list' 5import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list'
6import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 6import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
7import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
7 8
8export const ModerationRoutes: Routes = [ 9export const ModerationRoutes: Routes = [
9 { 10 {
@@ -46,6 +47,28 @@ export const ModerationRoutes: Routes = [
46 title: 'Blacklisted videos' 47 title: 'Blacklisted videos'
47 } 48 }
48 } 49 }
50 },
51 {
52 path: 'blocklist/accounts',
53 component: InstanceAccountBlocklistComponent,
54 canActivate: [ UserRightGuard ],
55 data: {
56 userRight: UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
57 meta: {
58 title: 'Muted accounts'
59 }
60 }
61 },
62 {
63 path: 'blocklist/servers',
64 component: InstanceServerBlocklistComponent,
65 canActivate: [ UserRightGuard ],
66 data: {
67 userRight: UserRight.MANAGE_SERVER_REDUNDANCY,
68 meta: {
69 title: 'Muted instances'
70 }
71 }
49 } 72 }
50 ] 73 ]
51 } 74 }
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 287ab3e46..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
@@ -9,7 +9,7 @@
9 <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> 9 <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
10 <th i18n>Video</th> 10 <th i18n>Video</th>
11 <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th> 11 <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
12 <th style="width: 50px;"></th> 12 <th style="width: 120px;"></th>
13 </tr> 13 </tr>
14 </ng-template> 14 </ng-template>
15 15
@@ -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 681db7434..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,16 +28,17 @@ 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
37 this.videoAbuseActions = [ 39 this.videoAbuseActions = [
38 { 40 {
39 label: this.i18n('Delete'), 41 label: this.i18n('Delete this report'),
40 handler: videoAbuse => this.removeVideoAbuse(videoAbuse) 42 handler: videoAbuse => this.removeVideoAbuse(videoAbuse)
41 }, 43 },
42 { 44 {
@@ -57,7 +59,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
57 } 59 }
58 60
59 ngOnInit () { 61 ngOnInit () {
60 this.loadSort() 62 this.initialize()
61 } 63 }
62 64
63 openModerationCommentModal (videoAbuse: VideoAbuse) { 65 openModerationCommentModal (videoAbuse: VideoAbuse) {
@@ -85,19 +87,16 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
85 } 87 }
86 88
87 async removeVideoAbuse (videoAbuse: VideoAbuse) { 89 async removeVideoAbuse (videoAbuse: VideoAbuse) {
88 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse?'), this.i18n('Delete')) 90 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
89 if (res === false) return 91 if (res === false) return
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 0585e0490..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,8 +7,9 @@
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: 50px;"></th> 12 <th style="width: 120px;"></th>
12 </tr> 13 </tr>
13 </ng-template> 14 </ng-template>
14 15
@@ -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 bb051d00f..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()
@@ -39,13 +41,23 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
39 } 41 }
40 42
41 ngOnInit () { 43 ngOnInit () {
42 this.loadSort() 44 this.initialize()
43 } 45 }
44 46
45 getVideoUrl (videoBlacklist: VideoBlacklist) { 47 getVideoUrl (videoBlacklist: VideoBlacklist) {
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/index.ts b/client/src/app/+admin/users/user-edit/index.ts
index fd80a02e0..ec734ef92 100644
--- a/client/src/app/+admin/users/user-edit/index.ts
+++ b/client/src/app/+admin/users/user-edit/index.ts
@@ -1,2 +1,3 @@
1export * from './user-create.component' 1export * from './user-create.component'
2export * from './user-update.component' 2export * from './user-update.component'
3export * from './user-password.component'
diff --git a/client/src/app/+admin/users/user-edit/user-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-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html
index 56cf7d17d..c6566da24 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.html
@@ -81,3 +81,17 @@
81 81
82 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> 82 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
83</form> 83</form>
84
85<div *ngIf="!isCreation()" class="danger-zone">
86 <div class="account-title" i18n>Danger Zone</div>
87
88 <div class="form-group reset-password-email">
89 <label i18n>Send a link to reset the password by email to the user</label>
90 <button (click)="resetPassword()" i18n>Ask for new password</button>
91 </div>
92
93 <div class="form-group">
94 <label i18n>Manually set the user password</label>
95 <my-user-password [userId]="userId"></my-user-password>
96 </div>
97</div>
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.scss b/client/src/app/+admin/users/user-edit/user-edit.component.scss
index 6675f65cc..c1cc4ca45 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.scss
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.scss
@@ -14,7 +14,7 @@ input:not([type=submit]) {
14 @include peertube-select-container(340px); 14 @include peertube-select-container(340px);
15} 15}
16 16
17input[type=submit] { 17input[type=submit], button {
18 @include peertube-button; 18 @include peertube-button;
19 @include orange-button; 19 @include orange-button;
20 20
@@ -25,3 +25,23 @@ input[type=submit] {
25 margin-top: 5px; 25 margin-top: 5px;
26 font-size: 11px; 26 font-size: 11px;
27} 27}
28
29.account-title {
30 @include in-content-small-title;
31
32 margin-top: 55px;
33 margin-bottom: 30px;
34}
35
36.danger-zone {
37 .reset-password-email {
38 margin-bottom: 30px;
39 padding-bottom: 30px;
40 border-bottom: 1px solid rgba(0, 0, 0, 0.1);
41
42 button {
43 display: block;
44 margin-top: 0;
45 }
46 }
47}
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts
index 07b087b5b..649b35b0c 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.ts
+++ b/client/src/app/+admin/users/user-edit/user-edit.ts
@@ -1,14 +1,14 @@
1import { ServerService } from '../../../core' 1import { ServerService } from '../../../core'
2import { FormReactive } from '../../../shared' 2import { FormReactive } from '../../../shared'
3import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared' 3import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared'
4import { EditCustomConfigComponent } from '../../../+admin/config/edit-custom-config/'
5import { ConfigService } from '@app/+admin/config/shared/config.service' 4import { ConfigService } from '@app/+admin/config/shared/config.service'
6 5
7export abstract class UserEdit extends FormReactive { 6export abstract class UserEdit extends FormReactive {
8
9 videoQuotaOptions: { value: string, label: string }[] = [] 7 videoQuotaOptions: { value: string, label: string }[] = []
10 videoQuotaDailyOptions: { value: string, label: string }[] = [] 8 videoQuotaDailyOptions: { value: string, label: string }[] = []
11 roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) 9 roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] }))
10 username: string
11 userId: number
12 12
13 protected abstract serverService: ServerService 13 protected abstract serverService: ServerService
14 protected abstract configService: ConfigService 14 protected abstract configService: ConfigService
@@ -23,7 +23,9 @@ export abstract class UserEdit extends FormReactive {
23 } 23 }
24 24
25 computeQuotaWithTranscoding () { 25 computeQuotaWithTranscoding () {
26 const resolutions = this.serverService.getConfig().transcoding.enabledResolutions 26 const transcodingConfig = this.serverService.getConfig().transcoding
27
28 const resolutions = transcodingConfig.enabledResolutions
27 const higherResolution = VideoResolution.H_1080P 29 const higherResolution = VideoResolution.H_1080P
28 let multiplier = 0 30 let multiplier = 0
29 31
@@ -31,9 +33,15 @@ export abstract class UserEdit extends FormReactive {
31 multiplier += resolution / higherResolution 33 multiplier += resolution / higherResolution
32 } 34 }
33 35
36 if (transcodingConfig.hls.enabled) multiplier *= 2
37
34 return multiplier * parseInt(this.form.value['videoQuota'], 10) 38 return multiplier * parseInt(this.form.value['videoQuota'], 10)
35 } 39 }
36 40
41 resetPassword () {
42 return
43 }
44
37 protected buildQuotaOptions () { 45 protected buildQuotaOptions () {
38 // These are used by a HTML select, so convert key into strings 46 // These are used by a HTML select, so convert key into strings
39 this.videoQuotaOptions = this.configService 47 this.videoQuotaOptions = this.configService
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.html b/client/src/app/+admin/users/user-edit/user-password.component.html
new file mode 100644
index 000000000..a1e1f6216
--- /dev/null
+++ b/client/src/app/+admin/users/user-edit/user-password.component.html
@@ -0,0 +1,21 @@
1<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
2 <div class="form-group">
3
4 <div class="input-group">
5 <input id="password" [attr.type]="showPassword ? 'text' : 'password'"
6 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
7 >
8 <div class="input-group-append">
9 <button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button">
10 <ng-container *ngIf="!showPassword" i18n>Show</ng-container>
11 <ng-container *ngIf="!!showPassword" i18n>Hide</ng-container>
12 </button>
13 </div>
14 </div>
15 <div *ngIf="formErrors.password" class="form-error">
16 {{ formErrors.password }}
17 </div>
18 </div>
19
20 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
21</form>
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.scss b/client/src/app/+admin/users/user-edit/user-password.component.scss
new file mode 100644
index 000000000..217d585af
--- /dev/null
+++ b/client/src/app/+admin/users/user-edit/user-password.component.scss
@@ -0,0 +1,22 @@
1@import '_variables';
2@import '_mixins';
3
4input:not([type=submit]):not([type=checkbox]) {
5 @include peertube-input-text(340px);
6
7 display: block;
8 border-top-right-radius: 0;
9 border-bottom-right-radius: 0;
10 border-right: none;
11}
12
13input[type=submit] {
14 @include peertube-button;
15 @include orange-button;
16
17 margin-top: 10px;
18}
19
20.input-group-append {
21 height: 30px;
22}
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.ts b/client/src/app/+admin/users/user-edit/user-password.component.ts
new file mode 100644
index 000000000..5b3040440
--- /dev/null
+++ b/client/src/app/+admin/users/user-edit/user-password.component.ts
@@ -0,0 +1,64 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { UserService } from '@app/shared/users/user.service'
4import { Notifier } from '../../../core'
5import { User, UserUpdate } from '../../../../../../shared'
6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
8import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
9import { FormReactive } from '../../../shared'
10
11@Component({
12 selector: 'my-user-password',
13 templateUrl: './user-password.component.html',
14 styleUrls: [ './user-password.component.scss' ]
15})
16export class UserPasswordComponent extends FormReactive implements OnInit {
17 error: string
18 username: string
19 showPassword = false
20
21 @Input() userId: number
22
23 constructor (
24 protected formValidatorService: FormValidatorService,
25 private userValidatorsService: UserValidatorsService,
26 private route: ActivatedRoute,
27 private router: Router,
28 private notifier: Notifier,
29 private userService: UserService,
30 private i18n: I18n
31 ) {
32 super()
33 }
34
35 ngOnInit () {
36 this.buildForm({
37 password: this.userValidatorsService.USER_PASSWORD
38 })
39 }
40
41 formValidated () {
42 this.error = undefined
43
44 const userUpdate: UserUpdate = this.form.value
45
46 this.userService.updateUser(this.userId, userUpdate).subscribe(
47 () => {
48 this.notifier.success(
49 this.i18n('Password changed for user {{username}}.', { username: this.username })
50 )
51 },
52
53 err => this.error = err.message
54 )
55 }
56
57 togglePasswordVisibility () {
58 this.showPassword = !this.showPassword
59 }
60
61 getFormButtonTitle () {
62 return this.i18n('Update user password')
63 }
64}
diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts
index cd3885a99..94ef87b08 100644
--- a/client/src/app/+admin/users/user-edit/user-update.component.ts
+++ b/client/src/app/+admin/users/user-edit/user-update.component.ts
@@ -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'
@@ -19,6 +19,7 @@ import { UserService } from '@app/shared'
19export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { 19export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
20 error: string 20 error: string
21 userId: number 21 userId: number
22 userEmail: string
22 username: string 23 username: string
23 24
24 private paramsSub: Subscription 25 private paramsSub: Subscription
@@ -30,7 +31,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
30 private userValidatorsService: UserValidatorsService, 31 private userValidatorsService: UserValidatorsService,
31 private route: ActivatedRoute, 32 private route: ActivatedRoute,
32 private router: Router, 33 private router: Router,
33 private notificationsService: NotificationsService, 34 private notifier: Notifier,
34 private userService: UserService, 35 private userService: UserService,
35 private i18n: I18n 36 private i18n: I18n
36 ) { 37 ) {
@@ -73,10 +74,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
73 74
74 this.userService.updateUser(this.userId, userUpdate).subscribe( 75 this.userService.updateUser(this.userId, userUpdate).subscribe(
75 () => { 76 () => {
76 this.notificationsService.success( 77 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' ]) 78 this.router.navigate([ '/admin/users/list' ])
81 }, 79 },
82 80
@@ -92,9 +90,22 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
92 return this.i18n('Update user') 90 return this.i18n('Update user')
93 } 91 }
94 92
93 resetPassword () {
94 this.userService.askResetPassword(this.userEmail).subscribe(
95 () => {
96 this.notifier.success(
97 this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username })
98 )
99 },
100
101 err => this.error = err.message
102 )
103 }
104
95 private onUserFetched (userJson: User) { 105 private onUserFetched (userJson: User) {
96 this.userId = userJson.id 106 this.userId = userJson.id
97 this.username = userJson.username 107 this.username = userJson.username
108 this.userEmail = userJson.email
98 109
99 this.form.patchValue({ 110 this.form.patchValue({
100 email: userJson.email, 111 email: userJson.email,
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html
index cca057ba1..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>
@@ -10,9 +10,32 @@
10<p-table 10<p-table
11 [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" 11 [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
12 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" 12 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
13 [(selection)]="selectedUsers"
13> 14>
15 <ng-template pTemplate="caption">
16 <div class="caption">
17 <div>
18 <my-action-dropdown
19 *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange"
20 [actions]="bulkUserActions" [entry]="selectedUsers"
21 >
22 </my-action-dropdown>
23 </div>
24
25 <div>
26 <input
27 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
28 (keyup)="onSearch($event.target.value)"
29 >
30 </div>
31 </div>
32 </ng-template>
33
14 <ng-template pTemplate="header"> 34 <ng-template pTemplate="header">
15 <tr> 35 <tr>
36 <th style="width: 40px">
37 <p-tableHeaderCheckbox></p-tableHeaderCheckbox>
38 </th>
16 <th style="width: 40px"></th> 39 <th style="width: 40px"></th>
17 <th i18n pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th> 40 <th i18n pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th>
18 <th i18n>Email</th> 41 <th i18n>Email</th>
@@ -25,22 +48,42 @@
25 48
26 <ng-template pTemplate="body" let-expanded="expanded" let-user> 49 <ng-template pTemplate="body" let-expanded="expanded" let-user>
27 50
28 <tr [ngClass]="{ banned: user.blocked }"> 51 <tr [pSelectableRow]="user" [ngClass]="{ banned: user.blocked }">
52 <td>
53 <p-tableCheckbox [value]="user"></p-tableCheckbox>
54 </td>
55
29 <td> 56 <td>
30 <span *ngIf="user.blockedReason" class="expander" [pRowToggler]="user"> 57 <span *ngIf="user.blockedReason" class="expander" [pRowToggler]="user">
31 <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> 58 <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
32 </span> 59 </span>
33 </td> 60 </td>
61
34 <td> 62 <td>
35 {{ user.username }} 63 <a i18n-title title="Go to the account page" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]">
36 <span *ngIf="user.blocked" class="banned-info">(banned)</span> 64 {{ user.username }}
65 <span i18n *ngIf="user.blocked" class="banned-info">(banned)</span>
66 </a>
37 </td> 67 </td>
38 <td>{{ user.email }}</td> 68
69 <td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">{{ user.email }}</td>
70
71 <ng-template #emailWithVerificationStatus>
72 <td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login">
73 <em>? {{ user.email }}</em>
74 </td>
75 <ng-template #emailVerifiedNotFalse>
76 <td i18n-title title="User's email is verified / User can login without email verification">
77 &#x2713; {{ user.email }}
78 </td>
79 </ng-template>
80 </ng-template>
81
39 <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td> 82 <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td>
40 <td>{{ user.roleLabel }}</td> 83 <td>{{ user.roleLabel }}</td>
41 <td>{{ user.createdAt }}</td> 84 <td>{{ user.createdAt }}</td>
42 <td class="action-cell"> 85 <td class="action-cell">
43 <my-user-moderation-dropdown [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()"> 86 <my-user-moderation-dropdown *ngIf="!isInSelectionMode()" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()">
44 </my-user-moderation-dropdown> 87 </my-user-moderation-dropdown>
45 </td> 88 </td>
46 </tr> 89 </tr>
@@ -56,3 +99,4 @@
56 </ng-template> 99 </ng-template>
57</p-table> 100</p-table>
58 101
102<my-user-ban-modal #userBanModal (userBanned)="onUserChanged()"></my-user-ban-modal>
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 47291918d..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 {
@@ -15,4 +15,12 @@ tr.banned {
15 15
16.ban-reason-label { 16.ban-reason-label {
17 font-weight: $font-semibold; 17 font-weight: $font-semibold;
18} \ No newline at end of file 18}
19
20.caption {
21 justify-content: space-between;
22
23 input {
24 @include peertube-input-text(250px);
25 }
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 dee3ed643..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,10 +1,12 @@
1import { Component, OnInit } 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 } from '../../../core' 4import { ConfirmService, ServerService } from '../../../core'
5import { RestPagination, RestTable, UserService } from '../../../shared' 5import { RestPagination, RestTable, UserService } from '../../../shared'
6import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { User } from '../../../../../../shared' 7import { User } from '../../../../../../shared'
8import { UserBanModalComponent } from '@app/shared/moderation'
9import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
8 10
9@Component({ 11@Component({
10 selector: 'my-user-list', 12 selector: 'my-user-list',
@@ -12,38 +14,139 @@ import { User } from '../../../../../../shared'
12 styleUrls: [ './user-list.component.scss' ] 14 styleUrls: [ './user-list.component.scss' ]
13}) 15})
14export class UserListComponent extends RestTable implements OnInit { 16export class UserListComponent extends RestTable implements OnInit {
17 @ViewChild('userBanModal') userBanModal: UserBanModalComponent
18
15 users: User[] = [] 19 users: User[] = []
16 totalRecords = 0 20 totalRecords = 0
17 rowsPerPage = 10 21 rowsPerPage = 10
18 sort: SortMeta = { field: 'createdAt', order: 1 } 22 sort: SortMeta = { field: 'createdAt', order: 1 }
19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 23 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
20 24
25 selectedUsers: User[] = []
26 bulkUserActions: DropdownAction<User[]>[] = []
27
21 constructor ( 28 constructor (
22 private notificationsService: NotificationsService, 29 private notifier: Notifier,
23 private confirmService: ConfirmService, 30 private confirmService: ConfirmService,
31 private serverService: ServerService,
24 private userService: UserService, 32 private userService: UserService,
25 private i18n: I18n 33 private i18n: I18n
26 ) { 34 ) {
27 super() 35 super()
28 } 36 }
29 37
38 get requiresEmailVerification () {
39 return this.serverService.getConfig().signup.requiresEmailVerification
40 }
41
30 ngOnInit () { 42 ngOnInit () {
31 this.loadSort() 43 this.initialize()
44
45 this.bulkUserActions = [
46 {
47 label: this.i18n('Delete'),
48 handler: users => this.removeUsers(users)
49 },
50 {
51 label: this.i18n('Ban'),
52 handler: users => this.openBanUserModal(users),
53 isDisplayed: users => users.every(u => u.blocked === false)
54 },
55 {
56 label: this.i18n('Unban'),
57 handler: users => this.unbanUsers(users),
58 isDisplayed: users => users.every(u => u.blocked === true)
59 },
60 {
61 label: this.i18n('Set Email as Verified'),
62 handler: users => this.setEmailsAsVerified(users),
63 isDisplayed: users => this.requiresEmailVerification && users.every(u => !u.blocked && u.emailVerified === false)
64 }
65 ]
66 }
67
68 openBanUserModal (users: User[]) {
69 for (const user of users) {
70 if (user.username === 'root') {
71 this.notifier.error(this.i18n('You cannot ban root.'))
72 return
73 }
74 }
75
76 this.userBanModal.openModal(users)
32 } 77 }
33 78
34 onUserChanged () { 79 onUserChanged () {
35 this.loadData() 80 this.loadData()
36 } 81 }
37 82
83 async unbanUsers (users: User[]) {
84 const message = this.i18n('Do you really want to unban {{num}} users?', { num: users.length })
85
86 const res = await this.confirmService.confirm(message, this.i18n('Unban'))
87 if (res === false) return
88
89 this.userService.unbanUsers(users)
90 .subscribe(
91 () => {
92 const message = this.i18n('{{num}} users unbanned.', { num: users.length })
93
94 this.notifier.success(message)
95 this.loadData()
96 },
97
98 err => this.notifier.error(err.message)
99 )
100 }
101
102 async removeUsers (users: User[]) {
103 for (const user of users) {
104 if (user.username === 'root') {
105 this.notifier.error(this.i18n('You cannot delete root.'))
106 return
107 }
108 }
109
110 const message = this.i18n('If you remove these users, you will not be able to create others with the same username!')
111 const res = await this.confirmService.confirm(message, this.i18n('Delete'))
112 if (res === false) return
113
114 this.userService.removeUser(users).subscribe(
115 () => {
116 this.notifier.success(this.i18n('{{num}} users deleted.', { num: users.length }))
117 this.loadData()
118 },
119
120 err => this.notifier.error(err.message)
121 )
122 }
123
124 async setEmailsAsVerified (users: User[]) {
125 this.userService.updateUsers(users, { emailVerified: true }).subscribe(
126 () => {
127 this.notifier.success(this.i18n('{{num}} users email set as verified.', { num: users.length }))
128 this.loadData()
129 },
130
131 err => this.notifier.error(err.message)
132 )
133 }
134
135 isInSelectionMode () {
136 return this.selectedUsers.length !== 0
137 }
138
38 protected loadData () { 139 protected loadData () {
39 this.userService.getUsers(this.pagination, this.sort) 140 this.selectedUsers = []
40 .subscribe( 141
41 resultList => { 142 this.userService.getUsers(this.pagination, this.sort, this.search)
42 this.users = resultList.data 143 .subscribe(
43 this.totalRecords = resultList.total 144 resultList => {
44 }, 145 this.users = resultList.data
45 146 this.totalRecords = resultList.total
46 err => this.notificationsService.error(this.i18n('Error'), err.message) 147 },
47 ) 148
149 err => this.notifier.error(err.message)
150 )
48 } 151 }
49} 152}
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html
new file mode 100644
index 000000000..a96a11f5e
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html
@@ -0,0 +1,26 @@
1<div class="admin-sub-header">
2 <div i18n class="form-sub-title">Muted accounts</div>
3</div>
4
5<p-table
6 [value]="blockedAccounts" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
7 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
8>
9
10 <ng-template pTemplate="header">
11 <tr>
12 <th i18n>Account</th>
13 <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
14 </tr>
15 </ng-template>
16
17 <ng-template pTemplate="body" let-accountBlock>
18 <tr>
19 <td>{{ accountBlock.blockedAccount.nameWithHost }}</td>
20 <td>{{ accountBlock.createdAt }}</td>
21 <td class="action-cell">
22 <button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button>
23 </td>
24 </tr>
25 </ng-template>
26</p-table>
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss
new file mode 100644
index 000000000..6028b75ea
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss
@@ -0,0 +1,7 @@
1@import '_variables';
2@import '_mixins';
3
4.unblock-button {
5 @include peertube-button;
6 @include grey-button;
7} \ No newline at end of file
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
new file mode 100644
index 000000000..e3025dec4
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts
@@ -0,0 +1,56 @@
1import { Component, OnInit } from '@angular/core'
2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { AccountBlock, BlocklistService } from '@app/shared/blocklist'
7
8@Component({
9 selector: 'my-account-blocklist',
10 styleUrls: [ './my-account-blocklist.component.scss' ],
11 templateUrl: './my-account-blocklist.component.html'
12})
13export class MyAccountBlocklistComponent extends RestTable implements OnInit {
14 blockedAccounts: AccountBlock[] = []
15 totalRecords = 0
16 rowsPerPage = 10
17 sort: SortMeta = { field: 'createdAt', order: -1 }
18 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
19
20 constructor (
21 private notifier: Notifier,
22 private blocklistService: BlocklistService,
23 private i18n: I18n
24 ) {
25 super()
26 }
27
28 ngOnInit () {
29 this.initialize()
30 }
31
32 unblockAccount (accountBlock: AccountBlock) {
33 const blockedAccount = accountBlock.blockedAccount
34
35 this.blocklistService.unblockAccountByUser(blockedAccount)
36 .subscribe(
37 () => {
38 this.notifier.success(this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: blockedAccount.nameWithHost }))
39
40 this.loadData()
41 }
42 )
43 }
44
45 protected loadData () {
46 return this.blocklistService.getUserAccountBlocklist(this.pagination, this.sort)
47 .subscribe(
48 resultList => {
49 this.blockedAccounts = resultList.data
50 this.totalRecords = resultList.total
51 },
52
53 err => this.notifier.error(err.message)
54 )
55 }
56}
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html
new file mode 100644
index 000000000..329cfb08f
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html
@@ -0,0 +1,27 @@
1<div class="admin-sub-header">
2 <div i18n class="form-sub-title">Muted instances</div>
3</div>
4
5<p-table
6 [value]="blockedServers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
7 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
8>
9
10 <ng-template pTemplate="header">
11 <tr>
12 <th i18n>Instance</th>
13 <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
14 <th></th>
15 </tr>
16 </ng-template>
17
18 <ng-template pTemplate="body" let-serverBlock>
19 <tr>
20 <td>{{ serverBlock.blockedServer.host }}</td>
21 <td>{{ serverBlock.createdAt }}</td>
22 <td class="action-cell">
23 <button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button>
24 </td>
25 </tr>
26 </ng-template>
27</p-table>
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss
new file mode 100644
index 000000000..6028b75ea
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss
@@ -0,0 +1,7 @@
1@import '_variables';
2@import '_mixins';
3
4.unblock-button {
5 @include peertube-button;
6 @include grey-button;
7} \ No newline at end of file
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
new file mode 100644
index 000000000..4c5cc28b8
--- /dev/null
+++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts
@@ -0,0 +1,57 @@
1import { Component, OnInit } from '@angular/core'
2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { ServerBlock } from '../../../../../shared'
7import { BlocklistService } from '@app/shared/blocklist'
8
9@Component({
10 selector: 'my-account-server-blocklist',
11 styleUrls: [ './my-account-server-blocklist.component.scss' ],
12 templateUrl: './my-account-server-blocklist.component.html'
13})
14export class MyAccountServerBlocklistComponent extends RestTable implements OnInit {
15 blockedServers: ServerBlock[] = []
16 totalRecords = 0
17 rowsPerPage = 10
18 sort: SortMeta = { field: 'createdAt', order: -1 }
19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
20
21 constructor (
22 private notifier: Notifier,
23 private blocklistService: BlocklistService,
24 private i18n: I18n
25 ) {
26 super()
27 }
28
29 ngOnInit () {
30 this.initialize()
31 }
32
33 unblockServer (serverBlock: ServerBlock) {
34 const host = serverBlock.blockedServer.host
35
36 this.blocklistService.unblockServerByUser(host)
37 .subscribe(
38 () => {
39 this.notifier.success(this.i18n('Instance {{host}} unmuted.', { host }))
40
41 this.loadData()
42 }
43 )
44 }
45
46 protected loadData () {
47 return this.blocklistService.getUserServerBlocklist(this.pagination, this.sort)
48 .subscribe(
49 resultList => {
50 this.blockedServers = resultList.data
51 this.totalRecords = resultList.total
52 },
53
54 err => this.notifier.error(err.message)
55 )
56 }
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 13517b9f4..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,27 +21,14 @@ 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 }
32 29
33 ngOnInit () { 30 ngOnInit () {
34 this.loadSort() 31 this.initialize()
35 }
36
37 protected loadData () {
38 return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort)
39 .subscribe(
40 resultList => {
41 this.videoChangeOwnerships = resultList.data
42 this.totalRecords = resultList.total
43 },
44
45 err => this.notificationsService.error(this.i18n('Error'), err.message)
46 )
47 } 32 }
48 33
49 createByString (account: Account) { 34 createByString (account: Account) {
@@ -62,7 +47,19 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit {
62 this.videoOwnershipService.refuseOwnership(videoChangeOwnership.id) 47 this.videoOwnershipService.refuseOwnership(videoChangeOwnership.id)
63 .subscribe( 48 .subscribe(
64 () => this.loadData(), 49 () => this.loadData(),
65 err => this.notificationsService.error(this.i18n('Error'), err.message) 50 err => this.notifier.error(err.message)
51 )
52 }
53
54 protected loadData () {
55 return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort)
56 .subscribe(
57 resultList => {
58 this.videoChangeOwnerships = resultList.data
59 this.totalRecords = resultList.total
60 },
61
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 4b2168e35..9996218ca 100644
--- a/client/src/app/+my-account/my-account-routing.module.ts
+++ b/client/src/app/+my-account/my-account-routing.module.ts
@@ -11,6 +11,10 @@ import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-accoun
11import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' 11import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
12import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' 12import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
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'
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'
14 18
15const myAccountRoutes: Routes = [ 19const myAccountRoutes: Routes = [
16 { 20 {
@@ -94,6 +98,42 @@ const myAccountRoutes: Routes = [
94 title: 'Ownership changes' 98 title: 'Ownership changes'
95 } 99 }
96 } 100 }
101 },
102 {
103 path: 'blocklist/accounts',
104 component: MyAccountBlocklistComponent,
105 data: {
106 meta: {
107 title: 'Muted accounts'
108 }
109 }
110 },
111 {
112 path: 'blocklist/servers',
113 component: MyAccountServerBlocklistComponent,
114 data: {
115 meta: {
116 title: 'Muted instances'
117 }
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 }
97 } 137 }
98 ] 138 ]
99 } 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.html b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html
index 96629940f..049119fa8 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html
+++ b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html
@@ -15,10 +15,19 @@
15 </div> 15 </div>
16 </div> 16 </div>
17 17
18 <my-peertube-checkbox 18 <div class="form-group">
19 inputName="autoPlayVideo" formControlName="autoPlayVideo" 19 <my-peertube-checkbox
20 i18n-labelText labelText="Automatically plays video" 20 inputName="webTorrentEnabled" formControlName="webTorrentEnabled"
21 ></my-peertube-checkbox> 21 i18n-labelText labelText="Use WebTorrent to exchange parts of the video with others"
22 ></my-peertube-checkbox>
23 </div>
24
25 <div class="form-group">
26 <my-peertube-checkbox
27 inputName="autoPlayVideo" formControlName="autoPlayVideo"
28 i18n-labelText labelText="Automatically plays video"
29 ></my-peertube-checkbox>
30 </div>
22 31
23 <input type="submit" i18n-value value="Save" [disabled]="!form.valid"> 32 <input type="submit" i18n-value value="Save" [disabled]="!form.valid">
24</form> 33</form>
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 7089b2057..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 ) {
@@ -29,12 +29,14 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
29 ngOnInit () { 29 ngOnInit () {
30 this.buildForm({ 30 this.buildForm({
31 nsfwPolicy: null, 31 nsfwPolicy: null,
32 webTorrentEnabled: null,
32 autoPlayVideo: null 33 autoPlayVideo: null
33 }) 34 })
34 35
35 this.userInformationLoaded.subscribe(() => { 36 this.userInformationLoaded.subscribe(() => {
36 this.form.patchValue({ 37 this.form.patchValue({
37 nsfwPolicy: this.user.nsfwPolicy, 38 nsfwPolicy: this.user.nsfwPolicy,
39 webTorrentEnabled: this.user.webTorrentEnabled,
38 autoPlayVideo: this.user.autoPlayVideo === true 40 autoPlayVideo: this.user.autoPlayVideo === true
39 }) 41 })
40 }) 42 })
@@ -42,20 +44,22 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
42 44
43 updateDetails () { 45 updateDetails () {
44 const nsfwPolicy = this.form.value['nsfwPolicy'] 46 const nsfwPolicy = this.form.value['nsfwPolicy']
47 const webTorrentEnabled = this.form.value['webTorrentEnabled']
45 const autoPlayVideo = this.form.value['autoPlayVideo'] 48 const autoPlayVideo = this.form.value['autoPlayVideo']
46 const details: UserUpdateMe = { 49 const details: UserUpdateMe = {
47 nsfwPolicy, 50 nsfwPolicy,
51 webTorrentEnabled,
48 autoPlayVideo 52 autoPlayVideo
49 } 53 }
50 54
51 this.userService.updateMyProfile(details).subscribe( 55 this.userService.updateMyProfile(details).subscribe(
52 () => { 56 () => {
53 this.notificationsService.success(this.i18n('Success'), this.i18n('Information updated.')) 57 this.notifier.success(this.i18n('Information updated.'))
54 58
55 this.authService.refreshUserInformation() 59 this.authService.refreshUserInformation()
56 }, 60 },
57 61
58 err => this.notificationsService.error(this.i18n('Error'), err.message) 62 err => this.notifier.error(err.message)
59 ) 63 )
60 } 64 }
61} 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-edit.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts
index ccdd9a3dc..4dc65dd99 100644
--- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts
+++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts
@@ -4,7 +4,11 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
4export abstract class MyAccountVideoChannelEdit extends FormReactive { 4export abstract class MyAccountVideoChannelEdit extends FormReactive {
5 // We need it even in the create component because it's used in the edit template 5 // We need it even in the create component because it's used in the edit template
6 videoChannelToUpdate: VideoChannel 6 videoChannelToUpdate: VideoChannel
7 instanceHost: string
7 8
8 abstract isCreation (): boolean 9 abstract isCreation (): boolean
9 abstract getFormButtonTitle (): string 10 abstract getFormButtonTitle (): string
11
12 // FIXME: We need this method so angular does not complain in the child template
13 onAvatarChange (formData: FormData) { /* empty */ }
10} 14}
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 56697030b..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, ViewChild } 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'
@@ -17,18 +16,16 @@ import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators
17 styleUrls: [ './my-account-video-channel-edit.component.scss' ] 16 styleUrls: [ './my-account-video-channel-edit.component.scss' ]
18}) 17})
19export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelEdit implements OnInit, OnDestroy { 18export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelEdit implements OnInit, OnDestroy {
20 @ViewChild('avatarfileInput') avatarfileInput
21
22 error: string 19 error: string
23
24 videoChannelToUpdate: VideoChannel 20 videoChannelToUpdate: VideoChannel
21
25 private paramsSub: Subscription 22 private paramsSub: Subscription
26 23
27 constructor ( 24 constructor (
28 protected formValidatorService: FormValidatorService, 25 protected formValidatorService: FormValidatorService,
29 private authService: AuthService, 26 private authService: AuthService,
30 private videoChannelValidatorsService: VideoChannelValidatorsService, 27 private videoChannelValidatorsService: VideoChannelValidatorsService,
31 private notificationsService: NotificationsService, 28 private notifier: Notifier,
32 private router: Router, 29 private router: Router,
33 private route: ActivatedRoute, 30 private route: ActivatedRoute,
34 private videoChannelService: VideoChannelService, 31 private videoChannelService: VideoChannelService,
@@ -81,10 +78,11 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
81 this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe( 78 this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe(
82 () => { 79 () => {
83 this.authService.refreshUserInformation() 80 this.authService.refreshUserInformation()
84 this.notificationsService.success( 81
85 this.i18n('Success'), 82 this.notifier.success(
86 this.i18n('Video channel {{videoChannelName}} updated.', { videoChannelName: videoChannelUpdate.displayName }) 83 this.i18n('Video channel {{videoChannelName}} updated.', { videoChannelName: videoChannelUpdate.displayName })
87 ) 84 )
85
88 this.router.navigate([ '/my-account', 'video-channels' ]) 86 this.router.navigate([ '/my-account', 'video-channels' ])
89 }, 87 },
90 88
@@ -96,12 +94,12 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
96 this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData) 94 this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData)
97 .subscribe( 95 .subscribe(
98 data => { 96 data => {
99 this.notificationsService.success(this.i18n('Success'), this.i18n('Avatar changed.')) 97 this.notifier.success(this.i18n('Avatar changed.'))
100 98
101 this.videoChannelToUpdate.updateAvatar(data.avatar) 99 this.videoChannelToUpdate.updateAvatar(data.avatar)
102 }, 100 },
103 101
104 err => this.notificationsService.error(this.i18n('Error'), err.message) 102 err => this.notifier.error(err.message)
105 ) 103 )
106 } 104 }
107 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 d9fb20446..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 ) {
@@ -27,7 +27,7 @@ export class MyAccountVideoImportsComponent extends RestTable implements OnInit
27 } 27 }
28 28
29 ngOnInit () { 29 ngOnInit () {
30 this.loadSort() 30 this.initialize()
31 } 31 }
32 32
33 isVideoImportSuccess (videoImport: VideoImport) { 33 isVideoImportSuccess (videoImport: VideoImport) {
@@ -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 7560f0128..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
@@ -169,7 +163,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
169 163
170 private spliceVideosById (id: number) { 164 private spliceVideosById (id: number) {
171 for (const key of Object.keys(this.loadedPages)) { 165 for (const key of Object.keys(this.loadedPages)) {
172 const videos = this.loadedPages[ key ] 166 const videos: Video[] = this.loadedPages[ key ]
173 const index = videos.findIndex(v => v.id === id) 167 const index = videos.findIndex(v => v.id === id)
174 168
175 if (index !== -1) { 169 if (index !== -1) {
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 69b198faa..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">
@@ -22,9 +23,9 @@
22 </span> 23 </span>
23 24
24 <input 25 <input
25 type="submit" i18n-value value="Submit" class="action-button-submit" 26 type="submit" i18n-value value="Submit" class="action-button-submit"
26 [disabled]="!form.valid" 27 [disabled]="!form.valid"
27 (click)="close()" 28 (click)="close()"
28 /> 29 />
29 </div> 30 </div>
30 </div> 31 </div>
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 7437b939a..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
@@ -49,15 +49,13 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni
49 .catch((_) => _) // Called when closing (cancel) the modal without validating, do nothing 49 .catch((_) => _) // Called when closing (cancel) the modal without validating, do nothing
50 } 50 }
51 51
52 search (event) { 52 search (event: { query: string }) {
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 b602fd69f..3999252be 100644
--- a/client/src/app/+my-account/my-account.component.html
+++ b/client/src/app/+my-account/my-account.component.html
@@ -1,26 +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 <a i18n routerLink="/my-account/ownership" routerLinkActive="active" class="title-page">Ownership changes</a>
23 </div>
24 3
25 <div class="margin-content"> 4 <div class="margin-content">
26 <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 20b2639b5..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 { 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 bad60a8fb..8a4102d80 100644
--- a/client/src/app/+my-account/my-account.component.ts
+++ b/client/src/app/+my-account/my-account.component.ts
@@ -1,37 +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
17 private routeSub: Subscription
18 13
19 constructor ( 14 constructor (
20 private serverService: ServerService, 15 private serverService: ServerService,
21 private router: Router,
22 private i18n: I18n 16 private i18n: I18n
23 ) {} 17 ) {
24 18
25 ngOnInit () { 19 const libraryEntries: TopMenuDropdownParam = {
26 this.updateLibraryLabel(this.router.url) 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 }
27 40
28 this.routeSub = this.router.events 41 if (this.isVideoImportEnabled()) {
29 .pipe(filter(event => event instanceof NavigationStart)) 42 libraryEntries.children.push({
30 .subscribe((event: NavigationStart) => this.updateLibraryLabel(event.url)) 43 label: 'My imports',
31 } 44 routerLink: '/my-account/video-imports'
45 })
46 }
32 47
33 ngOnDestroy () { 48 const miscEntries: TopMenuDropdownParam = {
34 if (this.routeSub) this.routeSub.unsubscribe() 49 label: this.i18n('Misc'),
50 children: [
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 }
65
66 this.menuEntries = [
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 ]
35 } 78 }
36 79
37 isVideoImportEnabled () { 80 isVideoImportEnabled () {
@@ -40,19 +83,4 @@ export class MyAccountComponent implements OnInit, OnDestroy {
40 return importConfig.http.enabled || importConfig.torrent.enabled 83 return importConfig.http.enabled || importConfig.torrent.enabled
41 } 84 }
42 85
43 private updateLibraryLabel (url: string) {
44 const [ path ] = url.split('?')
45
46 if (path.startsWith('/my-account/video-channels')) {
47 this.libraryLabel = this.i18n('Channels')
48 } else if (path.startsWith('/my-account/videos')) {
49 this.libraryLabel = this.i18n('Videos')
50 } else if (path.startsWith('/my-account/subscriptions')) {
51 this.libraryLabel = this.i18n('Subscriptions')
52 } else if (path.startsWith('/my-account/video-imports')) {
53 this.libraryLabel = this.i18n('Video imports')
54 } else {
55 this.libraryLabel = ''
56 }
57 }
58} 86}
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index ad21162a8..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'
@@ -19,6 +20,11 @@ import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-i
19import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' 20import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
20import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone' 21import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone'
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'
23import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-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'
22 28
23@NgModule({ 29@NgModule({
24 imports: [ 30 imports: [
@@ -26,7 +32,8 @@ import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-sub
26 MyAccountRoutingModule, 32 MyAccountRoutingModule,
27 AutoCompleteModule, 33 AutoCompleteModule,
28 SharedModule, 34 SharedModule,
29 TableModule 35 TableModule,
36 InputSwitchModule
30 ], 37 ],
31 38
32 declarations: [ 39 declarations: [
@@ -45,7 +52,12 @@ import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-sub
45 ActorAvatarInfoComponent, 52 ActorAvatarInfoComponent,
46 MyAccountVideoImportsComponent, 53 MyAccountVideoImportsComponent,
47 MyAccountDangerZoneComponent, 54 MyAccountDangerZoneComponent,
48 MyAccountSubscriptionsComponent 55 MyAccountSubscriptionsComponent,
56 MyAccountBlocklistComponent,
57 MyAccountServerBlocklistComponent,
58 MyAccountHistoryComponent,
59 MyAccountNotificationsComponent,
60 MyAccountNotificationPreferencesComponent
49 ], 61 ],
50 62
51 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 7b80b1ed4..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, 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',
@@ -10,7 +10,7 @@ import { Account } from '@app/shared/account/account.model'
10 styleUrls: [ './actor-avatar-info.component.scss' ] 10 styleUrls: [ './actor-avatar-info.component.scss' ]
11}) 11})
12export class ActorAvatarInfoComponent { 12export class ActorAvatarInfoComponent {
13 @ViewChild('avatarfileInput') avatarfileInput 13 @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement>
14 14
15 @Input() actor: VideoChannel | Account 15 @Input() actor: VideoChannel | Account
16 16
@@ -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 26b3bf4b1..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
@@ -25,12 +25,11 @@ export class VerifyAccountEmailComponent implements OnInit {
25 } 25 }
26 26
27 ngOnInit () { 27 ngOnInit () {
28
29 this.userId = this.route.snapshot.queryParams['userId'] 28 this.userId = this.route.snapshot.queryParams['userId']
30 this.verificationString = this.route.snapshot.queryParams['verificationString'] 29 this.verificationString = this.route.snapshot.queryParams['verificationString']
31 30
32 if (!this.userId || !this.verificationString) { 31 if (!this.userId || !this.verificationString) {
33 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.'))
34 } else { 33 } else {
35 this.verifyEmail() 34 this.verifyEmail()
36 } 35 }
@@ -47,7 +46,7 @@ export class VerifyAccountEmailComponent implements OnInit {
47 }, 46 },
48 47
49 err => { 48 err => {
50 this.notificationsService.error(this.i18n('Error'), err.message) 49 this.notifier.error(err.message)
51 } 50 }
52 ) 51 )
53 } 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 800d97b7f..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',
@@ -25,7 +25,7 @@ import { ScreenService } from '@app/shared/misc/screen.service'
25export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { 25export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
26 titlePage: string 26 titlePage: string
27 marginContent = false // Disable margin 27 marginContent = false // Disable margin
28 currentRoute = '/video-channel/videos' 28 currentRoute = '/video-channels/videos'
29 loadOnInit = false 29 loadOnInit = false
30 30
31 private videoChannel: VideoChannel 31 private videoChannel: VideoChannel
@@ -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-channel/' + 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 34e890b40..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: [
@@ -69,7 +69,7 @@ export function metaFactory (serverService: ServerService): MetaLoader {
69 providers: [ 69 providers: [
70 { 70 {
71 provide: TRANSLATIONS, 71 provide: TRANSLATIONS,
72 useFactory: (locale) => { 72 useFactory: (locale: string) => {
73 // On dev mode, test localization 73 // On dev mode, test localization
74 if (isOnDevLocale()) { 74 if (isOnDevLocale()) {
75 locale = buildFileLocale(getDevLocale()) 75 locale = buildFileLocale(getDevLocale())
diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts
index 74ed1c580..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,8 +71,10 @@ 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',
77 WEBTORRENT_ENABLED: 'peertube-videojs-' + 'webtorrent_enabled',
75 AUTO_PLAY_VIDEO: 'auto_play_video' 78 AUTO_PLAY_VIDEO: 'auto_play_video'
76 } 79 }
77 80
@@ -87,7 +90,9 @@ export class AuthUser extends User {
87 email: peertubeLocalStorage.getItem(this.KEYS.EMAIL), 90 email: peertubeLocalStorage.getItem(this.KEYS.EMAIL),
88 role: parseInt(peertubeLocalStorage.getItem(this.KEYS.ROLE), 10) as UserRole, 91 role: parseInt(peertubeLocalStorage.getItem(this.KEYS.ROLE), 10) as UserRole,
89 nsfwPolicy: peertubeLocalStorage.getItem(this.KEYS.NSFW_POLICY) as NSFWPolicyType, 92 nsfwPolicy: peertubeLocalStorage.getItem(this.KEYS.NSFW_POLICY) as NSFWPolicyType,
90 autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true' 93 webTorrentEnabled: peertubeLocalStorage.getItem(this.KEYS.WEBTORRENT_ENABLED) === 'true',
94 autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true',
95 videosHistoryEnabled: peertubeLocalStorage.getItem(this.KEYS.VIDEOS_HISTORY_ENABLED) === 'true'
91 }, 96 },
92 Tokens.load() 97 Tokens.load()
93 ) 98 )
@@ -101,12 +106,14 @@ export class AuthUser extends User {
101 peertubeLocalStorage.removeItem(this.KEYS.ID) 106 peertubeLocalStorage.removeItem(this.KEYS.ID)
102 peertubeLocalStorage.removeItem(this.KEYS.ROLE) 107 peertubeLocalStorage.removeItem(this.KEYS.ROLE)
103 peertubeLocalStorage.removeItem(this.KEYS.NSFW_POLICY) 108 peertubeLocalStorage.removeItem(this.KEYS.NSFW_POLICY)
109 peertubeLocalStorage.removeItem(this.KEYS.WEBTORRENT_ENABLED)
110 peertubeLocalStorage.removeItem(this.KEYS.VIDEOS_HISTORY_ENABLED)
104 peertubeLocalStorage.removeItem(this.KEYS.AUTO_PLAY_VIDEO) 111 peertubeLocalStorage.removeItem(this.KEYS.AUTO_PLAY_VIDEO)
105 peertubeLocalStorage.removeItem(this.KEYS.EMAIL) 112 peertubeLocalStorage.removeItem(this.KEYS.EMAIL)
106 Tokens.flush() 113 Tokens.flush()
107 } 114 }
108 115
109 constructor (userHash: UserConstructorHash, hashTokens: TokenOptions) { 116 constructor (userHash: Partial<ServerUserModel>, hashTokens: TokenOptions) {
110 super(userHash) 117 super(userHash)
111 this.tokens = new Tokens(hashTokens) 118 this.tokens = new Tokens(hashTokens)
112 } 119 }
@@ -138,6 +145,7 @@ export class AuthUser extends User {
138 peertubeLocalStorage.setItem(AuthUser.KEYS.EMAIL, this.email) 145 peertubeLocalStorage.setItem(AuthUser.KEYS.EMAIL, this.email)
139 peertubeLocalStorage.setItem(AuthUser.KEYS.ROLE, this.role.toString()) 146 peertubeLocalStorage.setItem(AuthUser.KEYS.ROLE, this.role.toString())
140 peertubeLocalStorage.setItem(AuthUser.KEYS.NSFW_POLICY, this.nsfwPolicy.toString()) 147 peertubeLocalStorage.setItem(AuthUser.KEYS.NSFW_POLICY, this.nsfwPolicy.toString())
148 peertubeLocalStorage.setItem(AuthUser.KEYS.WEBTORRENT_ENABLED, JSON.stringify(this.webTorrentEnabled))
141 peertubeLocalStorage.setItem(AuthUser.KEYS.AUTO_PLAY_VIDEO, JSON.stringify(this.autoPlayVideo)) 149 peertubeLocalStorage.setItem(AuthUser.KEYS.AUTO_PLAY_VIDEO, JSON.stringify(this.autoPlayVideo))
142 this.tokens.save() 150 this.tokens.save()
143 } 151 }
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts
index 9c36b946e..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 () {
@@ -221,7 +217,7 @@ export class AuthService {
221 } 217 }
222 218
223 refreshUserInformation () { 219 refreshUserInformation () {
224 const obj = { 220 const obj: UserLoginWithUsername = {
225 access_token: this.user.getAccessToken(), 221 access_token: this.user.getAccessToken(),
226 refresh_token: null, 222 refresh_token: null,
227 token_type: this.user.getTokenType(), 223 token_type: this.user.getTokenType(),
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 2f1ef1fc2..c868ccdcc 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,
@@ -44,7 +51,10 @@ export class ServerService {
44 requiresEmailVerification: false 51 requiresEmailVerification: false
45 }, 52 },
46 transcoding: { 53 transcoding: {
47 enabledResolutions: [] 54 enabledResolutions: [],
55 hls: {
56 enabled: false
57 }
48 }, 58 },
49 avatar: { 59 avatar: {
50 file: { 60 file: {
@@ -80,6 +90,11 @@ export class ServerService {
80 enabled: false 90 enabled: false
81 } 91 }
82 } 92 }
93 },
94 trending: {
95 videos: {
96 intervalDays: 0
97 }
83 } 98 }
84 } 99 }
85 private videoCategories: Array<VideoConstant<number>> = [] 100 private videoCategories: Array<VideoConstant<number>> = []
@@ -141,10 +156,6 @@ export class ServerService {
141 return this.videoPrivacies 156 return this.videoPrivacies
142 } 157 }
143 158
144 getAbout () {
145 return this.http.get<About>(ServerService.BASE_CONFIG_URL + '/about')
146 }
147
148 private loadVideoAttributeEnum ( 159 private loadVideoAttributeEnum (
149 attributeName: 'categories' | 'licences' | 'languages' | 'privacies', 160 attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
150 hashToPopulate: VideoConstant<string | number>[], 161 hashToPopulate: VideoConstant<string | number>[],
@@ -154,7 +165,7 @@ export class ServerService {
154 this.localeObservable 165 this.localeObservable
155 .pipe( 166 .pipe(
156 switchMap(translations => { 167 switchMap(translations => {
157 return this.http.get(ServerService.BASE_VIDEO_URL + attributeName) 168 return this.http.get<{ [id: string]: string }>(ServerService.BASE_VIDEO_URL + attributeName)
158 .pipe(map(data => ({ data, translations }))) 169 .pipe(map(data => ({ data, translations })))
159 }) 170 })
160 ) 171 )
diff --git a/client/src/app/core/theme/theme.service.ts b/client/src/app/core/theme/theme.service.ts
index a6eef0898..50c19ecac 100644
--- a/client/src/app/core/theme/theme.service.ts
+++ b/client/src/app/core/theme/theme.service.ts
@@ -5,7 +5,7 @@ import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
5export class ThemeService { 5export class ThemeService {
6 private theme = document.querySelector('body') 6 private theme = document.querySelector('body')
7 private darkTheme = false 7 private darkTheme = false
8 private previousTheme = {} 8 private previousTheme: { [ id: string ]: string } = {}
9 9
10 constructor () { 10 constructor () {
11 // initialise the alternative theme with dark theme colors 11 // initialise the alternative theme with dark theme colors
@@ -33,7 +33,7 @@ export class ThemeService {
33 } 33 }
34 } 34 }
35 35
36 private switchProperty (property, newValue?) { 36 private switchProperty (property: string, newValue?: string) {
37 const propertyOldvalue = window.getComputedStyle(this.theme).getPropertyValue('--' + property) 37 const propertyOldvalue = window.getComputedStyle(this.theme).getPropertyValue('--' + property)
38 this.theme.style.setProperty('--' + property, (newValue) ? newValue : this.previousTheme[property]) 38 this.theme.style.setProperty('--' + property, (newValue) ? newValue : this.previousTheme[property])
39 this.previousTheme[property] = propertyOldvalue 39 this.previousTheme[property] = propertyOldvalue
diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html
index a04354db5..46a87c79c 100644
--- a/client/src/app/header/header.component.html
+++ b/client/src/app/header/header.component.html
@@ -1,10 +1,10 @@
1<input 1<input
2 type="text" id="search-video" name="search-video" i18n-placeholder placeholder="Search..." 2 type="text" id="search-video" name="search-video" i18n-placeholder placeholder="Search..."
3 [(ngModel)]="searchValue" (keyup.enter)="doSearch()" 3 [(ngModel)]="searchValue" (keyup.enter)="doSearch()"
4> 4>
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 bd03c338a..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.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 a842765ba..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
@@ -131,10 +124,14 @@ menu {
131 transition: background-color .1s ease-in-out; 124 transition: background-color .1s ease-in-out;
132 @include disable-default-a-behaviour; 125 @include disable-default-a-behaviour;
133 126
134 &:hover, &.focus-visible { 127 &.active {
135 background-color: rgba(255, 255, 255, 0.15); 128 background-color: rgba(255, 255, 255, 0.15);
136 } 129 }
137 130
131 &:hover, &.focus-visible {
132 background-color: rgba(255, 255, 255, 0.10);
133 }
134
138 .icon { 135 .icon {
139 @include icon(22px); 136 @include icon(22px);
140 137
@@ -246,7 +243,7 @@ menu {
246 } 243 }
247} 244}
248 245
249@media screen and (max-width: 400px) { 246@media screen and (max-width: $mobile-view) {
250 .menu-wrapper { 247 .menu-wrapper {
251 width: 100% !important; 248 width: 100% !important;
252 } 249 }
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts
index 95926f5f0..371beb4a5 100644
--- a/client/src/app/menu/menu.component.ts
+++ b/client/src/app/menu/menu.component.ts
@@ -18,7 +18,7 @@ export class MenuComponent implements OnInit {
18 userHasAdminAccess = false 18 userHasAdminAccess = false
19 helpVisible = false 19 helpVisible = false
20 20
21 private routesPerRight = { 21 private routesPerRight: { [ role in UserRight ]?: string } = {
22 [UserRight.MANAGE_USERS]: '/admin/users', 22 [UserRight.MANAGE_USERS]: '/admin/users',
23 [UserRight.MANAGE_SERVER_FOLLOW]: '/admin/friends', 23 [UserRight.MANAGE_SERVER_FOLLOW]: '/admin/friends',
24 [UserRight.MANAGE_VIDEO_ABUSES]: '/admin/moderation/video-abuses', 24 [UserRight.MANAGE_VIDEO_ABUSES]: '/admin/moderation/video-abuses',
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 911d56843..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, RedirectService } 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,10 +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 redirectService: RedirectService, 42 private notifier: Notifier,
44 private notificationsService: NotificationsService,
45 private searchService: SearchService, 43 private searchService: SearchService,
46 private authService: AuthService 44 private authService: AuthService,
45 private serverService: ServerService
47 ) { } 46 ) { }
48 47
49 ngOnInit () { 48 ngOnInit () {
@@ -69,7 +68,7 @@ export class SearchComponent implements OnInit, OnDestroy {
69 this.search() 68 this.search()
70 }, 69 },
71 70
72 err => this.notificationsService.error('Error', err.text) 71 err => this.notifier.error(err.text)
73 ) 72 )
74 } 73 }
75 74
@@ -77,6 +76,10 @@ export class SearchComponent implements OnInit, OnDestroy {
77 if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe() 76 if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe()
78 } 77 }
79 78
79 isVideoBlur (video: Video) {
80 return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig())
81 }
82
80 isVideoChannel (d: VideoChannel | Video): d is VideoChannel { 83 isVideoChannel (d: VideoChannel | Video): d is VideoChannel {
81 return d instanceof VideoChannel 84 return d instanceof VideoChannel
82 } 85 }
@@ -113,9 +116,7 @@ export class SearchComponent implements OnInit, OnDestroy {
113 this.firstSearch = false 116 this.firstSearch = false
114 }, 117 },
115 118
116 error => { 119 err => this.notifier.error(err.message)
117 this.notificationsService.error(this.i18n('Error'), error.message)
118 }
119 ) 120 )
120 121
121 } 122 }
@@ -147,7 +148,8 @@ export class SearchComponent implements OnInit, OnDestroy {
147 } 148 }
148 149
149 private updateTitle () { 150 private updateTitle () {
150 this.metaService.setTitle(this.i18n('Search') + ' ' + this.currentSearch) 151 const suffix = this.currentSearch ? ' ' + this.currentSearch : ''
152 this.metaService.setTitle(this.i18n('Search') + suffix)
151 } 153 }
152 154
153 private updateUrlFromAdvancedSearch () { 155 private updateUrlFromAdvancedSearch () {
diff --git a/client/src/app/shared/account/account.model.ts b/client/src/app/shared/account/account.model.ts
index 42f2cfeaf..c5cd2051c 100644
--- a/client/src/app/shared/account/account.model.ts
+++ b/client/src/app/shared/account/account.model.ts
@@ -5,6 +5,10 @@ export class Account extends Actor implements ServerAccount {
5 displayName: string 5 displayName: string
6 description: string 6 description: string
7 nameWithHost: string 7 nameWithHost: string
8 mutedByUser: boolean
9 mutedByInstance: boolean
10 mutedServerByUser: boolean
11 mutedServerByInstance: boolean
8 12
9 userId?: number 13 userId?: number
10 14
@@ -15,5 +19,10 @@ export class Account extends Actor implements ServerAccount {
15 this.description = hash.description 19 this.description = hash.description
16 this.userId = hash.userId 20 this.userId = hash.userId
17 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) 21 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host)
22
23 this.mutedByUser = false
24 this.mutedByInstance = false
25 this.mutedServerByUser = false
26 this.mutedServerByInstance = false
18 } 27 }
19} 28}
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/blocklist/account-block.model.ts b/client/src/app/shared/blocklist/account-block.model.ts
new file mode 100644
index 000000000..e7b433d88
--- /dev/null
+++ b/client/src/app/shared/blocklist/account-block.model.ts
@@ -0,0 +1,14 @@
1import { AccountBlock as AccountBlockServer } from '../../../../../shared'
2import { Account } from '../account/account.model'
3
4export class AccountBlock implements AccountBlockServer {
5 byAccount: Account
6 blockedAccount: Account
7 createdAt: Date | string
8
9 constructor (block: AccountBlockServer) {
10 this.byAccount = new Account(block.byAccount)
11 this.blockedAccount = new Account(block.blockedAccount)
12 this.createdAt = block.createdAt
13 }
14}
diff --git a/client/src/app/shared/blocklist/blocklist.service.ts b/client/src/app/shared/blocklist/blocklist.service.ts
new file mode 100644
index 000000000..c1f7312f0
--- /dev/null
+++ b/client/src/app/shared/blocklist/blocklist.service.ts
@@ -0,0 +1,135 @@
1import { Injectable } from '@angular/core'
2import { environment } from '../../../environments/environment'
3import { HttpClient, HttpParams } from '@angular/common/http'
4import { RestExtractor, RestPagination, RestService } from '../rest'
5import { SortMeta } from 'primeng/api'
6import { catchError, map } from 'rxjs/operators'
7import { AccountBlock as AccountBlockServer, ResultList, ServerBlock } from '../../../../../shared'
8import { Account } from '@app/shared/account/account.model'
9import { AccountBlock } from '@app/shared/blocklist/account-block.model'
10
11@Injectable()
12export class BlocklistService {
13 static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist'
14 static BASE_SERVER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/server/blocklist'
15
16 constructor (
17 private authHttp: HttpClient,
18 private restExtractor: RestExtractor,
19 private restService: RestService
20 ) { }
21
22 /*********************** User -> Account blocklist ***********************/
23
24 getUserAccountBlocklist (pagination: RestPagination, sort: SortMeta) {
25 let params = new HttpParams()
26 params = this.restService.addRestGetParams(params, pagination, sort)
27
28 return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', { params })
29 .pipe(
30 map(res => this.restExtractor.convertResultListDateToHuman(res)),
31 map(res => this.restExtractor.applyToResultListData(res, this.formatAccountBlock.bind(this))),
32 catchError(err => this.restExtractor.handleError(err))
33 )
34 }
35
36 blockAccountByUser (account: Account) {
37 const body = { accountName: account.nameWithHost }
38
39 return this.authHttp.post(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', body)
40 .pipe(catchError(err => this.restExtractor.handleError(err)))
41 }
42
43 unblockAccountByUser (account: Account) {
44 const path = BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts/' + account.nameWithHost
45
46 return this.authHttp.delete(path)
47 .pipe(catchError(err => this.restExtractor.handleError(err)))
48 }
49
50 /*********************** User -> Server blocklist ***********************/
51
52 getUserServerBlocklist (pagination: RestPagination, sort: SortMeta) {
53 let params = new HttpParams()
54 params = this.restService.addRestGetParams(params, pagination, sort)
55
56 return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', { params })
57 .pipe(
58 map(res => this.restExtractor.convertResultListDateToHuman(res)),
59 catchError(err => this.restExtractor.handleError(err))
60 )
61 }
62
63 blockServerByUser (host: string) {
64 const body = { host }
65
66 return this.authHttp.post(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', body)
67 .pipe(catchError(err => this.restExtractor.handleError(err)))
68 }
69
70 unblockServerByUser (host: string) {
71 const path = BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers/' + host
72
73 return this.authHttp.delete(path)
74 .pipe(catchError(err => this.restExtractor.handleError(err)))
75 }
76
77 /*********************** Instance -> Account blocklist ***********************/
78
79 getInstanceAccountBlocklist (pagination: RestPagination, sort: SortMeta) {
80 let params = new HttpParams()
81 params = this.restService.addRestGetParams(params, pagination, sort)
82
83 return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts', { params })
84 .pipe(
85 map(res => this.restExtractor.convertResultListDateToHuman(res)),
86 map(res => this.restExtractor.applyToResultListData(res, this.formatAccountBlock.bind(this))),
87 catchError(err => this.restExtractor.handleError(err))
88 )
89 }
90
91 blockAccountByInstance (account: Account) {
92 const body = { accountName: account.nameWithHost }
93
94 return this.authHttp.post(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts', body)
95 .pipe(catchError(err => this.restExtractor.handleError(err)))
96 }
97
98 unblockAccountByInstance (account: Account) {
99 const path = BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts/' + account.nameWithHost
100
101 return this.authHttp.delete(path)
102 .pipe(catchError(err => this.restExtractor.handleError(err)))
103 }
104
105 /*********************** Instance -> Server blocklist ***********************/
106
107 getInstanceServerBlocklist (pagination: RestPagination, sort: SortMeta) {
108 let params = new HttpParams()
109 params = this.restService.addRestGetParams(params, pagination, sort)
110
111 return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/servers', { params })
112 .pipe(
113 map(res => this.restExtractor.convertResultListDateToHuman(res)),
114 catchError(err => this.restExtractor.handleError(err))
115 )
116 }
117
118 blockServerByInstance (host: string) {
119 const body = { host }
120
121 return this.authHttp.post(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/servers', body)
122 .pipe(catchError(err => this.restExtractor.handleError(err)))
123 }
124
125 unblockServerByInstance (host: string) {
126 const path = BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/servers/' + host
127
128 return this.authHttp.delete(path)
129 .pipe(catchError(err => this.restExtractor.handleError(err)))
130 }
131
132 private formatAccountBlock (accountBlock: AccountBlockServer) {
133 return new AccountBlock(accountBlock)
134 }
135}
diff --git a/client/src/app/shared/blocklist/index.ts b/client/src/app/shared/blocklist/index.ts
new file mode 100644
index 000000000..5886ca07e
--- /dev/null
+++ b/client/src/app/shared/blocklist/index.ts
@@ -0,0 +1,2 @@
1export * from './blocklist.service'
2export * from './account-block.model'
diff --git a/client/src/app/shared/buttons/action-dropdown.component.html b/client/src/app/shared/buttons/action-dropdown.component.html
index 8110e2515..114b1d71f 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.html
+++ b/client/src/app/shared/buttons/action-dropdown.component.html
@@ -1,17 +1,27 @@
1<div class="dropdown-root" ngbDropdown [placement]="placement"> 1<div class="dropdown-root" ngbDropdown [placement]="placement">
2 <div class="action-button" [ngClass]="{ small: buttonSize === 'small' }" ngbDropdownToggle role="button"> 2 <div
3 <span class="icon icon-action"></span> 3 class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange' }"
4 ngbDropdownToggle role="button"
5 >
6 <my-global-icon *ngIf="!label" class="more-icon" iconName="more"></my-global-icon>
7 <span *ngIf="label" class="dropdown-toggle">{{ label }}</span>
4 </div> 8 </div>
5 9
6 <div ngbDropdownMenu class="dropdown-menu"> 10 <div ngbDropdownMenu class="dropdown-menu">
7 <ng-container *ngFor="let action of actions"> 11 <ng-container *ngFor="let actions of getActions()">
8 <div class="dropdown-item" *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true"> 12
9 <a *ngIf="action.linkBuilder" class="dropdown-item" [routerLink]="action.linkBuilder(entry)">{{ action.label }}</a> 13 <ng-container *ngFor="let action of actions">
14 <ng-container *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true">
15 <a *ngIf="action.linkBuilder" class="dropdown-item" [routerLink]="action.linkBuilder(entry)">{{ action.label }}</a>
16
17 <span *ngIf="!action.linkBuilder" class="custom-action dropdown-item" (click)="action.handler(entry)" role="button">
18 {{ action.label }}
19 </span>
20 </ng-container>
21 </ng-container>
22
23 <div class="dropdown-divider"></div>
10 24
11 <span *ngIf="!action.linkBuilder" class="custom-action" class="dropdown-item" (click)="action.handler(entry)" role="button">
12 {{ action.label }}
13 </span>
14 </div>
15 </ng-container> 25 </ng-container>
16 </div> 26 </div>
17</div> \ No newline at end of file 27</div>
diff --git a/client/src/app/shared/buttons/action-dropdown.component.scss b/client/src/app/shared/buttons/action-dropdown.component.scss
index 00f120fb8..985b2ca88 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.scss
+++ b/client/src/app/shared/buttons/action-dropdown.component.scss
@@ -1,9 +1,20 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4.dropdown-divider:last-child {
5 display: none;
6}
7
4.action-button { 8.action-button {
5 @include peertube-button; 9 @include peertube-button;
6 @include grey-button; 10
11 &.grey {
12 @include grey-button;
13 }
14
15 &.orange {
16 @include orange-button;
17 }
7 18
8 display: inline-block; 19 display: inline-block;
9 padding: 0 10px; 20 padding: 0 10px;
@@ -13,14 +24,11 @@
13 } 24 }
14 25
15 &:hover, &:active, &:focus { 26 &:hover, &:active, &:focus {
16 background-color: $grey-color; 27 background-color: $grey-background-color;
17 } 28 }
18 29
19 .icon-action { 30 .more-icon {
20 @include icon(21px); 31 width: 21px;
21
22 background-image: url('../../../assets/images/video/more.svg');
23 top: -1px;
24 } 32 }
25 33
26 &.small { 34 &.small {
@@ -30,9 +38,19 @@
30 } 38 }
31} 39}
32 40
41.dropdown-toggle::after {
42 position: relative;
43 top: 1px;
44}
45
33.dropdown-menu { 46.dropdown-menu {
34 .dropdown-item { 47 .dropdown-item {
35 cursor: pointer; 48 cursor: pointer;
36 color: #000 !important; 49 color: #000 !important;
50
51 a, span {
52 display: block;
53 width: 100%;
54 }
37 } 55 }
38} \ No newline at end of file 56}
diff --git a/client/src/app/shared/buttons/action-dropdown.component.ts b/client/src/app/shared/buttons/action-dropdown.component.ts
index 1838ff697..275e2b51e 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.ts
+++ b/client/src/app/shared/buttons/action-dropdown.component.ts
@@ -2,9 +2,9 @@ import { Component, Input } from '@angular/core'
2 2
3export type DropdownAction<T> = { 3export type DropdownAction<T> = {
4 label?: string 4 label?: string
5 handler?: (T) => any 5 handler?: (a: T) => any
6 linkBuilder?: (T) => (string | number)[] 6 linkBuilder?: (a: T) => (string | number)[]
7 isDisplayed?: (T) => boolean 7 isDisplayed?: (a: T) => boolean
8} 8}
9 9
10@Component({ 10@Component({
@@ -14,8 +14,16 @@ export type DropdownAction<T> = {
14}) 14})
15 15
16export class ActionDropdownComponent<T> { 16export class ActionDropdownComponent<T> {
17 @Input() actions: DropdownAction<T>[] = [] 17 @Input() actions: DropdownAction<T>[] | DropdownAction<T>[][] = []
18 @Input() entry: T 18 @Input() entry: T
19 @Input() placement = 'left' 19 @Input() placement = 'bottom-left'
20 @Input() buttonSize: 'normal' | 'small' = 'normal' 20 @Input() buttonSize: 'normal' | 'small' = 'normal'
21 @Input() label: string
22 @Input() theme: 'orange' | 'grey' = 'grey'
23
24 getActions () {
25 if (this.actions.length !== 0 && Array.isArray(this.actions[0])) return this.actions
26
27 return [ this.actions ]
28 }
21} 29}
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 967cb1409..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',
@@ -8,9 +9,9 @@ import { Component, Input } from '@angular/core'
8 9
9export class ButtonComponent { 10export class ButtonComponent {
10 @Input() label = '' 11 @Input() label = ''
11 @Input() className = undefined 12 @Input() className: string = undefined
12 @Input() icon = undefined 13 @Input() icon: GlobalIconName = undefined
13 @Input() title = undefined 14 @Input() title: string = undefined
14 15
15 getTitle () { 16 getTitle () {
16 return this.title || this.label 17 return this.title || this.label
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/shared/buttons/edit-button.component.ts b/client/src/app/shared/buttons/edit-button.component.ts
index 7abaacc26..1fe4f7b30 100644
--- a/client/src/app/shared/buttons/edit-button.component.ts
+++ b/client/src/app/shared/buttons/edit-button.component.ts
@@ -8,5 +8,5 @@ import { Component, Input } from '@angular/core'
8 8
9export class EditButtonComponent { 9export class EditButtonComponent {
10 @Input() label: string 10 @Input() label: string
11 @Input() routerLink = [] 11 @Input() routerLink: string[] = []
12} 12}
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 1fd1cdf68..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,24 +88,24 @@ 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
101 this.USER_DESCRIPTION = { 101 this.USER_DESCRIPTION = {
102 VALIDATORS: [ 102 VALIDATORS: [
103 Validators.minLength(3), 103 Validators.minLength(3),
104 Validators.maxLength(250) 104 Validators.maxLength(1000)
105 ], 105 ],
106 MESSAGES: { 106 MESSAGES: {
107 'minlength': this.i18n('Description must be at least 3 characters long.'), 107 'minlength': this.i18n('Description must be at least 3 characters long.'),
108 'maxlength': this.i18n('Description cannot be more than 250 characters long.') 108 'maxlength': this.i18n('Description cannot be more than 1000 characters long.')
109 } 109 }
110 } 110 }
111 111
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-change-ownership-validators.service.ts b/client/src/app/shared/forms/form-validators/video-change-ownership-validators.service.ts
index 087b80b44..c6fbb7538 100644
--- a/client/src/app/shared/forms/form-validators/video-change-ownership-validators.service.ts
+++ b/client/src/app/shared/forms/form-validators/video-change-ownership-validators.service.ts
@@ -1,5 +1,5 @@
1import { I18n } from '@ngx-translate/i18n-polyfill' 1import { I18n } from '@ngx-translate/i18n-polyfill'
2import { Validators } from '@angular/forms' 2import { AbstractControl, ValidationErrors, Validators } from '@angular/forms'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { BuildFormValidator } from '@app/shared' 4import { BuildFormValidator } from '@app/shared'
5 5
@@ -9,10 +9,19 @@ export class VideoChangeOwnershipValidatorsService {
9 9
10 constructor (private i18n: I18n) { 10 constructor (private i18n: I18n) {
11 this.USERNAME = { 11 this.USERNAME = {
12 VALIDATORS: [ Validators.required ], 12 VALIDATORS: [ Validators.required, this.localAccountValidator ],
13 MESSAGES: { 13 MESSAGES: {
14 'required': this.i18n('The username is required.') 14 'required': this.i18n('The username is required.'),
15 'localAccountOnly': this.i18n('You can only transfer ownership to a local account')
15 } 16 }
16 } 17 }
17 } 18 }
19
20 localAccountValidator (control: AbstractControl): ValidationErrors {
21 if (control.value.includes('@')) {
22 return { 'localAccountOnly': true }
23 }
24
25 return null
26 }
18} 27}
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 1ce3a0dca..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,50 +14,50 @@ 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
42 this.VIDEO_CHANNEL_DESCRIPTION = { 42 this.VIDEO_CHANNEL_DESCRIPTION = {
43 VALIDATORS: [ 43 VALIDATORS: [
44 Validators.minLength(3), 44 Validators.minLength(3),
45 Validators.maxLength(500) 45 Validators.maxLength(1000)
46 ], 46 ],
47 MESSAGES: { 47 MESSAGES: {
48 'minlength': i18n('Description must be at least 3 characters long.'), 48 'minlength': i18n('Description must be at least 3 characters long.'),
49 'maxlength': i18n('Description cannot be more than 500 characters long.') 49 'maxlength': i18n('Description cannot be more than 1000 characters long.')
50 } 50 }
51 } 51 }
52 52
53 this.VIDEO_CHANNEL_SUPPORT = { 53 this.VIDEO_CHANNEL_SUPPORT = {
54 VALIDATORS: [ 54 VALIDATORS: [
55 Validators.minLength(3), 55 Validators.minLength(3),
56 Validators.maxLength(500) 56 Validators.maxLength(1000)
57 ], 57 ],
58 MESSAGES: { 58 MESSAGES: {
59 'minlength': i18n('Support text must be at least 3 characters long.'), 59 'minlength': i18n('Support text must be at least 3 characters long.'),
60 'maxlength': i18n('Support text cannot be more than 500 characters long.') 60 'maxlength': i18n('Support text cannot be more than 1000 characters long.')
61 } 61 }
62 } 62 }
63 } 63 }
diff --git a/client/src/app/shared/forms/form-validators/video-validators.service.ts b/client/src/app/shared/forms/form-validators/video-validators.service.ts
index 396be6f3b..81ed0666f 100644
--- a/client/src/app/shared/forms/form-validators/video-validators.service.ts
+++ b/client/src/app/shared/forms/form-validators/video-validators.service.ts
@@ -79,10 +79,10 @@ export class VideoValidatorsService {
79 } 79 }
80 80
81 this.VIDEO_SUPPORT = { 81 this.VIDEO_SUPPORT = {
82 VALIDATORS: [ Validators.minLength(3), Validators.maxLength(500) ], 82 VALIDATORS: [ Validators.minLength(3), Validators.maxLength(1000) ],
83 MESSAGES: { 83 MESSAGES: {
84 'minlength': this.i18n('Video support must be at least 3 characters long.'), 84 'minlength': this.i18n('Video support must be at least 3 characters long.'),
85 'maxlength': this.i18n('Video support cannot be more than 500 characters long.') 85 'maxlength': this.i18n('Video support cannot be more than 1000 characters long.')
86 } 86 }
87 } 87 }
88 88
diff --git a/client/src/app/shared/forms/index.ts b/client/src/app/shared/forms/index.ts
index 41c321c4c..8febbfee9 100644
--- a/client/src/app/shared/forms/index.ts
+++ b/client/src/app/shared/forms/index.ts
@@ -1,3 +1,4 @@
1export * from './form-validators' 1export * from './form-validators'
2export * from './form-reactive' 2export * from './form-reactive'
3export * from './reactive-file.component' 3export * from './reactive-file.component'
4export * from './textarea-autoresize.directive'
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 38691f050..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="form-group"> 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.scss b/client/src/app/shared/forms/peertube-checkbox.component.scss
index ee133f190..6e4e20775 100644
--- a/client/src/app/shared/forms/peertube-checkbox.component.scss
+++ b/client/src/app/shared/forms/peertube-checkbox.component.scss
@@ -1,7 +1,7 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4.form-group { 4.root {
5 display: flex; 5 display: flex;
6 6
7 .form-group-checkbox { 7 .form-group-checkbox {
@@ -20,6 +20,10 @@
20 } 20 }
21 } 21 }
22 22
23 label {
24 margin-bottom: 0;
25 }
26
23 my-help { 27 my-help {
24 position: relative; 28 position: relative;
25 top: -2px; 29 top: -2px;
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/forms/textarea-autoresize.directive.ts b/client/src/app/shared/forms/textarea-autoresize.directive.ts
new file mode 100644
index 000000000..f8c855c16
--- /dev/null
+++ b/client/src/app/shared/forms/textarea-autoresize.directive.ts
@@ -0,0 +1,25 @@
1// Thanks: https://github.com/evseevdev/ngx-textarea-autosize
2import { AfterViewInit, Directive, ElementRef, HostBinding, HostListener } from '@angular/core'
3
4@Directive({
5 selector: 'textarea[myAutoResize]'
6})
7export class TextareaAutoResizeDirective implements AfterViewInit {
8 @HostBinding('attr.rows') rows = '1'
9 @HostBinding('style.overflow') overflow = 'hidden'
10
11 constructor (private elem: ElementRef) { }
12
13 public ngAfterViewInit () {
14 this.resize()
15 }
16
17 @HostListener('input')
18 resize () {
19 const textarea = this.elem.nativeElement as HTMLTextAreaElement
20 // Reset textarea height to auto that correctly calculate the new height
21 textarea.style.height = 'auto'
22 // Set new height
23 textarea.style.height = `${textarea.scrollHeight}px`
24 }
25}
diff --git a/client/src/app/shared/guards/can-deactivate-guard.service.ts b/client/src/app/shared/guards/can-deactivate-guard.service.ts
index e2a79e8c4..3a35fcfb3 100644
--- a/client/src/app/shared/guards/can-deactivate-guard.service.ts
+++ b/client/src/app/shared/guards/can-deactivate-guard.service.ts
@@ -4,8 +4,10 @@ import { Observable } from 'rxjs'
4import { ConfirmService } from '../../core/index' 4import { ConfirmService } from '../../core/index'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
6 6
7export type CanComponentDeactivateResult = { text?: string, canDeactivate: Observable<boolean> | boolean }
8
7export interface CanComponentDeactivate { 9export interface CanComponentDeactivate {
8 canDeactivate: () => { text?: string, canDeactivate: Observable<boolean> | boolean } 10 canDeactivate: () => CanComponentDeactivateResult
9} 11}
10 12
11@Injectable() 13@Injectable()
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/from-now.pipe.ts b/client/src/app/shared/misc/from-now.pipe.ts
index 33e6d25fe..00b5be6c9 100644
--- a/client/src/app/shared/misc/from-now.pipe.ts
+++ b/client/src/app/shared/misc/from-now.pipe.ts
@@ -7,8 +7,9 @@ export class FromNowPipe implements PipeTransform {
7 7
8 constructor (private i18n: I18n) { } 8 constructor (private i18n: I18n) { }
9 9
10 transform (value: number) { 10 transform (arg: number | Date | string) {
11 const seconds = Math.floor((Date.now() - value) / 1000) 11 const argDate = new Date(arg)
12 const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000)
12 13
13 let interval = Math.floor(seconds / 31536000) 14 let interval = Math.floor(seconds / 31536000)
14 if (interval > 1) { 15 if (interval > 1) {
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/peertube-local-storage.ts b/client/src/app/shared/misc/peertube-local-storage.ts
index 260f994b6..fb5c45acf 100644
--- a/client/src/app/shared/misc/peertube-local-storage.ts
+++ b/client/src/app/shared/misc/peertube-local-storage.ts
@@ -6,7 +6,7 @@ class MemoryStorage {
6 [key: string]: any 6 [key: string]: any
7 [index: number]: string 7 [index: number]: string
8 8
9 getItem (key) { 9 getItem (key: any) {
10 const stringKey = String(key) 10 const stringKey = String(key)
11 if (valuesMap.has(key)) { 11 if (valuesMap.has(key)) {
12 return String(valuesMap.get(stringKey)) 12 return String(valuesMap.get(stringKey))
@@ -15,11 +15,11 @@ class MemoryStorage {
15 return null 15 return null
16 } 16 }
17 17
18 setItem (key, val) { 18 setItem (key: any, val: any) {
19 valuesMap.set(String(key), String(val)) 19 valuesMap.set(String(key), String(val))
20 } 20 }
21 21
22 removeItem (key) { 22 removeItem (key: any) {
23 valuesMap.delete(key) 23 valuesMap.delete(key)
24 } 24 }
25 25
diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts
index c8b7ebc67..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: object, 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)
@@ -124,9 +130,14 @@ function sortBy (obj: any[], key1: string, key2?: string) {
124 }) 130 })
125} 131}
126 132
133function scrollToTop () {
134 window.scroll(0, 0)
135}
136
127export { 137export {
128 sortBy, 138 sortBy,
129 durationToString, 139 durationToString,
140 lineFeedToHtml,
130 objectToUrlEncoded, 141 objectToUrlEncoded,
131 getParameterByName, 142 getParameterByName,
132 populateAsyncUserVideoChannels, 143 populateAsyncUserVideoChannels,
@@ -134,6 +145,7 @@ export {
134 dateToHuman, 145 dateToHuman,
135 immutableAssign, 146 immutableAssign,
136 objectToFormData, 147 objectToFormData,
137 lineFeedToHtml, 148 objectLineFeedToHtml,
138 removeElementFromArray 149 removeElementFromArray,
150 scrollToTop
139} 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 b2958caa4..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 {{ userToBan.username }}</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 67ae38e48..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'
@@ -15,15 +15,15 @@ import { User } from '../../../../../shared'
15}) 15})
16export class UserBanModalComponent extends FormReactive implements OnInit { 16export class UserBanModalComponent extends FormReactive implements OnInit {
17 @ViewChild('modal') modal: NgbModal 17 @ViewChild('modal') modal: NgbModal
18 @Output() userBanned = new EventEmitter<User>() 18 @Output() userBanned = new EventEmitter<User | User[]>()
19 19
20 private userToBan: User 20 private usersToBan: User | User[]
21 private openedModal: NgbModalRef 21 private openedModal: NgbModalRef
22 22
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
@@ -37,32 +37,33 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
37 }) 37 })
38 } 38 }
39 39
40 openModal (user: User) { 40 openModal (user: User | User[]) {
41 this.userToBan = user 41 this.usersToBan = user
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.userToBan = undefined 46 this.usersToBan = undefined
47 this.openedModal.close() 47 this.openedModal.close()
48 } 48 }
49 49
50 async banUser () { 50 async banUser () {
51 const reason = this.form.value['reason'] || undefined 51 const reason = this.form.value['reason'] || undefined
52 52
53 this.userService.banUser(this.userToBan, reason) 53 this.userService.banUsers(this.usersToBan, reason)
54 .subscribe( 54 .subscribe(
55 () => { 55 () => {
56 this.notificationsService.success( 56 const message = Array.isArray(this.usersToBan)
57 this.i18n('Success'), 57 ? this.i18n('{{num}} users banned.', { num: this.usersToBan.length })
58 this.i18n('User {{username}} banned.', { username: this.userToBan.username }) 58 : this.i18n('User {{username}} banned.', { username: this.usersToBan.username })
59 )
60 59
61 this.userBanned.emit(this.userToBan) 60 this.notifier.success(message)
62 this.hideBanUserModal() 61
62 this.userBanned.emit(this.usersToBan)
63 this.hide()
63 }, 64 },
64 65
65 err => this.notificationsService.error(this.i18n('Error'), err.message) 66 err => this.notifier.error(err.message)
66 ) 67 )
67 } 68 }
68 69
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.html b/client/src/app/shared/moderation/user-moderation-dropdown.component.html
index ed1a4c863..7367a7e59 100644
--- a/client/src/app/shared/moderation/user-moderation-dropdown.component.html
+++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.html
@@ -1,5 +1,8 @@
1<ng-container *ngIf="user && userActions.length !== 0"> 1<ng-container *ngIf="userActions.length !== 0">
2 <my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal> 2 <my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal>
3 3
4 <my-action-dropdown i18n-label label="Actions" [actions]="userActions" [entry]="user" [buttonSize]="buttonSize"></my-action-dropdown> 4 <my-action-dropdown
5 [actions]="userActions" [entry]="{ user: user, account: account }"
6 [buttonSize]="buttonSize" [placement]="placement"
7 ></my-action-dropdown>
5</ng-container> \ No newline at end of file 8</ng-container> \ No newline at end of file
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 4f88456de..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,50 +1,53 @@
1import { Component, EventEmitter, Input, OnInit, 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 { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
6import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component' 4import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component'
7import { UserService } from '@app/shared/users' 5import { UserService } from '@app/shared/users'
8import { AuthService, ConfirmService } from '@app/core' 6import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core'
9import { User, UserRight } from '../../../../../shared/models/users' 7import { User, UserRight } from '../../../../../shared/models/users'
8import { Account } from '@app/shared/account/account.model'
9import { BlocklistService } from '@app/shared/blocklist'
10 10
11@Component({ 11@Component({
12 selector: 'my-user-moderation-dropdown', 12 selector: 'my-user-moderation-dropdown',
13 templateUrl: './user-moderation-dropdown.component.html', 13 templateUrl: './user-moderation-dropdown.component.html',
14 styleUrls: [ './user-moderation-dropdown.component.scss' ] 14 styleUrls: [ './user-moderation-dropdown.component.scss' ]
15}) 15})
16export class UserModerationDropdownComponent implements OnInit { 16export class UserModerationDropdownComponent implements OnChanges {
17 @ViewChild('userBanModal') userBanModal: UserBanModalComponent 17 @ViewChild('userBanModal') userBanModal: UserBanModalComponent
18 18
19 @Input() user: User 19 @Input() user: User
20 @Input() account: Account
21
20 @Input() buttonSize: 'normal' | 'small' = 'normal' 22 @Input() buttonSize: 'normal' | 'small' = 'normal'
23 @Input() placement = 'left'
21 24
22 @Output() userChanged = new EventEmitter() 25 @Output() userChanged = new EventEmitter()
23 @Output() userDeleted = new EventEmitter() 26 @Output() userDeleted = new EventEmitter()
24 27
25 userActions: DropdownAction<User>[] = [] 28 userActions: DropdownAction<{ user: User, account: Account }>[][] = []
26
27 private openedModal: NgbModalRef
28 29
29 constructor ( 30 constructor (
30 private authService: AuthService, 31 private authService: AuthService,
31 private notificationsService: NotificationsService, 32 private notifier: Notifier,
32 private confirmService: ConfirmService, 33 private confirmService: ConfirmService,
34 private serverService: ServerService,
33 private userService: UserService, 35 private userService: UserService,
36 private blocklistService: BlocklistService,
34 private i18n: I18n 37 private i18n: I18n
35 ) { } 38 ) { }
36 39
37 ngOnInit () { 40 get requiresEmailVerification () {
38 this.buildActions() 41 return this.serverService.getConfig().signup.requiresEmailVerification
39 } 42 }
40 43
41 hideBanUserModal () { 44 ngOnChanges () {
42 this.openedModal.close() 45 this.buildActions()
43 } 46 }
44 47
45 openBanUserModal (user: User) { 48 openBanUserModal (user: User) {
46 if (user.username === 'root') { 49 if (user.username === 'root') {
47 this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot ban root.')) 50 this.notifier.error(this.i18n('You cannot ban root.'))
48 return 51 return
49 } 52 }
50 53
@@ -60,24 +63,21 @@ export class UserModerationDropdownComponent implements OnInit {
60 const res = await this.confirmService.confirm(message, this.i18n('Unban')) 63 const res = await this.confirmService.confirm(message, this.i18n('Unban'))
61 if (res === false) return 64 if (res === false) return
62 65
63 this.userService.unbanUser(user) 66 this.userService.unbanUsers(user)
64 .subscribe( 67 .subscribe(
65 () => { 68 () => {
66 this.notificationsService.success( 69 this.notifier.success(this.i18n('User {{username}} unbanned.', { username: user.username }))
67 this.i18n('Success'),
68 this.i18n('User {{username}} unbanned.', { username: user.username })
69 )
70 70
71 this.userChanged.emit() 71 this.userChanged.emit()
72 }, 72 },
73 73
74 err => this.notificationsService.error(this.i18n('Error'), err.message) 74 err => this.notifier.error(err.message)
75 ) 75 )
76 } 76 }
77 77
78 async removeUser (user: User) { 78 async removeUser (user: User) {
79 if (user.username === 'root') { 79 if (user.username === 'root') {
80 this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot delete root.')) 80 this.notifier.error(this.i18n('You cannot delete root.'))
81 return 81 return
82 } 82 }
83 83
@@ -87,17 +87,138 @@ export class UserModerationDropdownComponent implements OnInit {
87 87
88 this.userService.removeUser(user).subscribe( 88 this.userService.removeUser(user).subscribe(
89 () => { 89 () => {
90 this.notificationsService.success( 90 this.notifier.success(this.i18n('User {{username}} deleted.', { username: user.username }))
91 this.i18n('Success'),
92 this.i18n('User {{username}} deleted.', { username: user.username })
93 )
94 this.userDeleted.emit() 91 this.userDeleted.emit()
95 }, 92 },
96 93
97 err => this.notificationsService.error(this.i18n('Error'), err.message) 94 err => this.notifier.error(err.message)
98 ) 95 )
99 } 96 }
100 97
98 setEmailAsVerified (user: User) {
99 this.userService.updateUser(user.id, { emailVerified: true }).subscribe(
100 () => {
101 this.notifier.success(this.i18n('User {{username}} email set as verified', { username: user.username }))
102
103 this.userChanged.emit()
104 },
105
106 err => this.notifier.error(err.message)
107 )
108 }
109
110 blockAccountByUser (account: Account) {
111 this.blocklistService.blockAccountByUser(account)
112 .subscribe(
113 () => {
114 this.notifier.success(this.i18n('Account {{nameWithHost}} muted.', { nameWithHost: account.nameWithHost }))
115
116 this.account.mutedByUser = true
117 this.userChanged.emit()
118 },
119
120 err => this.notifier.error(err.message)
121 )
122 }
123
124 unblockAccountByUser (account: Account) {
125 this.blocklistService.unblockAccountByUser(account)
126 .subscribe(
127 () => {
128 this.notifier.success(this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: account.nameWithHost }))
129
130 this.account.mutedByUser = false
131 this.userChanged.emit()
132 },
133
134 err => this.notifier.error(err.message)
135 )
136 }
137
138 blockServerByUser (host: string) {
139 this.blocklistService.blockServerByUser(host)
140 .subscribe(
141 () => {
142 this.notifier.success(this.i18n('Instance {{host}} muted.', { host }))
143
144 this.account.mutedServerByUser = true
145 this.userChanged.emit()
146 },
147
148 err => this.notifier.error(err.message)
149 )
150 }
151
152 unblockServerByUser (host: string) {
153 this.blocklistService.unblockServerByUser(host)
154 .subscribe(
155 () => {
156 this.notifier.success(this.i18n('Instance {{host}} unmuted.', { host }))
157
158 this.account.mutedServerByUser = false
159 this.userChanged.emit()
160 },
161
162 err => this.notifier.error(err.message)
163 )
164 }
165
166 blockAccountByInstance (account: Account) {
167 this.blocklistService.blockAccountByInstance(account)
168 .subscribe(
169 () => {
170 this.notifier.success(this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost }))
171
172 this.account.mutedByInstance = true
173 this.userChanged.emit()
174 },
175
176 err => this.notifier.error(err.message)
177 )
178 }
179
180 unblockAccountByInstance (account: Account) {
181 this.blocklistService.unblockAccountByInstance(account)
182 .subscribe(
183 () => {
184 this.notifier.success(this.i18n('Account {{nameWithHost}} unmuted by the instance.', { nameWithHost: account.nameWithHost }))
185
186 this.account.mutedByInstance = false
187 this.userChanged.emit()
188 },
189
190 err => this.notifier.error(err.message)
191 )
192 }
193
194 blockServerByInstance (host: string) {
195 this.blocklistService.blockServerByInstance(host)
196 .subscribe(
197 () => {
198 this.notifier.success(this.i18n('Instance {{host}} muted by the instance.', { host }))
199
200 this.account.mutedServerByInstance = true
201 this.userChanged.emit()
202 },
203
204 err => this.notifier.error(err.message)
205 )
206 }
207
208 unblockServerByInstance (host: string) {
209 this.blocklistService.unblockServerByInstance(host)
210 .subscribe(
211 () => {
212 this.notifier.success(this.i18n('Instance {{host}} unmuted by the instance.', { host }))
213
214 this.account.mutedServerByInstance = false
215 this.userChanged.emit()
216 },
217
218 err => this.notifier.error(err.message)
219 )
220 }
221
101 getRouterUserEditLink (user: User) { 222 getRouterUserEditLink (user: User) {
102 return [ '/admin', 'users', 'update', user.id ] 223 return [ '/admin', 'users', 'update', user.id ]
103 } 224 }
@@ -108,28 +229,100 @@ export class UserModerationDropdownComponent implements OnInit {
108 if (this.authService.isLoggedIn()) { 229 if (this.authService.isLoggedIn()) {
109 const authUser = this.authService.getUser() 230 const authUser = this.authService.getUser()
110 231
111 if (authUser.hasRight(UserRight.MANAGE_USERS)) { 232 if (this.user && authUser.id === this.user.id) return
112 this.userActions = this.userActions.concat([ 233
234 if (this.user && authUser.hasRight(UserRight.MANAGE_USERS)) {
235 this.userActions.push([
113 { 236 {
114 label: this.i18n('Edit'), 237 label: this.i18n('Edit'),
115 linkBuilder: this.getRouterUserEditLink 238 linkBuilder: ({ user }) => this.getRouterUserEditLink(user)
116 }, 239 },
117 { 240 {
118 label: this.i18n('Delete'), 241 label: this.i18n('Delete'),
119 handler: user => this.removeUser(user) 242 handler: ({ user }) => this.removeUser(user)
120 }, 243 },
121 { 244 {
122 label: this.i18n('Ban'), 245 label: this.i18n('Ban'),
123 handler: user => this.openBanUserModal(user), 246 handler: ({ user }) => this.openBanUserModal(user),
124 isDisplayed: user => !user.blocked 247 isDisplayed: ({ user }) => !user.blocked
125 }, 248 },
126 { 249 {
127 label: this.i18n('Unban'), 250 label: this.i18n('Unban'),
128 handler: user => this.unbanUser(user), 251 handler: ({ user }) => this.unbanUser(user),
129 isDisplayed: user => user.blocked 252 isDisplayed: ({ user }) => user.blocked
253 },
254 {
255 label: this.i18n('Set Email as Verified'),
256 handler: ({ user }) => this.setEmailAsVerified(user),
257 isDisplayed: ({ user }) => this.requiresEmailVerification && !user.blocked && user.emailVerified === false
130 } 258 }
131 ]) 259 ])
132 } 260 }
261
262 // Actions on accounts/servers
263 if (this.account) {
264 // User actions
265 this.userActions.push([
266 {
267 label: this.i18n('Mute this account'),
268 isDisplayed: ({ account }) => account.mutedByUser === false,
269 handler: ({ account }) => this.blockAccountByUser(account)
270 },
271 {
272 label: this.i18n('Unmute this account'),
273 isDisplayed: ({ account }) => account.mutedByUser === true,
274 handler: ({ account }) => this.unblockAccountByUser(account)
275 },
276 {
277 label: this.i18n('Mute the instance'),
278 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false,
279 handler: ({ account }) => this.blockServerByUser(account.host)
280 },
281 {
282 label: this.i18n('Unmute the instance'),
283 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true,
284 handler: ({ account }) => this.unblockServerByUser(account.host)
285 }
286 ])
287
288 let instanceActions: DropdownAction<{ user: User, account: Account }>[] = []
289
290 // Instance actions
291 if (authUser.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)) {
292 instanceActions = instanceActions.concat([
293 {
294 label: this.i18n('Mute this account by your instance'),
295 isDisplayed: ({ account }) => account.mutedByInstance === false,
296 handler: ({ account }) => this.blockAccountByInstance(account)
297 },
298 {
299 label: this.i18n('Unmute this account by your instance'),
300 isDisplayed: ({ account }) => account.mutedByInstance === true,
301 handler: ({ account }) => this.unblockAccountByInstance(account)
302 }
303 ])
304 }
305
306 // Instance actions
307 if (authUser.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) {
308 instanceActions = instanceActions.concat([
309 {
310 label: this.i18n('Mute the instance by your instance'),
311 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false,
312 handler: ({ account }) => this.blockServerByInstance(account.host)
313 },
314 {
315 label: this.i18n('Unmute the instance by your instance'),
316 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true,
317 handler: ({ account }) => this.unblockServerByInstance(account.host)
318 }
319 ])
320 }
321
322 if (instanceActions.length !== 0) {
323 this.userActions.push(instanceActions)
324 }
325 }
133 } 326 }
134 } 327 }
135} 328}
diff --git a/client/src/app/shared/overview/videos-overview.model.ts b/client/src/app/shared/overview/videos-overview.model.ts
index cf02bdb3d..c8eafc8e8 100644
--- a/client/src/app/shared/overview/videos-overview.model.ts
+++ b/client/src/app/shared/overview/videos-overview.model.ts
@@ -16,4 +16,5 @@ export class VideosOverview implements VideosOverviewServer {
16 tag: string 16 tag: string
17 videos: Video[] 17 videos: Video[]
18 }[] 18 }[]
19 [key: string]: any
19} 20}
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 3f4072efd..2529c9eaf 100644
--- a/client/src/app/videos/+video-watch/comment/linkifier.service.ts
+++ b/client/src/app/shared/renderer/linkifier.service.ts
@@ -1,7 +1,8 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' 2import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
3import * as linkify from 'linkifyjs' 3// FIXME: use @types/linkify when https://github.com/DefinitelyTyped/DefinitelyTyped/pull/29682/files is merged?
4import * as linkifyHtml from 'linkifyjs/html' 4const linkify = require('linkifyjs')
5const linkifyHtml = require('linkifyjs/html')
5 6
6@Injectable() 7@Injectable()
7export class LinkifierService { 8export class LinkifierService {
@@ -40,7 +41,7 @@ export class LinkifierService {
40 const TT_UNDERSCORE = TT.UNDERSCORE 41 const TT_UNDERSCORE = TT.UNDERSCORE
41 const TT_DOT = TT.DOT 42 const TT_DOT = TT.DOT
42 43
43 function MENTION (value) { 44 function MENTION (this: any, value: any) {
44 this.v = value 45 this.v = value
45 } 46 }
46 47
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 6492aa66d..e6518dd1d 100644
--- a/client/src/app/shared/rest/rest-extractor.service.ts
+++ b/client/src/app/shared/rest/rest-extractor.service.ts
@@ -33,7 +33,7 @@ export class RestExtractor {
33 return this.applyToResultListData(result, this.convertDateToHuman, [ fieldsToConvert ]) 33 return this.applyToResultListData(result, this.convertDateToHuman, [ fieldsToConvert ])
34 } 34 }
35 35
36 convertDateToHuman (target: object, fieldsToConvert: string[]) { 36 convertDateToHuman (target: { [ id: string ]: string }, fieldsToConvert: string[]) {
37 fieldsToConvert.forEach(field => target[field] = dateToHuman(target[field])) 37 fieldsToConvert.forEach(field => target[field] = dateToHuman(target[field]))
38 38
39 return target 39 return target
@@ -80,10 +80,11 @@ 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
86 const errorObj = { 87 const errorObj: { message: string, status: string, body: string } = {
87 message: errorMessage, 88 message: errorMessage,
88 status: undefined, 89 status: undefined,
89 body: undefined 90 body: undefined
diff --git a/client/src/app/shared/rest/rest-table.ts b/client/src/app/shared/rest/rest-table.ts
index fe1a91d2d..884588207 100644
--- a/client/src/app/shared/rest/rest-table.ts
+++ b/client/src/app/shared/rest/rest-table.ts
@@ -1,8 +1,9 @@
1import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' 1import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
2import { LazyLoadEvent } from 'primeng/components/common/lazyloadevent' 2import { LazyLoadEvent } from 'primeng/components/common/lazyloadevent'
3import { SortMeta } from 'primeng/components/common/sortmeta' 3import { SortMeta } from 'primeng/components/common/sortmeta'
4
5import { RestPagination } from './rest-pagination' 4import { RestPagination } from './rest-pagination'
5import { Subject } from 'rxjs'
6import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
6 7
7export abstract class RestTable { 8export abstract class RestTable {
8 9
@@ -11,9 +12,14 @@ export abstract class RestTable {
11 abstract sort: SortMeta 12 abstract sort: SortMeta
12 abstract pagination: RestPagination 13 abstract pagination: RestPagination
13 14
15 protected search: string
16 private searchStream: Subject<string>
14 private sortLocalStorageKey = 'rest-table-sort-' + this.constructor.name 17 private sortLocalStorageKey = 'rest-table-sort-' + this.constructor.name
15 18
16 protected abstract loadData (): void 19 initialize () {
20 this.loadSort()
21 this.initSearch()
22 }
17 23
18 loadSort () { 24 loadSort () {
19 const result = peertubeLocalStorage.getItem(this.sortLocalStorageKey) 25 const result = peertubeLocalStorage.getItem(this.sortLocalStorageKey)
@@ -46,4 +52,23 @@ export abstract class RestTable {
46 peertubeLocalStorage.setItem(this.sortLocalStorageKey, JSON.stringify(this.sort)) 52 peertubeLocalStorage.setItem(this.sortLocalStorageKey, JSON.stringify(this.sort))
47 } 53 }
48 54
55 initSearch () {
56 this.searchStream = new Subject()
57
58 this.searchStream
59 .pipe(
60 debounceTime(400),
61 distinctUntilChanged()
62 )
63 .subscribe(search => {
64 this.search = search
65 this.loadData()
66 })
67 }
68
69 onSearch (search: string) {
70 this.searchStream.next(search)
71 }
72
73 protected abstract loadData (): void
49} 74}
diff --git a/client/src/app/shared/rest/rest.service.ts b/client/src/app/shared/rest/rest.service.ts
index 4560c2024..e6d4e6e5e 100644
--- a/client/src/app/shared/rest/rest.service.ts
+++ b/client/src/app/shared/rest/rest.service.ts
@@ -32,7 +32,7 @@ export class RestService {
32 return newParams 32 return newParams
33 } 33 }
34 34
35 addObjectParams (params: HttpParams, object: object) { 35 addObjectParams (params: HttpParams, object: { [ name: string ]: any }) {
36 for (const name of Object.keys(object)) { 36 for (const name of Object.keys(object)) {
37 const value = object[name] 37 const value = object[name]
38 if (!value) continue 38 if (!value) continue
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index 9647a7966..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'
@@ -25,7 +24,7 @@ import { VideoAbuseService } from './video-abuse'
25import { VideoBlacklistService } from './video-blacklist' 24import { VideoBlacklistService } from './video-blacklist'
26import { VideoOwnershipService } from './video-ownership' 25import { VideoOwnershipService } from './video-ownership'
27import { VideoMiniatureComponent } from './video/video-miniature.component' 26import { VideoMiniatureComponent } from './video/video-miniature.component'
28import { VideoFeedComponent } from './video/video-feed.component' 27import { FeedComponent } from './video/feed.component'
29import { VideoThumbnailComponent } from './video/video-thumbnail.component' 28import { VideoThumbnailComponent } from './video/video-thumbnail.component'
30import { VideoService } from './video/video.service' 29import { VideoService } from './video/video.service'
31import { AccountService } from '@app/shared/account/account.service' 30import { AccountService } from '@app/shared/account/account.service'
@@ -34,16 +33,19 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
34import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 33import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
35import { 34import {
36 CustomConfigValidatorsService, 35 CustomConfigValidatorsService,
36 InstanceValidatorsService,
37 LoginValidatorsService, 37 LoginValidatorsService,
38 ReactiveFileComponent, 38 ReactiveFileComponent,
39 ResetPasswordValidatorsService, 39 ResetPasswordValidatorsService,
40 TextareaAutoResizeDirective,
40 UserValidatorsService, 41 UserValidatorsService,
41 VideoAbuseValidatorsService, 42 VideoAbuseValidatorsService,
43 VideoAcceptOwnershipValidatorsService,
42 VideoBlacklistValidatorsService, 44 VideoBlacklistValidatorsService,
45 VideoChangeOwnershipValidatorsService,
43 VideoChannelValidatorsService, 46 VideoChannelValidatorsService,
44 VideoCommentValidatorsService, 47 VideoCommentValidatorsService,
45 VideoValidatorsService, 48 VideoValidatorsService
46 VideoChangeOwnershipValidatorsService, VideoAcceptOwnershipValidatorsService
47} from '@app/shared/forms' 49} from '@app/shared/forms'
48import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' 50import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
49import { ScreenService } from '@app/shared/misc/screen.service' 51import { ScreenService } from '@app/shared/misc/screen.service'
@@ -53,11 +55,20 @@ import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.c
53import { VideoImportService } from '@app/shared/video-import/video-import.service' 55import { VideoImportService } from '@app/shared/video-import/video-import.service'
54import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component' 56import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component'
55import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' 57import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
56import { SubscribeButtonComponent, RemoteSubscribeComponent, UserSubscriptionService } from '@app/shared/user-subscription' 58import { RemoteSubscribeComponent, SubscribeButtonComponent, UserSubscriptionService } from '@app/shared/user-subscription'
57import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component' 59import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component'
58import { OverviewService } from '@app/shared/overview' 60import { OverviewService } from '@app/shared/overview'
59import { UserBanModalComponent } from '@app/shared/moderation' 61import { UserBanModalComponent } from '@app/shared/moderation'
60import { 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'
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'
61 72
62@NgModule({ 73@NgModule({
63 imports: [ 74 imports: [
@@ -81,7 +92,7 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod
81 LoaderComponent, 92 LoaderComponent,
82 VideoThumbnailComponent, 93 VideoThumbnailComponent,
83 VideoMiniatureComponent, 94 VideoMiniatureComponent,
84 VideoFeedComponent, 95 FeedComponent,
85 ButtonComponent, 96 ButtonComponent,
86 DeleteButtonComponent, 97 DeleteButtonComponent,
87 EditButtonComponent, 98 EditButtonComponent,
@@ -91,6 +102,7 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod
91 FromNowPipe, 102 FromNowPipe,
92 MarkdownTextareaComponent, 103 MarkdownTextareaComponent,
93 InfiniteScrollerDirective, 104 InfiniteScrollerDirective,
105 TextareaAutoResizeDirective,
94 HelpComponent, 106 HelpComponent,
95 ReactiveFileComponent, 107 ReactiveFileComponent,
96 PeertubeCheckboxComponent, 108 PeertubeCheckboxComponent,
@@ -98,7 +110,11 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod
98 RemoteSubscribeComponent, 110 RemoteSubscribeComponent,
99 InstanceFeaturesTableComponent, 111 InstanceFeaturesTableComponent,
100 UserBanModalComponent, 112 UserBanModalComponent,
101 UserModerationDropdownComponent 113 UserModerationDropdownComponent,
114 TopMenuDropdownComponent,
115 UserNotificationsComponent,
116 ConfirmComponent,
117 GlobalIconComponent
102 ], 118 ],
103 119
104 exports: [ 120 exports: [
@@ -121,13 +137,14 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod
121 LoaderComponent, 137 LoaderComponent,
122 VideoThumbnailComponent, 138 VideoThumbnailComponent,
123 VideoMiniatureComponent, 139 VideoMiniatureComponent,
124 VideoFeedComponent, 140 FeedComponent,
125 ButtonComponent, 141 ButtonComponent,
126 DeleteButtonComponent, 142 DeleteButtonComponent,
127 EditButtonComponent, 143 EditButtonComponent,
128 ActionDropdownComponent, 144 ActionDropdownComponent,
129 MarkdownTextareaComponent, 145 MarkdownTextareaComponent,
130 InfiniteScrollerDirective, 146 InfiniteScrollerDirective,
147 TextareaAutoResizeDirective,
131 HelpComponent, 148 HelpComponent,
132 ReactiveFileComponent, 149 ReactiveFileComponent,
133 PeertubeCheckboxComponent, 150 PeertubeCheckboxComponent,
@@ -136,6 +153,10 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod
136 InstanceFeaturesTableComponent, 153 InstanceFeaturesTableComponent,
137 UserBanModalComponent, 154 UserBanModalComponent,
138 UserModerationDropdownComponent, 155 UserModerationDropdownComponent,
156 TopMenuDropdownComponent,
157 UserNotificationsComponent,
158 ConfirmComponent,
159 GlobalIconComponent,
139 160
140 NumberFormatterPipe, 161 NumberFormatterPipe,
141 ObjectLengthPipe, 162 ObjectLengthPipe,
@@ -152,7 +173,6 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod
152 UserService, 173 UserService,
153 VideoService, 174 VideoService,
154 AccountService, 175 AccountService,
155 MarkdownService,
156 VideoChannelService, 176 VideoChannelService,
157 VideoCaptionService, 177 VideoCaptionService,
158 VideoImportService, 178 VideoImportService,
@@ -172,10 +192,20 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod
172 OverviewService, 192 OverviewService,
173 VideoChangeOwnershipValidatorsService, 193 VideoChangeOwnershipValidatorsService,
174 VideoAcceptOwnershipValidatorsService, 194 VideoAcceptOwnershipValidatorsService,
195 InstanceValidatorsService,
196 BlocklistService,
197 UserHistoryService,
198 InstanceService,
199
200 MarkdownService,
201 LinkifierService,
202 HtmlRendererService,
175 203
176 I18nPrimengCalendarService, 204 I18nPrimengCalendarService,
177 ScreenService, 205 ScreenService,
178 206
207 UserNotificationService,
208
179 I18n 209 I18n
180 ] 210 ]
181}) 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 877f1bf3a..c15f1de8c 100644
--- a/client/src/app/shared/users/user.model.ts
+++ b/client/src/app/shared/users/user.model.ts
@@ -1,38 +1,20 @@
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 videoQuota?: number,
19 videoQuotaDaily?: number,
20 nsfwPolicy?: NSFWPolicyType,
21 autoPlayVideo?: boolean,
22 createdAt?: Date,
23 account?: AccountServerModel,
24 videoChannels?: VideoChannel[]
25
26 blocked?: boolean
27 blockedReason?: string
28}
29export class User implements UserServerModel { 6export class User implements UserServerModel {
30 id: number 7 id: number
31 username: string 8 username: string
32 email: string 9 email: string
10 emailVerified: boolean
33 role: UserRole 11 role: UserRole
34 nsfwPolicy: NSFWPolicyType 12 nsfwPolicy: NSFWPolicyType
13
14 webTorrentEnabled: boolean
35 autoPlayVideo: boolean 15 autoPlayVideo: boolean
16 videosHistoryEnabled: boolean
17
36 videoQuota: number 18 videoQuota: number
37 videoQuotaDaily: number 19 videoQuotaDaily: number
38 account: Account 20 account: Account
@@ -42,7 +24,9 @@ export class User implements UserServerModel {
42 blocked: boolean 24 blocked: boolean
43 blockedReason?: string 25 blockedReason?: string
44 26
45 constructor (hash: UserConstructorHash) { 27 notificationSettings?: UserNotificationSetting
28
29 constructor (hash: Partial<UserServerModel>) {
46 this.id = hash.id 30 this.id = hash.id
47 this.username = hash.username 31 this.username = hash.username
48 this.email = hash.email 32 this.email = hash.email
@@ -52,11 +36,15 @@ export class User implements UserServerModel {
52 this.videoQuota = hash.videoQuota 36 this.videoQuota = hash.videoQuota
53 this.videoQuotaDaily = hash.videoQuotaDaily 37 this.videoQuotaDaily = hash.videoQuotaDaily
54 this.nsfwPolicy = hash.nsfwPolicy 38 this.nsfwPolicy = hash.nsfwPolicy
39 this.webTorrentEnabled = hash.webTorrentEnabled
40 this.videosHistoryEnabled = hash.videosHistoryEnabled
55 this.autoPlayVideo = hash.autoPlayVideo 41 this.autoPlayVideo = hash.autoPlayVideo
56 this.createdAt = hash.createdAt 42 this.createdAt = hash.createdAt
57 this.blocked = hash.blocked 43 this.blocked = hash.blocked
58 this.blockedReason = hash.blockedReason 44 this.blockedReason = hash.blockedReason
59 45
46 this.notificationSettings = hash.notificationSettings
47
60 if (hash.account !== undefined) { 48 if (hash.account !== undefined) {
61 this.account = new Account(hash.account) 49 this.account = new Account(hash.account)
62 } 50 }
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts
index d9b81c181..cc5c051f1 100644
--- a/client/src/app/shared/users/user.service.ts
+++ b/client/src/app/shared/users/user.service.ts
@@ -1,5 +1,5 @@
1import { Observable } from 'rxjs' 1import { from, Observable } from 'rxjs'
2import { catchError, map } from 'rxjs/operators' 2import { catchError, concatMap, map, toArray } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http' 3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
5import { ResultList, User, UserCreate, UserRole, UserUpdate, UserUpdateMe, UserVideoQuota } from '../../../../../shared' 5import { ResultList, User, UserCreate, UserRole, UserUpdate, UserUpdateMe, UserVideoQuota } from '../../../../../shared'
@@ -153,15 +153,26 @@ export class UserService {
153 ) 153 )
154 } 154 }
155 155
156 updateUsers (users: User[], userUpdate: UserUpdate) {
157 return from(users)
158 .pipe(
159 concatMap(u => this.authHttp.put(UserService.BASE_USERS_URL + u.id, userUpdate)),
160 toArray(),
161 catchError(err => this.restExtractor.handleError(err))
162 )
163 }
164
156 getUser (userId: number) { 165 getUser (userId: number) {
157 return this.authHttp.get<User>(UserService.BASE_USERS_URL + userId) 166 return this.authHttp.get<User>(UserService.BASE_USERS_URL + userId)
158 .pipe(catchError(err => this.restExtractor.handleError(err))) 167 .pipe(catchError(err => this.restExtractor.handleError(err)))
159 } 168 }
160 169
161 getUsers (pagination: RestPagination, sort: SortMeta): Observable<ResultList<User>> { 170 getUsers (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<User>> {
162 let params = new HttpParams() 171 let params = new HttpParams()
163 params = this.restService.addRestGetParams(params, pagination, sort) 172 params = this.restService.addRestGetParams(params, pagination, sort)
164 173
174 if (search) params = params.append('search', search)
175
165 return this.authHttp.get<ResultList<User>>(UserService.BASE_USERS_URL, { params }) 176 return this.authHttp.get<ResultList<User>>(UserService.BASE_USERS_URL, { params })
166 .pipe( 177 .pipe(
167 map(res => this.restExtractor.convertResultListDateToHuman(res)), 178 map(res => this.restExtractor.convertResultListDateToHuman(res)),
@@ -170,21 +181,38 @@ export class UserService {
170 ) 181 )
171 } 182 }
172 183
173 removeUser (user: { id: number }) { 184 removeUser (usersArg: User | User[]) {
174 return this.authHttp.delete(UserService.BASE_USERS_URL + user.id) 185 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
175 .pipe(catchError(err => this.restExtractor.handleError(err))) 186
187 return from(users)
188 .pipe(
189 concatMap(u => this.authHttp.delete(UserService.BASE_USERS_URL + u.id)),
190 toArray(),
191 catchError(err => this.restExtractor.handleError(err))
192 )
176 } 193 }
177 194
178 banUser (user: { id: number }, reason?: string) { 195 banUsers (usersArg: User | User[], reason?: string) {
179 const body = reason ? { reason } : {} 196 const body = reason ? { reason } : {}
197 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
180 198
181 return this.authHttp.post(UserService.BASE_USERS_URL + user.id + '/block', body) 199 return from(users)
182 .pipe(catchError(err => this.restExtractor.handleError(err))) 200 .pipe(
201 concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/block', body)),
202 toArray(),
203 catchError(err => this.restExtractor.handleError(err))
204 )
183 } 205 }
184 206
185 unbanUser (user: { id: number }) { 207 unbanUsers (usersArg: User | User[]) {
186 return this.authHttp.post(UserService.BASE_USERS_URL + user.id + '/unblock', {}) 208 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
187 .pipe(catchError(err => this.restExtractor.handleError(err))) 209
210 return from(users)
211 .pipe(
212 concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/unblock', {})),
213 toArray(),
214 catchError(err => this.restExtractor.handleError(err))
215 )
188 } 216 }
189 217
190 private formatUser (user: User) { 218 private formatUser (user: User) {
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 d543ab7c1..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,21 @@
1<div [ngClass]="{ 'margin-content': marginContent }"> 1<div [ngClass]="{ 'margin-content': marginContent }">
2 <div *ngIf="titlePage" class="title-page title-page-single"> 2 <div class="videos-header">
3 {{ titlePage }} 3 <div *ngIf="titlePage" class="title-page title-page-single">
4 <div placement="bottom" [ngbTooltip]="titleTooltip" container="body">
5 {{ titlePage }}
6 </div>
7 </div>
8
9 <my-feed [syndicationItems]="syndicationItems"></my-feed>
10
11 <div class="moderation-block" *ngIf="displayModerationBlock">
12 <my-peertube-checkbox
13 (change)="toggleModerationDisplay()"
14 inputName="display-unlisted-private" i18n-labelText labelText="Display unlisted and private videos"
15 >
16 </my-peertube-checkbox>
17 </div>
4 </div> 18 </div>
5 <my-video-feed [syndicationItems]="syndicationItems"></my-video-feed>
6 19
7 <div class="no-results" i18n *ngIf="pagination.totalItems === 0">No results.</div> 20 <div class="no-results" i18n *ngIf="pagination.totalItems === 0">No results.</div>
8 <div 21 <div
diff --git a/client/src/app/shared/video/abstract-video-list.scss b/client/src/app/shared/video/abstract-video-list.scss
index 3f9c73a29..292ede698 100644
--- a/client/src/app/shared/video/abstract-video-list.scss
+++ b/client/src/app/shared/video/abstract-video-list.scss
@@ -8,12 +8,27 @@
8 } 8 }
9} 9}
10 10
11.title-page.title-page-single { 11.videos-header {
12 margin-right: 5px; 12 display: flex;
13} 13 height: 80px;
14 align-items: center;
15
16 .title-page.title-page-single {
17 margin: 0 5px 0 0;
18 }
14 19
15my-video-feed { 20 my-feed {
16 display: inline-block; 21 display: inline-block;
22 top: 1px;
23 min-width: 60px;
24 }
25
26 .moderation-block {
27 display: flex;
28 flex-grow: 1;
29 justify-content: flex-end;
30 align-items: center;
31 }
17} 32}
18 33
19@media screen and (max-width: 500px) { 34@media screen and (max-width: 500px) {
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts
index 763791165..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'
@@ -12,6 +11,8 @@ import { Video } from './video.model'
12import { I18n } from '@ngx-translate/i18n-polyfill' 11import { 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'
14import { Syndication } from '@app/shared/video/syndication.model'
15import { Notifier } from '@app/core'
15 16
16export abstract class AbstractVideoList implements OnInit, OnDestroy { 17export abstract class AbstractVideoList implements OnInit, OnDestroy {
17 private static LINES_PER_PAGE = 4 18 private static LINES_PER_PAGE = 4
@@ -27,7 +28,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
27 sort: VideoSortField = '-publishedAt' 28 sort: VideoSortField = '-publishedAt'
28 categoryOneOf?: number 29 categoryOneOf?: number
29 defaultSort: VideoSortField = '-publishedAt' 30 defaultSort: VideoSortField = '-publishedAt'
30 syndicationItems = [] 31 syndicationItems: Syndication[] = []
31 32
32 loadOnInit = true 33 loadOnInit = true
33 marginContent = true 34 marginContent = true
@@ -37,11 +38,13 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
37 videoPages: Video[][] = [] 38 videoPages: Video[][] = []
38 ownerDisplayType: OwnerDisplayType = 'account' 39 ownerDisplayType: OwnerDisplayType = 'account'
39 firstLoadedPage: number 40 firstLoadedPage: number
41 displayModerationBlock = false
42 titleTooltip: string
40 43
41 protected baseVideoWidth = 215 44 protected baseVideoWidth = 215
42 protected baseVideoHeight = 205 45 protected baseVideoHeight = 205
43 46
44 protected abstract notificationsService: NotificationsService 47 protected abstract notifier: Notifier
45 protected abstract authService: AuthService 48 protected abstract authService: AuthService
46 protected abstract router: Router 49 protected abstract router: Router
47 protected abstract route: ActivatedRoute 50 protected abstract route: ActivatedRoute
@@ -58,7 +61,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
58 private resizeSubscription: Subscription 61 private resizeSubscription: Subscription
59 62
60 abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}> 63 abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}>
61 abstract generateSyndicationList () 64 abstract generateSyndicationList (): void
62 65
63 get user () { 66 get user () {
64 return this.authService.getUser() 67 return this.authService.getUser()
@@ -155,11 +158,15 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
155 }, 158 },
156 error => { 159 error => {
157 this.loadingPage[page] = false 160 this.loadingPage[page] = false
158 this.notificationsService.error(this.i18n('Error'), error.message) 161 this.notifier.error(error.message)
159 } 162 }
160 ) 163 )
161 } 164 }
162 165
166 toggleModerationDisplay () {
167 throw new Error('toggleModerationDisplay is not implemented')
168 }
169
163 protected hasMoreVideos () { 170 protected hasMoreVideos () {
164 // No results 171 // No results
165 if (this.pagination.totalItems === 0) return false 172 if (this.pagination.totalItems === 0) return false
@@ -206,7 +213,9 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
206 protected setNewRouteParams () { 213 protected setNewRouteParams () {
207 const paramsObject = this.buildRouteParams() 214 const paramsObject = this.buildRouteParams()
208 215
209 const queryParams = Object.keys(paramsObject).map(p => p + '=' + paramsObject[p]).join('&') 216 const queryParams = Object.keys(paramsObject)
217 .map(p => p + '=' + paramsObject[p])
218 .join('&')
210 this.location.replaceState(this.currentRoute, queryParams) 219 this.location.replaceState(this.currentRoute, queryParams)
211 } 220 }
212 221
diff --git a/client/src/app/shared/video/video-feed.component.html b/client/src/app/shared/video/feed.component.html
index 16116ba88..f7624ec01 100644
--- a/client/src/app/shared/video/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
new file mode 100644
index 000000000..ed1dc17d3
--- /dev/null
+++ b/client/src/app/shared/video/feed.component.scss
@@ -0,0 +1,18 @@
1@import '_variables';
2@import '_mixins';
3
4.video-feed {
5 a {
6 color: black;
7 display: block;
8 }
9
10 my-global-icon {
11 cursor: pointer;
12 width: 12px;
13 position: relative;
14 top: -2px;
15
16 @include apply-svg-color(var(--mainForegroundColor))
17 }
18}
diff --git a/client/src/app/shared/video/feed.component.ts b/client/src/app/shared/video/feed.component.ts
new file mode 100644
index 000000000..12507458f
--- /dev/null
+++ b/client/src/app/shared/video/feed.component.ts
@@ -0,0 +1,11 @@
1import { Component, Input } from '@angular/core'
2import { Syndication } from '@app/shared/video/syndication.model'
3
4@Component({
5 selector: 'my-feed',
6 styleUrls: [ './feed.component.scss' ],
7 templateUrl: './feed.component.html'
8})
9export class FeedComponent {
10 @Input() syndicationItems: Syndication[]
11}
diff --git a/client/src/app/shared/video/syndication.model.ts b/client/src/app/shared/video/syndication.model.ts
new file mode 100644
index 000000000..c59ab01e8
--- /dev/null
+++ b/client/src/app/shared/video/syndication.model.ts
@@ -0,0 +1,7 @@
1import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum'
2
3export interface Syndication {
4 format: FeedFormat,
5 label: string,
6 url: string
7}
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts
index 5ff3926c4..388357343 100644
--- a/client/src/app/shared/video/video-details.model.ts
+++ b/client/src/app/shared/video/video-details.model.ts
@@ -3,6 +3,8 @@ import { AuthUser } from '../../core'
3import { Video } from '../../shared/video/video.model' 3import { Video } from '../../shared/video/video.model'
4import { Account } from '@app/shared/account/account.model' 4import { Account } from '@app/shared/account/account.model'
5import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 5import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
6import { VideoStreamingPlaylist } from '../../../../../shared/models/videos/video-streaming-playlist.model'
7import { VideoStreamingPlaylistType } from '../../../../../shared/models/videos/video-streaming-playlist.type'
6 8
7export class VideoDetails extends Video implements VideoDetailsServerModel { 9export class VideoDetails extends Video implements VideoDetailsServerModel {
8 descriptionPath: string 10 descriptionPath: string
@@ -20,6 +22,10 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
20 likesPercent: number 22 likesPercent: number
21 dislikesPercent: number 23 dislikesPercent: number
22 24
25 trackerUrls: string[]
26
27 streamingPlaylists: VideoStreamingPlaylist[]
28
23 constructor (hash: VideoDetailsServerModel, translations = {}) { 29 constructor (hash: VideoDetailsServerModel, translations = {}) {
24 super(hash, translations) 30 super(hash, translations)
25 31
@@ -32,6 +38,9 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
32 this.commentsEnabled = hash.commentsEnabled 38 this.commentsEnabled = hash.commentsEnabled
33 this.downloadEnabled = hash.downloadEnabled 39 this.downloadEnabled = hash.downloadEnabled
34 40
41 this.trackerUrls = hash.trackerUrls
42 this.streamingPlaylists = hash.streamingPlaylists
43
35 this.buildLikeAndDislikePercents() 44 this.buildLikeAndDislikePercents()
36 } 45 }
37 46
@@ -55,4 +64,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
55 this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 64 this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100
56 this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 65 this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100
57 } 66 }
67
68 getHlsPlaylist () {
69 return this.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
70 }
58} 71}
diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts
index 5bb0510f9..18c62a1f9 100644
--- a/client/src/app/shared/video/video-edit.model.ts
+++ b/client/src/app/shared/video/video-edit.model.ts
@@ -59,14 +59,14 @@ export class VideoEdit implements VideoUpdate {
59 } 59 }
60 } 60 }
61 61
62 patch (values: Object) { 62 patch (values: { [ id: string ]: string }) {
63 Object.keys(values).forEach((key) => { 63 Object.keys(values).forEach((key) => {
64 this[ key ] = values[ key ] 64 this[ key ] = values[ key ]
65 }) 65 })
66 66
67 // If schedule publication, the video is private and will be changed to public privacy 67 // If schedule publication, the video is private and will be changed to public privacy
68 if (parseInt(values['privacy'], 10) === VideoEdit.SPECIAL_SCHEDULED_PRIVACY) { 68 if (parseInt(values['privacy'], 10) === VideoEdit.SPECIAL_SCHEDULED_PRIVACY) {
69 const updateAt = (values['schedulePublicationAt'] as Date) 69 const updateAt = new Date(values['schedulePublicationAt'])
70 updateAt.setSeconds(0) 70 updateAt.setSeconds(0)
71 71
72 this.privacy = VideoPrivacy.PRIVATE 72 this.privacy = VideoPrivacy.PRIVATE
diff --git a/client/src/app/shared/video/video-feed.component.scss b/client/src/app/shared/video/video-feed.component.scss
deleted file mode 100644
index 385764be0..000000000
--- a/client/src/app/shared/video/video-feed.component.scss
+++ /dev/null
@@ -1,19 +0,0 @@
1@import '_mixins';
2
3.video-feed {
4 a {
5 color: black;
6 display: block;
7 }
8
9 .icon {
10 @include icon(12px);
11
12 &.icon-syndication {
13 position: relative;
14 top: -2px;
15 background-color: var(--mainForegroundColor);
16 mask-image: url('../../../assets/images/global/syndication.svg');
17 }
18 }
19} \ No newline at end of file
diff --git a/client/src/app/shared/video/video-feed.component.ts b/client/src/app/shared/video/video-feed.component.ts
deleted file mode 100644
index 6922153c0..000000000
--- a/client/src/app/shared/video/video-feed.component.ts
+++ /dev/null
@@ -1,10 +0,0 @@
1import { Component, Input } from '@angular/core'
2
3@Component({
4 selector: 'my-video-feed',
5 styleUrls: [ './video-feed.component.scss' ],
6 templateUrl: './video-feed.component.html'
7})
8export class VideoFeedComponent {
9 @Input() syndicationItems
10}
diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html
index cfc483018..2c635fa2f 100644
--- a/client/src/app/shared/video/video-miniature.component.html
+++ b/client/src/app/shared/video/video-miniature.component.html
@@ -7,6 +7,9 @@
7 class="video-miniature-name" 7 class="video-miniature-name"
8 [routerLink]="[ '/videos/watch', video.uuid ]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }" 8 [routerLink]="[ '/videos/watch', video.uuid ]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }"
9 > 9 >
10 <span *ngIf="isUnlistedVideo()" class="badge badge-warning" i18n>Unlisted</span>
11 <span *ngIf="isPrivateVideo()" class="badge badge-danger" i18n>Private</span>
12
10 {{ video.name }} 13 {{ video.name }}
11 </a> 14 </a>
12 15
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-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts
index 7e8692b0b..2f951a1f1 100644
--- a/client/src/app/shared/video/video-miniature.component.ts
+++ b/client/src/app/shared/video/video-miniature.component.ts
@@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core
2import { User } from '../users' 2import { User } from '../users'
3import { Video } from './video.model' 3import { Video } from './video.model'
4import { ServerService } from '@app/core' 4import { ServerService } from '@app/core'
5import { VideoPrivacy } from '../../../../../shared'
5 6
6export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto' 7export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto'
7 8
@@ -49,4 +50,12 @@ export class VideoMiniatureComponent implements OnInit {
49 displayOwnerVideoChannel () { 50 displayOwnerVideoChannel () {
50 return this.ownerDisplayTypeChosen === 'videoChannel' 51 return this.ownerDisplayTypeChosen === 'videoChannel'
51 } 52 }
53
54 isUnlistedVideo () {
55 return this.video.privacy.id === VideoPrivacy.UNLISTED
56 }
57
58 isPrivateVideo () {
59 return this.video.privacy.id === VideoPrivacy.PRIVATE
60 }
52} 61}
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/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts
index c9d6da7a4..565aad93b 100644
--- a/client/src/app/shared/video/video.service.ts
+++ b/client/src/app/shared/video/video.service.ts
@@ -6,11 +6,11 @@ import { Video as VideoServerModel, VideoDetails as VideoDetailsServerModel } fr
6import { ResultList } from '../../../../../shared/models/result-list.model' 6import { ResultList } from '../../../../../shared/models/result-list.model'
7import { 7import {
8 UserVideoRate, 8 UserVideoRate,
9 UserVideoRateType,
9 UserVideoRateUpdate, 10 UserVideoRateUpdate,
10 VideoConstant, 11 VideoConstant,
11 VideoFilter, 12 VideoFilter,
12 VideoPrivacy, 13 VideoPrivacy,
13 VideoRateType,
14 VideoUpdate 14 VideoUpdate
15} from '../../../../../shared/models/videos' 15} from '../../../../../shared/models/videos'
16import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum' 16import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum'
@@ -275,9 +275,9 @@ export class VideoService implements VideosProvider {
275 275
276 loadCompleteDescription (descriptionPath: string) { 276 loadCompleteDescription (descriptionPath: string) {
277 return this.authHttp 277 return this.authHttp
278 .get(environment.apiUrl + descriptionPath) 278 .get<{ description: string }>(environment.apiUrl + descriptionPath)
279 .pipe( 279 .pipe(
280 map(res => res[ 'description' ]), 280 map(res => res.description),
281 catchError(err => this.restExtractor.handleError(err)) 281 catchError(err => this.restExtractor.handleError(err))
282 ) 282 )
283 } 283 }
@@ -333,7 +333,7 @@ export class VideoService implements VideosProvider {
333 return privacies 333 return privacies
334 } 334 }
335 335
336 private setVideoRate (id: number, rateType: VideoRateType) { 336 private setVideoRate (id: number, rateType: UserVideoRateType) {
337 const url = VideoService.BASE_VIDEO_URL + id + '/rate' 337 const url = VideoService.BASE_VIDEO_URL + id + '/rate'
338 const body: UserVideoRateUpdate = { 338 const body: UserVideoRateUpdate = {
339 rating: rateType 339 rating: rateType
diff --git a/client/src/app/signup/signup.component.html b/client/src/app/signup/signup.component.html
index aad4b5be3..07d24b381 100644
--- a/client/src/app/signup/signup.component.html
+++ b/client/src/app/signup/signup.component.html
@@ -4,6 +4,7 @@
4 Create an account 4 Create an account
5 </div> 5 </div>
6 6
7 <div *ngIf="info" class="alert alert-info">{{ info }}</div>
7 <div *ngIf="error" class="alert alert-danger">{{ error }}</div> 8 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
8 9
9 <div class="d-flex justify-content-left flex-wrap"> 10 <div class="d-flex justify-content-left flex-wrap">
@@ -51,7 +52,7 @@
51 <div class="form-group form-group-terms"> 52 <div class="form-group form-group-terms">
52 <my-peertube-checkbox 53 <my-peertube-checkbox
53 inputName="terms" formControlName="terms" 54 inputName="terms" formControlName="terms"
54 i18n-labelHtml labelHtml="I have read and agree to the <a href='/about/instance#terms-section' target='_blank'rel='noopener noreferrer'>Terms</a> of this instance" 55 i18n-labelHtml labelHtml="I am at least 16 years old and agree to the <a href='/about/instance#terms-section' target='_blank'rel='noopener noreferrer'>Terms</a> of this instance"
55 ></my-peertube-checkbox> 56 ></my-peertube-checkbox>
56 57
57 <div *ngIf="formErrors.terms" class="form-error"> 58 <div *ngIf="formErrors.terms" class="form-error">
@@ -59,11 +60,11 @@
59 </div> 60 </div>
60 </div> 61 </div>
61 62
62 <input type="submit" i18n-value value="Signup" [disabled]="!form.valid"> 63 <input type="submit" i18n-value value="Signup" [disabled]="!form.valid || signupDone">
63 </form> 64 </form>
64 65
65 <div> 66 <div>
66 <label for="email" i18n>Features found on this instance</label> 67 <label i18n>Features found on this instance</label>
67 <my-instance-features-table></my-instance-features-table> 68 <my-instance-features-table></my-instance-features-table>
68 </div> 69 </div>
69 </div> 70 </div>
diff --git a/client/src/app/signup/signup.component.ts b/client/src/app/signup/signup.component.ts
index cf2657b85..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 { 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
@@ -12,12 +11,15 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val
12 styleUrls: [ './signup.component.scss' ] 11 styleUrls: [ './signup.component.scss' ]
13}) 12})
14export class SignupComponent extends FormReactive implements OnInit { 13export class SignupComponent extends FormReactive implements OnInit {
14 info: string = null
15 error: string = null 15 error: string = null
16 signupDone = false
16 17
17 constructor ( 18 constructor (
18 protected formValidatorService: FormValidatorService, 19 protected formValidatorService: FormValidatorService,
20 private authService: AuthService,
19 private userValidatorsService: UserValidatorsService, 21 private userValidatorsService: UserValidatorsService,
20 private notificationsService: NotificationsService, 22 private notifier: Notifier,
21 private userService: UserService, 23 private userService: UserService,
22 private serverService: ServerService, 24 private serverService: ServerService,
23 private redirectService: RedirectService, 25 private redirectService: RedirectService,
@@ -50,18 +52,24 @@ export class SignupComponent extends FormReactive implements OnInit {
50 52
51 this.userService.signup(userCreate).subscribe( 53 this.userService.signup(userCreate).subscribe(
52 () => { 54 () => {
55 this.signupDone = true
56
53 if (this.requiresEmailVerification) { 57 if (this.requiresEmailVerification) {
54 this.notificationsService.alert( 58 this.info = this.i18n('Welcome! Now please check your emails to verify your account and complete signup.')
55 this.i18n('Welcome'), 59 return
56 this.i18n('Please check your email to verify your account and complete signup.')
57 )
58 } else {
59 this.notificationsService.success(
60 this.i18n('Success'),
61 this.i18n('Registration for {{username}} complete.', { username: userCreate.username })
62 )
63 } 60 }
64 this.redirectService.redirectToHomepage() 61
62 // Auto login
63 this.authService.login(userCreate.username, userCreate.password)
64 .subscribe(
65 () => {
66 this.notifier.success(this.i18n('You are now logged in as {{username}}!', { username: userCreate.username }))
67
68 this.redirectService.redirectToHomepage()
69 },
70
71 err => this.error = err.message
72 )
65 }, 73 },
66 74
67 err => this.error = err.message 75 err => this.error = err.message
diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html
index 30aefdbfc..19043eee6 100644
--- a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html
+++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html
@@ -3,7 +3,7 @@
3 3
4 <div class="modal-header"> 4 <div class="modal-header">
5 <h4 i18n class="modal-title">Add caption</h4> 5 <h4 i18n class="modal-title">Add caption</h4>
6 <span class="close" aria-label="Close" role="button" (click)="hide()"></span> 6 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
7 </div> 7 </div>
8 8
9 <div class="modal-body"> 9 <div class="modal-body">
diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
index 07c33030a..1413e7262 100644
--- a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
+++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
@@ -5,6 +5,7 @@ import { VideoCaptionsValidatorsService } from '@app/shared/forms/form-validator
5import { ServerService } from '@app/core' 5import { ServerService } from '@app/core'
6import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' 6import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
7import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
8import { VideoConstant } from '../../../../../../shared'
8 9
9@Component({ 10@Component({
10 selector: 'my-video-caption-add-modal', 11 selector: 'my-video-caption-add-modal',
@@ -19,7 +20,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
19 20
20 @ViewChild('modal') modal: ElementRef 21 @ViewChild('modal') modal: ElementRef
21 22
22 videoCaptionLanguages = [] 23 videoCaptionLanguages: VideoConstant<string>[] = []
23 24
24 private openedModal: NgbModalRef 25 private openedModal: NgbModalRef
25 private closingModal = false 26 private closingModal = false
@@ -59,6 +60,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
59 hide () { 60 hide () {
60 this.closingModal = true 61 this.closingModal = true
61 this.openedModal.close() 62 this.openedModal.close()
63 this.form.reset()
62 } 64 }
63 65
64 isReplacingExistingCaption () { 66 isReplacingExistingCaption () {
@@ -70,8 +72,6 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
70 } 72 }
71 73
72 async addCaption () { 74 async addCaption () {
73 this.hide()
74
75 const languageId = this.form.value[ 'language' ] 75 const languageId = this.form.value[ 'language' ]
76 const languageObject = this.videoCaptionLanguages.find(l => l.id === languageId) 76 const languageObject = this.videoCaptionLanguages.find(l => l.id === languageId)
77 77
@@ -80,6 +80,6 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
80 captionfile: this.form.value[ 'captionfile' ] 80 captionfile: this.form.value[ 'captionfile' ]
81 }) 81 })
82 82
83 this.form.reset() 83 this.hide()
84 } 84 }
85} 85}
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html
index 8c74a1ca6..7fd9af208 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html
@@ -127,10 +127,11 @@
127 127
128 <my-peertube-checkbox 128 <my-peertube-checkbox
129 inputName="downloadEnabled" formControlName="downloadEnabled" 129 inputName="downloadEnabled" formControlName="downloadEnabled"
130 i18n-labelText labelText="Disable downloading" 130 i18n-labelText labelText="Download enabled"
131 ></my-peertube-checkbox> 131 ></my-peertube-checkbox>
132 132
133 <my-peertube-checkbox 133 <my-peertube-checkbox
134 *ngIf="waitTranscodingEnabled"
134 inputName="waitTranscoding" formControlName="waitTranscoding" 135 inputName="waitTranscoding" formControlName="waitTranscoding"
135 i18n-labelText labelText="Wait transcoding before publishing the video" 136 i18n-labelText labelText="Wait transcoding before publishing the video"
136 i18n-helpHtml helpHtml="If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends." 137 i18n-helpHtml helpHtml="If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends."
@@ -147,7 +148,7 @@
147 148
148 <div class="captions-header"> 149 <div class="captions-header">
149 <a (click)="openAddCaptionModal()" class="create-caption"> 150 <a (click)="openAddCaptionModal()" class="create-caption">
150 <span class="icon icon-add"></span> 151 <my-global-icon iconName="add"></my-global-icon>
151 <ng-container i18n>Add another caption</ng-container> 152 <ng-container i18n>Add another caption</ng-container>
152 </a> 153 </a>
153 </div> 154 </div>
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.scss b/client/src/app/videos/+video-edit/shared/video-edit.component.scss
index b039d7ad4..bb775cb0a 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.scss
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.scss
@@ -5,6 +5,11 @@
5 @include peertube-select-container(auto); 5 @include peertube-select-container(auto);
6} 6}
7 7
8my-peertube-checkbox {
9 display: block;
10 margin-bottom: 1rem;
11}
12
8.video-edit { 13.video-edit {
9 height: 100%; 14 height: 100%;
10 min-height: 300px; 15 min-height: 300px;
@@ -18,10 +23,6 @@
18 display: block; 23 display: block;
19 } 24 }
20 25
21 input, select {
22 font-size: 15px
23 }
24
25 .label-tags + span { 26 .label-tags + span {
26 font-size: 15px; 27 font-size: 15px;
27 } 28 }
@@ -37,7 +38,7 @@
37 text-align: right; 38 text-align: right;
38 39
39 .create-caption { 40 .create-caption {
40 @include create-button('../../../../assets/images/global/add.svg'); 41 @include create-button;
41 } 42 }
42 } 43 }
43 44
@@ -95,13 +96,14 @@
95 display: inline-block; 96 display: inline-block;
96 margin-right: 25px; 97 margin-right: 25px;
97 98
98 color: #585858; 99 color: $grey-foreground-color;
99 font-size: 15px; 100 font-size: 15px;
100 } 101 }
101 102
102 .submit-button { 103 .submit-button {
103 @include peertube-button; 104 @include peertube-button;
104 @include orange-button; 105 @include orange-button;
106 @include button-with-icon(20px, 1px);
105 107
106 display: inline-block; 108 display: inline-block;
107 109
@@ -114,16 +116,6 @@
114 color: inherit; 116 color: inherit;
115 font-weight: $font-semibold; 117 font-weight: $font-semibold;
116 } 118 }
117
118 .icon.icon-validate {
119 @include icon(20px);
120
121 cursor: inherit;
122 position: relative;
123 top: -1px;
124 margin-right: 4px;
125 background-image: url('../../../../assets/images/global/validate.svg');
126 }
127 } 119 }
128} 120}
129 121
@@ -171,10 +163,10 @@ p-calendar {
171 } 163 }
172 164
173 tag { 165 tag {
174 background-color: var(--inputColor) !important; 166 background-color: $grey-background-color !important;
167 color: #000 !important;
175 border-radius: 3px !important; 168 border-radius: 3px !important;
176 font-size: 15px !important; 169 font-size: 15px !important;
177 color: var(--mainForegroundColor) !important;
178 height: 30px !important; 170 height: 30px !important;
179 line-height: 30px !important; 171 line-height: 30px !important;
180 margin: 0 5px 0 0 !important; 172 margin: 0 5px 0 0 !important;
@@ -197,7 +189,10 @@ p-calendar {
197 top: -1px; 189 top: -1px;
198 height: auto !important; 190 height: auto !important;
199 vertical-align: middle !important; 191 vertical-align: middle !important;
200 fill: #585858 !important; 192
193 path {
194 fill: $grey-foreground-color !important;
195 }
201 } 196 }
202 197
203 &:hover { 198 &:hover {
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.ts b/client/src/app/videos/+video-edit/shared/video-edit.component.ts
index 1dae931e2..3ed7a4a10 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.ts
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts
@@ -2,7 +2,7 @@ import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'
2import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms' 2import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared' 4import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared'
5import { NotificationsService } from 'angular2-notifications' 5import { Notifier } from '@app/core'
6import { ServerService } from '../../../core/server' 6import { ServerService } from '../../../core/server'
7import { VideoEdit } from '../../../shared/video/video-edit.model' 7import { VideoEdit } from '../../../shared/video/video-edit.model'
8import { map } from 'rxjs/operators' 8import { map } from 'rxjs/operators'
@@ -27,6 +27,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
27 @Input() userVideoChannels: { id: number, label: string, support: string }[] = [] 27 @Input() userVideoChannels: { id: number, label: string, support: string }[] = []
28 @Input() schedulePublicationPossible = true 28 @Input() schedulePublicationPossible = true
29 @Input() videoCaptions: VideoCaptionEdit[] = [] 29 @Input() videoCaptions: VideoCaptionEdit[] = []
30 @Input() waitTranscodingEnabled = true
30 31
31 @ViewChild('videoCaptionAddModal') videoCaptionAddModal: VideoCaptionAddModalComponent 32 @ViewChild('videoCaptionAddModal') videoCaptionAddModal: VideoCaptionAddModalComponent
32 33
@@ -48,7 +49,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
48 calendarTimezone: string 49 calendarTimezone: string
49 calendarDateFormat: string 50 calendarDateFormat: string
50 51
51 private schedulerInterval 52 private schedulerInterval: any
52 private firstPatchDone = false 53 private firstPatchDone = false
53 private initialVideoCaptions: string[] = [] 54 private initialVideoCaptions: string[] = []
54 55
@@ -58,7 +59,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
58 private videoCaptionService: VideoCaptionService, 59 private videoCaptionService: VideoCaptionService,
59 private route: ActivatedRoute, 60 private route: ActivatedRoute,
60 private router: Router, 61 private router: Router,
61 private notificationsService: NotificationsService, 62 private notifier: Notifier,
62 private serverService: ServerService, 63 private serverService: ServerService,
63 private i18nPrimengCalendarService: I18nPrimengCalendarService 64 private i18nPrimengCalendarService: I18nPrimengCalendarService
64 ) { 65 ) {
@@ -77,14 +78,14 @@ export class VideoEditComponent implements OnInit, OnDestroy {
77 } 78 }
78 79
79 updateForm () { 80 updateForm () {
80 const defaultValues = { 81 const defaultValues: any = {
81 nsfw: 'false', 82 nsfw: 'false',
82 commentsEnabled: 'true', 83 commentsEnabled: 'true',
83 downloadEnabled: 'true', 84 downloadEnabled: 'true',
84 waitTranscoding: 'true', 85 waitTranscoding: 'true',
85 tags: [] 86 tags: []
86 } 87 }
87 const obj = { 88 const obj: any = {
88 name: this.videoValidatorsService.VIDEO_NAME, 89 name: this.videoValidatorsService.VIDEO_NAME,
89 privacy: this.videoValidatorsService.VIDEO_PRIVACY, 90 privacy: this.videoValidatorsService.VIDEO_PRIVACY,
90 channelId: this.videoValidatorsService.VIDEO_CHANNEL, 91 channelId: this.videoValidatorsService.VIDEO_CHANNEL,
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
index a933a64f0..28eb143c9 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
@@ -1,6 +1,6 @@
1<div *ngIf="!hasImportedVideo" class="upload-video-container"> 1<div *ngIf="!hasImportedVideo" class="upload-video-container">
2 <div class="import-video-torrent"> 2 <div class="first-step-block">
3 <div class="icon icon-upload"></div> 3 <my-global-icon class="upload-icon" iconName="upload"></my-global-icon>
4 4
5 <div class="button-file"> 5 <div class="button-file">
6 <span i18n>Select the torrent to import</span> 6 <span i18n>Select the torrent to import</span>
@@ -45,7 +45,12 @@
45 </div> 45 </div>
46</div> 46</div>
47 47
48<div *ngIf="hasImportedVideo" class="alert alert-info" i18n> 48<div *ngIf="error" class="alert alert-danger">
49 <div i18n>Sorry, but something went wrong</div>
50 {{ error }}
51</div>
52
53<div *ngIf="hasImportedVideo && !error" class="alert alert-info" i18n>
49 Congratulations, the video will be imported with BitTorrent! You can already add information about this video. 54 Congratulations, the video will be imported with BitTorrent! You can already add information about this video.
50</div> 55</div>
51 56
@@ -61,7 +66,7 @@
61 (click)="updateSecondStep()" 66 (click)="updateSecondStep()"
62 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" 67 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"
63 > 68 >
64 <span class="icon icon-validate"></span> 69 <my-global-icon iconName="validate"></my-global-icon>
65 <input type="button" i18n-value value="Update" /> 70 <input type="button" i18n-value value="Update" />
66 </div> 71 </div>
67 </div> 72 </div>
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss
index 262b0b68e..6d59ed834 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss
@@ -1,37 +1,7 @@
1@import 'variables'; 1@import 'variables';
2@import 'mixins'; 2@import 'mixins';
3 3
4$width-size: 190px; 4.first-step-block {
5
6.peertube-select-container {
7 @include peertube-select-container($width-size);
8}
9
10.import-video-torrent {
11 display: flex;
12 flex-direction: column;
13 align-items: center;
14
15 .icon.icon-upload {
16 @include icon(90px);
17 margin-bottom: 25px;
18 cursor: default;
19
20 background-image: url('../../../../assets/images/video/upload.svg');
21 }
22
23 .button-file {
24 @include peertube-button-file(auto);
25
26 min-width: 190px;
27 }
28
29 .button-file-extension {
30 display: block;
31 font-size: 12px;
32 margin-top: 5px;
33 }
34
35 .torrent-or-magnet { 5 .torrent-or-magnet {
36 margin: 10px 0; 6 margin: 10px 0;
37 } 7 }
@@ -39,19 +9,6 @@ $width-size: 190px;
39 .form-group-magnet-uri { 9 .form-group-magnet-uri {
40 margin-bottom: 40px; 10 margin-bottom: 40px;
41 } 11 }
42
43 input[type=text] {
44 @include peertube-input-text($width-size);
45 display: block;
46 }
47
48 input[type=button] {
49 @include peertube-button;
50 @include orange-button;
51
52 width: $width-size;
53 margin-top: 30px;
54 }
55} 12}
56 13
57 14
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
index 0f6fd17a9..c12a1d653 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
@@ -1,8 +1,7 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications'
4import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos' 3import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos'
5import { AuthService, ServerService } from '../../../core' 4import { AuthService, Notifier, ServerService } from '../../../core'
6import { VideoService } from '../../../shared/video/video.service' 5import { VideoService } from '../../../shared/video/video.service'
7import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
8import { LoadingBarService } from '@ngx-loading-bar/core' 7import { LoadingBarService } from '@ngx-loading-bar/core'
@@ -12,20 +11,22 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
12import { FormValidatorService } from '@app/shared' 11import { FormValidatorService } from '@app/shared'
13import { VideoCaptionService } from '@app/shared/video-caption' 12import { VideoCaptionService } from '@app/shared/video-caption'
14import { VideoImportService } from '@app/shared/video-import' 13import { VideoImportService } from '@app/shared/video-import'
14import { scrollToTop } from '@app/shared/misc/utils'
15 15
16@Component({ 16@Component({
17 selector: 'my-video-import-torrent', 17 selector: 'my-video-import-torrent',
18 templateUrl: './video-import-torrent.component.html', 18 templateUrl: './video-import-torrent.component.html',
19 styleUrls: [ 19 styleUrls: [
20 '../shared/video-edit.component.scss', 20 '../shared/video-edit.component.scss',
21 './video-import-torrent.component.scss' 21 './video-import-torrent.component.scss',
22 './video-send.scss'
22 ] 23 ]
23}) 24})
24export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { 25export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate {
25 @Output() firstStepDone = new EventEmitter<string>() 26 @Output() firstStepDone = new EventEmitter<string>()
26 @ViewChild('torrentfileInput') torrentfileInput 27 @Output() firstStepError = new EventEmitter<void>()
28 @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement>
27 29
28 videoFileName: string
29 magnetUri = '' 30 magnetUri = ''
30 31
31 isImportingVideo = false 32 isImportingVideo = false
@@ -33,13 +34,14 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
33 isUpdatingVideo = false 34 isUpdatingVideo = false
34 35
35 video: VideoEdit 36 video: VideoEdit
37 error: string
36 38
37 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC 39 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
38 40
39 constructor ( 41 constructor (
40 protected formValidatorService: FormValidatorService, 42 protected formValidatorService: FormValidatorService,
41 protected loadingBar: LoadingBarService, 43 protected loadingBar: LoadingBarService,
42 protected notificationsService: NotificationsService, 44 protected notifier: Notifier,
43 protected authService: AuthService, 45 protected authService: AuthService,
44 protected serverService: ServerService, 46 protected serverService: ServerService,
45 protected videoService: VideoService, 47 protected videoService: VideoService,
@@ -64,7 +66,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
64 } 66 }
65 67
66 fileChange () { 68 fileChange () {
67 const torrentfile = this.torrentfileInput.nativeElement.files[0] as File 69 const torrentfile = this.torrentfileInput.nativeElement.files[0]
68 if (!torrentfile) return 70 if (!torrentfile) return
69 71
70 this.importVideo(torrentfile) 72 this.importVideo(torrentfile)
@@ -106,7 +108,8 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
106 err => { 108 err => {
107 this.loadingBar.complete() 109 this.loadingBar.complete()
108 this.isImportingVideo = false 110 this.isImportingVideo = false
109 this.notificationsService.error(this.i18n('Error'), err.message) 111 this.firstStepError.emit()
112 this.notifier.error(err.message)
110 } 113 }
111 ) 114 )
112 } 115 }
@@ -125,14 +128,14 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
125 .subscribe( 128 .subscribe(
126 () => { 129 () => {
127 this.isUpdatingVideo = false 130 this.isUpdatingVideo = false
128 this.notificationsService.success(this.i18n('Success'), this.i18n('Video to import updated.')) 131 this.notifier.success(this.i18n('Video to import updated.'))
129 132
130 this.router.navigate([ '/my-account', 'video-imports' ]) 133 this.router.navigate([ '/my-account', 'video-imports' ])
131 }, 134 },
132 135
133 err => { 136 err => {
134 this.isUpdatingVideo = false 137 this.error = err.message
135 this.notificationsService.error(this.i18n('Error'), err.message) 138 scrollToTop()
136 console.error(err) 139 console.error(err)
137 } 140 }
138 ) 141 )
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
index 9f5fc6d22..3550c3585 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
@@ -1,6 +1,6 @@
1<div *ngIf="!hasImportedVideo" class="upload-video-container"> 1<div *ngIf="!hasImportedVideo" class="upload-video-container">
2 <div class="import-video-url"> 2 <div class="first-step-block">
3 <div class="icon icon-upload"></div> 3 <my-global-icon class="upload-icon" iconName="upload"></my-global-icon>
4 4
5 <div class="form-group"> 5 <div class="form-group">
6 <label i18n for="targetUrl">URL</label> 6 <label i18n for="targetUrl">URL</label>
@@ -37,7 +37,13 @@
37 </div> 37 </div>
38</div> 38</div>
39 39
40<div *ngIf="hasImportedVideo" class="alert alert-info" i18n> 40
41<div *ngIf="error" class="alert alert-danger">
42 <div i18n>Sorry, but something went wrong</div>
43 {{ error }}
44</div>
45
46<div *ngIf="!error && hasImportedVideo" class="alert alert-info" i18n>
41 Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video. 47 Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video.
42</div> 48</div>
43 49
@@ -53,7 +59,7 @@
53 (click)="updateSecondStep()" 59 (click)="updateSecondStep()"
54 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" 60 [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"
55 > 61 >
56 <span class="icon icon-validate"></span> 62 <my-global-icon iconName="validate"></my-global-icon>
57 <input type="button" i18n-value value="Update" /> 63 <input type="button" i18n-value value="Update" />
58 </div> 64 </div>
59 </div> 65 </div>
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss
deleted file mode 100644
index 7c6deda1d..000000000
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss
+++ /dev/null
@@ -1,37 +0,0 @@
1@import 'variables';
2@import 'mixins';
3
4$width-size: 190px;
5
6.peertube-select-container {
7 @include peertube-select-container($width-size);
8}
9
10.import-video-url {
11 display: flex;
12 flex-direction: column;
13 align-items: center;
14
15 .icon.icon-upload {
16 @include icon(90px);
17 margin-bottom: 25px;
18 cursor: default;
19
20 background-image: url('../../../../assets/images/video/upload.svg');
21 }
22
23 input[type=text] {
24 @include peertube-input-text($width-size);
25 display: block;
26 }
27
28 input[type=button] {
29 @include peertube-button;
30 @include orange-button;
31
32 width: $width-size;
33 margin-top: 30px;
34 }
35}
36
37
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
index fbc85c74f..d11685916 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
@@ -1,8 +1,7 @@
1import { Component, EventEmitter, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications'
4import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos' 3import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos'
5import { AuthService, ServerService } from '../../../core' 4import { AuthService, Notifier, ServerService } from '../../../core'
6import { VideoService } from '../../../shared/video/video.service' 5import { VideoService } from '../../../shared/video/video.service'
7import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
8import { LoadingBarService } from '@ngx-loading-bar/core' 7import { LoadingBarService } from '@ngx-loading-bar/core'
@@ -12,33 +11,35 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
12import { FormValidatorService } from '@app/shared' 11import { FormValidatorService } from '@app/shared'
13import { VideoCaptionService } from '@app/shared/video-caption' 12import { VideoCaptionService } from '@app/shared/video-caption'
14import { VideoImportService } from '@app/shared/video-import' 13import { VideoImportService } from '@app/shared/video-import'
14import { scrollToTop } from '@app/shared/misc/utils'
15 15
16@Component({ 16@Component({
17 selector: 'my-video-import-url', 17 selector: 'my-video-import-url',
18 templateUrl: './video-import-url.component.html', 18 templateUrl: './video-import-url.component.html',
19 styleUrls: [ 19 styleUrls: [
20 '../shared/video-edit.component.scss', 20 '../shared/video-edit.component.scss',
21 './video-import-url.component.scss' 21 './video-send.scss'
22 ] 22 ]
23}) 23})
24export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { 24export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate {
25 @Output() firstStepDone = new EventEmitter<string>() 25 @Output() firstStepDone = new EventEmitter<string>()
26 @Output() firstStepError = new EventEmitter<void>()
26 27
27 targetUrl = '' 28 targetUrl = ''
28 videoFileName: string
29 29
30 isImportingVideo = false 30 isImportingVideo = false
31 hasImportedVideo = false 31 hasImportedVideo = false
32 isUpdatingVideo = false 32 isUpdatingVideo = false
33 33
34 video: VideoEdit 34 video: VideoEdit
35 error: string
35 36
36 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC 37 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
37 38
38 constructor ( 39 constructor (
39 protected formValidatorService: FormValidatorService, 40 protected formValidatorService: FormValidatorService,
40 protected loadingBar: LoadingBarService, 41 protected loadingBar: LoadingBarService,
41 protected notificationsService: NotificationsService, 42 protected notifier: Notifier,
42 protected authService: AuthService, 43 protected authService: AuthService,
43 protected serverService: ServerService, 44 protected serverService: ServerService,
44 protected videoService: VideoService, 45 protected videoService: VideoService,
@@ -98,7 +99,8 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
98 err => { 99 err => {
99 this.loadingBar.complete() 100 this.loadingBar.complete()
100 this.isImportingVideo = false 101 this.isImportingVideo = false
101 this.notificationsService.error(this.i18n('Error'), err.message) 102 this.firstStepError.emit()
103 this.notifier.error(err.message)
102 } 104 }
103 ) 105 )
104 } 106 }
@@ -117,14 +119,14 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
117 .subscribe( 119 .subscribe(
118 () => { 120 () => {
119 this.isUpdatingVideo = false 121 this.isUpdatingVideo = false
120 this.notificationsService.success(this.i18n('Success'), this.i18n('Video to import updated.')) 122 this.notifier.success(this.i18n('Video to import updated.'))
121 123
122 this.router.navigate([ '/my-account', 'video-imports' ]) 124 this.router.navigate([ '/my-account', 'video-imports' ])
123 }, 125 },
124 126
125 err => { 127 err => {
126 this.isUpdatingVideo = false 128 this.error = err.message
127 this.notificationsService.error(this.i18n('Error'), err.message) 129 scrollToTop()
128 console.error(err) 130 console.error(err)
129 } 131 }
130 ) 132 )
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-send.scss b/client/src/app/videos/+video-edit/video-add-components/video-send.scss
new file mode 100644
index 000000000..8769dd302
--- /dev/null
+++ b/client/src/app/videos/+video-edit/video-add-components/video-send.scss
@@ -0,0 +1,54 @@
1@import 'variables';
2@import 'mixins';
3
4$width-size: 190px;
5
6.alert.alert-danger {
7 text-align: center;
8
9 & > div {
10 font-weight: $font-semibold;
11 }
12}
13
14.first-step-block {
15 display: flex;
16 flex-direction: column;
17 align-items: center;
18
19 .upload-icon {
20 width: 90px;
21 margin-bottom: 25px;
22
23 @include apply-svg-color(#C6C6C6);
24 }
25
26 .peertube-select-container {
27 @include peertube-select-container($width-size);
28 }
29
30 input[type=text] {
31 @include peertube-input-text($width-size);
32 display: block;
33 }
34
35 input[type=button] {
36 @include peertube-button;
37 @include orange-button;
38
39 width: $width-size;
40 margin-top: 30px;
41 }
42
43 .button-file {
44 @include peertube-button-file(auto);
45
46 min-width: 190px;
47 }
48
49 .button-file-extension {
50 display: block;
51 font-size: 12px;
52 margin-top: 5px;
53 }
54}
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-send.ts b/client/src/app/videos/+video-edit/video-add-components/video-send.ts
index 6d1bac3f2..580c123a0 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-send.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-send.ts
@@ -1,18 +1,17 @@
1import { EventEmitter, OnInit } from '@angular/core' 1import { EventEmitter, OnInit } from '@angular/core'
2import { LoadingBarService } from '@ngx-loading-bar/core' 2import { LoadingBarService } from '@ngx-loading-bar/core'
3import { NotificationsService } from 'angular2-notifications' 3import { AuthService, Notifier, ServerService } from '@app/core'
4import { catchError, switchMap, tap } from 'rxjs/operators' 4import { catchError, switchMap, tap } from 'rxjs/operators'
5import { FormReactive } from '@app/shared' 5import { FormReactive } from '@app/shared'
6import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
7import { VideoConstant, VideoPrivacy } from '../../../../../../shared' 6import { VideoConstant, VideoPrivacy } from '../../../../../../shared'
8import { AuthService, ServerService } from '@app/core'
9import { VideoService } from '@app/shared/video/video.service' 7import { VideoService } from '@app/shared/video/video.service'
10import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' 8import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
11import { VideoCaptionService } from '@app/shared/video-caption' 9import { VideoCaptionService } from '@app/shared/video-caption'
12import { VideoEdit } from '@app/shared/video/video-edit.model' 10import { VideoEdit } from '@app/shared/video/video-edit.model'
13import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils' 11import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
12import { CanComponentDeactivateResult } from '@app/shared/guards/can-deactivate-guard.service'
14 13
15export abstract class VideoSend extends FormReactive implements OnInit, CanComponentDeactivate { 14export abstract class VideoSend extends FormReactive implements OnInit {
16 userVideoChannels: { id: number, label: string, support: string }[] = [] 15 userVideoChannels: { id: number, label: string, support: string }[] = []
17 videoPrivacies: VideoConstant<VideoPrivacy>[] = [] 16 videoPrivacies: VideoConstant<VideoPrivacy>[] = []
18 videoCaptions: VideoCaptionEdit[] = [] 17 videoCaptions: VideoCaptionEdit[] = []
@@ -21,16 +20,17 @@ export abstract class VideoSend extends FormReactive implements OnInit, CanCompo
21 firstStepChannelId = 0 20 firstStepChannelId = 0
22 21
23 abstract firstStepDone: EventEmitter<string> 22 abstract firstStepDone: EventEmitter<string>
23 abstract firstStepError: EventEmitter<void>
24 protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy 24 protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy
25 25
26 protected loadingBar: LoadingBarService 26 protected loadingBar: LoadingBarService
27 protected notificationsService: NotificationsService 27 protected notifier: Notifier
28 protected authService: AuthService 28 protected authService: AuthService
29 protected serverService: ServerService 29 protected serverService: ServerService
30 protected videoService: VideoService 30 protected videoService: VideoService
31 protected videoCaptionService: VideoCaptionService 31 protected videoCaptionService: VideoCaptionService
32 32
33 abstract canDeactivate () 33 abstract canDeactivate (): CanComponentDeactivateResult
34 34
35 ngOnInit () { 35 ngOnInit () {
36 this.buildForm({}) 36 this.buildForm({})
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
index fa57c8cb5..b252cd60a 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
@@ -1,12 +1,12 @@
1<div *ngIf="!isUploadingVideo" class="upload-video-container"> 1<div *ngIf="!isUploadingVideo" class="upload-video-container">
2 <div class="upload-video"> 2 <div class="first-step-block">
3 <div class="icon icon-upload"></div> 3 <my-global-icon class="upload-icon" iconName="upload"></my-global-icon>
4 4
5 <div class="button-file"> 5 <div class="button-file">
6 <span i18n>Select the file to upload</span> 6 <span i18n>Select the file to upload</span>
7 <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" /> 7 <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" />
8 </div> 8 </div>
9 <span class="button-file-extension">(.mp4, .webm, .ogv)</span> 9 <span class="button-file-extension">({{ videoExtensions }})</span>
10 10
11 <div class="form-group form-group-channel"> 11 <div class="form-group form-group-channel">
12 <label i18n for="first-step-channel">Channel</label> 12 <label i18n for="first-step-channel">Channel</label>
@@ -29,7 +29,7 @@
29 </div> 29 </div>
30</div> 30</div>
31 31
32<div *ngIf="isUploadingVideo" class="upload-progress-cancel"> 32<div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel">
33 <p-progressBar 33 <p-progressBar
34 [value]="videoUploadPercents" 34 [value]="videoUploadPercents"
35 [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }" 35 [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }"
@@ -37,11 +37,21 @@
37 <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" /> 37 <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" />
38</div> 38</div>
39 39
40<div *ngIf="error" class="alert alert-danger">
41 <div i18n>Sorry, but something went wrong</div>
42 {{ error }}
43</div>
44
45<div *ngIf="videoUploaded && !error" class="alert alert-info" i18n>
46 Congratulations! Your video is now available in your private library.
47</div>
48
40<!-- Hidden because we want to load the component --> 49<!-- Hidden because we want to load the component -->
41<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form"> 50<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form">
42 <my-video-edit 51 <my-video-edit
43 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" 52 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
44 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" 53 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
54 [waitTranscodingEnabled]="waitTranscodingEnabled"
45 ></my-video-edit> 55 ></my-video-edit>
46 56
47 <div class="submit-container"> 57 <div class="submit-container">
@@ -51,8 +61,8 @@
51 (click)="updateSecondStep()" 61 (click)="updateSecondStep()"
52 [ngClass]="{ disabled: isPublishingButtonDisabled() }" 62 [ngClass]="{ disabled: isPublishingButtonDisabled() }"
53 > 63 >
54 <span class="icon icon-validate"></span> 64 <my-global-icon iconName="validate"></my-global-icon>
55 <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" /> 65 <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" />
56 </div> 66 </div>
57 </div> 67 </div>
58</form> \ No newline at end of file 68</form>
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
index dbae5230d..8adf8f169 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
@@ -1,39 +1,9 @@
1@import 'variables'; 1@import 'variables';
2@import 'mixins'; 2@import 'mixins';
3 3
4.peertube-select-container { 4.first-step-block .form-group-channel {
5 @include peertube-select-container(190px); 5 margin-bottom: 20px;
6} 6 margin-top: 35px;
7
8.upload-video {
9 display: flex;
10 flex-direction: column;
11 align-items: center;
12
13 .form-group-channel {
14 margin-bottom: 20px;
15 margin-top: 35px;
16 }
17
18 .icon.icon-upload {
19 @include icon(90px);
20 margin-bottom: 25px;
21 cursor: default;
22
23 background-image: url('../../../../assets/images/video/upload.svg');
24 }
25
26 .button-file {
27 @include peertube-button-file(auto);
28
29 min-width: 190px;
30 }
31
32 .button-file-extension {
33 display: block;
34 font-size: 12px;
35 margin-top: 5px;
36 }
37} 7}
38 8
39.upload-progress-cancel { 9.upload-progress-cancel {
@@ -46,9 +16,7 @@
46 16
47 /deep/ .ui-progressbar { 17 /deep/ .ui-progressbar {
48 font-size: 15px !important; 18 font-size: 15px !important;
49 color: #fff !important;
50 height: 30px !important; 19 height: 30px !important;
51 line-height: 30px !important;
52 border-radius: 3px !important; 20 border-radius: 3px !important;
53 background-color: rgba(11, 204, 41, 0.16) !important; 21 background-color: rgba(11, 204, 41, 0.16) !important;
54 22
@@ -60,6 +28,8 @@
60 text-align: left; 28 text-align: left;
61 padding-left: 18px; 29 padding-left: 18px;
62 margin-top: 0 !important; 30 margin-top: 0 !important;
31 color: #fff !important;
32 line-height: 30px !important;
63 } 33 }
64 } 34 }
65 35
@@ -82,4 +52,4 @@
82 52
83 margin-left: 10px; 53 margin-left: 10px;
84 } 54 }
85} \ No newline at end of file 55}
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
index ac6c1786f..9cadf52cb 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
@@ -1,12 +1,11 @@
1import { HttpEventType, HttpResponse } from '@angular/common/http' 1import { HttpEventType, HttpResponse } from '@angular/common/http'
2import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' 2import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
3import { Router } from '@angular/router' 3import { Router } from '@angular/router'
4import { LoadingBarService } from '@ngx-loading-bar/core' 4import { LoadingBarService } from '@ngx-loading-bar/core'
5import { NotificationsService } from 'angular2-notifications'
6import { BytesPipe } from 'ngx-pipes' 5import { BytesPipe } from 'ngx-pipes'
7import { Subscription } from 'rxjs' 6import { Subscription } from 'rxjs'
8import { VideoPrivacy } from '../../../../../../shared/models/videos' 7import { VideoPrivacy } from '../../../../../../shared/models/videos'
9import { AuthService, ServerService } from '../../../core' 8import { AuthService, Notifier, ServerService } from '../../../core'
10import { VideoEdit } from '../../../shared/video/video-edit.model' 9import { VideoEdit } from '../../../shared/video/video-edit.model'
11import { VideoService } from '../../../shared/video/video.service' 10import { VideoService } from '../../../shared/video/video.service'
12import { I18n } from '@ngx-translate/i18n-polyfill' 11import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -14,18 +13,21 @@ import { VideoSend } from '@app/videos/+video-edit/video-add-components/video-se
14import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' 13import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
15import { FormValidatorService, UserService } from '@app/shared' 14import { FormValidatorService, UserService } from '@app/shared'
16import { VideoCaptionService } from '@app/shared/video-caption' 15import { VideoCaptionService } from '@app/shared/video-caption'
16import { scrollToTop } from '@app/shared/misc/utils'
17 17
18@Component({ 18@Component({
19 selector: 'my-video-upload', 19 selector: 'my-video-upload',
20 templateUrl: './video-upload.component.html', 20 templateUrl: './video-upload.component.html',
21 styleUrls: [ 21 styleUrls: [
22 '../shared/video-edit.component.scss', 22 '../shared/video-edit.component.scss',
23 './video-upload.component.scss' 23 './video-upload.component.scss',
24 './video-send.scss'
24 ] 25 ]
25}) 26})
26export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { 27export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate {
27 @Output() firstStepDone = new EventEmitter<string>() 28 @Output() firstStepDone = new EventEmitter<string>()
28 @ViewChild('videofileInput') videofileInput 29 @Output() firstStepError = new EventEmitter<void>()
30 @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
29 31
30 // So that it can be accessed in the template 32 // So that it can be accessed in the template
31 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY 33 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
@@ -42,13 +44,16 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
42 id: 0, 44 id: 0,
43 uuid: '' 45 uuid: ''
44 } 46 }
47 waitTranscodingEnabled = true
48
49 error: string
45 50
46 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC 51 protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
47 52
48 constructor ( 53 constructor (
49 protected formValidatorService: FormValidatorService, 54 protected formValidatorService: FormValidatorService,
50 protected loadingBar: LoadingBarService, 55 protected loadingBar: LoadingBarService,
51 protected notificationsService: NotificationsService, 56 protected notifier: Notifier,
52 protected authService: AuthService, 57 protected authService: AuthService,
53 protected serverService: ServerService, 58 protected serverService: ServerService,
54 protected videoService: VideoService, 59 protected videoService: VideoService,
@@ -105,20 +110,15 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
105 this.isUploadingVideo = false 110 this.isUploadingVideo = false
106 this.videoUploadPercents = 0 111 this.videoUploadPercents = 0
107 this.videoUploadObservable = null 112 this.videoUploadObservable = null
108 this.notificationsService.info(this.i18n('Info'), this.i18n('Upload cancelled')) 113 this.notifier.info(this.i18n('Upload cancelled'))
109 } 114 }
110 } 115 }
111 116
112 uploadFirstStep () { 117 uploadFirstStep () {
113 const videofile = this.videofileInput.nativeElement.files[0] as File 118 const videofile = this.videofileInput.nativeElement.files[0]
114 if (!videofile) return 119 if (!videofile) return
115 120
116 // Cannot upload videos > 8GB for now 121 // Check global user quota
117 if (videofile.size > 8 * 1024 * 1024 * 1024) {
118 this.notificationsService.error(this.i18n('Error'), this.i18n('We are sorry but PeerTube cannot handle videos > 8GB'))
119 return
120 }
121
122 const bytePipes = new BytesPipe() 122 const bytePipes = new BytesPipe()
123 const videoQuota = this.authService.getUser().videoQuota 123 const videoQuota = this.authService.getUser().videoQuota
124 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) { 124 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
@@ -130,10 +130,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
130 videoQuota: bytePipes.transform(videoQuota, 0) 130 videoQuota: bytePipes.transform(videoQuota, 0)
131 } 131 }
132 ) 132 )
133 this.notificationsService.error(this.i18n('Error'), msg) 133 this.notifier.error(msg)
134 return 134 return
135 } 135 }
136 136
137 // Check daily user quota
137 const videoQuotaDaily = this.authService.getUser().videoQuotaDaily 138 const videoQuotaDaily = this.authService.getUser().videoQuotaDaily
138 if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) { 139 if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) {
139 const msg = this.i18n( 140 const msg = this.i18n(
@@ -144,10 +145,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
144 quotaDaily: bytePipes.transform(videoQuotaDaily, 0) 145 quotaDaily: bytePipes.transform(videoQuotaDaily, 0)
145 } 146 }
146 ) 147 )
147 this.notificationsService.error(this.i18n('Error'), msg) 148 this.notifier.error(msg)
148 return 149 return
149 } 150 }
150 151
152 // Build name field
151 const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '') 153 const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '')
152 let name: string 154 let name: string
153 155
@@ -155,6 +157,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
155 if (nameWithoutExtension.length < 3) name = videofile.name 157 if (nameWithoutExtension.length < 3) name = videofile.name
156 else name = nameWithoutExtension 158 else name = nameWithoutExtension
157 159
160 // Force user to wait transcoding for unsupported video types in web browsers
161 if (!videofile.name.endsWith('.mp4') && !videofile.name.endsWith('.webm') && !videofile.name.endsWith('.ogv')) {
162 this.waitTranscodingEnabled = false
163 }
164
158 const privacy = this.firstStepPrivacyId.toString() 165 const privacy = this.firstStepPrivacyId.toString()
159 const nsfw = false 166 const nsfw = false
160 const waitTranscoding = true 167 const waitTranscoding = true
@@ -203,7 +210,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
203 this.isUploadingVideo = false 210 this.isUploadingVideo = false
204 this.videoUploadPercents = 0 211 this.videoUploadPercents = 0
205 this.videoUploadObservable = null 212 this.videoUploadObservable = null
206 this.notificationsService.error(this.i18n('Error'), err.message) 213 this.firstStepError.emit()
214 this.notifier.error(err.message)
207 } 215 }
208 ) 216 )
209 } 217 }
@@ -232,13 +240,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
232 this.isUpdatingVideo = false 240 this.isUpdatingVideo = false
233 this.isUploadingVideo = false 241 this.isUploadingVideo = false
234 242
235 this.notificationsService.success(this.i18n('Success'), this.i18n('Video published.')) 243 this.notifier.success(this.i18n('Video published.'))
236 this.router.navigate([ '/videos/watch', video.uuid ]) 244 this.router.navigate([ '/videos/watch', video.uuid ])
237 }, 245 },
238 246
239 err => { 247 err => {
240 this.isUpdatingVideo = false 248 this.error = err.message
241 this.notificationsService.error(this.i18n('Error'), err.message) 249 scrollToTop()
242 console.error(err) 250 console.error(err)
243 } 251 }
244 ) 252 )
diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html
index e14e23aed..72a233b72 100644
--- a/client/src/app/videos/+video-edit/video-add.component.html
+++ b/client/src/app/videos/+video-edit/video-add.component.html
@@ -6,24 +6,24 @@
6 6
7 <ngb-tabset class="video-add-tabset root-tabset bootstrap" [ngClass]="{ 'hide-nav': secondStepType !== undefined }"> 7 <ngb-tabset class="video-add-tabset root-tabset bootstrap" [ngClass]="{ 'hide-nav': secondStepType !== undefined }">
8 8
9 <ngb-tab i18n-title title=""> 9 <ngb-tab>
10 <ng-template ngbTabTitle><span i18n>Upload a file</span></ng-template> 10 <ng-template ngbTabTitle><span i18n>Upload a file</span></ng-template>
11 <ng-template ngbTabContent> 11 <ng-template ngbTabContent>
12 <my-video-upload #videoUpload (firstStepDone)="onFirstStepDone('upload', $event)"></my-video-upload> 12 <my-video-upload #videoUpload (firstStepDone)="onFirstStepDone('upload', $event)" (firstStepError)="onError()"></my-video-upload>
13 </ng-template> 13 </ng-template>
14 </ngb-tab> 14 </ngb-tab>
15 15
16 <ngb-tab *ngIf="isVideoImportHttpEnabled()"> 16 <ngb-tab *ngIf="isVideoImportHttpEnabled()">
17 <ng-template ngbTabTitle><span i18n>Import with URL</span></ng-template> 17 <ng-template ngbTabTitle><span i18n>Import with URL</span></ng-template>
18 <ng-template ngbTabContent> 18 <ng-template ngbTabContent>
19 <my-video-import-url #videoImportUrl (firstStepDone)="onFirstStepDone('import-url', $event)"></my-video-import-url> 19 <my-video-import-url #videoImportUrl (firstStepDone)="onFirstStepDone('import-url', $event)" (firstStepError)="onError()"></my-video-import-url>
20 </ng-template> 20 </ng-template>
21 </ngb-tab> 21 </ngb-tab>
22 22
23 <ngb-tab *ngIf="isVideoImportTorrentEnabled()"> 23 <ngb-tab *ngIf="isVideoImportTorrentEnabled()">
24 <ng-template ngbTabTitle><span i18n>Import with torrent</span></ng-template> 24 <ng-template ngbTabTitle><span i18n>Import with torrent</span></ng-template>
25 <ng-template ngbTabContent> 25 <ng-template ngbTabContent>
26 <my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)"></my-video-import-torrent> 26 <my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)" (firstStepError)="onError()"></my-video-import-torrent>
27 </ng-template> 27 </ng-template>
28 </ngb-tab> 28 </ngb-tab>
29 </ngb-tabset> 29 </ngb-tabset>
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts
index 1a9247dbe..01fdfcb66 100644
--- a/client/src/app/videos/+video-edit/video-add.component.ts
+++ b/client/src/app/videos/+video-edit/video-add.component.ts
@@ -1,4 +1,4 @@
1import { Component, ViewChild } from '@angular/core' 1import { Component, HostListener, ViewChild } from '@angular/core'
2import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' 2import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
3import { VideoImportUrlComponent } from '@app/videos/+video-edit/video-add-components/video-import-url.component' 3import { VideoImportUrlComponent } from '@app/videos/+video-edit/video-add-components/video-import-url.component'
4import { VideoUploadComponent } from '@app/videos/+video-edit/video-add-components/video-upload.component' 4import { VideoUploadComponent } from '@app/videos/+video-edit/video-add-components/video-upload.component'
@@ -27,7 +27,22 @@ export class VideoAddComponent implements CanComponentDeactivate {
27 this.videoName = videoName 27 this.videoName = videoName
28 } 28 }
29 29
30 canDeactivate () { 30 onError () {
31 this.videoName = undefined
32 this.secondStepType = undefined
33 }
34
35 @HostListener('window:beforeunload', [ '$event' ])
36 onUnload (event: any) {
37 const { text, canDeactivate } = this.canDeactivate()
38
39 if (canDeactivate) return
40
41 event.returnValue = text
42 return text
43 }
44
45 canDeactivate (): { canDeactivate: boolean, text?: string} {
31 if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate() 46 if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate()
32 if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate() 47 if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate()
33 if (this.secondStepType === 'import-torrent') return this.videoImportTorrent.canDeactivate() 48 if (this.secondStepType === 'import-torrent') return this.videoImportTorrent.canDeactivate()
diff --git a/client/src/app/videos/+video-edit/video-update.component.html b/client/src/app/videos/+video-edit/video-update.component.html
index 9242c30a0..4992bb369 100644
--- a/client/src/app/videos/+video-edit/video-update.component.html
+++ b/client/src/app/videos/+video-edit/video-update.component.html
@@ -8,12 +8,12 @@
8 <my-video-edit 8 <my-video-edit
9 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible" 9 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible"
10 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" 10 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
11 [videoCaptions]="videoCaptions" 11 [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled"
12 ></my-video-edit> 12 ></my-video-edit>
13 13
14 <div class="submit-container"> 14 <div class="submit-container">
15 <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"> 15 <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }">
16 <span class="icon icon-validate"></span> 16 <my-global-icon iconName="validate"></my-global-icon>
17 <input type="button" i18n-value value="Update" /> 17 <input type="button" i18n-value value="Update" />
18 </div> 18 </div>
19 </div> 19 </div>
diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts
index 3a0f3a39a..9e849014e 100644
--- a/client/src/app/videos/+video-edit/video-update.component.ts
+++ b/client/src/app/videos/+video-edit/video-update.component.ts
@@ -1,8 +1,8 @@
1import { map, switchMap } from 'rxjs/operators' 1import { map, switchMap } from 'rxjs/operators'
2import { Component, OnInit } from '@angular/core' 2import { Component, HostListener, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { LoadingBarService } from '@ngx-loading-bar/core' 4import { LoadingBarService } from '@ngx-loading-bar/core'
5import { NotificationsService } from 'angular2-notifications' 5import { Notifier } from '@app/core'
6import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos' 6import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos'
7import { ServerService } from '../../core' 7import { ServerService } from '../../core'
8import { FormReactive } from '../../shared' 8import { FormReactive } from '../../shared'
@@ -12,6 +12,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
12import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 12import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
13import { VideoCaptionService } from '@app/shared/video-caption' 13import { VideoCaptionService } from '@app/shared/video-caption'
14import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' 14import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
15import { VideoDetails } from '@app/shared/video/video-details.model'
15 16
16@Component({ 17@Component({
17 selector: 'my-videos-update', 18 selector: 'my-videos-update',
@@ -26,6 +27,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
26 userVideoChannels: { id: number, label: string, support: string }[] = [] 27 userVideoChannels: { id: number, label: string, support: string }[] = []
27 schedulePublicationPossible = false 28 schedulePublicationPossible = false
28 videoCaptions: VideoCaptionEdit[] = [] 29 videoCaptions: VideoCaptionEdit[] = []
30 waitTranscodingEnabled = true
29 31
30 private updateDone = false 32 private updateDone = false
31 33
@@ -33,7 +35,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
33 protected formValidatorService: FormValidatorService, 35 protected formValidatorService: FormValidatorService,
34 private route: ActivatedRoute, 36 private route: ActivatedRoute,
35 private router: Router, 37 private router: Router,
36 private notificationsService: NotificationsService, 38 private notifier: Notifier,
37 private serverService: ServerService, 39 private serverService: ServerService,
38 private videoService: VideoService, 40 private videoService: VideoService,
39 private loadingBar: LoadingBarService, 41 private loadingBar: LoadingBarService,
@@ -65,25 +67,42 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
65 67
66 this.videoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies) 68 this.videoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies)
67 69
70 const videoFiles = (video as VideoDetails).files
71 if (videoFiles.length > 1) { // Already transcoded
72 this.waitTranscodingEnabled = false
73 }
74
68 // FIXME: Angular does not detect the change inside this subscription, so use the patched setTimeout 75 // FIXME: Angular does not detect the change inside this subscription, so use the patched setTimeout
69 setTimeout(() => this.hydrateFormFromVideo()) 76 setTimeout(() => this.hydrateFormFromVideo())
70 }, 77 },
71 78
72 err => { 79 err => {
73 console.error(err) 80 console.error(err)
74 this.notificationsService.error(this.i18n('Error'), err.message) 81 this.notifier.error(err.message)
75 } 82 }
76 ) 83 )
77 } 84 }
78 85
79 canDeactivate () { 86 @HostListener('window:beforeunload', [ '$event' ])
87 onUnload (event: any) {
88 const { text, canDeactivate } = this.canDeactivate()
89
90 if (canDeactivate) return
91
92 event.returnValue = text
93 return text
94 }
95
96 canDeactivate (): { canDeactivate: boolean, text?: string } {
80 if (this.updateDone === true) return { canDeactivate: true } 97 if (this.updateDone === true) return { canDeactivate: true }
81 98
99 const text = this.i18n('You have unsaved changes! If you leave, your changes will be lost.')
100
82 for (const caption of this.videoCaptions) { 101 for (const caption of this.videoCaptions) {
83 if (caption.action) return { canDeactivate: false } 102 if (caption.action) return { canDeactivate: false, text }
84 } 103 }
85 104
86 return { canDeactivate: this.formChanged === false } 105 return { canDeactivate: this.formChanged === false, text }
87 } 106 }
88 107
89 checkForm () { 108 checkForm () {
@@ -114,14 +133,14 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
114 this.updateDone = true 133 this.updateDone = true
115 this.isUpdatingVideo = false 134 this.isUpdatingVideo = false
116 this.loadingBar.complete() 135 this.loadingBar.complete()
117 this.notificationsService.success(this.i18n('Success'), this.i18n('Video updated.')) 136 this.notifier.success(this.i18n('Video updated.'))
118 this.router.navigate([ '/videos/watch', this.video.uuid ]) 137 this.router.navigate([ '/videos/watch', this.video.uuid ])
119 }, 138 },
120 139
121 err => { 140 err => {
122 this.loadingBar.complete() 141 this.loadingBar.complete()
123 this.isUpdatingVideo = false 142 this.isUpdatingVideo = false
124 this.notificationsService.error(this.i18n('Error'), err.message) 143 this.notifier.error(err.message)
125 console.error(err) 144 console.error(err)
126 } 145 }
127 ) 146 )
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.html b/client/src/app/videos/+video-watch/comment/video-comment-add.component.html
index b58a56596..d8a7a78c4 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.html
+++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.html
@@ -3,7 +3,7 @@
3 <img [src]="getAvatarUrl()" alt="Avatar" /> 3 <img [src]="getAvatarUrl()" alt="Avatar" />
4 4
5 <div class="form-group"> 5 <div class="form-group">
6 <textarea i18n-placeholder placeholder="Add comment..." autosize 6 <textarea i18n-placeholder placeholder="Add comment..." myAutoResize
7 [readonly]="(user === null) ? true : false" 7 [readonly]="(user === null) ? true : false"
8 (click)="openVisitorModal($event)" 8 (click)="openVisitorModal($event)"
9 formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }" 9 formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }"
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
index fb7de0e04..fd85c28f2 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
@@ -1,6 +1,6 @@
1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications' 3import { Notifier } from '@app/core'
4import { Observable } from 'rxjs' 4import { Observable } from 'rxjs'
5import { VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model' 5import { VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model'
6import { FormReactive } from '../../../shared' 6import { FormReactive } from '../../../shared'
@@ -29,14 +29,14 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
29 @Output() commentCreated = new EventEmitter<VideoCommentCreate>() 29 @Output() commentCreated = new EventEmitter<VideoCommentCreate>()
30 30
31 @ViewChild('visitorModal') visitorModal: NgbModal 31 @ViewChild('visitorModal') visitorModal: NgbModal
32 @ViewChild('textarea') private textareaElement: ElementRef 32 @ViewChild('textarea') textareaElement: ElementRef
33 33
34 private addingComment = false 34 addingComment = false
35 35
36 constructor ( 36 constructor (
37 protected formValidatorService: FormValidatorService, 37 protected formValidatorService: FormValidatorService,
38 private videoCommentValidatorsService: VideoCommentValidatorsService, 38 private videoCommentValidatorsService: VideoCommentValidatorsService,
39 private notificationsService: NotificationsService, 39 private notifier: Notifier,
40 private videoCommentService: VideoCommentService, 40 private videoCommentService: VideoCommentService,
41 private authService: AuthService, 41 private authService: AuthService,
42 private modalService: NgbModal, 42 private modalService: NgbModal,
@@ -70,13 +70,13 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
70 } 70 }
71 71
72 onValidKey () { 72 onValidKey () {
73 this.onValueChanged() 73 this.check()
74 if (!this.form.valid) return 74 if (!this.form.valid) return
75 75
76 this.formValidated() 76 this.formValidated()
77 } 77 }
78 78
79 openVisitorModal (event) { 79 openVisitorModal (event: any) {
80 if (this.user === null) { // we only open it for visitors 80 if (this.user === null) { // we only open it for visitors
81 // fixing ng-bootstrap ModalService and the "Expression Changed After It Has Been Checked" Error 81 // fixing ng-bootstrap ModalService and the "Expression Changed After It Has Been Checked" Error
82 event.srcElement.blur() 82 event.srcElement.blur()
@@ -115,7 +115,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
115 err => { 115 err => {
116 this.addingComment = false 116 this.addingComment = false
117 117
118 this.notificationsService.error(this.i18n('Error'), err.text) 118 this.notifier.error(err.text)
119 } 119 }
120 ) 120 )
121 } 121 }
@@ -135,7 +135,6 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
135 135
136 gotoLogin () { 136 gotoLogin () {
137 this.hideVisitorModal() 137 this.hideVisitorModal()
138 this.authService.redirectUrl = this.router.url
139 this.router.navigate([ '/login' ]) 138 this.router.navigate([ '/login' ])
140 } 139 }
141 140
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.scss b/client/src/app/videos/+video-watch/comment/video-comment.component.scss
index 84da5727e..731ecbf8f 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.component.scss
+++ b/client/src/app/videos/+video-watch/comment/video-comment.component.scss
@@ -41,7 +41,7 @@
41 } 41 }
42 42
43 .comment-date { 43 .comment-date {
44 color: #585858; 44 color: $grey-foreground-color;
45 margin-left: 10px; 45 margin-left: 10px;
46 } 46 }
47 } 47 }
@@ -69,7 +69,7 @@
69 69
70 .comment-action-reply, 70 .comment-action-reply,
71 .comment-action-delete { 71 .comment-action-delete {
72 color: #585858; 72 color: $grey-foreground-color;
73 cursor: pointer; 73 cursor: pointer;
74 margin-right: 10px; 74 margin-right: 10px;
75 75
@@ -108,4 +108,4 @@
108 .root-comment { 108 .root-comment {
109 font-size: 14px; 109 font-size: 14px;
110 } 110 }
111} \ No newline at end of file 111}
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.ts b/client/src/app/videos/+video-watch/comment/video-comment.component.ts
index e90008de9..aba7f9d1c 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment.component.ts
@@ -1,11 +1,10 @@
1import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'
2import { LinkifierService } from '@app/videos/+video-watch/comment/linkifier.service'
3import * as sanitizeHtml from 'sanitize-html'
4import { UserRight } from '../../../../../../shared/models/users' 2import { UserRight } from '../../../../../../shared/models/users'
5import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' 3import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model'
6import { AuthService } from '../../../core/auth' 4import { AuthService } from '../../../core/auth'
7import { Video } from '../../../shared/video/video.model' 5import { Video } from '../../../shared/video/video.model'
8import { VideoComment } from './video-comment.model' 6import { VideoComment } from './video-comment.model'
7import { HtmlRendererService } from '@app/shared/renderer'
9 8
10@Component({ 9@Component({
11 selector: 'my-video-comment', 10 selector: 'my-video-comment',
@@ -26,10 +25,10 @@ export class VideoCommentComponent implements OnInit, OnChanges {
26 @Output() resetReply = new EventEmitter() 25 @Output() resetReply = new EventEmitter()
27 26
28 sanitizedCommentHTML = '' 27 sanitizedCommentHTML = ''
29 newParentComments = [] 28 newParentComments: VideoComment[] = []
30 29
31 constructor ( 30 constructor (
32 private linkifierService: LinkifierService, 31 private htmlRenderer: HtmlRendererService,
33 private authService: AuthService 32 private authService: AuthService
34 ) {} 33 ) {}
35 34
@@ -87,27 +86,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
87 } 86 }
88 87
89 private init () { 88 private init () {
90 // Convert possible markdown to html 89 this.sanitizedCommentHTML = this.htmlRenderer.toSafeHtml(this.comment.text)
91 const html = this.linkifierService.linkify(this.comment.text)
92
93 this.sanitizedCommentHTML = sanitizeHtml(html, {
94 allowedTags: [ 'a', 'p', 'span', 'br' ],
95 allowedSchemes: [ 'http', 'https' ],
96 allowedAttributes: {
97 'a': [ 'href', 'class', 'target' ]
98 },
99 transformTags: {
100 a: (tagName, attribs) => {
101 return {
102 tagName,
103 attribs: Object.assign(attribs, {
104 target: '_blank',
105 rel: 'noopener noreferrer'
106 })
107 }
108 }
109 }
110 })
111 90
112 this.newParentComments = this.parentComments.concat([ this.comment ]) 91 this.newParentComments = this.parentComments.concat([ this.comment ])
113 } 92 }
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.model.ts b/client/src/app/videos/+video-watch/comment/video-comment.model.ts
index fe591811e..824fb24c3 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.model.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment.model.ts
@@ -14,7 +14,7 @@ export class VideoComment implements VideoCommentServerModel {
14 account: AccountInterface 14 account: AccountInterface
15 totalReplies: number 15 totalReplies: number
16 by: string 16 by: string
17 accountAvatarUrl 17 accountAvatarUrl: string
18 18
19 constructor (hash: VideoCommentServerModel) { 19 constructor (hash: VideoCommentServerModel) {
20 this.id = hash.id 20 this.id = hash.id
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.service.ts b/client/src/app/videos/+video-watch/comment/video-comment.service.ts
index 9bcb4b7de..b8e5878c5 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.service.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment.service.ts
@@ -1,7 +1,7 @@
1import { catchError, map } from 'rxjs/operators' 1import { catchError, map } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http' 2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { lineFeedToHtml } from '@app/shared/misc/utils' 4import { objectLineFeedToHtml } from '@app/shared/misc/utils'
5import { Observable } from 'rxjs' 5import { Observable } from 'rxjs'
6import { ResultList, FeedFormat } from '../../../../../../shared/models' 6import { ResultList, FeedFormat } from '../../../../../../shared/models'
7import { 7import {
@@ -28,22 +28,22 @@ export class VideoCommentService {
28 28
29 addCommentThread (videoId: number | string, comment: VideoCommentCreate) { 29 addCommentThread (videoId: number | string, comment: VideoCommentCreate) {
30 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' 30 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads'
31 const normalizedComment = lineFeedToHtml(comment, 'text') 31 const normalizedComment = objectLineFeedToHtml(comment, 'text')
32 32
33 return this.authHttp.post(url, normalizedComment) 33 return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment)
34 .pipe( 34 .pipe(
35 map(data => this.extractVideoComment(data['comment'])), 35 map(data => this.extractVideoComment(data.comment)),
36 catchError(err => this.restExtractor.handleError(err)) 36 catchError(err => this.restExtractor.handleError(err))
37 ) 37 )
38 } 38 }
39 39
40 addCommentReply (videoId: number | string, inReplyToCommentId: number, comment: VideoCommentCreate) { 40 addCommentReply (videoId: number | string, inReplyToCommentId: number, comment: VideoCommentCreate) {
41 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId 41 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId
42 const normalizedComment = lineFeedToHtml(comment, 'text') 42 const normalizedComment = objectLineFeedToHtml(comment, 'text')
43 43
44 return this.authHttp.post(url, normalizedComment) 44 return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment)
45 .pipe( 45 .pipe(
46 map(data => this.extractVideoComment(data[ 'comment' ])), 46 map(data => this.extractVideoComment(data.comment)),
47 catchError(err => this.restExtractor.handleError(err)) 47 catchError(err => this.restExtractor.handleError(err))
48 ) 48 )
49 } 49 }
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.html b/client/src/app/videos/+video-watch/comment/video-comments.component.html
index 42e129d65..44016d8ad 100644
--- a/client/src/app/videos/+video-watch/comment/video-comments.component.html
+++ b/client/src/app/videos/+video-watch/comment/video-comments.component.html
@@ -4,7 +4,7 @@
4 Comments 4 Comments
5 </div> 5 </div>
6 6
7 <my-video-feed [syndicationItems]="syndicationItems"></my-video-feed> 7 <my-feed [syndicationItems]="syndicationItems"></my-feed>
8 </div> 8 </div>
9 9
10 <ng-template [ngIf]="video.commentsEnabled === true"> 10 <ng-template [ngIf]="video.commentsEnabled === true">
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.scss b/client/src/app/videos/+video-watch/comment/video-comments.component.scss
index dbb44c66c..575e331e4 100644
--- a/client/src/app/videos/+video-watch/comment/video-comments.component.scss
+++ b/client/src/app/videos/+video-watch/comment/video-comments.component.scss
@@ -23,7 +23,7 @@
23 margin-right: 0; 23 margin-right: 0;
24} 24}
25 25
26my-video-feed { 26my-feed {
27 display: inline-block; 27 display: inline-block;
28 margin-left: 5px; 28 margin-left: 5px;
29} 29}
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts
index c864d82b7..2616820d2 100644
--- a/client/src/app/videos/+video-watch/comment/video-comments.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comments.component.ts
@@ -1,17 +1,17 @@
1import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild, ElementRef } from '@angular/core' 1import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core'
2import { ActivatedRoute } from '@angular/router' 2import { ActivatedRoute } from '@angular/router'
3import { ConfirmService } from '@app/core' 3import { ConfirmService, Notifier } from '@app/core'
4import { NotificationsService } from 'angular2-notifications'
5import { Subscription } from 'rxjs' 4import { Subscription } from 'rxjs'
6import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' 5import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model'
7import { AuthService } from '../../../core/auth' 6import { AuthService } from '../../../core/auth'
8import { ComponentPagination } from '../../../shared/rest/component-pagination.model' 7import { ComponentPagination, hasMoreItems } from '../../../shared/rest/component-pagination.model'
9import { User } from '../../../shared/users' 8import { User } from '../../../shared/users'
10import { VideoSortField } from '../../../shared/video/sort-field.type' 9import { VideoSortField } from '../../../shared/video/sort-field.type'
11import { VideoDetails } from '../../../shared/video/video-details.model' 10import { VideoDetails } from '../../../shared/video/video-details.model'
12import { VideoComment } from './video-comment.model' 11import { VideoComment } from './video-comment.model'
13import { VideoCommentService } from './video-comment.service' 12import { VideoCommentService } from './video-comment.service'
14import { I18n } from '@ngx-translate/i18n-polyfill' 13import { I18n } from '@ngx-translate/i18n-polyfill'
14import { Syndication } from '@app/shared/video/syndication.model'
15 15
16@Component({ 16@Component({
17 selector: 'my-video-comments', 17 selector: 'my-video-comments',
@@ -35,13 +35,13 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
35 threadComments: { [ id: number ]: VideoCommentThreadTree } = {} 35 threadComments: { [ id: number ]: VideoCommentThreadTree } = {}
36 threadLoading: { [ id: number ]: boolean } = {} 36 threadLoading: { [ id: number ]: boolean } = {}
37 37
38 syndicationItems = [] 38 syndicationItems: Syndication[] = []
39 39
40 private sub: Subscription 40 private sub: Subscription
41 41
42 constructor ( 42 constructor (
43 private authService: AuthService, 43 private authService: AuthService,
44 private notificationsService: NotificationsService, 44 private notifier: Notifier,
45 private confirmService: ConfirmService, 45 private confirmService: ConfirmService,
46 private videoCommentService: VideoCommentService, 46 private videoCommentService: VideoCommentService,
47 private activatedRoute: ActivatedRoute, 47 private activatedRoute: ActivatedRoute,
@@ -83,15 +83,11 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
83 this.highlightedThread = new VideoComment(res.comment) 83 this.highlightedThread = new VideoComment(res.comment)
84 84
85 // Scroll to the highlighted thread 85 // Scroll to the highlighted thread
86 setTimeout(() => { 86 setTimeout(() => this.commentHighlightBlock.nativeElement.scrollIntoView(), 0)
87 // -60 because of the fixed header
88 const scrollY = this.commentHighlightBlock.nativeElement.offsetTop - 60
89 window.scroll(0, scrollY)
90 }, 500)
91 } 87 }
92 }, 88 },
93 89
94 err => this.notificationsService.error(this.i18n('Error'), err.message) 90 err => this.notifier.error(err.message)
95 ) 91 )
96 } 92 }
97 93
@@ -103,7 +99,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
103 this.componentPagination.totalItems = res.totalComments 99 this.componentPagination.totalItems = res.totalComments
104 }, 100 },
105 101
106 err => this.notificationsService.error(this.i18n('Error'), err.message) 102 err => this.notifier.error(err.message)
107 ) 103 )
108 } 104 }
109 105
@@ -154,7 +150,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
154 if (this.highlightedThread.id === commentToDelete.id) this.highlightedThread = undefined 150 if (this.highlightedThread.id === commentToDelete.id) this.highlightedThread = undefined
155 }, 151 },
156 152
157 err => this.notificationsService.error(this.i18n('Error'), err.message) 153 err => this.notifier.error(err.message)
158 ) 154 )
159 } 155 }
160 156
@@ -165,22 +161,11 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
165 onNearOfBottom () { 161 onNearOfBottom () {
166 this.componentPagination.currentPage++ 162 this.componentPagination.currentPage++
167 163
168 if (this.hasMoreComments()) { 164 if (hasMoreItems(this.componentPagination)) {
169 this.loadMoreComments() 165 this.loadMoreComments()
170 } 166 }
171 } 167 }
172 168
173 private hasMoreComments () {
174 // No results
175 if (this.componentPagination.totalItems === 0) return false
176
177 // Not loaded yet
178 if (!this.componentPagination.totalItems) return true
179
180 const maxPage = this.componentPagination.totalItems / this.componentPagination.itemsPerPage
181 return maxPage > this.componentPagination.currentPage
182 }
183
184 private deleteLocalCommentThread (parentComment: VideoCommentThreadTree, commentToDelete: VideoComment) { 169 private deleteLocalCommentThread (parentComment: VideoCommentThreadTree, commentToDelete: VideoComment) {
185 for (const commentChild of parentComment.children) { 170 for (const commentChild of parentComment.children) {
186 if (commentChild.comment.id === commentToDelete.id) { 171 if (commentChild.comment.id === commentToDelete.id) {
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html b/client/src/app/videos/+video-watch/modal/video-blacklist.component.html
index c436501b4..1a87bdcd4 100644
--- a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-blacklist.component.html
@@ -1,7 +1,7 @@
1<ng-template #modal> 1<ng-template #modal>
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Blacklist video</h4> 3 <h4 i18n class="modal-title">Blacklist video</h4>
4 <span class="close" aria-label="Close" role="button" (click)="hide()"></span> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body"> 7 <div class="modal-body">
@@ -15,6 +15,13 @@
15 </div> 15 </div>
16 </div> 16 </div>
17 17
18 <div class="form-group" *ngIf="video.isLocal">
19 <my-peertube-checkbox
20 inputName="unfederate" formControlName="unfederate"
21 i18n-labelText labelText="Unfederate the video (ask for its deletion from the remote instances)"
22 ></my-peertube-checkbox>
23 </div>
24
18 <div class="form-group inputs"> 25 <div class="form-group inputs">
19 <span i18n class="action-button action-button-cancel" (click)="hide()"> 26 <span i18n class="action-button action-button-cancel" (click)="hide()">
20 Cancel 27 Cancel
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts b/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts
index 2c123ebed..50a7cadd1 100644
--- a/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts
@@ -1,12 +1,11 @@
1import { Component, Input, OnInit, ViewChild } from '@angular/core' 1import { Component, Input, OnInit, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier, RedirectService } from '@app/core'
3import { FormReactive, VideoBlacklistService, VideoBlacklistValidatorsService } from '../../../shared/index' 3import { FormReactive, VideoBlacklistService, VideoBlacklistValidatorsService } from '../../../shared/index'
4import { VideoDetails } from '../../../shared/video/video-details.model' 4import { VideoDetails } from '../../../shared/video/video-details.model'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
9import { RedirectService } from '@app/core'
10 9
11@Component({ 10@Component({
12 selector: 'my-video-blacklist', 11 selector: 'my-video-blacklist',
@@ -27,7 +26,7 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
27 private modalService: NgbModal, 26 private modalService: NgbModal,
28 private videoBlacklistValidatorsService: VideoBlacklistValidatorsService, 27 private videoBlacklistValidatorsService: VideoBlacklistValidatorsService,
29 private videoBlacklistService: VideoBlacklistService, 28 private videoBlacklistService: VideoBlacklistService,
30 private notificationsService: NotificationsService, 29 private notifier: Notifier,
31 private redirectService: RedirectService, 30 private redirectService: RedirectService,
32 private i18n: I18n 31 private i18n: I18n
33 ) { 32 ) {
@@ -35,9 +34,12 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
35 } 34 }
36 35
37 ngOnInit () { 36 ngOnInit () {
37 const defaultValues = { unfederate: 'true' }
38
38 this.buildForm({ 39 this.buildForm({
39 reason: this.videoBlacklistValidatorsService.VIDEO_BLACKLIST_REASON 40 reason: this.videoBlacklistValidatorsService.VIDEO_BLACKLIST_REASON,
40 }) 41 unfederate: null
42 }, defaultValues)
41 } 43 }
42 44
43 show () { 45 show () {
@@ -51,16 +53,17 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit {
51 53
52 blacklist () { 54 blacklist () {
53 const reason = this.form.value[ 'reason' ] || undefined 55 const reason = this.form.value[ 'reason' ] || undefined
56 const unfederate = this.video.isLocal ? this.form.value[ 'unfederate' ] : undefined
54 57
55 this.videoBlacklistService.blacklistVideo(this.video.id, reason) 58 this.videoBlacklistService.blacklistVideo(this.video.id, reason, unfederate)
56 .subscribe( 59 .subscribe(
57 () => { 60 () => {
58 this.notificationsService.success(this.i18n('Success'), this.i18n('Video blacklisted.')) 61 this.notifier.success(this.i18n('Video blacklisted.'))
59 this.hide() 62 this.hide()
60 this.redirectService.redirectToHomepage() 63 this.redirectService.redirectToHomepage()
61 }, 64 },
62 65
63 err => this.notificationsService.error(this.i18n('Error'), err.message) 66 err => this.notifier.error(err.message)
64 ) 67 )
65 } 68 }
66} 69}
diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.html b/client/src/app/videos/+video-watch/modal/video-download.component.html
index f46f92a17..2bb5d6d37 100644
--- a/client/src/app/videos/+video-watch/modal/video-download.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-download.component.html
@@ -1,7 +1,7 @@
1<ng-template #modal let-hide="close"> 1<ng-template #modal let-hide="close">
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Download video</h4> 3 <h4 i18n class="modal-title">Download video</h4>
4 <span class="close" aria-hidden="true" (click)="hide()"></span> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body"> 7 <div class="modal-body">
diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.ts b/client/src/app/videos/+video-watch/modal/video-download.component.ts
index b1b2c0623..834385771 100644
--- a/client/src/app/videos/+video-watch/modal/video-download.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-download.component.ts
@@ -2,7 +2,7 @@ import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'
2import { VideoDetails } from '../../../shared/video/video-details.model' 2import { VideoDetails } from '../../../shared/video/video-details.model'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { NotificationsService } from 'angular2-notifications' 5import { Notifier } from '@app/core'
6 6
7@Component({ 7@Component({
8 selector: 'my-video-download', 8 selector: 'my-video-download',
@@ -18,7 +18,7 @@ export class VideoDownloadComponent implements OnInit {
18 resolutionId: number | string = -1 18 resolutionId: number | string = -1
19 19
20 constructor ( 20 constructor (
21 private notificationsService: NotificationsService, 21 private notifier: Notifier,
22 private modalService: NgbModal, 22 private modalService: NgbModal,
23 private i18n: I18n 23 private i18n: I18n
24 ) { } 24 ) { }
@@ -63,6 +63,6 @@ export class VideoDownloadComponent implements OnInit {
63 } 63 }
64 64
65 activateCopiedMessage () { 65 activateCopiedMessage () {
66 this.notificationsService.success(this.i18n('Success'), this.i18n('Copied')) 66 this.notifier.success(this.i18n('Copied'))
67 } 67 }
68} 68}
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.html b/client/src/app/videos/+video-watch/modal/video-report.component.html
index 8d9a49276..b9434da26 100644
--- a/client/src/app/videos/+video-watch/modal/video-report.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-report.component.html
@@ -1,11 +1,16 @@
1<ng-template #modal> 1<ng-template #modal>
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Report video</h4> 3 <h4 i18n class="modal-title">Report video</h4>
4 <span class="close" aria-label="Close" role="button" (click)="hide()"></span> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body"> 7 <div class="modal-body">
8 8
9 <div i18n class="information">
10 Your report will be sent to moderators of {{ currentHost }}.
11 <ng-container *ngIf="isRemoteVideo()"> It will be forwarded to origin instance {{ originHost }} too.</ng-container>
12 </div>
13
9 <form novalidate [formGroup]="form" (ngSubmit)="report()"> 14 <form novalidate [formGroup]="form" (ngSubmit)="report()">
10 <div class="form-group"> 15 <div class="form-group">
11 <textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }"> 16 <textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }">
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.scss b/client/src/app/videos/+video-watch/modal/video-report.component.scss
index afcdb9a16..4713660a2 100644
--- a/client/src/app/videos/+video-watch/modal/video-report.component.scss
+++ b/client/src/app/videos/+video-watch/modal/video-report.component.scss
@@ -1,6 +1,10 @@
1@import 'variables'; 1@import 'variables';
2@import 'mixins'; 2@import 'mixins';
3 3
4.information {
5 margin-bottom: 20px;
6}
7
4textarea { 8textarea {
5 @include peertube-textarea(100%, 100px); 9 @include peertube-textarea(100%, 100px);
6} 10}
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.ts b/client/src/app/videos/+video-watch/modal/video-report.component.ts
index 297afb19f..911f3b447 100644
--- a/client/src/app/videos/+video-watch/modal/video-report.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-report.component.ts
@@ -1,5 +1,5 @@
1import { Component, Input, OnInit, ViewChild } from '@angular/core' 1import { Component, Input, OnInit, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { FormReactive, VideoAbuseService } from '../../../shared/index' 3import { FormReactive, VideoAbuseService } from '../../../shared/index'
4import { VideoDetails } from '../../../shared/video/video-details.model' 4import { VideoDetails } from '../../../shared/video/video-details.model'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -27,12 +27,24 @@ export class VideoReportComponent extends FormReactive implements OnInit {
27 private modalService: NgbModal, 27 private modalService: NgbModal,
28 private videoAbuseValidatorsService: VideoAbuseValidatorsService, 28 private videoAbuseValidatorsService: VideoAbuseValidatorsService,
29 private videoAbuseService: VideoAbuseService, 29 private videoAbuseService: VideoAbuseService,
30 private notificationsService: NotificationsService, 30 private notifier: Notifier,
31 private i18n: I18n 31 private i18n: I18n
32 ) { 32 ) {
33 super() 33 super()
34 } 34 }
35 35
36 get currentHost () {
37 return window.location.host
38 }
39
40 get originHost () {
41 if (this.isRemoteVideo()) {
42 return this.video.account.host
43 }
44
45 return ''
46 }
47
36 ngOnInit () { 48 ngOnInit () {
37 this.buildForm({ 49 this.buildForm({
38 reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON 50 reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON
@@ -54,11 +66,15 @@ export class VideoReportComponent extends FormReactive implements OnInit {
54 this.videoAbuseService.reportVideo(this.video.id, reason) 66 this.videoAbuseService.reportVideo(this.video.id, reason)
55 .subscribe( 67 .subscribe(
56 () => { 68 () => {
57 this.notificationsService.success(this.i18n('Success'), this.i18n('Video reported.')) 69 this.notifier.success(this.i18n('Video reported.'))
58 this.hide() 70 this.hide()
59 }, 71 },
60 72
61 err => this.notificationsService.error(this.i18n('Error'), err.message) 73 err => this.notifier.error(err.message)
62 ) 74 )
63 } 75 }
76
77 isRemoteVideo () {
78 return !this.video.isLocal
79 }
64} 80}
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.html b/client/src/app/videos/+video-watch/modal/video-share.component.html
index 301f67f2d..9f3c37fe8 100644
--- a/client/src/app/videos/+video-watch/modal/video-share.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-share.component.html
@@ -1,7 +1,7 @@
1<ng-template #modal let-hide="close"> 1<ng-template #modal let-hide="close">
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Share</h4> 3 <h4 i18n class="modal-title">Share</h4>
4 <span class="close" aria-hidden="true" (click)="hide()"></span> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body"> 7 <div class="modal-body">
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.ts b/client/src/app/videos/+video-watch/modal/video-share.component.ts
index 17e2b31e1..c6205e355 100644
--- a/client/src/app/videos/+video-watch/modal/video-share.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-share.component.ts
@@ -1,5 +1,5 @@
1import { Component, ElementRef, Input, ViewChild } from '@angular/core' 1import { Component, ElementRef, Input, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { Notifier } from '@app/core'
3import { VideoDetails } from '../../../shared/video/video-details.model' 3import { VideoDetails } from '../../../shared/video/video-details.model'
4import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils' 4import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
@@ -23,7 +23,7 @@ export class VideoShareComponent {
23 23
24 constructor ( 24 constructor (
25 private modalService: NgbModal, 25 private modalService: NgbModal,
26 private notificationsService: NotificationsService, 26 private notifier: Notifier,
27 private i18n: I18n 27 private i18n: I18n
28 ) { } 28 ) { }
29 29
@@ -49,7 +49,7 @@ export class VideoShareComponent {
49 } 49 }
50 50
51 activateCopiedMessage () { 51 activateCopiedMessage () {
52 this.notificationsService.success(this.i18n('Success'), this.i18n('Copied')) 52 this.notifier.success(this.i18n('Copied'))
53 } 53 }
54 54
55 getStartCheckboxLabel () { 55 getStartCheckboxLabel () {
diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.html b/client/src/app/videos/+video-watch/modal/video-support.component.html
index 00c304709..2a05224a8 100644
--- a/client/src/app/videos/+video-watch/modal/video-support.component.html
+++ b/client/src/app/videos/+video-watch/modal/video-support.component.html
@@ -1,7 +1,7 @@
1<ng-template #modal let-hide="close"> 1<ng-template #modal let-hide="close">
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Support</h4> 3 <h4 i18n class="modal-title">Support</h4>
4 <span class="close" aria-label="Close" role="button" (click)="hide()"></span> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body" [innerHTML]="videoHTMLSupport"></div> 7 <div class="modal-body" [innerHTML]="videoHTMLSupport"></div>
diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.ts b/client/src/app/videos/+video-watch/modal/video-support.component.ts
index 154002120..deb8fbc67 100644
--- a/client/src/app/videos/+video-watch/modal/video-support.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-support.component.ts
@@ -1,8 +1,7 @@
1import { Component, Input, ViewChild } from '@angular/core' 1import { Component, Input, ViewChild } from '@angular/core'
2import { MarkdownService } from '@app/videos/shared'
3
4import { VideoDetails } from '../../../shared/video/video-details.model' 2import { VideoDetails } from '../../../shared/video/video-details.model'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { MarkdownService } from '@app/shared/renderer'
6 5
7@Component({ 6@Component({
8 selector: 'my-video-support', 7 selector: 'my-video-support',
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html
index ba04d638f..1875230d8 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -53,55 +53,57 @@
53 <div 53 <div
54 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()" 54 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()"
55 class="action-button action-button-like" role="button" [attr.aria-pressed]="userRating === 'like'" 55 class="action-button action-button-like" role="button" [attr.aria-pressed]="userRating === 'like'"
56 i18n-title title="Like this video"
56 > 57 >
57 <span class="icon icon-like" i18n-title title="Like this video" ></span> 58 <my-global-icon iconName="like"></my-global-icon>
58 </div> 59 </div>
59 60
60 <div 61 <div
61 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()" 62 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()"
62 class="action-button action-button-dislike" role="button" [attr.aria-pressed]="userRating === 'dislike'" 63 class="action-button action-button-dislike" role="button" [attr.aria-pressed]="userRating === 'dislike'"
64 i18n-title title="Dislike this video"
63 > 65 >
64 <span class="icon icon-dislike" i18n-title title="Dislike this video"></span> 66 <my-global-icon iconName="dislike"></my-global-icon>
65 </div> 67 </div>
66 68
67 <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support"> 69 <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support">
68 <span class="icon icon-support"></span> 70 <my-global-icon iconName="heart"></my-global-icon>
69 <span class="icon-text" i18n>Support</span> 71 <span class="icon-text" i18n>Support</span>
70 </div> 72 </div>
71 73
72 <div (click)="showShareModal()" class="action-button action-button-share" role="button"> 74 <div (click)="showShareModal()" class="action-button action-button-share" role="button">
73 <span class="icon icon-share"></span> 75 <my-global-icon iconName="share"></my-global-icon>
74 <span class="icon-text" i18n>Share</span> 76 <span class="icon-text" i18n>Share</span>
75 </div> 77 </div>
76 78
77 <div class="action-more" ngbDropdown placement="top" role="button"> 79 <div class="action-more" ngbDropdown placement="top" role="button">
78 <div class="action-button" ngbDropdownToggle role="button"> 80 <div class="action-button" ngbDropdownToggle role="button">
79 <span class="icon icon-more"></span> 81 <my-global-icon class="more-icon" iconName="more"></my-global-icon>
80 </div> 82 </div>
81 83
82 <div ngbDropdownMenu> 84 <div ngbDropdownMenu>
83 <a *ngIf="isVideoDownloadable()" class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)"> 85 <a *ngIf="isVideoDownloadable()" class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)">
84 <span class="icon icon-download"></span> <ng-container i18n>Download</ng-container> 86 <my-global-icon iconName="download"></my-global-icon> <ng-container i18n>Download</ng-container>
85 </a> 87 </a>
86 88
87 <a *ngIf="isUserLoggedIn()" class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)"> 89 <a *ngIf="isUserLoggedIn()" class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)">
88 <span class="icon icon-alert"></span> <ng-container i18n>Report</ng-container> 90 <my-global-icon iconName="alert"></my-global-icon> <ng-container i18n>Report</ng-container>
89 </a> 91 </a>
90 92
91 <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]"> 93 <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]">
92 <span class="icon icon-edit"></span> <ng-container i18n>Update</ng-container> 94 <my-global-icon iconName="edit"></my-global-icon> <ng-container i18n>Update</ng-container>
93 </a> 95 </a>
94 96
95 <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)"> 97 <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)">
96 <span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container> 98 <my-global-icon iconName="no"></my-global-icon> <ng-container i18n>Blacklist</ng-container>
97 </a> 99 </a>
98 100
99 <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)"> 101 <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)">
100 <span class="icon icon-unblacklist"></span> <ng-container i18n>Unblacklist</ng-container> 102 <my-global-icon iconName="undo"></my-global-icon> <ng-container i18n>Unblacklist</ng-container>
101 </a> 103 </a>
102 104
103 <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)"> 105 <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)">
104 <span class="icon icon-delete"></span> <ng-container i18n>Delete</ng-container> 106 <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete</ng-container>
105 </a> 107 </a>
106 </div> 108 </div>
107 </div> 109 </div>
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss
index f31e4694a..b03ed197d 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -162,7 +162,7 @@ $other-videos-width: 260px;
162 } 162 }
163 } 163 }
164 164
165 my-video-feed { 165 my-feed {
166 margin-left: 5px; 166 margin-left: 5px;
167 margin-top: 1px; 167 margin-top: 1px;
168 } 168 }
@@ -183,6 +183,8 @@ $other-videos-width: 260px;
183 .action-button { 183 .action-button {
184 @include peertube-button; 184 @include peertube-button;
185 @include grey-button; 185 @include grey-button;
186 @include button-with-icon(21px, 0, -1px);
187 @include apply-svg-color($grey-foreground-color);
186 188
187 font-size: 15px; 189 font-size: 15px;
188 font-weight: $font-semibold; 190 font-weight: $font-semibold;
@@ -194,53 +196,25 @@ $other-videos-width: 260px;
194 display: none; 196 display: none;
195 } 197 }
196 198
197 .icon {
198 @include icon(21px);
199
200 position: relative;
201 top: -2px;
202
203 &.icon-like {
204 background-image: url('../../../assets/images/video/like-grey.svg');
205 }
206
207 &.icon-dislike {
208 background-image: url('../../../assets/images/video/dislike-grey.svg');
209 }
210
211 &.icon-support {
212 background-image: url('../../../assets/images/video/heart.svg');
213 }
214
215 &.icon-share {
216 background-image: url('../../../assets/images/video/share.svg');
217 }
218
219 &.icon-more {
220 background-image: url('../../../assets/images/video/more.svg');
221 top: -1px;
222 }
223 }
224
225 .icon-text {
226 margin-left: 3px;
227 }
228
229 &.action-button-like.activated { 199 &.action-button-like.activated {
230 background-color: $green; 200 background-color: $green;
231 201
232 .icon-like { 202 my-global-icon {
233 background-image: url('../../../assets/images/video/like-white.svg'); 203 @include apply-svg-color(#fff);
234 } 204 }
235 } 205 }
236 206
237 &.action-button-dislike.activated { 207 &.action-button-dislike.activated {
238 background-color: $red; 208 background-color: $red;
239 209
240 .icon-dislike { 210 my-global-icon {
241 background-image: url('../../../assets/images/video/dislike-white.svg'); 211 @include apply-svg-color(#fff);
242 } 212 }
243 } 213 }
214
215 .icon-text {
216 margin-left: 3px;
217 }
244 } 218 }
245 219
246 .action-more { 220 .action-more {
@@ -249,36 +223,12 @@ $other-videos-width: 260px;
249 .dropdown-menu .dropdown-item { 223 .dropdown-menu .dropdown-item {
250 padding: 6px 24px; 224 padding: 6px 24px;
251 225
252 .icon { 226 my-global-icon {
253 @include icon(24px); 227 width: 24px;
254 228
255 margin-right: 10px; 229 margin-right: 10px;
256 position: relative; 230 position: relative;
257 top: -1px; 231 top: -2px;
258
259 &.icon-download {
260 background-image: url('../../../assets/images/video/download-black.svg');
261 }
262
263 &.icon-edit {
264 background-image: url('../../../assets/images/global/edit-black.svg');
265 }
266
267 &.icon-alert {
268 background-image: url('../../../assets/images/video/alert.svg');
269 }
270
271 &.icon-blacklist {
272 background-image: url('../../../assets/images/video/blacklist.svg');
273 }
274
275 &.icon-unblacklist {
276 background-image: url('../../../assets/images/global/undo.svg');
277 }
278
279 &.icon-delete {
280 background-image: url('../../../assets/images/global/delete-black.svg');
281 }
282 } 232 }
283 } 233 }
284 } 234 }
@@ -320,7 +270,7 @@ $other-videos-width: 260px;
320 .video-info-description-more { 270 .video-info-description-more {
321 cursor: pointer; 271 cursor: pointer;
322 font-weight: $font-semibold; 272 font-weight: $font-semibold;
323 color: #585858; 273 color: $grey-foreground-color;
324 font-size: 14px; 274 font-size: 14px;
325 275
326 .glyphicon { 276 .glyphicon {
@@ -339,7 +289,7 @@ $other-videos-width: 260px;
339 min-width: 91px; 289 min-width: 91px;
340 padding-right: 5px; 290 padding-right: 5px;
341 display: inline-block; 291 display: inline-block;
342 color: #585858; 292 color: $grey-foreground-color;
343 font-weight: $font-bold; 293 font-weight: $font-bold;
344 } 294 }
345 295
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index a7fd45695..4dbfa41e5 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -5,30 +5,29 @@ import { RedirectService } from '@app/core/routing/redirect.service'
5import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' 5import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
6import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' 6import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
7import { MetaService } from '@ngx-meta/core' 7import { MetaService } from '@ngx-meta/core'
8import { NotificationsService } from 'angular2-notifications' 8import { Notifier, ServerService } from '@app/core'
9import { forkJoin, Subscription } from 'rxjs' 9import { forkJoin, Subscription } from 'rxjs'
10import * as videojs from 'video.js'
11import 'videojs-hotkeys'
12import { Hotkey, HotkeysService } from 'angular2-hotkeys' 10import { Hotkey, HotkeysService } from 'angular2-hotkeys'
13import * as WebTorrent from 'webtorrent' 11import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared'
14import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoRateType, VideoState } from '../../../../../shared'
15import '../../../assets/player/peertube-videojs-plugin'
16import { AuthService, ConfirmService } from '../../core' 12import { AuthService, ConfirmService } from '../../core'
17import { RestExtractor, VideoBlacklistService } from '../../shared' 13import { RestExtractor, VideoBlacklistService } from '../../shared'
18import { VideoDetails } from '../../shared/video/video-details.model' 14import { VideoDetails } from '../../shared/video/video-details.model'
19import { VideoService } from '../../shared/video/video.service' 15import { VideoService } from '../../shared/video/video.service'
20import { MarkdownService } from '../shared'
21import { VideoDownloadComponent } from './modal/video-download.component' 16import { VideoDownloadComponent } from './modal/video-download.component'
22import { VideoReportComponent } from './modal/video-report.component' 17import { VideoReportComponent } from './modal/video-report.component'
23import { VideoShareComponent } from './modal/video-share.component' 18import { VideoShareComponent } from './modal/video-share.component'
24import { VideoBlacklistComponent } from './modal/video-blacklist.component' 19import { VideoBlacklistComponent } from './modal/video-blacklist.component'
25import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' 20import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component'
26import { addContextMenu, getVideojsOptions, loadLocaleInVideoJS } from '../../../assets/player/peertube-player'
27import { ServerService } from '@app/core'
28import { I18n } from '@ngx-translate/i18n-polyfill' 21import { I18n } from '@ngx-translate/i18n-polyfill'
29import { environment } from '../../../environments/environment' 22import { environment } from '../../../environments/environment'
30import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
31import { VideoCaptionService } from '@app/shared/video-caption' 23import { VideoCaptionService } from '@app/shared/video-caption'
24import { MarkdownService } from '@app/shared/renderer'
25import {
26 P2PMediaLoaderOptions,
27 PeertubePlayerManager,
28 PeertubePlayerManagerOptions,
29 PlayerMode
30} from '../../../assets/player/peertube-player-manager'
32 31
33@Component({ 32@Component({
34 selector: 'my-video-watch', 33 selector: 'my-video-watch',
@@ -45,7 +44,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
45 @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent 44 @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent
46 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent 45 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
47 46
48 player: videojs.Player 47 player: any
49 playerElement: HTMLVideoElement 48 playerElement: HTMLVideoElement
50 userRating: UserVideoRateType = null 49 userRating: UserVideoRateType = null
51 video: VideoDetails = null 50 video: VideoDetails = null
@@ -60,7 +59,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
60 remoteServerDown = false 59 remoteServerDown = false
61 hotkeys: Hotkey[] 60 hotkeys: Hotkey[]
62 61
63 private videojsLocaleLoaded = false
64 private paramsSub: Subscription 62 private paramsSub: Subscription
65 63
66 constructor ( 64 constructor (
@@ -75,7 +73,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
75 private authService: AuthService, 73 private authService: AuthService,
76 private serverService: ServerService, 74 private serverService: ServerService,
77 private restExtractor: RestExtractor, 75 private restExtractor: RestExtractor,
78 private notificationsService: NotificationsService, 76 private notifier: Notifier,
79 private markdownService: MarkdownService, 77 private markdownService: MarkdownService,
80 private zone: NgZone, 78 private zone: NgZone,
81 private redirectService: RedirectService, 79 private redirectService: RedirectService,
@@ -91,7 +89,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
91 89
92 ngOnInit () { 90 ngOnInit () {
93 if ( 91 if (
94 WebTorrent.WEBRTC_SUPPORT === false || 92 !!((window as any).RTCPeerConnection || (window as any).mozRTCPeerConnection || (window as any).webkitRTCPeerConnection) === false ||
95 peertubeLocalStorage.getItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true' 93 peertubeLocalStorage.getItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true'
96 ) { 94 ) {
97 this.hasAlreadyAcceptedPrivacyConcern = true 95 this.hasAlreadyAcceptedPrivacyConcern = true
@@ -112,11 +110,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
112 ) 110 )
113 .pipe( 111 .pipe(
114 // If 401, the video is private or blacklisted so redirect to 404 112 // If 401, the video is private or blacklisted so redirect to 404
115 catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 404 ])) 113 catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ]))
116 ) 114 )
117 .subscribe(([ video, captionsResult ]) => { 115 .subscribe(([ video, captionsResult ]) => {
118 const startTime = this.route.snapshot.queryParams.start 116 const startTime = this.route.snapshot.queryParams.start
119 this.onVideoFetched(video, captionsResult.data, startTime) 117 const subtitle = this.route.snapshot.queryParams.subtitle
118 const playerMode = this.route.snapshot.queryParams.mode
119
120 this.onVideoFetched(video, captionsResult.data, { startTime, subtitle, playerMode })
120 .catch(err => this.handleError(err)) 121 .catch(err => this.handleError(err))
121 }) 122 })
122 }) 123 })
@@ -201,7 +202,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
201 202
202 error => { 203 error => {
203 this.descriptionLoading = false 204 this.descriptionLoading = false
204 this.notificationsService.error(this.i18n('Error'), error.message) 205 this.notifier.error(error.message)
205 } 206 }
206 ) 207 )
207 } 208 }
@@ -243,16 +244,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
243 244
244 this.videoBlacklistService.removeVideoFromBlacklist(this.video.id).subscribe( 245 this.videoBlacklistService.removeVideoFromBlacklist(this.video.id).subscribe(
245 () => { 246 () => {
246 this.notificationsService.success( 247 this.notifier.success(this.i18n('Video {{name}} removed from the blacklist.', { name: this.video.name }))
247 this.i18n('Success'),
248 this.i18n('Video {{name}} removed from the blacklist.', { name: this.video.name })
249 )
250 248
251 this.video.blacklisted = false 249 this.video.blacklisted = false
252 this.video.blacklistedReason = null 250 this.video.blacklistedReason = null
253 }, 251 },
254 252
255 err => this.notificationsService.error(this.i18n('Error'), err.message) 253 err => this.notifier.error(err.message)
256 ) 254 )
257 } 255 }
258 256
@@ -290,17 +288,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
290 288
291 this.videoService.removeVideo(this.video.id) 289 this.videoService.removeVideo(this.video.id)
292 .subscribe( 290 .subscribe(
293 status => { 291 () => {
294 this.notificationsService.success( 292 this.notifier.success(this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name }))
295 this.i18n('Success'),
296 this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name })
297 )
298 293
299 // Go back to the video-list. 294 // Go back to the video-list.
300 this.redirectService.redirectToHomepage() 295 this.redirectService.redirectToHomepage()
301 }, 296 },
302 297
303 error => this.notificationsService.error(this.i18n('Error'), error.message) 298 error => this.notifier.error(error.message)
304 ) 299 )
305 } 300 }
306 301
@@ -354,7 +349,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
354 return 349 return
355 } 350 }
356 351
357 this.notificationsService.error(this.i18n('Error'), errorMessage) 352 this.notifier.error(errorMessage)
358 } 353 }
359 354
360 private checkUserRating () { 355 private checkUserRating () {
@@ -369,11 +364,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
369 } 364 }
370 }, 365 },
371 366
372 err => this.notificationsService.error(this.i18n('Error'), err.message) 367 err => this.notifier.error(err.message)
373 ) 368 )
374 } 369 }
375 370
376 private async onVideoFetched (video: VideoDetails, videoCaptions: VideoCaption[], startTimeFromUrl: number) { 371 private async onVideoFetched (
372 video: VideoDetails,
373 videoCaptions: VideoCaption[],
374 urlOptions: { startTime?: number, subtitle?: string, playerMode?: string }
375 ) {
377 this.video = video 376 this.video = video
378 377
379 // Re init attributes 378 // Re init attributes
@@ -381,8 +380,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
381 this.completeDescriptionShown = false 380 this.completeDescriptionShown = false
382 this.remoteServerDown = false 381 this.remoteServerDown = false
383 382
384 let startTime = startTimeFromUrl || (this.video.userHistory ? this.video.userHistory.currentTime : 0) 383 let startTime = urlOptions.startTime || (this.video.userHistory ? this.video.userHistory.currentTime : 0)
385 // Don't start the video if we are at the end 384 // If we are at the end of the video, reset the timer
386 if (this.video.duration - startTime <= 1) startTime = 0 385 if (this.video.duration - startTime <= 1) startTime = 0
387 386
388 if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) { 387 if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) {
@@ -409,40 +408,64 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
409 src: environment.apiUrl + c.captionPath 408 src: environment.apiUrl + c.captionPath
410 })) 409 }))
411 410
412 const videojsOptions = getVideojsOptions({ 411 const options: PeertubePlayerManagerOptions = {
413 autoplay: this.isAutoplay(), 412 common: {
414 inactivityTimeout: 2500, 413 autoplay: this.isAutoplay(),
415 videoFiles: this.video.files, 414
416 videoCaptions: playerCaptions, 415 playerElement: this.playerElement,
417 playerElement: this.playerElement, 416 onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element,
418 videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null, 417
419 videoDuration: this.video.duration, 418 videoDuration: this.video.duration,
420 enableHotkeys: true, 419 enableHotkeys: true,
421 peertubeLink: false, 420 inactivityTimeout: 2500,
422 poster: this.video.previewUrl, 421 poster: this.video.previewUrl,
423 startTime, 422 startTime,
424 theaterMode: true, 423
425 language: this.localeId, 424 theaterMode: true,
426 425 captions: videoCaptions.length !== 0,
427 userWatching: this.user ? { 426 peertubeLink: false,
428 url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), 427
429 authorizationHeader: this.authService.getRequestHeaderValue() 428 videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null,
430 } : undefined 429 embedUrl: this.video.embedUrl,
431 }) 430
431 language: this.localeId,
432
433 subtitle: urlOptions.subtitle,
434
435 userWatching: this.user && this.user.videosHistoryEnabled === true ? {
436 url: this.videoService.getUserWatchingVideoUrl(this.video.uuid),
437 authorizationHeader: this.authService.getRequestHeaderValue()
438 } : undefined,
432 439
433 if (this.videojsLocaleLoaded === false) { 440 serverUrl: environment.apiUrl,
434 await loadLocaleInVideoJS(environment.apiUrl, videojs, isOnDevLocale() ? getDevLocale() : this.localeId) 441
435 this.videojsLocaleLoaded = true 442 videoCaptions: playerCaptions
443 },
444
445 webtorrent: {
446 videoFiles: this.video.files
447 }
436 } 448 }
437 449
438 const self = this 450 const mode: PlayerMode = urlOptions.playerMode === 'p2p-media-loader' ? 'p2p-media-loader' : 'webtorrent'
439 this.zone.runOutsideAngular(async () => { 451
440 videojs(this.playerElement, videojsOptions, function () { 452 if (mode === 'p2p-media-loader') {
441 self.player = this 453 const hlsPlaylist = this.video.getHlsPlaylist()
442 this.on('customError', (event, data) => self.handleError(data.err))
443 454
444 addContextMenu(self.player, self.video.embedUrl) 455 const p2pMediaLoader = {
445 }) 456 playlistUrl: hlsPlaylist.playlistUrl,
457 segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
458 redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
459 trackerAnnounce: this.video.trackerUrls,
460 videoFiles: this.video.files
461 } as P2PMediaLoaderOptions
462
463 Object.assign(options, { p2pMediaLoader })
464 }
465
466 this.zone.runOutsideAngular(async () => {
467 this.player = await PeertubePlayerManager.initialize(mode, options)
468 this.player.on('customError', ({ err }: { err: any }) => this.handleError(err))
446 }) 469 })
447 470
448 this.setVideoDescriptionHTML() 471 this.setVideoDescriptionHTML()
@@ -452,7 +475,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
452 this.checkUserRating() 475 this.checkUserRating()
453 } 476 }
454 477
455 private setRating (nextRating) { 478 private setRating (nextRating: UserVideoRateType) {
456 let method 479 let method
457 switch (nextRating) { 480 switch (nextRating) {
458 case 'like': 481 case 'like':
@@ -474,11 +497,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
474 this.userRating = nextRating 497 this.userRating = nextRating
475 }, 498 },
476 499
477 err => this.notificationsService.error(this.i18n('Error'), err.message) 500 (err: { message: string }) => this.notifier.error(err.message)
478 ) 501 )
479 } 502 }
480 503
481 private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) { 504 private updateVideoRating (oldRating: UserVideoRateType, newRating: UserVideoRateType) {
482 let likesToIncrement = 0 505 let likesToIncrement = 0
483 let dislikesToIncrement = 0 506 let dislikesToIncrement = 0
484 507
diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts
index 5582ab40f..2f448db78 100644
--- a/client/src/app/videos/+video-watch/video-watch.module.ts
+++ b/client/src/app/videos/+video-watch/video-watch.module.ts
@@ -1,9 +1,7 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { LinkifierService } from '@app/videos/+video-watch/comment/linkifier.service'
3import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' 2import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
4import { ClipboardModule } from 'ngx-clipboard' 3import { ClipboardModule } from 'ngx-clipboard'
5import { SharedModule } from '../../shared' 4import { SharedModule } from '../../shared'
6import { MarkdownService } from '../shared'
7import { VideoCommentAddComponent } from './comment/video-comment-add.component' 5import { VideoCommentAddComponent } from './comment/video-comment-add.component'
8import { VideoCommentComponent } from './comment/video-comment.component' 6import { VideoCommentComponent } from './comment/video-comment.component'
9import { VideoCommentService } from './comment/video-comment.service' 7import { VideoCommentService } from './comment/video-comment.service'
@@ -17,7 +15,6 @@ import { NgxQRCodeModule } from 'ngx-qrcode2'
17import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' 15import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
18import { VideoBlacklistComponent } from '@app/videos/+video-watch/modal/video-blacklist.component' 16import { VideoBlacklistComponent } from '@app/videos/+video-watch/modal/video-blacklist.component'
19import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module' 17import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module'
20import { TextareaAutosizeModule } from 'ngx-textarea-autosize'
21 18
22@NgModule({ 19@NgModule({
23 imports: [ 20 imports: [
@@ -26,7 +23,6 @@ import { TextareaAutosizeModule } from 'ngx-textarea-autosize'
26 ClipboardModule, 23 ClipboardModule,
27 NgbTooltipModule, 24 NgbTooltipModule,
28 NgxQRCodeModule, 25 NgxQRCodeModule,
29 TextareaAutosizeModule,
30 RecommendationsModule 26 RecommendationsModule
31 ], 27 ],
32 28
@@ -48,8 +44,6 @@ import { TextareaAutosizeModule } from 'ngx-textarea-autosize'
48 ], 44 ],
49 45
50 providers: [ 46 providers: [
51 MarkdownService,
52 LinkifierService,
53 VideoCommentService 47 VideoCommentService
54 ] 48 ]
55}) 49})
diff --git a/client/src/app/videos/shared/index.ts b/client/src/app/videos/shared/index.ts
deleted file mode 100644
index 7a66944b9..000000000
--- a/client/src/app/videos/shared/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './markdown.service'
diff --git a/client/src/app/videos/video-list/video-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts
index c91c639ca..c0be4b885 100644
--- a/client/src/app/videos/video-list/video-local.component.ts
+++ b/client/src/app/videos/video-list/video-local.component.ts
@@ -2,7 +2,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { immutableAssign } from '@app/shared/misc/utils' 3import { immutableAssign } from '@app/shared/misc/utils'
4import { Location } from '@angular/common' 4import { Location } from '@angular/common'
5import { NotificationsService } from 'angular2-notifications'
6import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
7import { AbstractVideoList } from '../../shared/video/abstract-video-list' 6import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 7import { VideoSortField } from '../../shared/video/sort-field.type'
@@ -10,6 +9,8 @@ import { VideoService } from '../../shared/video/video.service'
10import { VideoFilter } from '../../../../../shared/models/videos/video-query.type' 9import { VideoFilter } from '../../../../../shared/models/videos/video-query.type'
11import { I18n } from '@ngx-translate/i18n-polyfill' 10import { I18n } from '@ngx-translate/i18n-polyfill'
12import { ScreenService } from '@app/shared/misc/screen.service' 11import { ScreenService } from '@app/shared/misc/screen.service'
12import { UserRight } from '../../../../../shared/models/users'
13import { Notifier } from '@app/core'
13 14
14@Component({ 15@Component({
15 selector: 'my-videos-local', 16 selector: 'my-videos-local',
@@ -25,7 +26,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
25 constructor ( 26 constructor (
26 protected router: Router, 27 protected router: Router,
27 protected route: ActivatedRoute, 28 protected route: ActivatedRoute,
28 protected notificationsService: NotificationsService, 29 protected notifier: Notifier,
29 protected authService: AuthService, 30 protected authService: AuthService,
30 protected location: Location, 31 protected location: Location,
31 protected i18n: I18n, 32 protected i18n: I18n,
@@ -40,6 +41,11 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
40 ngOnInit () { 41 ngOnInit () {
41 super.ngOnInit() 42 super.ngOnInit()
42 43
44 if (this.authService.isLoggedIn()) {
45 const user = this.authService.getUser()
46 this.displayModerationBlock = user.hasRight(UserRight.SEE_ALL_VIDEOS)
47 }
48
43 this.generateSyndicationList() 49 this.generateSyndicationList()
44 } 50 }
45 51
@@ -56,4 +62,10 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
56 generateSyndicationList () { 62 generateSyndicationList () {
57 this.syndicationItems = this.videoService.getVideoFeedUrls(this.sort, this.filter, this.categoryOneOf) 63 this.syndicationItems = this.videoService.getVideoFeedUrls(this.sort, this.filter, this.categoryOneOf)
58 } 64 }
65
66 toggleModerationDisplay () {
67 this.filter = this.filter === 'local' ? 'all-local' as 'all-local' : 'local' as 'local'
68
69 this.reloadVideos()
70 }
59} 71}
diff --git a/client/src/app/videos/video-list/video-overview.component.ts b/client/src/app/videos/video-list/video-overview.component.ts
index 2c6054721..7ff52b259 100644
--- a/client/src/app/videos/video-list/video-overview.component.ts
+++ b/client/src/app/videos/video-list/video-overview.component.ts
@@ -1,6 +1,5 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { AuthService } from '@app/core' 2import { AuthService, Notifier } from '@app/core'
3import { NotificationsService } from 'angular2-notifications'
4import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
5import { VideosOverview } from '@app/shared/overview/videos-overview.model' 4import { VideosOverview } from '@app/shared/overview/videos-overview.model'
6import { OverviewService } from '@app/shared/overview' 5import { OverviewService } from '@app/shared/overview'
@@ -21,7 +20,7 @@ export class VideoOverviewComponent implements OnInit {
21 20
22 constructor ( 21 constructor (
23 private i18n: I18n, 22 private i18n: I18n,
24 private notificationsService: NotificationsService, 23 private notifier: Notifier,
25 private authService: AuthService, 24 private authService: AuthService,
26 private overviewService: OverviewService 25 private overviewService: OverviewService
27 ) { } 26 ) { }
@@ -43,10 +42,7 @@ export class VideoOverviewComponent implements OnInit {
43 ) this.notResults = true 42 ) this.notResults = true
44 }, 43 },
45 44
46 err => { 45 err => this.notifier.error(err.message)
47 console.log(err)
48 this.notificationsService.error('Error', err.text)
49 }
50 ) 46 )
51 } 47 }
52 48
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts
index ac1fcfff3..f99c8abb6 100644
--- a/client/src/app/videos/video-list/video-recently-added.component.ts
+++ b/client/src/app/videos/video-list/video-recently-added.component.ts
@@ -2,13 +2,13 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { Location } from '@angular/common' 3import { Location } from '@angular/common'
4import { immutableAssign } from '@app/shared/misc/utils' 4import { immutableAssign } from '@app/shared/misc/utils'
5import { NotificationsService } from 'angular2-notifications'
6import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
7import { AbstractVideoList } from '../../shared/video/abstract-video-list' 6import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 7import { VideoSortField } from '../../shared/video/sort-field.type'
9import { VideoService } from '../../shared/video/video.service' 8import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
11import { ScreenService } from '@app/shared/misc/screen.service' 10import { ScreenService } from '@app/shared/misc/screen.service'
11import { Notifier } from '@app/core'
12 12
13@Component({ 13@Component({
14 selector: 'my-videos-recently-added', 14 selector: 'my-videos-recently-added',
@@ -24,7 +24,7 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
24 protected router: Router, 24 protected router: Router,
25 protected route: ActivatedRoute, 25 protected route: ActivatedRoute,
26 protected location: Location, 26 protected location: Location,
27 protected notificationsService: NotificationsService, 27 protected notifier: Notifier,
28 protected authService: AuthService, 28 protected authService: AuthService,
29 protected i18n: I18n, 29 protected i18n: I18n,
30 protected screenService: ScreenService, 30 protected screenService: ScreenService,
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts
index 8f3d3842b..6fd74e67a 100644
--- a/client/src/app/videos/video-list/video-trending.component.ts
+++ b/client/src/app/videos/video-list/video-trending.component.ts
@@ -2,13 +2,13 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { Location } from '@angular/common' 3import { Location } from '@angular/common'
4import { immutableAssign } from '@app/shared/misc/utils' 4import { immutableAssign } from '@app/shared/misc/utils'
5import { NotificationsService } from 'angular2-notifications'
6import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
7import { AbstractVideoList } from '../../shared/video/abstract-video-list' 6import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 7import { VideoSortField } from '../../shared/video/sort-field.type'
9import { VideoService } from '../../shared/video/video.service' 8import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
11import { ScreenService } from '@app/shared/misc/screen.service' 10import { ScreenService } from '@app/shared/misc/screen.service'
11import { Notifier, ServerService } from '@app/core'
12 12
13@Component({ 13@Component({
14 selector: 'my-videos-trending', 14 selector: 'my-videos-trending',
@@ -23,22 +23,37 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
23 constructor ( 23 constructor (
24 protected router: Router, 24 protected router: Router,
25 protected route: ActivatedRoute, 25 protected route: ActivatedRoute,
26 protected notificationsService: NotificationsService, 26 protected notifier: Notifier,
27 protected authService: AuthService, 27 protected authService: AuthService,
28 protected location: Location, 28 protected location: Location,
29 protected screenService: ScreenService, 29 protected screenService: ScreenService,
30 private serverService: ServerService,
30 protected i18n: I18n, 31 protected i18n: I18n,
31 private videoService: VideoService 32 private videoService: VideoService
32 ) { 33 ) {
33 super() 34 super()
34
35 this.titlePage = i18n('Trending')
36 } 35 }
37 36
38 ngOnInit () { 37 ngOnInit () {
39 super.ngOnInit() 38 super.ngOnInit()
40 39
41 this.generateSyndicationList() 40 this.generateSyndicationList()
41
42 this.serverService.configLoaded.subscribe(
43 () => {
44 const trendingDays = this.serverService.getConfig().trending.videos.intervalDays
45
46 if (trendingDays === 1) {
47 this.titlePage = this.i18n('Trending for the last 24 hours')
48 this.titleTooltip = this.i18n('Trending videos are those totalizing the greatest number of views during the last 24 hours.')
49 } else {
50 this.titlePage = this.i18n('Trending for the last {{days}} days', { days: trendingDays })
51 this.titleTooltip = this.i18n(
52 'Trending videos are those totalizing the greatest number of views during the last {{days}} days.',
53 { days: trendingDays }
54 )
55 }
56 })
42 } 57 }
43 58
44 ngOnDestroy () { 59 ngOnDestroy () {
diff --git a/client/src/app/videos/video-list/video-user-subscriptions.component.ts b/client/src/app/videos/video-list/video-user-subscriptions.component.ts
index 6e8959c54..bee828e12 100644
--- a/client/src/app/videos/video-list/video-user-subscriptions.component.ts
+++ b/client/src/app/videos/video-list/video-user-subscriptions.component.ts
@@ -2,7 +2,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { immutableAssign } from '@app/shared/misc/utils' 3import { immutableAssign } from '@app/shared/misc/utils'
4import { Location } from '@angular/common' 4import { Location } from '@angular/common'
5import { NotificationsService } from 'angular2-notifications'
6import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
7import { AbstractVideoList } from '../../shared/video/abstract-video-list' 6import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 7import { VideoSortField } from '../../shared/video/sort-field.type'
@@ -10,6 +9,7 @@ import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
11import { ScreenService } from '@app/shared/misc/screen.service' 10import { ScreenService } from '@app/shared/misc/screen.service'
12import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' 11import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
12import { Notifier } from '@app/core'
13 13
14@Component({ 14@Component({
15 selector: 'my-videos-user-subscriptions', 15 selector: 'my-videos-user-subscriptions',
@@ -25,7 +25,7 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
25 constructor ( 25 constructor (
26 protected router: Router, 26 protected router: Router,
27 protected route: ActivatedRoute, 27 protected route: ActivatedRoute,
28 protected notificationsService: NotificationsService, 28 protected notifier: Notifier,
29 protected authService: AuthService, 29 protected authService: AuthService,
30 protected location: Location, 30 protected location: Location,
31 protected i18n: I18n, 31 protected i18n: I18n,