aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.html2
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.html2
-rw-r--r--client/src/app/+accounts/accounts.component.html2
-rw-r--r--client/src/app/+admin/plugins/plugin-search/plugin-search.component.html6
-rw-r--r--client/src/app/+login/login.component.html2
-rw-r--r--client/src/app/+login/login.component.ts7
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts8
-rw-r--r--client/src/app/+search/search.component.html10
-rw-r--r--client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html2
-rw-r--r--client/src/app/+video-channels/video-channels.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts13
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts13
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts15
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts15
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comments.component.html15
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comments.component.ts1
-rw-r--r--client/src/app/core/notification/peertube-socket.service.ts7
-rw-r--r--client/src/app/core/plugins/hooks.service.ts18
-rw-r--r--client/src/app/core/plugins/plugin.service.ts6
-rw-r--r--client/src/app/header/search-typeahead.component.html4
-rw-r--r--client/src/app/menu/menu.component.scss325
-rw-r--r--client/src/app/modal/instance-config-warning-modal.component.html2
-rw-r--r--client/src/app/shared/shared-forms/input-toggle-hidden.component.html5
-rw-r--r--client/src/app/shared/shared-forms/select/select-options.component.ts9
-rw-r--r--client/src/app/shared/shared-main/angular/autofocus.directive.ts12
-rw-r--r--client/src/app/shared/shared-main/angular/index.ts1
-rw-r--r--client/src/app/shared/shared-main/auth/auth-interceptor.service.ts4
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts3
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.model.ts33
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.html48
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.ts9
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.html2
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.ts21
-rw-r--r--client/src/assets/images/feather/cloud-download.svg2
-rw-r--r--client/src/assets/images/feather/subscriptions.svg19
-rw-r--r--client/src/assets/images/misc/language.svg8
-rw-r--r--client/src/assets/images/misc/npm.svg2
-rw-r--r--client/src/assets/images/misc/peertube-x.svg25
-rw-r--r--client/src/assets/images/misc/playlist-add.svg4
-rw-r--r--client/src/assets/images/misc/support.svg4
-rw-r--r--client/src/assets/images/misc/video-lang.svg2
-rw-r--r--client/src/sass/bootstrap.scss4
-rw-r--r--client/src/sass/include/_mixins.scss25
-rw-r--r--client/src/sass/player/peertube-skin.scss18
-rw-r--r--client/src/standalone/videos/embed.ts2
-rw-r--r--client/src/types/register-client-option.model.ts3
46 files changed, 413 insertions, 329 deletions
diff --git a/client/src/app/+about/about-follows/about-follows.component.html b/client/src/app/+about/about-follows/about-follows.component.html
index 2cf890acf..e9139b503 100644
--- a/client/src/app/+about/about-follows/about-follows.component.html
+++ b/client/src/app/+about/about-follows/about-follows.component.html
@@ -1,7 +1,7 @@
1<div class="row"> 1<div class="row">
2 <h1 class="sr-only" i18n>Follows</h1> 2 <h1 class="sr-only" i18n>Follows</h1>
3 <div class="col-xl-6 col-md-12"> 3 <div class="col-xl-6 col-md-12">
4 <h2 i18n class="subtitle">Followers instances ({{ followersPagination.totalItems }})</h2> 4 <h2 i18n class="subtitle">Follower instances ({{ followersPagination.totalItems }})</h2>
5 5
6 <div i18n class="no-results" *ngIf="followersPagination.totalItems === 0">This instance does not have instances followers.</div> 6 <div i18n class="no-results" *ngIf="followersPagination.totalItems === 0">This instance does not have instances followers.</div>
7 7
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 d8794d602..1f372090e 100644
--- a/client/src/app/+about/about-instance/about-instance.component.html
+++ b/client/src/app/+about/about-instance/about-instance.component.html
@@ -83,7 +83,7 @@
83 fragment="business-model" 83 fragment="business-model"
84 #anchorLink 84 #anchorLink
85 (click)="onClickCopyLink(anchorLink)"> 85 (click)="onClickCopyLink(anchorLink)">
86 <h3 i18n class="section-title">How we will pay for this instance</h3> 86 <h3 i18n class="section-title">How we will pay for keeping our instance running</h3>
87 </a> 87 </a>
88 88
89 <div [innerHTML]="html.businessModel"></div> 89 <div [innerHTML]="html.businessModel"></div>
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index 5bd7b0824..1903bb36f 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -12,7 +12,7 @@
12 <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" 12 <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
13 class="btn btn-outline-secondary btn-sm copy-button" 13 class="btn btn-outline-secondary btn-sm copy-button"
14 > 14 >
15 <span class="glyphicon glyphicon-copy"></span> 15 <span class="glyphicon glyphicon-duplicate"></span>
16 </button> 16 </button>
17 </div> 17 </div>
18 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span> 18 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html
index 1b5fe45c6..8edf03a89 100644
--- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html
+++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html
@@ -3,7 +3,7 @@
3</div> 3</div>
4 4
5<div class="search-bar"> 5<div class="search-bar">
6 <input type="text" (input)="onSearchChange($event)" i18n-placeholder placeholder="Search..."/> 6 <input type="text" (input)="onSearchChange($event)" i18n-placeholder placeholder="Search..." autofocus />
7</div> 7</div>
8 8
9<div class="alert alert-info" i18n *ngIf="pluginInstalled"> 9<div class="alert alert-info" i18n *ngIf="pluginInstalled">
@@ -20,8 +20,8 @@
20 <my-global-icon iconName="search"></my-global-icon> 20 <my-global-icon iconName="search"></my-global-icon>
21 21
22 <ng-container i18n> 22 <ng-container i18n>
23 {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for "{{ search }}" 23 {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for {{ search }}"
24 </ng-container> 24 </ng-container>
25 </ng-container> 25 </ng-container>
26</div> 26</div>
27 27
diff --git a/client/src/app/+login/login.component.html b/client/src/app/+login/login.component.html
index 3171e5b0f..0167066a0 100644
--- a/client/src/app/+login/login.component.html
+++ b/client/src/app/+login/login.component.html
@@ -21,7 +21,7 @@
21 <label i18n for="username">User</label> 21 <label i18n for="username">User</label>
22 <input 22 <input
23 type="text" id="username" i18n-placeholder placeholder="Username or email address" required tabindex="1" 23 type="text" id="username" i18n-placeholder placeholder="Username or email address" required tabindex="1"
24 formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" #usernameInput 24 formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" autofocus
25 > 25 >
26 </div> 26 </div>
27 27
diff --git a/client/src/app/+login/login.component.ts b/client/src/app/+login/login.component.ts
index af747b7fa..d8ad49081 100644
--- a/client/src/app/+login/login.component.ts
+++ b/client/src/app/+login/login.component.ts
@@ -3,9 +3,9 @@ import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angula
3import { ActivatedRoute } from '@angular/router' 3import { ActivatedRoute } from '@angular/router'
4import { AuthService, Notifier, RedirectService, UserService } from '@app/core' 4import { AuthService, Notifier, RedirectService, UserService } from '@app/core'
5import { HooksService } from '@app/core/plugins/hooks.service' 5import { HooksService } from '@app/core/plugins/hooks.service'
6import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
7import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators' 6import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators'
8import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
8import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
9import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 9import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
10import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' 10import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
11 11
@@ -16,7 +16,6 @@ import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
16}) 16})
17 17
18export class LoginComponent extends FormReactive implements OnInit, AfterViewInit { 18export class LoginComponent extends FormReactive implements OnInit, AfterViewInit {
19 @ViewChild('usernameInput', { static: false }) usernameInput: ElementRef
20 @ViewChild('forgotPasswordModal', { static: true }) forgotPasswordModal: ElementRef 19 @ViewChild('forgotPasswordModal', { static: true }) forgotPasswordModal: ElementRef
21 20
22 accordion: NgbAccordion 21 accordion: NgbAccordion
@@ -91,10 +90,6 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
91 } 90 }
92 91
93 ngAfterViewInit () { 92 ngAfterViewInit () {
94 if (this.usernameInput) {
95 this.usernameInput.nativeElement.focus()
96 }
97
98 this.hooks.runAction('action:login.init', 'login') 93 this.hooks.runAction('action:login.init', 'login')
99 } 94 }
100 95
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
index ad7497f45..c7e173038 100644
--- 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
@@ -42,7 +42,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
42 newInstanceFollower: $localize`Your instance has a new follower`, 42 newInstanceFollower: $localize`Your instance has a new follower`,
43 autoInstanceFollowing: $localize`Your instance automatically followed another instance`, 43 autoInstanceFollowing: $localize`Your instance automatically followed another instance`,
44 abuseNewMessage: $localize`An abuse report received a new message`, 44 abuseNewMessage: $localize`An abuse report received a new message`,
45 abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators` 45 abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators`,
46 newPeerTubeVersion: $localize`A new PeerTube version is available`,
47 newPluginVersion: $localize`One of your plugin/theme has a new available version`
46 } 48 }
47 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] 49 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
48 50
@@ -51,7 +53,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
51 videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST, 53 videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
52 newUserRegistration: UserRight.MANAGE_USERS, 54 newUserRegistration: UserRight.MANAGE_USERS,
53 newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW, 55 newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW,
54 autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION 56 autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION,
57 newPeerTubeVersion: UserRight.MANAGE_DEBUG,
58 newPluginVersion: UserRight.MANAGE_DEBUG
55 } 59 }
56 } 60 }
57 61
diff --git a/client/src/app/+search/search.component.html b/client/src/app/+search/search.component.html
index 84be4fb14..74c6839e1 100644
--- a/client/src/app/+search/search.component.html
+++ b/client/src/app/+search/search.component.html
@@ -2,14 +2,12 @@
2 <div class="results-header"> 2 <div class="results-header">
3 <div class="first-line"> 3 <div class="first-line">
4 <div class="results-counter" *ngIf="pagination.totalItems"> 4 <div class="results-counter" *ngIf="pagination.totalItems">
5 <span i18n>{{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}} </span> 5 <span class="mr-1" i18n>{{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}}</span>
6 6
7 <span i18n *ngIf="advancedSearch.searchTarget === 'local'">on this instance</span> 7 <span class="mr-1" i18n *ngIf="advancedSearch.searchTarget === 'local'">on this instance</span>
8 <span i18n *ngIf="advancedSearch.searchTarget === 'search-index'">on the vidiverse</span> 8 <span class="mr-1" i18n *ngIf="advancedSearch.searchTarget === 'search-index'">on the vidiverse</span>
9 9
10 <span *ngIf="currentSearch" i18n> 10 <span *ngIf="currentSearch" i18n>for <span class="search-value">{{ currentSearch }}</span></span>
11 for <span class="search-value">{{ currentSearch }}</span>
12 </span>
13 </div> 11 </div>
14 12
15 <div 13 <div
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html
index 03770ceec..594935afd 100644
--- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html
+++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html
@@ -1,6 +1,6 @@
1<div class="margin-content"> 1<div class="margin-content">
2 <div i18n class="title-page title-page-single"> 2 <div i18n class="title-page title-page-single">
3 Created {{ pagination.totalItems }} playlists 3 Created {pagination.totalItems, plural, =1 {1 playlist} other {{{ pagination.totalItems }} playlists}}
4 </div> 4 </div>
5 5
6 <div i18n class="no-results" *ngIf="pagination.totalItems === 0">This channel does not have playlists.</div> 6 <div i18n class="no-results" *ngIf="pagination.totalItems === 0">This channel does not have playlists.</div>
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html
index 4b0d12b6e..b3ea19768 100644
--- a/client/src/app/+video-channels/video-channels.component.html
+++ b/client/src/app/+video-channels/video-channels.component.html
@@ -12,7 +12,7 @@
12 <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()" 12 <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()"
13 class="btn btn-outline-secondary btn-sm copy-button" 13 class="btn btn-outline-secondary btn-sm copy-button"
14 > 14 >
15 <span class="glyphicon glyphicon-copy"></span> 15 <span class="glyphicon glyphicon-duplicate"></span>
16 </button> 16 </button>
17 </div> 17 </div>
18 </div> 18 </div>
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
index 8780ca567..8e035b6bb 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
@@ -1,8 +1,8 @@
1 1
2import { forkJoin } from 'rxjs' 2import { forkJoin } from 'rxjs'
3import { Component, EventEmitter, OnInit, Output } from '@angular/core' 3import { AfterViewChecked, AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core'
4import { Router } from '@angular/router' 4import { Router } from '@angular/router'
5import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' 5import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
6import { scrollToTop } from '@app/helpers' 6import { scrollToTop } from '@app/helpers'
7import { FormValidatorService } from '@app/shared/shared-forms' 7import { FormValidatorService } from '@app/shared/shared-forms'
8import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' 8import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
@@ -19,7 +19,7 @@ import { VideoSend } from './video-send'
19 './video-send.scss' 19 './video-send.scss'
20 ] 20 ]
21}) 21})
22export class VideoGoLiveComponent extends VideoSend implements OnInit, CanComponentDeactivate { 22export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterViewInit, CanComponentDeactivate {
23 @Output() firstStepDone = new EventEmitter<string>() 23 @Output() firstStepDone = new EventEmitter<string>()
24 @Output() firstStepError = new EventEmitter<void>() 24 @Output() firstStepError = new EventEmitter<void>()
25 25
@@ -41,7 +41,8 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, CanCompon
41 protected videoService: VideoService, 41 protected videoService: VideoService,
42 protected videoCaptionService: VideoCaptionService, 42 protected videoCaptionService: VideoCaptionService,
43 private liveVideoService: LiveVideoService, 43 private liveVideoService: LiveVideoService,
44 private router: Router 44 private router: Router,
45 private hooks: HooksService
45 ) { 46 ) {
46 super() 47 super()
47 } 48 }
@@ -50,6 +51,10 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, CanCompon
50 super.ngOnInit() 51 super.ngOnInit()
51 } 52 }
52 53
54 ngAfterViewInit () {
55 this.hooks.runAction('action:go-live.init', 'video-edit')
56 }
57
53 canDeactivate () { 58 canDeactivate () {
54 return { canDeactivate: true } 59 return { canDeactivate: true }
55 } 60 }
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 01087e525..3aae24732 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,6 +1,6 @@
1import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { AfterViewInit, Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' 3import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
4import { scrollToTop } from '@app/helpers' 4import { scrollToTop } from '@app/helpers'
5import { FormValidatorService } from '@app/shared/shared-forms' 5import { FormValidatorService } from '@app/shared/shared-forms'
6import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' 6import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
@@ -18,7 +18,7 @@ import { VideoSend } from './video-send'
18 './video-send.scss' 18 './video-send.scss'
19 ] 19 ]
20}) 20})
21export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { 21export class VideoImportTorrentComponent extends VideoSend implements OnInit, AfterViewInit, CanComponentDeactivate {
22 @Output() firstStepDone = new EventEmitter<string>() 22 @Output() firstStepDone = new EventEmitter<string>()
23 @Output() firstStepError = new EventEmitter<void>() 23 @Output() firstStepError = new EventEmitter<void>()
24 @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement> 24 @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement>
@@ -43,7 +43,8 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
43 protected videoService: VideoService, 43 protected videoService: VideoService,
44 protected videoCaptionService: VideoCaptionService, 44 protected videoCaptionService: VideoCaptionService,
45 private router: Router, 45 private router: Router,
46 private videoImportService: VideoImportService 46 private videoImportService: VideoImportService,
47 private hooks: HooksService
47 ) { 48 ) {
48 super() 49 super()
49 } 50 }
@@ -52,6 +53,10 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
52 super.ngOnInit() 53 super.ngOnInit()
53 } 54 }
54 55
56 ngAfterViewInit () {
57 this.hooks.runAction('action:video-torrent-import.init', 'video-edit')
58 }
59
55 canDeactivate () { 60 canDeactivate () {
56 return { canDeactivate: true } 61 return { canDeactivate: true }
57 } 62 }
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 c447c179d..7a9fe369f 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,7 +1,7 @@
1import { map, switchMap } from 'rxjs/operators' 1import { map, switchMap } from 'rxjs/operators'
2import { Component, EventEmitter, OnInit, Output } from '@angular/core' 2import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core'
3import { Router } from '@angular/router' 3import { Router } from '@angular/router'
4import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' 4import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
5import { getAbsoluteAPIUrl, scrollToTop } from '@app/helpers' 5import { getAbsoluteAPIUrl, scrollToTop } from '@app/helpers'
6import { FormValidatorService } from '@app/shared/shared-forms' 6import { FormValidatorService } from '@app/shared/shared-forms'
7import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' 7import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
@@ -18,7 +18,7 @@ import { VideoSend } from './video-send'
18 './video-send.scss' 18 './video-send.scss'
19 ] 19 ]
20}) 20})
21export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { 21export class VideoImportUrlComponent extends VideoSend implements OnInit, AfterViewInit, CanComponentDeactivate {
22 @Output() firstStepDone = new EventEmitter<string>() 22 @Output() firstStepDone = new EventEmitter<string>()
23 @Output() firstStepError = new EventEmitter<void>() 23 @Output() firstStepError = new EventEmitter<void>()
24 24
@@ -42,8 +42,9 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
42 protected videoService: VideoService, 42 protected videoService: VideoService,
43 protected videoCaptionService: VideoCaptionService, 43 protected videoCaptionService: VideoCaptionService,
44 private router: Router, 44 private router: Router,
45 private videoImportService: VideoImportService 45 private videoImportService: VideoImportService,
46 ) { 46 private hooks: HooksService
47 ) {
47 super() 48 super()
48 } 49 }
49 50
@@ -51,6 +52,10 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
51 super.ngOnInit() 52 super.ngOnInit()
52 } 53 }
53 54
55 ngAfterViewInit () {
56 this.hooks.runAction('action:video-url-import.init', 'video-edit')
57 }
58
54 canDeactivate () { 59 canDeactivate () {
55 return { canDeactivate: true } 60 return { canDeactivate: true }
56 } 61 }
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 ca21b61cd..effb37077 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,15 +1,15 @@
1import { Subscription } from 'rxjs' 1import { Subscription } from 'rxjs'
2import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http' 2import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http'
3import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' 3import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
4import { Router } from '@angular/router' 4import { Router } from '@angular/router'
5import { AuthService, CanComponentDeactivate, Notifier, ServerService, UserService } from '@app/core' 5import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core'
6import { scrollToTop, uploadErrorHandler } from '@app/helpers' 6import { scrollToTop, uploadErrorHandler } from '@app/helpers'
7import { FormValidatorService } from '@app/shared/shared-forms' 7import { FormValidatorService } from '@app/shared/shared-forms'
8import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' 8import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
9import { LoadingBarService } from '@ngx-loading-bar/core' 9import { LoadingBarService } from '@ngx-loading-bar/core'
10import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
10import { VideoPrivacy } from '@shared/models' 11import { VideoPrivacy } from '@shared/models'
11import { VideoSend } from './video-send' 12import { VideoSend } from './video-send'
12import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
13 13
14@Component({ 14@Component({
15 selector: 'my-video-upload', 15 selector: 'my-video-upload',
@@ -20,7 +20,7 @@ import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
20 './video-send.scss' 20 './video-send.scss'
21 ] 21 ]
22}) 22})
23export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { 23export class VideoUploadComponent extends VideoSend implements OnInit, AfterViewInit, OnDestroy, CanComponentDeactivate {
24 @Output() firstStepDone = new EventEmitter<string>() 24 @Output() firstStepDone = new EventEmitter<string>()
25 @Output() firstStepError = new EventEmitter<void>() 25 @Output() firstStepError = new EventEmitter<void>()
26 @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement> 26 @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
@@ -60,7 +60,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
60 protected videoService: VideoService, 60 protected videoService: VideoService,
61 protected videoCaptionService: VideoCaptionService, 61 protected videoCaptionService: VideoCaptionService,
62 private userService: UserService, 62 private userService: UserService,
63 private router: Router 63 private router: Router,
64 private hooks: HooksService
64 ) { 65 ) {
65 super() 66 super()
66 } 67 }
@@ -79,6 +80,10 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
79 }) 80 })
80 } 81 }
81 82
83 ngAfterViewInit () {
84 this.hooks.runAction('action:video-upload.init', 'video-edit')
85 }
86
82 ngOnDestroy () { 87 ngOnDestroy () {
83 if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe() 88 if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe()
84 } 89 }
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 4a6426d30..9e6fde2e0 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
@@ -1,12 +1,7 @@
1<div> 1<div>
2 <div class="title-block"> 2 <div class="title-block">
3 <h2 class="title-page title-page-single"> 3 <h2 class="title-page title-page-single">
4 <ng-container *ngIf="totalNotDeletedComments > 0; then hasComments; else noComments"></ng-container> 4 {totalNotDeletedComments, plural, =0 {Comments} =1 {1 Comment} other {{{totalNotDeletedComments}} Comments}}
5 <ng-template #hasComments>
6 <ng-container i18n *ngIf="totalNotDeletedComments === 1; else manyComments">1 Comment</ng-container>
7 <ng-template i18n #manyComments>{{ totalNotDeletedComments }} Comments</ng-template>
8 </ng-template>
9 <ng-template i18n #noComments>Comments</ng-template>
10 </h2> 5 </h2>
11 6
12 <my-feed [syndicationItems]="syndicationItems"></my-feed> 7 <my-feed [syndicationItems]="syndicationItems"></my-feed>
@@ -79,15 +74,17 @@
79 <span class="glyphicon glyphicon-menu-down"></span> 74 <span class="glyphicon glyphicon-menu-down"></span>
80 75
81 <ng-container *ngIf="comment.totalRepliesFromVideoAuthor > 0; then hasAuthorComments; else noAuthorComments"></ng-container> 76 <ng-container *ngIf="comment.totalRepliesFromVideoAuthor > 0; then hasAuthorComments; else noAuthorComments"></ng-container>
77
82 <ng-template #hasAuthorComments> 78 <ng-template #hasAuthorComments>
83 <ng-container *ngIf="comment.totalReplies !== comment.totalRepliesFromVideoAuthor; else onlyAuthorComments" i18n> 79 <ng-container *ngIf="comment.totalReplies !== comment.totalRepliesFromVideoAuthor; else onlyAuthorComments" i18n>
84 View {{ comment.totalReplies }} replies from {{ video?.account?.displayName || 'the author' }} and others 80 View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}} from {{ video?.account?.displayName || 'the author' }} and others
85 </ng-container> 81 </ng-container>
86 <ng-template i18n #onlyAuthorComments> 82 <ng-template i18n #onlyAuthorComments>
87 View {{ comment.totalReplies }} replies from {{ video?.account?.displayName || 'the author' }} 83 View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}} from {{ video?.account?.displayName || 'the author' }}
88 </ng-template> 84 </ng-template>
89 </ng-template> 85 </ng-template>
90 <ng-template i18n #noAuthorComments>View {{ comment.totalReplies }} replies</ng-template> 86
87 <ng-template i18n #noAuthorComments>View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}}</ng-template>
91 88
92 <my-small-loader class="comment-thread-loading ml-1" [loading]="threadLoading[comment.id]"></my-small-loader> 89 <my-small-loader class="comment-thread-loading ml-1" [loading]="threadLoading[comment.id]"></my-small-loader>
93 </div> 90 </div>
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 d36dd9e34..210236b61 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
@@ -5,7 +5,6 @@ import { AuthService, ComponentPagination, ConfirmService, hasMoreItems, Notifie
5import { HooksService } from '@app/core/plugins/hooks.service' 5import { HooksService } from '@app/core/plugins/hooks.service'
6import { Syndication, VideoDetails } from '@app/shared/shared-main' 6import { Syndication, VideoDetails } from '@app/shared/shared-main'
7import { VideoComment, VideoCommentService, VideoCommentThreadTree } from '@app/shared/shared-video-comment' 7import { VideoComment, VideoCommentService, VideoCommentThreadTree } from '@app/shared/shared-video-comment'
8import { ThisReceiver } from '@angular/compiler'
9 8
10@Component({ 9@Component({
11 selector: 'my-video-comments', 10 selector: 'my-video-comments',
diff --git a/client/src/app/core/notification/peertube-socket.service.ts b/client/src/app/core/notification/peertube-socket.service.ts
index bc3f7b893..eab1c63f2 100644
--- a/client/src/app/core/notification/peertube-socket.service.ts
+++ b/client/src/app/core/notification/peertube-socket.service.ts
@@ -58,12 +58,11 @@ export class PeerTubeSocket {
58 this.notificationSocket = this.io(environment.apiUrl + '/user-notifications', { 58 this.notificationSocket = this.io(environment.apiUrl + '/user-notifications', {
59 query: { accessToken: this.auth.getAccessToken() } 59 query: { accessToken: this.auth.getAccessToken() }
60 }) 60 })
61
62 this.notificationSocket.on('new-notification', (n: UserNotificationServer) => {
63 this.ngZone.run(() => this.dispatchNotificationEvent('new', n))
64 })
65 }) 61 })
66 62
63 this.notificationSocket.on('new-notification', (n: UserNotificationServer) => {
64 this.ngZone.run(() => this.dispatchNotificationEvent('new', n))
65 })
67 } 66 }
68 67
69 private async initLiveVideosSocket () { 68 private async initLiveVideosSocket () {
diff --git a/client/src/app/core/plugins/hooks.service.ts b/client/src/app/core/plugins/hooks.service.ts
index ec47aa48c..ddde198d2 100644
--- a/client/src/app/core/plugins/hooks.service.ts
+++ b/client/src/app/core/plugins/hooks.service.ts
@@ -3,13 +3,29 @@ import { mergeMap, switchMap } from 'rxjs/operators'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { PluginService } from '@app/core/plugins/plugin.service' 4import { PluginService } from '@app/core/plugins/plugin.service'
5import { ClientActionHookName, ClientFilterHookName, PluginClientScope } from '@shared/models' 5import { ClientActionHookName, ClientFilterHookName, PluginClientScope } from '@shared/models'
6import { AuthService, AuthStatus } from '../auth'
6 7
7type RawFunction<U, T> = (params: U) => T 8type RawFunction<U, T> = (params: U) => T
8type ObservableFunction<U, T> = RawFunction<U, Observable<T>> 9type ObservableFunction<U, T> = RawFunction<U, Observable<T>>
9 10
10@Injectable() 11@Injectable()
11export class HooksService { 12export class HooksService {
12 constructor (private pluginService: PluginService) { } 13 constructor (
14 private authService: AuthService,
15 private pluginService: PluginService
16 ) {
17 // Run auth hooks
18 this.authService.userInformationLoaded
19 .subscribe(() => this.runAction('action:auth-user.information-loaded', 'common', { user: this.authService.getUser() }))
20
21 this.authService.loginChangedSource.subscribe(obj => {
22 if (obj === AuthStatus.LoggedIn) {
23 this.runAction('action:auth-user.logged-in', 'common')
24 } else if (obj === AuthStatus.LoggedOut) {
25 this.runAction('action:auth-user.logged-out', 'common')
26 }
27 })
28 }
13 29
14 wrapObsFun 30 wrapObsFun
15 <P, R, H1 extends ClientFilterHookName, H2 extends ClientFilterHookName> 31 <P, R, H1 extends ClientFilterHookName, H2 extends ClientFilterHookName>
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts
index b755fda2c..54dba5e17 100644
--- a/client/src/app/core/plugins/plugin.service.ts
+++ b/client/src/app/core/plugins/plugin.service.ts
@@ -235,6 +235,12 @@ export class PluginService implements ClientHook {
235 .toPromise() 235 .toPromise()
236 }, 236 },
237 237
238 getServerConfig: () => {
239 return this.server.getConfig()
240 .pipe(catchError(res => this.restExtractor.handleError(res)))
241 .toPromise()
242 },
243
238 isLoggedIn: () => { 244 isLoggedIn: () => {
239 return this.authService.isLoggedIn() 245 return this.authService.isLoggedIn()
240 }, 246 },
diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html
index 03e86b8e6..f84086b4a 100644
--- a/client/src/app/header/search-typeahead.component.html
+++ b/client/src/app/header/search-typeahead.component.html
@@ -34,7 +34,8 @@
34 34
35 <!-- search instructions, when search input is empty --> 35 <!-- search instructions, when search input is empty -->
36 <div *ngIf="areInstructionsDisplayed()" id="typeahead-instructions" class="overflow-hidden"> 36 <div *ngIf="areInstructionsDisplayed()" id="typeahead-instructions" class="overflow-hidden">
37 <div class="d-flex justify-content-between"> 37 <span class="text-muted" i18n>Your query will be matched against video names or descriptions, channel names.</span>
38 <div class="d-flex justify-content-between mt-3">
38 <label class="small-title" i18n>ADVANCED SEARCH</label> 39 <label class="small-title" i18n>ADVANCED SEARCH</label>
39 <div class="advanced-search-status c-help"> 40 <div class="advanced-search-status c-help">
40 <span [ngClass]="canSearchAnyURI ? 'text-success' : 'text-muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows."> 41 <span [ngClass]="canSearchAnyURI ? 'text-success' : 'text-muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows.">
@@ -55,7 +56,6 @@
55 <em>UUID</em> <span class="text-muted" i18n>will list the matching video</span> 56 <em>UUID</em> <span class="text-muted" i18n>will list the matching video</span>
56 </li> 57 </li>
57 </ul> 58 </ul>
58 <span class="text-muted" i18n>Any other input will return matching video or channel names.</span>
59 </div> 59 </div>
60 </div> 60 </div>
61 61
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss
index 2ea66e57d..aa247d268 100644
--- a/client/src/app/menu/menu.component.scss
+++ b/client/src/app/menu/menu.component.scss
@@ -3,6 +3,7 @@
3 3
4$menu-link-icon-size: 22px; 4$menu-link-icon-size: 22px;
5$menu-link-icon-margin-right: 18px; 5$menu-link-icon-margin-right: 18px;
6$footer-links-base-opacity: .8;
6 7
7@mixin menu-link { 8@mixin menu-link {
8 display: flex; 9 display: flex;
@@ -91,168 +92,168 @@ menu {
91 align-items: center; 92 align-items: center;
92 justify-content: left; 93 justify-content: left;
93 94
94 .logged-in-more { 95 my-notification {
95 $main-radius: 25px; 96 margin-left: auto;
97 margin-right: 15px;
98 }
99 }
100}
96 101
97 flex: 1; 102.logged-in-more {
98 margin-left: 13px; 103 $main-radius: 25px;
99 border-radius: $main-radius;
100 transition: all .1s ease-in-out;
101 cursor: pointer;
102 104
103 *, & { 105 flex: 1;
104 line-height: 1; 106 margin-left: 13px;
105 } 107 border-radius: $main-radius;
108 transition: all .1s ease-in-out;
109 cursor: pointer;
106 110
107 &.show { 111 *, & {
108 background-color: rgba(255, 255, 255, 0.20); 112 line-height: 1;
109 box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325); 113 }
110 }
111 114
112 @mixin display-hints($is-mobile: false) { 115 &.show {
113 background-color: rgba(255, 255, 255, 0.15); 116 background-color: rgba(255, 255, 255, 0.20);
114 117 box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325);
115 @if $is-mobile { 118 }
116 .dropdown-toggle-indicator { 119
117 display: inherit !important; 120 @mixin display-hints($is-mobile: false) {
118 } 121 background-color: rgba(255, 255, 255, 0.15);
119 .dropdown-toggle:first-child {
120 padding-right: 30px !important;
121 }
122 }
123 }
124 122
125 &:hover { 123 @if $is-mobile {
126 @include display-hints; 124 .dropdown-toggle-indicator {
125 display: inherit !important;
126 }
127 .dropdown-toggle:first-child {
128 padding-right: 30px !important;
127 } 129 }
130 }
131 }
128 132
129 /* smartphones and touchscreens */ 133 &:hover {
130 @media (hover: none) and (pointer: coarse) { 134 @include display-hints;
131 @include display-hints($is-mobile: true); 135 }
132 136
133 /* fill space when on mobile */ 137 /* smartphones and touchscreens */
134 max-width: calc(100% - 80px); 138 @media (hover: none) and (pointer: coarse) {
135 .dropdown-toggle { 139 @include display-hints($is-mobile: true);
136 max-width: 100%;
137 }
138 .logged-in-info {
139 max-width: calc(100% - 45px) !important;
140 }
141 140
142 } 141 /* fill space when on mobile */
142 max-width: calc(100% - 80px);
143 .dropdown-toggle {
144 max-width: 100%;
145 }
146 .logged-in-info {
147 max-width: calc(100% - 45px) !important;
148 }
143 149
144 .dropdown-toggle-indicator { 150 }
145 position: relative;
146 width: 0;
147 display: none;
148
149 span {
150 position: absolute;
151 right: -35px;
152 top: -8px;
153 color: grey;
154 width: $main-radius;
155 }
156 }
157 151
158 .dropdown-toggle { 152 .dropdown-toggle-indicator {
159 &::after { 153 position: relative;
160 border: none; 154 width: 0;
161 } 155 display: none;
162 }
163 156
164 .dropdown-toggle:first-child { 157 span {
165 display: flex; 158 position: absolute;
166 align-items: center; 159 right: -35px;
167 padding: 5px 7px; 160 top: -8px;
168 border-radius: $main-radius; 161 color: grey;
169 } 162 width: $main-radius;
163 }
164 }
170 165
171 img { 166 .dropdown-toggle {
172 @include avatar(34px); 167 &::after {
168 border: none;
169 }
170 }
173 171
174 margin-right: 10px; 172 .dropdown-toggle:first-child {
175 } 173 display: flex;
174 align-items: center;
175 padding: 5px 7px;
176 border-radius: $main-radius;
177 }
178
179 img {
180 @include avatar(34px);
176 181
177 .logged-in-info { 182 margin-right: 10px;
178 max-width: 105px; 183 }
184}
179 185
180 flex-grow: 1; 186.logged-in-info {
187 max-width: 105px;
181 188
182 .logged-in-display-name, 189 flex-grow: 1;
183 .logged-in-username {
184 @include ellipsis;
185 }
186 190
187 .logged-in-display-name { 191 .logged-in-display-name,
188 font-size: 16px; 192 .logged-in-username {
189 font-weight: $font-semibold; 193 @include ellipsis;
190 color: pvar(--menuForegroundColor); 194 }
191 195
192 @include disable-default-a-behaviour; 196 .logged-in-display-name {
193 } 197 font-size: 16px;
198 font-weight: $font-semibold;
199 color: pvar(--menuForegroundColor);
194 200
195 .logged-in-username { 201 @include disable-default-a-behaviour;
196 font-size: 13px; 202 }
197 color: #C6C6C6;
198 margin-top: 3px;
199 }
200 }
201 }
202 203
203 my-notification { 204 .logged-in-username {
204 margin-left: auto; 205 font-size: 13px;
205 margin-right: 15px; 206 color: #C6C6C6;
206 } 207 margin-top: 3px;
207 } 208 }
209}
208 210
209 .logged-in-menu { 211.logged-in-menu {
210 display: flex; 212 display: flex;
211 flex-direction: column; 213 flex-direction: column;
212 align-items: flex-start; 214 align-items: flex-start;
213 border-top: 1px solid var(--greyForegroundColor); 215 border-top: 1px solid var(--greyForegroundColor);
214 line-height: $line-height-normal; 216 line-height: $line-height-normal;
215 217
216 a { 218 a {
217 @include menu-link; 219 @include menu-link;
218 @include disable-default-a-behaviour; 220 @include disable-default-a-behaviour;
219 221
220 $icon-size: 13px; 222 $icon-size: 13px;
221 $additional-margin: ($menu-link-icon-size - $icon-size) / 2; 223 $additional-margin: ($menu-link-icon-size - $icon-size) / 2;
222 224
223 font-size: 14px; 225 font-size: 14px;
224 width: 100%; 226 width: 100%;
225 min-height: 35px; 227 min-height: 35px;
226 228
227 my-global-icon { 229 my-global-icon {
228 width: $icon-size; 230 width: $icon-size;
229 height: $icon-size; 231 height: $icon-size;
230 232
231 // Keep aligned with other icons 233 // Keep aligned with other icons
232 margin-left: $additional-margin; 234 margin-left: $additional-margin;
233 235
234 &[iconName="channel"] { 236 &[iconName="channel"] {
235 margin-top: -2px; 237 margin-top: -2px;
236 }
237 } 238 }
239 }
238 240
239 &.active, 241 &.active,
240 &:hover, 242 &:hover,
241 &:focus-visible { 243 &:focus-visible {
242 my-global-icon { 244 my-global-icon {
243 @include apply-svg-color(var(--menuForegroundColor)); 245 @include apply-svg-color(var(--menuForegroundColor));
244 }
245 } 246 }
247 }
246 248
247 &.active { 249 &.active {
248 $border-left-width: 4px; 250 $border-left-width: 4px;
249 251
250 font-weight: $font-semibold; 252 font-weight: $font-semibold;
251 border-left: $border-left-width solid var(--mainColor); 253 border-left: $border-left-width solid var(--mainColor);
252 254
253 my-global-icon { 255 my-global-icon {
254 margin-left: $additional-margin - $border-left-width; 256 margin-left: $additional-margin - $border-left-width;
255 }
256 } 257 }
257 } 258 }
258 } 259 }
@@ -333,50 +334,48 @@ menu {
333 flex-direction: column; 334 flex-direction: column;
334 padding: 0 $menu-lateral-padding; 335 padding: 0 $menu-lateral-padding;
335 } 336 }
337}
336 338
337 $footer-links-base-opacity: .8; 339.footer-links {
338 340 &, > div {
339 .footer-links { 341 display: flex;
340 &, > div { 342 flex-wrap: wrap;
341 display: flex; 343 }
342 flex-wrap: wrap;
343 }
344 344
345 a, span[role=button] { 345 a, span[role=button] {
346 display: inline-block; 346 display: inline-block;
347 text-decoration: none; 347 text-decoration: none;
348 color: pvar(--menuForegroundColor); 348 color: pvar(--menuForegroundColor);
349 opacity: $footer-links-base-opacity; 349 opacity: $footer-links-base-opacity;
350 white-space: nowrap;
351 font-size: 90%;
352 font-weight: 500;
353 line-height: 1.4rem;
354 margin-right: 8px;
355
356 &.inline-global-icon {
357 display: inline-flex;
358 align-items: center;
350 white-space: nowrap; 359 white-space: nowrap;
351 font-size: 90%; 360 height: 1.4rem;
352 font-weight: 500; 361
353 line-height: 1.4rem; 362 my-global-icon {
354 margin-right: 8px; 363 @include apply-svg-color(pvar(--menuForegroundColor));
355 364
356 &.inline-global-icon { 365 display: flex;
357 display: inline-flex; 366 width: auto;
358 align-items: center; 367 height: 90%;
359 white-space: nowrap; 368 margin-right: .2rem;
360 height: 1.4rem;
361
362 my-global-icon {
363 @include apply-svg-color(pvar(--menuForegroundColor));
364
365 display: flex;
366 width: auto;
367 height: 90%;
368 margin-right: .2rem;
369 }
370 } 369 }
371 } 370 }
372 } 371 }
372}
373 373
374 .footer-copyleft small a { 374.footer-copyleft small a {
375 @include disable-default-a-behaviour; 375 @include disable-default-a-behaviour;
376 376
377 color: pvar(--menuForegroundColor); 377 color: pvar(--menuForegroundColor);
378 opacity: $footer-links-base-opacity - .2; 378 opacity: $footer-links-base-opacity - .2;
379 }
380} 379}
381 380
382.dropdown { 381.dropdown {
diff --git a/client/src/app/modal/instance-config-warning-modal.component.html b/client/src/app/modal/instance-config-warning-modal.component.html
index 5a8adf726..498adfeff 100644
--- a/client/src/app/modal/instance-config-warning-modal.component.html
+++ b/client/src/app/modal/instance-config-warning-modal.component.html
@@ -15,7 +15,7 @@
15 15
16 <li i18n *ngIf="!about.instance.administrator">Who you are</li> 16 <li i18n *ngIf="!about.instance.administrator">Who you are</li>
17 <li i18n *ngIf="!about.instance.maintenanceLifetime">How long you plan to maintain your instance</li> 17 <li i18n *ngIf="!about.instance.maintenanceLifetime">How long you plan to maintain your instance</li>
18 <li i18n *ngIf="!about.instance.businessModel">How you plan to pay your instance</li> 18 <li i18n *ngIf="!about.instance.businessModel">How you plan to pay for keeping your instance running</li>
19 19
20 <li i18n *ngIf="!about.instance.moderationInformation">How you will moderate your instance</li> 20 <li i18n *ngIf="!about.instance.moderationInformation">How you will moderate your instance</li>
21 <li i18n *ngIf="!about.instance.terms">Instance terms</li> 21 <li i18n *ngIf="!about.instance.terms">Instance terms</li>
diff --git a/client/src/app/shared/shared-forms/input-toggle-hidden.component.html b/client/src/app/shared/shared-forms/input-toggle-hidden.component.html
index e7441e4c1..9f252f299 100644
--- a/client/src/app/shared/shared-forms/input-toggle-hidden.component.html
+++ b/client/src/app/shared/shared-forms/input-toggle-hidden.component.html
@@ -12,9 +12,10 @@
12 12
13 <button 13 <button
14 *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button" 14 *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button"
15 class="btn btn-outline-secondary" i18n-title title="Copy" 15 class="btn btn-outline-secondary text-uppercase" i18n-title title="Copy"
16 > 16 >
17 <span class="glyphicon glyphicon-copy"></span> 17 <span class="glyphicon glyphicon-duplicate"></span>
18 Copy
18 </button> 19 </button>
19 </div> 20 </div>
20</div> 21</div>
diff --git a/client/src/app/shared/shared-forms/select/select-options.component.ts b/client/src/app/shared/shared-forms/select/select-options.component.ts
index 2890670e5..8482b9dea 100644
--- a/client/src/app/shared/shared-forms/select/select-options.component.ts
+++ b/client/src/app/shared/shared-forms/select/select-options.component.ts
@@ -1,4 +1,4 @@
1import { Component, forwardRef, Input } from '@angular/core' 1import { Component, forwardRef, HostListener, Input } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { SelectOptionsItem } from '../../../../types/select-options-item.model' 3import { SelectOptionsItem } from '../../../../types/select-options-item.model'
4 4
@@ -26,6 +26,13 @@ export class SelectOptionsComponent implements ControlValueAccessor {
26 26
27 propagateChange = (_: any) => { /* empty */ } 27 propagateChange = (_: any) => { /* empty */ }
28 28
29 // Allow plugins to update our value
30 @HostListener('change', [ '$event.target' ])
31 handleChange (event: any) {
32 this.writeValue(event.value)
33 this.onModelChange()
34 }
35
29 writeValue (id: number | string) { 36 writeValue (id: number | string) {
30 this.selectedId = id 37 this.selectedId = id
31 } 38 }
diff --git a/client/src/app/shared/shared-main/angular/autofocus.directive.ts b/client/src/app/shared/shared-main/angular/autofocus.directive.ts
new file mode 100644
index 000000000..5f087d79d
--- /dev/null
+++ b/client/src/app/shared/shared-main/angular/autofocus.directive.ts
@@ -0,0 +1,12 @@
1import { AfterViewInit, Directive, ElementRef } from '@angular/core'
2
3@Directive({
4 selector: '[autofocus]'
5})
6export class AutofocusDirective implements AfterViewInit {
7 constructor (private host: ElementRef) { }
8
9 ngAfterViewInit () {
10 this.host.nativeElement.focus()
11 }
12}
diff --git a/client/src/app/shared/shared-main/angular/index.ts b/client/src/app/shared/shared-main/angular/index.ts
index 29f8b3650..8ea47bb33 100644
--- a/client/src/app/shared/shared-main/angular/index.ts
+++ b/client/src/app/shared/shared-main/angular/index.ts
@@ -1,3 +1,4 @@
1export * from './autofocus.directive'
1export * from './bytes.pipe' 2export * from './bytes.pipe'
2export * from './duration-formatter.pipe' 3export * from './duration-formatter.pipe'
3export * from './from-now.pipe' 4export * from './from-now.pipe'
diff --git a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
index 3ddaffbdf..4fe3b964d 100644
--- a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
+++ b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
@@ -27,7 +27,9 @@ export class AuthInterceptor implements HttpInterceptor {
27 catchError((err: HttpErrorResponse) => { 27 catchError((err: HttpErrorResponse) => {
28 if (err.status === HttpStatusCode.UNAUTHORIZED_401 && err.error && err.error.code === 'invalid_token') { 28 if (err.status === HttpStatusCode.UNAUTHORIZED_401 && err.error && err.error.code === 'invalid_token') {
29 return this.handleTokenExpired(req, next) 29 return this.handleTokenExpired(req, next)
30 } else if (err.status === HttpStatusCode.UNAUTHORIZED_401) { 30 }
31
32 if (err.status === HttpStatusCode.UNAUTHORIZED_401) {
31 return this.handleNotAuthenticated(err) 33 return this.handleNotAuthenticated(err)
32 } 34 }
33 35
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts
index 9d550996d..3e21d491a 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -19,6 +19,7 @@ import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
19import { SharedGlobalIconModule } from '../shared-icons' 19import { SharedGlobalIconModule } from '../shared-icons'
20import { AccountService, ActorAvatarInfoComponent, VideoAvatarChannelComponent } from './account' 20import { AccountService, ActorAvatarInfoComponent, VideoAvatarChannelComponent } from './account'
21import { 21import {
22 AutofocusDirective,
22 BytesPipe, 23 BytesPipe,
23 DurationFormatterPipe, 24 DurationFormatterPipe,
24 FromNowPipe, 25 FromNowPipe,
@@ -71,6 +72,7 @@ import { VideoChannelService } from './video-channel'
71 NumberFormatterPipe, 72 NumberFormatterPipe,
72 BytesPipe, 73 BytesPipe,
73 DurationFormatterPipe, 74 DurationFormatterPipe,
75 AutofocusDirective,
74 76
75 InfiniteScrollerDirective, 77 InfiniteScrollerDirective,
76 PeerTubeTemplateDirective, 78 PeerTubeTemplateDirective,
@@ -125,6 +127,7 @@ import { VideoChannelService } from './video-channel'
125 BytesPipe, 127 BytesPipe,
126 NumberFormatterPipe, 128 NumberFormatterPipe,
127 DurationFormatterPipe, 129 DurationFormatterPipe,
130 AutofocusDirective,
128 131
129 InfiniteScrollerDirective, 132 InfiniteScrollerDirective,
130 PeerTubeTemplateDirective, 133 PeerTubeTemplateDirective,
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts
index 1211995fd..88a4811da 100644
--- a/client/src/app/shared/shared-main/users/user-notification.model.ts
+++ b/client/src/app/shared/shared-main/users/user-notification.model.ts
@@ -6,6 +6,7 @@ import {
6 AbuseState, 6 AbuseState,
7 ActorInfo, 7 ActorInfo,
8 FollowState, 8 FollowState,
9 PluginType,
9 UserNotification as UserNotificationServer, 10 UserNotification as UserNotificationServer,
10 UserNotificationType, 11 UserNotificationType,
11 UserRight, 12 UserRight,
@@ -74,20 +75,40 @@ export class UserNotification implements UserNotificationServer {
74 } 75 }
75 } 76 }
76 77
78 plugin?: {
79 name: string
80 type: PluginType
81 latestVersion: string
82 }
83
84 peertube?: {
85 latestVersion: string
86 }
87
77 createdAt: string 88 createdAt: string
78 updatedAt: string 89 updatedAt: string
79 90
80 // Additional fields 91 // Additional fields
81 videoUrl?: string 92 videoUrl?: string
82 commentUrl?: any[] 93 commentUrl?: any[]
94
83 abuseUrl?: string 95 abuseUrl?: string
84 abuseQueryParams?: { [id: string]: string } = {} 96 abuseQueryParams?: { [id: string]: string } = {}
97
85 videoAutoBlacklistUrl?: string 98 videoAutoBlacklistUrl?: string
99
86 accountUrl?: string 100 accountUrl?: string
101
87 videoImportIdentifier?: string 102 videoImportIdentifier?: string
88 videoImportUrl?: string 103 videoImportUrl?: string
104
89 instanceFollowUrl?: string 105 instanceFollowUrl?: string
90 106
107 peertubeVersionLink?: string
108
109 pluginUrl?: string
110 pluginQueryParams?: { [id: string]: string } = {}
111
91 constructor (hash: UserNotificationServer, user: AuthUser) { 112 constructor (hash: UserNotificationServer, user: AuthUser) {
92 this.id = hash.id 113 this.id = hash.id
93 this.type = hash.type 114 this.type = hash.type
@@ -114,6 +135,9 @@ export class UserNotification implements UserNotificationServer {
114 this.actorFollow = hash.actorFollow 135 this.actorFollow = hash.actorFollow
115 if (this.actorFollow) this.setAccountAvatarUrl(this.actorFollow.follower) 136 if (this.actorFollow) this.setAccountAvatarUrl(this.actorFollow.follower)
116 137
138 this.plugin = hash.plugin
139 this.peertube = hash.peertube
140
117 this.createdAt = hash.createdAt 141 this.createdAt = hash.createdAt
118 this.updatedAt = hash.updatedAt 142 this.updatedAt = hash.updatedAt
119 143
@@ -197,6 +221,15 @@ export class UserNotification implements UserNotificationServer {
197 case UserNotificationType.AUTO_INSTANCE_FOLLOWING: 221 case UserNotificationType.AUTO_INSTANCE_FOLLOWING:
198 this.instanceFollowUrl = '/admin/follows/following-list' 222 this.instanceFollowUrl = '/admin/follows/following-list'
199 break 223 break
224
225 case UserNotificationType.NEW_PEERTUBE_VERSION:
226 this.peertubeVersionLink = 'https://joinpeertube.org/news'
227 break
228
229 case UserNotificationType.NEW_PLUGIN_VERSION:
230 this.pluginUrl = `/admin/plugins/list-installed`
231 this.pluginQueryParams.pluginType = this.plugin.type + ''
232 break
200 } 233 }
201 } catch (err) { 234 } catch (err) {
202 this.type = null 235 this.type = null
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html
index 265af8d55..325f0eaae 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.html
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.html
@@ -4,7 +4,7 @@
4 <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)"> 4 <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)">
5 5
6 <ng-container [ngSwitch]="notification.type"> 6 <ng-container [ngSwitch]="notification.type">
7 <ng-container *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION"> 7 <ng-container *ngSwitchCase="1"> <!-- UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION -->
8 <ng-container *ngIf="notification.video; then hasVideo; else noVideo"></ng-container> 8 <ng-container *ngIf="notification.video; then hasVideo; else noVideo"></ng-container>
9 9
10 <ng-template #hasVideo> 10 <ng-template #hasVideo>
@@ -26,7 +26,7 @@
26 </ng-template> 26 </ng-template>
27 </ng-container> 27 </ng-container>
28 28
29 <ng-container *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO"> 29 <ng-container *ngSwitchCase="5"> <!-- UserNotificationType.UNBLACKLIST_ON_MY_VIDEO -->
30 <my-global-icon iconName="undo" aria-hidden="true"></my-global-icon> 30 <my-global-icon iconName="undo" aria-hidden="true"></my-global-icon>
31 31
32 <div class="message" i18n> 32 <div class="message" i18n>
@@ -34,7 +34,7 @@
34 </div> 34 </div>
35 </ng-container> 35 </ng-container>
36 36
37 <ng-container *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO"> 37 <ng-container *ngSwitchCase="4"> <!-- UserNotificationType.BLACKLIST_ON_MY_VIDEO -->
38 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon> 38 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon>
39 39
40 <div class="message" i18n> 40 <div class="message" i18n>
@@ -42,7 +42,7 @@
42 </div> 42 </div>
43 </ng-container> 43 </ng-container>
44 44
45 <ng-container *ngSwitchCase="UserNotificationType.NEW_ABUSE_FOR_MODERATORS"> 45 <ng-container *ngSwitchCase="3"> <!-- UserNotificationType.NEW_ABUSE_FOR_MODERATORS -->
46 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> 46 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
47 47
48 <div class="message" *ngIf="notification.videoUrl" i18n> 48 <div class="message" *ngIf="notification.videoUrl" i18n>
@@ -63,7 +63,7 @@
63 </div> 63 </div>
64 </ng-container> 64 </ng-container>
65 65
66 <ng-container *ngSwitchCase="UserNotificationType.ABUSE_STATE_CHANGE"> 66 <ng-container *ngSwitchCase="15"> <!-- UserNotificationType.ABUSE_STATE_CHANGE -->
67 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> 67 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
68 68
69 <div class="message" i18n> 69 <div class="message" i18n>
@@ -73,7 +73,7 @@
73 </div> 73 </div>
74 </ng-container> 74 </ng-container>
75 75
76 <ng-container *ngSwitchCase="UserNotificationType.ABUSE_NEW_MESSAGE"> 76 <ng-container *ngSwitchCase="16"> <!-- UserNotificationType.ABUSE_NEW_MESSAGE -->
77 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> 77 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
78 78
79 <div class="message" i18n> 79 <div class="message" i18n>
@@ -81,7 +81,7 @@
81 </div> 81 </div>
82 </ng-container> 82 </ng-container>
83 83
84 <ng-container *ngSwitchCase="UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS"> 84 <ng-container *ngSwitchCase="12"> <!-- UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS -->
85 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon> 85 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon>
86 86
87 <div class="message" i18n> 87 <div class="message" i18n>
@@ -89,7 +89,7 @@
89 </div> 89 </div>
90 </ng-container> 90 </ng-container>
91 91
92 <ng-container *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO"> 92 <ng-container *ngSwitchCase="2">
93 <ng-container *ngIf="notification.comment"> 93 <ng-container *ngIf="notification.comment">
94 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> 94 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
95 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> 95 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
@@ -109,7 +109,7 @@
109 </ng-container> 109 </ng-container>
110 </ng-container> 110 </ng-container>
111 111
112 <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED"> 112 <ng-container *ngSwitchCase="6"> <!-- UserNotificationType.MY_VIDEO_PUBLISHED -->
113 <my-global-icon iconName="film" aria-hidden="true"></my-global-icon> 113 <my-global-icon iconName="film" aria-hidden="true"></my-global-icon>
114 114
115 <div class="message" i18n> 115 <div class="message" i18n>
@@ -117,7 +117,7 @@
117 </div> 117 </div>
118 </ng-container> 118 </ng-container>
119 119
120 <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS"> 120 <ng-container *ngSwitchCase="7"> <!-- UserNotificationType.MY_VIDEO_IMPORT_SUCCESS -->
121 <my-global-icon iconName="cloud-download" aria-hidden="true"></my-global-icon> 121 <my-global-icon iconName="cloud-download" aria-hidden="true"></my-global-icon>
122 122
123 <div class="message" i18n> 123 <div class="message" i18n>
@@ -125,7 +125,7 @@
125 </div> 125 </div>
126 </ng-container> 126 </ng-container>
127 127
128 <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR"> 128 <ng-container *ngSwitchCase="8"> <!-- UserNotificationType.MY_VIDEO_IMPORT_ERROR -->
129 <my-global-icon iconName="cloud-error" aria-hidden="true"></my-global-icon> 129 <my-global-icon iconName="cloud-error" aria-hidden="true"></my-global-icon>
130 130
131 <div class="message" i18n> 131 <div class="message" i18n>
@@ -133,7 +133,7 @@
133 </div> 133 </div>
134 </ng-container> 134 </ng-container>
135 135
136 <ng-container *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION"> 136 <ng-container *ngSwitchCase="9"> <!-- UserNotificationType.NEW_USER_REGISTRATION -->
137 <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon> 137 <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon>
138 138
139 <div class="message" i18n> 139 <div class="message" i18n>
@@ -141,7 +141,7 @@
141 </div> 141 </div>
142 </ng-container> 142 </ng-container>
143 143
144 <ng-container *ngSwitchCase="UserNotificationType.NEW_FOLLOW"> 144 <ng-container *ngSwitchCase="10"> <!-- UserNotificationType.NEW_FOLLOW -->
145 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> 145 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
146 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" /> 146 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" />
147 </a> 147 </a>
@@ -154,7 +154,7 @@
154 </div> 154 </div>
155 </ng-container> 155 </ng-container>
156 156
157 <ng-container *ngSwitchCase="UserNotificationType.COMMENT_MENTION"> 157 <ng-container *ngSwitchCase="11">
158 <ng-container *ngIf="notification.comment"> 158 <ng-container *ngIf="notification.comment">
159 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> 159 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
160 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> 160 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
@@ -174,7 +174,7 @@
174 </ng-container> 174 </ng-container>
175 </ng-container> 175 </ng-container>
176 176
177 <ng-container *ngSwitchCase="UserNotificationType.NEW_INSTANCE_FOLLOWER"> 177 <ng-container *ngSwitchCase="13"> <!-- UserNotificationType.NEW_INSTANCE_FOLLOWER -->
178 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon> 178 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon>
179 179
180 <div class="message" i18n> 180 <div class="message" i18n>
@@ -183,7 +183,7 @@
183 </div> 183 </div>
184 </ng-container> 184 </ng-container>
185 185
186 <ng-container *ngSwitchCase="UserNotificationType.AUTO_INSTANCE_FOLLOWING"> 186 <ng-container *ngSwitchCase="14"> <!-- UserNotificationType.AUTO_INSTANCE_FOLLOWING -->
187 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon> 187 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon>
188 188
189 <div class="message" i18n> 189 <div class="message" i18n>
@@ -191,6 +191,22 @@
191 </div> 191 </div>
192 </ng-container> 192 </ng-container>
193 193
194 <ng-container *ngSwitchCase="17"> <!-- UserNotificationType.NEW_PLUGIN_VERSION -->
195 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
196
197 <div class="message" i18n>
198 <a (click)="markAsRead(notification)" [routerLink]="notification.pluginUrl" [queryParams]="notification.pluginQueryParams">A new version of the plugin/theme {{ notification.plugin.name }}</a> is available: {{ notification.plugin.latestVersion }}
199 </div>
200 </ng-container>
201
202 <ng-container *ngSwitchCase="18"> <!-- UserNotificationType.NEW_PEERTUBE_VERSION -->
203 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
204
205 <div class="message" i18n>
206 <a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }}
207 </div>
208 </ng-container>
209
194 <ng-container *ngSwitchDefault> 210 <ng-container *ngSwitchDefault>
195 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> 211 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
196 212
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.ts b/client/src/app/shared/shared-main/users/user-notifications.component.ts
index 387c49d94..d7c722355 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.ts
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.ts
@@ -21,9 +21,6 @@ export class UserNotificationsComponent implements OnInit {
21 notifications: UserNotification[] = [] 21 notifications: UserNotification[] = []
22 sortField = 'createdAt' 22 sortField = 'createdAt'
23 23
24 // So we can access it in the template
25 UserNotificationType = UserNotificationType
26
27 componentPagination: ComponentPagination 24 componentPagination: ComponentPagination
28 25
29 onDataSubject = new Subject<any[]>() 26 onDataSubject = new Subject<any[]>()
@@ -48,7 +45,7 @@ export class UserNotificationsComponent implements OnInit {
48 } 45 }
49 46
50 loadNotifications (reset?: boolean) { 47 loadNotifications (reset?: boolean) {
51 this.userNotificationService.listMyNotifications({ 48 const options = {
52 pagination: this.componentPagination, 49 pagination: this.componentPagination,
53 ignoreLoadingBar: this.ignoreLoadingBar, 50 ignoreLoadingBar: this.ignoreLoadingBar,
54 sort: { 51 sort: {
@@ -56,7 +53,9 @@ export class UserNotificationsComponent implements OnInit {
56 // if we order by creation date, we want DESC. all other fields are ASC (like unread). 53 // if we order by creation date, we want DESC. all other fields are ASC (like unread).
57 order: this.sortField === 'createdAt' ? -1 : 1 54 order: this.sortField === 'createdAt' ? -1 : 1
58 } 55 }
59 }) 56 }
57
58 this.userNotificationService.listMyNotifications(options)
60 .subscribe( 59 .subscribe(
61 result => { 60 result => {
62 this.notifications = reset ? result.data : this.notifications.concat(result.data) 61 this.notifications = reset ? result.data : this.notifications.concat(result.data)
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.html b/client/src/app/shared/shared-video-miniature/video-download.component.html
index 4608e93e7..0e659fbe2 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.html
@@ -36,7 +36,7 @@
36 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> 36 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" />
37 <div class="input-group-append" *ngIf="!isConfidentialVideo()"> 37 <div class="input-group-append" *ngIf="!isConfidentialVideo()">
38 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> 38 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary">
39 <span class="glyphicon glyphicon-copy"></span> 39 <span class="glyphicon glyphicon-duplicate"></span>
40 </button> 40 </button>
41 </div> 41 </div>
42 </div> 42 </div>
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.ts b/client/src/app/shared/shared-video-miniature/video-download.component.ts
index 90f4daf7c..e0b7b51ff 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.ts
@@ -1,7 +1,9 @@
1import { mapValues, pick } from 'lodash-es' 1import { mapValues, pick } from 'lodash-es'
2import { pipe } from 'rxjs'
3import { tap } from 'rxjs/operators'
2import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' 4import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core'
3import { AuthService, Notifier } from '@app/core' 5import { AuthService, HooksService, Notifier } from '@app/core'
4import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
5import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' 7import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models'
6import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main' 8import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main'
7 9
@@ -16,7 +18,7 @@ type FileMetadata = { [key: string]: { label: string, value: string }}
16export class VideoDownloadComponent { 18export class VideoDownloadComponent {
17 @ViewChild('modal', { static: true }) modal: ElementRef 19 @ViewChild('modal', { static: true }) modal: ElementRef
18 20
19 downloadType: 'direct' | 'torrent' = 'torrent' 21 downloadType: 'direct' | 'torrent' = 'direct'
20 resolutionId: number | string = -1 22 resolutionId: number | string = -1
21 subtitleLanguageId: string 23 subtitleLanguageId: string
22 24
@@ -26,7 +28,7 @@ export class VideoDownloadComponent {
26 videoFileMetadataVideoStream: FileMetadata | undefined 28 videoFileMetadataVideoStream: FileMetadata | undefined
27 videoFileMetadataAudioStream: FileMetadata | undefined 29 videoFileMetadataAudioStream: FileMetadata | undefined
28 videoCaptions: VideoCaption[] 30 videoCaptions: VideoCaption[]
29 activeModal: NgbActiveModal 31 activeModal: NgbModalRef
30 32
31 type: DownloadType = 'video' 33 type: DownloadType = 'video'
32 34
@@ -38,7 +40,8 @@ export class VideoDownloadComponent {
38 private notifier: Notifier, 40 private notifier: Notifier,
39 private modalService: NgbModal, 41 private modalService: NgbModal,
40 private videoService: VideoService, 42 private videoService: VideoService,
41 private auth: AuthService 43 private auth: AuthService,
44 private hooks: HooksService
42 ) { 45 ) {
43 this.bytesPipe = new BytesPipe() 46 this.bytesPipe = new BytesPipe()
44 this.numbersPipe = new NumberFormatterPipe(this.localeId) 47 this.numbersPipe = new NumberFormatterPipe(this.localeId)
@@ -64,7 +67,12 @@ export class VideoDownloadComponent {
64 67
65 this.resolutionId = this.getVideoFiles()[0].resolution.id 68 this.resolutionId = this.getVideoFiles()[0].resolution.id
66 this.onResolutionIdChange() 69 this.onResolutionIdChange()
70
67 if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id 71 if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id
72
73 this.activeModal.shown.subscribe(() => {
74 this.hooks.runAction('action:modal.video-download.shown', 'common')
75 })
68 } 76 }
69 77
70 onClose () { 78 onClose () {
@@ -88,6 +96,7 @@ export class VideoDownloadComponent {
88 if (this.videoFile.metadata || !this.videoFile.metadataUrl) return 96 if (this.videoFile.metadata || !this.videoFile.metadataUrl) return
89 97
90 await this.hydrateMetadataFromMetadataUrl(this.videoFile) 98 await this.hydrateMetadataFromMetadataUrl(this.videoFile)
99 if (!this.videoFile.metadata) return
91 100
92 this.videoFileMetadataFormat = this.videoFile 101 this.videoFileMetadataFormat = this.videoFile
93 ? this.getMetadataFormat(this.videoFile.metadata.format) 102 ? this.getMetadataFormat(this.videoFile.metadata.format)
@@ -201,7 +210,7 @@ export class VideoDownloadComponent {
201 210
202 private hydrateMetadataFromMetadataUrl (file: VideoFile) { 211 private hydrateMetadataFromMetadataUrl (file: VideoFile) {
203 const observable = this.videoService.getVideoFileMetadata(file.metadataUrl) 212 const observable = this.videoService.getVideoFileMetadata(file.metadataUrl)
204 observable.subscribe(res => file.metadata = res) 213 .pipe(tap(res => file.metadata = res))
205 214
206 return observable.toPromise() 215 return observable.toPromise()
207 } 216 }
diff --git a/client/src/assets/images/feather/cloud-download.svg b/client/src/assets/images/feather/cloud-download.svg
index 3a4e58df1..16526d338 100644
--- a/client/src/assets/images/feather/cloud-download.svg
+++ b/client/src/assets/images/feather/cloud-download.svg
@@ -1,6 +1,6 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> 1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2 <defs/> 2 <defs/>
3 <g fill="none" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-width="2"> 3 <g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-width="2">
4 <path stroke-linejoin="round" d="M8 17H5h0a4 4 0 111-7.9v-.6a5.5 5.5 0 0110.8-1.4A5 5 0 0123 12a5 5 0 01-5 5h-2"/> 4 <path stroke-linejoin="round" d="M8 17H5h0a4 4 0 111-7.9v-.6a5.5 5.5 0 0110.8-1.4A5 5 0 0123 12a5 5 0 01-5 5h-2"/>
5 <path d="M12 13v8"/> 5 <path d="M12 13v8"/>
6 <path stroke-linejoin="round" d="M15 20l-3 3-3-3"/> 6 <path stroke-linejoin="round" d="M15 20l-3 3-3-3"/>
diff --git a/client/src/assets/images/feather/subscriptions.svg b/client/src/assets/images/feather/subscriptions.svg
deleted file mode 100644
index c7216352a..000000000
--- a/client/src/assets/images/feather/subscriptions.svg
+++ /dev/null
@@ -1,19 +0,0 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2 <defs/>
3 <defs>
4 <linearGradient id="a" x1="50%" x2="50%" y1="0%" y2="97.33%">
5 <stop stop-color="#000" offset="0%"/>
6 <stop stop-color="#000" offset="100%" stop-opacity=".25"/>
7 </linearGradient>
8 <linearGradient id="b" x1="50%" x2="50%" y1="0%" y2="97.86%">
9 <stop stop-color="#000" offset="0%"/>
10 <stop stop-color="#000" offset="100%" stop-opacity=".25"/>
11 </linearGradient>
12 </defs>
13 <g fill="none" fill-rule="evenodd">
14 <circle cx="12" cy="10" r="3" fill="#000"/>
15 <path fill="url(#a)" fill-rule="nonzero" d="M16.39 13.85A5.68 5.68 0 0018 10c0-3.26-2.74-6-6-6s-6 2.74-6 6c0 1.42.58 2.7 1.62 3.85a.5.5 0 00.74-.67A4.7 4.7 0 017 10c0-2.7 2.3-5 5-5s5 2.3 5 5a4.7 4.7 0 01-1.36 3.18.5.5 0 10.75.67z"/>
16 <path fill="url(#b)" fill-rule="nonzero" d="M17.57 18.3A9.99 9.99 0 0012 0a10 10 0 00-5.56 18.31 1 1 0 101.11-1.66 7.99 7.99 0 118.9 0 1 1 0 101.12 1.66z"/>
17 <path fill="#000" d="M9.33 15.98A1.64 1.64 0 0111 14h2c1.1 0 1.85.88 1.67 1.98l-1 6.04c-.1.54-.61.98-1.17.98h-1c-.55 0-1.07-.43-1.16-.98l-1.01-6.04z"/>
18 </g>
19</svg>
diff --git a/client/src/assets/images/misc/language.svg b/client/src/assets/images/misc/language.svg
index 8fd1d0ba8..204136f0b 100644
--- a/client/src/assets/images/misc/language.svg
+++ b/client/src/assets/images/misc/language.svg
@@ -1,7 +1,7 @@
1<svg xmlns="http://www.w3.org/2000/svg" transform="scale(1.2)" viewBox="0 0 200 200"> 1<svg xmlns="http://www.w3.org/2000/svg" transform="scale(1.2)" viewBox="0 0 200 200">
2 <defs/> 2 <defs/>
3 <path stroke="#000" stroke-width="3" d="M93 155H42a18 18 0 01-18-18V29a5 5 0 015-5h89a5 5 0 015 6L98 151a5 5 0 01-5 4zM34 34v103a8 8 0 008 8h47l22-111z"/> 3 <path stroke="currentColor" stroke-width="3" d="M93 155H42a18 18 0 01-18-18V29a5 5 0 015-5h89a5 5 0 015 6L98 151a5 5 0 01-5 4zM34 34v103a8 8 0 008 8h47l22-111z"/>
4 <path stroke="#000" stroke-width="3" d="M171 176H75a5 5 0 01-5-6l4-21a5 5 0 0110 2l-3 15h85V63a8 8 0 00-8-8h-45a5 5 0 010-10h45a18 18 0 0118 18v108a5 5 0 01-5 5zM50 92h0a5 5 0 01-5-5V63a17 17 0 0135 0v24a5 5 0 01-10 0V62a7 7 0 00-15 0v25a5 5 0 01-5 5z"/> 4 <path stroke="currentColor" stroke-width="3" d="M171 176H75a5 5 0 01-5-6l4-21a5 5 0 0110 2l-3 15h85V63a8 8 0 00-8-8h-45a5 5 0 010-10h45a18 18 0 0118 18v108a5 5 0 01-5 5zM50 92h0a5 5 0 01-5-5V63a17 17 0 0135 0v24a5 5 0 01-10 0V62a7 7 0 00-15 0v25a5 5 0 01-5 5z"/>
5 <path stroke="#000" stroke-width="3" d="M75 76H50a5 5 0 010-10h25a5 5 0 010 10zM120 155a5 5 0 01-3-9l21-21h-18a5 5 0 010-10h30a5 5 0 014 9l-30 30a5 5 0 01-4 1z"/> 5 <path stroke="currentColor" stroke-width="3" d="M75 76H50a5 5 0 010-10h25a5 5 0 010 10zM120 155a5 5 0 01-3-9l21-21h-18a5 5 0 010-10h30a5 5 0 014 9l-30 30a5 5 0 01-4 1z"/>
6 <path stroke="#000" stroke-width="3" d="M150 155a5 5 0 01-4-1l-14-15a5 5 0 017-7l15 14a5 5 0 01-4 9zM143 110h-15a5 5 0 110-10h15a5 5 0 010 10z"/> 6 <path stroke="currentColor" stroke-width="3" d="M150 155a5 5 0 01-4-1l-14-15a5 5 0 017-7l15 14a5 5 0 01-4 9zM143 110h-15a5 5 0 110-10h15a5 5 0 010 10z"/>
7</svg> 7</svg>
diff --git a/client/src/assets/images/misc/npm.svg b/client/src/assets/images/misc/npm.svg
index 1d1d82784..8a4869f12 100644
--- a/client/src/assets/images/misc/npm.svg
+++ b/client/src/assets/images/misc/npm.svg
@@ -1,5 +1,5 @@
1<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 18 7" style="transform: scale(1.3) translateY(1px);"> 1<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 18 7" style="transform: scale(1.3) translateY(1px);">
2 <path fill="#000" d="M0,0h18v6H9v1H5V6H0V0z M1,5h2V2h1v3h1V1H1V5z M6,1v5h2V5h2V1H6z M8,2h1v2H8V2z M11,1v4h2V2h1v3h1V2h1v3h1V1H11z"/> 2 <path fill="currentColor" d="M0,0h18v6H9v1H5V6H0V0z M1,5h2V2h1v3h1V1H1V5z M6,1v5h2V5h2V1H6z M8,2h1v2H8V2z M11,1v4h2V2h1v3h1V2h1v3h1V1H11z"/>
3 <polygon fill="#FFFFFF" points="1,5 3,5 3,2 4,2 4,5 5,5 5,1 1,1 "/> 3 <polygon fill="#FFFFFF" points="1,5 3,5 3,2 4,2 4,5 5,5 5,1 1,1 "/>
4 <polygon fill="#FFFFFF" d="M6,1v5h2V5h2V1H6z M9,4H8V2h1V4z"/> 4 <polygon fill="#FFFFFF" d="M6,1v5h2V5h2V1H6z M9,4H8V2h1V4z"/>
5 <polygon fill="#FFFFFF" points="11,1 11,5 13,5 13,2 14,2 14,5 15,5 15,2 16,2 16,5 17,5 17,1 "/> 5 <polygon fill="#FFFFFF" points="11,1 11,5 13,5 13,2 14,2 14,5 15,5 15,2 16,2 16,5 17,5 17,1 "/>
diff --git a/client/src/assets/images/misc/peertube-x.svg b/client/src/assets/images/misc/peertube-x.svg
index 0099e627d..30ab665e7 100644
--- a/client/src/assets/images/misc/peertube-x.svg
+++ b/client/src/assets/images/misc/peertube-x.svg
@@ -1,20 +1,17 @@
1<?xml version="1.0" encoding="utf-8"?> 1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2<!-- Generator: Adobe Illustrator 23.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> 2 <style type="text/css">
3<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" 3 .st0{fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}
4 viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> 4 .st1{fill:currentColor;}
5<style type="text/css">
6 .st0{fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}
7 .st1{fill:#211F20;}
8</style> 5</style>
9<line class="st0" x1="17.1" y1="9.5" x2="22.1" y2="14.5"/> 6 <line class="st0" x1="17.1" y1="9.5" x2="22.1" y2="14.5" />
10<line class="st0" x1="22.1" y1="9.5" x2="17.1" y2="14.5"/> 7 <line class="st0" x1="22.1" y1="9.5" x2="17.1" y2="14.5" />
11<g>
12 <g> 8 <g>
13 <g> 9 <g>
14 <path class="st1" d="M2,2.6V12l6.9-4.3"/> 10 <g>
15 <path class="st1" d="M2,12v9.4l6.9-5.2"/> 11 <path class="st1" d="M2,2.6V12l6.9-4.3" />
16 <path class="st1" d="M8.9,7.7v8.6l6.9-4.3"/> 12 <path class="st1" d="M2,12v9.4l6.9-5.2" />
13 <path class="st1" d="M8.9,7.7v8.6l6.9-4.3" />
14 </g>
17 </g> 15 </g>
18 </g> 16 </g>
19</g>
20</svg> 17</svg>
diff --git a/client/src/assets/images/misc/playlist-add.svg b/client/src/assets/images/misc/playlist-add.svg
index 7ec77b851..4be495e83 100644
--- a/client/src/assets/images/misc/playlist-add.svg
+++ b/client/src/assets/images/misc/playlist-add.svg
@@ -1,5 +1,5 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 426.7 426.7"> 1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 426.7 426.7">
2 <defs/> 2 <defs/>
3 <path fill="#000" d="M0 64h256v42.7H0zM0 149.3h256V192H0zM0 234.7h170.7v42.7H0z"/> 3 <path fill="currentColor" d="M0 64h256v42.7H0zM0 149.3h256V192H0zM0 234.7h170.7v42.7H0z"/>
4 <path fill="#000" d="M341.3 234.7v-85.4h-42.6v85.4h-85.4v42.6h85.4v85.4h42.6v-85.4h85.4v-42.6z"/> 4 <path fill="currentColor" d="M341.3 234.7v-85.4h-42.6v85.4h-85.4v42.6h85.4v85.4h42.6v-85.4h85.4v-42.6z"/>
5</svg> 5</svg>
diff --git a/client/src/assets/images/misc/support.svg b/client/src/assets/images/misc/support.svg
index 66280e18d..be3f58c24 100644
--- a/client/src/assets/images/misc/support.svg
+++ b/client/src/assets/images/misc/support.svg
@@ -6,9 +6,9 @@
6 <g transform="translate(2.669496,27.625894)"> 6 <g transform="translate(2.669496,27.625894)">
7 <g transform="matrix(0.1,0,0,-0.1,0,511)"> 7 <g transform="matrix(0.1,0,0,-0.1,0,511)">
8 <path d="m 3744.3542,4564.3712 c -217.4,-34.2 -520.3,-200.3 -693.7,-376.2 -263.8,-263.8 -388.4,-571.6 -388.4,-952.6 0,-256.5 44,-437.2 173.4,-684 75.7,-144.1 197.9,-280.9 747.5,-842.7 1106.5,-1133.40001 1138.2,-1165.20001 1253,-1194.50001 188.1,-51.3 214.9,-29.3 1162.7,938.00001 498.3,508.1 911.1,950.2 962.4,1030.8 263.8,415.3 283.3,964.9 48.8,1409.4 -180.8,342 -581.3,620.4 -972.2,676.6 -332.2,48.9 -671.7,-36.6 -967.3,-236.9 l -156.3,-109.9 -119.7,87.9 c -158.8,117.2 -351.8,202.7 -554.5,244.3 -183.1,39.1 -295.4,41.6 -495.7,9.8 z" 8 <path d="m 3744.3542,4564.3712 c -217.4,-34.2 -520.3,-200.3 -693.7,-376.2 -263.8,-263.8 -388.4,-571.6 -388.4,-952.6 0,-256.5 44,-437.2 173.4,-684 75.7,-144.1 197.9,-280.9 747.5,-842.7 1106.5,-1133.40001 1138.2,-1165.20001 1253,-1194.50001 188.1,-51.3 214.9,-29.3 1162.7,938.00001 498.3,508.1 911.1,950.2 962.4,1030.8 263.8,415.3 283.3,964.9 48.8,1409.4 -180.8,342 -581.3,620.4 -972.2,676.6 -332.2,48.9 -671.7,-36.6 -967.3,-236.9 l -156.3,-109.9 -119.7,87.9 c -158.8,117.2 -351.8,202.7 -554.5,244.3 -183.1,39.1 -295.4,41.6 -495.7,9.8 z"
9 fill="#000"/> 9 fill="currentColor"/>
10 <path d="m 7991.4051,47.633899 c -39.1,-19.5 -473.9,-437.299999 -964.9,-925.800029 l -891.6,-891.59997 h -830.5 c -757.2,0 -837.8,4.9 -913.6,44 -207.6,112.4 -227.2,415.2 -39.1,561.8 66,53.7 83,53.7 950.2,53.7 989.3,0 1008.8,2.5 1094.3,173.49997 56.2,105 56.2,317.50003 4.9,427.50003 -83.1,175.9 4.8,168.5 -1915.1,168.5 h -1722 l -173.4,-63.5 c -95.3,-34.2 -232.1,-102.6 -305.3,-151.5 -73.3,-48.9 -442.1,-400.60003 -823.2,-779.2 l -688.80006,-693.7 664.40006,-647.3 c 366.4,-354.2 779.2,-754.8 918.4,-889.1 l 251.6,-241.8 481.2,481.2 481.2,481.2 h 1487.6 c 1294.6,0 1494.9,4.9 1565.8,39.1 58.6,26.9 339.6,368.8 1028.4,1248.2 522.8,666.89997 964.9,1243.3 982,1284.9 41.5,92.8 2.5,212.499999 -95.3,297.999999 -66,53.7 -95.3,61.1 -273.6,61.1 -132,-0.1 -224.8,-12.3 -273.6,-39.2 z" 10 <path d="m 7991.4051,47.633899 c -39.1,-19.5 -473.9,-437.299999 -964.9,-925.800029 l -891.6,-891.59997 h -830.5 c -757.2,0 -837.8,4.9 -913.6,44 -207.6,112.4 -227.2,415.2 -39.1,561.8 66,53.7 83,53.7 950.2,53.7 989.3,0 1008.8,2.5 1094.3,173.49997 56.2,105 56.2,317.50003 4.9,427.50003 -83.1,175.9 4.8,168.5 -1915.1,168.5 h -1722 l -173.4,-63.5 c -95.3,-34.2 -232.1,-102.6 -305.3,-151.5 -73.3,-48.9 -442.1,-400.60003 -823.2,-779.2 l -688.80006,-693.7 664.40006,-647.3 c 366.4,-354.2 779.2,-754.8 918.4,-889.1 l 251.6,-241.8 481.2,481.2 481.2,481.2 h 1487.6 c 1294.6,0 1494.9,4.9 1565.8,39.1 58.6,26.9 339.6,368.8 1028.4,1248.2 522.8,666.89997 964.9,1243.3 982,1284.9 41.5,92.8 2.5,212.499999 -95.3,297.999999 -66,53.7 -95.3,61.1 -273.6,61.1 -132,-0.1 -224.8,-12.3 -273.6,-39.2 z"
11 fill="#000"/> 11 fill="currentColor"/>
12 </g> 12 </g>
13 </g> 13 </g>
14</svg> 14</svg>
diff --git a/client/src/assets/images/misc/video-lang.svg b/client/src/assets/images/misc/video-lang.svg
index 5ffed18da..6bcaeb9be 100644
--- a/client/src/assets/images/misc/video-lang.svg
+++ b/client/src/assets/images/misc/video-lang.svg
@@ -1,7 +1,7 @@
1<svg xmlns="http://www.w3.org/2000/svg" transform="scale(1.1)" viewBox="0 0 24 24"> 1<svg xmlns="http://www.w3.org/2000/svg" transform="scale(1.1)" viewBox="0 0 24 24">
2 <defs/> 2 <defs/>
3 <g class="layer"> 3 <g class="layer">
4 <path fill="#fff" fill-rule="evenodd" stroke="#000" stroke-width="1.8" d="M20.5 6.7s-.2-1.4-.8-2c-.7-.8-1.6-.8-2-.9-2.7-.2-6.9-.2-6.9-.2h0s-4.2 0-7 .2c-.3 0-1.2 0-2 .9-.5.6-.7 2-.7 2L.9 10v1.6l.2 3.3s.2 1.4.8 2c.7.8 1.7.8 2.2.9 1.6.2 6.7.2 6.7.2s4.2 0 7-.2c.3 0 1.2 0 2-.9.5-.6.7-2 .7-2l.2-3.3V10l-.2-3.3h0z"/> 4 <path fill="#fff" fill-rule="evenodd" stroke="currentColor" stroke-width="1.8" d="M20.5 6.7s-.2-1.4-.8-2c-.7-.8-1.6-.8-2-.9-2.7-.2-6.9-.2-6.9-.2h0s-4.2 0-7 .2c-.3 0-1.2 0-2 .9-.5.6-.7 2-.7 2L.9 10v1.6l.2 3.3s.2 1.4.8 2c.7.8 1.7.8 2.2.9 1.6.2 6.7.2 6.7.2s4.2 0 7-.2c.3 0 1.2 0 2-.9.5-.6.7-2 .7-2l.2-3.3V10l-.2-3.3h0z"/>
5 <path d="M8.7 14.7a.7.7 0 01-.5-1.2l2.9-3H8.7a.7.7 0 010-1.3h4a.7.7 0 01.5 1.2l-4 4a.7.7 0 01-.5.3zM11.7 8.6h-2a.7.7 0 110-1.4h2a.7.7 0 010 1.4z"/> 5 <path d="M8.7 14.7a.7.7 0 01-.5-1.2l2.9-3H8.7a.7.7 0 010-1.3h4a.7.7 0 01.5 1.2l-4 4a.7.7 0 01-.5.3zM11.7 8.6h-2a.7.7 0 110-1.4h2a.7.7 0 010 1.4z"/>
6 </g> 6 </g>
7</svg> 7</svg>
diff --git a/client/src/sass/bootstrap.scss b/client/src/sass/bootstrap.scss
index 7047f6e03..75dc91d7a 100644
--- a/client/src/sass/bootstrap.scss
+++ b/client/src/sass/bootstrap.scss
@@ -9,6 +9,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
9 animation: spin .7s infinite linear; 9 animation: spin .7s infinite linear;
10} 10}
11 11
12.glyphicon-duplicate {
13 font-size: 70%;
14}
15
12.flex-auto { 16.flex-auto {
13 flex: auto; 17 flex: auto;
14} 18}
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index ca11488cb..cf5ac8fd8 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -41,9 +41,6 @@
41 word-break: break-word; 41 word-break: break-word;
42 word-wrap: break-word; 42 word-wrap: break-word;
43 overflow-wrap: break-word; 43 overflow-wrap: break-word;
44 -webkit-hyphens: auto;
45 -ms-hyphens: auto;
46 -moz-hyphens: auto;
47 hyphens: auto; 44 hyphens: auto;
48} 45}
49 46
@@ -52,28 +49,6 @@
52 ::ng-deep .material { 49 ::ng-deep .material {
53 color: $color; 50 color: $color;
54 } 51 }
55
56 ::ng-deep svg {
57 path[fill="#000"],
58 g[fill="#000"],
59 rect[fill="#000"],
60 circle[fill="#000"],
61 polygon[fill="#000"] {
62 fill: $color;
63 }
64
65 path[stroke="#000"],
66 g[stroke="#000"],
67 rect[stroke="#000"],
68 circle[stroke="#000"],
69 polygon[stroke="#000"] {
70 stroke: $color;
71 }
72
73 stop[stop-color="#000"] {
74 stop-color: $color;
75 }
76 }
77} 52}
78 53
79@mixin fill-svg-color ($color) { 54@mixin fill-svg-color ($color) {
diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss
index 0144e89fb..81aacf1d7 100644
--- a/client/src/sass/player/peertube-skin.scss
+++ b/client/src/sass/player/peertube-skin.scss
@@ -43,10 +43,6 @@ body {
43 } 43 }
44 } 44 }
45 45
46 .vjs-button > .vjs-icon-placeholder::before {
47 line-height: $control-bar-height;
48 }
49
50 .vjs-volume-level::before { 46 .vjs-volume-level::before {
51 content: ''; /* Remove Circle From Progress Bar */ 47 content: ''; /* Remove Circle From Progress Bar */
52 } 48 }
@@ -242,8 +238,19 @@ body {
242 @include disable-outline; 238 @include disable-outline;
243 239
244 cursor: pointer; 240 cursor: pointer;
245 font-size: $play-control-font-size;
246 width: 2em; 241 width: 2em;
242
243 .vjs-icon-placeholder {
244 line-height: $control-bar-height;
245 position: relative;
246 top: -1px;
247
248 &::before {
249 font-size: 28px;
250 line-height: unset;
251 position: relative;
252 }
253 }
247 } 254 }
248 255
249 .vjs-time-control { 256 .vjs-time-control {
@@ -375,7 +382,6 @@ body {
375 .vjs-mute-control { 382 .vjs-mute-control {
376 @include disable-outline; 383 @include disable-outline;
377 384
378 line-height: $control-bar-height;
379 padding: 0; 385 padding: 0;
380 width: 30px; 386 width: 30px;
381 387
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index cf4bc6f03..c87270027 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -783,6 +783,8 @@ export class PeerTubeEmbed {
783 783
784 showModal: unimplemented, 784 showModal: unimplemented,
785 785
786 getServerConfig: unimplemented,
787
786 markdownRenderer: { 788 markdownRenderer: {
787 textMarkdownToHTML: unimplemented, 789 textMarkdownToHTML: unimplemented,
788 enhancedMarkdownToHTML: unimplemented 790 enhancedMarkdownToHTML: unimplemented
diff --git a/client/src/types/register-client-option.model.ts b/client/src/types/register-client-option.model.ts
index e3c6d803d..7e5356a2b 100644
--- a/client/src/types/register-client-option.model.ts
+++ b/client/src/types/register-client-option.model.ts
@@ -1,5 +1,6 @@
1import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model' 1import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
2import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model' 2import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model'
3import { ServerConfig } from '@shared/models/server'
3 4
4export type RegisterClientOptions = { 5export type RegisterClientOptions = {
5 registerHook: (options: RegisterClientHookOptions) => void 6 registerHook: (options: RegisterClientHookOptions) => void
@@ -16,6 +17,8 @@ export type RegisterClientHelpers = {
16 17
17 getSettings: () => Promise<{ [ name: string ]: string }> 18 getSettings: () => Promise<{ [ name: string ]: string }>
18 19
20 getServerConfig: () => Promise<ServerConfig>
21
19 notifier: { 22 notifier: {
20 info: (text: string, title?: string, timeout?: number) => void, 23 info: (text: string, title?: string, timeout?: number) => void,
21 error: (text: string, title?: string, timeout?: number) => void, 24 error: (text: string, title?: string, timeout?: number) => void,