diff options
390 files changed, 8207 insertions, 4809 deletions
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index b06143c8f..ac2e6ce92 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml | |||
@@ -1 +1 @@ | |||
custom: ["https://joinpeertube.org/roadmap", "https://soutenir.framasoft.org/en/"] | custom: ["https://soutenir.framasoft.org/en/"] | ||
diff --git a/.github/workflows/stats.yml b/.github/workflows/stats.yml index b5fb6d2a6..a2f0945b3 100644 --- a/.github/workflows/stats.yml +++ b/.github/workflows/stats.yml | |||
@@ -5,6 +5,7 @@ on: | |||
5 | branches: | 5 | branches: |
6 | - develop | 6 | - develop |
7 | - ci | 7 | - ci |
8 | - next | ||
8 | pull_request: | 9 | pull_request: |
9 | types: [synchronize, opened] | 10 | types: [synchronize, opened] |
10 | 11 | ||
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f8706d4be..442317ce2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml | |||
@@ -6,6 +6,7 @@ on: | |||
6 | - develop | 6 | - develop |
7 | - master | 7 | - master |
8 | - ci | 8 | - ci |
9 | - next | ||
9 | pull_request: | 10 | pull_request: |
10 | types: [synchronize, opened] | 11 | types: [synchronize, opened] |
11 | schedule: | 12 | schedule: |
diff --git a/.gitignore b/.gitignore index 07b2fb7ef..98f337490 100644 --- a/.gitignore +++ b/.gitignore | |||
@@ -30,9 +30,11 @@ yarn-error.log | |||
30 | # IDE | 30 | # IDE |
31 | /*.sublime-project | 31 | /*.sublime-project |
32 | /*.sublime-workspace | 32 | /*.sublime-workspace |
33 | /*.vscode | ||
33 | /**/.idea | 34 | /**/.idea |
34 | /dist | 35 | /dist |
35 | /PeerTube.iml | 36 | /PeerTube.iml |
37 | *.swp | ||
36 | 38 | ||
37 | # Zanata | 39 | # Zanata |
38 | /.zanata-cache | 40 | /.zanata-cache |
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/account-about/account-about.component.html b/client/src/app/+accounts/account-about/account-about.component.html deleted file mode 100644 index e9e0e4079..000000000 --- a/client/src/app/+accounts/account-about/account-about.component.html +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | <h1 class="sr-only" i18n>About</h1> | ||
2 | <div class="margin-content"> | ||
3 | <div *ngIf="account" class="row no-gutters"> | ||
4 | <div class="block col-md-6 col-sm-12 pr-2"> | ||
5 | <h2 i18n class="small-title">DESCRIPTION</h2> | ||
6 | <div class="content" [innerHtml]="getAccountDescription()"></div> | ||
7 | </div> | ||
8 | |||
9 | <div class="block col-md-6 col-sm-12"> | ||
10 | <h2 i18n class="small-title">STATS</h2> | ||
11 | |||
12 | <div i18n class="content">Joined {{ account.createdAt | date }}</div> | ||
13 | </div> | ||
14 | </div> | ||
15 | </div> | ||
diff --git a/client/src/app/+accounts/account-about/account-about.component.scss b/client/src/app/+accounts/account-about/account-about.component.scss deleted file mode 100644 index 5bcd4b561..000000000 --- a/client/src/app/+accounts/account-about/account-about.component.scss +++ /dev/null | |||
@@ -1,12 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .block { | ||
5 | margin-bottom: 40px; | ||
6 | |||
7 | .small-title { | ||
8 | @include in-content-small-title; | ||
9 | |||
10 | margin-bottom: 20px; | ||
11 | } | ||
12 | } | ||
diff --git a/client/src/app/+accounts/account-about/account-about.component.ts b/client/src/app/+accounts/account-about/account-about.component.ts deleted file mode 100644 index 6cf846d72..000000000 --- a/client/src/app/+accounts/account-about/account-about.component.ts +++ /dev/null | |||
@@ -1,40 +0,0 @@ | |||
1 | import { Subscription } from 'rxjs' | ||
2 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
3 | import { MarkdownService } from '@app/core' | ||
4 | import { Account, AccountService } from '@app/shared/shared-main' | ||
5 | |||
6 | @Component({ | ||
7 | selector: 'my-account-about', | ||
8 | templateUrl: './account-about.component.html', | ||
9 | styleUrls: [ './account-about.component.scss' ] | ||
10 | }) | ||
11 | export class AccountAboutComponent implements OnInit, OnDestroy { | ||
12 | account: Account | ||
13 | descriptionHTML = '' | ||
14 | |||
15 | private accountSub: Subscription | ||
16 | |||
17 | constructor ( | ||
18 | private accountService: AccountService, | ||
19 | private markdownService: MarkdownService | ||
20 | ) { } | ||
21 | |||
22 | ngOnInit () { | ||
23 | // Parent get the account for us | ||
24 | this.accountSub = this.accountService.accountLoaded | ||
25 | .subscribe(async account => { | ||
26 | this.account = account | ||
27 | this.descriptionHTML = await this.markdownService.textMarkdownToHTML(this.account.description, true) | ||
28 | }) | ||
29 | } | ||
30 | |||
31 | ngOnDestroy () { | ||
32 | if (this.accountSub) this.accountSub.unsubscribe() | ||
33 | } | ||
34 | |||
35 | getAccountDescription () { | ||
36 | if (this.descriptionHTML) return this.descriptionHTML | ||
37 | |||
38 | return $localize`No description` | ||
39 | } | ||
40 | } | ||
diff --git a/client/src/app/+accounts/account-search/account-search.component.ts b/client/src/app/+accounts/account-search/account-search.component.ts index dda4bf0c7..f54ab846a 100644 --- a/client/src/app/+accounts/account-search/account-search.component.ts +++ b/client/src/app/+accounts/account-search/account-search.component.ts | |||
@@ -64,9 +64,14 @@ export class AccountSearchComponent extends AbstractVideoList implements OnInit, | |||
64 | } | 64 | } |
65 | 65 | ||
66 | updateSearch (value: string) { | 66 | updateSearch (value: string) { |
67 | if (value === '') this.router.navigate(['../videos'], { relativeTo: this.route }) | ||
68 | this.search = value | 67 | this.search = value |
69 | 68 | ||
69 | if (!this.search) { | ||
70 | this.router.navigate([ '../videos' ], { relativeTo: this.route }) | ||
71 | return | ||
72 | } | ||
73 | |||
74 | this.videos = [] | ||
70 | this.reloadVideos() | 75 | this.reloadVideos() |
71 | } | 76 | } |
72 | 77 | ||
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html index 5dbb341d2..19a4b3c9c 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html | |||
@@ -1,33 +1,50 @@ | |||
1 | <h1 class="sr-only" i18n>Video channels</h1> | 1 | <h1 class="sr-only" i18n>Video channels</h1> |
2 | |||
2 | <div class="margin-content"> | 3 | <div class="margin-content"> |
3 | 4 | ||
4 | <div class="no-results" i18n *ngIf="channelPagination.totalItems === 0">This account does not have channels.</div> | 5 | <div class="no-results" i18n *ngIf="channelPagination.totalItems === 0">This account does not have channels.</div> |
5 | 6 | ||
6 | <div class="channels" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onChannelDataSubject.asObservable()"> | 7 | <div class="channels" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onChannelDataSubject.asObservable()"> |
7 | <div class="section channel" *ngFor="let videoChannel of videoChannels"> | 8 | <div class="channel" *ngFor="let videoChannel of videoChannels"> |
8 | <div class="section-title"> | 9 | |
9 | <a [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel"> | 10 | <div class="channel-avatar-row"> |
11 | <a class="avatar-link" [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel"> | ||
10 | <img [src]="videoChannel.avatarUrl" alt="Avatar" /> | 12 | <img [src]="videoChannel.avatarUrl" alt="Avatar" /> |
13 | </a> | ||
11 | 14 | ||
12 | <h2 class="section-title">{{ videoChannel.displayName }}</h2> | 15 | <h2> |
16 | <a [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel"> | ||
17 | {{ videoChannel.displayName }} | ||
18 | </a> | ||
19 | </h2> | ||
20 | |||
21 | <div class="actor-counters"> | ||
13 | <div class="followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> | 22 | <div class="followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> |
14 | </a> | ||
15 | 23 | ||
16 | <my-subscribe-button [videoChannels]="[videoChannel]"></my-subscribe-button> | 24 | <span class="videos-count" *ngIf="getTotalVideosOf(videoChannel) !== undefined" i18n> |
25 | {getTotalVideosOf(videoChannel), plural, =1 {1 videos} other {{{ getTotalVideosOf(videoChannel) }} videos}} | ||
26 | </span> | ||
27 | </div> | ||
28 | |||
29 | <div class="description-html" [innerHTML]="getChannelDescription(videoChannel)"></div> | ||
17 | </div> | 30 | </div> |
18 | 31 | ||
19 | <div *ngIf="getVideosOf(videoChannel)" class="videos"> | 32 | <my-subscribe-button [videoChannels]="[videoChannel]"></my-subscribe-button> |
20 | <div class="no-results my-5" i18n *ngIf="getVideosOf(videoChannel).length === 0">This channel doesn't have any videos.</div> | 33 | |
34 | <a i18n class="button-show-channel peertube-button-link orange-button-inverted" [routerLink]="getVideoChannelLink(videoChannel)">Show this channel</a> | ||
35 | |||
36 | <div class="videos"> | ||
37 | <div class="no-results" i18n *ngIf="getTotalVideosOf(videoChannel) === 0">This channel doesn't have any videos.</div> | ||
21 | 38 | ||
22 | <my-video-miniature | 39 | <my-video-miniature |
23 | *ngFor="let video of getVideosOf(videoChannel)" | 40 | *ngFor="let video of getVideosOf(videoChannel)" |
24 | [video]="video" [user]="userMiniature" [displayVideoActions]="true" | 41 | [video]="video" [user]="userMiniature" [displayVideoActions]="true" [displayOptions]="miniatureDisplayOptions" |
25 | ></my-video-miniature> | 42 | ></my-video-miniature> |
26 | </div> | ||
27 | 43 | ||
28 | <a *ngIf="getVideosOf(videoChannel).length !== 0" class="show-more" i18n [routerLink]="getVideoChannelLink(videoChannel)"> | 44 | <div *ngIf="getTotalVideosOf(videoChannel)" class="miniature-show-channel"> |
29 | SHOW THIS CHANNEL | 45 | <a i18n [routerLink]="getVideoChannelLink(videoChannel)">SHOW THIS CHANNEL ></a> |
30 | </a> | 46 | </div> |
47 | </div> | ||
31 | </div> | 48 | </div> |
32 | </div> | 49 | </div> |
33 | </div> | 50 | </div> |
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss index 4957e91d7..7e88802f3 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss | |||
@@ -3,37 +3,175 @@ | |||
3 | @import '_miniature'; | 3 | @import '_miniature'; |
4 | 4 | ||
5 | .margin-content { | 5 | .margin-content { |
6 | @include fluid-videos-miniature-layout; | 6 | @include grid-videos-miniature-margins; |
7 | } | 7 | } |
8 | 8 | ||
9 | .section { | 9 | .channel { |
10 | @include miniature-rows; | 10 | max-width: $max-channels-width; |
11 | background-color: pvar(--channelBackgroundColor); | ||
12 | padding: 30px; | ||
11 | 13 | ||
12 | padding-top: 0 !important; | 14 | margin: 30px 0; |
13 | 15 | ||
14 | .section-title { | 16 | display: grid; |
17 | grid-template-columns: 1fr auto; | ||
18 | grid-template-rows: auto auto; | ||
19 | column-gap: 15px; | ||
20 | } | ||
21 | |||
22 | .channel-avatar-row { | ||
23 | grid-column: 1; | ||
24 | grid-row: 1; | ||
25 | |||
26 | display: grid; | ||
27 | grid-template-columns: auto auto 1fr; | ||
28 | grid-template-rows: auto 1fr; | ||
29 | |||
30 | .avatar-link { | ||
31 | grid-column: 1; | ||
32 | grid-row: 1 / 3; | ||
33 | margin-right: 30px; | ||
34 | } | ||
35 | |||
36 | img { | ||
37 | @include channel-avatar(75px); | ||
38 | } | ||
39 | |||
40 | a { | ||
41 | color: pvar(--mainForegroundColor); | ||
42 | } | ||
43 | |||
44 | h2 { | ||
45 | grid-row: 1; | ||
46 | grid-column: 2; | ||
47 | font-size: 20px; | ||
48 | line-height: 1; | ||
49 | font-weight: $font-bold; | ||
50 | margin: 0; | ||
51 | } | ||
52 | |||
53 | .actor-counters { | ||
54 | grid-row: 1; | ||
55 | grid-column: 3; | ||
56 | color: pvar(--greyForegroundColor); | ||
57 | font-size: 16px; | ||
58 | display: flex; | ||
15 | align-items: center; | 59 | align-items: center; |
60 | margin-left: 15px; | ||
16 | } | 61 | } |
17 | 62 | ||
18 | .videos { | 63 | .actor-counters > *:not(:last-child)::after { |
19 | overflow: hidden; | 64 | content: '•'; |
65 | margin: 0 10px; | ||
66 | color: pvar(--mainColor); | ||
67 | } | ||
20 | 68 | ||
21 | .no-results { | 69 | .description-html { |
22 | height: 50px; | 70 | grid-column: 2 / 4; |
23 | } | 71 | grid-row: 2; |
72 | |||
73 | max-height: 80px; | ||
74 | font-size: 16px; | ||
75 | |||
76 | @include fade-text(30px, pvar(--channelBackgroundColor)); | ||
77 | } | ||
78 | } | ||
79 | |||
80 | my-subscribe-button { | ||
81 | grid-row: 1; | ||
82 | grid-column: 2; | ||
83 | } | ||
84 | |||
85 | .videos { | ||
86 | display: flex; | ||
87 | grid-column: 1 / 3; | ||
88 | grid-row: 2; | ||
89 | margin-top: 30px; | ||
90 | |||
91 | position: relative; | ||
92 | overflow: hidden; | ||
93 | |||
94 | my-video-miniature { | ||
95 | margin-right: 15px; | ||
96 | min-width: $video-thumbnail-medium-width; | ||
97 | max-width: $video-thumbnail-medium-width; | ||
98 | } | ||
99 | |||
100 | .no-results { | ||
101 | height: auto; | ||
24 | } | 102 | } |
103 | } | ||
25 | 104 | ||
26 | my-video-miniature ::ng-deep my-video-actions-dropdown > my-action-dropdown { | 105 | .miniature-show-channel { |
27 | // Fix our overflow | 106 | height: 100%; |
28 | position: absolute; | 107 | position: absolute; |
108 | right: 0; | ||
109 | background: linear-gradient(90deg, transparent 0, pvar(--channelBackgroundColor) 45px); | ||
110 | padding: ($video-thumbnail-medium-height / 2 - 10px) 15px 0 60px; | ||
111 | z-index: z(miniature) + 1; | ||
112 | |||
113 | a { | ||
114 | color: pvar(--mainColor); | ||
115 | font-size: 16px; | ||
116 | font-weight: $font-semibold; | ||
29 | } | 117 | } |
30 | } | 118 | } |
31 | 119 | ||
120 | .button-show-channel { | ||
121 | display: none; | ||
122 | } | ||
123 | |||
32 | @media screen and (max-width: $mobile-view) { | 124 | @media screen and (max-width: $mobile-view) { |
33 | .section { | 125 | .channel { |
34 | .section-title { | 126 | padding: 15px; |
35 | flex-direction: column; | 127 | } |
36 | align-items: normal; | 128 | |
129 | .channel-avatar-row { | ||
130 | grid-template-columns: auto auto auto 1fr; | ||
131 | |||
132 | .avatar-link { | ||
133 | grid-row: 1 / 4; | ||
134 | } | ||
135 | |||
136 | h2 { | ||
137 | font-size: 16px; | ||
37 | } | 138 | } |
139 | |||
140 | .actor-counters { | ||
141 | margin: 0; | ||
142 | font-size: 13px; | ||
143 | grid-row: 2; | ||
144 | grid-column: 2 / 4; | ||
145 | } | ||
146 | |||
147 | .description-html { | ||
148 | grid-row: 3; | ||
149 | font-size: 14px; | ||
150 | } | ||
151 | } | ||
152 | |||
153 | .show-channel a { | ||
154 | @include peertube-button-link; | ||
155 | @include orange-button-inverted; | ||
156 | } | ||
157 | |||
158 | .videos { | ||
159 | display: none; | ||
160 | } | ||
161 | |||
162 | my-subscribe-button, | ||
163 | .button-show-channel { | ||
164 | grid-column: 1 / 4; | ||
165 | grid-row: 3; | ||
166 | margin-top: 15px; | ||
167 | } | ||
168 | |||
169 | my-subscribe-button { | ||
170 | justify-self: start; | ||
171 | } | ||
172 | |||
173 | .button-show-channel { | ||
174 | display: block; | ||
175 | justify-self: end; | ||
38 | } | 176 | } |
39 | } | 177 | } |
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts index f2beb6689..0628c7a96 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import { from, Subject, Subscription } from 'rxjs' | 1 | import { from, Subject, Subscription } from 'rxjs' |
2 | import { concatMap, map, switchMap, tap } from 'rxjs/operators' | 2 | import { concatMap, map, switchMap, tap } from 'rxjs/operators' |
3 | import { Component, OnDestroy, OnInit } from '@angular/core' | 3 | import { Component, OnDestroy, OnInit } from '@angular/core' |
4 | import { ComponentPagination, hasMoreItems, ScreenService, User, UserService } from '@app/core' | 4 | import { ComponentPagination, hasMoreItems, MarkdownService, ScreenService, User, UserService } from '@app/core' |
5 | import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' | 5 | import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' |
6 | import { NSFWPolicyType, VideoSortField } from '@shared/models' | 6 | import { NSFWPolicyType, VideoSortField } from '@shared/models' |
7 | import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature' | ||
7 | 8 | ||
8 | @Component({ | 9 | @Component({ |
9 | selector: 'my-account-video-channels', | 10 | selector: 'my-account-video-channels', |
@@ -13,7 +14,10 @@ import { NSFWPolicyType, VideoSortField } from '@shared/models' | |||
13 | export class AccountVideoChannelsComponent implements OnInit, OnDestroy { | 14 | export class AccountVideoChannelsComponent implements OnInit, OnDestroy { |
14 | account: Account | 15 | account: Account |
15 | videoChannels: VideoChannel[] = [] | 16 | videoChannels: VideoChannel[] = [] |
16 | videos: { [id: number]: Video[] } = {} | 17 | |
18 | videos: { [id: number]: { total: number, videos: Video[] } } = {} | ||
19 | |||
20 | channelsDescriptionHTML: { [ id: number ]: string } = {} | ||
17 | 21 | ||
18 | channelPagination: ComponentPagination = { | 22 | channelPagination: ComponentPagination = { |
19 | currentPage: 1, | 23 | currentPage: 1, |
@@ -23,7 +27,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { | |||
23 | 27 | ||
24 | videosPagination: ComponentPagination = { | 28 | videosPagination: ComponentPagination = { |
25 | currentPage: 1, | 29 | currentPage: 1, |
26 | itemsPerPage: 12, | 30 | itemsPerPage: 5, |
27 | totalItems: null | 31 | totalItems: null |
28 | } | 32 | } |
29 | videosSort: VideoSortField = '-publishedAt' | 33 | videosSort: VideoSortField = '-publishedAt' |
@@ -32,6 +36,16 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { | |||
32 | 36 | ||
33 | userMiniature: User | 37 | userMiniature: User |
34 | nsfwPolicy: NSFWPolicyType | 38 | nsfwPolicy: NSFWPolicyType |
39 | miniatureDisplayOptions: MiniatureDisplayOptions = { | ||
40 | date: true, | ||
41 | views: true, | ||
42 | by: false, | ||
43 | avatar: false, | ||
44 | privacyLabel: false, | ||
45 | privacyText: false, | ||
46 | state: false, | ||
47 | blacklistInfo: false | ||
48 | } | ||
35 | 49 | ||
36 | private accountSub: Subscription | 50 | private accountSub: Subscription |
37 | 51 | ||
@@ -39,7 +53,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { | |||
39 | private accountService: AccountService, | 53 | private accountService: AccountService, |
40 | private videoChannelService: VideoChannelService, | 54 | private videoChannelService: VideoChannelService, |
41 | private videoService: VideoService, | 55 | private videoService: VideoService, |
42 | private screenService: ScreenService, | 56 | private markdown: MarkdownService, |
43 | private userService: UserService | 57 | private userService: UserService |
44 | ) { } | 58 | ) { } |
45 | 59 | ||
@@ -78,23 +92,36 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { | |||
78 | } | 92 | } |
79 | 93 | ||
80 | return this.videoService.getVideoChannelVideos(options) | 94 | return this.videoService.getVideoChannelVideos(options) |
81 | .pipe(map(data => ({ videoChannel, videos: data.data }))) | 95 | .pipe(map(data => ({ videoChannel, videos: data.data, total: data.total }))) |
82 | }) | 96 | }) |
83 | ) | 97 | ) |
84 | .subscribe(({ videoChannel, videos }) => { | 98 | .subscribe(async ({ videoChannel, videos, total }) => { |
99 | this.channelsDescriptionHTML[videoChannel.id] = await this.markdown.textMarkdownToHTML(videoChannel.description) | ||
100 | |||
85 | this.videoChannels.push(videoChannel) | 101 | this.videoChannels.push(videoChannel) |
86 | 102 | ||
87 | this.videos[videoChannel.id] = videos | 103 | this.videos[videoChannel.id] = { videos, total } |
88 | 104 | ||
89 | this.onChannelDataSubject.next([ videoChannel ]) | 105 | this.onChannelDataSubject.next([ videoChannel ]) |
90 | }) | 106 | }) |
91 | } | 107 | } |
92 | 108 | ||
93 | getVideosOf (videoChannel: VideoChannel) { | 109 | getVideosOf (videoChannel: VideoChannel) { |
94 | const numberOfVideos = this.screenService.getNumberOfAvailableMiniatures() | 110 | const obj = this.videos[ videoChannel.id ] |
111 | if (!obj) return [] | ||
112 | |||
113 | return obj.videos | ||
114 | } | ||
115 | |||
116 | getTotalVideosOf (videoChannel: VideoChannel) { | ||
117 | const obj = this.videos[ videoChannel.id ] | ||
118 | if (!obj) return undefined | ||
119 | |||
120 | return obj.total | ||
121 | } | ||
95 | 122 | ||
96 | // 2 rows | 123 | getChannelDescription (videoChannel: VideoChannel) { |
97 | return this.videos[ videoChannel.id ].slice(0, numberOfVideos * 2) | 124 | return this.channelsDescriptionHTML[videoChannel.id] |
98 | } | 125 | } |
99 | 126 | ||
100 | onNearOfBottom () { | 127 | onNearOfBottom () { |
diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts index 484d60e25..75af45e90 100644 --- a/client/src/app/+accounts/account-videos/account-videos.component.ts +++ b/client/src/app/+accounts/account-videos/account-videos.component.ts | |||
@@ -16,6 +16,7 @@ import { VideoFilter } from '@shared/models' | |||
16 | ] | 16 | ] |
17 | }) | 17 | }) |
18 | export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { | 18 | export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { |
19 | // No value because we don't want a page title | ||
19 | titlePage: string | 20 | titlePage: string |
20 | loadOnInit = false | 21 | loadOnInit = false |
21 | loadUserVideoPreferences = true | 22 | loadUserVideoPreferences = true |
@@ -77,11 +78,6 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, | |||
77 | 78 | ||
78 | return this.videoService | 79 | return this.videoService |
79 | .getAccountVideos(options) | 80 | .getAccountVideos(options) |
80 | .pipe( | ||
81 | tap(({ total }) => { | ||
82 | this.titlePage = $localize`Published ${total} videos` | ||
83 | }) | ||
84 | ) | ||
85 | } | 81 | } |
86 | 82 | ||
87 | toggleModerationDisplay () { | 83 | toggleModerationDisplay () { |
@@ -93,4 +89,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, | |||
93 | generateSyndicationList () { | 89 | generateSyndicationList () { |
94 | this.syndicationItems = this.videoService.getAccountFeedUrls(this.account.id) | 90 | this.syndicationItems = this.videoService.getAccountFeedUrls(this.account.id) |
95 | } | 91 | } |
92 | |||
93 | displayAsRow () { | ||
94 | return this.screenService.isInMobileView() | ||
95 | } | ||
96 | } | 96 | } |
diff --git a/client/src/app/+accounts/accounts-routing.module.ts b/client/src/app/+accounts/accounts-routing.module.ts index 15937a67b..3bf0f7185 100644 --- a/client/src/app/+accounts/accounts-routing.module.ts +++ b/client/src/app/+accounts/accounts-routing.module.ts | |||
@@ -1,11 +1,10 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | 3 | import { MetaGuard } from '@ngx-meta/core' |
4 | import { AccountsComponent } from './accounts.component' | ||
5 | import { AccountVideosComponent } from './account-videos/account-videos.component' | ||
6 | import { AccountAboutComponent } from './account-about/account-about.component' | ||
7 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' | ||
8 | import { AccountSearchComponent } from './account-search/account-search.component' | 4 | import { AccountSearchComponent } from './account-search/account-search.component' |
5 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' | ||
6 | import { AccountVideosComponent } from './account-videos/account-videos.component' | ||
7 | import { AccountsComponent } from './accounts.component' | ||
9 | 8 | ||
10 | const accountsRoutes: Routes = [ | 9 | const accountsRoutes: Routes = [ |
11 | { | 10 | { |
@@ -32,15 +31,6 @@ const accountsRoutes: Routes = [ | |||
32 | } | 31 | } |
33 | }, | 32 | }, |
34 | { | 33 | { |
35 | path: 'about', | ||
36 | component: AccountAboutComponent, | ||
37 | data: { | ||
38 | meta: { | ||
39 | title: $localize`About account` | ||
40 | } | ||
41 | } | ||
42 | }, | ||
43 | { | ||
44 | path: 'videos', | 34 | path: 'videos', |
45 | component: AccountVideosComponent, | 35 | component: AccountVideosComponent, |
46 | data: { | 36 | data: { |
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index 5bd7b0824..03d083bb6 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html | |||
@@ -1,57 +1,89 @@ | |||
1 | <div *ngIf="account" class="row"> | 1 | <div *ngIf="account" class="root"> |
2 | <div class="sub-menu"> | 2 | <div class="account-info"> |
3 | 3 | ||
4 | <div class="actor"> | 4 | <div class="account-avatar-row"> |
5 | <img [src]="account.avatarUrl" alt="Avatar" /> | 5 | <img class="account-avatar" [src]="account.avatarUrl" alt="Avatar" /> |
6 | 6 | ||
7 | <div class="actor-info"> | 7 | <div> |
8 | <div class="actor-names"> | 8 | <div class="section-label" i18n>PEERTUBE ACCOUNT</div> |
9 | <div class="actor-display-name">{{ account.displayName }}</div> | 9 | |
10 | <div class="actor-name"> | 10 | <div class="actor-info"> |
11 | <span>{{ account.nameWithHost }}</span> | 11 | <div> |
12 | <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" | 12 | <div class="actor-display-name"> |
13 | class="btn btn-outline-secondary btn-sm copy-button" | 13 | <h1>{{ account.displayName }}</h1> |
14 | > | 14 | |
15 | <span class="glyphicon glyphicon-copy"></span> | 15 | <my-user-moderation-dropdown |
16 | </button> | 16 | [prependActions]="prependModerationActions" |
17 | buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto" | ||
18 | (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()" | ||
19 | ></my-user-moderation-dropdown> | ||
20 | |||
21 | <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span> | ||
22 | <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> | ||
23 | <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span> | ||
24 | <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span> | ||
25 | <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span> | ||
26 | </div> | ||
27 | |||
28 | <div class="actor-handle"> | ||
29 | <span>@{{ account.nameWithHost }}</span> | ||
30 | <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" | ||
31 | class="btn btn-outline-secondary btn-sm copy-button" title="Copy account handle" i18n-title | ||
32 | > | ||
33 | <span class="glyphicon glyphicon-duplicate"></span> | ||
34 | </button> | ||
35 | </div> | ||
36 | |||
37 | <div class="actor-counters"> | ||
38 | <span i18n>{naiveAggregatedSubscribers(), plural, =1 {1 subscriber} other {{{ naiveAggregatedSubscribers() }} subscribers}}</span> | ||
39 | |||
40 | <span class="videos-count" *ngIf="accountVideosCount !== undefined" i18n> | ||
41 | {accountVideosCount, plural, =1 {1 videos} other {{{ accountVideosCount }} videos}} | ||
42 | </span> | ||
43 | </div> | ||
17 | </div> | 44 | </div> |
18 | <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span> | ||
19 | <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> | ||
20 | <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span> | ||
21 | <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span> | ||
22 | <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span> | ||
23 | |||
24 | <my-user-moderation-dropdown | ||
25 | [prependActions]="prependModerationActions" | ||
26 | buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto" | ||
27 | (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()" | ||
28 | ></my-user-moderation-dropdown> | ||
29 | </div> | ||
30 | <div class="actor-followers" [title]="accountFollowerTitle"> | ||
31 | {{ subscribersDisplayFor(naiveAggregatedSubscribers) }} | ||
32 | </div> | 45 | </div> |
33 | </div> | 46 | </div> |
47 | </div> | ||
34 | 48 | ||
35 | <div class="right-buttons"> | 49 | <div class="description" [ngClass]="{ expanded: accountDescriptionExpanded }"> |
36 | <a *ngIf="isAccountManageable && !isInSmallView" routerLink="/my-account" class="btn btn-outline-tertiary mr-2" i18n>Manage account</a> | 50 | <div class="description-html" [innerHTML]="accountDescriptionHTML"></div> |
37 | <my-subscribe-button *ngIf="videoChannels" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button> | 51 | |
38 | </div> | 52 | <div class="created-at" i18n>Account created on {{ account.createdAt | date }}</div> |
39 | </div> | 53 | </div> |
40 | 54 | ||
41 | <div class="links w-100"> | 55 | <div *ngIf="hasShowMoreDescription()" class="show-more" role="button" |
42 | <ng-template #linkTemplate let-item="item"> | 56 | (click)="accountDescriptionExpanded = !accountDescriptionExpanded" |
43 | <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> | 57 | title="Show the complete description" i18n-title i18n |
44 | </ng-template> | 58 | > |
59 | Show more... | ||
60 | </div> | ||
45 | 61 | ||
46 | <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> | 62 | <div class="buttons"> |
63 | <a *ngIf="isManageable()" routerLink="/my-account" class="peertube-button-link orange-button" i18n> | ||
64 | Manage account | ||
65 | </a> | ||
47 | 66 | ||
48 | <simple-search-input (searchChanged)="searchChanged($event)" name="search-videos" i18n-placeholder placeholder="Search videos"></simple-search-input> | 67 | <my-subscribe-button *ngIf="hasVideoChannels() && !isManageable()" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button> |
49 | </div> | 68 | </div> |
50 | </div> | 69 | </div> |
51 | 70 | ||
52 | <div class="margin-content"> | 71 | <div class="links"> |
53 | <router-outlet (activate)="onOutletLoaded($event)"></router-outlet> | 72 | <ng-template #linkTemplate let-item="item"> |
73 | <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> | ||
74 | </ng-template> | ||
75 | |||
76 | <list-overflow [hidden]="hideMenu" [items]="links" [itemTemplate]="linkTemplate"></list-overflow> | ||
77 | |||
78 | <simple-search-input | ||
79 | [alwaysShow]="!isInSmallView()" (searchChanged)="searchChanged($event)" | ||
80 | (inputDisplayChanged)="onSearchInputDisplayChanged($event)" name="search-videos" | ||
81 | i18n-iconTitle icon-title="Search account videos" | ||
82 | i18n-placeholder placeholder="Search account videos" | ||
83 | ></simple-search-input> | ||
54 | </div> | 84 | </div> |
85 | |||
86 | <router-outlet (activate)="onOutletLoaded($event)"></router-outlet> | ||
55 | </div> | 87 | </div> |
56 | 88 | ||
57 | <ng-container *ngIf="prependModerationActions"> | 89 | <ng-container *ngIf="prependModerationActions"> |
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss index 40c6b6493..a836e84ce 100644 --- a/client/src/app/+accounts/accounts.component.scss +++ b/client/src/app/+accounts/accounts.component.scss | |||
@@ -1,48 +1,29 @@ | |||
1 | // Bootstrap grid utilities require functions, variables and mixins | ||
2 | @import 'node_modules/bootstrap/scss/functions'; | ||
3 | @import 'node_modules/bootstrap/scss/variables'; | ||
4 | @import 'node_modules/bootstrap/scss/mixins'; | ||
5 | @import 'node_modules/bootstrap/scss/grid'; | ||
6 | |||
7 | @import '_variables'; | 1 | @import '_variables'; |
8 | @import '_mixins'; | 2 | @import '_mixins'; |
9 | 3 | @import '_actor'; | |
10 | .sub-menu { | 4 | @import '_miniature'; |
11 | @include sub-menu-with-actor; | 5 | |
12 | 6 | .root { | |
13 | .actor { | 7 | --myGlobalTopPadding: 60px; |
14 | width: 100%; | 8 | --myImgMargin: 30px; |
15 | } | 9 | --myFontSize: 16px; |
10 | --myGreyFontSize: 16px; | ||
16 | } | 11 | } |
17 | 12 | ||
18 | .margin-content { | 13 | .section-label { |
19 | // margin-content is required, but child views have their own margins | 14 | @include section-label-responsive; |
20 | // that match views outside the scope of accounts, so we only align | ||
21 | // them with the margins of .sub-menu when required. | ||
22 | margin: 0; | ||
23 | } | 15 | } |
24 | 16 | ||
25 | .right-buttons { | 17 | .links { |
26 | display: flex; | 18 | @include grid-videos-miniature-margins; |
27 | height: max-content; | ||
28 | margin-left: auto; | ||
29 | margin-top: 10px; | ||
30 | |||
31 | @include media-breakpoint-down(lg) { | ||
32 | flex-flow: column-reverse; | ||
33 | 19 | ||
34 | a { | 20 | display: flex; |
35 | margin-top: 0.25rem; | 21 | justify-content: space-between; |
36 | margin-right: 0 !important; | 22 | align-items: center; |
37 | } | 23 | max-width: $max-channels-width; |
38 | } | ||
39 | |||
40 | a { | ||
41 | @include peertube-button-outline; | ||
42 | } | ||
43 | 24 | ||
44 | my-subscribe-button { | 25 | simple-search-input { |
45 | min-height: 30px; | 26 | margin-left: auto; |
46 | } | 27 | } |
47 | } | 28 | } |
48 | 29 | ||
@@ -60,39 +41,106 @@ my-user-moderation-dropdown, | |||
60 | 41 | ||
61 | .copy-button { | 42 | .copy-button { |
62 | border: none; | 43 | border: none; |
63 | padding: 5px; | 44 | } |
64 | margin-top: -2px; | 45 | |
46 | .account-info { | ||
47 | @include grid-videos-miniature-margins(false, 15px); | ||
48 | |||
49 | display: grid; | ||
50 | grid-template-columns: 1fr min-content; | ||
51 | grid-template-rows: auto auto; | ||
52 | |||
53 | background-color: pvar(--submenuBackgroundColor); | ||
54 | margin-bottom: 45px; | ||
55 | padding-top: var(--myGlobalTopPadding); | ||
56 | font-size: var(--myFontSize); | ||
57 | } | ||
58 | |||
59 | .account-avatar-row { | ||
60 | @include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize)); | ||
61 | } | ||
62 | |||
63 | .description { | ||
64 | grid-column: 1 / 3; | ||
65 | max-width: 1000px; | ||
66 | word-break: break-word; | ||
67 | } | ||
68 | |||
69 | .created-at { | ||
70 | margin-top: 15px; | ||
71 | color: pvar(--greyForegroundColor); | ||
72 | padding-bottom: 60px; | ||
73 | } | ||
74 | |||
75 | .show-more { | ||
76 | @include show-more-description; | ||
77 | |||
78 | display: none; | ||
79 | text-align: center; | ||
80 | } | ||
81 | |||
82 | .buttons { | ||
83 | grid-column: 2; | ||
84 | grid-row: 1; | ||
85 | |||
86 | display: flex; | ||
87 | flex-wrap: wrap; | ||
88 | justify-content: flex-end; | ||
89 | align-content: flex-start; | ||
90 | |||
91 | > *:not(:last-child) { | ||
92 | margin-bottom: 15px; | ||
93 | } | ||
94 | |||
95 | > a { | ||
96 | white-space: nowrap; | ||
97 | } | ||
98 | } | ||
99 | |||
100 | @media screen and (max-width: $small-view) { | ||
101 | .root { | ||
102 | --myGlobalTopPadding: 45px; | ||
103 | --myChannelImgMargin: 15px; | ||
104 | } | ||
105 | |||
106 | .account-info { | ||
107 | display: block; | ||
108 | padding-bottom: 60px; | ||
109 | } | ||
110 | |||
111 | .description:not(.expanded) { | ||
112 | max-height: 70px; | ||
113 | |||
114 | @include fade-text(30px, pvar(--submenuBackgroundColor)); | ||
115 | } | ||
116 | |||
117 | .show-more { | ||
118 | display: block; | ||
119 | } | ||
120 | |||
121 | .buttons { | ||
122 | justify-content: center; | ||
123 | } | ||
65 | } | 124 | } |
66 | 125 | ||
67 | @media screen and (max-width: $mobile-view) { | 126 | @media screen and (max-width: $mobile-view) { |
68 | .sub-menu { | 127 | .root { |
69 | .actor { | 128 | --myGlobalTopPadding: 15px; |
70 | flex-direction: column; | 129 | --myFontSize: 14px; |
71 | align-items: center; | 130 | --myGreyFontSize: 13px; |
72 | 131 | } | |
73 | img, | 132 | |
74 | .actor-info .actor-names .actor-display-name { | 133 | .account-info { |
75 | margin-right: 0; | 134 | display: block; |
76 | } | 135 | padding-bottom: 30px; |
77 | 136 | } | |
78 | .actor-info { | 137 | |
79 | .actor-names { | 138 | .links { |
80 | flex-direction: column; | 139 | margin: auto !important; |
81 | align-items: center; | 140 | width: min-content; |
82 | } | 141 | } |
83 | 142 | ||
84 | my-user-moderation-dropdown { | 143 | .show-more { |
85 | margin-left: 0; | 144 | margin-bottom: 30px; |
86 | } | ||
87 | |||
88 | .actor-followers { | ||
89 | text-align: center; | ||
90 | } | ||
91 | } | ||
92 | |||
93 | .right-buttons { | ||
94 | margin-left: 0; | ||
95 | } | ||
96 | } | ||
97 | } | 145 | } |
98 | } | 146 | } |
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index e6a5a5d5e..fbd7380a9 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts | |||
@@ -2,11 +2,19 @@ import { Subscription } from 'rxjs' | |||
2 | import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators' | 2 | import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators' |
3 | import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' | 3 | import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' |
4 | import { ActivatedRoute } from '@angular/router' | 4 | import { ActivatedRoute } from '@angular/router' |
5 | import { AuthService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core' | 5 | import { AuthService, MarkdownService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core' |
6 | import { Account, AccountService, DropdownAction, ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 6 | import { |
7 | Account, | ||
8 | AccountService, | ||
9 | DropdownAction, | ||
10 | ListOverflowItem, | ||
11 | VideoChannel, | ||
12 | VideoChannelService, | ||
13 | VideoService | ||
14 | } from '@app/shared/shared-main' | ||
7 | import { AccountReportComponent } from '@app/shared/shared-moderation' | 15 | import { AccountReportComponent } from '@app/shared/shared-moderation' |
8 | import { User, UserRight } from '@shared/models' | ||
9 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 16 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
17 | import { User, UserRight } from '@shared/models' | ||
10 | import { AccountSearchComponent } from './account-search/account-search.component' | 18 | import { AccountSearchComponent } from './account-search/account-search.component' |
11 | 19 | ||
12 | @Component({ | 20 | @Component({ |
@@ -15,16 +23,23 @@ import { AccountSearchComponent } from './account-search/account-search.componen | |||
15 | }) | 23 | }) |
16 | export class AccountsComponent implements OnInit, OnDestroy { | 24 | export class AccountsComponent implements OnInit, OnDestroy { |
17 | @ViewChild('accountReportModal') accountReportModal: AccountReportComponent | 25 | @ViewChild('accountReportModal') accountReportModal: AccountReportComponent |
26 | |||
18 | accountSearch: AccountSearchComponent | 27 | accountSearch: AccountSearchComponent |
19 | 28 | ||
20 | account: Account | 29 | account: Account |
21 | accountUser: User | 30 | accountUser: User |
31 | |||
22 | videoChannels: VideoChannel[] = [] | 32 | videoChannels: VideoChannel[] = [] |
33 | |||
23 | links: ListOverflowItem[] = [] | 34 | links: ListOverflowItem[] = [] |
35 | hideMenu = false | ||
24 | 36 | ||
25 | isAccountManageable = false | ||
26 | accountFollowerTitle = '' | 37 | accountFollowerTitle = '' |
27 | 38 | ||
39 | accountVideosCount: number | ||
40 | accountDescriptionHTML = '' | ||
41 | accountDescriptionExpanded = false | ||
42 | |||
28 | prependModerationActions: DropdownAction<any>[] | 43 | prependModerationActions: DropdownAction<any>[] |
29 | 44 | ||
30 | private routeSub: Subscription | 45 | private routeSub: Subscription |
@@ -38,6 +53,8 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
38 | private restExtractor: RestExtractor, | 53 | private restExtractor: RestExtractor, |
39 | private redirectService: RedirectService, | 54 | private redirectService: RedirectService, |
40 | private authService: AuthService, | 55 | private authService: AuthService, |
56 | private videoService: VideoService, | ||
57 | private markdown: MarkdownService, | ||
41 | private screenService: ScreenService | 58 | private screenService: ScreenService |
42 | ) { | 59 | ) { |
43 | } | 60 | } |
@@ -62,9 +79,8 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
62 | ) | 79 | ) |
63 | 80 | ||
64 | this.links = [ | 81 | this.links = [ |
65 | { label: $localize`VIDEO CHANNELS`, routerLink: 'video-channels' }, | 82 | { label: $localize`CHANNELS`, routerLink: 'video-channels' }, |
66 | { label: $localize`VIDEOS`, routerLink: 'videos' }, | 83 | { label: $localize`VIDEOS`, routerLink: 'videos' } |
67 | { label: $localize`ABOUT`, routerLink: 'about' } | ||
68 | ] | 84 | ] |
69 | } | 85 | } |
70 | 86 | ||
@@ -72,19 +88,29 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
72 | if (this.routeSub) this.routeSub.unsubscribe() | 88 | if (this.routeSub) this.routeSub.unsubscribe() |
73 | } | 89 | } |
74 | 90 | ||
75 | get naiveAggregatedSubscribers () { | 91 | naiveAggregatedSubscribers () { |
76 | return this.videoChannels.reduce( | 92 | return this.videoChannels.reduce( |
77 | (acc, val) => acc + val.followersCount, | 93 | (acc, val) => acc + val.followersCount, |
78 | this.account.followersCount // accumulator starts with the base number of subscribers the account has | 94 | this.account.followersCount // accumulator starts with the base number of subscribers the account has |
79 | ) | 95 | ) |
80 | } | 96 | } |
81 | 97 | ||
82 | get isInSmallView () { | 98 | isUserLoggedIn () { |
99 | return this.authService.isLoggedIn() | ||
100 | } | ||
101 | |||
102 | isInSmallView () { | ||
83 | return this.screenService.isInSmallView() | 103 | return this.screenService.isInSmallView() |
84 | } | 104 | } |
85 | 105 | ||
106 | isManageable () { | ||
107 | if (!this.isUserLoggedIn()) return false | ||
108 | |||
109 | return this.account?.userId === this.authService.getUser().id | ||
110 | } | ||
111 | |||
86 | onUserChanged () { | 112 | onUserChanged () { |
87 | this.getUserIfNeeded(this.account) | 113 | this.loadUserIfNeeded(this.account) |
88 | } | 114 | } |
89 | 115 | ||
90 | onUserDeleted () { | 116 | onUserDeleted () { |
@@ -113,40 +139,38 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
113 | if (this.accountSearch) this.accountSearch.updateSearch(search) | 139 | if (this.accountSearch) this.accountSearch.updateSearch(search) |
114 | } | 140 | } |
115 | 141 | ||
116 | private onAccount (account: Account) { | 142 | onSearchInputDisplayChanged (displayed: boolean) { |
143 | this.hideMenu = this.isInSmallView() && displayed | ||
144 | } | ||
145 | |||
146 | hasVideoChannels () { | ||
147 | return this.videoChannels.length !== 0 | ||
148 | } | ||
149 | |||
150 | hasShowMoreDescription () { | ||
151 | return !this.accountDescriptionExpanded && this.accountDescriptionHTML.length > 100 | ||
152 | } | ||
153 | |||
154 | private async onAccount (account: Account) { | ||
155 | this.accountFollowerTitle = $localize`${account.followersCount} direct account followers` | ||
156 | |||
117 | this.prependModerationActions = undefined | 157 | this.prependModerationActions = undefined |
118 | 158 | ||
119 | this.account = account | 159 | this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description) |
120 | 160 | ||
121 | if (this.authService.isLoggedIn()) { | 161 | // After the markdown renderer to avoid layout changes |
122 | this.authService.userInformationLoaded.subscribe( | 162 | this.account = account |
123 | () => { | ||
124 | this.isAccountManageable = this.account.userId && this.account.userId === this.authService.getUser().id | ||
125 | |||
126 | const followers = this.subscribersDisplayFor(account.followersCount) | ||
127 | this.accountFollowerTitle = $localize`${followers} direct account followers` | ||
128 | |||
129 | // It's not our account, we can report it | ||
130 | if (!this.isAccountManageable) { | ||
131 | this.prependModerationActions = [ | ||
132 | { | ||
133 | label: $localize`Report this account`, | ||
134 | handler: () => this.showReportModal() | ||
135 | } | ||
136 | ] | ||
137 | } | ||
138 | } | ||
139 | ) | ||
140 | } | ||
141 | 163 | ||
142 | this.getUserIfNeeded(account) | 164 | this.updateModerationActions() |
165 | this.loadUserIfNeeded(account) | ||
166 | this.loadAccountVideosCount() | ||
143 | } | 167 | } |
144 | 168 | ||
145 | private showReportModal () { | 169 | private showReportModal () { |
146 | this.accountReportModal.show() | 170 | this.accountReportModal.show() |
147 | } | 171 | } |
148 | 172 | ||
149 | private getUserIfNeeded (account: Account) { | 173 | private loadUserIfNeeded (account: Account) { |
150 | if (!account.userId || !this.authService.isLoggedIn()) return | 174 | if (!account.userId || !this.authService.isLoggedIn()) return |
151 | 175 | ||
152 | const user = this.authService.getUser() | 176 | const user = this.authService.getUser() |
@@ -158,4 +182,33 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
158 | ) | 182 | ) |
159 | } | 183 | } |
160 | } | 184 | } |
185 | |||
186 | private updateModerationActions () { | ||
187 | if (!this.authService.isLoggedIn()) return | ||
188 | |||
189 | this.authService.userInformationLoaded.subscribe( | ||
190 | () => { | ||
191 | if (this.isManageable()) return | ||
192 | |||
193 | // It's not our account, we can report it | ||
194 | this.prependModerationActions = [ | ||
195 | { | ||
196 | label: $localize`Report this account`, | ||
197 | handler: () => this.showReportModal() | ||
198 | } | ||
199 | ] | ||
200 | } | ||
201 | ) | ||
202 | } | ||
203 | |||
204 | private loadAccountVideosCount () { | ||
205 | this.videoService.getAccountVideos({ | ||
206 | account: this.account, | ||
207 | videoPagination: { | ||
208 | currentPage: 1, | ||
209 | itemsPerPage: 0 | ||
210 | }, | ||
211 | sort: '-publishedAt' | ||
212 | }).subscribe(res => this.accountVideosCount = res.total) | ||
213 | } | ||
161 | } | 214 | } |
diff --git a/client/src/app/+accounts/accounts.module.ts b/client/src/app/+accounts/accounts.module.ts index 6da65cbc1..3354b4189 100644 --- a/client/src/app/+accounts/accounts.module.ts +++ b/client/src/app/+accounts/accounts.module.ts | |||
@@ -5,10 +5,9 @@ import { SharedMainModule } from '@app/shared/shared-main' | |||
5 | import { SharedModerationModule } from '@app/shared/shared-moderation' | 5 | import { SharedModerationModule } from '@app/shared/shared-moderation' |
6 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' | 6 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' |
7 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' | 7 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' |
8 | import { AccountAboutComponent } from './account-about/account-about.component' | 8 | import { AccountSearchComponent } from './account-search/account-search.component' |
9 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' | 9 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' |
10 | import { AccountVideosComponent } from './account-videos/account-videos.component' | 10 | import { AccountVideosComponent } from './account-videos/account-videos.component' |
11 | import { AccountSearchComponent } from './account-search/account-search.component' | ||
12 | import { AccountsRoutingModule } from './accounts-routing.module' | 11 | import { AccountsRoutingModule } from './accounts-routing.module' |
13 | import { AccountsComponent } from './accounts.component' | 12 | import { AccountsComponent } from './accounts.component' |
14 | 13 | ||
@@ -28,7 +27,6 @@ import { AccountsComponent } from './accounts.component' | |||
28 | AccountsComponent, | 27 | AccountsComponent, |
29 | AccountVideosComponent, | 28 | AccountVideosComponent, |
30 | AccountVideoChannelsComponent, | 29 | AccountVideoChannelsComponent, |
31 | AccountAboutComponent, | ||
32 | AccountSearchComponent | 30 | AccountSearchComponent |
33 | ], | 31 | ], |
34 | 32 | ||
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index fd648a425..bac65c88e 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts | |||
@@ -3,6 +3,7 @@ import { SelectButtonModule } from 'primeng/selectbutton' | |||
3 | import { TableModule } from 'primeng/table' | 3 | import { TableModule } from 'primeng/table' |
4 | import { NgModule } from '@angular/core' | 4 | import { NgModule } from '@angular/core' |
5 | import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' | 5 | import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' |
6 | import { SharedActorImageModule } from '@app/shared/shared-actor-image' | ||
6 | import { SharedFormModule } from '@app/shared/shared-forms' | 7 | import { SharedFormModule } from '@app/shared/shared-forms' |
7 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' | 8 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' |
8 | import { SharedMainModule } from '@app/shared/shared-main' | 9 | import { SharedMainModule } from '@app/shared/shared-main' |
@@ -49,6 +50,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom | |||
49 | SharedGlobalIconModule, | 50 | SharedGlobalIconModule, |
50 | SharedAbuseListModule, | 51 | SharedAbuseListModule, |
51 | SharedVideoCommentModule, | 52 | SharedVideoCommentModule, |
53 | SharedActorImageModule, | ||
52 | 54 | ||
53 | TableModule, | 55 | TableModule, |
54 | SelectButtonModule, | 56 | SelectButtonModule, |
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts index 82c371f4d..d6aca10e7 100644 --- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts +++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts | |||
@@ -164,7 +164,8 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV | |||
164 | baseUrl: `${environment.originServerUrl}/videos/embed/${entry.video.uuid}`, | 164 | baseUrl: `${environment.originServerUrl}/videos/embed/${entry.video.uuid}`, |
165 | title: false, | 165 | title: false, |
166 | warningTitle: false | 166 | warningTitle: false |
167 | }) | 167 | }), |
168 | entry.video.name | ||
168 | ) | 169 | ) |
169 | } | 170 | } |
170 | 171 | ||
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/+admin/plugins/shared/plugin-list.component.scss b/client/src/app/+admin/plugins/shared/plugin-list.component.scss index 83030b7e0..f59a01b74 100644 --- a/client/src/app/+admin/plugins/shared/plugin-list.component.scss +++ b/client/src/app/+admin/plugins/shared/plugin-list.component.scss | |||
@@ -3,7 +3,7 @@ | |||
3 | 3 | ||
4 | .plugin { | 4 | .plugin { |
5 | margin: 15px 0; | 5 | margin: 15px 0; |
6 | background-color: pvar(--submenuColor); | 6 | background-color: pvar(--submenuBackgroundColor); |
7 | } | 7 | } |
8 | 8 | ||
9 | .first-row { | 9 | .first-row { |
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html index 243c6556a..5e92c0f36 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.html +++ b/client/src/app/+admin/users/user-edit/user-edit.component.html | |||
@@ -72,7 +72,7 @@ | |||
72 | <div class="anchor" id="user"></div> <!-- user anchor --> | 72 | <div class="anchor" id="user"></div> <!-- user anchor --> |
73 | <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div> | 73 | <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div> |
74 | <div *ngIf="!isCreation() && user" class="account-title"> | 74 | <div *ngIf="!isCreation() && user" class="account-title"> |
75 | <my-actor-avatar-info [actor]="user.account"></my-actor-avatar-info> | 75 | <my-actor-avatar-edit [actor]="user.account" [editable]="false" [displaySubscribers]="false" [displayUsername]="false"></my-actor-avatar-edit> |
76 | </div> | 76 | </div> |
77 | </div> | 77 | </div> |
78 | 78 | ||
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.scss b/client/src/app/+admin/users/user-edit/user-edit.component.scss index aa87b8d6d..8b0ac8783 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.scss +++ b/client/src/app/+admin/users/user-edit/user-edit.component.scss | |||
@@ -72,11 +72,3 @@ input[type=submit], button { | |||
72 | @include dashboard; | 72 | @include dashboard; |
73 | max-width: 900px; | 73 | max-width: 900px; |
74 | } | 74 | } |
75 | |||
76 | my-actor-avatar-info ::ng-deep { | ||
77 | .actor-img-edit-container, | ||
78 | .actor-info-followers, | ||
79 | .actor-info-username { | ||
80 | display: none; | ||
81 | } | ||
82 | } | ||
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 | |||
3 | import { ActivatedRoute } from '@angular/router' | 3 | import { ActivatedRoute } from '@angular/router' |
4 | import { AuthService, Notifier, RedirectService, UserService } from '@app/core' | 4 | import { AuthService, Notifier, RedirectService, UserService } from '@app/core' |
5 | import { HooksService } from '@app/core/plugins/hooks.service' | 5 | import { HooksService } from '@app/core/plugins/hooks.service' |
6 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' | ||
7 | import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators' | 6 | import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators' |
8 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 7 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' |
8 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' | ||
9 | import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | 9 | import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
10 | import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' | 10 | import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' |
11 | 11 | ||
@@ -16,7 +16,6 @@ import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' | |||
16 | }) | 16 | }) |
17 | 17 | ||
18 | export class LoginComponent extends FormReactive implements OnInit, AfterViewInit { | 18 | export 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/+my-account/my-account-settings/my-account-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html index b0d2ec58d..48d06280b 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html | |||
@@ -3,7 +3,7 @@ | |||
3 | <div class="form-group col-12 col-lg-4 col-xl-3"></div> | 3 | <div class="form-group col-12 col-lg-4 col-xl-3"></div> |
4 | 4 | ||
5 | <div class="form-group col-12 col-lg-8 col-xl-9"> | 5 | <div class="form-group col-12 col-lg-8 col-xl-9"> |
6 | <my-actor-avatar-info [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-info> | 6 | <my-actor-avatar-edit [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-edit> |
7 | </div> | 7 | </div> |
8 | </div> | 8 | </div> |
9 | 9 | ||
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index 076864563..3df48d0aa 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts | |||
@@ -3,6 +3,7 @@ import { TableModule } from 'primeng/table' | |||
3 | import { DragDropModule } from '@angular/cdk/drag-drop' | 3 | import { DragDropModule } from '@angular/cdk/drag-drop' |
4 | import { NgModule } from '@angular/core' | 4 | import { NgModule } from '@angular/core' |
5 | import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' | 5 | import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' |
6 | import { SharedActorImageModule } from '@app/shared/shared-actor-image' | ||
6 | import { SharedFormModule } from '@app/shared/shared-forms' | 7 | import { SharedFormModule } from '@app/shared/shared-forms' |
7 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' | 8 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' |
8 | import { SharedMainModule } from '@app/shared/shared-main' | 9 | import { SharedMainModule } from '@app/shared/shared-main' |
@@ -10,6 +11,7 @@ import { SharedModerationModule } from '@app/shared/shared-moderation' | |||
10 | import { SharedShareModal } from '@app/shared/shared-share-modal' | 11 | import { SharedShareModal } from '@app/shared/shared-share-modal' |
11 | import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' | 12 | import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' |
12 | import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' | 13 | import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' |
14 | import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' | ||
13 | import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' | 15 | import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' |
14 | import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' | 16 | import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' |
15 | import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' | 17 | import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' |
@@ -20,7 +22,6 @@ import { MyAccountDangerZoneComponent } from './my-account-settings/my-account-d | |||
20 | import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' | 22 | import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' |
21 | import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' | 23 | import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' |
22 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' | 24 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' |
23 | import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' | ||
24 | import { MyAccountComponent } from './my-account.component' | 25 | import { MyAccountComponent } from './my-account.component' |
25 | 26 | ||
26 | @NgModule({ | 27 | @NgModule({ |
@@ -37,7 +38,8 @@ import { MyAccountComponent } from './my-account.component' | |||
37 | SharedUserInterfaceSettingsModule, | 38 | SharedUserInterfaceSettingsModule, |
38 | SharedGlobalIconModule, | 39 | SharedGlobalIconModule, |
39 | SharedAbuseListModule, | 40 | SharedAbuseListModule, |
40 | SharedShareModal | 41 | SharedShareModal, |
42 | SharedActorImageModule | ||
41 | ], | 43 | ], |
42 | 44 | ||
43 | declarations: [ | 45 | declarations: [ |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts index a625493de..b3265210f 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts | |||
@@ -8,10 +8,12 @@ import { | |||
8 | VIDEO_CHANNEL_SUPPORT_VALIDATOR | 8 | VIDEO_CHANNEL_SUPPORT_VALIDATOR |
9 | } from '@app/shared/form-validators/video-channel-validators' | 9 | } from '@app/shared/form-validators/video-channel-validators' |
10 | import { FormValidatorService } from '@app/shared/shared-forms' | 10 | import { FormValidatorService } from '@app/shared/shared-forms' |
11 | import { VideoChannelService } from '@app/shared/shared-main' | 11 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' |
12 | import { VideoChannelCreate } from '@shared/models' | 12 | import { VideoChannelCreate } from '@shared/models' |
13 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 13 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
14 | import { MyVideoChannelEdit } from './my-video-channel-edit' | 14 | import { MyVideoChannelEdit } from './my-video-channel-edit' |
15 | import { switchMap } from 'rxjs/operators' | ||
16 | import { of } from 'rxjs' | ||
15 | 17 | ||
16 | @Component({ | 18 | @Component({ |
17 | templateUrl: './my-video-channel-edit.component.html', | 19 | templateUrl: './my-video-channel-edit.component.html', |
@@ -19,6 +21,10 @@ import { MyVideoChannelEdit } from './my-video-channel-edit' | |||
19 | }) | 21 | }) |
20 | export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements OnInit { | 22 | export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements OnInit { |
21 | error: string | 23 | error: string |
24 | videoChannel = new VideoChannel({}) | ||
25 | |||
26 | private avatar: FormData | ||
27 | private banner: FormData | ||
22 | 28 | ||
23 | constructor ( | 29 | constructor ( |
24 | protected formValidatorService: FormValidatorService, | 30 | protected formValidatorService: FormValidatorService, |
@@ -50,23 +56,43 @@ export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements | |||
50 | support: body.support || null | 56 | support: body.support || null |
51 | } | 57 | } |
52 | 58 | ||
53 | this.videoChannelService.createVideoChannel(videoChannelCreate).subscribe( | 59 | this.videoChannelService.createVideoChannel(videoChannelCreate) |
54 | () => { | 60 | .pipe( |
55 | this.authService.refreshUserInformation() | 61 | switchMap(() => this.uploadAvatar()), |
62 | switchMap(() => this.uploadBanner()) | ||
63 | ).subscribe( | ||
64 | () => { | ||
65 | this.authService.refreshUserInformation() | ||
66 | |||
67 | this.notifier.success($localize`Video channel ${videoChannelCreate.displayName} created.`) | ||
68 | this.router.navigate(['/my-library', 'video-channels']) | ||
69 | }, | ||
56 | 70 | ||
57 | this.notifier.success($localize`Video channel ${videoChannelCreate.displayName} created.`) | 71 | err => { |
58 | this.router.navigate([ '/my-library', 'video-channels' ]) | 72 | if (err.status === HttpStatusCode.CONFLICT_409) { |
59 | }, | 73 | this.error = $localize`This name already exists on this instance.` |
74 | return | ||
75 | } | ||
60 | 76 | ||
61 | err => { | 77 | this.error = err.message |
62 | if (err.status === HttpStatusCode.CONFLICT_409) { | ||
63 | this.error = $localize`This name already exists on this instance.` | ||
64 | return | ||
65 | } | 78 | } |
79 | ) | ||
80 | } | ||
81 | |||
82 | onAvatarChange (formData: FormData) { | ||
83 | this.avatar = formData | ||
84 | } | ||
85 | |||
86 | onAvatarDelete () { | ||
87 | this.avatar = null | ||
88 | } | ||
89 | |||
90 | onBannerChange (formData: FormData) { | ||
91 | this.banner = formData | ||
92 | } | ||
66 | 93 | ||
67 | this.error = err.message | 94 | onBannerDelete () { |
68 | } | 95 | this.banner = null |
69 | ) | ||
70 | } | 96 | } |
71 | 97 | ||
72 | isCreation () { | 98 | isCreation () { |
@@ -76,4 +102,20 @@ export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements | |||
76 | getFormButtonTitle () { | 102 | getFormButtonTitle () { |
77 | return $localize`Create` | 103 | return $localize`Create` |
78 | } | 104 | } |
105 | |||
106 | getUsername () { | ||
107 | return this.form.value.name | ||
108 | } | ||
109 | |||
110 | private uploadAvatar () { | ||
111 | if (!this.avatar) return of(undefined) | ||
112 | |||
113 | return this.videoChannelService.changeVideoChannelImage(this.getUsername(), this.avatar, 'avatar') | ||
114 | } | ||
115 | |||
116 | private uploadBanner () { | ||
117 | if (!this.banner) return of(undefined) | ||
118 | |||
119 | return this.videoChannelService.changeVideoChannelImage(this.getUsername(), this.banner, 'banner') | ||
120 | } | ||
79 | } | 121 | } |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html index 735f9e3ba..2910dffad 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html | |||
@@ -10,7 +10,7 @@ | |||
10 | <ng-container *ngIf="!isCreation()"> | 10 | <ng-container *ngIf="!isCreation()"> |
11 | <li class="breadcrumb-item active" i18n>Edit</li> | 11 | <li class="breadcrumb-item active" i18n>Edit</li> |
12 | <li class="breadcrumb-item active" aria-current="page"> | 12 | <li class="breadcrumb-item active" aria-current="page"> |
13 | <a *ngIf="videoChannelToUpdate" [routerLink]="[ '/my-library/video-channels/update', videoChannelToUpdate?.nameWithHost ]">{{ videoChannelToUpdate?.displayName }}</a> | 13 | <a *ngIf="videoChannel" [routerLink]="[ '/my-library/video-channels/update', videoChannel?.nameWithHost ]">{{ videoChannel?.displayName }}</a> |
14 | </li> | 14 | </li> |
15 | </ng-container> | 15 | </ng-container> |
16 | </ol> | 16 | </ol> |
@@ -23,10 +23,22 @@ | |||
23 | <div class="form-row"> <!-- channel grid --> | 23 | <div class="form-row"> <!-- channel grid --> |
24 | <div class="form-group col-12 col-lg-4 col-xl-3"> | 24 | <div class="form-group col-12 col-lg-4 col-xl-3"> |
25 | <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div> | 25 | <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div> |
26 | <div *ngIf="!isCreation() && videoChannelToUpdate" class="video-channel-title" i18n>CHANNEL</div> | 26 | <div *ngIf="!isCreation() && videoChannel" class="video-channel-title" i18n>CHANNEL</div> |
27 | </div> | 27 | </div> |
28 | 28 | ||
29 | <div class="form-group col-12 col-lg-8 col-xl-9"> | 29 | <div class="form-group col-12 col-lg-8 col-xl-9"> |
30 | <h6 i18n>Banner image of your channel</h6> | ||
31 | |||
32 | <my-actor-banner-edit | ||
33 | *ngIf="videoChannel" [previewImage]="isCreation()" | ||
34 | [actor]="videoChannel" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()" | ||
35 | ></my-actor-banner-edit> | ||
36 | |||
37 | <my-actor-avatar-edit | ||
38 | *ngIf="videoChannel" [previewImage]="isCreation()" | ||
39 | [actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()" | ||
40 | [displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()" | ||
41 | ></my-actor-avatar-edit> | ||
30 | 42 | ||
31 | <div class="form-group" *ngIf="isCreation()"> | 43 | <div class="form-group" *ngIf="isCreation()"> |
32 | <label i18n for="name">Name</label> | 44 | <label i18n for="name">Name</label> |
@@ -44,11 +56,6 @@ | |||
44 | </div> | 56 | </div> |
45 | </div> | 57 | </div> |
46 | 58 | ||
47 | <my-actor-avatar-info | ||
48 | *ngIf="!isCreation() && videoChannelToUpdate" | ||
49 | [actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()" | ||
50 | ></my-actor-avatar-info> | ||
51 | |||
52 | <div class="form-group"> | 59 | <div class="form-group"> |
53 | <label i18n for="display-name">Display name</label> | 60 | <label i18n for="display-name">Display name</label> |
54 | <input | 61 | <input |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss index 8f8af655c..22de103d1 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss | |||
@@ -10,11 +10,16 @@ label { | |||
10 | @include settings-big-title; | 10 | @include settings-big-title; |
11 | } | 11 | } |
12 | 12 | ||
13 | my-actor-avatar-info { | 13 | my-actor-avatar-edit, |
14 | my-actor-banner-edit { | ||
14 | display: block; | 15 | display: block; |
15 | margin-bottom: 20px; | 16 | margin-bottom: 20px; |
16 | } | 17 | } |
17 | 18 | ||
19 | my-actor-banner-edit { | ||
20 | max-width: 500px; | ||
21 | } | ||
22 | |||
18 | .input-group { | 23 | .input-group { |
19 | @include peertube-input-group(fit-content); | 24 | @include peertube-input-group(fit-content); |
20 | } | 25 | } |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts index 3e20a27ee..33bb90f14 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts | |||
@@ -2,8 +2,7 @@ import { FormReactive } from '@app/shared/shared-forms' | |||
2 | import { VideoChannel } from '@app/shared/shared-main' | 2 | import { VideoChannel } from '@app/shared/shared-main' |
3 | 3 | ||
4 | export abstract class MyVideoChannelEdit extends FormReactive { | 4 | export abstract class MyVideoChannelEdit extends FormReactive { |
5 | // We need it even in the create component because it's used in the edit template | 5 | videoChannel: VideoChannel |
6 | videoChannelToUpdate: VideoChannel | ||
7 | 6 | ||
8 | abstract isCreation (): boolean | 7 | abstract isCreation (): boolean |
9 | abstract getFormButtonTitle (): string | 8 | abstract getFormButtonTitle (): string |
@@ -12,10 +11,6 @@ export abstract class MyVideoChannelEdit extends FormReactive { | |||
12 | return window.location.host | 11 | return window.location.host |
13 | } | 12 | } |
14 | 13 | ||
15 | // We need this method so angular does not complain in child template that doesn't need this | ||
16 | onAvatarChange (formData: FormData) { /* empty */ } | ||
17 | onAvatarDelete () { /* empty */ } | ||
18 | |||
19 | // Should be implemented by the child | 14 | // Should be implemented by the child |
20 | isBulkUpdateVideosDisplayed () { | 15 | isBulkUpdateVideosDisplayed () { |
21 | return false | 16 | return false |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts index 6cd1ff503..a29af176c 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts | |||
@@ -1,7 +1,9 @@ | |||
1 | import { Subscription } from 'rxjs' | 1 | import { Subscription } from 'rxjs' |
2 | import { HttpErrorResponse } from '@angular/common/http' | ||
2 | import { Component, OnDestroy, OnInit } from '@angular/core' | 3 | import { Component, OnDestroy, OnInit } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { AuthService, Notifier, ServerService } from '@app/core' | 5 | import { AuthService, Notifier, ServerService } from '@app/core' |
6 | import { uploadErrorHandler } from '@app/helpers' | ||
5 | import { | 7 | import { |
6 | VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, | 8 | VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, |
7 | VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, | 9 | VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, |
@@ -11,8 +13,6 @@ import { FormValidatorService } from '@app/shared/shared-forms' | |||
11 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 13 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' |
12 | import { ServerConfig, VideoChannelUpdate } from '@shared/models' | 14 | import { ServerConfig, VideoChannelUpdate } from '@shared/models' |
13 | import { MyVideoChannelEdit } from './my-video-channel-edit' | 15 | import { MyVideoChannelEdit } from './my-video-channel-edit' |
14 | import { HttpErrorResponse } from '@angular/common/http' | ||
15 | import { uploadErrorHandler } from '@app/helpers' | ||
16 | 16 | ||
17 | @Component({ | 17 | @Component({ |
18 | selector: 'my-video-channel-update', | 18 | selector: 'my-video-channel-update', |
@@ -21,7 +21,7 @@ import { uploadErrorHandler } from '@app/helpers' | |||
21 | }) | 21 | }) |
22 | export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements OnInit, OnDestroy { | 22 | export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements OnInit, OnDestroy { |
23 | error: string | 23 | error: string |
24 | videoChannelToUpdate: VideoChannel | 24 | videoChannel: VideoChannel |
25 | 25 | ||
26 | private paramsSub: Subscription | 26 | private paramsSub: Subscription |
27 | private oldSupportField: string | 27 | private oldSupportField: string |
@@ -56,7 +56,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements | |||
56 | 56 | ||
57 | this.videoChannelService.getVideoChannel(videoChannelId).subscribe( | 57 | this.videoChannelService.getVideoChannel(videoChannelId).subscribe( |
58 | videoChannelToUpdate => { | 58 | videoChannelToUpdate => { |
59 | this.videoChannelToUpdate = videoChannelToUpdate | 59 | this.videoChannel = videoChannelToUpdate |
60 | 60 | ||
61 | this.oldSupportField = videoChannelToUpdate.support | 61 | this.oldSupportField = videoChannelToUpdate.support |
62 | 62 | ||
@@ -87,7 +87,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements | |||
87 | bulkVideosSupportUpdate: body.bulkVideosSupportUpdate || false | 87 | bulkVideosSupportUpdate: body.bulkVideosSupportUpdate || false |
88 | } | 88 | } |
89 | 89 | ||
90 | this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe( | 90 | this.videoChannelService.updateVideoChannel(this.videoChannel.name, videoChannelUpdate).subscribe( |
91 | () => { | 91 | () => { |
92 | this.authService.refreshUserInformation() | 92 | this.authService.refreshUserInformation() |
93 | 93 | ||
@@ -101,12 +101,12 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements | |||
101 | } | 101 | } |
102 | 102 | ||
103 | onAvatarChange (formData: FormData) { | 103 | onAvatarChange (formData: FormData) { |
104 | this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData) | 104 | this.videoChannelService.changeVideoChannelImage(this.videoChannel.name, formData, 'avatar') |
105 | .subscribe( | 105 | .subscribe( |
106 | data => { | 106 | data => { |
107 | this.notifier.success($localize`Avatar changed.`) | 107 | this.notifier.success($localize`Avatar changed.`) |
108 | 108 | ||
109 | this.videoChannelToUpdate.updateAvatar(data.avatar) | 109 | this.videoChannel.updateAvatar(data.avatar) |
110 | }, | 110 | }, |
111 | 111 | ||
112 | (err: HttpErrorResponse) => uploadErrorHandler({ | 112 | (err: HttpErrorResponse) => uploadErrorHandler({ |
@@ -118,12 +118,42 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements | |||
118 | } | 118 | } |
119 | 119 | ||
120 | onAvatarDelete () { | 120 | onAvatarDelete () { |
121 | this.videoChannelService.deleteVideoChannelAvatar(this.videoChannelToUpdate.name) | 121 | this.videoChannelService.deleteVideoChannelImage(this.videoChannel.name, 'avatar') |
122 | .subscribe( | 122 | .subscribe( |
123 | data => { | 123 | data => { |
124 | this.notifier.success($localize`Avatar deleted.`) | 124 | this.notifier.success($localize`Avatar deleted.`) |
125 | 125 | ||
126 | this.videoChannelToUpdate.resetAvatar() | 126 | this.videoChannel.resetAvatar() |
127 | }, | ||
128 | |||
129 | err => this.notifier.error(err.message) | ||
130 | ) | ||
131 | } | ||
132 | |||
133 | onBannerChange (formData: FormData) { | ||
134 | this.videoChannelService.changeVideoChannelImage(this.videoChannel.name, formData, 'banner') | ||
135 | .subscribe( | ||
136 | data => { | ||
137 | this.notifier.success($localize`Banner changed.`) | ||
138 | |||
139 | this.videoChannel.updateBanner(data.banner) | ||
140 | }, | ||
141 | |||
142 | (err: HttpErrorResponse) => uploadErrorHandler({ | ||
143 | err, | ||
144 | name: $localize`banner`, | ||
145 | notifier: this.notifier | ||
146 | }) | ||
147 | ) | ||
148 | } | ||
149 | |||
150 | onBannerDelete () { | ||
151 | this.videoChannelService.deleteVideoChannelImage(this.videoChannel.name, 'banner') | ||
152 | .subscribe( | ||
153 | data => { | ||
154 | this.notifier.success($localize`Banner deleted.`) | ||
155 | |||
156 | this.videoChannel.resetBanner() | ||
127 | }, | 157 | }, |
128 | 158 | ||
129 | err => this.notifier.error(err.message) | 159 | err => this.notifier.error(err.message) |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss index f2f42459f..8804fa95c 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss | |||
@@ -17,10 +17,11 @@ input[type=text] { | |||
17 | 17 | ||
18 | .video-channel { | 18 | .video-channel { |
19 | @include row-blocks; | 19 | @include row-blocks; |
20 | |||
20 | padding-bottom: 0; | 21 | padding-bottom: 0; |
21 | 22 | ||
22 | img { | 23 | img { |
23 | @include avatar(80px); | 24 | @include channel-avatar(80px); |
24 | 25 | ||
25 | margin-right: 10px; | 26 | margin-right: 10px; |
26 | } | 27 | } |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts index 92b56db49..53557ca02 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { ChartModule } from 'primeng/chart' | 1 | import { ChartModule } from 'primeng/chart' |
2 | import { NgModule } from '@angular/core' | 2 | import { NgModule } from '@angular/core' |
3 | import { SharedActorImageModule } from '@app/shared/shared-actor-image' | ||
3 | import { SharedFormModule } from '@app/shared/shared-forms' | 4 | import { SharedFormModule } from '@app/shared/shared-forms' |
4 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' | 5 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' |
5 | import { SharedMainModule } from '@app/shared/shared-main' | 6 | import { SharedMainModule } from '@app/shared/shared-main' |
@@ -16,7 +17,8 @@ import { MyVideoChannelsComponent } from './my-video-channels.component' | |||
16 | 17 | ||
17 | SharedMainModule, | 18 | SharedMainModule, |
18 | SharedFormModule, | 19 | SharedFormModule, |
19 | SharedGlobalIconModule | 20 | SharedGlobalIconModule, |
21 | SharedActorImageModule | ||
20 | ], | 22 | ], |
21 | 23 | ||
22 | declarations: [ | 24 | declarations: [ |
diff --git a/client/src/app/+my-library/my-history/my-history.component.html b/client/src/app/+my-library/my-history/my-history.component.html index c180161e7..9dec64645 100644 --- a/client/src/app/+my-library/my-history/my-history.component.html +++ b/client/src/app/+my-library/my-history/my-history.component.html | |||
@@ -4,7 +4,7 @@ | |||
4 | </h1> | 4 | </h1> |
5 | 5 | ||
6 | <div class="top-buttons"> | 6 | <div class="top-buttons"> |
7 | <div> | 7 | <div class="search-wrapper"> |
8 | <div class="input-group has-feedback has-clear"> | 8 | <div class="input-group has-feedback has-clear"> |
9 | <input | 9 | <input |
10 | type="text" name="history-search" id="history-search" i18n-placeholder placeholder="Search your history" | 10 | type="text" name="history-search" id="history-search" i18n-placeholder placeholder="Search your history" |
@@ -15,7 +15,7 @@ | |||
15 | </div> | 15 | </div> |
16 | </div> | 16 | </div> |
17 | 17 | ||
18 | <div class="history-switch ml-auto mr-3"> | 18 | <div class="history-switch"> |
19 | <my-input-switch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></my-input-switch> | 19 | <my-input-switch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></my-input-switch> |
20 | <label i18n>Track watch history</label> | 20 | <label i18n>Track watch history</label> |
21 | </div> | 21 | </div> |
diff --git a/client/src/app/+my-library/my-history/my-history.component.scss b/client/src/app/+my-library/my-history/my-history.component.scss index 928a8a3da..af4a34b4b 100644 --- a/client/src/app/+my-library/my-history/my-history.component.scss +++ b/client/src/app/+my-library/my-history/my-history.component.scss | |||
@@ -11,16 +11,24 @@ | |||
11 | 11 | ||
12 | .top-buttons { | 12 | .top-buttons { |
13 | margin-bottom: 30px; | 13 | margin-bottom: 30px; |
14 | display: flex; | 14 | display: grid; |
15 | grid-template-columns: 250px 1fr auto auto; | ||
15 | align-items: center; | 16 | align-items: center; |
16 | flex-wrap: wrap; | ||
17 | 17 | ||
18 | #history-search { | 18 | .search-wrapper { |
19 | @include peertube-input-text(250px); | 19 | grid-column: 1; |
20 | |||
21 | input { | ||
22 | @include peertube-input-text(250px); | ||
23 | } | ||
20 | } | 24 | } |
21 | 25 | ||
22 | .history-switch { | 26 | .history-switch { |
27 | grid-column: 3; | ||
28 | |||
23 | display: flex; | 29 | display: flex; |
30 | margin-left: auto; | ||
31 | margin-right: 15px; | ||
24 | 32 | ||
25 | label { | 33 | label { |
26 | margin: 0 0 0 5px; | 34 | margin: 0 0 0 5px; |
@@ -31,6 +39,8 @@ | |||
31 | } | 39 | } |
32 | 40 | ||
33 | .delete-history { | 41 | .delete-history { |
42 | grid-column: 4; | ||
43 | |||
34 | @include peertube-button; | 44 | @include peertube-button; |
35 | @include grey-button; | 45 | @include grey-button; |
36 | @include button-with-icon; | 46 | @include button-with-icon; |
@@ -40,26 +50,27 @@ | |||
40 | } | 50 | } |
41 | 51 | ||
42 | .video { | 52 | .video { |
43 | @include row-blocks; | 53 | @include row-blocks($column-responsive: false); |
44 | |||
45 | .my-video-miniature { | ||
46 | flex-grow: 1; | ||
47 | } | ||
48 | } | 54 | } |
49 | 55 | ||
50 | @media screen and (max-width: $mobile-view) { | 56 | @media screen and (max-width: $small-view) { |
51 | .top-buttons { | 57 | .top-buttons { |
52 | .history-switch label, .delete-history { | 58 | grid-template-columns: auto 1fr auto; |
53 | @include ellipsis; | 59 | row-gap: 20px; |
54 | } | ||
55 | 60 | ||
56 | .history-switch label { | 61 | .history-switch { |
57 | width: 60%; | 62 | grid-row: 1; |
63 | grid-column: 1; | ||
64 | margin: 0; | ||
58 | } | 65 | } |
59 | 66 | ||
60 | .delete-history { | 67 | .delete-history { |
61 | margin-left: auto; | 68 | grid-row: 1; |
62 | max-width: 32%; | 69 | grid-column: 3; |
70 | } | ||
71 | |||
72 | .search-wrapper { | ||
73 | grid-column: 1 / 4; | ||
63 | } | 74 | } |
64 | } | 75 | } |
65 | } | 76 | } |
diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html index 510b400c0..ff448ad87 100644 --- a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html +++ b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html | |||
@@ -6,7 +6,7 @@ | |||
6 | </span> | 6 | </span> |
7 | </h1> | 7 | </h1> |
8 | 8 | ||
9 | <div class="video-subscriptions-header d-flex justify-content-between"> | 9 | <div class="video-subscriptions-header"> |
10 | <div class="has-feedback has-clear"> | 10 | <div class="has-feedback has-clear"> |
11 | <input type="text" placeholder="Search your subscriptions" i18n-placeholder [(ngModel)]="subscriptionsSearch" | 11 | <input type="text" placeholder="Search your subscriptions" i18n-placeholder [(ngModel)]="subscriptionsSearch" |
12 | (ngModelChange)="onSubscriptionsSearchChanged()" /> | 12 | (ngModelChange)="onSubscriptionsSearchChanged()" /> |
diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss index 5ead45dd8..3c1a4d2ad 100644 --- a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss +++ b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss | |||
@@ -9,40 +9,40 @@ input[type=text] { | |||
9 | @include row-blocks; | 9 | @include row-blocks; |
10 | 10 | ||
11 | img { | 11 | img { |
12 | @include avatar(80px); | 12 | @include channel-avatar(80px); |
13 | 13 | ||
14 | margin-right: 10px; | 14 | margin-right: 10px; |
15 | } | 15 | } |
16 | } | ||
16 | 17 | ||
17 | .video-channel-info { | 18 | .video-channel-info { |
18 | flex-grow: 1; | 19 | flex-grow: 1; |
19 | 20 | ||
20 | a.video-channel-names { | 21 | a.video-channel-names { |
21 | @include disable-default-a-behaviour; | 22 | @include disable-default-a-behaviour; |
22 | 23 | ||
23 | width: fit-content; | 24 | width: fit-content; |
24 | display: flex; | 25 | display: flex; |
25 | align-items: baseline; | 26 | align-items: baseline; |
26 | color: pvar(--mainForegroundColor); | 27 | color: pvar(--mainForegroundColor); |
27 | 28 | ||
28 | .video-channel-display-name { | 29 | .video-channel-display-name { |
29 | font-weight: $font-semibold; | 30 | font-weight: $font-semibold; |
30 | font-size: 18px; | 31 | font-size: 18px; |
31 | } | 32 | } |
32 | 33 | ||
33 | .video-channel-name { | 34 | .video-channel-name { |
34 | font-size: 14px; | 35 | font-size: 14px; |
35 | color: $grey-actor-name; | 36 | color: $grey-actor-name; |
36 | margin-left: 5px; | 37 | margin-left: 5px; |
37 | } | ||
38 | } | 38 | } |
39 | } | 39 | } |
40 | } | ||
40 | 41 | ||
41 | .actor-owner { | 42 | .actor-owner { |
42 | @include actor-owner; | 43 | @include actor-owner; |
43 | 44 | ||
44 | margin-top: 0; | 45 | margin-top: 0; |
45 | } | ||
46 | } | 46 | } |
47 | 47 | ||
48 | .video-subscriptions-header { | 48 | .video-subscriptions-header { |
@@ -50,32 +50,22 @@ input[type=text] { | |||
50 | } | 50 | } |
51 | 51 | ||
52 | @media screen and (max-width: $small-view) { | 52 | @media screen and (max-width: $small-view) { |
53 | .video-channel { | 53 | .video-subscriptions-header input[type=text] { |
54 | .video-channel-info { | 54 | width: 100% !important; |
55 | padding-bottom: 10px; | ||
56 | text-align: center; | ||
57 | |||
58 | .video-channel-names { | ||
59 | flex-direction: column; | ||
60 | align-items: center !important; | ||
61 | margin: auto; | ||
62 | } | ||
63 | } | ||
64 | |||
65 | img { | ||
66 | margin-right: 0; | ||
67 | } | ||
68 | } | 55 | } |
69 | } | ||
70 | 56 | ||
71 | @media screen and (max-width: $mobile-view) { | 57 | .video-channel-info { |
72 | .video-subscriptions-header { | 58 | padding-bottom: 10px; |
73 | flex-direction: column; | 59 | text-align: center; |
74 | 60 | ||
75 | input[type=text] { | 61 | .video-channel-names { |
76 | width: 100% !important; | 62 | flex-direction: column; |
63 | align-items: center !important; | ||
64 | margin: auto; | ||
77 | } | 65 | } |
78 | } | 66 | } |
79 | } | ||
80 | |||
81 | 67 | ||
68 | img { | ||
69 | margin-right: 0; | ||
70 | } | ||
71 | } | ||
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html index a97b2b4fb..e7e3c17b3 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html | |||
@@ -1,6 +1,6 @@ | |||
1 | <div class="row"> | 1 | <div class="root"> |
2 | 2 | ||
3 | <div class="playlist-info col-xs-12 col-md-5 col-xl-3"> | 3 | <div class="playlist-info"> |
4 | <my-video-playlist-miniature | 4 | <my-video-playlist-miniature |
5 | *ngIf="playlist" [playlist]="playlist" [toManage]="false" [displayChannel]="true" | 5 | *ngIf="playlist" [playlist]="playlist" [toManage]="false" [displayChannel]="true" |
6 | [displayDescription]="true" [displayPrivacy]="true" | 6 | [displayDescription]="true" [displayPrivacy]="true" |
@@ -20,7 +20,7 @@ | |||
20 | 20 | ||
21 | </div> | 21 | </div> |
22 | 22 | ||
23 | <div class="playlist-elements col-xs-12 col-md-7 col-xl-9"> | 23 | <div class="playlist-elements"> |
24 | <div class="no-results" *ngIf="pagination.totalItems === 0"> | 24 | <div class="no-results" *ngIf="pagination.totalItems === 0"> |
25 | <div i18n>No videos in this playlist.</div> | 25 | <div i18n>No videos in this playlist.</div> |
26 | 26 | ||
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss index de7e1993f..0c68dedf6 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss | |||
@@ -2,21 +2,25 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | @import '_miniature'; | 3 | @import '_miniature'; |
4 | 4 | ||
5 | .root { | ||
6 | display: grid; | ||
7 | grid-template-columns: auto 1fr; | ||
8 | } | ||
9 | |||
5 | .playlist-info { | 10 | .playlist-info { |
6 | background-color: pvar(--submenuColor); | 11 | grid-column: 1; |
7 | margin-left: -$not-expanded-horizontal-margins; | 12 | background-color: pvar(--submenuBackgroundColor); |
13 | margin-left: calc(#{pvar(--horizontalMarginContent)} * -1); | ||
8 | margin-top: -$sub-menu-margin-bottom; | 14 | margin-top: -$sub-menu-margin-bottom; |
9 | 15 | ||
10 | padding: 10px; | 16 | padding: 15px; |
11 | 17 | ||
12 | display: flex; | 18 | display: flex; |
13 | flex-direction: column; | 19 | flex-direction: column; |
14 | justify-content: flex-start; | ||
15 | align-items: center; | ||
16 | 20 | ||
17 | /* fix ellipsis dots background color */ | 21 | /* fix ellipsis dots background color */ |
18 | ::ng-deep .miniature-name::after { | 22 | ::ng-deep .miniature-name::after { |
19 | background-color: pvar(--submenuColor) !important; | 23 | background-color: pvar(--submenuBackgroundColor) !important; |
20 | } | 24 | } |
21 | } | 25 | } |
22 | 26 | ||
@@ -59,15 +63,35 @@ | |||
59 | transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); | 63 | transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); |
60 | } | 64 | } |
61 | 65 | ||
62 | @media screen and (max-width: $small-view) { | 66 | .playlist-elements { |
67 | grid-column: 2; | ||
68 | } | ||
69 | |||
70 | my-video-playlist-miniature { | ||
71 | width: $video-thumbnail-width; | ||
72 | } | ||
73 | |||
74 | @include on-small-main-col { | ||
75 | my-video-playlist-miniature { | ||
76 | width: $video-thumbnail-medium-width; | ||
77 | } | ||
78 | } | ||
79 | |||
80 | @include on-mobile-main-col { | ||
81 | .root { | ||
82 | display: block; | ||
83 | } | ||
84 | |||
63 | .playlist-info { | 85 | .playlist-info { |
64 | width: 100vw; | 86 | width: calc(100% + (2 * var(--horizontalMarginContent))); |
65 | padding-top: 20px; | 87 | padding-top: 20px; |
66 | margin-left: calc(#{var(--expanded-horizontal-margin-content)} * -1); | 88 | margin-bottom: 10px; |
67 | } | 89 | } |
68 | 90 | ||
69 | .playlist-elements { | 91 | my-video-playlist-miniature, |
70 | padding: 0 !important; | 92 | .playlist-buttons { |
93 | margin-left: auto; | ||
94 | margin-right: auto; | ||
71 | } | 95 | } |
72 | 96 | ||
73 | ::ng-deep my-video-playlist-element-miniature { | 97 | ::ng-deep my-video-playlist-element-miniature { |
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html index afcf6a084..b88ea3db7 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html | |||
@@ -1,8 +1,6 @@ | |||
1 | <h1> | 1 | <h1> |
2 | <span> | 2 | <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon> |
3 | <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon> | 3 | <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span> |
4 | <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span> | ||
5 | </span> | ||
6 | </h1> | 4 | </h1> |
7 | 5 | ||
8 | <div class="video-playlists-header d-flex justify-content-between"> | 6 | <div class="video-playlists-header d-flex justify-content-between"> |
@@ -21,10 +19,10 @@ | |||
21 | 19 | ||
22 | <div class="video-playlists" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> | 20 | <div class="video-playlists" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> |
23 | <div *ngFor="let playlist of videoPlaylists" class="video-playlist"> | 21 | <div *ngFor="let playlist of videoPlaylists" class="video-playlist"> |
24 | <div class="miniature-wrapper"> | 22 | <my-video-playlist-miniature |
25 | <my-video-playlist-miniature [playlist]="playlist" [toManage]="true" [displayChannel]="true" [displayDescription]="true" [displayPrivacy]="true" | 23 | [playlist]="playlist" [toManage]="true" [displayChannel]="true" |
26 | ></my-video-playlist-miniature> | 24 | [displayDescription]="true" [displayPrivacy]="true" [displayAsRow]="true" |
27 | </div> | 25 | ></my-video-playlist-miniature> |
28 | 26 | ||
29 | <div *ngIf="isRegularPlaylist(playlist)" class="video-playlist-buttons"> | 27 | <div *ngIf="isRegularPlaylist(playlist)" class="video-playlist-buttons"> |
30 | <my-delete-button label (click)="deleteVideoPlaylist(playlist)"></my-delete-button> | 28 | <my-delete-button label (click)="deleteVideoPlaylist(playlist)"></my-delete-button> |
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss index 2b7c88246..94187efd4 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss | |||
@@ -1,6 +1,10 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | h1 { | ||
5 | display: flex; | ||
6 | } | ||
7 | |||
4 | .create-button { | 8 | .create-button { |
5 | @include create-button; | 9 | @include create-button; |
6 | } | 10 | } |
@@ -9,64 +13,45 @@ input[type=text] { | |||
9 | @include peertube-input-text(300px); | 13 | @include peertube-input-text(300px); |
10 | } | 14 | } |
11 | 15 | ||
12 | ::ng-deep .action-button { | ||
13 | &.action-button-delete { | ||
14 | margin-right: 10px; | ||
15 | } | ||
16 | } | ||
17 | |||
18 | .video-playlist { | 16 | .video-playlist { |
19 | @include row-blocks; | 17 | @include row-blocks($column-responsive: false); |
20 | 18 | } | |
21 | .miniature-wrapper { | ||
22 | flex-grow: 1; | ||
23 | |||
24 | ::ng-deep .miniature { | ||
25 | display: flex; | ||
26 | |||
27 | .miniature-info { | ||
28 | margin-left: 10px; | ||
29 | width: auto; | ||
30 | } | ||
31 | } | ||
32 | } | ||
33 | 19 | ||
34 | .video-playlist-buttons { | 20 | .video-playlist-buttons { |
35 | min-width: 190px; | 21 | display: flex; |
36 | height: max-content; | 22 | margin-left: 10px; |
37 | } | 23 | align-self: flex-end; |
38 | } | 24 | } |
39 | 25 | ||
40 | .video-playlists-header { | 26 | .video-playlists-header { |
41 | margin-bottom: 30px; | 27 | margin-bottom: 30px; |
42 | } | 28 | } |
43 | 29 | ||
44 | @media screen and (max-width: $small-view) { | 30 | my-video-playlist-miniature { |
31 | display: block; | ||
32 | flex-grow: 1; | ||
33 | } | ||
34 | |||
35 | my-delete-button { | ||
36 | margin-right: 10px; | ||
37 | } | ||
38 | |||
39 | @include on-small-main-col { | ||
45 | .video-playlists-header { | 40 | .video-playlists-header { |
46 | text-align: center; | 41 | text-align: center; |
47 | } | 42 | } |
48 | 43 | ||
49 | .video-playlist { | 44 | .video-playlist { |
50 | 45 | flex-wrap: wrap; | |
51 | .video-playlist-buttons { | ||
52 | margin-top: 10px; | ||
53 | } | ||
54 | } | 46 | } |
55 | 47 | ||
56 | my-video-playlist-miniature ::ng-deep .miniature { | 48 | .video-playlist-buttons { |
57 | flex-direction: column; | 49 | margin-top: 10px; |
58 | 50 | margin-left: auto; | |
59 | .miniature-info { | ||
60 | margin-left: 0 !important; | ||
61 | } | ||
62 | |||
63 | .miniature-name { | ||
64 | max-width: $video-thumbnail-width; | ||
65 | } | ||
66 | } | 51 | } |
67 | } | 52 | } |
68 | 53 | ||
69 | @media screen and (max-width: $mobile-view) { | 54 | @include on-mobile-main-col { |
70 | .video-playlists-header { | 55 | .video-playlists-header { |
71 | flex-direction: column; | 56 | flex-direction: column; |
72 | 57 | ||
@@ -75,4 +60,8 @@ input[type=text] { | |||
75 | margin-bottom: 12px; | 60 | margin-bottom: 12px; |
76 | } | 61 | } |
77 | } | 62 | } |
63 | |||
64 | .action-button { | ||
65 | margin-left: 0; | ||
66 | } | ||
78 | } | 67 | } |
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.html b/client/src/app/+my-library/my-videos/my-videos.component.html index 5fa4c02ec..e9f436378 100644 --- a/client/src/app/+my-library/my-videos/my-videos.component.html +++ b/client/src/app/+my-library/my-videos/my-videos.component.html | |||
@@ -25,6 +25,17 @@ | |||
25 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> | 25 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> |
26 | <span class="sr-only" i18n>Clear filters</span> | 26 | <span class="sr-only" i18n>Clear filters</span> |
27 | </div> | 27 | </div> |
28 | |||
29 | <div class="peertube-select-container peertube-select-button"> | ||
30 | <select [(ngModel)]="sort" (ngModelChange)="onChangeSortColumn()" class="form-control"> | ||
31 | <option value="undefined" disabled>Sort by</option> | ||
32 | <option value="-publishedAt" i18n>Last published first</option> | ||
33 | <option value="-createdAt" i18n>Last created first</option> | ||
34 | <option value="-views" i18n>Most viewed first</option> | ||
35 | <option value="-likes" i18n>Most liked first</option> | ||
36 | <option value="-duration" i18n>Longest first</option> | ||
37 | </select> | ||
38 | </div> | ||
28 | </div> | 39 | </div> |
29 | 40 | ||
30 | <my-videos-selection | 41 | <my-videos-selection |
@@ -34,7 +45,6 @@ | |||
34 | [miniatureDisplayOptions]="miniatureDisplayOptions" | 45 | [miniatureDisplayOptions]="miniatureDisplayOptions" |
35 | [titlePage]="titlePage" | 46 | [titlePage]="titlePage" |
36 | [getVideosObservableFunction]="getVideosObservableFunction" | 47 | [getVideosObservableFunction]="getVideosObservableFunction" |
37 | [ownerDisplayType]="ownerDisplayType" | ||
38 | [user]="user" | 48 | [user]="user" |
39 | #videosSelection | 49 | #videosSelection |
40 | > | 50 | > |
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.scss b/client/src/app/+my-library/my-videos/my-videos.component.scss index 59fc5fe80..aaf21126b 100644 --- a/client/src/app/+my-library/my-videos/my-videos.component.scss +++ b/client/src/app/+my-library/my-videos/my-videos.component.scss | |||
@@ -5,6 +5,11 @@ input[type=text] { | |||
5 | @include peertube-input-text(300px); | 5 | @include peertube-input-text(300px); |
6 | } | 6 | } |
7 | 7 | ||
8 | .peertube-select-container { | ||
9 | @include peertube-select-container(auto); | ||
10 | margin-left: 0.5rem; | ||
11 | } | ||
12 | |||
8 | h1 { | 13 | h1 { |
9 | display: flex; | 14 | display: flex; |
10 | justify-content: space-between; | 15 | justify-content: space-between; |
@@ -32,36 +37,9 @@ h1 { | |||
32 | } | 37 | } |
33 | } | 38 | } |
34 | 39 | ||
35 | ::ng-deep { | ||
36 | .video { | ||
37 | flex-wrap: wrap; | ||
38 | } | ||
39 | |||
40 | .action-button span { | ||
41 | white-space: nowrap; | ||
42 | } | ||
43 | |||
44 | .video-miniature { | ||
45 | &.display-as-row { | ||
46 | // width: min-content !important; | ||
47 | width: 100% !important; | ||
48 | |||
49 | .video-bottom .video-miniature-information { | ||
50 | width: max-content !important; | ||
51 | min-width: unset !important; | ||
52 | } | ||
53 | } | ||
54 | |||
55 | .video-bottom { | ||
56 | max-width: 350px; | ||
57 | } | ||
58 | } | ||
59 | } | ||
60 | |||
61 | .action-button { | 40 | .action-button { |
62 | display: flex; | 41 | display: flex; |
63 | margin-left: 55px; | 42 | margin-left: 10px; |
64 | margin-top: 10px; | ||
65 | align-self: flex-end; | 43 | align-self: flex-end; |
66 | } | 44 | } |
67 | 45 | ||
@@ -69,7 +47,7 @@ my-edit-button { | |||
69 | margin-right: 10px; | 47 | margin-right: 10px; |
70 | } | 48 | } |
71 | 49 | ||
72 | @media screen and (max-width: $small-view) { | 50 | @include on-small-main-col { |
73 | h1 { | 51 | h1 { |
74 | flex-direction: column; | 52 | flex-direction: column; |
75 | 53 | ||
@@ -80,59 +58,25 @@ my-edit-button { | |||
80 | } | 58 | } |
81 | 59 | ||
82 | .action-button { | 60 | .action-button { |
83 | flex-direction: column; | 61 | margin-top: 10px; |
84 | align-self: center; | 62 | margin-left: auto; |
85 | align-items: center; | ||
86 | margin-left: 0px; | ||
87 | } | ||
88 | |||
89 | my-edit-button { | ||
90 | margin: 15px 0 5px 0; | ||
91 | width: 100%; | ||
92 | text-align: center; | ||
93 | |||
94 | ::ng-deep { | ||
95 | .action-button { | ||
96 | /* same width than a.video-thumbnail */ | ||
97 | width: $video-thumbnail-width; | ||
98 | } | ||
99 | } | ||
100 | } | ||
101 | |||
102 | ::ng-deep { | ||
103 | .video-miniature { | ||
104 | align-items: center; | ||
105 | |||
106 | .video-bottom, | ||
107 | .video-bottom .video-miniature-information { | ||
108 | /* same width than a.video-thumbnail */ | ||
109 | max-width: $video-thumbnail-width !important; | ||
110 | } | ||
111 | } | ||
112 | } | 63 | } |
113 | } | 64 | } |
114 | 65 | ||
115 | // Adapt my-video-miniature on small screens with menu | 66 | @include on-mobile-main-col { |
116 | @media screen and (min-width: $small-view) and (max-width: #{breakpoint(lg) + ($not-expanded-horizontal-margins / 3) * 2}) { | ||
117 | :host-context(.main-col:not(.expanded)) { | ||
118 | ::ng-deep { | ||
119 | .video-miniature { | ||
120 | flex-direction: column; | ||
121 | |||
122 | .video-miniature-name { | ||
123 | max-width: $video-thumbnail-width; | ||
124 | } | ||
125 | } | ||
126 | } | ||
127 | } | ||
128 | } | ||
129 | |||
130 | @media screen and (max-width: $mobile-view) { | ||
131 | .videos-header { | 67 | .videos-header { |
132 | flex-direction: column; | 68 | flex-direction: column; |
133 | 69 | ||
134 | input[type=text] { | 70 | input[type=text] { |
135 | width: 100% !important; | 71 | width: 100%; |
72 | margin-bottom: 12px; | ||
73 | } | ||
74 | .peertube-select-container { | ||
75 | margin-left: 0; | ||
136 | } | 76 | } |
137 | } | 77 | } |
78 | |||
79 | .action-button { | ||
80 | margin-left: 0; | ||
81 | } | ||
138 | } | 82 | } |
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.ts b/client/src/app/+my-library/my-videos/my-videos.component.ts index 6a2a62608..356e158d6 100644 --- a/client/src/app/+my-library/my-videos/my-videos.component.ts +++ b/client/src/app/+my-library/my-videos/my-videos.component.ts | |||
@@ -2,12 +2,12 @@ import { concat, Observable, Subject } from 'rxjs' | |||
2 | import { debounceTime, tap, toArray } from 'rxjs/operators' | 2 | import { debounceTime, tap, toArray } from 'rxjs/operators' |
3 | import { Component, OnInit, ViewChild } from '@angular/core' | 3 | import { Component, OnInit, ViewChild } from '@angular/core' |
4 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User, UserService } from '@app/core' | 5 | import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' |
6 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' | 6 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' |
7 | import { immutableAssign } from '@app/helpers' | 7 | import { immutableAssign } from '@app/helpers' |
8 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 8 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
9 | import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' | 9 | import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' |
10 | import { MiniatureDisplayOptions, OwnerDisplayType, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' | 10 | import { MiniatureDisplayOptions, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' |
11 | import { VideoSortField } from '@shared/models' | 11 | import { VideoSortField } from '@shared/models' |
12 | import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component' | 12 | import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component' |
13 | 13 | ||
@@ -36,7 +36,6 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
36 | state: true, | 36 | state: true, |
37 | blacklistInfo: true | 37 | blacklistInfo: true |
38 | } | 38 | } |
39 | ownerDisplayType: OwnerDisplayType = 'videoChannel' | ||
40 | 39 | ||
41 | videoActions: DropdownAction<{ video: Video }>[] = [] | 40 | videoActions: DropdownAction<{ video: Video }>[] = [] |
42 | 41 | ||
@@ -44,6 +43,7 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
44 | videosSearch: string | 43 | videosSearch: string |
45 | videosSearchChanged = new Subject<string>() | 44 | videosSearchChanged = new Subject<string>() |
46 | getVideosObservableFunction = this.getVideosObservable.bind(this) | 45 | getVideosObservableFunction = this.getVideosObservable.bind(this) |
46 | sort: VideoSortField = '-publishedAt' | ||
47 | 47 | ||
48 | user: User | 48 | user: User |
49 | 49 | ||
@@ -81,6 +81,10 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
81 | this.videosSearchChanged.next() | 81 | this.videosSearchChanged.next() |
82 | } | 82 | } |
83 | 83 | ||
84 | onChangeSortColumn () { | ||
85 | this.videosSelection.reloadVideos() | ||
86 | } | ||
87 | |||
84 | disableForReuse () { | 88 | disableForReuse () { |
85 | this.videosSelection.disableForReuse() | 89 | this.videosSelection.disableForReuse() |
86 | } | 90 | } |
@@ -89,10 +93,10 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
89 | this.videosSelection.enabledForReuse() | 93 | this.videosSelection.enabledForReuse() |
90 | } | 94 | } |
91 | 95 | ||
92 | getVideosObservable (page: number, sort: VideoSortField) { | 96 | getVideosObservable (page: number) { |
93 | const newPagination = immutableAssign(this.pagination, { currentPage: page }) | 97 | const newPagination = immutableAssign(this.pagination, { currentPage: page }) |
94 | 98 | ||
95 | return this.videoService.getMyVideos(newPagination, sort, this.videosSearch) | 99 | return this.videoService.getMyVideos(newPagination, this.sort, this.videosSearch) |
96 | .pipe( | 100 | .pipe( |
97 | tap(res => this.pagination.totalItems = res.total) | 101 | tap(res => this.pagination.totalItems = res.total) |
98 | ) | 102 | ) |
diff --git a/client/src/app/+search/search.component.html b/client/src/app/+search/search.component.html index 84be4fb14..65d4b6ecd 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 |
@@ -35,11 +33,11 @@ | |||
35 | 33 | ||
36 | <ng-container *ngFor="let result of results"> | 34 | <ng-container *ngFor="let result of results"> |
37 | <div *ngIf="isVideoChannel(result)" class="entry video-channel"> | 35 | <div *ngIf="isVideoChannel(result)" class="entry video-channel"> |
38 | <a *ngIf="!isExternalChannelUrl()" [routerLink]="getChannelUrl(result)"> | 36 | <a class="link-avatar" *ngIf="!isExternalChannelUrl()" [routerLink]="getChannelUrl(result)"> |
39 | <img [src]="result.avatarUrl" alt="Avatar" /> | 37 | <img [src]="result.avatarUrl" alt="Avatar" /> |
40 | </a> | 38 | </a> |
41 | 39 | ||
42 | <a *ngIf="isExternalChannelUrl()" [href]="getChannelUrl(result)" target="_blank"> | 40 | <a class="link-avatar" *ngIf="isExternalChannelUrl()" [href]="getChannelUrl(result)" target="_blank"> |
43 | <img [src]="result.avatarUrl" alt="Avatar" /> | 41 | <img [src]="result.avatarUrl" alt="Avatar" /> |
44 | </a> | 42 | </a> |
45 | 43 | ||
diff --git a/client/src/app/+search/search.component.scss b/client/src/app/+search/search.component.scss index 64927fa4b..91c8272d7 100644 --- a/client/src/app/+search/search.component.scss +++ b/client/src/app/+search/search.component.scss | |||
@@ -1,159 +1,122 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | @mixin build-channel-img-size ($video-img-width) { | ||
5 | $image-size: min(130px, $video-img-width); | ||
6 | $margin-size: ($video-img-width - $image-size) / 2; // So we have the same width than the video miniature | ||
7 | |||
8 | @include channel-avatar($image-size); | ||
9 | |||
10 | margin: 0 $margin-size 0 $margin-size; | ||
11 | } | ||
12 | |||
4 | .search-result { | 13 | .search-result { |
5 | padding: 40px; | 14 | padding: 40px; |
15 | } | ||
6 | 16 | ||
7 | .results-header { | 17 | .results-header { |
8 | font-size: 16px; | 18 | font-size: 16px; |
9 | padding-bottom: 20px; | 19 | padding-bottom: 20px; |
10 | margin-bottom: 30px; | 20 | margin-bottom: 30px; |
11 | border-bottom: 1px solid #DADADA; | 21 | border-bottom: 1px solid #DADADA; |
12 | 22 | ||
13 | .first-line { | 23 | .first-line { |
14 | display: flex; | 24 | display: flex; |
15 | flex-direction: row; | 25 | flex-direction: row; |
16 | 26 | ||
17 | .results-counter { | 27 | .results-counter { |
18 | flex-grow: 1; | 28 | flex-grow: 1; |
19 | 29 | ||
20 | .search-value { | 30 | .search-value { |
21 | font-weight: $font-semibold; | 31 | font-weight: $font-semibold; |
22 | } | ||
23 | } | 32 | } |
33 | } | ||
24 | 34 | ||
25 | .results-filter-button { | 35 | .results-filter-button { |
26 | cursor: pointer; | 36 | cursor: pointer; |
27 | 37 | ||
28 | .icon.icon-filter { | 38 | .icon.icon-filter { |
29 | @include icon(20px); | 39 | @include icon(20px); |
30 | 40 | ||
31 | position: relative; | 41 | position: relative; |
32 | top: -1px; | 42 | top: -1px; |
33 | margin-right: 5px; | 43 | margin-right: 5px; |
34 | background-image: url('../../assets/images/feather/filter.svg'); | 44 | background-image: url('../../assets/images/feather/filter.svg'); |
35 | } | ||
36 | } | 45 | } |
37 | } | 46 | } |
38 | } | 47 | } |
48 | } | ||
39 | 49 | ||
40 | .entry { | 50 | .entry { |
41 | display: flex; | 51 | display: flex; |
42 | min-height: 130px; | 52 | margin-bottom: 40px; |
43 | padding-bottom: 20px; | 53 | max-width: 800px; |
44 | margin-bottom: 20px; | 54 | } |
45 | 55 | ||
46 | &.video-channel { | 56 | .video-channel { |
47 | img { | 57 | img { |
48 | $image-size: 130px; | 58 | @include build-channel-img-size($video-thumbnail-width); |
49 | $margin-size: ($video-thumbnail-width - $image-size) / 2; // So we have the same width than the video miniature | 59 | } |
60 | } | ||
50 | 61 | ||
51 | @include avatar($image-size); | 62 | .video-channel-info { |
63 | flex-grow: 1; | ||
64 | margin: 0 10px; | ||
65 | width: fit-content; | ||
66 | } | ||
52 | 67 | ||
53 | margin: 0 ($margin-size + 10) 0 $margin-size; | 68 | .video-channel-names { |
54 | } | 69 | @include disable-default-a-behaviour; |
55 | 70 | ||
56 | .video-channel-info { | 71 | display: flex; |
57 | flex-grow: 1; | 72 | align-items: baseline; |
58 | width: fit-content; | 73 | color: pvar(--mainForegroundColor); |
59 | 74 | width: fit-content; | |
60 | .video-channel-names { | ||
61 | @include disable-default-a-behaviour; | ||
62 | |||
63 | display: flex; | ||
64 | align-items: baseline; | ||
65 | color: pvar(--mainForegroundColor); | ||
66 | width: fit-content; | ||
67 | |||
68 | .video-channel-display-name { | ||
69 | font-weight: $font-semibold; | ||
70 | font-size: 18px; | ||
71 | } | ||
72 | |||
73 | .video-channel-name { | ||
74 | font-size: 14px; | ||
75 | color: $grey-actor-name; | ||
76 | margin-left: 5px; | ||
77 | } | ||
78 | } | ||
79 | } | ||
80 | } | ||
81 | } | ||
82 | } | 75 | } |
83 | 76 | ||
84 | @media screen and (min-width: $small-view) and (max-width: breakpoint(xl)) { | 77 | .video-channel-display-name { |
85 | .video-channel-info .video-channel-names { | 78 | font-weight: $font-semibold; |
86 | flex-direction: column !important; | 79 | font-size: $video-miniature-row-name-font-size; |
80 | } | ||
87 | 81 | ||
88 | .video-channel-name { | 82 | .video-channel-name { |
89 | @include ellipsis; // Ellipsis and max-width on channel-name to not break screen | 83 | font-size: $video-miniature-row-info-font-size; |
84 | color: pvar(--greyForegroundColor); | ||
85 | margin-left: 5px; | ||
86 | } | ||
90 | 87 | ||
91 | max-width: 250px; | 88 | // Use the same breakpoints than in video-miniature |
92 | margin-left: 0 !important; | 89 | @include on-small-main-col { |
93 | } | 90 | .video-channel { |
94 | } | 91 | display: grid; |
92 | grid-template-columns: auto 1fr; | ||
93 | grid-template-rows: auto auto; | ||
95 | 94 | ||
96 | :host-context(.main-col:not(.expanded)) { | 95 | .link-avatar { |
97 | // Override the min-width: 500px to not break screen | 96 | grid-column: 1; |
98 | ::ng-deep .video-miniature-information { | 97 | grid-row: 1 / -1; |
99 | min-width: 300px !important; | ||
100 | } | 98 | } |
101 | } | ||
102 | } | ||
103 | 99 | ||
104 | @media screen and (min-width: $small-view) and (max-width: breakpoint(lg)) { | 100 | img { |
105 | :host-context(.main-col:not(.expanded)) { | 101 | @include build-channel-img-size($video-thumbnail-medium-width); |
106 | .video-channel-info .video-channel-names { | ||
107 | .video-channel-name { | ||
108 | max-width: 160px; | ||
109 | } | ||
110 | } | 102 | } |
103 | } | ||
111 | 104 | ||
112 | // Override the min-width: 500px to not break screen | 105 | .video-channel-info { |
113 | ::ng-deep .video-miniature-information { | 106 | grid-column: 2; |
114 | min-width: $video-thumbnail-width !important; | 107 | grid-row: 1; |
115 | } | ||
116 | } | 108 | } |
117 | 109 | ||
118 | :host-context(.expanded) { | 110 | my-subscribe-button { |
119 | // Override the min-width: 500px to not break screen | 111 | grid-column: 2; |
120 | ::ng-deep .video-miniature-information { | 112 | grid-row: 2; |
121 | min-width: 300px !important; | 113 | align-self: end; |
122 | } | ||
123 | } | 114 | } |
124 | } | 115 | } |
125 | 116 | ||
126 | @media screen and (max-width: $small-view) { | 117 | @include on-mobile-main-col { |
127 | .search-result { | 118 | .video-channel img { |
128 | .entry.video-channel, | 119 | @include build-channel-img-size($video-thumbnail-small-width); |
129 | .entry.video { | ||
130 | flex-direction: column; | ||
131 | height: auto; | ||
132 | justify-content: center; | ||
133 | align-items: center; | ||
134 | text-align: center; | ||
135 | |||
136 | img { | ||
137 | margin: 0; | ||
138 | } | ||
139 | |||
140 | img { | ||
141 | margin: 0; | ||
142 | } | ||
143 | |||
144 | .video-channel-info .video-channel-names { | ||
145 | align-items: center; | ||
146 | flex-direction: column !important; | ||
147 | |||
148 | .video-channel-name { | ||
149 | margin-left: 0 !important; | ||
150 | } | ||
151 | } | ||
152 | |||
153 | my-subscribe-button { | ||
154 | margin-top: 5px; | ||
155 | } | ||
156 | } | ||
157 | } | 120 | } |
158 | } | 121 | } |
159 | 122 | ||
@@ -164,28 +127,13 @@ | |||
164 | .results-header { | 127 | .results-header { |
165 | font-size: 15px !important; | 128 | font-size: 15px !important; |
166 | } | 129 | } |
130 | } | ||
167 | 131 | ||
168 | .entry { | 132 | .video-channel-display-name { |
169 | &.video { | 133 | font-size: $video-miniature-row-mobile-name-font-size; |
170 | .video-info-name, | 134 | } |
171 | .video-info-account { | 135 | |
172 | margin: auto; | 136 | .video-channel-name { |
173 | } | 137 | font-size: $video-miniature-row-mobile-info-font-size; |
174 | |||
175 | my-video-thumbnail { | ||
176 | margin-right: 0 !important; | ||
177 | |||
178 | ::ng-deep .video-thumbnail { | ||
179 | width: 100%; | ||
180 | height: auto; | ||
181 | |||
182 | img { | ||
183 | width: 100%; | ||
184 | height: auto; | ||
185 | } | ||
186 | } | ||
187 | } | ||
188 | } | ||
189 | } | ||
190 | } | 138 | } |
191 | } | 139 | } |
diff --git a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.html b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.html deleted file mode 100644 index 8dff8ba91..000000000 --- a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.html +++ /dev/null | |||
@@ -1,22 +0,0 @@ | |||
1 | <div class="margin-content"> | ||
2 | <div *ngIf="videoChannel" class="row no-gutters"> | ||
3 | <div class="description col-md-6 col-sm-12 pr-2"> | ||
4 | <div class="block"> | ||
5 | <div i18n class="small-title">DESCRIPTION</div> | ||
6 | <div class="content" [innerHtml]="getVideoChannelDescription()"></div> | ||
7 | </div> | ||
8 | |||
9 | <div class="block" *ngIf="supportHTML"> | ||
10 | <div i18n class="small-title">SUPPORT THIS CHANNEL</div> | ||
11 | <div class="content" [innerHtml]="supportHTML"></div> | ||
12 | </div> | ||
13 | </div> | ||
14 | |||
15 | <div class="stats col-md-6 col-sm-12"> | ||
16 | <div class="block"> | ||
17 | <div i18n class="small-title">STATS</div> | ||
18 | <div i18n class="content">Created {{ videoChannel.createdAt | date }}</div> | ||
19 | </div> | ||
20 | </div> | ||
21 | </div> | ||
22 | </div> | ||
diff --git a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.scss b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.scss deleted file mode 100644 index 5bcd4b561..000000000 --- a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.scss +++ /dev/null | |||
@@ -1,12 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .block { | ||
5 | margin-bottom: 40px; | ||
6 | |||
7 | .small-title { | ||
8 | @include in-content-small-title; | ||
9 | |||
10 | margin-bottom: 20px; | ||
11 | } | ||
12 | } | ||
diff --git a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts deleted file mode 100644 index 537c7d08e..000000000 --- a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts +++ /dev/null | |||
@@ -1,43 +0,0 @@ | |||
1 | import { Subscription } from 'rxjs' | ||
2 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
3 | import { MarkdownService } from '@app/core' | ||
4 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' | ||
5 | |||
6 | @Component({ | ||
7 | selector: 'my-video-channel-about', | ||
8 | templateUrl: './video-channel-about.component.html', | ||
9 | styleUrls: [ './video-channel-about.component.scss' ] | ||
10 | }) | ||
11 | export class VideoChannelAboutComponent implements OnInit, OnDestroy { | ||
12 | videoChannel: VideoChannel | ||
13 | descriptionHTML = '' | ||
14 | supportHTML = '' | ||
15 | |||
16 | private videoChannelSub: Subscription | ||
17 | |||
18 | constructor ( | ||
19 | private videoChannelService: VideoChannelService, | ||
20 | private markdownService: MarkdownService | ||
21 | ) { } | ||
22 | |||
23 | ngOnInit () { | ||
24 | // Parent get the video channel for us | ||
25 | this.videoChannelSub = this.videoChannelService.videoChannelLoaded | ||
26 | .subscribe(async videoChannel => { | ||
27 | this.videoChannel = videoChannel | ||
28 | |||
29 | this.descriptionHTML = await this.markdownService.textMarkdownToHTML(this.videoChannel.description) | ||
30 | this.supportHTML = await this.markdownService.enhancedMarkdownToHTML(this.videoChannel.support) | ||
31 | }) | ||
32 | } | ||
33 | |||
34 | ngOnDestroy () { | ||
35 | if (this.videoChannelSub) this.videoChannelSub.unsubscribe() | ||
36 | } | ||
37 | |||
38 | getVideoChannelDescription () { | ||
39 | if (this.descriptionHTML) return this.descriptionHTML | ||
40 | |||
41 | return $localize`No description` | ||
42 | } | ||
43 | } | ||
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..b69d1682a 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,13 +1,13 @@ | |||
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" *ngIf="pagination.totalItems"> |
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> |
7 | 7 | ||
8 | <div class="video-playlist" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()"> | 8 | <div class="playlists" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()"> |
9 | <div *ngFor="let playlist of videoPlaylists" class="playlist-miniature-container"> | 9 | <div *ngFor="let playlist of videoPlaylists" class="playlist-wrapper"> |
10 | <my-video-playlist-miniature [playlist]="playlist" [toManage]="false"></my-video-playlist-miniature> | 10 | <my-video-playlist-miniature [playlist]="playlist" [toManage]="false" [displayAsRow]="displayAsRow()"></my-video-playlist-miniature> |
11 | </div> | 11 | </div> |
12 | </div> | 12 | </div> |
13 | </div> | 13 | </div> |
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss index cb2931858..acd2e409e 100644 --- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss +++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss | |||
@@ -1,14 +1,32 @@ | |||
1 | .title-page { | 1 | @import '_variables'; |
2 | margin-top: 0; | 2 | @import '_mixins'; |
3 | } | 3 | @import '_miniature'; |
4 | 4 | ||
5 | .video-playlist { | 5 | .playlists { |
6 | display: flex; | 6 | display: flex; |
7 | flex-wrap: wrap; | 7 | flex-wrap: wrap; |
8 | justify-content: center; | 8 | justify-content: center; |
9 | 9 | ||
10 | .playlist-miniature-container { | 10 | .playlist-wrapper { |
11 | margin-right: 15px; | 11 | margin-right: 15px; |
12 | margin-bottom: 30px; | 12 | margin-bottom: 30px; |
13 | } | 13 | } |
14 | } | 14 | } |
15 | |||
16 | .margin-content { | ||
17 | @include grid-videos-miniature-layout; | ||
18 | } | ||
19 | |||
20 | @media screen and (max-width: $mobile-view) { | ||
21 | .title-page { | ||
22 | display: block; | ||
23 | text-align: center; | ||
24 | } | ||
25 | |||
26 | .playlists { | ||
27 | justify-content: left; | ||
28 | |||
29 | margin-left: pvar(--horizontalMarginContent) !important; | ||
30 | margin-right: pvar(--horizontalMarginContent) !important; | ||
31 | } | ||
32 | } | ||
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts index 8b507c626..14465bb8d 100644 --- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts +++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Subject, Subscription } from 'rxjs' | 1 | import { Subject, Subscription } from 'rxjs' |
2 | import { Component, OnDestroy, OnInit } from '@angular/core' | 2 | import { Component, OnDestroy, OnInit } from '@angular/core' |
3 | import { ComponentPagination, hasMoreItems } from '@app/core' | 3 | import { ComponentPagination, hasMoreItems, ScreenService } from '@app/core' |
4 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 4 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' |
5 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' | 5 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' |
6 | 6 | ||
@@ -25,7 +25,8 @@ export class VideoChannelPlaylistsComponent implements OnInit, OnDestroy { | |||
25 | 25 | ||
26 | constructor ( | 26 | constructor ( |
27 | private videoPlaylistService: VideoPlaylistService, | 27 | private videoPlaylistService: VideoPlaylistService, |
28 | private videoChannelService: VideoChannelService | 28 | private videoChannelService: VideoChannelService, |
29 | private screenService: ScreenService | ||
29 | ) {} | 30 | ) {} |
30 | 31 | ||
31 | ngOnInit () { | 32 | ngOnInit () { |
@@ -48,6 +49,10 @@ export class VideoChannelPlaylistsComponent implements OnInit, OnDestroy { | |||
48 | this.loadVideoPlaylists() | 49 | this.loadVideoPlaylists() |
49 | } | 50 | } |
50 | 51 | ||
52 | displayAsRow () { | ||
53 | return this.screenService.isInMobileView() | ||
54 | } | ||
55 | |||
51 | private loadVideoPlaylists () { | 56 | private loadVideoPlaylists () { |
52 | this.videoPlaylistService.listChannelPlaylists(this.videoChannel, this.pagination) | 57 | this.videoPlaylistService.listChannelPlaylists(this.videoChannel, this.pagination) |
53 | .subscribe(res => { | 58 | .subscribe(res => { |
diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts index 803651505..d83fc1324 100644 --- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts +++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts | |||
@@ -5,7 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router' | |||
5 | import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core' | 5 | import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core' |
6 | import { immutableAssign } from '@app/helpers' | 6 | import { immutableAssign } from '@app/helpers' |
7 | import { VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' | 7 | import { VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' |
8 | import { AbstractVideoList } from '@app/shared/shared-video-miniature' | 8 | import { AbstractVideoList, MiniatureDisplayOptions } from '@app/shared/shared-video-miniature' |
9 | import { VideoFilter } from '@shared/models' | 9 | import { VideoFilter } from '@shared/models' |
10 | 10 | ||
11 | @Component({ | 11 | @Component({ |
@@ -16,12 +16,24 @@ import { VideoFilter } from '@shared/models' | |||
16 | ] | 16 | ] |
17 | }) | 17 | }) |
18 | export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { | 18 | export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { |
19 | // No value because we don't want a page title | ||
19 | titlePage: string | 20 | titlePage: string |
20 | loadOnInit = false | 21 | loadOnInit = false |
21 | loadUserVideoPreferences = true | 22 | loadUserVideoPreferences = true |
22 | 23 | ||
23 | filter: VideoFilter = null | 24 | filter: VideoFilter = null |
24 | 25 | ||
26 | displayOptions: MiniatureDisplayOptions = { | ||
27 | date: true, | ||
28 | views: true, | ||
29 | by: false, | ||
30 | avatar: false, | ||
31 | privacyLabel: true, | ||
32 | privacyText: false, | ||
33 | state: false, | ||
34 | blacklistInfo: false | ||
35 | } | ||
36 | |||
25 | private videoChannel: VideoChannel | 37 | private videoChannel: VideoChannel |
26 | private videoChannelSub: Subscription | 38 | private videoChannelSub: Subscription |
27 | 39 | ||
@@ -83,13 +95,6 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On | |||
83 | 95 | ||
84 | return this.videoService | 96 | return this.videoService |
85 | .getVideoChannelVideos(options) | 97 | .getVideoChannelVideos(options) |
86 | .pipe( | ||
87 | tap(({ total }) => { | ||
88 | this.titlePage = total === 1 | ||
89 | ? $localize`Published 1 video` | ||
90 | : $localize`Published ${total} videos` | ||
91 | }) | ||
92 | ) | ||
93 | } | 98 | } |
94 | 99 | ||
95 | generateSyndicationList () { | 100 | generateSyndicationList () { |
@@ -101,4 +106,8 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On | |||
101 | 106 | ||
102 | this.reloadVideos() | 107 | this.reloadVideos() |
103 | } | 108 | } |
109 | |||
110 | displayAsRow () { | ||
111 | return this.screenService.isInMobileView() | ||
112 | } | ||
104 | } | 113 | } |
diff --git a/client/src/app/+video-channels/video-channels-routing.module.ts b/client/src/app/+video-channels/video-channels-routing.module.ts index f8c32f14e..fcaad8934 100644 --- a/client/src/app/+video-channels/video-channels-routing.module.ts +++ b/client/src/app/+video-channels/video-channels-routing.module.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | 3 | import { MetaGuard } from '@ngx-meta/core' |
4 | import { VideoChannelAboutComponent } from './video-channel-about/video-channel-about.component' | ||
5 | import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' | 4 | import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' |
6 | import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' | 5 | import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' |
7 | import { VideoChannelsComponent } from './video-channels.component' | 6 | import { VideoChannelsComponent } from './video-channels.component' |
@@ -38,15 +37,6 @@ const videoChannelsRoutes: Routes = [ | |||
38 | title: $localize`Video channel playlists` | 37 | title: $localize`Video channel playlists` |
39 | } | 38 | } |
40 | } | 39 | } |
41 | }, | ||
42 | { | ||
43 | path: 'about', | ||
44 | component: VideoChannelAboutComponent, | ||
45 | data: { | ||
46 | meta: { | ||
47 | title: $localize`About video channel` | ||
48 | } | ||
49 | } | ||
50 | } | 40 | } |
51 | ] | 41 | ] |
52 | } | 42 | } |
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html index 4b0d12b6e..1312a1b3c 100644 --- a/client/src/app/+video-channels/video-channels.component.html +++ b/client/src/app/+video-channels/video-channels.component.html | |||
@@ -1,50 +1,129 @@ | |||
1 | <div *ngIf="videoChannel" class="row"> | 1 | <div class="root" *ngIf="videoChannel"> |
2 | <div class="sub-menu"> | 2 | <div class="banner" *ngIf="videoChannel.bannerUrl"> |
3 | 3 | <img [src]="videoChannel.bannerUrl" alt="Channel banner"> | |
4 | <div class="actor"> | 4 | </div> |
5 | <img [src]="videoChannel.avatarUrl" alt="Avatar" /> | 5 | |
6 | 6 | <div class="channel-info"> | |
7 | <div class="actor-info"> | 7 | |
8 | <div class="actor-names"> | 8 | <ng-template #buttonsTemplate> |
9 | <div class="actor-display-name">{{ videoChannel.displayName }}</div> | 9 | <a *ngIf="isManageable()" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="peertube-button-link orange-button" i18n> |
10 | <div class="actor-name"> | 10 | Manage channel |
11 | <span>{{ videoChannel.nameWithHost }}</span> | 11 | </a> |
12 | <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()" | 12 | |
13 | class="btn btn-outline-secondary btn-sm copy-button" | 13 | <my-subscribe-button *ngIf="!isManageable()" #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button> |
14 | > | 14 | |
15 | <span class="glyphicon glyphicon-copy"></span> | 15 | <button *ngIf="videoChannel.support" (click)="showSupportModal()" class="support-button peertube-button orange-button-inverted"> |
16 | </button> | 16 | <my-global-icon iconName="support" aria-hidden="true"></my-global-icon> |
17 | <span class="icon-text" i18n>Support</span> | ||
18 | </button> | ||
19 | </ng-template> | ||
20 | |||
21 | <ng-template #ownerTemplate> | ||
22 | <div class="owner-block"> | ||
23 | <div class="avatar-row"> | ||
24 | <a [routerLink]="getAccountUrl()" title="View account" i18n-title> | ||
25 | <img class="account-avatar" [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> | ||
26 | </a> | ||
27 | |||
28 | <div class="actor-info"> | ||
29 | <h4> | ||
30 | <a [routerLink]="getAccountUrl()" title="View account" i18n-title>{{ videoChannel.ownerAccount.displayName }}</a> | ||
31 | </h4> | ||
32 | |||
33 | <div class="actor-handle">@{{ videoChannel.ownerBy }}</div> | ||
17 | </div> | 34 | </div> |
18 | </div> | 35 | </div> |
19 | 36 | ||
20 | <div class="right-buttons"> | 37 | <div class="owner-description"> |
21 | <a *ngIf="isChannelManageable && !isInSmallView" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n> | 38 | <div class="description-html" [innerHTML]="ownerDescriptionHTML"></div> |
22 | Manage channel | ||
23 | </a> | ||
24 | <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button> | ||
25 | </div> | 39 | </div> |
26 | 40 | ||
27 | <div class="actor-lower"> | 41 | <a class="view-account short" [routerLink]="getAccountUrl()" i18n> |
28 | <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> | 42 | View account |
43 | </a> | ||
29 | 44 | ||
30 | <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner"> | 45 | <a class="view-account complete" [routerLink]="getAccountUrl()" i18n> |
31 | <span class="d-inline-flex"><span i18n class="d-none d-sm-block mr-1">Created by</span>{{ videoChannel.ownerBy }}</span> | 46 | View owner account |
32 | <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> | 47 | </a> |
33 | </a> | 48 | </div> |
49 | </ng-template> | ||
50 | |||
51 | <div class="channel-avatar-row"> | ||
52 | <img class="channel-avatar" [src]="videoChannel.avatarUrl" alt="Avatar" /> | ||
53 | |||
54 | <div> | ||
55 | <div class="section-label" i18n>VIDEO CHANNEL</div> | ||
56 | |||
57 | <div class="actor-info"> | ||
58 | <div> | ||
59 | <div class="actor-display-name"> | ||
60 | <h1>{{ videoChannel.displayName }}</h1> | ||
61 | </div> | ||
62 | |||
63 | <div class="actor-handle"> | ||
64 | <span>@{{ videoChannel.nameWithHost }}</span> | ||
65 | <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()" | ||
66 | class="btn btn-outline-secondary btn-sm copy-button" title="Copy channel handle" i18n-title | ||
67 | > | ||
68 | <span class="glyphicon glyphicon-duplicate"></span> | ||
69 | </button> | ||
70 | </div> | ||
71 | |||
72 | <div class="actor-counters"> | ||
73 | <span i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</span> | ||
74 | |||
75 | <span class="videos-count" *ngIf="channelVideosCount !== undefined" i18n> | ||
76 | {channelVideosCount, plural, =1 {1 videos} other {{{ channelVideosCount }} videos}} | ||
77 | </span> | ||
78 | </div> | ||
79 | </div> | ||
80 | |||
81 | <div class="channel-buttons right"> | ||
82 | <ng-template *ngTemplateOutlet="buttonsTemplate"></ng-template> | ||
83 | </div> | ||
34 | </div> | 84 | </div> |
35 | </div> | 85 | </div> |
36 | </div> | 86 | </div> |
37 | 87 | ||
38 | <div class="links w-100"> | 88 | <div class="channel-description" [ngClass]="{ expanded: channelDescriptionExpanded }"> |
39 | <ng-template #linkTemplate let-item="item"> | 89 | <div class="description-html" [innerHTML]="channelDescriptionHTML"></div> |
40 | <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> | ||
41 | </ng-template> | ||
42 | 90 | ||
43 | <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> | 91 | <div class="created-at" i18n>Channel created on {{ videoChannel.createdAt | date }}</div> |
44 | </div> | 92 | </div> |
93 | |||
94 | <div *ngIf="hasShowMoreDescription()" class="show-more" role="button" | ||
95 | (click)="channelDescriptionExpanded = !channelDescriptionExpanded" | ||
96 | title="Show the complete description" i18n-title i18n | ||
97 | > | ||
98 | Show more... | ||
99 | </div> | ||
100 | |||
101 | <div class="channel-buttons bottom"> | ||
102 | <ng-template *ngTemplateOutlet="buttonsTemplate"></ng-template> | ||
103 | </div> | ||
104 | |||
105 | <div class="owner-card"> | ||
106 | <div class="section-label" i18n>OWNER ACCOUNT</div> | ||
107 | |||
108 | <ng-template *ngTemplateOutlet="ownerTemplate"></ng-template> | ||
109 | </div> | ||
110 | </div> | ||
111 | |||
112 | <div class="bottom-owner"> | ||
113 | <div class="section-label" i18n>OWNER ACCOUNT</div> | ||
114 | |||
115 | <ng-template *ngTemplateOutlet="ownerTemplate"></ng-template> | ||
45 | </div> | 116 | </div> |
46 | 117 | ||
47 | <div class="margin-content"> | 118 | <div class="links"> |
48 | <router-outlet></router-outlet> | 119 | <ng-template #linkTemplate let-item="item"> |
120 | <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> | ||
121 | </ng-template> | ||
122 | |||
123 | <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> | ||
49 | </div> | 124 | </div> |
125 | |||
126 | <router-outlet></router-outlet> | ||
50 | </div> | 127 | </div> |
128 | |||
129 | <my-support-modal #supportModal [videoChannel]="videoChannel"></my-support-modal> | ||
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss index 22f21dcc6..b19b4c81b 100644 --- a/client/src/app/+video-channels/video-channels.component.scss +++ b/client/src/app/+video-channels/video-channels.component.scss | |||
@@ -1,89 +1,308 @@ | |||
1 | // Bootstrap grid utilities require functions, variables and mixins | ||
2 | @import 'node_modules/bootstrap/scss/functions'; | ||
3 | @import 'node_modules/bootstrap/scss/variables'; | ||
4 | @import 'node_modules/bootstrap/scss/mixins'; | ||
5 | @import 'node_modules/bootstrap/scss/grid'; | ||
6 | |||
7 | @import '_variables'; | 1 | @import '_variables'; |
8 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | @import '_actor'; | ||
4 | @import '_miniature'; | ||
9 | 5 | ||
10 | .sub-menu { | 6 | .root { |
11 | @include sub-menu-with-actor; | 7 | --myGlobalTopPadding: 60px; |
8 | --myChannelImgMargin: 30px; | ||
9 | --myFontSize: 16px; | ||
10 | --myGreyChannelFontSize: 16px; | ||
11 | --myGreyOwnerFontSize: 14px; | ||
12 | } | ||
12 | 13 | ||
13 | .actor, .actor-info { | 14 | .banner { |
14 | width: 100%; | 15 | @include block-ratio('img', $banner-inverted-ratio); |
15 | } | 16 | } |
16 | 17 | ||
17 | .actor-info { | 18 | .section-label { |
18 | display: grid !important; | 19 | @include section-label-responsive; |
19 | grid-template-columns: 1fr auto; | 20 | } |
20 | grid-template-rows: 1fr auto / 1fr auto; | ||
21 | grid-template-areas: "name buttons" "lower buttons"; | ||
22 | 21 | ||
23 | @include media-breakpoint-down(lg) { | 22 | .links { |
24 | grid-template-areas: "name name" "lower buttons"; | 23 | @include grid-videos-miniature-margins; |
25 | } | 24 | } |
25 | |||
26 | .actor-info { | ||
27 | min-width: 1px; | ||
28 | width: 100%; | ||
29 | |||
30 | > h4, | ||
31 | > .actor-handle { | ||
32 | @include ellipsis; | ||
26 | } | 33 | } |
34 | } | ||
35 | |||
36 | .channel-info { | ||
37 | @include grid-videos-miniature-margins(false, 15px); | ||
38 | |||
39 | display: grid; | ||
40 | grid-template-columns: 1fr auto; | ||
41 | grid-template-rows: auto auto; | ||
42 | |||
43 | background-color: pvar(--channelBackgroundColor); | ||
44 | margin-bottom: 45px; | ||
45 | padding-top: var(--myGlobalTopPadding); | ||
46 | font-size: var(--myFontSize); | ||
47 | } | ||
48 | |||
49 | .channel-avatar-row { | ||
50 | @include avatar-row-responsive(var(--myChannelImgMargin), var(--myGreyChannelFontSize)); | ||
51 | } | ||
52 | |||
53 | .support-button { | ||
54 | @include button-with-icon(21px, 0, -1px); | ||
55 | } | ||
56 | |||
57 | .channel-description { | ||
58 | grid-column: 1; | ||
59 | word-break: break-word; | ||
60 | } | ||
61 | |||
62 | .show-more { | ||
63 | @include show-more-description; | ||
64 | |||
65 | display: none; | ||
66 | } | ||
67 | |||
68 | .channel-buttons { | ||
69 | display: flex; | ||
70 | flex-wrap: wrap; | ||
27 | 71 | ||
28 | .actor-names { | 72 | > *:not(:last-child) { |
29 | grid-area: name; | 73 | margin-right: 15px; |
30 | } | 74 | } |
75 | } | ||
76 | |||
77 | .channel-buttons.right { | ||
78 | margin-left: 45px; | ||
79 | } | ||
80 | |||
81 | // Only used by mobile | ||
82 | .channel-buttons.bottom { | ||
83 | display: none; | ||
84 | } | ||
85 | |||
86 | .created-at { | ||
87 | margin-top: 15px; | ||
88 | color: pvar(--greyForegroundColor); | ||
89 | padding-bottom: 60px; | ||
90 | } | ||
91 | |||
92 | .owner-card { | ||
93 | margin-left: 105px; | ||
94 | grid-column: 2; | ||
95 | // Takes all the column | ||
96 | grid-row: 1 / 3; | ||
97 | place-self: end; | ||
98 | } | ||
99 | |||
100 | // Only used on mobile | ||
101 | .bottom-owner { | ||
102 | display: none; | ||
103 | } | ||
104 | |||
105 | .owner-block { | ||
106 | background-color: pvar(--mainBackgroundColor); | ||
107 | padding: 30px; | ||
108 | width: 300px; | ||
109 | font-size: var(--myFontSize); | ||
31 | 110 | ||
32 | .actor-name { | 111 | .avatar-row { |
33 | flex-grow: 1; | 112 | display: flex; |
113 | margin-bottom: 15px; | ||
34 | 114 | ||
35 | .copy-button { | 115 | img { |
36 | border: none; | 116 | @include avatar(48px); |
37 | padding: 5px; | ||
38 | margin-top: -2px; | ||
39 | } | 117 | } |
118 | |||
119 | .actor-info { | ||
120 | margin-left: 15px; | ||
121 | } | ||
122 | |||
123 | h4 { | ||
124 | font-size: 18px; | ||
125 | margin: 0; | ||
126 | |||
127 | a { | ||
128 | color: pvar(--mainForegroundColor); | ||
129 | } | ||
130 | } | ||
131 | |||
132 | .actor-handle { | ||
133 | font-size: var(--myGreyOwnerFontSize); | ||
134 | color: pvar(--greyForegroundColor); | ||
135 | } | ||
136 | } | ||
137 | |||
138 | .owner-description { | ||
139 | max-height: 140px; | ||
140 | word-break: break-word; | ||
141 | |||
142 | @include fade-text(120px, pvar(--mainBackgroundColor)); | ||
40 | } | 143 | } |
41 | } | 144 | } |
42 | 145 | ||
43 | .margin-content { | 146 | .view-account.short { |
44 | // margin-content is required, but child views have their own margins | 147 | @include peertube-button-link; |
45 | // that match views outside the scope of accounts, so we only align | 148 | @include orange-button-inverted; |
46 | // them with the margins of .sub-menu when required. | 149 | |
47 | margin: 0; | 150 | margin-top: 30px; |
151 | } | ||
152 | |||
153 | .view-account.complete { | ||
154 | display: none; | ||
48 | } | 155 | } |
49 | 156 | ||
50 | .right-buttons { | 157 | .copy-button { |
51 | display: flex; | 158 | border: none; |
52 | height: max-content; | 159 | } |
53 | margin-left: auto; | 160 | |
54 | margin-top: 10px; | 161 | @media screen and (max-width: 1400px) { |
162 | // Takes all the row width | ||
163 | .channel-avatar-row { | ||
164 | grid-column: 1 / 3; | ||
165 | } | ||
166 | |||
167 | .owner-card { | ||
168 | grid-row: 2; | ||
169 | margin-left: 60px; | ||
170 | } | ||
171 | } | ||
172 | |||
173 | @media screen and (max-width: 1100px) { | ||
174 | .root { | ||
175 | --myGlobalTopPadding: 45px; | ||
176 | --myChannelImgMargin: 15px; | ||
177 | } | ||
178 | |||
179 | .channel-info { | ||
180 | display: flex; | ||
181 | flex-direction: column; | ||
182 | margin-bottom: 0; | ||
183 | } | ||
184 | |||
185 | .channel-description:not(.expanded) { | ||
186 | max-height: 70px; | ||
187 | |||
188 | @include fade-text(30px, pvar(--channelBackgroundColor)); | ||
189 | } | ||
190 | |||
191 | .show-more { | ||
192 | display: inline-block; | ||
193 | } | ||
194 | |||
195 | .channel-buttons.bottom { | ||
196 | display: flex; | ||
197 | justify-content: center; | ||
198 | margin-bottom: 30px; | ||
199 | } | ||
200 | |||
201 | .channel-buttons.right { | ||
202 | display: none; | ||
203 | } | ||
204 | |||
205 | .owner-card { | ||
206 | display: none; | ||
207 | } | ||
208 | |||
209 | .bottom-owner { | ||
210 | display: block; | ||
211 | width: 100%; | ||
212 | border-bottom: 2px solid $separator-border-color; | ||
213 | padding: var(--myGlobalTopPadding) 45px; | ||
214 | margin-bottom: 60px; | ||
215 | } | ||
55 | 216 | ||
56 | grid-row: buttons-start / span buttons-end; | 217 | .owner-block { |
57 | grid-column: buttons-start; | 218 | display: grid; |
219 | width: 100%; | ||
220 | padding: 0; | ||
58 | 221 | ||
59 | @include media-breakpoint-down(lg) { | 222 | .avatar-row { |
60 | flex-flow: column-reverse; | 223 | grid-column: 1; |
224 | margin-right: 30px; | ||
225 | } | ||
226 | |||
227 | .owner-description { | ||
228 | grid-column: 2; | ||
229 | max-height: 70px; | ||
230 | |||
231 | @include fade-text(30px, pvar(--mainBackgroundColor)); | ||
232 | } | ||
61 | 233 | ||
62 | a { | 234 | .view-account { |
63 | margin-top: 0.25rem; | 235 | grid-column: 2; |
64 | margin-right: 0 !important; | ||
65 | } | 236 | } |
66 | } | 237 | } |
67 | 238 | ||
68 | a { | 239 | .view-account.complete { |
69 | @include peertube-button-outline; | 240 | display: block; |
70 | line-height: 1.8; | 241 | text-align: right; |
242 | margin-top: 10px; | ||
243 | color: pvar(--mainColor); | ||
71 | } | 244 | } |
72 | 245 | ||
73 | my-subscribe-button { | 246 | .view-account.short { |
74 | height: min-content; | 247 | display: none; |
75 | } | 248 | } |
76 | } | 249 | } |
77 | 250 | ||
78 | @media screen and (max-width: $mobile-view) { | 251 | @media screen and (max-width: $mobile-view) { |
79 | .sub-menu { | 252 | .root { |
80 | .actor { | 253 | --myGlobalTopPadding: 15px; |
81 | flex-direction: column; | 254 | --myFontSize: 14px; |
255 | --myGreyChannelFontSize: 13px; | ||
256 | --myGreyOwnerFontSize: 13px; | ||
257 | } | ||
258 | |||
259 | .links { | ||
260 | margin: auto !important; | ||
261 | width: min-content; | ||
262 | } | ||
263 | |||
264 | .show-more { | ||
265 | margin-bottom: 30px; | ||
266 | } | ||
267 | |||
268 | .bottom-owner { | ||
269 | padding: 15px; | ||
270 | margin-bottom: 30px; | ||
82 | 271 | ||
83 | .actor-info .actor-names { | 272 | .section-label { |
273 | display: none; | ||
274 | } | ||
275 | } | ||
276 | |||
277 | .owner-block { | ||
278 | display: block; | ||
279 | |||
280 | .avatar-row { | ||
281 | display: flex; | ||
282 | flex-direction: row-reverse; | ||
283 | margin: 0; | ||
284 | |||
285 | h4 { | ||
286 | font-size: 16px; | ||
287 | } | ||
288 | |||
289 | .actor-info { | ||
290 | display: flex; | ||
84 | flex-direction: column; | 291 | flex-direction: column; |
85 | align-items: normal; | 292 | align-items: flex-end; |
293 | justify-content: flex-end; | ||
294 | margin-top: -5px; | ||
295 | } | ||
296 | |||
297 | img { | ||
298 | @include channel-avatar(64px); | ||
299 | |||
300 | margin: -30px 0 0 15px; | ||
86 | } | 301 | } |
87 | } | 302 | } |
303 | |||
304 | .owner-description { | ||
305 | display: none; | ||
306 | } | ||
88 | } | 307 | } |
89 | } | 308 | } |
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts index bb601e227..41fdb5e79 100644 --- a/client/src/app/+video-channels/video-channels.component.ts +++ b/client/src/app/+video-channels/video-channels.component.ts | |||
@@ -3,8 +3,9 @@ import { Subscription } from 'rxjs' | |||
3 | import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators' | 3 | import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators' |
4 | import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' | 4 | import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' |
5 | import { ActivatedRoute } from '@angular/router' | 5 | import { ActivatedRoute } from '@angular/router' |
6 | import { AuthService, Notifier, RestExtractor, ScreenService } from '@app/core' | 6 | import { AuthService, MarkdownService, Notifier, RestExtractor, ScreenService } from '@app/core' |
7 | import { ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 7 | import { ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' |
8 | import { SupportModalComponent } from '@app/shared/shared-support-modal' | ||
8 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' | 9 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' |
9 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 10 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
10 | 11 | ||
@@ -14,12 +15,18 @@ import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | |||
14 | }) | 15 | }) |
15 | export class VideoChannelsComponent implements OnInit, OnDestroy { | 16 | export class VideoChannelsComponent implements OnInit, OnDestroy { |
16 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent | 17 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent |
18 | @ViewChild('supportModal') supportModal: SupportModalComponent | ||
17 | 19 | ||
18 | videoChannel: VideoChannel | 20 | videoChannel: VideoChannel |
19 | hotkeys: Hotkey[] | 21 | hotkeys: Hotkey[] |
20 | links: ListOverflowItem[] = [] | 22 | links: ListOverflowItem[] = [] |
21 | isChannelManageable = false | 23 | isChannelManageable = false |
22 | 24 | ||
25 | channelVideosCount: number | ||
26 | ownerDescriptionHTML = '' | ||
27 | channelDescriptionHTML = '' | ||
28 | channelDescriptionExpanded = false | ||
29 | |||
23 | private routeSub: Subscription | 30 | private routeSub: Subscription |
24 | 31 | ||
25 | constructor ( | 32 | constructor ( |
@@ -27,9 +34,11 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
27 | private notifier: Notifier, | 34 | private notifier: Notifier, |
28 | private authService: AuthService, | 35 | private authService: AuthService, |
29 | private videoChannelService: VideoChannelService, | 36 | private videoChannelService: VideoChannelService, |
37 | private videoService: VideoService, | ||
30 | private restExtractor: RestExtractor, | 38 | private restExtractor: RestExtractor, |
31 | private hotkeysService: HotkeysService, | 39 | private hotkeysService: HotkeysService, |
32 | private screenService: ScreenService | 40 | private screenService: ScreenService, |
41 | private markdown: MarkdownService | ||
33 | ) { } | 42 | ) { } |
34 | 43 | ||
35 | ngOnInit () { | 44 | ngOnInit () { |
@@ -43,16 +52,14 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
43 | HttpStatusCode.NOT_FOUND_404 | 52 | HttpStatusCode.NOT_FOUND_404 |
44 | ])) | 53 | ])) |
45 | ) | 54 | ) |
46 | .subscribe(videoChannel => { | 55 | .subscribe(async videoChannel => { |
56 | this.channelDescriptionHTML = await this.markdown.textMarkdownToHTML(videoChannel.description) | ||
57 | this.ownerDescriptionHTML = await this.markdown.textMarkdownToHTML(videoChannel.ownerAccount.description) | ||
58 | |||
59 | // After the markdown renderer to avoid layout changes | ||
47 | this.videoChannel = videoChannel | 60 | this.videoChannel = videoChannel |
48 | 61 | ||
49 | if (this.authService.isLoggedIn()) { | 62 | this.loadChannelVideosCount() |
50 | this.authService.userInformationLoaded | ||
51 | .subscribe(() => { | ||
52 | const channelUserId = this.videoChannel.ownerAccount.userId | ||
53 | this.isChannelManageable = channelUserId && channelUserId === this.authService.getUser().id | ||
54 | }) | ||
55 | } | ||
56 | }) | 63 | }) |
57 | 64 | ||
58 | this.hotkeys = [ | 65 | this.hotkeys = [ |
@@ -67,8 +74,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
67 | 74 | ||
68 | this.links = [ | 75 | this.links = [ |
69 | { label: $localize`VIDEOS`, routerLink: 'videos' }, | 76 | { label: $localize`VIDEOS`, routerLink: 'videos' }, |
70 | { label: $localize`VIDEO PLAYLISTS`, routerLink: 'video-playlists' }, | 77 | { label: $localize`PLAYLISTS`, routerLink: 'video-playlists' } |
71 | { label: $localize`ABOUT`, routerLink: 'about' } | ||
72 | ] | 78 | ] |
73 | } | 79 | } |
74 | 80 | ||
@@ -79,7 +85,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
79 | if (this.isUserLoggedIn()) this.hotkeysService.remove(this.hotkeys) | 85 | if (this.isUserLoggedIn()) this.hotkeysService.remove(this.hotkeys) |
80 | } | 86 | } |
81 | 87 | ||
82 | get isInSmallView () { | 88 | isInSmallView () { |
83 | return this.screenService.isInSmallView() | 89 | return this.screenService.isInSmallView() |
84 | } | 90 | } |
85 | 91 | ||
@@ -87,12 +93,36 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
87 | return this.authService.isLoggedIn() | 93 | return this.authService.isLoggedIn() |
88 | } | 94 | } |
89 | 95 | ||
90 | get isManageable () { | 96 | isManageable () { |
91 | if (!this.isUserLoggedIn()) return false | 97 | if (!this.isUserLoggedIn()) return false |
92 | return this.videoChannel.ownerAccount.userId === this.authService.getUser().id | 98 | |
99 | return this.videoChannel?.ownerAccount.userId === this.authService.getUser().id | ||
93 | } | 100 | } |
94 | 101 | ||
95 | activateCopiedMessage () { | 102 | activateCopiedMessage () { |
96 | this.notifier.success($localize`Username copied`) | 103 | this.notifier.success($localize`Username copied`) |
97 | } | 104 | } |
105 | |||
106 | hasShowMoreDescription () { | ||
107 | return !this.channelDescriptionExpanded && this.channelDescriptionHTML.length > 100 | ||
108 | } | ||
109 | |||
110 | showSupportModal () { | ||
111 | this.supportModal.show() | ||
112 | } | ||
113 | |||
114 | getAccountUrl () { | ||
115 | return [ '/accounts', this.videoChannel.ownerBy ] | ||
116 | } | ||
117 | |||
118 | private loadChannelVideosCount () { | ||
119 | this.videoService.getVideoChannelVideos({ | ||
120 | videoChannel: this.videoChannel, | ||
121 | videoPagination: { | ||
122 | currentPage: 1, | ||
123 | itemsPerPage: 0 | ||
124 | }, | ||
125 | sort: '-publishedAt' | ||
126 | }).subscribe(res => this.channelVideosCount = res.total) | ||
127 | } | ||
98 | } | 128 | } |
diff --git a/client/src/app/+video-channels/video-channels.module.ts b/client/src/app/+video-channels/video-channels.module.ts index 05236ff85..408f86225 100644 --- a/client/src/app/+video-channels/video-channels.module.ts +++ b/client/src/app/+video-channels/video-channels.module.ts | |||
@@ -2,10 +2,10 @@ import { NgModule } from '@angular/core' | |||
2 | import { SharedFormModule } from '@app/shared/shared-forms' | 2 | import { SharedFormModule } from '@app/shared/shared-forms' |
3 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' | 3 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' |
4 | import { SharedMainModule } from '@app/shared/shared-main' | 4 | import { SharedMainModule } from '@app/shared/shared-main' |
5 | import { SharedSupportModal } from '@app/shared/shared-support-modal' | ||
5 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' | 6 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' |
6 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' | 7 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' |
7 | import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist' | 8 | import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist' |
8 | import { VideoChannelAboutComponent } from './video-channel-about/video-channel-about.component' | ||
9 | import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' | 9 | import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' |
10 | import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' | 10 | import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' |
11 | import { VideoChannelsRoutingModule } from './video-channels-routing.module' | 11 | import { VideoChannelsRoutingModule } from './video-channels-routing.module' |
@@ -20,13 +20,13 @@ import { VideoChannelsComponent } from './video-channels.component' | |||
20 | SharedVideoPlaylistModule, | 20 | SharedVideoPlaylistModule, |
21 | SharedVideoMiniatureModule, | 21 | SharedVideoMiniatureModule, |
22 | SharedUserSubscriptionModule, | 22 | SharedUserSubscriptionModule, |
23 | SharedGlobalIconModule | 23 | SharedGlobalIconModule, |
24 | SharedSupportModal | ||
24 | ], | 25 | ], |
25 | 26 | ||
26 | declarations: [ | 27 | declarations: [ |
27 | VideoChannelsComponent, | 28 | VideoChannelsComponent, |
28 | VideoChannelVideosComponent, | 29 | VideoChannelVideosComponent, |
29 | VideoChannelAboutComponent, | ||
30 | VideoChannelPlaylistsComponent | 30 | VideoChannelPlaylistsComponent |
31 | ], | 31 | ], |
32 | 32 | ||
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 | ||
2 | import { forkJoin } from 'rxjs' | 2 | import { forkJoin } from 'rxjs' |
3 | import { Component, EventEmitter, OnInit, Output } from '@angular/core' | 3 | import { AfterViewChecked, AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core' |
4 | import { Router } from '@angular/router' | 4 | import { Router } from '@angular/router' |
5 | import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' | 5 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core' |
6 | import { scrollToTop } from '@app/helpers' | 6 | import { scrollToTop } from '@app/helpers' |
7 | import { FormValidatorService } from '@app/shared/shared-forms' | 7 | import { FormValidatorService } from '@app/shared/shared-forms' |
8 | import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' | 8 | import { 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 | }) |
22 | export class VideoGoLiveComponent extends VideoSend implements OnInit, CanComponentDeactivate { | 22 | export 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.scss b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss index 1fef74994..dd87641fc 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss +++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss | |||
@@ -3,8 +3,8 @@ | |||
3 | 3 | ||
4 | .first-step-block { | 4 | .first-step-block { |
5 | .torrent-or-magnet { | 5 | .torrent-or-magnet { |
6 | @include divider($color: pvar(--inputPlaceholderColor), $background: pvar(--submenuColor)); | 6 | @include divider($color: pvar(--inputPlaceholderColor), $background: pvar(--submenuBackgroundColor)); |
7 | 7 | ||
8 | &[data-content] { | 8 | &[data-content] { |
9 | margin: 1.5rem 0; | 9 | margin: 1.5rem 0; |
10 | } | 10 | } |
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 @@ | |||
1 | import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { AfterViewInit, Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' | 3 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core' |
4 | import { scrollToTop } from '@app/helpers' | 4 | import { scrollToTop } from '@app/helpers' |
5 | import { FormValidatorService } from '@app/shared/shared-forms' | 5 | import { FormValidatorService } from '@app/shared/shared-forms' |
6 | import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' | 6 | import { 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 | }) |
21 | export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { | 21 | export 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 @@ | |||
1 | import { map, switchMap } from 'rxjs/operators' | 1 | import { map, switchMap } from 'rxjs/operators' |
2 | import { Component, EventEmitter, OnInit, Output } from '@angular/core' | 2 | import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core' |
3 | import { Router } from '@angular/router' | 3 | import { Router } from '@angular/router' |
4 | import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' | 4 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core' |
5 | import { getAbsoluteAPIUrl, scrollToTop } from '@app/helpers' | 5 | import { getAbsoluteAPIUrl, scrollToTop } from '@app/helpers' |
6 | import { FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormValidatorService } from '@app/shared/shared-forms' |
7 | import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' | 7 | import { 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 | }) |
21 | export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { | 21 | export 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 @@ | |||
1 | import { Subscription } from 'rxjs' | 1 | import { Subscription } from 'rxjs' |
2 | import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http' | 2 | import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http' |
3 | import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' | 3 | import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' |
4 | import { Router } from '@angular/router' | 4 | import { Router } from '@angular/router' |
5 | import { AuthService, CanComponentDeactivate, Notifier, ServerService, UserService } from '@app/core' | 5 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core' |
6 | import { scrollToTop, uploadErrorHandler } from '@app/helpers' | 6 | import { scrollToTop, uploadErrorHandler } from '@app/helpers' |
7 | import { FormValidatorService } from '@app/shared/shared-forms' | 7 | import { FormValidatorService } from '@app/shared/shared-forms' |
8 | import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' | 8 | import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' |
9 | import { LoadingBarService } from '@ngx-loading-bar/core' | 9 | import { LoadingBarService } from '@ngx-loading-bar/core' |
10 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
10 | import { VideoPrivacy } from '@shared/models' | 11 | import { VideoPrivacy } from '@shared/models' |
11 | import { VideoSend } from './video-send' | 12 | import { VideoSend } from './video-send' |
12 | import { 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 | }) |
23 | export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { | 23 | export 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-edit/video-add.component.scss b/client/src/app/+videos/+video-edit/video-add.component.scss index 5db9e823d..1ebee946b 100644 --- a/client/src/app/+videos/+video-edit/video-add.component.scss +++ b/client/src/app/+videos/+video-edit/video-add.component.scss | |||
@@ -67,7 +67,7 @@ $nav-link-height: 40px; | |||
67 | &.active { | 67 | &.active { |
68 | border-color: $border-color; | 68 | border-color: $border-color; |
69 | border-bottom-color: transparent; | 69 | border-bottom-color: transparent; |
70 | background-color: pvar(--submenuColor) !important; | 70 | background-color: pvar(--submenuBackgroundColor) !important; |
71 | 71 | ||
72 | span { | 72 | span { |
73 | border-bottom-color: pvar(--mainColor); | 73 | border-bottom-color: pvar(--mainColor); |
@@ -84,7 +84,7 @@ $nav-link-height: 40px; | |||
84 | border: $border-width $border-type $border-color; | 84 | border: $border-width $border-type $border-color; |
85 | border-top: transparent; | 85 | border-top: transparent; |
86 | 86 | ||
87 | background-color: pvar(--submenuColor); | 87 | background-color: pvar(--submenuBackgroundColor); |
88 | border-bottom-left-radius: 3px; | 88 | border-bottom-left-radius: 3px; |
89 | border-bottom-right-radius: 3px; | 89 | border-bottom-right-radius: 3px; |
90 | width: 100%; | 90 | width: 100%; |
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 | |||
5 | import { HooksService } from '@app/core/plugins/hooks.service' | 5 | import { HooksService } from '@app/core/plugins/hooks.service' |
6 | import { Syndication, VideoDetails } from '@app/shared/shared-main' | 6 | import { Syndication, VideoDetails } from '@app/shared/shared-main' |
7 | import { VideoComment, VideoCommentService, VideoCommentThreadTree } from '@app/shared/shared-video-comment' | 7 | import { VideoComment, VideoCommentService, VideoCommentThreadTree } from '@app/shared/shared-video-comment' |
8 | import { 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/+videos/+video-watch/modal/video-support.component.ts b/client/src/app/+videos/+video-watch/modal/video-support.component.ts deleted file mode 100644 index bd5290a72..000000000 --- a/client/src/app/+videos/+video-watch/modal/video-support.component.ts +++ /dev/null | |||
@@ -1,31 +0,0 @@ | |||
1 | import { Component, Input, ViewChild } from '@angular/core' | ||
2 | import { MarkdownService } from '@app/core' | ||
3 | import { VideoDetails } from '@app/shared/shared-main' | ||
4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
5 | |||
6 | @Component({ | ||
7 | selector: 'my-video-support', | ||
8 | templateUrl: './video-support.component.html', | ||
9 | styleUrls: [ './video-support.component.scss' ] | ||
10 | }) | ||
11 | export class VideoSupportComponent { | ||
12 | @Input() video: VideoDetails = null | ||
13 | |||
14 | @ViewChild('modal', { static: true }) modal: NgbModal | ||
15 | |||
16 | videoHTMLSupport = '' | ||
17 | |||
18 | constructor ( | ||
19 | private markdownService: MarkdownService, | ||
20 | private modalService: NgbModal | ||
21 | ) { } | ||
22 | |||
23 | show () { | ||
24 | const modalRef = this.modalService.open(this.modal, { centered: true }) | ||
25 | |||
26 | this.markdownService.enhancedMarkdownToHTML(this.video.support) | ||
27 | .then(r => this.videoHTMLSupport = r) | ||
28 | |||
29 | return modalRef | ||
30 | } | ||
31 | } | ||
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html index 3c7c679b8..e0e9f92e7 100644 --- a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html +++ b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html | |||
@@ -1,4 +1,4 @@ | |||
1 | <div class="other-videos"> | 1 | <div class="other-videos" [ngClass]="{ 'display-as-row': displayAsRow }"> |
2 | <ng-container *ngIf="hasVideos$ | async"> | 2 | <ng-container *ngIf="hasVideos$ | async"> |
3 | <div class="title-page-container"> | 3 | <div class="title-page-container"> |
4 | <h2 i18n class="title-page title-page-single"> | 4 | <h2 i18n class="title-page title-page-single"> |
@@ -14,7 +14,7 @@ | |||
14 | 14 | ||
15 | <ng-container *ngFor="let video of (videos$ | async); let i = index; let length = count"> | 15 | <ng-container *ngFor="let video of (videos$ | async); let i = index; let length = count"> |
16 | <my-video-miniature | 16 | <my-video-miniature |
17 | [displayOptions]="displayOptions" [video]="video" [user]="userMiniature" | 17 | [displayOptions]="displayOptions" [video]="video" [user]="userMiniature" [displayAsRow]="displayAsRow" |
18 | (videoBlocked)="onVideoRemoved()" (videoRemoved)="onVideoRemoved()" (videoAccountMuted)="onVideoRemoved()"> | 18 | (videoBlocked)="onVideoRemoved()" (videoRemoved)="onVideoRemoved()" (videoAccountMuted)="onVideoRemoved()"> |
19 | </my-video-miniature> | 19 | </my-video-miniature> |
20 | 20 | ||
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss index b278c9654..c9fae6f27 100644 --- a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss +++ b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss | |||
@@ -1,3 +1,6 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
1 | .title-page-container { | 4 | .title-page-container { |
2 | display: flex; | 5 | display: flex; |
3 | justify-content: space-between; | 6 | justify-content: space-between; |
@@ -11,6 +14,10 @@ | |||
11 | } | 14 | } |
12 | } | 15 | } |
13 | 16 | ||
17 | .title-page { | ||
18 | margin-top: 0; | ||
19 | } | ||
20 | |||
14 | .title-page-autoplay { | 21 | .title-page-autoplay { |
15 | display: flex; | 22 | display: flex; |
16 | width: max-content; | 23 | width: max-content; |
@@ -29,3 +36,29 @@ | |||
29 | hr { | 36 | hr { |
30 | margin-top: 0; | 37 | margin-top: 0; |
31 | } | 38 | } |
39 | |||
40 | my-video-miniature { | ||
41 | display: block; | ||
42 | } | ||
43 | |||
44 | .other-videos:not(.display-as-row) my-video-miniature { | ||
45 | min-width: $video-thumbnail-medium-width; | ||
46 | max-width: $video-thumbnail-medium-width; | ||
47 | } | ||
48 | |||
49 | .display-as-row { | ||
50 | my-video-miniature { | ||
51 | margin-bottom: 20px; | ||
52 | } | ||
53 | |||
54 | hr { | ||
55 | display: none; | ||
56 | } | ||
57 | |||
58 | @media screen and (max-width: $mobile-view) { | ||
59 | my-video-miniature { | ||
60 | margin-bottom: 10px; | ||
61 | } | ||
62 | } | ||
63 | } | ||
64 | |||
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts index a1c8e0661..89b9c01b6 100644 --- a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts +++ b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts | |||
@@ -16,6 +16,8 @@ import { RecommendedVideosStore } from './recommended-videos.store' | |||
16 | export class RecommendedVideosComponent implements OnInit, OnChanges { | 16 | export class RecommendedVideosComponent implements OnInit, OnChanges { |
17 | @Input() inputRecommendation: RecommendationInfo | 17 | @Input() inputRecommendation: RecommendationInfo |
18 | @Input() playlist: VideoPlaylist | 18 | @Input() playlist: VideoPlaylist |
19 | @Input() displayAsRow: boolean | ||
20 | |||
19 | @Output() gotRecommendations = new EventEmitter<Video[]>() | 21 | @Output() gotRecommendations = new EventEmitter<Video[]>() |
20 | 22 | ||
21 | autoPlayNextVideo: boolean | 23 | autoPlayNextVideo: boolean |
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.html b/client/src/app/+videos/+video-watch/video-avatar-channel.component.html index 310cc926f..5058f05dd 100644 --- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.html +++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.html | |||
@@ -1,8 +1,9 @@ | |||
1 | <div class="wrapper" [ngClass]="'avatar-' + size"> | 1 | <div class="wrapper" [ngClass]="'avatar-' + size"> |
2 | <ng-container *ngIf="!isChannelAvatarNull() && !genericChannel"> | 2 | <ng-container *ngIf="!isChannelAvatarNull() && !genericChannel"> |
3 | <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> | 3 | <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> |
4 | <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" /> | 4 | <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" class="channel-avatar" /> |
5 | </a> | 5 | </a> |
6 | |||
6 | <a [routerLink]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle"> | 7 | <a [routerLink]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle"> |
7 | <img [src]="video.accountAvatarUrl" i18n-alt alt="Account avatar" /> | 8 | <img [src]="video.accountAvatarUrl" i18n-alt alt="Account avatar" /> |
8 | </a> | 9 | </a> |
@@ -14,7 +15,7 @@ | |||
14 | </a> | 15 | </a> |
15 | 16 | ||
16 | <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> | 17 | <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> |
17 | <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" /> | 18 | <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" class="channel-avatar" /> |
18 | </a> | 19 | </a> |
19 | </ng-container> | 20 | </ng-container> |
20 | 21 | ||
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.scss b/client/src/app/+videos/+video-watch/video-avatar-channel.component.scss index 37709fce6..4998e85fa 100644 --- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.scss +++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.scss | |||
@@ -25,8 +25,12 @@ | |||
25 | position: absolute; | 25 | position: absolute; |
26 | top:50%; | 26 | top:50%; |
27 | left:50%; | 27 | left:50%; |
28 | border-radius: 50%; | 28 | transform: translate(-50%,-50%); |
29 | transform: translate(-50%,-50%) | 29 | border-radius: 5px; |
30 | |||
31 | &:not(.channel-avatar) { | ||
32 | border-radius: 50%; | ||
33 | } | ||
30 | } | 34 | } |
31 | 35 | ||
32 | a:nth-of-type(2) img { | 36 | a:nth-of-type(2) img { |
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts b/client/src/app/+videos/+video-watch/video-avatar-channel.component.ts index 440e2b522..0b6e796df 100644 --- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts +++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | 1 | import { Component, Input, OnInit } from '@angular/core' |
2 | import { Video } from '../video/video.model' | 2 | import { Video } from '@app/shared/shared-main/video' |
3 | 3 | ||
4 | @Component({ | 4 | @Component({ |
5 | selector: 'my-video-avatar-channel', | 5 | selector: 'my-video-avatar-channel', |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.html b/client/src/app/+videos/+video-watch/video-watch.component.html index b17f898ce..99103c2c3 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.html +++ b/client/src/app/+videos/+video-watch/video-watch.component.html | |||
@@ -142,7 +142,7 @@ | |||
142 | <ng-container *ngIf="isUserLoggedIn()"> | 142 | <ng-container *ngIf="isUserLoggedIn()"> |
143 | <my-video-actions-dropdown | 143 | <my-video-actions-dropdown |
144 | placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" [videoCaptions]="videoCaptions" | 144 | placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" [videoCaptions]="videoCaptions" |
145 | [displayOptions]="videoActionsOptions" (videoRemoved)="onVideoRemoved()" (modalOpened)="onModalOpened()" | 145 | [displayOptions]="videoActionsOptions" (videoRemoved)="onVideoRemoved()" |
146 | ></my-video-actions-dropdown> | 146 | ></my-video-actions-dropdown> |
147 | </ng-container> | 147 | </ng-container> |
148 | </div> | 148 | </div> |
@@ -289,6 +289,7 @@ | |||
289 | </div> | 289 | </div> |
290 | 290 | ||
291 | <my-recommended-videos | 291 | <my-recommended-videos |
292 | [displayAsRow]="displayOtherVideosAsRow()" | ||
292 | [inputRecommendation]="{ uuid: video.uuid, tags: video.tags }" | 293 | [inputRecommendation]="{ uuid: video.uuid, tags: video.tags }" |
293 | [playlist]="playlist" | 294 | [playlist]="playlist" |
294 | (gotRecommendations)="onRecommendations($event)" | 295 | (gotRecommendations)="onRecommendations($event)" |
@@ -313,6 +314,6 @@ | |||
313 | </div> | 314 | </div> |
314 | 315 | ||
315 | <ng-container *ngIf="video !== null"> | 316 | <ng-container *ngIf="video !== null"> |
316 | <my-video-support #videoSupportModal [video]="video"></my-video-support> | 317 | <my-support-modal #supportModal [video]="video"></my-support-modal> |
317 | <my-video-share #videoShareModal [video]="video" [videoCaptions]="videoCaptions" [playlist]="playlist"></my-video-share> | 318 | <my-video-share #videoShareModal [video]="video" [videoCaptions]="videoCaptions" [playlist]="playlist"></my-video-share> |
318 | </ng-container> | 319 | </ng-container> |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.scss b/client/src/app/+videos/+video-watch/video-watch.component.scss index 555126cbc..2e566e3fb 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.scss +++ b/client/src/app/+videos/+video-watch/video-watch.component.scss | |||
@@ -3,7 +3,7 @@ | |||
3 | @import '_bootstrap-variables'; | 3 | @import '_bootstrap-variables'; |
4 | @import '_miniature'; | 4 | @import '_miniature'; |
5 | 5 | ||
6 | $player-factor: 1.7; // 16/9 | 6 | $player-factor: #{16/9}; |
7 | $video-info-margin-left: 44px; | 7 | $video-info-margin-left: 44px; |
8 | 8 | ||
9 | @function getPlayerHeight($width){ | 9 | @function getPlayerHeight($width){ |
@@ -179,12 +179,6 @@ $video-info-margin-left: 44px; | |||
179 | &:hover { | 179 | &:hover { |
180 | opacity: 0.8; | 180 | opacity: 0.8; |
181 | } | 181 | } |
182 | |||
183 | img { | ||
184 | @include avatar(18px); | ||
185 | |||
186 | margin: -2px 5px 0 0; | ||
187 | } | ||
188 | } | 182 | } |
189 | 183 | ||
190 | .video-info-channel-left { | 184 | .video-info-channel-left { |
@@ -413,37 +407,12 @@ $video-info-margin-left: 44px; | |||
413 | } | 407 | } |
414 | } | 408 | } |
415 | } | 409 | } |
410 | } | ||
416 | 411 | ||
417 | ::ng-deep .other-videos { | 412 | my-recommended-videos { |
418 | padding-left: 15px; | 413 | display: block; |
419 | min-width: $video-miniature-width; | 414 | padding-left: 15px; |
420 | 415 | min-width: 250px; | |
421 | @media screen and (min-width: 1800px - (3* $video-miniature-width)) { | ||
422 | width: min-content; | ||
423 | } | ||
424 | |||
425 | .title-page { | ||
426 | margin: 0 !important; | ||
427 | } | ||
428 | |||
429 | .video-miniature { | ||
430 | display: flex; | ||
431 | width: max-content; | ||
432 | height: 100%; | ||
433 | padding-bottom: 20px; | ||
434 | flex-wrap: wrap; | ||
435 | } | ||
436 | |||
437 | .video-bottom { | ||
438 | @media screen and (max-width: 1800px - (3* $video-miniature-width)) { | ||
439 | margin-left: 1rem; | ||
440 | } | ||
441 | @media screen and (max-width: 500px) { | ||
442 | margin-left: 0; | ||
443 | margin-top: .5rem; | ||
444 | } | ||
445 | } | ||
446 | } | ||
447 | } | 416 | } |
448 | 417 | ||
449 | my-video-comments { | 418 | my-video-comments { |
@@ -537,6 +506,7 @@ my-video-comments { | |||
537 | } | 506 | } |
538 | } | 507 | } |
539 | 508 | ||
509 | // Use the same breakpoint than in the typescript component to display the other video miniatures as row | ||
540 | @media screen and (max-width: 1100px) { | 510 | @media screen and (max-width: 1100px) { |
541 | #video-wrapper { | 511 | #video-wrapper { |
542 | flex-direction: column; | 512 | flex-direction: column; |
@@ -549,15 +519,10 @@ my-video-comments { | |||
549 | 519 | ||
550 | .video-bottom { | 520 | .video-bottom { |
551 | flex-direction: column; | 521 | flex-direction: column; |
522 | } | ||
552 | 523 | ||
553 | ::ng-deep .other-videos { | 524 | my-recommended-videos { |
554 | padding-left: 0 !important; | 525 | padding-left: 0; |
555 | |||
556 | ::ng-deep .video-miniature { | ||
557 | flex-direction: row; | ||
558 | width: auto; | ||
559 | } | ||
560 | } | ||
561 | } | 526 | } |
562 | } | 527 | } |
563 | 528 | ||
@@ -579,10 +544,6 @@ my-video-comments { | |||
579 | } | 544 | } |
580 | } | 545 | } |
581 | 546 | ||
582 | ::ng-deep .other-videos .video-miniature { | ||
583 | flex-direction: column; | ||
584 | } | ||
585 | |||
586 | .privacy-concerns { | 547 | .privacy-concerns { |
587 | width: 100%; | 548 | width: 100%; |
588 | } | 549 | } |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index 7a98cab3b..de5fb4ed0 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts | |||
@@ -21,6 +21,7 @@ import { RedirectService } from '@app/core/routing/redirect.service' | |||
21 | import { isXPercentInViewport, scrollToTop } from '@app/helpers' | 21 | import { isXPercentInViewport, scrollToTop } from '@app/helpers' |
22 | import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' | 22 | import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' |
23 | import { VideoShareComponent } from '@app/shared/shared-share-modal' | 23 | import { VideoShareComponent } from '@app/shared/shared-share-modal' |
24 | import { SupportModalComponent } from '@app/shared/shared-support-modal' | ||
24 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' | 25 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' |
25 | import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature' | 26 | import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature' |
26 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' | 27 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' |
@@ -28,7 +29,12 @@ import { MetaService } from '@ngx-meta/core' | |||
28 | import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' | 29 | import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' |
29 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 30 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
30 | import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models' | 31 | import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models' |
31 | import { getStoredP2PEnabled, getStoredTheater } from '../../../assets/player/peertube-player-local-storage' | 32 | import { |
33 | cleanupVideoWatch, | ||
34 | getStoredP2PEnabled, | ||
35 | getStoredTheater, | ||
36 | getStoredVideoWatchHistory | ||
37 | } from '../../../assets/player/peertube-player-local-storage' | ||
32 | import { | 38 | import { |
33 | CustomizationOptions, | 39 | CustomizationOptions, |
34 | P2PMediaLoaderOptions, | 40 | P2PMediaLoaderOptions, |
@@ -39,7 +45,6 @@ import { | |||
39 | } from '../../../assets/player/peertube-player-manager' | 45 | } from '../../../assets/player/peertube-player-manager' |
40 | import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils' | 46 | import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils' |
41 | import { environment } from '../../../environments/environment' | 47 | import { environment } from '../../../environments/environment' |
42 | import { VideoSupportComponent } from './modal/video-support.component' | ||
43 | import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' | 48 | import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' |
44 | 49 | ||
45 | type URLOptions = CustomizationOptions & { playerMode: PlayerMode } | 50 | type URLOptions = CustomizationOptions & { playerMode: PlayerMode } |
@@ -54,7 +59,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
54 | 59 | ||
55 | @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent | 60 | @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent |
56 | @ViewChild('videoShareModal') videoShareModal: VideoShareComponent | 61 | @ViewChild('videoShareModal') videoShareModal: VideoShareComponent |
57 | @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent | 62 | @ViewChild('supportModal') supportModal: SupportModalComponent |
58 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent | 63 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent |
59 | @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent | 64 | @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent |
60 | 65 | ||
@@ -195,6 +200,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
195 | this.theaterEnabled = getStoredTheater() | 200 | this.theaterEnabled = getStoredTheater() |
196 | 201 | ||
197 | this.hooks.runAction('action:video-watch.init', 'video-watch') | 202 | this.hooks.runAction('action:video-watch.init', 'video-watch') |
203 | |||
204 | setTimeout(cleanupVideoWatch, 1500) // Run in timeout to ensure we're not blocking the UI | ||
198 | } | 205 | } |
199 | 206 | ||
200 | ngOnDestroy () { | 207 | ngOnDestroy () { |
@@ -277,23 +284,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
277 | } | 284 | } |
278 | 285 | ||
279 | showSupportModal () { | 286 | showSupportModal () { |
280 | // Check video was playing before opening support modal | 287 | this.supportModal.show() |
281 | const isVideoPlaying = this.isPlaying() | ||
282 | |||
283 | this.pausePlayer() | ||
284 | |||
285 | const modalRef = this.videoSupportModal.show() | ||
286 | |||
287 | modalRef.result.then(() => { | ||
288 | if (isVideoPlaying) { | ||
289 | this.resumePlayer() | ||
290 | } | ||
291 | }) | ||
292 | } | 288 | } |
293 | 289 | ||
294 | showShareModal () { | 290 | showShareModal () { |
295 | this.pausePlayer() | ||
296 | |||
297 | this.videoShareModal.show(this.currentTime, this.videoWatchPlaylist.currentPlaylistPosition) | 291 | this.videoShareModal.show(this.currentTime, this.videoWatchPlaylist.currentPlaylistPosition) |
298 | } | 292 | } |
299 | 293 | ||
@@ -316,10 +310,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
316 | } | 310 | } |
317 | } | 311 | } |
318 | 312 | ||
319 | onModalOpened () { | ||
320 | this.pausePlayer() | ||
321 | } | ||
322 | |||
323 | onVideoRemoved () { | 313 | onVideoRemoved () { |
324 | this.redirectService.redirectToHomepage() | 314 | this.redirectService.redirectToHomepage() |
325 | } | 315 | } |
@@ -396,6 +386,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
396 | this.loadVideo(videoId) | 386 | this.loadVideo(videoId) |
397 | } | 387 | } |
398 | 388 | ||
389 | displayOtherVideosAsRow () { | ||
390 | // Use the same value as in the SASS file | ||
391 | return this.screenService.getWindowInnerWidth() <= 1100 | ||
392 | } | ||
393 | |||
399 | private loadVideo (videoId: string) { | 394 | private loadVideo (videoId: string) { |
400 | // Video did not change | 395 | // Video did not change |
401 | if (this.video && this.video.uuid === videoId) return | 396 | if (this.video && this.video.uuid === videoId) return |
@@ -768,9 +763,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
768 | const getStartTime = () => { | 763 | const getStartTime = () => { |
769 | const byUrl = urlOptions.startTime !== undefined | 764 | const byUrl = urlOptions.startTime !== undefined |
770 | const byHistory = video.userHistory && (!this.playlist || urlOptions.resume !== undefined) | 765 | const byHistory = video.userHistory && (!this.playlist || urlOptions.resume !== undefined) |
766 | const byLocalStorage = getStoredVideoWatchHistory(video.uuid) | ||
771 | 767 | ||
772 | if (byUrl) return timeToInt(urlOptions.startTime) | 768 | if (byUrl) return timeToInt(urlOptions.startTime) |
773 | if (byHistory) return video.userHistory.currentTime | 769 | if (byHistory) return video.userHistory.currentTime |
770 | if (byLocalStorage) return byLocalStorage.duration | ||
774 | 771 | ||
775 | return 0 | 772 | return 0 |
776 | } | 773 | } |
@@ -815,6 +812,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
815 | ? this.videoService.getVideoViewUrl(video.uuid) | 812 | ? this.videoService.getVideoViewUrl(video.uuid) |
816 | : null, | 813 | : null, |
817 | embedUrl: video.embedUrl, | 814 | embedUrl: video.embedUrl, |
815 | embedTitle: video.name, | ||
818 | 816 | ||
819 | isLive: video.isLive, | 817 | isLive: video.isLive, |
820 | 818 | ||
@@ -827,7 +825,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
827 | 825 | ||
828 | serverUrl: environment.apiUrl, | 826 | serverUrl: environment.apiUrl, |
829 | 827 | ||
830 | videoCaptions: playerCaptions | 828 | videoCaptions: playerCaptions, |
829 | |||
830 | videoUUID: video.uuid | ||
831 | }, | 831 | }, |
832 | 832 | ||
833 | webtorrent: { | 833 | webtorrent: { |
@@ -867,24 +867,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
867 | return { playerMode: mode, playerOptions: options } | 867 | return { playerMode: mode, playerOptions: options } |
868 | } | 868 | } |
869 | 869 | ||
870 | private pausePlayer () { | ||
871 | if (!this.player) return | ||
872 | |||
873 | this.player.pause() | ||
874 | } | ||
875 | |||
876 | private resumePlayer () { | ||
877 | if (!this.player) return | ||
878 | |||
879 | this.player.play() | ||
880 | } | ||
881 | |||
882 | private isPlaying () { | ||
883 | if (!this.player) return | ||
884 | |||
885 | return !this.player.paused() | ||
886 | } | ||
887 | |||
888 | private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) { | 870 | private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) { |
889 | if (!this.liveVideosSub) { | 871 | if (!this.liveVideosSub) { |
890 | this.liveVideosSub = this.buildLiveEventsSubscription() | 872 | this.liveVideosSub = this.buildLiveEventsSubscription() |
diff --git a/client/src/app/+videos/+video-watch/video-watch.module.ts b/client/src/app/+videos/+video-watch/video-watch.module.ts index fbda9b9c4..3e9f3822e 100644 --- a/client/src/app/+videos/+video-watch/video-watch.module.ts +++ b/client/src/app/+videos/+video-watch/video-watch.module.ts | |||
@@ -4,6 +4,7 @@ import { SharedGlobalIconModule } from '@app/shared/shared-icons' | |||
4 | import { SharedMainModule } from '@app/shared/shared-main' | 4 | import { SharedMainModule } from '@app/shared/shared-main' |
5 | import { SharedModerationModule } from '@app/shared/shared-moderation' | 5 | import { SharedModerationModule } from '@app/shared/shared-moderation' |
6 | import { SharedShareModal } from '@app/shared/shared-share-modal' | 6 | import { SharedShareModal } from '@app/shared/shared-share-modal' |
7 | import { SharedSupportModal } from '@app/shared/shared-support-modal' | ||
7 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' | 8 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' |
8 | import { SharedVideoModule } from '@app/shared/shared-video' | 9 | import { SharedVideoModule } from '@app/shared/shared-video' |
9 | import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' | 10 | import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' |
@@ -13,9 +14,9 @@ import { VideoCommentService } from '../../shared/shared-video-comment/video-com | |||
13 | import { VideoCommentAddComponent } from './comment/video-comment-add.component' | 14 | import { VideoCommentAddComponent } from './comment/video-comment-add.component' |
14 | import { VideoCommentComponent } from './comment/video-comment.component' | 15 | import { VideoCommentComponent } from './comment/video-comment.component' |
15 | import { VideoCommentsComponent } from './comment/video-comments.component' | 16 | import { VideoCommentsComponent } from './comment/video-comments.component' |
16 | import { VideoSupportComponent } from './modal/video-support.component' | ||
17 | import { RecommendationsModule } from './recommendations/recommendations.module' | 17 | import { RecommendationsModule } from './recommendations/recommendations.module' |
18 | import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive' | 18 | import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive' |
19 | import { VideoAvatarChannelComponent } from './video-avatar-channel.component' | ||
19 | import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' | 20 | import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' |
20 | import { VideoWatchRoutingModule } from './video-watch-routing.module' | 21 | import { VideoWatchRoutingModule } from './video-watch-routing.module' |
21 | import { VideoWatchComponent } from './video-watch.component' | 22 | import { VideoWatchComponent } from './video-watch.component' |
@@ -34,18 +35,20 @@ import { VideoWatchComponent } from './video-watch.component' | |||
34 | SharedGlobalIconModule, | 35 | SharedGlobalIconModule, |
35 | SharedVideoCommentModule, | 36 | SharedVideoCommentModule, |
36 | SharedShareModal, | 37 | SharedShareModal, |
37 | SharedVideoModule | 38 | SharedVideoModule, |
39 | SharedSupportModal | ||
38 | ], | 40 | ], |
39 | 41 | ||
40 | declarations: [ | 42 | declarations: [ |
41 | VideoWatchComponent, | 43 | VideoWatchComponent, |
42 | VideoWatchPlaylistComponent, | 44 | VideoWatchPlaylistComponent, |
43 | 45 | ||
44 | VideoSupportComponent, | ||
45 | VideoCommentsComponent, | 46 | VideoCommentsComponent, |
46 | VideoCommentAddComponent, | 47 | VideoCommentAddComponent, |
47 | VideoCommentComponent, | 48 | VideoCommentComponent, |
48 | 49 | ||
50 | VideoAvatarChannelComponent, | ||
51 | |||
49 | TimestampRouteTransformerDirective, | 52 | TimestampRouteTransformerDirective, |
50 | TimestampRouteTransformerDirective | 53 | TimestampRouteTransformerDirective |
51 | ], | 54 | ], |
diff --git a/client/src/app/+videos/video-list/overview/video-overview.component.html b/client/src/app/+videos/video-list/overview/video-overview.component.html index ca986c634..639a96c43 100644 --- a/client/src/app/+videos/video-list/overview/video-overview.component.html +++ b/client/src/app/+videos/video-list/overview/video-overview.component.html | |||
@@ -14,7 +14,7 @@ | |||
14 | </h1> | 14 | </h1> |
15 | 15 | ||
16 | <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> | 16 | <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> |
17 | <my-video-miniature [video]="video" [fitWidth]="true" [user]="userMiniature" [displayVideoActions]="true"> | 17 | <my-video-miniature [video]="video" [user]="userMiniature" [displayVideoActions]="true"> |
18 | </my-video-miniature> | 18 | </my-video-miniature> |
19 | </div> | 19 | </div> |
20 | </div> | 20 | </div> |
@@ -25,7 +25,7 @@ | |||
25 | </h2> | 25 | </h2> |
26 | 26 | ||
27 | <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> | 27 | <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> |
28 | <my-video-miniature [video]="video" [fitWidth]="true" [user]="userMiniature" [displayVideoActions]="true"> | 28 | <my-video-miniature [video]="video" [user]="userMiniature" [displayVideoActions]="true"> |
29 | </my-video-miniature> | 29 | </my-video-miniature> |
30 | </div> | 30 | </div> |
31 | </div> | 31 | </div> |
@@ -40,7 +40,7 @@ | |||
40 | </div> | 40 | </div> |
41 | 41 | ||
42 | <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> | 42 | <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> |
43 | <my-video-miniature [video]="video" [fitWidth]="true" [user]="userMiniature" [displayVideoActions]="true"> | 43 | <my-video-miniature [video]="video" [user]="userMiniature" [displayVideoActions]="true"> |
44 | </my-video-miniature> | 44 | </my-video-miniature> |
45 | </div> | 45 | </div> |
46 | </div> | 46 | </div> |
diff --git a/client/src/app/+videos/video-list/overview/video-overview.component.scss b/client/src/app/+videos/video-list/overview/video-overview.component.scss index c1d10188a..ec73c628c 100644 --- a/client/src/app/+videos/video-list/overview/video-overview.component.scss +++ b/client/src/app/+videos/video-list/overview/video-overview.component.scss | |||
@@ -8,9 +8,84 @@ | |||
8 | } | 8 | } |
9 | 9 | ||
10 | .margin-content { | 10 | .margin-content { |
11 | @include fluid-videos-miniature-layout; | 11 | @include grid-videos-miniature-layout; |
12 | } | 12 | } |
13 | 13 | ||
14 | .section { | 14 | .section { |
15 | @include miniature-rows; | 15 | &:first-child { |
16 | padding-top: 30px; | ||
17 | |||
18 | .section-title { | ||
19 | border-top: none !important; | ||
20 | } | ||
21 | } | ||
22 | |||
23 | .section-title { | ||
24 | font-size: 24px; | ||
25 | font-weight: $font-semibold; | ||
26 | padding-top: 15px; | ||
27 | margin-bottom: 15px; | ||
28 | display: flex; | ||
29 | justify-content: space-between; | ||
30 | |||
31 | &:not(h2) { | ||
32 | border-top: 1px solid $separator-border-color; | ||
33 | } | ||
34 | |||
35 | a { | ||
36 | &:hover, &:focus:not(.focus-visible), &:active { | ||
37 | text-decoration: none; | ||
38 | outline: none; | ||
39 | } | ||
40 | |||
41 | color: pvar(--mainForegroundColor); | ||
42 | } | ||
43 | } | ||
44 | |||
45 | &.channel { | ||
46 | .section-title { | ||
47 | a { | ||
48 | display: flex; | ||
49 | width: fit-content; | ||
50 | align-items: center; | ||
51 | |||
52 | img { | ||
53 | @include channel-avatar(28px); | ||
54 | |||
55 | margin-right: 8px; | ||
56 | } | ||
57 | } | ||
58 | |||
59 | .followers { | ||
60 | color: pvar(--greyForegroundColor); | ||
61 | font-weight: normal; | ||
62 | font-size: 14px; | ||
63 | margin-left: 10px; | ||
64 | position: relative; | ||
65 | top: 2px; | ||
66 | } | ||
67 | } | ||
68 | } | ||
69 | |||
70 | .show-more { | ||
71 | position: relative; | ||
72 | top: -5px; | ||
73 | display: inline-block; | ||
74 | font-size: 16px; | ||
75 | text-transform: uppercase; | ||
76 | color: pvar(--greyForegroundColor); | ||
77 | margin-bottom: 10px; | ||
78 | font-weight: $font-semibold; | ||
79 | text-decoration: none; | ||
80 | } | ||
81 | |||
82 | @media screen and (max-width: $mobile-view) { | ||
83 | max-height: initial; | ||
84 | overflow: initial; | ||
85 | |||
86 | .section-title { | ||
87 | font-size: 17px; | ||
88 | margin-left: 10px; | ||
89 | } | ||
90 | } | ||
16 | } | 91 | } |
diff --git a/client/src/app/+videos/video-list/video-user-subscriptions.component.ts b/client/src/app/+videos/video-list/video-user-subscriptions.component.ts index e352a2b2c..6aabb93a5 100644 --- a/client/src/app/+videos/video-list/video-user-subscriptions.component.ts +++ b/client/src/app/+videos/video-list/video-user-subscriptions.component.ts | |||
@@ -7,7 +7,7 @@ import { HooksService } from '@app/core/plugins/hooks.service' | |||
7 | import { immutableAssign } from '@app/helpers' | 7 | import { immutableAssign } from '@app/helpers' |
8 | import { VideoService } from '@app/shared/shared-main' | 8 | import { VideoService } from '@app/shared/shared-main' |
9 | import { UserSubscriptionService } from '@app/shared/shared-user-subscription' | 9 | import { UserSubscriptionService } from '@app/shared/shared-user-subscription' |
10 | import { AbstractVideoList, OwnerDisplayType } from '@app/shared/shared-video-miniature' | 10 | import { AbstractVideoList } from '@app/shared/shared-video-miniature' |
11 | import { FeedFormat, VideoSortField } from '@shared/models' | 11 | import { FeedFormat, VideoSortField } from '@shared/models' |
12 | import { environment } from '../../../environments/environment' | 12 | import { environment } from '../../../environments/environment' |
13 | import { copyToClipboard } from '../../../root-helpers/utils' | 13 | import { copyToClipboard } from '../../../root-helpers/utils' |
@@ -20,7 +20,6 @@ import { copyToClipboard } from '../../../root-helpers/utils' | |||
20 | export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy { | 20 | export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy { |
21 | titlePage: string | 21 | titlePage: string |
22 | sort = '-publishedAt' as VideoSortField | 22 | sort = '-publishedAt' as VideoSortField |
23 | ownerDisplayType: OwnerDisplayType = 'auto' | ||
24 | groupByDate = true | 23 | groupByDate = true |
25 | 24 | ||
26 | constructor ( | 25 | constructor ( |
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index e8447719a..42293e412 100644 --- a/client/src/app/app.component.scss +++ b/client/src/app/app.component.scss | |||
@@ -43,6 +43,10 @@ $assets-path: '../assets'; | |||
43 | background-color: pvar(--mainForegroundColor); | 43 | background-color: pvar(--mainForegroundColor); |
44 | mask-image: url('#{$assets-path}/images/misc/menu.svg'); | 44 | mask-image: url('#{$assets-path}/images/misc/menu.svg'); |
45 | margin: 0 18px 0 20px; | 45 | margin: 0 18px 0 20px; |
46 | |||
47 | @media screen and (max-width: $mobile-view) { | ||
48 | margin: 0 10px; | ||
49 | } | ||
46 | } | 50 | } |
47 | } | 51 | } |
48 | 52 | ||
@@ -71,16 +75,11 @@ $assets-path: '../assets'; | |||
71 | } | 75 | } |
72 | 76 | ||
73 | @media screen and (max-width: $mobile-view) { | 77 | @media screen and (max-width: $mobile-view) { |
74 | width: 70px; | ||
75 | 78 | ||
76 | .peertube-title { | 79 | .peertube-title { |
77 | display: none; | 80 | display: none; |
78 | } | 81 | } |
79 | } | 82 | } |
80 | |||
81 | @media screen and (max-width: 350px) { | ||
82 | flex: auto; | ||
83 | } | ||
84 | } | 83 | } |
85 | 84 | ||
86 | .header-right { | 85 | .header-right { |
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' | |||
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { PluginService } from '@app/core/plugins/plugin.service' | 4 | import { PluginService } from '@app/core/plugins/plugin.service' |
5 | import { ClientActionHookName, ClientFilterHookName, PluginClientScope } from '@shared/models' | 5 | import { ClientActionHookName, ClientFilterHookName, PluginClientScope } from '@shared/models' |
6 | import { AuthService, AuthStatus } from '../auth' | ||
6 | 7 | ||
7 | type RawFunction<U, T> = (params: U) => T | 8 | type RawFunction<U, T> = (params: U) => T |
8 | type ObservableFunction<U, T> = RawFunction<U, Observable<T>> | 9 | type ObservableFunction<U, T> = RawFunction<U, Observable<T>> |
9 | 10 | ||
10 | @Injectable() | 11 | @Injectable() |
11 | export class HooksService { | 12 | export 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/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 11288fc54..906191ae1 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts | |||
@@ -98,6 +98,12 @@ export class ServerService { | |||
98 | extensions: [] | 98 | extensions: [] |
99 | } | 99 | } |
100 | }, | 100 | }, |
101 | banner: { | ||
102 | file: { | ||
103 | size: { max: 0 }, | ||
104 | extensions: [] | ||
105 | } | ||
106 | }, | ||
101 | video: { | 107 | video: { |
102 | image: { | 108 | image: { |
103 | size: { max: 0 }, | 109 | size: { max: 0 }, |
diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts index 15a4f7f82..8aaaa238d 100644 --- a/client/src/app/core/users/user.model.ts +++ b/client/src/app/core/users/user.model.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Account } from '@app/shared/shared-main/account/account.model' | 1 | import { Account } from '@app/shared/shared-main/account/account.model' |
2 | import { hasUserRight } from '@shared/core-utils/users' | 2 | import { hasUserRight } from '@shared/core-utils/users' |
3 | import { | 3 | import { |
4 | Avatar, | 4 | ActorImage, |
5 | NSFWPolicyType, | 5 | NSFWPolicyType, |
6 | User as UserServerModel, | 6 | User as UserServerModel, |
7 | UserAdminFlag, | 7 | UserAdminFlag, |
@@ -131,7 +131,7 @@ export class User implements UserServerModel { | |||
131 | } | 131 | } |
132 | } | 132 | } |
133 | 133 | ||
134 | updateAccountAvatar (newAccountAvatar?: Avatar) { | 134 | updateAccountAvatar (newAccountAvatar?: ActorImage) { |
135 | if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar) | 135 | if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar) |
136 | else this.account.resetAvatar() | 136 | else this.account.resetAvatar() |
137 | } | 137 | } |
diff --git a/client/src/app/core/users/user.service.ts b/client/src/app/core/users/user.service.ts index 33cc1f668..3de83152c 100644 --- a/client/src/app/core/users/user.service.ts +++ b/client/src/app/core/users/user.service.ts | |||
@@ -7,8 +7,7 @@ import { AuthService } from '@app/core/auth' | |||
7 | import { getBytes } from '@root-helpers/bytes' | 7 | import { getBytes } from '@root-helpers/bytes' |
8 | import { UserLocalStorageKeys } from '@root-helpers/users' | 8 | import { UserLocalStorageKeys } from '@root-helpers/users' |
9 | import { | 9 | import { |
10 | Avatar, | 10 | ActorImage, |
11 | NSFWPolicyType, | ||
12 | ResultList, | 11 | ResultList, |
13 | User as UserServerModel, | 12 | User as UserServerModel, |
14 | UserCreate, | 13 | UserCreate, |
@@ -136,7 +135,7 @@ export class UserService { | |||
136 | changeAvatar (avatarForm: FormData) { | 135 | changeAvatar (avatarForm: FormData) { |
137 | const url = UserService.BASE_USERS_URL + 'me/avatar/pick' | 136 | const url = UserService.BASE_USERS_URL + 'me/avatar/pick' |
138 | 137 | ||
139 | return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm) | 138 | return this.authHttp.post<{ avatar: ActorImage }>(url, avatarForm) |
140 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 139 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
141 | } | 140 | } |
142 | 141 | ||
diff --git a/client/src/app/core/wrappers/screen.service.ts b/client/src/app/core/wrappers/screen.service.ts index a085e5bdc..c133b5fe9 100644 --- a/client/src/app/core/wrappers/screen.service.ts +++ b/client/src/app/core/wrappers/screen.service.ts | |||
@@ -38,11 +38,10 @@ export class ScreenService { | |||
38 | 38 | ||
39 | let numberOfVideos = 1 | 39 | let numberOfVideos = 1 |
40 | 40 | ||
41 | if (screenWidth > 1850) numberOfVideos = 7 | 41 | if (screenWidth > 1850) numberOfVideos = 5 |
42 | else if (screenWidth > 1600) numberOfVideos = 6 | 42 | else if (screenWidth > 1600) numberOfVideos = 4 |
43 | else if (screenWidth > 1370) numberOfVideos = 5 | 43 | else if (screenWidth > 1370) numberOfVideos = 3 |
44 | else if (screenWidth > 1100) numberOfVideos = 4 | 44 | else if (screenWidth > 1100) numberOfVideos = 2 |
45 | else if (screenWidth > 850) numberOfVideos = 3 | ||
46 | 45 | ||
47 | return numberOfVideos | 46 | return numberOfVideos |
48 | } | 47 | } |
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/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss index f8d68e986..a60aa38d6 100644 --- a/client/src/app/header/search-typeahead.component.scss +++ b/client/src/app/header/search-typeahead.component.scss | |||
@@ -86,7 +86,7 @@ li.suggestion { | |||
86 | flex: 1; | 86 | flex: 1; |
87 | 87 | ||
88 | input { | 88 | input { |
89 | width: unset; | 89 | width: 70px; |
90 | } | 90 | } |
91 | } | 91 | } |
92 | 92 | ||
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/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index ed20d9c01..9b6b7cda5 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts | |||
@@ -10,6 +10,7 @@ import { LanguageChooserComponent } from '@app/menu/language-chooser.component' | |||
10 | import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component' | 10 | import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component' |
11 | import { ServerConfig, UserRight, VideoConstant } from '@shared/models' | 11 | import { ServerConfig, UserRight, VideoConstant } from '@shared/models' |
12 | import { NgbDropdown, NgbDropdownConfig } from '@ng-bootstrap/ng-bootstrap' | 12 | import { NgbDropdown, NgbDropdownConfig } from '@ng-bootstrap/ng-bootstrap' |
13 | import { PeertubeModalService } from '@app/shared/shared-main/peertube-modal/peertube-modal.service' | ||
13 | 14 | ||
14 | const logger = debug('peertube:menu:MenuComponent') | 15 | const logger = debug('peertube:menu:MenuComponent') |
15 | 16 | ||
@@ -54,6 +55,7 @@ export class MenuComponent implements OnInit { | |||
54 | private hotkeysService: HotkeysService, | 55 | private hotkeysService: HotkeysService, |
55 | private screenService: ScreenService, | 56 | private screenService: ScreenService, |
56 | private menuService: MenuService, | 57 | private menuService: MenuService, |
58 | private modalService: PeertubeModalService, | ||
57 | private dropdownConfig: NgbDropdownConfig, | 59 | private dropdownConfig: NgbDropdownConfig, |
58 | private router: Router | 60 | private router: Router |
59 | ) { | 61 | ) { |
@@ -130,6 +132,9 @@ export class MenuComponent implements OnInit { | |||
130 | this.authService.userInformationLoaded | 132 | this.authService.userInformationLoaded |
131 | .subscribe(() => this.buildUserLanguages()) | 133 | .subscribe(() => this.buildUserLanguages()) |
132 | }) | 134 | }) |
135 | |||
136 | this.modalService.openQuickSettingsSubject | ||
137 | .subscribe(() => this.openQuickSettings()) | ||
133 | } | 138 | } |
134 | 139 | ||
135 | isRegistrationAllowed () { | 140 | isRegistrationAllowed () { |
diff --git a/client/src/app/menu/notification.component.scss b/client/src/app/menu/notification.component.scss index 40feb9e66..c65787779 100644 --- a/client/src/app/menu/notification.component.scss +++ b/client/src/app/menu/notification.component.scss | |||
@@ -1,6 +1,9 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .content { | ||
5 | scrollbar-color: auto; | ||
6 | } | ||
4 | 7 | ||
5 | .notification-inbox-popover { | 8 | .notification-inbox-popover { |
6 | padding: 10px; | 9 | padding: 10px; |
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-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts index e34836a18..eeb9f128b 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts | |||
@@ -117,7 +117,8 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV | |||
117 | warningTitle: false, | 117 | warningTitle: false, |
118 | startTime: abuse.video.startAt, | 118 | startTime: abuse.video.startAt, |
119 | stopTime: abuse.video.endAt | 119 | stopTime: abuse.video.endAt |
120 | }) | 120 | }), |
121 | abuse.video.name | ||
121 | ) | 122 | ) |
122 | } | 123 | } |
123 | 124 | ||
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html new file mode 100644 index 000000000..0829263f4 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html | |||
@@ -0,0 +1,41 @@ | |||
1 | <div class="actor" *ngIf="actor"> | ||
2 | <div class="d-flex"> | ||
3 | <img [ngClass]="{ channel: isChannel() }" [src]="preview || actor.avatarUrl" alt="Avatar" /> | ||
4 | |||
5 | <div class="actor-img-edit-container"> | ||
6 | |||
7 | <div *ngIf="editable && !hasAvatar()" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body"> | ||
8 | <my-global-icon iconName="upload"></my-global-icon> | ||
9 | <label class="sr-only" for="avatarfile" i18n>Upload a new avatar</label> | ||
10 | <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/> | ||
11 | </div> | ||
12 | |||
13 | <div | ||
14 | *ngIf="editable && hasAvatar()" class="actor-img-edit-button" | ||
15 | #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-image-info" autoClose="outside" placement="right" | ||
16 | > | ||
17 | <my-global-icon iconName="edit"></my-global-icon> | ||
18 | <label class="sr-only" for="avatarMenu" i18n>Change your avatar</label> | ||
19 | </div> | ||
20 | |||
21 | </div> | ||
22 | </div> | ||
23 | |||
24 | <div class="actor-info"> | ||
25 | <div class="actor-info-display-name">{{ actor.displayName }}</div> | ||
26 | <div *ngIf="displayUsername" class="actor-info-username">{{ actor.name }}</div> | ||
27 | <div *ngIf="displaySubscribers" i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div> | ||
28 | </div> | ||
29 | </div> | ||
30 | |||
31 | <ng-template #avatarEditContent> | ||
32 | <div class="dropdown-item c-hand" [ngbTooltip]="avatarFormat" placement="right" container="body"> | ||
33 | <my-global-icon iconName="upload"></my-global-icon> | ||
34 | <span for="avatarfile" i18n>Upload a new avatar</span> | ||
35 | <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/> | ||
36 | </div> | ||
37 | <div class="dropdown-item c-hand" (click)="deleteAvatar()" (key.enter)="deleteAvatar()"> | ||
38 | <my-global-icon iconName="delete"></my-global-icon> | ||
39 | <span i18n>Remove avatar</span> | ||
40 | </div> | ||
41 | </ng-template> | ||
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss new file mode 100644 index 000000000..8b0172315 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss | |||
@@ -0,0 +1,54 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .actor { | ||
5 | display: flex; | ||
6 | |||
7 | img { | ||
8 | margin-right: 15px; | ||
9 | |||
10 | &:not(.channel) { | ||
11 | @include avatar(100px); | ||
12 | } | ||
13 | |||
14 | &.channel { | ||
15 | @include channel-avatar(100px); | ||
16 | } | ||
17 | } | ||
18 | |||
19 | .actor-info { | ||
20 | display: inline-flex; | ||
21 | flex-direction: column; | ||
22 | |||
23 | .actor-info-display-name { | ||
24 | font-size: 20px; | ||
25 | font-weight: $font-bold; | ||
26 | |||
27 | @media screen and (max-width: $small-view) { | ||
28 | font-size: 16px; | ||
29 | } | ||
30 | } | ||
31 | |||
32 | .actor-info-username { | ||
33 | position: relative; | ||
34 | font-size: 14px; | ||
35 | color: pvar(--greyForegroundColor); | ||
36 | } | ||
37 | |||
38 | .actor-info-followers { | ||
39 | font-size: 15px; | ||
40 | padding-bottom: .5rem; | ||
41 | } | ||
42 | } | ||
43 | } | ||
44 | |||
45 | .actor-img-edit-container { | ||
46 | position: relative; | ||
47 | width: 0; | ||
48 | } | ||
49 | |||
50 | .actor-img-edit-button { | ||
51 | top: 55px; | ||
52 | right: 45px; | ||
53 | border-radius: 50%; | ||
54 | } | ||
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts index b459c591f..d0d269489 100644 --- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts +++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts | |||
@@ -1,21 +1,27 @@ | |||
1 | import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' | ||
2 | import { Notifier, ServerService } from '@app/core' | 3 | import { Notifier, ServerService } from '@app/core' |
4 | import { Account, VideoChannel } from '@app/shared/shared-main' | ||
3 | import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' | 5 | import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' |
4 | import { getBytes } from '@root-helpers/bytes' | 6 | import { getBytes } from '@root-helpers/bytes' |
5 | import { Account } from '../account/account.model' | ||
6 | import { VideoChannel } from '../video-channel/video-channel.model' | ||
7 | import { Actor } from './actor.model' | ||
8 | 7 | ||
9 | @Component({ | 8 | @Component({ |
10 | selector: 'my-actor-avatar-info', | 9 | selector: 'my-actor-avatar-edit', |
11 | templateUrl: './actor-avatar-info.component.html', | 10 | templateUrl: './actor-avatar-edit.component.html', |
12 | styleUrls: [ './actor-avatar-info.component.scss' ] | 11 | styleUrls: [ |
12 | './actor-image-edit.scss', | ||
13 | './actor-avatar-edit.component.scss' | ||
14 | ] | ||
13 | }) | 15 | }) |
14 | export class ActorAvatarInfoComponent implements OnInit, OnChanges { | 16 | export class ActorAvatarEditComponent implements OnInit { |
15 | @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement> | 17 | @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement> |
16 | @ViewChild('avatarPopover') avatarPopover: NgbPopover | 18 | @ViewChild('avatarPopover') avatarPopover: NgbPopover |
17 | 19 | ||
18 | @Input() actor: VideoChannel | Account | 20 | @Input() actor: VideoChannel | Account |
21 | @Input() editable = true | ||
22 | @Input() displaySubscribers = true | ||
23 | @Input() displayUsername = true | ||
24 | @Input() previewImage = false | ||
19 | 25 | ||
20 | @Output() avatarChange = new EventEmitter<FormData>() | 26 | @Output() avatarChange = new EventEmitter<FormData>() |
21 | @Output() avatarDelete = new EventEmitter<void>() | 27 | @Output() avatarDelete = new EventEmitter<void>() |
@@ -24,9 +30,10 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges { | |||
24 | maxAvatarSize = 0 | 30 | maxAvatarSize = 0 |
25 | avatarExtensions = '' | 31 | avatarExtensions = '' |
26 | 32 | ||
27 | private avatarUrl: string | 33 | preview: SafeResourceUrl |
28 | 34 | ||
29 | constructor ( | 35 | constructor ( |
36 | private sanitizer: DomSanitizer, | ||
30 | private serverService: ServerService, | 37 | private serverService: ServerService, |
31 | private notifier: Notifier | 38 | private notifier: Notifier |
32 | ) { } | 39 | ) { } |
@@ -42,12 +49,6 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges { | |||
42 | }) | 49 | }) |
43 | } | 50 | } |
44 | 51 | ||
45 | ngOnChanges (changes: SimpleChanges) { | ||
46 | if (changes['actor']) { | ||
47 | this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.actor) | ||
48 | } | ||
49 | } | ||
50 | |||
51 | onAvatarChange (input: HTMLInputElement) { | 52 | onAvatarChange (input: HTMLInputElement) { |
52 | this.avatarfileInput = new ElementRef(input) | 53 | this.avatarfileInput = new ElementRef(input) |
53 | 54 | ||
@@ -61,13 +62,22 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges { | |||
61 | formData.append('avatarfile', avatarfile) | 62 | formData.append('avatarfile', avatarfile) |
62 | this.avatarPopover?.close() | 63 | this.avatarPopover?.close() |
63 | this.avatarChange.emit(formData) | 64 | this.avatarChange.emit(formData) |
65 | |||
66 | if (this.previewImage) { | ||
67 | this.preview = this.sanitizer.bypassSecurityTrustResourceUrl(URL.createObjectURL(avatarfile)) | ||
68 | } | ||
64 | } | 69 | } |
65 | 70 | ||
66 | deleteAvatar () { | 71 | deleteAvatar () { |
72 | this.preview = undefined | ||
67 | this.avatarDelete.emit() | 73 | this.avatarDelete.emit() |
68 | } | 74 | } |
69 | 75 | ||
70 | hasAvatar () { | 76 | hasAvatar () { |
71 | return !!this.avatarUrl | 77 | return !!this.preview || !!this.actor.avatar |
78 | } | ||
79 | |||
80 | isChannel () { | ||
81 | return !!(this.actor as VideoChannel).ownerAccount | ||
72 | } | 82 | } |
73 | } | 83 | } |
diff --git a/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html new file mode 100644 index 000000000..266fc26c5 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html | |||
@@ -0,0 +1,34 @@ | |||
1 | <div class="actor" *ngIf="actor"> | ||
2 | <div class="actor-img-edit-container"> | ||
3 | <div class="banner-placeholder"> | ||
4 | <img *ngIf="hasBanner()" [src]="preview || actor.bannerUrl" alt="Banner" /> | ||
5 | </div> | ||
6 | |||
7 | <div *ngIf="!hasBanner()" class="actor-img-edit-button" [ngbTooltip]="bannerFormat" placement="right" container="body"> | ||
8 | <my-global-icon iconName="upload"></my-global-icon> | ||
9 | <label for="bannerfile" i18n>Upload a new banner</label> | ||
10 | <input #bannerfileInput type="file" name="bannerfile" id="bannerfile" [accept]="bannerExtensions" (change)="onBannerChange(bannerfileInput)"/> | ||
11 | </div> | ||
12 | |||
13 | <div | ||
14 | *ngIf="hasBanner()" class="actor-img-edit-button" | ||
15 | #bannerPopover="ngbPopover" [ngbPopover]="bannerEditContent" popoverClass="popover-image-info" autoClose="outside" placement="right" | ||
16 | > | ||
17 | <my-global-icon iconName="edit"></my-global-icon> | ||
18 | <label for="bannerMenu" i18n>Change your banner</label> | ||
19 | </div> | ||
20 | </div> | ||
21 | </div> | ||
22 | |||
23 | <ng-template #bannerEditContent> | ||
24 | <div class="dropdown-item c-hand" [ngbTooltip]="bannerFormat" placement="right" container="body"> | ||
25 | <my-global-icon iconName="upload"></my-global-icon> | ||
26 | <span for="bannerfile" i18n>Upload a new banner</span> | ||
27 | <input #bannerfileInput type="file" name="bannerfile" id="bannerfile" [accept]="bannerExtensions" (change)="onBannerChange(bannerfileInput)"/> | ||
28 | </div> | ||
29 | |||
30 | <div class="dropdown-item c-hand" (click)="deleteBanner()" (key.enter)="deleteBanner()"> | ||
31 | <my-global-icon iconName="delete"></my-global-icon> | ||
32 | <span i18n>Remove banner</span> | ||
33 | </div> | ||
34 | </ng-template> | ||
diff --git a/client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss new file mode 100644 index 000000000..23606f871 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss | |||
@@ -0,0 +1,27 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .banner-placeholder { | ||
5 | @include block-ratio('> div, > img', $banner-inverted-ratio); | ||
6 | } | ||
7 | |||
8 | .banner-placeholder { | ||
9 | background-color: pvar(--greyBackgroundColor); | ||
10 | } | ||
11 | |||
12 | .actor-img-edit-container { | ||
13 | position: relative; | ||
14 | display: flex; | ||
15 | justify-content: center; | ||
16 | align-items: center; | ||
17 | } | ||
18 | |||
19 | .actor-img-edit-button { | ||
20 | position: absolute; | ||
21 | width: auto; | ||
22 | |||
23 | label { | ||
24 | font-weight: $font-semibold; | ||
25 | margin-bottom: 0; | ||
26 | } | ||
27 | } | ||
diff --git a/client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts new file mode 100644 index 000000000..8c12d3c4c --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts | |||
@@ -0,0 +1,76 @@ | |||
1 | import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core' | ||
2 | import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' | ||
3 | import { Notifier, ServerService } from '@app/core' | ||
4 | import { VideoChannel } from '@app/shared/shared-main' | ||
5 | import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' | ||
6 | import { getBytes } from '@root-helpers/bytes' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-actor-banner-edit', | ||
10 | templateUrl: './actor-banner-edit.component.html', | ||
11 | styleUrls: [ | ||
12 | './actor-image-edit.scss', | ||
13 | './actor-banner-edit.component.scss' | ||
14 | ] | ||
15 | }) | ||
16 | export class ActorBannerEditComponent implements OnInit { | ||
17 | @ViewChild('bannerfileInput') bannerfileInput: ElementRef<HTMLInputElement> | ||
18 | @ViewChild('bannerPopover') bannerPopover: NgbPopover | ||
19 | |||
20 | @Input() actor: VideoChannel | ||
21 | @Input() previewImage = false | ||
22 | |||
23 | @Output() bannerChange = new EventEmitter<FormData>() | ||
24 | @Output() bannerDelete = new EventEmitter<void>() | ||
25 | |||
26 | bannerFormat = '' | ||
27 | maxBannerSize = 0 | ||
28 | bannerExtensions = '' | ||
29 | |||
30 | preview: SafeResourceUrl | ||
31 | |||
32 | constructor ( | ||
33 | private sanitizer: DomSanitizer, | ||
34 | private serverService: ServerService, | ||
35 | private notifier: Notifier | ||
36 | ) { } | ||
37 | |||
38 | ngOnInit (): void { | ||
39 | this.serverService.getConfig() | ||
40 | .subscribe(config => { | ||
41 | this.maxBannerSize = config.banner.file.size.max | ||
42 | this.bannerExtensions = config.banner.file.extensions.join(', ') | ||
43 | |||
44 | // tslint:disable:max-line-length | ||
45 | this.bannerFormat = $localize`ratio 6/1, recommended size: 1600x266, max size: ${getBytes(this.maxBannerSize)}, extensions: ${this.bannerExtensions}` | ||
46 | }) | ||
47 | } | ||
48 | |||
49 | onBannerChange (input: HTMLInputElement) { | ||
50 | this.bannerfileInput = new ElementRef(input) | ||
51 | |||
52 | const bannerfile = this.bannerfileInput.nativeElement.files[ 0 ] | ||
53 | if (bannerfile.size > this.maxBannerSize) { | ||
54 | this.notifier.error('Error', $localize`This image is too large.`) | ||
55 | return | ||
56 | } | ||
57 | |||
58 | const formData = new FormData() | ||
59 | formData.append('bannerfile', bannerfile) | ||
60 | this.bannerPopover?.close() | ||
61 | this.bannerChange.emit(formData) | ||
62 | |||
63 | if (this.previewImage) { | ||
64 | this.preview = this.sanitizer.bypassSecurityTrustResourceUrl(URL.createObjectURL(bannerfile)) | ||
65 | } | ||
66 | } | ||
67 | |||
68 | deleteBanner () { | ||
69 | this.preview = undefined | ||
70 | this.bannerDelete.emit() | ||
71 | } | ||
72 | |||
73 | hasBanner () { | ||
74 | return !!this.preview || !!this.actor.bannerUrl | ||
75 | } | ||
76 | } | ||
diff --git a/client/src/app/shared/shared-actor-image/actor-image-edit.scss b/client/src/app/shared/shared-actor-image/actor-image-edit.scss new file mode 100644 index 000000000..918955a89 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-image-edit.scss | |||
@@ -0,0 +1,35 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .actor ::ng-deep .popover-image-info .popover-body { | ||
5 | padding: 0; | ||
6 | |||
7 | .dropdown-item { | ||
8 | padding: 6px 10px; | ||
9 | border-radius: 4px; | ||
10 | |||
11 | &:first-child { | ||
12 | @include peertube-file; | ||
13 | display: block; | ||
14 | } | ||
15 | } | ||
16 | } | ||
17 | |||
18 | .actor-img-edit-button { | ||
19 | @include peertube-button-file(21px); | ||
20 | @include button-with-icon(19px); | ||
21 | @include orange-button; | ||
22 | |||
23 | margin-top: 10px; | ||
24 | margin-bottom: 5px; | ||
25 | cursor: pointer; | ||
26 | |||
27 | input { | ||
28 | width: 30px; | ||
29 | height: 30px; | ||
30 | } | ||
31 | |||
32 | my-global-icon { | ||
33 | right: 7px; | ||
34 | } | ||
35 | } | ||
diff --git a/client/src/app/shared/shared-actor-image/index.ts b/client/src/app/shared/shared-actor-image/index.ts new file mode 100644 index 000000000..18a9038eb --- /dev/null +++ b/client/src/app/shared/shared-actor-image/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './shared-actor-image.module' | |||
diff --git a/client/src/app/shared/shared-actor-image/shared-actor-image.module.ts b/client/src/app/shared/shared-actor-image/shared-actor-image.module.ts new file mode 100644 index 000000000..6044f9925 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/shared-actor-image.module.ts | |||
@@ -0,0 +1,29 @@ | |||
1 | |||
2 | import { CommonModule } from '@angular/common' | ||
3 | import { NgModule } from '@angular/core' | ||
4 | import { SharedGlobalIconModule } from '../shared-icons' | ||
5 | import { SharedMainModule } from '../shared-main' | ||
6 | import { ActorAvatarEditComponent } from './actor-avatar-edit.component' | ||
7 | import { ActorBannerEditComponent } from './actor-banner-edit.component' | ||
8 | |||
9 | @NgModule({ | ||
10 | imports: [ | ||
11 | CommonModule, | ||
12 | |||
13 | SharedMainModule, | ||
14 | SharedGlobalIconModule | ||
15 | ], | ||
16 | |||
17 | declarations: [ | ||
18 | ActorAvatarEditComponent, | ||
19 | ActorBannerEditComponent | ||
20 | ], | ||
21 | |||
22 | exports: [ | ||
23 | ActorAvatarEditComponent, | ||
24 | ActorBannerEditComponent | ||
25 | ], | ||
26 | |||
27 | providers: [ ] | ||
28 | }) | ||
29 | export class SharedActorImageModule { } | ||
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/markdown-textarea.component.scss b/client/src/app/shared/shared-forms/markdown-textarea.component.scss index fcddfea03..8203c7d1c 100644 --- a/client/src/app/shared/shared-forms/markdown-textarea.component.scss +++ b/client/src/app/shared/shared-forms/markdown-textarea.component.scss | |||
@@ -131,7 +131,7 @@ $input-border-radius: 3px; | |||
131 | border-right: none; | 131 | border-right: none; |
132 | 132 | ||
133 | :last-child { | 133 | :last-child { |
134 | margin-right: $not-expanded-horizontal-margins; | 134 | margin-right: pvar(--horizontalMarginContent); |
135 | } | 135 | } |
136 | } | 136 | } |
137 | 137 | ||
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 @@ | |||
1 | import { Component, forwardRef, Input } from '@angular/core' | 1 | import { Component, forwardRef, HostListener, Input } from '@angular/core' |
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { SelectOptionsItem } from '../../../../types/select-options-item.model' | 3 | import { 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-instance/instance-about-accordion.component.scss b/client/src/app/shared/shared-instance/instance-about-accordion.component.scss index 275600d60..2f6b420e3 100644 --- a/client/src/app/shared/shared-instance/instance-about-accordion.component.scss +++ b/client/src/app/shared/shared-instance/instance-about-accordion.component.scss | |||
@@ -31,7 +31,7 @@ ngb-accordion ::ng-deep { | |||
31 | padding: 0; | 31 | padding: 0; |
32 | 32 | ||
33 | & + .collapse.show { | 33 | & + .collapse.show { |
34 | background-color: var(--submenuColor); | 34 | background-color: var(--submenuBackgroundColor); |
35 | } | 35 | } |
36 | } | 36 | } |
37 | } | 37 | } |
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.html b/client/src/app/shared/shared-instance/instance-features-table.component.html index ce2557147..d505b6739 100644 --- a/client/src/app/shared/shared-instance/instance-features-table.component.html +++ b/client/src/app/shared/shared-instance/instance-features-table.component.html | |||
@@ -11,7 +11,7 @@ | |||
11 | <tr> | 11 | <tr> |
12 | <th i18n class="label" scope="row"> | 12 | <th i18n class="label" scope="row"> |
13 | <div>Default NSFW/sensitive videos policy</div> | 13 | <div>Default NSFW/sensitive videos policy</div> |
14 | <div class="more-info">can be redefined by the users</div> | 14 | <div class="c-hand more-info" (click)="openQuickSettingsHighlight()">can be redefined by the users</div> |
15 | </th> | 15 | </th> |
16 | 16 | ||
17 | <td class="value">{{ buildNSFWLabel() }}</td> | 17 | <td class="value">{{ buildNSFWLabel() }}</td> |
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.ts b/client/src/app/shared/shared-instance/instance-features-table.component.ts index 0166157f9..c3b3dfdfd 100644 --- a/client/src/app/shared/shared-instance/instance-features-table.component.ts +++ b/client/src/app/shared/shared-instance/instance-features-table.component.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ServerService } from '@app/core' | 2 | import { ServerService } from '@app/core' |
3 | import { ServerConfig } from '@shared/models' | 3 | import { ServerConfig } from '@shared/models' |
4 | import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service' | ||
4 | 5 | ||
5 | @Component({ | 6 | @Component({ |
6 | selector: 'my-instance-features-table', | 7 | selector: 'my-instance-features-table', |
@@ -11,7 +12,10 @@ export class InstanceFeaturesTableComponent implements OnInit { | |||
11 | quotaHelpIndication = '' | 12 | quotaHelpIndication = '' |
12 | serverConfig: ServerConfig | 13 | serverConfig: ServerConfig |
13 | 14 | ||
14 | constructor (private serverService: ServerService) { } | 15 | constructor ( |
16 | private serverService: ServerService, | ||
17 | private modalService: PeertubeModalService | ||
18 | ) { } | ||
15 | 19 | ||
16 | get initialUserVideoQuota () { | 20 | get initialUserVideoQuota () { |
17 | return this.serverConfig.user.videoQuota | 21 | return this.serverConfig.user.videoQuota |
@@ -56,6 +60,10 @@ export class InstanceFeaturesTableComponent implements OnInit { | |||
56 | return this.serverService.getServerVersionAndCommit() | 60 | return this.serverService.getServerVersionAndCommit() |
57 | } | 61 | } |
58 | 62 | ||
63 | openQuickSettingsHighlight () { | ||
64 | this.modalService.openQuickSettingsSubject.next() | ||
65 | } | ||
66 | |||
59 | private getApproximateTime (seconds: number) { | 67 | private getApproximateTime (seconds: number) { |
60 | const hours = Math.floor(seconds / 3600) | 68 | const hours = Math.floor(seconds / 3600) |
61 | let pluralSuffix = '' | 69 | let pluralSuffix = '' |
diff --git a/client/src/app/shared/shared-main/account/account.model.ts b/client/src/app/shared/shared-main/account/account.model.ts index b71a893d1..17fddff09 100644 --- a/client/src/app/shared/shared-main/account/account.model.ts +++ b/client/src/app/shared/shared-main/account/account.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { Account as ServerAccount, Avatar } from '@shared/models' | 1 | import { Account as ServerAccount, ActorImage } from '@shared/models' |
2 | import { Actor } from './actor.model' | 2 | import { Actor } from './actor.model' |
3 | 3 | ||
4 | export class Account extends Actor implements ServerAccount { | 4 | export class Account extends Actor implements ServerAccount { |
@@ -38,7 +38,7 @@ export class Account extends Actor implements ServerAccount { | |||
38 | this.mutedServerByInstance = false | 38 | this.mutedServerByInstance = false |
39 | } | 39 | } |
40 | 40 | ||
41 | updateAvatar (newAvatar: Avatar) { | 41 | updateAvatar (newAvatar: ActorImage) { |
42 | this.avatar = newAvatar | 42 | this.avatar = newAvatar |
43 | 43 | ||
44 | this.updateComputedAttributes() | 44 | this.updateComputedAttributes() |
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.html b/client/src/app/shared/shared-main/account/actor-avatar-info.component.html deleted file mode 100644 index 30584fd00..000000000 --- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.html +++ /dev/null | |||
@@ -1,43 +0,0 @@ | |||
1 | <ng-container *ngIf="actor"> | ||
2 | <div class="actor"> | ||
3 | <div class="d-flex"> | ||
4 | <img [src]="actor.avatarUrl" alt="Avatar" /> | ||
5 | |||
6 | <div class="actor-img-edit-container"> | ||
7 | |||
8 | <div *ngIf="!hasAvatar()" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body"> | ||
9 | <my-global-icon iconName="upload"></my-global-icon> | ||
10 | <label class="sr-only" for="avatarfile" i18n>Upload a new avatar</label> | ||
11 | <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/> | ||
12 | </div> | ||
13 | |||
14 | <div *ngIf="hasAvatar()" class="actor-img-edit-button" #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-avatar-info" autoClose="outside" placement="right"> | ||
15 | <my-global-icon iconName="edit"></my-global-icon> | ||
16 | <label class="sr-only" for="avatarMenu" i18n>Change your avatar</label> | ||
17 | </div> | ||
18 | |||
19 | </div> | ||
20 | </div> | ||
21 | |||
22 | |||
23 | <div class="actor-info"> | ||
24 | <div class="actor-info-names"> | ||
25 | <div class="actor-info-display-name">{{ actor.displayName }}</div> | ||
26 | <div class="actor-info-username">{{ actor.name }}</div> | ||
27 | </div> | ||
28 | <div i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div> | ||
29 | </div> | ||
30 | </div> | ||
31 | </ng-container> | ||
32 | |||
33 | <ng-template #avatarEditContent> | ||
34 | <div class="dropdown-item c-hand" [ngbTooltip]="avatarFormat" placement="right" container="body"> | ||
35 | <my-global-icon iconName="upload"></my-global-icon> | ||
36 | <span for="avatarfile" i18n>Upload a new avatar</span> | ||
37 | <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/> | ||
38 | </div> | ||
39 | <div class="dropdown-item c-hand" (click)="deleteAvatar()" (key.enter)="deleteAvatar()"> | ||
40 | <my-global-icon iconName="delete"></my-global-icon> | ||
41 | <span i18n>Remove avatar</span> | ||
42 | </div> | ||
43 | </ng-template> | ||
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss b/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss deleted file mode 100644 index 57c298508..000000000 --- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss +++ /dev/null | |||
@@ -1,86 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .actor { | ||
5 | display: flex; | ||
6 | |||
7 | img { | ||
8 | @include avatar(100px); | ||
9 | |||
10 | margin-right: 15px; | ||
11 | } | ||
12 | |||
13 | .actor-img-edit-container { | ||
14 | position: relative; | ||
15 | width: 0; | ||
16 | |||
17 | .actor-img-edit-button { | ||
18 | @include peertube-button-file(21px); | ||
19 | @include button-with-icon(19px); | ||
20 | @include orange-button; | ||
21 | |||
22 | margin-top: 10px; | ||
23 | margin-bottom: 5px; | ||
24 | border-radius: 50%; | ||
25 | top: 55px; | ||
26 | right: 45px; | ||
27 | cursor: pointer; | ||
28 | |||
29 | input { | ||
30 | width: 30px; | ||
31 | height: 30px; | ||
32 | } | ||
33 | |||
34 | my-global-icon { | ||
35 | right: 7px; | ||
36 | } | ||
37 | } | ||
38 | } | ||
39 | |||
40 | .actor-info { | ||
41 | justify-content: center; | ||
42 | display: inline-flex; | ||
43 | flex-direction: column; | ||
44 | |||
45 | .actor-info-names { | ||
46 | display: flex; | ||
47 | align-items: center; | ||
48 | |||
49 | .actor-info-display-name { | ||
50 | font-size: 20px; | ||
51 | font-weight: $font-bold; | ||
52 | |||
53 | @media screen and (max-width: $small-view) { | ||
54 | font-size: 16px; | ||
55 | } | ||
56 | } | ||
57 | |||
58 | .actor-info-username { | ||
59 | margin-left: 7px; | ||
60 | position: relative; | ||
61 | top: 2px; | ||
62 | font-size: 14px; | ||
63 | color: $grey-actor-name; | ||
64 | } | ||
65 | } | ||
66 | |||
67 | .actor-info-followers { | ||
68 | font-size: 15px; | ||
69 | padding-bottom: .5rem; | ||
70 | } | ||
71 | } | ||
72 | } | ||
73 | |||
74 | .actor-img-edit-container ::ng-deep .popover-avatar-info .popover-body { | ||
75 | padding: 0; | ||
76 | |||
77 | .dropdown-item { | ||
78 | padding: 6px 10px; | ||
79 | border-radius: 4px; | ||
80 | |||
81 | &:first-child { | ||
82 | @include peertube-file; | ||
83 | display: block; | ||
84 | } | ||
85 | } | ||
86 | } | ||
diff --git a/client/src/app/shared/shared-main/account/actor.model.ts b/client/src/app/shared/shared-main/account/actor.model.ts index 8222c9769..1ee0c297e 100644 --- a/client/src/app/shared/shared-main/account/actor.model.ts +++ b/client/src/app/shared/shared-main/account/actor.model.ts | |||
@@ -1,17 +1,20 @@ | |||
1 | import { Actor as ActorServer, Avatar } from '@shared/models' | 1 | import { Actor as ActorServer, ActorImage } from '@shared/models' |
2 | import { getAbsoluteAPIUrl } from '@app/helpers' | 2 | import { getAbsoluteAPIUrl } from '@app/helpers' |
3 | 3 | ||
4 | export abstract class Actor implements ActorServer { | 4 | export abstract class Actor implements ActorServer { |
5 | id: number | 5 | id: number |
6 | url: string | ||
7 | name: string | 6 | name: string |
7 | |||
8 | host: string | 8 | host: string |
9 | url: string | ||
10 | |||
9 | followingCount: number | 11 | followingCount: number |
10 | followersCount: number | 12 | followersCount: number |
13 | |||
11 | createdAt: Date | string | 14 | createdAt: Date | string |
12 | updatedAt: Date | string | 15 | updatedAt: Date | string |
13 | avatar: Avatar | ||
14 | 16 | ||
17 | avatar: ActorImage | ||
15 | avatarUrl: string | 18 | avatarUrl: string |
16 | 19 | ||
17 | isLocal: boolean | 20 | isLocal: boolean |
@@ -24,6 +27,8 @@ export abstract class Actor implements ActorServer { | |||
24 | 27 | ||
25 | return absoluteAPIUrl + actor.avatar.path | 28 | return absoluteAPIUrl + actor.avatar.path |
26 | } | 29 | } |
30 | |||
31 | return '' | ||
27 | } | 32 | } |
28 | 33 | ||
29 | static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) { | 34 | static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) { |
@@ -42,11 +47,11 @@ export abstract class Actor implements ActorServer { | |||
42 | return host.trim() === thisHost | 47 | return host.trim() === thisHost |
43 | } | 48 | } |
44 | 49 | ||
45 | protected constructor (hash: ActorServer) { | 50 | protected constructor (hash: Partial<ActorServer>) { |
46 | this.id = hash.id | 51 | this.id = hash.id |
47 | this.url = hash.url | 52 | this.url = hash.url ?? '' |
48 | this.name = hash.name | 53 | this.name = hash.name ?? '' |
49 | this.host = hash.host | 54 | this.host = hash.host ?? '' |
50 | this.followingCount = hash.followingCount | 55 | this.followingCount = hash.followingCount |
51 | this.followersCount = hash.followersCount | 56 | this.followersCount = hash.followersCount |
52 | 57 | ||
diff --git a/client/src/app/shared/shared-main/account/index.ts b/client/src/app/shared/shared-main/account/index.ts index 61c800e56..b80ddb9f5 100644 --- a/client/src/app/shared/shared-main/account/index.ts +++ b/client/src/app/shared/shared-main/account/index.ts | |||
@@ -1,5 +1,3 @@ | |||
1 | export * from './account.model' | 1 | export * from './account.model' |
2 | export * from './account.service' | 2 | export * from './account.service' |
3 | export * from './actor-avatar-info.component' | ||
4 | export * from './actor.model' | 3 | export * from './actor.model' |
5 | export * from './video-avatar-channel.component' | ||
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 @@ | |||
1 | import { AfterViewInit, Directive, ElementRef } from '@angular/core' | ||
2 | |||
3 | @Directive({ | ||
4 | selector: '[autofocus]' | ||
5 | }) | ||
6 | export 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 @@ | |||
1 | export * from './autofocus.directive' | ||
1 | export * from './bytes.pipe' | 2 | export * from './bytes.pipe' |
2 | export * from './duration-formatter.pipe' | 3 | export * from './duration-formatter.pipe' |
3 | export * from './from-now.pipe' | 4 | export * 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/misc/simple-search-input.component.html b/client/src/app/shared/shared-main/misc/simple-search-input.component.html index fb0d97122..c20c02e23 100644 --- a/client/src/app/shared/shared-main/misc/simple-search-input.component.html +++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.html | |||
@@ -1,14 +1,15 @@ | |||
1 | <span> | 1 | <div class="root"> |
2 | <my-global-icon iconName="search" aria-label="Search" role="button" (click)="showInput()"></my-global-icon> | ||
3 | |||
4 | <input | 2 | <input |
5 | #ref | 3 | #ref |
6 | type="text" | 4 | type="text" |
7 | [(ngModel)]="value" | 5 | [(ngModel)]="value" |
8 | (focusout)="focusLost()" | ||
9 | (keyup.enter)="searchChange()" | 6 | (keyup.enter)="searchChange()" |
10 | [hidden]="!shown" | 7 | [hidden]="!inputShown" |
11 | [name]="name" | 8 | [name]="name" |
12 | [placeholder]="placeholder" | 9 | [placeholder]="placeholder" |
13 | > | 10 | > |
14 | </span> | 11 | |
12 | <my-global-icon iconName="search" aria-label="Search" role="button" (click)="onIconClick()" [title]="iconTitle"></my-global-icon> | ||
13 | |||
14 | <my-global-icon *ngIf="!alwaysShow && inputShown" i18n-title title="Close search" iconName="cross" (click)="hideInput()"></my-global-icon> | ||
15 | </div> | ||
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.scss b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss index 591b04fb2..5ae48f81b 100644 --- a/client/src/app/shared/shared-main/misc/simple-search-input.component.scss +++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss | |||
@@ -1,29 +1,29 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | span { | 4 | .root { |
5 | opacity: .6; | 5 | display: flex; |
6 | |||
7 | &:focus-within { | ||
8 | opacity: 1; | ||
9 | } | ||
10 | } | 6 | } |
11 | 7 | ||
12 | my-global-icon { | 8 | my-global-icon { |
13 | height: 18px; | 9 | height: 28px; |
14 | position: relative; | 10 | width: 28px; |
15 | top: -2px; | 11 | margin-left: 10px; |
16 | } | 12 | cursor: pointer; |
17 | 13 | ||
18 | input { | 14 | &:hover { |
19 | @include peertube-input-text(150px); | 15 | color: pvar(--mainHoverColor); |
16 | } | ||
20 | 17 | ||
21 | height: 22px; // maximum height for the account/video-channels links | 18 | &[iconName=search] { |
22 | padding-left: 10px; | 19 | color: pvar(--mainForegroundColor); |
23 | background-color: transparent; | 20 | } |
24 | border: none; | ||
25 | 21 | ||
26 | &::placeholder { | 22 | &[iconName=cross] { |
27 | font-size: 15px; | 23 | color: pvar(--mainForegroundColor); |
28 | } | 24 | } |
29 | } | 25 | } |
26 | |||
27 | input { | ||
28 | @include peertube-input-text(200px); | ||
29 | } | ||
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.ts b/client/src/app/shared/shared-main/misc/simple-search-input.component.ts index 86ae9ab42..224d71134 100644 --- a/client/src/app/shared/shared-main/misc/simple-search-input.component.ts +++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { Subject } from 'rxjs' | 1 | import { Subject } from 'rxjs' |
4 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | 2 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' |
3 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | ||
4 | import { ActivatedRoute, Router } from '@angular/router' | ||
5 | 5 | ||
6 | @Component({ | 6 | @Component({ |
7 | selector: 'simple-search-input', | 7 | selector: 'simple-search-input', |
@@ -13,11 +13,14 @@ export class SimpleSearchInputComponent implements OnInit { | |||
13 | 13 | ||
14 | @Input() name = 'search' | 14 | @Input() name = 'search' |
15 | @Input() placeholder = $localize`Search` | 15 | @Input() placeholder = $localize`Search` |
16 | @Input() iconTitle = $localize`Search` | ||
17 | @Input() alwaysShow = true | ||
16 | 18 | ||
17 | @Output() searchChanged = new EventEmitter<string>() | 19 | @Output() searchChanged = new EventEmitter<string>() |
20 | @Output() inputDisplayChanged = new EventEmitter<boolean>() | ||
18 | 21 | ||
19 | value = '' | 22 | value = '' |
20 | shown: boolean | 23 | inputShown: boolean |
21 | 24 | ||
22 | private searchSubject = new Subject<string>() | 25 | private searchSubject = new Subject<string>() |
23 | 26 | ||
@@ -35,20 +38,51 @@ export class SimpleSearchInputComponent implements OnInit { | |||
35 | .subscribe(value => this.searchChanged.emit(value)) | 38 | .subscribe(value => this.searchChanged.emit(value)) |
36 | 39 | ||
37 | this.searchSubject.next(this.value) | 40 | this.searchSubject.next(this.value) |
41 | |||
42 | if (this.isInputShown()) this.showInput(false) | ||
38 | } | 43 | } |
39 | 44 | ||
40 | showInput () { | 45 | isInputShown () { |
41 | this.shown = true | 46 | if (this.alwaysShow) return true |
42 | setTimeout(() => this.input.nativeElement.focus()) | 47 | |
48 | return this.inputShown | ||
49 | } | ||
50 | |||
51 | onIconClick () { | ||
52 | if (!this.isInputShown()) { | ||
53 | this.showInput() | ||
54 | return | ||
55 | } | ||
56 | |||
57 | this.searchChange() | ||
58 | } | ||
59 | |||
60 | showInput (focus = true) { | ||
61 | this.inputShown = true | ||
62 | this.inputDisplayChanged.emit(this.inputShown) | ||
63 | |||
64 | if (focus) { | ||
65 | setTimeout(() => this.input.nativeElement.focus()) | ||
66 | } | ||
67 | } | ||
68 | |||
69 | hideInput () { | ||
70 | this.inputShown = false | ||
71 | |||
72 | if (this.isInputShown() === false) { | ||
73 | this.inputDisplayChanged.emit(this.inputShown) | ||
74 | } | ||
43 | } | 75 | } |
44 | 76 | ||
45 | focusLost () { | 77 | focusLost () { |
46 | if (this.value !== '') return | 78 | if (this.value) return |
47 | this.shown = false | 79 | |
80 | this.hideInput() | ||
48 | } | 81 | } |
49 | 82 | ||
50 | searchChange () { | 83 | searchChange () { |
51 | this.router.navigate(['./search'], { relativeTo: this.route }) | 84 | this.router.navigate([ './search' ], { relativeTo: this.route }) |
85 | |||
52 | this.searchSubject.next(this.value) | 86 | this.searchSubject.next(this.value) |
53 | } | 87 | } |
54 | } | 88 | } |
diff --git a/client/src/app/shared/shared-main/peertube-modal/index.ts b/client/src/app/shared/shared-main/peertube-modal/index.ts new file mode 100644 index 000000000..d631522e4 --- /dev/null +++ b/client/src/app/shared/shared-main/peertube-modal/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './peertube-modal.service' | |||
diff --git a/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts b/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts new file mode 100644 index 000000000..79da08a5c --- /dev/null +++ b/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { Subject } from 'rxjs' | ||
3 | |||
4 | @Injectable({ providedIn: 'root' }) | ||
5 | export class PeertubeModalService { | ||
6 | openQuickSettingsSubject = new Subject<void>() | ||
7 | } | ||
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..16d230f46 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts | |||
@@ -6,19 +6,20 @@ import { NgModule } from '@angular/core' | |||
6 | import { FormsModule, ReactiveFormsModule } from '@angular/forms' | 6 | import { FormsModule, ReactiveFormsModule } from '@angular/forms' |
7 | import { RouterModule } from '@angular/router' | 7 | import { RouterModule } from '@angular/router' |
8 | import { | 8 | import { |
9 | NgbButtonsModule, | ||
9 | NgbCollapseModule, | 10 | NgbCollapseModule, |
10 | NgbDropdownModule, | 11 | NgbDropdownModule, |
11 | NgbModalModule, | 12 | NgbModalModule, |
12 | NgbNavModule, | 13 | NgbNavModule, |
13 | NgbPopoverModule, | 14 | NgbPopoverModule, |
14 | NgbTooltipModule, | 15 | NgbTooltipModule |
15 | NgbButtonsModule | ||
16 | } from '@ng-bootstrap/ng-bootstrap' | 16 | } from '@ng-bootstrap/ng-bootstrap' |
17 | import { LoadingBarModule } from '@ngx-loading-bar/core' | 17 | import { LoadingBarModule } from '@ngx-loading-bar/core' |
18 | import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' | 18 | import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' |
19 | import { SharedGlobalIconModule } from '../shared-icons' | 19 | import { SharedGlobalIconModule } from '../shared-icons' |
20 | import { AccountService, ActorAvatarInfoComponent, VideoAvatarChannelComponent } from './account' | 20 | import { AccountService } from './account' |
21 | import { | 21 | import { |
22 | AutofocusDirective, | ||
22 | BytesPipe, | 23 | BytesPipe, |
23 | DurationFormatterPipe, | 24 | DurationFormatterPipe, |
24 | FromNowPipe, | 25 | FromNowPipe, |
@@ -31,7 +32,7 @@ import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditBu | |||
31 | import { DateToggleComponent } from './date' | 32 | import { DateToggleComponent } from './date' |
32 | import { FeedComponent } from './feeds' | 33 | import { FeedComponent } from './feeds' |
33 | import { LoaderComponent, SmallLoaderComponent } from './loaders' | 34 | import { LoaderComponent, SmallLoaderComponent } from './loaders' |
34 | import { HelpComponent, ListOverflowComponent, TopMenuDropdownComponent, SimpleSearchInputComponent } from './misc' | 35 | import { HelpComponent, ListOverflowComponent, SimpleSearchInputComponent, TopMenuDropdownComponent } from './misc' |
35 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' | 36 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' |
36 | import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' | 37 | import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' |
37 | import { VideoCaptionService } from './video-caption' | 38 | import { VideoCaptionService } from './video-caption' |
@@ -64,13 +65,11 @@ import { VideoChannelService } from './video-channel' | |||
64 | ], | 65 | ], |
65 | 66 | ||
66 | declarations: [ | 67 | declarations: [ |
67 | VideoAvatarChannelComponent, | ||
68 | ActorAvatarInfoComponent, | ||
69 | |||
70 | FromNowPipe, | 68 | FromNowPipe, |
71 | NumberFormatterPipe, | 69 | NumberFormatterPipe, |
72 | BytesPipe, | 70 | BytesPipe, |
73 | DurationFormatterPipe, | 71 | DurationFormatterPipe, |
72 | AutofocusDirective, | ||
74 | 73 | ||
75 | InfiniteScrollerDirective, | 74 | InfiniteScrollerDirective, |
76 | PeerTubeTemplateDirective, | 75 | PeerTubeTemplateDirective, |
@@ -118,13 +117,11 @@ import { VideoChannelService } from './video-channel' | |||
118 | 117 | ||
119 | PrimeSharedModule, | 118 | PrimeSharedModule, |
120 | 119 | ||
121 | VideoAvatarChannelComponent, | ||
122 | ActorAvatarInfoComponent, | ||
123 | |||
124 | FromNowPipe, | 120 | FromNowPipe, |
125 | BytesPipe, | 121 | BytesPipe, |
126 | NumberFormatterPipe, | 122 | NumberFormatterPipe, |
127 | DurationFormatterPipe, | 123 | DurationFormatterPipe, |
124 | AutofocusDirective, | ||
128 | 125 | ||
129 | InfiniteScrollerDirective, | 126 | InfiniteScrollerDirective, |
130 | PeerTubeTemplateDirective, | 127 | 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-main/video-channel/video-channel.model.ts b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts index c6a63fe6c..1ba3fcc0e 100644 --- a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts +++ b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts | |||
@@ -1,15 +1,22 @@ | |||
1 | import { VideoChannel as ServerVideoChannel, ViewsPerDate, Account, Avatar } from '@shared/models' | 1 | import { getAbsoluteAPIUrl } from '@app/helpers' |
2 | import { Account as ServerAccount, ActorImage, VideoChannel as ServerVideoChannel, ViewsPerDate } from '@shared/models' | ||
3 | import { Account } from '../account/account.model' | ||
2 | import { Actor } from '../account/actor.model' | 4 | import { Actor } from '../account/actor.model' |
3 | 5 | ||
4 | export class VideoChannel extends Actor implements ServerVideoChannel { | 6 | export class VideoChannel extends Actor implements ServerVideoChannel { |
5 | displayName: string | 7 | displayName: string |
6 | description: string | 8 | description: string |
7 | support: string | 9 | support: string |
10 | |||
8 | isLocal: boolean | 11 | isLocal: boolean |
12 | |||
9 | nameWithHost: string | 13 | nameWithHost: string |
10 | nameWithHostForced: string | 14 | nameWithHostForced: string |
11 | 15 | ||
12 | ownerAccount?: Account | 16 | banner: ActorImage |
17 | bannerUrl: string | ||
18 | |||
19 | ownerAccount?: ServerAccount | ||
13 | ownerBy?: string | 20 | ownerBy?: string |
14 | ownerAvatarUrl?: string | 21 | ownerAvatarUrl?: string |
15 | 22 | ||
@@ -21,19 +28,33 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
21 | return Actor.GET_ACTOR_AVATAR_URL(actor) || this.GET_DEFAULT_AVATAR_URL() | 28 | return Actor.GET_ACTOR_AVATAR_URL(actor) || this.GET_DEFAULT_AVATAR_URL() |
22 | } | 29 | } |
23 | 30 | ||
31 | static GET_ACTOR_BANNER_URL (channel: ServerVideoChannel) { | ||
32 | if (channel?.banner?.url) return channel.banner.url | ||
33 | |||
34 | if (channel && channel.banner) { | ||
35 | const absoluteAPIUrl = getAbsoluteAPIUrl() | ||
36 | |||
37 | return absoluteAPIUrl + channel.banner.path | ||
38 | } | ||
39 | |||
40 | return '' | ||
41 | } | ||
42 | |||
24 | static GET_DEFAULT_AVATAR_URL () { | 43 | static GET_DEFAULT_AVATAR_URL () { |
25 | return `${window.location.origin}/client/assets/images/default-avatar-videochannel.png` | 44 | return `${window.location.origin}/client/assets/images/default-avatar-videochannel.png` |
26 | } | 45 | } |
27 | 46 | ||
28 | constructor (hash: ServerVideoChannel) { | 47 | constructor (hash: Partial<ServerVideoChannel>) { |
29 | super(hash) | 48 | super(hash) |
30 | 49 | ||
31 | this.updateComputedAttributes() | ||
32 | |||
33 | this.displayName = hash.displayName | 50 | this.displayName = hash.displayName |
34 | this.description = hash.description | 51 | this.description = hash.description |
35 | this.support = hash.support | 52 | this.support = hash.support |
53 | |||
54 | this.banner = hash.banner | ||
55 | |||
36 | this.isLocal = hash.isLocal | 56 | this.isLocal = hash.isLocal |
57 | |||
37 | this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) | 58 | this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) |
38 | this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) | 59 | this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) |
39 | 60 | ||
@@ -46,22 +67,34 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
46 | if (hash.ownerAccount) { | 67 | if (hash.ownerAccount) { |
47 | this.ownerAccount = hash.ownerAccount | 68 | this.ownerAccount = hash.ownerAccount |
48 | this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host) | 69 | this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host) |
49 | this.ownerAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.ownerAccount) | 70 | this.ownerAvatarUrl = Account.GET_ACTOR_AVATAR_URL(this.ownerAccount) |
50 | } | 71 | } |
72 | |||
73 | this.updateComputedAttributes() | ||
51 | } | 74 | } |
52 | 75 | ||
53 | updateAvatar (newAvatar: Avatar) { | 76 | updateAvatar (newAvatar: ActorImage) { |
54 | this.avatar = newAvatar | 77 | this.avatar = newAvatar |
55 | 78 | ||
56 | this.updateComputedAttributes() | 79 | this.updateComputedAttributes() |
57 | } | 80 | } |
58 | 81 | ||
59 | resetAvatar () { | 82 | resetAvatar () { |
60 | this.avatar = null | 83 | this.updateAvatar(null) |
61 | this.avatarUrl = VideoChannel.GET_DEFAULT_AVATAR_URL() | 84 | } |
85 | |||
86 | updateBanner (newBanner: ActorImage) { | ||
87 | this.banner = newBanner | ||
88 | |||
89 | this.updateComputedAttributes() | ||
90 | } | ||
91 | |||
92 | resetBanner () { | ||
93 | this.updateBanner(null) | ||
62 | } | 94 | } |
63 | 95 | ||
64 | private updateComputedAttributes () { | 96 | updateComputedAttributes () { |
65 | this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this) | 97 | this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this) |
98 | this.bannerUrl = VideoChannel.GET_ACTOR_BANNER_URL(this) | ||
66 | } | 99 | } |
67 | } | 100 | } |
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts index eff3fad4d..e65261763 100644 --- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts +++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts | |||
@@ -3,7 +3,7 @@ import { catchError, map, tap } from 'rxjs/operators' | |||
3 | import { HttpClient, HttpParams } from '@angular/common/http' | 3 | import { HttpClient, HttpParams } from '@angular/common/http' |
4 | import { Injectable } from '@angular/core' | 4 | import { Injectable } from '@angular/core' |
5 | import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' | 5 | import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' |
6 | import { Avatar, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models' | 6 | import { ActorImage, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models' |
7 | import { environment } from '../../../../environments/environment' | 7 | import { environment } from '../../../../environments/environment' |
8 | import { Account } from '../account' | 8 | import { Account } from '../account' |
9 | import { AccountService } from '../account/account.service' | 9 | import { AccountService } from '../account/account.service' |
@@ -82,15 +82,15 @@ export class VideoChannelService { | |||
82 | ) | 82 | ) |
83 | } | 83 | } |
84 | 84 | ||
85 | changeVideoChannelAvatar (videoChannelName: string, avatarForm: FormData) { | 85 | changeVideoChannelImage (videoChannelName: string, avatarForm: FormData, type: 'avatar' | 'banner') { |
86 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar/pick' | 86 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type + '/pick' |
87 | 87 | ||
88 | return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm) | 88 | return this.authHttp.post<{ avatar?: ActorImage, banner?: ActorImage }>(url, avatarForm) |
89 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 89 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
90 | } | 90 | } |
91 | 91 | ||
92 | deleteVideoChannelAvatar (videoChannelName: string) { | 92 | deleteVideoChannelImage (videoChannelName: string, type: 'avatar' | 'banner') { |
93 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar' | 93 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type |
94 | 94 | ||
95 | return this.authHttp.delete(url) | 95 | return this.authHttp.delete(url) |
96 | .pipe( | 96 | .pipe( |
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts index adb6e884f..1c2c4a575 100644 --- a/client/src/app/shared/shared-main/video/video.model.ts +++ b/client/src/app/shared/shared-main/video/video.model.ts | |||
@@ -6,7 +6,7 @@ import { Actor } from '@app/shared/shared-main/account/actor.model' | |||
6 | import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model' | 6 | import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model' |
7 | import { peertubeTranslate } from '@shared/core-utils/i18n' | 7 | import { peertubeTranslate } from '@shared/core-utils/i18n' |
8 | import { | 8 | import { |
9 | Avatar, | 9 | ActorImage, |
10 | ServerConfig, | 10 | ServerConfig, |
11 | UserRight, | 11 | UserRight, |
12 | Video as VideoServerModel, | 12 | Video as VideoServerModel, |
@@ -72,7 +72,7 @@ export class Video implements VideoServerModel { | |||
72 | displayName: string | 72 | displayName: string |
73 | url: string | 73 | url: string |
74 | host: string | 74 | host: string |
75 | avatar?: Avatar | 75 | avatar?: ActorImage |
76 | } | 76 | } |
77 | 77 | ||
78 | channel: { | 78 | channel: { |
@@ -81,7 +81,7 @@ export class Video implements VideoServerModel { | |||
81 | displayName: string | 81 | displayName: string |
82 | url: string | 82 | url: string |
83 | host: string | 83 | host: string |
84 | avatar?: Avatar | 84 | avatar?: ActorImage |
85 | } | 85 | } |
86 | 86 | ||
87 | userHistory?: { | 87 | userHistory?: { |
diff --git a/client/src/app/shared/shared-moderation/moderation.scss b/client/src/app/shared/shared-moderation/moderation.scss index 4a4e05535..cdcc12fe0 100644 --- a/client/src/app/shared/shared-moderation/moderation.scss +++ b/client/src/app/shared/shared-moderation/moderation.scss | |||
@@ -32,7 +32,7 @@ | |||
32 | color: pvar(--inputPlaceholderColor); | 32 | color: pvar(--inputPlaceholderColor); |
33 | } | 33 | } |
34 | 34 | ||
35 | @include large-screen-ratio($selector: 'div, ::ng-deep iframe') { | 35 | @include block-ratio($selector: 'div, ::ng-deep iframe') { |
36 | width: 100% !important; | 36 | width: 100% !important; |
37 | height: 100% !important; | 37 | height: 100% !important; |
38 | left: 0; | 38 | left: 0; |
diff --git a/client/src/app/shared/shared-moderation/report-modals/report.component.scss b/client/src/app/shared/shared-moderation/report-modals/report.component.scss index b2606cbd8..0567330f5 100644 --- a/client/src/app/shared/shared-moderation/report-modals/report.component.scss +++ b/client/src/app/shared/shared-moderation/report-modals/report.component.scss | |||
@@ -21,7 +21,7 @@ textarea { | |||
21 | } | 21 | } |
22 | 22 | ||
23 | .screenratio { | 23 | .screenratio { |
24 | @include large-screen-ratio($selector: 'div, ::ng-deep iframe') { | 24 | @include block-ratio($selector: 'div, ::ng-deep iframe') { |
25 | left: 0; | 25 | left: 0; |
26 | }; | 26 | }; |
27 | } | 27 | } |
diff --git a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts index 5b06c0bc7..4ca6f52ad 100644 --- a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts +++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts | |||
@@ -61,7 +61,8 @@ export class VideoReportComponent extends FormReactive implements OnInit { | |||
61 | baseUrl: this.video.embedUrl, | 61 | baseUrl: this.video.embedUrl, |
62 | title: false, | 62 | title: false, |
63 | warningTitle: false | 63 | warningTitle: false |
64 | }) | 64 | }), |
65 | this.video.name | ||
65 | ) | 66 | ) |
66 | ) | 67 | ) |
67 | } | 68 | } |
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.ts b/client/src/app/shared/shared-share-modal/video-share.component.ts index b06ff3751..e8760bfcc 100644 --- a/client/src/app/shared/shared-share-modal/video-share.component.ts +++ b/client/src/app/shared/shared-share-modal/video-share.component.ts | |||
@@ -86,14 +86,14 @@ export class VideoShareComponent { | |||
86 | const options = this.getVideoOptions(this.video.embedUrl) | 86 | const options = this.getVideoOptions(this.video.embedUrl) |
87 | 87 | ||
88 | const embedUrl = buildVideoLink(options) | 88 | const embedUrl = buildVideoLink(options) |
89 | return buildVideoOrPlaylistEmbed(embedUrl) | 89 | return buildVideoOrPlaylistEmbed(embedUrl, this.video.name) |
90 | } | 90 | } |
91 | 91 | ||
92 | getPlaylistIframeCode () { | 92 | getPlaylistIframeCode () { |
93 | const options = this.getPlaylistOptions(this.playlist.embedUrl) | 93 | const options = this.getPlaylistOptions(this.playlist.embedUrl) |
94 | 94 | ||
95 | const embedUrl = buildPlaylistLink(options) | 95 | const embedUrl = buildPlaylistLink(options) |
96 | return buildVideoOrPlaylistEmbed(embedUrl) | 96 | return buildVideoOrPlaylistEmbed(embedUrl, this.playlist.displayName) |
97 | } | 97 | } |
98 | 98 | ||
99 | getVideoUrl () { | 99 | getVideoUrl () { |
diff --git a/client/src/app/shared/shared-support-modal/index.ts b/client/src/app/shared/shared-support-modal/index.ts new file mode 100644 index 000000000..f41bb4bc2 --- /dev/null +++ b/client/src/app/shared/shared-support-modal/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './support-modal.component' | ||
2 | |||
3 | export * from './shared-support-modal.module' | ||
diff --git a/client/src/app/shared/shared-support-modal/shared-support-modal.module.ts b/client/src/app/shared/shared-support-modal/shared-support-modal.module.ts new file mode 100644 index 000000000..1101d5535 --- /dev/null +++ b/client/src/app/shared/shared-support-modal/shared-support-modal.module.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { SharedFormModule } from '../shared-forms' | ||
3 | import { SharedGlobalIconModule } from '../shared-icons' | ||
4 | import { SharedMainModule } from '../shared-main/shared-main.module' | ||
5 | import { SupportModalComponent } from './support-modal.component' | ||
6 | |||
7 | @NgModule({ | ||
8 | imports: [ | ||
9 | SharedMainModule, | ||
10 | SharedFormModule, | ||
11 | SharedGlobalIconModule | ||
12 | ], | ||
13 | |||
14 | declarations: [ | ||
15 | SupportModalComponent | ||
16 | ], | ||
17 | |||
18 | exports: [ | ||
19 | SupportModalComponent | ||
20 | ], | ||
21 | |||
22 | providers: [ ] | ||
23 | }) | ||
24 | export class SharedSupportModal { } | ||
diff --git a/client/src/app/+videos/+video-watch/modal/video-support.component.html b/client/src/app/shared/shared-support-modal/support-modal.component.html index 935656d23..4a967987f 100644 --- a/client/src/app/+videos/+video-watch/modal/video-support.component.html +++ b/client/src/app/shared/shared-support-modal/support-modal.component.html | |||
@@ -1,10 +1,10 @@ | |||
1 | <ng-template #modal let-hide="close"> | 1 | <ng-template #modal let-hide="close"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Support {{ video.account.displayName }}</h4> | 3 | <h4 i18n class="modal-title">Support {{ displayName }}</h4> |
4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body" [innerHTML]="videoHTMLSupport"></div> | 7 | <div class="modal-body" [innerHTML]="htmlSupport"></div> |
8 | 8 | ||
9 | <div class="modal-footer inputs"> | 9 | <div class="modal-footer inputs"> |
10 | <input | 10 | <input |
diff --git a/client/src/app/+videos/+video-watch/modal/video-support.component.scss b/client/src/app/shared/shared-support-modal/support-modal.component.scss index 184e09027..184e09027 100644 --- a/client/src/app/+videos/+video-watch/modal/video-support.component.scss +++ b/client/src/app/shared/shared-support-modal/support-modal.component.scss | |||
diff --git a/client/src/app/shared/shared-support-modal/support-modal.component.ts b/client/src/app/shared/shared-support-modal/support-modal.component.ts new file mode 100644 index 000000000..ae603c7a8 --- /dev/null +++ b/client/src/app/shared/shared-support-modal/support-modal.component.ts | |||
@@ -0,0 +1,40 @@ | |||
1 | import { Component, Input, ViewChild } from '@angular/core' | ||
2 | import { MarkdownService } from '@app/core' | ||
3 | import { VideoDetails } from '@app/shared/shared-main' | ||
4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
5 | import { VideoChannel } from '@shared/models' | ||
6 | |||
7 | @Component({ | ||
8 | selector: 'my-support-modal', | ||
9 | templateUrl: './support-modal.component.html', | ||
10 | styleUrls: [ './support-modal.component.scss' ] | ||
11 | }) | ||
12 | export class SupportModalComponent { | ||
13 | @Input() video: VideoDetails = null | ||
14 | @Input() videoChannel: VideoChannel = null | ||
15 | |||
16 | @ViewChild('modal', { static: true }) modal: NgbModal | ||
17 | |||
18 | htmlSupport = '' | ||
19 | displayName = '' | ||
20 | |||
21 | constructor ( | ||
22 | private markdownService: MarkdownService, | ||
23 | private modalService: NgbModal | ||
24 | ) { } | ||
25 | |||
26 | show () { | ||
27 | const modalRef = this.modalService.open(this.modal, { centered: true }) | ||
28 | |||
29 | const support = this.video?.support || this.videoChannel.support | ||
30 | |||
31 | this.markdownService.enhancedMarkdownToHTML(support) | ||
32 | .then(r => this.htmlSupport = r) | ||
33 | |||
34 | this.displayName = this.video | ||
35 | ? this.video.channel.displayName | ||
36 | : this.videoChannel.displayName | ||
37 | |||
38 | return modalRef | ||
39 | } | ||
40 | } | ||
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.html b/client/src/app/shared/shared-video-miniature/abstract-video-list.html index 07f79cd6d..ee5df28be 100644 --- a/client/src/app/shared/shared-video-miniature/abstract-video-list.html +++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.html | |||
@@ -43,7 +43,7 @@ | |||
43 | <div class="no-results" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">No results.</div> | 43 | <div class="no-results" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">No results.</div> |
44 | <div | 44 | <div |
45 | myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()" | 45 | myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()" |
46 | class="videos" | 46 | class="videos" [ngClass]="{ 'display-as-row': displayAsRow() }" |
47 | > | 47 | > |
48 | <ng-container *ngFor="let video of videos; trackBy: videoById;"> | 48 | <ng-container *ngFor="let video of videos; trackBy: videoById;"> |
49 | <h2 class="date-title" *ngIf="getCurrentGroupedDateLabel(video)"> | 49 | <h2 class="date-title" *ngIf="getCurrentGroupedDateLabel(video)"> |
@@ -52,8 +52,7 @@ | |||
52 | 52 | ||
53 | <div class="video-wrapper"> | 53 | <div class="video-wrapper"> |
54 | <my-video-miniature | 54 | <my-video-miniature |
55 | [fitWidth]="true" | 55 | [video]="video" [user]="userMiniature" [displayAsRow]="displayAsRow()" |
56 | [video]="video" [user]="userMiniature" [ownerDisplayType]="ownerDisplayType" | ||
57 | [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions" | 56 | [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions" |
58 | (videoBlocked)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)" | 57 | (videoBlocked)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)" |
59 | > | 58 | > |
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.scss b/client/src/app/shared/shared-video-miniature/abstract-video-list.scss index 0a8aa8fa4..6570b63d0 100644 --- a/client/src/app/shared/shared-video-miniature/abstract-video-list.scss +++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.scss | |||
@@ -69,7 +69,16 @@ $iconSize: 16px; | |||
69 | } | 69 | } |
70 | 70 | ||
71 | .margin-content { | 71 | .margin-content { |
72 | @include fluid-videos-miniature-layout; | 72 | @include grid-videos-miniature-layout; |
73 | } | ||
74 | |||
75 | .display-as-row.videos { | ||
76 | margin-left: pvar(--horizontalMarginContent); | ||
77 | margin-right: pvar(--horizontalMarginContent); | ||
78 | |||
79 | .video-wrapper { | ||
80 | margin-bottom: 15px; | ||
81 | } | ||
73 | } | 82 | } |
74 | 83 | ||
75 | @media screen and (max-width: $mobile-view) { | 84 | @media screen and (max-width: $mobile-view) { |
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts index c13cb3748..f83380513 100644 --- a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts +++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts | |||
@@ -28,8 +28,8 @@ import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@sha | |||
28 | import { ServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/models' | 28 | import { ServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/models' |
29 | import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' | 29 | import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' |
30 | import { Syndication, Video } from '../shared-main' | 30 | import { Syndication, Video } from '../shared-main' |
31 | import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component' | ||
32 | import { GenericHeaderComponent, VideoListHeaderComponent } from './video-list-header.component' | 31 | import { GenericHeaderComponent, VideoListHeaderComponent } from './video-list-header.component' |
32 | import { MiniatureDisplayOptions } from './video-miniature.component' | ||
33 | 33 | ||
34 | enum GroupDate { | 34 | enum GroupDate { |
35 | UNKNOWN = 0, | 35 | UNKNOWN = 0, |
@@ -65,7 +65,6 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, AfterConte | |||
65 | loadOnInit = true | 65 | loadOnInit = true |
66 | loadUserVideoPreferences = false | 66 | loadUserVideoPreferences = false |
67 | 67 | ||
68 | ownerDisplayType: OwnerDisplayType = 'account' | ||
69 | displayModerationBlock = false | 68 | displayModerationBlock = false |
70 | titleTooltip: string | 69 | titleTooltip: string |
71 | displayVideoActions = true | 70 | displayVideoActions = true |
@@ -320,6 +319,11 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, AfterConte | |||
320 | viewContainerRef.createComponent(componentFactory, 0, injector) | 319 | viewContainerRef.createComponent(componentFactory, 0, injector) |
321 | } | 320 | } |
322 | 321 | ||
322 | // Can be redefined by child | ||
323 | displayAsRow () { | ||
324 | return false | ||
325 | } | ||
326 | |||
323 | // On videos hook for children that want to do something | 327 | // On videos hook for children that want to do something |
324 | protected onMoreVideos () { /* empty */ } | 328 | protected onMoreVideos () { /* empty */ } |
325 | 329 | ||
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..8a9218343 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 | |||
@@ -17,85 +17,116 @@ | |||
17 | </div> | 17 | </div> |
18 | 18 | ||
19 | <div class="modal-body"> | 19 | <div class="modal-body"> |
20 | <div class="form-group"> | 20 | <div class="alert alert-warning" *ngIf="isConfidentialVideo()" i18n> |
21 | <div class="alert alert-warning" *ngIf="isConfidentialVideo()" i18n> | 21 | The following link contains a private token and should not be shared with anyone. |
22 | The following link contains a private token and should not be shared with anyone. | 22 | </div> |
23 | </div> | ||
24 | 23 | ||
24 | <ng-container *ngIf="type === 'subtitles'"> | ||
25 | <div class="input-group input-group-sm"> | 25 | <div class="input-group input-group-sm"> |
26 | <div class="input-group-prepend peertube-select-container"> | ||
27 | <select *ngIf="type === 'video'" [(ngModel)]="resolutionId" (ngModelChange)="onResolutionIdChange()"> | ||
28 | <option *ngFor="let file of getVideoFiles()" [value]="file.resolution.id">{{ file.resolution.label }}</option> | ||
29 | </select> | ||
30 | |||
31 | <select *ngIf="type === 'subtitles'" [(ngModel)]="subtitleLanguageId"> | ||
32 | <option *ngFor="let caption of videoCaptions" [value]="caption.language.id">{{ caption.language.label }}</option> | ||
33 | </select> | ||
34 | </div> | ||
35 | |||
36 | <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> | 26 | <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> |
37 | <div class="input-group-append" *ngIf="!isConfidentialVideo()"> | 27 | <div class="input-group-append" *ngIf="!isConfidentialVideo()"> |
38 | <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> | 28 | <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> |
39 | <span class="glyphicon glyphicon-copy"></span> | 29 | <span class="glyphicon glyphicon-duplicate"></span> |
40 | </button> | 30 | </button> |
41 | </div> | 31 | </div> |
42 | </div> | 32 | </div> |
43 | </div> | 33 | </ng-container> |
44 | 34 | ||
45 | <ng-container *ngIf="type === 'video' && videoFile?.metadata"> | 35 | <ng-container *ngIf="type === 'video'"> |
46 | <div ngbNav #nav="ngbNav" class="nav-tabs"> | 36 | <div ngbNav #resolutionNav="ngbNav" class="nav-tabs" [activeId]="resolutionId" (activeIdChange)="onResolutionIdChange($event)"> |
47 | 37 | ||
48 | <ng-container ngbNavItem> | 38 | <ng-container *ngFor="let file of getVideoFiles()" [ngbNavItem]="file.resolution.id"> |
49 | <a ngbNavLink i18n>Format</a> | 39 | <a ngbNavLink i18n>{{ file.resolution.label }}</a> |
40 | |||
50 | <ng-template ngbNavContent> | 41 | <ng-template ngbNavContent> |
51 | <div class="file-metadata"> | 42 | <div class="nav-content"> |
52 | <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataFormat | keyvalue"> | 43 | <div class="input-group input-group-sm"> |
53 | <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> | 44 | <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> |
54 | <span class="metadata-attribute-value">{{ item.value.value }}</span> | 45 | <div class="input-group-append" *ngIf="!isConfidentialVideo()"> |
46 | <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> | ||
47 | <span class="glyphicon glyphicon-duplicate"></span> | ||
48 | </button> | ||
49 | </div> | ||
55 | </div> | 50 | </div> |
56 | </div> | 51 | </div> |
57 | </ng-template> | 52 | </ng-template> |
58 | </ng-container> | 53 | </ng-container> |
59 | 54 | </div> | |
60 | <ng-container ngbNavItem [disabled]="videoFileMetadataVideoStream === undefined"> | 55 | <div [ngbNavOutlet]="resolutionNav"></div> |
61 | <a ngbNavLink i18n>Video stream</a> | 56 | |
62 | <ng-template ngbNavContent> | 57 | <div class="advanced-filters collapse-transition" [ngbCollapse]="isAdvancedCustomizationCollapsed"> |
63 | <div class="file-metadata"> | 58 | <ng-container *ngIf="videoFile?.metadata"> |
64 | <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataVideoStream | keyvalue"> | 59 | <div ngbNav #nav="ngbNav" class="nav-tabs nav-metadata"> |
65 | <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> | 60 | <ng-container ngbNavItem> |
66 | <span class="metadata-attribute-value">{{ item.value.value }}</span> | 61 | <a ngbNavLink i18n>Format</a> |
67 | </div> | 62 | <ng-template ngbNavContent> |
63 | <div class="file-metadata"> | ||
64 | <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataFormat | keyvalue"> | ||
65 | <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> | ||
66 | <span class="metadata-attribute-value">{{ item.value.value }}</span> | ||
67 | </div> | ||
68 | </div> | ||
69 | </ng-template> | ||
70 | |||
71 | <ng-container ngbNavItem [disabled]="videoFileMetadataVideoStream === undefined"> | ||
72 | <a ngbNavLink i18n>Video stream</a> | ||
73 | <ng-template ngbNavContent> | ||
74 | <div class="file-metadata"> | ||
75 | <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataVideoStream | keyvalue"> | ||
76 | <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> | ||
77 | <span class="metadata-attribute-value">{{ item.value.value }}</span> | ||
78 | </div> | ||
79 | </div> | ||
80 | </ng-template> | ||
81 | </ng-container> | ||
82 | |||
83 | <ng-container ngbNavItem [disabled]="videoFileMetadataAudioStream === undefined"> | ||
84 | <a ngbNavLink i18n>Audio stream</a> | ||
85 | <ng-template ngbNavContent> | ||
86 | <div class="file-metadata"> | ||
87 | <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataAudioStream | keyvalue"> | ||
88 | <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> | ||
89 | <span class="metadata-attribute-value">{{ item.value.value }}</span> | ||
90 | </div> | ||
91 | </div> | ||
92 | </ng-template> | ||
93 | </ng-container> | ||
94 | |||
95 | </ng-container> | ||
96 | </div> | ||
97 | <div [ngbNavOutlet]="nav"></div> | ||
98 | <div class="download-type"> | ||
99 | <div class="peertube-radio-container"> | ||
100 | <input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct"> | ||
101 | <label i18n for="download-direct">Direct download</label> | ||
68 | </div> | 102 | </div> |
69 | </ng-template> | 103 | <div class="peertube-radio-container"> |
70 | </ng-container> | 104 | <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent"> |
71 | 105 | <label i18n for="download-torrent">Torrent (.torrent file)</label> | |
72 | <ng-container ngbNavItem [disabled]="videoFileMetadataAudioStream === undefined"> | ||
73 | <a ngbNavLink i18n>Audio stream</a> | ||
74 | <ng-template ngbNavContent> | ||
75 | <div class="file-metadata"> | ||
76 | <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataAudioStream | keyvalue"> | ||
77 | <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> | ||
78 | <span class="metadata-attribute-value">{{ item.value.value }}</span> | ||
79 | </div> | ||
80 | </div> | 106 | </div> |
81 | </ng-template> | 107 | </div> |
82 | </ng-container> | 108 | </ng-container> |
83 | </div> | 109 | </div> |
84 | 110 | ||
85 | <div [ngbNavOutlet]="nav"></div> | 111 | <div (click)="isAdvancedCustomizationCollapsed = !isAdvancedCustomizationCollapsed" role="button" class="advanced-filters-button" |
86 | </ng-container> | 112 | [attr.aria-expanded]="!isAdvancedCustomizationCollapsed" aria-controls="collapseBasic"> |
87 | 113 | <ng-container *ngIf="isAdvancedCustomizationCollapsed"> | |
88 | <div class="download-type" *ngIf="type === 'video'"> | 114 | <span class="glyphicon glyphicon-menu-down"></span> |
89 | <div class="peertube-radio-container"> | 115 | |
90 | <input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct"> | 116 | <ng-container i18n> |
91 | <label i18n for="download-direct">Direct download</label> | 117 | Advanced |
92 | </div> | 118 | </ng-container> |
93 | 119 | </ng-container> | |
94 | <div class="peertube-radio-container"> | 120 | |
95 | <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent"> | 121 | <ng-container *ngIf="!isAdvancedCustomizationCollapsed"> |
96 | <label i18n for="download-torrent">Torrent (.torrent file)</label> | 122 | <span class="glyphicon glyphicon-menu-up"></span> |
123 | |||
124 | <ng-container i18n> | ||
125 | Simple | ||
126 | </ng-container> | ||
127 | </ng-container> | ||
97 | </div> | 128 | </div> |
98 | </div> | 129 | </ng-container> |
99 | </div> | 130 | </div> |
100 | 131 | ||
101 | <div class="modal-footer inputs"> | 132 | <div class="modal-footer inputs"> |
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.scss b/client/src/app/shared/shared-video-miniature/video-download.component.scss index d407e9531..199c3dac8 100644 --- a/client/src/app/shared/shared-video-miniature/video-download.component.scss +++ b/client/src/app/shared/shared-video-miniature/video-download.component.scss | |||
@@ -1,6 +1,28 @@ | |||
1 | @import 'variables'; | 1 | @import 'variables'; |
2 | @import 'mixins'; | 2 | @import 'mixins'; |
3 | 3 | ||
4 | .nav-content { | ||
5 | margin-top: 30px; | ||
6 | } | ||
7 | |||
8 | .advanced-filters-button { | ||
9 | display: flex; | ||
10 | justify-content: center; | ||
11 | align-items: center; | ||
12 | margin-top: 20px; | ||
13 | font-size: 16px; | ||
14 | font-weight: 600; | ||
15 | cursor: pointer; | ||
16 | |||
17 | .nav-tabs { | ||
18 | margin-top: 10x; | ||
19 | } | ||
20 | |||
21 | .glyphicon { | ||
22 | margin-right: 5px; | ||
23 | } | ||
24 | } | ||
25 | |||
4 | .peertube-select-container { | 26 | .peertube-select-container { |
5 | @include peertube-select-container(85px); | 27 | @include peertube-select-container(85px); |
6 | 28 | ||
@@ -15,12 +37,21 @@ | |||
15 | } | 37 | } |
16 | } | 38 | } |
17 | 39 | ||
40 | .action-button-cancel { | ||
41 | @include peertube-button-link; | ||
42 | } | ||
43 | |||
44 | .action-button-submit { | ||
45 | @include peertube-button-link; | ||
46 | @include orange-button; | ||
47 | } | ||
48 | |||
18 | #dropdownDownloadType { | 49 | #dropdownDownloadType { |
19 | cursor: pointer; | 50 | cursor: pointer; |
20 | } | 51 | } |
21 | 52 | ||
22 | .download-type { | 53 | .download-type { |
23 | margin-top: 30px; | 54 | margin-top: 20px; |
24 | 55 | ||
25 | .peertube-radio-container { | 56 | .peertube-radio-container { |
26 | @include peertube-radio-container; | 57 | @include peertube-radio-container; |
@@ -30,6 +61,10 @@ | |||
30 | } | 61 | } |
31 | } | 62 | } |
32 | 63 | ||
64 | .nav-metadata { | ||
65 | margin-top: 20px; | ||
66 | } | ||
67 | |||
33 | .file-metadata { | 68 | .file-metadata { |
34 | padding: 1rem; | 69 | padding: 1rem; |
35 | } | 70 | } |
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..1e3745d94 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 @@ | |||
1 | import { mapValues, pick } from 'lodash-es' | 1 | import { mapValues, pick } from 'lodash-es' |
2 | import { pipe } from 'rxjs' | ||
3 | import { tap } from 'rxjs/operators' | ||
2 | import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' | 4 | import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' |
3 | import { AuthService, Notifier } from '@app/core' | 5 | import { AuthService, HooksService, Notifier } from '@app/core' |
4 | import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
5 | import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' | 7 | import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' |
6 | import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main' | 8 | import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main' |
7 | 9 | ||
@@ -16,7 +18,7 @@ type FileMetadata = { [key: string]: { label: string, value: string }} | |||
16 | export class VideoDownloadComponent { | 18 | export 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,9 @@ 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 |
32 | |||
33 | isAdvancedCustomizationCollapsed = true | ||
30 | 34 | ||
31 | type: DownloadType = 'video' | 35 | type: DownloadType = 'video' |
32 | 36 | ||
@@ -38,7 +42,8 @@ export class VideoDownloadComponent { | |||
38 | private notifier: Notifier, | 42 | private notifier: Notifier, |
39 | private modalService: NgbModal, | 43 | private modalService: NgbModal, |
40 | private videoService: VideoService, | 44 | private videoService: VideoService, |
41 | private auth: AuthService | 45 | private auth: AuthService, |
46 | private hooks: HooksService | ||
42 | ) { | 47 | ) { |
43 | this.bytesPipe = new BytesPipe() | 48 | this.bytesPipe = new BytesPipe() |
44 | this.numbersPipe = new NumberFormatterPipe(this.localeId) | 49 | this.numbersPipe = new NumberFormatterPipe(this.localeId) |
@@ -62,9 +67,13 @@ export class VideoDownloadComponent { | |||
62 | 67 | ||
63 | this.activeModal = this.modalService.open(this.modal, { centered: true }) | 68 | this.activeModal = this.modalService.open(this.modal, { centered: true }) |
64 | 69 | ||
65 | this.resolutionId = this.getVideoFiles()[0].resolution.id | 70 | this.onResolutionIdChange(this.getVideoFiles()[0].resolution.id) |
66 | this.onResolutionIdChange() | 71 | |
67 | if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id | 72 | if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id |
73 | |||
74 | this.activeModal.shown.subscribe(() => { | ||
75 | this.hooks.runAction('action:modal.video-download.shown', 'common') | ||
76 | }) | ||
68 | } | 77 | } |
69 | 78 | ||
70 | onClose () { | 79 | onClose () { |
@@ -83,11 +92,15 @@ export class VideoDownloadComponent { | |||
83 | : this.getVideoFileLink() | 92 | : this.getVideoFileLink() |
84 | } | 93 | } |
85 | 94 | ||
86 | async onResolutionIdChange () { | 95 | async onResolutionIdChange (resolutionId: number) { |
96 | this.resolutionId = resolutionId | ||
87 | this.videoFile = this.getVideoFile() | 97 | this.videoFile = this.getVideoFile() |
88 | if (this.videoFile.metadata || !this.videoFile.metadataUrl) return | ||
89 | 98 | ||
90 | await this.hydrateMetadataFromMetadataUrl(this.videoFile) | 99 | if (!this.videoFile.metadata) { |
100 | if (!this.videoFile.metadataUrl) return | ||
101 | |||
102 | await this.hydrateMetadataFromMetadataUrl(this.videoFile) | ||
103 | } | ||
91 | 104 | ||
92 | this.videoFileMetadataFormat = this.videoFile | 105 | this.videoFileMetadataFormat = this.videoFile |
93 | ? this.getMetadataFormat(this.videoFile.metadata.format) | 106 | ? this.getMetadataFormat(this.videoFile.metadata.format) |
@@ -101,9 +114,6 @@ export class VideoDownloadComponent { | |||
101 | } | 114 | } |
102 | 115 | ||
103 | getVideoFile () { | 116 | getVideoFile () { |
104 | // HTML select send us a string, so convert it to a number | ||
105 | this.resolutionId = parseInt(this.resolutionId.toString(), 10) | ||
106 | |||
107 | const file = this.getVideoFiles().find(f => f.resolution.id === this.resolutionId) | 117 | const file = this.getVideoFiles().find(f => f.resolution.id === this.resolutionId) |
108 | if (!file) { | 118 | if (!file) { |
109 | console.error('Could not find file with resolution %d.', this.resolutionId) | 119 | console.error('Could not find file with resolution %d.', this.resolutionId) |
@@ -201,7 +211,7 @@ export class VideoDownloadComponent { | |||
201 | 211 | ||
202 | private hydrateMetadataFromMetadataUrl (file: VideoFile) { | 212 | private hydrateMetadataFromMetadataUrl (file: VideoFile) { |
203 | const observable = this.videoService.getVideoFileMetadata(file.metadataUrl) | 213 | const observable = this.videoService.getVideoFileMetadata(file.metadataUrl) |
204 | observable.subscribe(res => file.metadata = res) | 214 | .pipe(tap(res => file.metadata = res)) |
205 | 215 | ||
206 | return observable.toPromise() | 216 | return observable.toPromise() |
207 | } | 217 | } |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html index 7a6df7b64..bac8bcc2d 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html | |||
@@ -1,4 +1,4 @@ | |||
1 | <div class="video-miniature" [ngClass]="{ 'display-as-row': displayAsRow, 'fit-width': fitWidth }" (mouseenter)="loadActions()"> | 1 | <div class="video-miniature" [ngClass]="getClasses()" (mouseenter)="loadActions()"> |
2 | <my-video-thumbnail | 2 | <my-video-thumbnail |
3 | [video]="video" [nsfw]="isVideoBlur" [videoRouterLink]="videoRouterLink" [videoHref]="videoHref" [videoTarget]="videoTarget" | 3 | [video]="video" [nsfw]="isVideoBlur" [videoRouterLink]="videoRouterLink" [videoHref]="videoHref" [videoTarget]="videoTarget" |
4 | [displayWatchLaterPlaylist]="isWatchLaterPlaylistDisplayed()" [inWatchLaterPlaylist]="inWatchLaterPlaylist" (watchLaterClick)="onWatchLaterClick($event)" | 4 | [displayWatchLaterPlaylist]="isWatchLaterPlaylistDisplayed()" [inWatchLaterPlaylist]="inWatchLaterPlaylist" (watchLaterClick)="onWatchLaterClick($event)" |
@@ -9,9 +9,9 @@ | |||
9 | 9 | ||
10 | <div class="video-bottom"> | 10 | <div class="video-bottom"> |
11 | <div class="video-miniature-information"> | 11 | <div class="video-miniature-information"> |
12 | <div class="d-inline-flex video-miniature-meta"> | 12 | <div class="d-flex video-miniature-meta"> |
13 | <a *ngIf="displayOptions.avatar" class="avatar" [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> | 13 | <a *ngIf="displayOptions.avatar" class="avatar" [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> |
14 | <img [src]="getAvatarUrl()" alt="" /> | 14 | <img [src]="getAvatarUrl()" alt="" [ngClass]="{ channel: displayOwnerVideoChannel() }" /> |
15 | </a> | 15 | </a> |
16 | 16 | ||
17 | <div class="w-100 d-flex flex-column"> | 17 | <div class="w-100 d-flex flex-column"> |
@@ -33,7 +33,7 @@ | |||
33 | </span> | 33 | </span> |
34 | </span> | 34 | </span> |
35 | 35 | ||
36 | <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]"> | 36 | <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> |
37 | {{ video.byAccount }} | 37 | {{ video.byAccount }} |
38 | </a> | 38 | </a> |
39 | <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> | 39 | <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss index 38cac5b6e..621951919 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss | |||
@@ -3,198 +3,205 @@ | |||
3 | @import '_miniature'; | 3 | @import '_miniature'; |
4 | 4 | ||
5 | $more-button-width: 40px; | 5 | $more-button-width: 40px; |
6 | $more-margin-right: 15px; | ||
7 | 6 | ||
8 | .video-miniature { | 7 | .video-miniature-name { |
9 | display: inline-flex; | 8 | @include miniature-name; |
10 | flex-direction: column; | 9 | } |
11 | padding-bottom: $video-miniature-margin-bottom; | ||
12 | vertical-align: top; | ||
13 | 10 | ||
14 | .video-bottom { | 11 | .video-miniature-information { |
15 | display: flex; | 12 | width: calc(100% - #{$more-button-width}); |
13 | } | ||
16 | 14 | ||
17 | .video-miniature-information { | 15 | .avatar { |
18 | width: $video-miniature-width - $more-button-width - $more-margin-right; | 16 | margin: 10px 10px 0 0; |
19 | line-height: normal; | ||
20 | 17 | ||
21 | .avatar { | 18 | img:not(.channel) { |
22 | margin: 10px 10px 0 0; | 19 | @include avatar(40px); |
20 | } | ||
23 | 21 | ||
24 | img { | 22 | img.channel { |
25 | @include avatar(40px); | 23 | @include channel-avatar(40px); |
26 | } | 24 | } |
27 | } | 25 | } |
28 | 26 | ||
29 | .video-miniature-name { | 27 | .video-miniature-created-at-views { |
30 | @include miniature-name; | 28 | font-size: 13px; |
31 | width: calc(100% - #{$more-button-width}); | 29 | } |
32 | } | ||
33 | 30 | ||
34 | .video-miniature-meta { | 31 | .video-miniature-account, |
35 | width: calc(100% + #{$more-button-width}); | 32 | .video-miniature-channel { |
36 | overflow: hidden; | 33 | @include disable-default-a-behaviour; |
37 | } | 34 | @include ellipsis; |
38 | 35 | ||
39 | .video-miniature-created-at-views { | 36 | display: block; |
40 | display: block; | 37 | font-size: 13px; |
41 | font-size: 13px; | 38 | color: pvar(--greyForegroundColor); |
42 | } | ||
43 | 39 | ||
44 | .video-miniature-account, | 40 | &:hover { |
45 | .video-miniature-channel { | 41 | color: $grey-foreground-hover-color; |
46 | @include disable-default-a-behaviour; | 42 | } |
47 | @include ellipsis; | 43 | } |
48 | 44 | ||
49 | display: block; | 45 | .video-info-privacy, |
50 | font-size: 13px; | 46 | .video-info-blocked .blocked-label, |
51 | color: pvar(--greyForegroundColor); | 47 | .video-info-nsfw { |
48 | font-weight: $font-semibold; | ||
49 | } | ||
52 | 50 | ||
53 | &:hover { | 51 | .video-info-blocked { |
54 | color: $grey-foreground-hover-color; | 52 | color: red; |
55 | } | ||
56 | } | ||
57 | 53 | ||
58 | .video-info-privacy, | 54 | .blocked-reason::before { |
59 | .video-info-blocked .blocked-label, | 55 | content: ' - '; |
60 | .video-info-nsfw { | 56 | } |
61 | font-weight: $font-semibold; | 57 | } |
62 | } | ||
63 | 58 | ||
64 | .video-info-blocked { | 59 | .video-info-nsfw { |
65 | color: red; | 60 | color: red; |
61 | } | ||
66 | 62 | ||
67 | .blocked-reason::before { | 63 | .video-actions { |
68 | content: ' - '; | 64 | width: $more-button-width; |
69 | } | 65 | height: 30px; |
70 | } | ||
71 | 66 | ||
72 | .video-info-nsfw { | 67 | ::ng-deep .dropdown-root:not(.show) { |
73 | color: red; | 68 | opacity: 0; |
74 | } | 69 | } |
75 | } | ||
76 | 70 | ||
77 | .video-actions { | 71 | ::ng-deep .playlist-dropdown.show + my-action-dropdown .dropdown-root { |
78 | margin-top: 3px; | 72 | opacity: 1; |
79 | width: $more-button-width; | 73 | } |
80 | height: 30px; | ||
81 | 74 | ||
82 | ::ng-deep .dropdown-root:not(.show) { | 75 | ::ng-deep .more-icon { |
83 | opacity: 0; | 76 | opacity: .6; |
84 | } | ||
85 | 77 | ||
86 | ::ng-deep .playlist-dropdown.show + my-action-dropdown .dropdown-root { | 78 | &:hover { |
87 | opacity: 1; | 79 | opacity: 1; |
88 | } | 80 | } |
81 | } | ||
82 | } | ||
89 | 83 | ||
90 | ::ng-deep .more-icon { | 84 | .video-miniature { |
91 | opacity: .6; | 85 | &:hover ::ng-deep .video-thumbnail-actions-overlay, |
86 | &:hover .video-actions ::ng-deep .dropdown-root { | ||
87 | opacity: 1 !important; | ||
88 | } | ||
89 | } | ||
92 | 90 | ||
93 | &:hover { | 91 | // Grid mode |
94 | opacity: 1; | 92 | // Takes all the width on mobile |
95 | } | 93 | .video-miniature:not(.display-as-row) { |
96 | } | 94 | display: flex; |
97 | } | 95 | flex-direction: column; |
96 | padding-bottom: $video-miniature-margin-bottom; | ||
97 | width: 100%; | ||
98 | 98 | ||
99 | @media screen and (max-width: $small-view) { | 99 | my-video-thumbnail { |
100 | .video-miniature-information { | 100 | @include block-ratio($selector: '::ng-deep .video-thumbnail'); |
101 | margin: 0 10px; | 101 | } |
102 | } | ||
103 | 102 | ||
104 | .video-actions { | 103 | .video-bottom { |
105 | margin: 0; | 104 | display: flex; |
106 | top: -3px; | 105 | width: 100%; |
106 | } | ||
107 | 107 | ||
108 | ::ng-deep .dropdown-root { | 108 | .video-miniature-name { |
109 | opacity: 1 !important; | 109 | margin-top: 10px; |
110 | } | 110 | margin-bottom: 5px; |
111 | } | ||
112 | } | ||
113 | } | 111 | } |
114 | 112 | ||
115 | &:hover ::ng-deep .video-thumbnail .video-thumbnail-actions-overlay, | 113 | .video-miniature-created-at-views { |
116 | &:hover .video-bottom .video-actions ::ng-deep .dropdown-root { | 114 | display: block; |
117 | opacity: 1; | ||
118 | } | 115 | } |
119 | 116 | ||
120 | &.fit-width { | 117 | .video-actions { |
118 | margin-top: 3px; | ||
119 | } | ||
120 | |||
121 | @media screen and (max-width: $small-view) { | ||
121 | width: 100%; | 122 | width: 100%; |
123 | margin-bottom: 25px; | ||
124 | |||
125 | .video-miniature-information { | ||
126 | margin: 0 10px; | ||
127 | |||
128 | width: 100%; | ||
129 | text-align: left; | ||
130 | } | ||
122 | 131 | ||
123 | .video-bottom { | 132 | .video-actions { |
124 | width: 100% !important; | 133 | margin: 0; |
134 | top: -3px; | ||
125 | 135 | ||
126 | .video-miniature-information { | 136 | ::ng-deep .dropdown-root { |
127 | width: calc(100% - #{$more-button-width}) !important; | 137 | opacity: 1 !important; |
128 | } | 138 | } |
129 | } | 139 | } |
130 | 140 | ||
131 | my-video-thumbnail { | 141 | ::ng-deep .video-thumbnail { |
132 | @include large-screen-ratio($selector: '::ng-deep .video-thumbnail'); | 142 | border-radius: 0; |
133 | } | 143 | } |
134 | } | 144 | } |
145 | } | ||
146 | |||
147 | .video-miniature.display-as-row { | ||
148 | --rowThumbnailWidth: #{$video-thumbnail-width}; | ||
149 | --rowThumbnailHeight: #{$video-thumbnail-height}; | ||
150 | |||
151 | display: flex; | ||
152 | flex-direction: row; | ||
135 | 153 | ||
136 | &.display-as-row { | 154 | .video-bottom { |
137 | flex-direction: row; | ||
138 | padding-bottom: 0; | ||
139 | height: auto; | ||
140 | display: flex; | 155 | display: flex; |
141 | flex-grow: 1; | 156 | } |
142 | 157 | ||
143 | my-video-thumbnail { | 158 | // We don't display avatar in row mode |
144 | margin-right: 10px; | 159 | .avatar { |
145 | } | 160 | display: none; |
161 | } | ||
146 | 162 | ||
147 | .video-bottom { | 163 | my-video-thumbnail { |
148 | .video-miniature-information { | 164 | min-width: var(--rowThumbnailWidth); |
149 | @media screen and (min-width: $small-view) { | 165 | max-width: var(--rowThumbnailWidth); |
150 | width: auto; | 166 | height: var(--rowThumbnailHeight); |
151 | min-width: 500px; | 167 | margin-right: 10px; |
152 | } | 168 | } |
153 | |||
154 | .video-miniature-name { | ||
155 | @include ellipsis-multiline(1.3em, 2); | ||
156 | |||
157 | margin-top: 2px; | ||
158 | margin-bottom: 5px; | ||
159 | } | ||
160 | |||
161 | .video-miniature-created-at-views, | ||
162 | .video-miniature-account, | ||
163 | .video-miniature-channel { | ||
164 | font-size: 95%; | ||
165 | width: fit-content; | ||
166 | } | ||
167 | |||
168 | .video-miniature-created-at-views + .video-miniature-channel { | ||
169 | margin-top: 5px; | ||
170 | } | ||
171 | |||
172 | .video-info-privacy { | ||
173 | margin-top: 5px; | ||
174 | } | ||
175 | |||
176 | .video-info-blocked { | ||
177 | margin-top: 3px; | ||
178 | } | ||
179 | } | ||
180 | 169 | ||
181 | .video-actions { | 170 | .video-miniature-name { |
182 | margin: 0; | 171 | @include ellipsis-multiline($video-miniature-row-name-font-size, 2); |
183 | top: -3px; | 172 | } |
184 | } | 173 | |
185 | } | 174 | .video-miniature-created-at-views, |
175 | .video-miniature-account, | ||
176 | .video-miniature-channel { | ||
177 | font-size: $video-miniature-row-info-font-size; | ||
178 | } | ||
186 | 179 | ||
187 | @media screen and (max-width: $small-view) { | 180 | .video-actions { |
188 | flex-direction: column; | 181 | margin-top: -3px; |
189 | height: auto; | 182 | } |
183 | } | ||
190 | 184 | ||
191 | my-video-thumbnail { | 185 | @include on-small-main-col { |
192 | margin-right: 0; | 186 | .video-miniature.display-as-row { |
193 | } | 187 | --rowThumbnailWidth: #{$video-thumbnail-medium-width}; |
188 | --rowThumbnailHeight: #{$video-thumbnail-medium-height}; | ||
189 | } | ||
190 | } | ||
194 | 191 | ||
195 | .video-miniature-information { | 192 | @include on-mobile-main-col { |
196 | min-width: initial; | 193 | .video-miniature.display-as-row { |
197 | } | 194 | --rowThumbnailWidth: #{$video-thumbnail-small-width}; |
195 | --rowThumbnailHeight: #{$video-thumbnail-small-height}; | ||
196 | |||
197 | .video-miniature-name { | ||
198 | font-size: $video-miniature-row-info-font-size; | ||
199 | } | ||
200 | |||
201 | .video-miniature-created-at-views, | ||
202 | .video-miniature-account, | ||
203 | .video-miniature-channel { | ||
204 | font-size: $video-miniature-row-mobile-info-font-size; | ||
198 | } | 205 | } |
199 | } | 206 | } |
200 | } | 207 | } |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts index cc5665ab1..48da92d6b 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts | |||
@@ -16,7 +16,6 @@ import { Video } from '../shared-main' | |||
16 | import { VideoPlaylistService } from '../shared-video-playlist' | 16 | import { VideoPlaylistService } from '../shared-video-playlist' |
17 | import { VideoActionsDisplayType } from './video-actions-dropdown.component' | 17 | import { VideoActionsDisplayType } from './video-actions-dropdown.component' |
18 | 18 | ||
19 | export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto' | ||
20 | export type MiniatureDisplayOptions = { | 19 | export type MiniatureDisplayOptions = { |
21 | date?: boolean | 20 | date?: boolean |
22 | views?: boolean | 21 | views?: boolean |
@@ -40,7 +39,6 @@ export class VideoMiniatureComponent implements OnInit { | |||
40 | @Input() user: User | 39 | @Input() user: User |
41 | @Input() video: Video | 40 | @Input() video: Video |
42 | 41 | ||
43 | @Input() ownerDisplayType: OwnerDisplayType = 'account' | ||
44 | @Input() displayOptions: MiniatureDisplayOptions = { | 42 | @Input() displayOptions: MiniatureDisplayOptions = { |
45 | date: true, | 43 | date: true, |
46 | views: true, | 44 | views: true, |
@@ -51,9 +49,9 @@ export class VideoMiniatureComponent implements OnInit { | |||
51 | state: false, | 49 | state: false, |
52 | blacklistInfo: false | 50 | blacklistInfo: false |
53 | } | 51 | } |
54 | @Input() displayAsRow = false | ||
55 | @Input() displayVideoActions = true | 52 | @Input() displayVideoActions = true |
56 | @Input() fitWidth = false | 53 | |
54 | @Input() displayAsRow = false | ||
57 | 55 | ||
58 | @Input() videoLinkType: VideoLinkType = 'internal' | 56 | @Input() videoLinkType: VideoLinkType = 'internal' |
59 | 57 | ||
@@ -89,7 +87,7 @@ export class VideoMiniatureComponent implements OnInit { | |||
89 | videoHref: string | 87 | videoHref: string |
90 | videoTarget: string | 88 | videoTarget: string |
91 | 89 | ||
92 | private ownerDisplayTypeChosen: 'account' | 'videoChannel' | 90 | private ownerDisplayType: 'account' | 'videoChannel' |
93 | 91 | ||
94 | constructor ( | 92 | constructor ( |
95 | private screenService: ScreenService, | 93 | private screenService: ScreenService, |
@@ -140,11 +138,11 @@ export class VideoMiniatureComponent implements OnInit { | |||
140 | } | 138 | } |
141 | 139 | ||
142 | displayOwnerAccount () { | 140 | displayOwnerAccount () { |
143 | return this.ownerDisplayTypeChosen === 'account' | 141 | return this.ownerDisplayType === 'account' |
144 | } | 142 | } |
145 | 143 | ||
146 | displayOwnerVideoChannel () { | 144 | displayOwnerVideoChannel () { |
147 | return this.ownerDisplayTypeChosen === 'videoChannel' | 145 | return this.ownerDisplayType === 'videoChannel' |
148 | } | 146 | } |
149 | 147 | ||
150 | isUnlistedVideo () { | 148 | isUnlistedVideo () { |
@@ -183,7 +181,7 @@ export class VideoMiniatureComponent implements OnInit { | |||
183 | } | 181 | } |
184 | 182 | ||
185 | getAvatarUrl () { | 183 | getAvatarUrl () { |
186 | if (this.ownerDisplayTypeChosen === 'account') { | 184 | if (this.displayOwnerAccount()) { |
187 | return this.video.accountAvatarUrl | 185 | return this.video.accountAvatarUrl |
188 | } | 186 | } |
189 | 187 | ||
@@ -244,21 +242,26 @@ export class VideoMiniatureComponent implements OnInit { | |||
244 | return this.displayVideoActions && this.isUserLoggedIn() && this.inWatchLaterPlaylist !== undefined | 242 | return this.displayVideoActions && this.isUserLoggedIn() && this.inWatchLaterPlaylist !== undefined |
245 | } | 243 | } |
246 | 244 | ||
247 | private setUpBy () { | 245 | getClasses () { |
248 | if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') { | 246 | return { |
249 | this.ownerDisplayTypeChosen = this.ownerDisplayType | 247 | 'display-as-row': this.displayAsRow |
250 | return | ||
251 | } | 248 | } |
249 | } | ||
250 | |||
251 | private setUpBy () { | ||
252 | const accountName = this.video.account.name | ||
252 | 253 | ||
253 | // If the video channel name an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12) | 254 | // If the video channel name is an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12) |
255 | // Or has not been customized (default created channel display name) | ||
254 | // -> Use the account name | 256 | // -> Use the account name |
255 | if ( | 257 | if ( |
256 | this.video.channel.name === `${this.video.account.name}_channel` || | 258 | this.video.channel.displayName === `Default ${accountName} channel` || |
259 | this.video.channel.displayName === `Main ${accountName} channel` || | ||
257 | this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) | 260 | this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) |
258 | ) { | 261 | ) { |
259 | this.ownerDisplayTypeChosen = 'account' | 262 | this.ownerDisplayType = 'account' |
260 | } else { | 263 | } else { |
261 | this.ownerDisplayTypeChosen = 'videoChannel' | 264 | this.ownerDisplayType = 'videoChannel' |
262 | } | 265 | } |
263 | } | 266 | } |
264 | 267 | ||
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.html b/client/src/app/shared/shared-video-miniature/videos-selection.component.html index 8caeaf092..dec9e99f3 100644 --- a/client/src/app/shared/shared-video-miniature/videos-selection.component.html +++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.html | |||
@@ -9,8 +9,7 @@ | |||
9 | 9 | ||
10 | <my-video-miniature | 10 | <my-video-miniature |
11 | [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions" | 11 | [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions" |
12 | [displayVideoActions]="false" [ownerDisplayType]="ownerDisplayType" | 12 | [displayVideoActions]="false" [user]="user" |
13 | [user]="user" | ||
14 | ></my-video-miniature> | 13 | ></my-video-miniature> |
15 | 14 | ||
16 | <!-- Display only once --> | 15 | <!-- Display only once --> |
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.scss b/client/src/app/shared/shared-video-miniature/videos-selection.component.scss index c33e11889..a2939d521 100644 --- a/client/src/app/shared/shared-video-miniature/videos-selection.component.scss +++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.scss | |||
@@ -5,24 +5,24 @@ | |||
5 | display: flex; | 5 | display: flex; |
6 | justify-content: flex-end; | 6 | justify-content: flex-end; |
7 | flex-grow: 1; | 7 | flex-grow: 1; |
8 | } | ||
8 | 9 | ||
9 | .action-selection-mode-child { | 10 | .action-selection-mode-child { |
10 | position: fixed; | 11 | position: fixed; |
11 | |||
12 | .action-button { | ||
13 | display: block; | ||
14 | margin-left: 55px; | ||
15 | } | ||
16 | 12 | ||
17 | .action-button-cancel-selection { | 13 | .action-button { |
18 | @include peertube-button; | 14 | display: block; |
19 | @include grey-button; | 15 | margin-left: 55px; |
20 | } | ||
21 | } | 16 | } |
22 | } | 17 | } |
23 | 18 | ||
19 | .action-button-cancel-selection { | ||
20 | @include peertube-button; | ||
21 | @include grey-button; | ||
22 | } | ||
23 | |||
24 | .video { | 24 | .video { |
25 | @include row-blocks; | 25 | @include row-blocks($column-responsive: false); |
26 | 26 | ||
27 | &:first-child { | 27 | &:first-child { |
28 | margin-top: 47px; | 28 | margin-top: 47px; |
@@ -40,18 +40,16 @@ | |||
40 | } | 40 | } |
41 | } | 41 | } |
42 | 42 | ||
43 | @media screen and (max-width: $small-view) { | ||
44 | .video { | ||
45 | flex-direction: column; | ||
46 | height: auto; | ||
47 | 43 | ||
48 | .checkbox-container { | 44 | @include on-small-main-col { |
49 | display: none; | 45 | .video { |
50 | } | 46 | flex-wrap: wrap; |
47 | } | ||
48 | } | ||
51 | 49 | ||
52 | my-button { | 50 | @include on-mobile-main-col { |
53 | margin-top: 10px; | 51 | .checkbox-container { |
54 | } | 52 | display: none; |
55 | } | 53 | } |
56 | 54 | ||
57 | .action-selection-mode { | 55 | .action-selection-mode { |
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts index ca1cf2264..f8c3800d7 100644 --- a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts +++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts | |||
@@ -17,7 +17,7 @@ import { AuthService, ComponentPagination, LocalStorageService, Notifier, Screen | |||
17 | import { ResultList, VideoSortField } from '@shared/models' | 17 | import { ResultList, VideoSortField } from '@shared/models' |
18 | import { PeerTubeTemplateDirective, Video } from '../shared-main' | 18 | import { PeerTubeTemplateDirective, Video } from '../shared-main' |
19 | import { AbstractVideoList } from './abstract-video-list' | 19 | import { AbstractVideoList } from './abstract-video-list' |
20 | import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component' | 20 | import { MiniatureDisplayOptions } from './video-miniature.component' |
21 | 21 | ||
22 | export type SelectionType = { [ id: number ]: boolean } | 22 | export type SelectionType = { [ id: number ]: boolean } |
23 | 23 | ||
@@ -31,7 +31,6 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni | |||
31 | @Input() pagination: ComponentPagination | 31 | @Input() pagination: ComponentPagination |
32 | @Input() titlePage: string | 32 | @Input() titlePage: string |
33 | @Input() miniatureDisplayOptions: MiniatureDisplayOptions | 33 | @Input() miniatureDisplayOptions: MiniatureDisplayOptions |
34 | @Input() ownerDisplayType: OwnerDisplayType | ||
35 | 34 | ||
36 | @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>> | 35 | @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>> |
37 | 36 | ||
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html index 86f6664cb..f50f95003 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html +++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html | |||
@@ -1,4 +1,4 @@ | |||
1 | <div class="miniature" [ngClass]="{ 'no-videos': playlist.videosLength === 0, 'to-manage': toManage }"> | 1 | <div class="miniature" [ngClass]="{ 'no-videos': playlist.videosLength === 0, 'to-manage': toManage, 'display-as-row': displayAsRow }"> |
2 | <a | 2 | <a |
3 | [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description" | 3 | [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description" |
4 | class="miniature-thumbnail" | 4 | class="miniature-thumbnail" |
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss index 1b16dbb01..c5be5f292 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss +++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss | |||
@@ -4,6 +4,7 @@ | |||
4 | 4 | ||
5 | .miniature { | 5 | .miniature { |
6 | display: inline-block; | 6 | display: inline-block; |
7 | width: 100%; | ||
7 | 8 | ||
8 | &.no-videos:not(.to-manage){ | 9 | &.no-videos:not(.to-manage){ |
9 | a { | 10 | a { |
@@ -17,62 +18,92 @@ | |||
17 | display: none; | 18 | display: none; |
18 | } | 19 | } |
19 | } | 20 | } |
21 | } | ||
20 | 22 | ||
21 | .miniature-thumbnail { | 23 | .miniature-thumbnail { |
22 | @include miniature-thumbnail; | 24 | @include miniature-thumbnail; |
23 | 25 | ||
24 | .miniature-playlist-info-overlay { | 26 | .miniature-playlist-info-overlay { |
25 | @include static-thumbnail-overlay; | 27 | @include static-thumbnail-overlay; |
26 | 28 | ||
27 | position: absolute; | 29 | position: absolute; |
28 | right: 0; | 30 | right: 0; |
29 | bottom: 0; | 31 | bottom: 0; |
30 | height: $video-thumbnail-height; | 32 | height: 100%; |
31 | padding: 0 10px; | 33 | padding: 0 10px; |
32 | display: flex; | 34 | display: flex; |
33 | align-items: center; | 35 | align-items: center; |
34 | font-size: 14px; | 36 | font-size: 14px; |
35 | font-weight: $font-semibold; | 37 | font-weight: $font-semibold; |
36 | } | ||
37 | } | 38 | } |
39 | } | ||
38 | 40 | ||
39 | .miniature-info { | 41 | .miniature-info { |
40 | width: 200px; | ||
41 | margin-top: 2px; | ||
42 | line-height: normal; | ||
43 | |||
44 | .miniature-name { | ||
45 | @include miniature-name; | ||
46 | 42 | ||
47 | @include ellipsis-multiline(1.3em, 2); | 43 | .miniature-name { |
44 | @include miniature-name; | ||
45 | @include ellipsis-multiline(1.3em, 2); | ||
48 | 46 | ||
49 | margin: 0; | 47 | margin: 0; |
50 | } | 48 | } |
51 | 49 | ||
52 | .by { | 50 | .by { |
53 | @include disable-default-a-behaviour; | 51 | @include disable-default-a-behaviour; |
54 | 52 | ||
55 | display: block; | 53 | display: block; |
56 | color: pvar(--greyForegroundColor); | 54 | color: pvar(--greyForegroundColor); |
57 | } | 55 | } |
58 | 56 | ||
59 | .privacy-date { | 57 | .privacy-date { |
60 | margin-top: 5px; | 58 | margin-top: 5px; |
61 | 59 | ||
62 | .video-info-privacy { | 60 | .video-info-privacy { |
63 | font-size: 14px; | 61 | font-size: 14px; |
64 | font-weight: $font-semibold; | 62 | font-weight: $font-semibold; |
65 | 63 | ||
66 | &::after { | 64 | &::after { |
67 | content: '-'; | 65 | content: '-'; |
68 | margin: 0 3px; | 66 | margin: 0 3px; |
69 | } | ||
70 | } | 67 | } |
71 | } | 68 | } |
69 | } | ||
72 | 70 | ||
73 | .video-info-description { | 71 | .video-info-description { |
74 | margin-top: 10px; | 72 | margin-top: 10px; |
75 | color: pvar(--greyForegroundColor); | 73 | color: pvar(--greyForegroundColor); |
76 | } | 74 | } |
75 | } | ||
76 | |||
77 | .miniature:not(.display-as-row) { | ||
78 | .miniature-thumbnail { | ||
79 | margin-top: 10px; | ||
80 | margin-bottom: 5px; | ||
81 | } | ||
82 | } | ||
83 | |||
84 | .miniature.display-as-row { | ||
85 | --rowThumbnailWidth: #{$video-thumbnail-width}; | ||
86 | --rowThumbnailHeight: #{$video-thumbnail-height}; | ||
87 | |||
88 | display: flex; | ||
89 | |||
90 | .miniature-thumbnail { | ||
91 | width: var(--rowThumbnailWidth); | ||
92 | height: var(--rowThumbnailHeight); | ||
93 | margin-right: 10px; | ||
94 | } | ||
95 | } | ||
96 | |||
97 | @include on-small-main-col { | ||
98 | .miniature.display-as-row { | ||
99 | --rowThumbnailWidth: #{$video-thumbnail-medium-width}; | ||
100 | --rowThumbnailHeight: #{$video-thumbnail-medium-height}; | ||
101 | } | ||
102 | } | ||
103 | |||
104 | @include on-mobile-main-col { | ||
105 | .miniature.display-as-row { | ||
106 | --rowThumbnailWidth: #{$video-thumbnail-small-width}; | ||
107 | --rowThumbnailHeight: #{$video-thumbnail-small-height}; | ||
77 | } | 108 | } |
78 | } | 109 | } |
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts index 251aa868a..6b0b1056f 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts +++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts | |||
@@ -12,6 +12,7 @@ export class VideoPlaylistMiniatureComponent { | |||
12 | @Input() displayChannel = false | 12 | @Input() displayChannel = false |
13 | @Input() displayDescription = false | 13 | @Input() displayDescription = false |
14 | @Input() displayPrivacy = false | 14 | @Input() displayPrivacy = false |
15 | @Input() displayAsRow = false | ||
15 | 16 | ||
16 | getPlaylistUrl () { | 17 | getPlaylistUrl () { |
17 | if (this.toManage) return [ '/my-library/video-playlists', this.playlist.uuid ] | 18 | if (this.toManage) return [ '/my-library/video-playlists', this.playlist.uuid ] |
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/assets/player/peertube-player-local-storage.ts b/client/src/assets/player/peertube-player-local-storage.ts index 75ccfe618..cf2cfb472 100644 --- a/client/src/assets/player/peertube-player-local-storage.ts +++ b/client/src/assets/player/peertube-player-local-storage.ts | |||
@@ -68,6 +68,51 @@ function getStoredLastSubtitle () { | |||
68 | return getLocalStorage('last-subtitle') | 68 | return getLocalStorage('last-subtitle') |
69 | } | 69 | } |
70 | 70 | ||
71 | function saveVideoWatchHistory(videoUUID: string, duration: number) { | ||
72 | return setLocalStorage(`video-watch-history`, JSON.stringify({ | ||
73 | ...getStoredVideoWatchHistory(), | ||
74 | [videoUUID]: { | ||
75 | duration, | ||
76 | date: `${(new Date()).toISOString()}` | ||
77 | } | ||
78 | })) | ||
79 | } | ||
80 | |||
81 | function getStoredVideoWatchHistory(videoUUID?: string) { | ||
82 | let data | ||
83 | |||
84 | try { | ||
85 | data = JSON.parse(getLocalStorage('video-watch-history')) | ||
86 | } catch (error) { | ||
87 | console.error('Cannot parse video watch history from local storage: ', error) | ||
88 | } | ||
89 | |||
90 | data = data || {} | ||
91 | |||
92 | if (videoUUID) return data[videoUUID] | ||
93 | |||
94 | return data | ||
95 | } | ||
96 | |||
97 | function cleanupVideoWatch() { | ||
98 | const data = getStoredVideoWatchHistory() | ||
99 | |||
100 | const newData = Object.keys(data).reduce((acc, videoUUID) => { | ||
101 | const date = Date.parse(data[videoUUID].date) | ||
102 | |||
103 | const diff = Math.ceil(((new Date()).getTime() - date) / (1000 * 3600 * 24)) | ||
104 | |||
105 | if (diff > 30) return acc | ||
106 | |||
107 | return { | ||
108 | ...acc, | ||
109 | [videoUUID]: data[videoUUID] | ||
110 | } | ||
111 | }, {}) | ||
112 | |||
113 | setLocalStorage('video-watch-history', JSON.stringify(newData)) | ||
114 | } | ||
115 | |||
71 | // --------------------------------------------------------------------------- | 116 | // --------------------------------------------------------------------------- |
72 | 117 | ||
73 | export { | 118 | export { |
@@ -81,7 +126,10 @@ export { | |||
81 | saveAverageBandwidth, | 126 | saveAverageBandwidth, |
82 | getAverageBandwidthInStore, | 127 | getAverageBandwidthInStore, |
83 | saveLastSubtitle, | 128 | saveLastSubtitle, |
84 | getStoredLastSubtitle | 129 | getStoredLastSubtitle, |
130 | saveVideoWatchHistory, | ||
131 | getStoredVideoWatchHistory, | ||
132 | cleanupVideoWatch | ||
85 | } | 133 | } |
86 | 134 | ||
87 | // --------------------------------------------------------------------------- | 135 | // --------------------------------------------------------------------------- |
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 2cbef48ea..119dec379 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -98,6 +98,7 @@ export interface CommonOptions extends CustomizationOptions { | |||
98 | 98 | ||
99 | videoViewUrl: string | 99 | videoViewUrl: string |
100 | embedUrl: string | 100 | embedUrl: string |
101 | embedTitle: string | ||
101 | 102 | ||
102 | isLive: boolean | 103 | isLive: boolean |
103 | 104 | ||
@@ -105,6 +106,8 @@ export interface CommonOptions extends CustomizationOptions { | |||
105 | 106 | ||
106 | videoCaptions: VideoJSCaption[] | 107 | videoCaptions: VideoJSCaption[] |
107 | 108 | ||
109 | videoUUID: string | ||
110 | |||
108 | userWatching?: UserWatching | 111 | userWatching?: UserWatching |
109 | 112 | ||
110 | serverUrl: string | 113 | serverUrl: string |
@@ -165,7 +168,7 @@ export class PeertubePlayerManager { | |||
165 | PeertubePlayerManager.alreadyPlayed = true | 168 | PeertubePlayerManager.alreadyPlayed = true |
166 | }) | 169 | }) |
167 | 170 | ||
168 | self.addContextMenu(mode, player, options.common.embedUrl) | 171 | self.addContextMenu(mode, player, options.common.embedUrl, options.common.embedTitle) |
169 | 172 | ||
170 | player.bezels() | 173 | player.bezels() |
171 | 174 | ||
@@ -203,7 +206,7 @@ export class PeertubePlayerManager { | |||
203 | videojs(newVideoElement, videojsOptions, function (this: videojs.Player) { | 206 | videojs(newVideoElement, videojsOptions, function (this: videojs.Player) { |
204 | const player = this | 207 | const player = this |
205 | 208 | ||
206 | self.addContextMenu(mode, player, options.common.embedUrl) | 209 | self.addContextMenu(mode, player, options.common.embedUrl, options.common.embedTitle) |
207 | 210 | ||
208 | PeertubePlayerManager.onPlayerChange(player) | 211 | PeertubePlayerManager.onPlayerChange(player) |
209 | }) | 212 | }) |
@@ -230,7 +233,8 @@ export class PeertubePlayerManager { | |||
230 | subtitle: commonOptions.subtitle, | 233 | subtitle: commonOptions.subtitle, |
231 | videoCaptions: commonOptions.videoCaptions, | 234 | videoCaptions: commonOptions.videoCaptions, |
232 | stopTime: commonOptions.stopTime, | 235 | stopTime: commonOptions.stopTime, |
233 | isLive: commonOptions.isLive | 236 | isLive: commonOptions.isLive, |
237 | videoUUID: commonOptions.videoUUID | ||
234 | } | 238 | } |
235 | } | 239 | } |
236 | 240 | ||
@@ -271,7 +275,7 @@ export class PeertubePlayerManager { | |||
271 | 275 | ||
272 | poster: commonOptions.poster, | 276 | poster: commonOptions.poster, |
273 | inactivityTimeout: commonOptions.inactivityTimeout, | 277 | inactivityTimeout: commonOptions.inactivityTimeout, |
274 | playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ], | 278 | playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ], |
275 | 279 | ||
276 | plugins, | 280 | plugins, |
277 | 281 | ||
@@ -492,7 +496,7 @@ export class PeertubePlayerManager { | |||
492 | return children | 496 | return children |
493 | } | 497 | } |
494 | 498 | ||
495 | private static addContextMenu (mode: PlayerMode, player: videojs.Player, videoEmbedUrl: string) { | 499 | private static addContextMenu (mode: PlayerMode, player: videojs.Player, videoEmbedUrl: string, videoEmbedTitle: string) { |
496 | const content = [ | 500 | const content = [ |
497 | { | 501 | { |
498 | label: player.localize('Copy the video URL'), | 502 | label: player.localize('Copy the video URL'), |
@@ -509,7 +513,7 @@ export class PeertubePlayerManager { | |||
509 | { | 513 | { |
510 | label: player.localize('Copy embed code'), | 514 | label: player.localize('Copy embed code'), |
511 | listener: () => { | 515 | listener: () => { |
512 | copyToClipboard(buildVideoOrPlaylistEmbed(videoEmbedUrl)) | 516 | copyToClipboard(buildVideoOrPlaylistEmbed(videoEmbedUrl, videoEmbedTitle)) |
513 | } | 517 | } |
514 | } | 518 | } |
515 | ] | 519 | ] |
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts index 75a6e662e..07c7e33f6 100644 --- a/client/src/assets/player/peertube-plugin.ts +++ b/client/src/assets/player/peertube-plugin.ts | |||
@@ -13,6 +13,7 @@ import { | |||
13 | getStoredVolume, | 13 | getStoredVolume, |
14 | saveLastSubtitle, | 14 | saveLastSubtitle, |
15 | saveMuteInStore, | 15 | saveMuteInStore, |
16 | saveVideoWatchHistory, | ||
16 | saveVolumeInStore | 17 | saveVolumeInStore |
17 | } from './peertube-player-local-storage' | 18 | } from './peertube-player-local-storage' |
18 | 19 | ||
@@ -120,7 +121,7 @@ class PeerTubePlugin extends Plugin { | |||
120 | this.initializePlayer() | 121 | this.initializePlayer() |
121 | this.runViewAdd() | 122 | this.runViewAdd() |
122 | 123 | ||
123 | if (options.userWatching) this.runUserWatchVideo(options.userWatching) | 124 | this.runUserWatchVideo(options.userWatching, options.videoUUID) |
124 | }) | 125 | }) |
125 | } | 126 | } |
126 | 127 | ||
@@ -178,7 +179,7 @@ class PeerTubePlugin extends Plugin { | |||
178 | }, 1000) | 179 | }, 1000) |
179 | } | 180 | } |
180 | 181 | ||
181 | private runUserWatchVideo (options: UserWatching) { | 182 | private runUserWatchVideo (options: UserWatching, videoUUID: string) { |
182 | let lastCurrentTime = 0 | 183 | let lastCurrentTime = 0 |
183 | 184 | ||
184 | this.userWatchingVideoInterval = setInterval(() => { | 185 | this.userWatchingVideoInterval = setInterval(() => { |
@@ -187,8 +188,12 @@ class PeerTubePlugin extends Plugin { | |||
187 | if (currentTime - lastCurrentTime >= 1) { | 188 | if (currentTime - lastCurrentTime >= 1) { |
188 | lastCurrentTime = currentTime | 189 | lastCurrentTime = currentTime |
189 | 190 | ||
190 | this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader) | 191 | if (options) { |
191 | .catch(err => console.error('Cannot notify user is watching.', err)) | 192 | this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader) |
193 | .catch(err => console.error('Cannot notify user is watching.', err)) | ||
194 | } else { | ||
195 | saveVideoWatchHistory(videoUUID, currentTime) | ||
196 | } | ||
192 | } | 197 | } |
193 | }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL) | 198 | }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL) |
194 | } | 199 | } |
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts index e5259092c..4a6c80247 100644 --- a/client/src/assets/player/peertube-videojs-typings.ts +++ b/client/src/assets/player/peertube-videojs-typings.ts | |||
@@ -108,6 +108,8 @@ type PeerTubePluginOptions = { | |||
108 | stopTime: number | string | 108 | stopTime: number | string |
109 | 109 | ||
110 | isLive: boolean | 110 | isLive: boolean |
111 | |||
112 | videoUUID: string | ||
111 | } | 113 | } |
112 | 114 | ||
113 | type PlaylistPluginOptions = { | 115 | type PlaylistPluginOptions = { |
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts index 6767459ce..d7451fa1d 100644 --- a/client/src/assets/player/utils.ts +++ b/client/src/assets/player/utils.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { VideoFile } from '@shared/models' | 1 | import { VideoFile } from '@shared/models' |
2 | import { escapeHTML } from '@shared/core-utils/renderer' | ||
2 | 3 | ||
3 | function toTitleCase (str: string) { | 4 | function toTitleCase (str: string) { |
4 | return str.charAt(0).toUpperCase() + str.slice(1) | 5 | return str.charAt(0).toUpperCase() + str.slice(1) |
@@ -170,9 +171,11 @@ function secondsToTime (seconds: number, full = false, symbol?: string) { | |||
170 | return time | 171 | return time |
171 | } | 172 | } |
172 | 173 | ||
173 | function buildVideoOrPlaylistEmbed (embedUrl: string) { | 174 | function buildVideoOrPlaylistEmbed (embedUrl: string, embedTitle: string) { |
175 | const title = escapeHTML(embedTitle) | ||
174 | return '<iframe width="560" height="315" ' + | 176 | return '<iframe width="560" height="315" ' + |
175 | 'sandbox="allow-same-origin allow-scripts allow-popups" ' + | 177 | 'sandbox="allow-same-origin allow-scripts allow-popups" ' + |
178 | 'title="' + title + '" ' + | ||
176 | 'src="' + embedUrl + '" ' + | 179 | 'src="' + embedUrl + '" ' + |
177 | 'frameborder="0" allowfullscreen>' + | 180 | 'frameborder="0" allowfullscreen>' + |
178 | '</iframe>' | 181 | '</iframe>' |
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss index a0009eecc..c01938147 100644 --- a/client/src/sass/application.scss +++ b/client/src/sass/application.scss | |||
@@ -15,6 +15,8 @@ $assets-path: '../../assets/'; | |||
15 | @import './primeng-custom'; | 15 | @import './primeng-custom'; |
16 | @import './ng-select.scss'; | 16 | @import './ng-select.scss'; |
17 | 17 | ||
18 | @import './classes.scss'; | ||
19 | |||
18 | [hidden] { | 20 | [hidden] { |
19 | display: none !important; | 21 | display: none !important; |
20 | } | 22 | } |
@@ -36,7 +38,9 @@ body { | |||
36 | 38 | ||
37 | --menuBackgroundColor: #{$menu-background}; | 39 | --menuBackgroundColor: #{$menu-background}; |
38 | --menuForegroundColor: #{$menu-color}; | 40 | --menuForegroundColor: #{$menu-color}; |
39 | --submenuColor: #{$sub-menu-color}; | 41 | |
42 | --submenuBackgroundColor: #{$sub-menu-background-color}; | ||
43 | --channelBackgroundColor: #{$channel-background-color}; | ||
40 | 44 | ||
41 | --inputForegroundColor: #{$input-foreground-color}; | 45 | --inputForegroundColor: #{$input-foreground-color}; |
42 | --inputBackgroundColor: #{$input-background-color}; | 46 | --inputBackgroundColor: #{$input-background-color}; |
@@ -53,7 +57,9 @@ body { | |||
53 | 57 | ||
54 | --activatedActionButtonColor: #{$activated-action-button-color}; | 58 | --activatedActionButtonColor: #{$activated-action-button-color}; |
55 | 59 | ||
56 | --expanded-horizontal-margin-content: #{$expanded-horizontal-margins}; | 60 | --horizontalMarginContent: #{$not-expanded-horizontal-margins}; |
61 | --videosHorizontalMarginContent: 6vw; | ||
62 | --mainColWidth: calc(100vw - #{$menu-width}); | ||
57 | 63 | ||
58 | font-family: $main-fonts; | 64 | font-family: $main-fonts; |
59 | font-weight: $font-regular; | 65 | font-weight: $font-regular; |
@@ -146,24 +152,26 @@ my-input-toggle-hidden ::ng-deep input { | |||
146 | outline: none; | 152 | outline: none; |
147 | 153 | ||
148 | .margin-content { | 154 | .margin-content { |
149 | margin-left: $not-expanded-horizontal-margins; | 155 | margin-left: pvar(--horizontalMarginContent); |
150 | margin-right: $not-expanded-horizontal-margins; | 156 | margin-right: pvar(--horizontalMarginContent); |
151 | flex-grow: 1; | 157 | flex-grow: 1; |
152 | } | 158 | } |
153 | 159 | ||
154 | .sub-menu { | 160 | .sub-menu { |
155 | background-color: pvar(--submenuColor); | 161 | background-color: pvar(--submenuBackgroundColor); |
156 | width: 100%; | 162 | width: 100%; |
157 | display: flex; | 163 | display: flex; |
158 | align-items: center; | 164 | align-items: center; |
159 | padding-left: $not-expanded-horizontal-margins; | 165 | padding-left: pvar(--horizontalMarginContent); |
160 | padding-right: $not-expanded-horizontal-margins; | 166 | padding-right: pvar(--horizontalMarginContent); |
161 | height: $sub-menu-height; | 167 | height: $sub-menu-height; |
162 | margin-bottom: $sub-menu-margin-bottom; | 168 | margin-bottom: $sub-menu-margin-bottom; |
169 | overflow-x: auto; | ||
163 | 170 | ||
164 | &.sub-menu-fixed { | 171 | &.sub-menu-fixed { |
165 | position: fixed; | 172 | position: fixed; |
166 | z-index: #{z('sub-menu') - 1}; | 173 | z-index: #{z('sub-menu') - 1}; |
174 | max-width: pvar(--mainColWidth); | ||
167 | } | 175 | } |
168 | } | 176 | } |
169 | 177 | ||
@@ -174,18 +182,11 @@ my-input-toggle-hidden ::ng-deep input { | |||
174 | 182 | ||
175 | // Override some properties if the main content is expanded (no menu on the left) | 183 | // Override some properties if the main content is expanded (no menu on the left) |
176 | &.expanded { | 184 | &.expanded { |
185 | --horizontalMarginContent: #{$expanded-horizontal-margins}; | ||
186 | --mainColWidth: 100vw; | ||
187 | |||
177 | margin-left: 0; | 188 | margin-left: 0; |
178 | width: 100%; | 189 | width: 100%; |
179 | |||
180 | .margin-content { | ||
181 | margin-left: var(--expanded-horizontal-margin-content); | ||
182 | margin-right: var(--expanded-horizontal-margin-content); | ||
183 | } | ||
184 | |||
185 | .sub-menu { | ||
186 | padding-left: var(--expanded-horizontal-margin-content); | ||
187 | padding-right: var(--expanded-horizontal-margin-content); | ||
188 | } | ||
189 | } | 190 | } |
190 | 191 | ||
191 | &.lock-scroll .main-row > router-outlet + * { | 192 | &.lock-scroll .main-row > router-outlet + * { |
@@ -263,7 +264,7 @@ my-input-toggle-hidden ::ng-deep input { | |||
263 | opacity: 0.6; | 264 | opacity: 0.6; |
264 | 265 | ||
265 | &.active { | 266 | &.active { |
266 | background-color: pvar(--submenuColor); | 267 | background-color: pvar(--submenuBackgroundColor); |
267 | } | 268 | } |
268 | 269 | ||
269 | &.active, &:hover, &:active, &:focus { | 270 | &.active, &:hover, &:active, &:focus { |
@@ -277,11 +278,6 @@ my-input-toggle-hidden ::ng-deep input { | |||
277 | font-weight: bold; | 278 | font-weight: bold; |
278 | } | 279 | } |
279 | 280 | ||
280 | @keyframes spin { | ||
281 | from { transform: scale(1) rotate(0deg);} | ||
282 | to { transform: scale(1) rotate(360deg);} | ||
283 | } | ||
284 | |||
285 | // In tables, don't have a hover different background | 281 | // In tables, don't have a hover different background |
286 | table { | 282 | table { |
287 | .action-button-edit, .action-button-delete { | 283 | .action-button-edit, .action-button-delete { |
@@ -338,29 +334,34 @@ ngx-loading-bar { | |||
338 | 334 | ||
339 | @media screen and (max-width: #{breakpoint(xxl)}) { | 335 | @media screen and (max-width: #{breakpoint(xxl)}) { |
340 | .main-col { | 336 | .main-col { |
337 | & { | ||
338 | --horizontalMarginContent: #{$not-expanded-horizontal-margins / 2}; | ||
339 | } | ||
340 | |||
341 | &.expanded { | 341 | &.expanded { |
342 | .margin-content { | 342 | --horizontalMarginContent: #{$expanded-horizontal-margins / 2}; |
343 | --expanded-horizontal-margin-content: #{$expanded-horizontal-margins/2}; | ||
344 | } | ||
345 | } | 343 | } |
344 | |||
345 | --videosHorizontalMarginContent: 30px; | ||
346 | } | 346 | } |
347 | } | 347 | } |
348 | 348 | ||
349 | @media screen and (max-width: #{breakpoint(lg)}) { | 349 | @media screen and (max-width: #{breakpoint(lg)}) { |
350 | /* the following applies from 500px to 900px and is partially overriden from 500px to 800px by changes below to $small-view */ | ||
351 | .main-col { | 350 | .main-col { |
352 | &, &.expanded { | 351 | --videosHorizontalMarginContent: #{pvar(--horizontalMarginContent)}; |
353 | .margin-content { | 352 | } |
354 | --expanded-horizontal-margin-content: #{$expanded-horizontal-margins/3}; | 353 | |
355 | } | 354 | /* the following applies from 500px to 900px and is partially overriden from 500px to 800px by changes below to $small-view */ |
355 | .main-col, | ||
356 | .main-col.expanded { | ||
357 | --horizontalMarginContent: #{$expanded-horizontal-margins / 3}; | ||
356 | 358 | ||
357 | .sub-menu { | 359 | .sub-menu { |
358 | padding-left: 50px; | 360 | padding-left: 50px; |
359 | padding-right: 50px; | 361 | padding-right: 50px; |
360 | 362 | ||
361 | .title-page { | 363 | .title-page { |
362 | font-size: 17px; | 364 | font-size: 17px; |
363 | } | ||
364 | } | 365 | } |
365 | } | 366 | } |
366 | } | 367 | } |
@@ -373,98 +374,46 @@ ngx-loading-bar { | |||
373 | } | 374 | } |
374 | 375 | ||
375 | @media screen and (max-width: $small-view) { | 376 | @media screen and (max-width: $small-view) { |
376 | .main-col { | 377 | .main-col, |
377 | margin-left: 0; | 378 | .main-col.expanded { |
378 | 379 | --horizontalMarginContent: 15px; | |
379 | &, &.expanded { | ||
380 | .margin-content { | ||
381 | --expanded-horizontal-margin-content: 15px; | ||
382 | } | ||
383 | |||
384 | .sub-menu { | ||
385 | width: 100vw; | ||
386 | padding-left: 15px; | ||
387 | padding-right: 15px; | ||
388 | margin-bottom: $sub-menu-margin-bottom-small-view; | ||
389 | overflow-x: auto; | ||
390 | } | ||
391 | |||
392 | // Use an appropriate offset top when sub-menu fixed | ||
393 | .margin-content.offset-content { | ||
394 | padding-top: $sub-menu-height + $sub-menu-margin-bottom-small-view; | ||
395 | } | ||
396 | |||
397 | .admin-sub-header { | ||
398 | @include admin-sub-header-responsive(15px*2); | ||
399 | } | ||
400 | 380 | ||
401 | my-markdown-textarea { | 381 | margin-left: 0; |
402 | .root { | ||
403 | max-width: 100% !important; | ||
404 | } | ||
405 | } | ||
406 | |||
407 | input[type=text], | ||
408 | input[type=password], | ||
409 | input[type=email], | ||
410 | textarea, | ||
411 | .peertube-select-container { | ||
412 | flex-grow: 1; | ||
413 | } | ||
414 | 382 | ||
415 | .caption input[type=text] { | 383 | .sub-menu { |
416 | width: unset !important; | 384 | width: 100vw; |
417 | flex-grow: 1; | 385 | padding-left: 15px; |
418 | } | 386 | padding-right: 15px; |
387 | margin-bottom: $sub-menu-margin-bottom-small-view; | ||
388 | overflow-x: auto; | ||
419 | } | 389 | } |
420 | } | ||
421 | } | ||
422 | 390 | ||
423 | // overflow-databale responsive rules | 391 | // Use an appropriate offset top when sub-menu fixed |
424 | @media screen and (min-width: #{breakpoint(lg)}) { | 392 | .margin-content.offset-content { |
425 | .main-col { | 393 | padding-top: $sub-menu-height + $sub-menu-margin-bottom-small-view; |
426 | &.expanded { | ||
427 | @include overflow-datatable(breakpoint(lg), $expanded-horizontal-margins/2, $mobile-paginator: false); | ||
428 | } | 394 | } |
429 | 395 | ||
430 | &:not(.expanded) { | 396 | .admin-sub-header { |
431 | @include overflow-datatable(breakpoint(lg), $not-expanded-horizontal-margins + $menu-width/2, $mobile-paginator: false); | 397 | @include admin-sub-header-responsive; |
432 | } | 398 | } |
433 | } | ||
434 | } | ||
435 | 399 | ||
436 | @media screen and (max-width: #{breakpoint(lg)}) { | 400 | my-markdown-textarea { |
437 | .main-col { | 401 | .root { |
438 | &.expanded { | 402 | max-width: 100% !important; |
439 | @include overflow-datatable(breakpoint(lg), $expanded-horizontal-margins/3); | 403 | } |
440 | } | 404 | } |
441 | 405 | ||
442 | &:not(.expanded) { | 406 | input[type=text], |
443 | @include overflow-datatable(breakpoint(lg), $expanded-horizontal-margins/3 + $menu-width/2); | 407 | input[type=password], |
444 | } | 408 | input[type=email], |
445 | } | 409 | textarea, |
446 | } | 410 | .peertube-select-container { |
447 | 411 | flex-grow: 1; | |
448 | @media screen and (max-width: $small-view) { | ||
449 | .main-col { | ||
450 | &:not(.expanded), | ||
451 | &.expanded { | ||
452 | @include overflow-datatable(breakpoint(lg), 15px); | ||
453 | } | 412 | } |
454 | } | ||
455 | } | ||
456 | 413 | ||
457 | @media screen and (min-width: $small-view) and (max-width: #{$small-view + $menu-width}) { | 414 | .caption input[type=text] { |
458 | .main-col { | 415 | width: unset !important; |
459 | &:not(.expanded) { | 416 | flex-grow: 1; |
460 | .admin-sub-header { | ||
461 | @include admin-sub-header-responsive($expanded-horizontal-margins/3 + $menu-width/2); | ||
462 | } | ||
463 | |||
464 | .sub-menu { | ||
465 | overflow-x: auto; | ||
466 | width: calc(100vw - #{$menu-width}); | ||
467 | } | ||
468 | } | 417 | } |
469 | } | 418 | } |
470 | } | 419 | } |
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/classes.scss b/client/src/sass/classes.scss new file mode 100644 index 000000000..af8e39573 --- /dev/null +++ b/client/src/sass/classes.scss | |||
@@ -0,0 +1,22 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .peertube-button { | ||
5 | @include peertube-button; | ||
6 | } | ||
7 | |||
8 | .peertube-button-link { | ||
9 | @include peertube-button-link; | ||
10 | } | ||
11 | |||
12 | .orange-button { | ||
13 | @include orange-button; | ||
14 | } | ||
15 | |||
16 | .orange-button-inverted { | ||
17 | @include orange-button-inverted; | ||
18 | } | ||
19 | |||
20 | .grey-button { | ||
21 | @include grey-button; | ||
22 | } | ||
diff --git a/client/src/sass/include/_actor.scss b/client/src/sass/include/_actor.scss new file mode 100644 index 000000000..8d82a042c --- /dev/null +++ b/client/src/sass/include/_actor.scss | |||
@@ -0,0 +1,92 @@ | |||
1 | @import '_variables'; | ||
2 | |||
3 | @mixin section-label-responsive { | ||
4 | color: pvar(--mainColor); | ||
5 | font-size: 12px; | ||
6 | margin-bottom: 15px; | ||
7 | font-weight: $font-bold; | ||
8 | letter-spacing: 2.5px; | ||
9 | |||
10 | @media screen and (max-width: $mobile-view) { | ||
11 | font-size: 10px; | ||
12 | letter-spacing: 2.1px; | ||
13 | margin-bottom: 5px; | ||
14 | } | ||
15 | } | ||
16 | |||
17 | @mixin show-more-description { | ||
18 | color: pvar(--mainColor); | ||
19 | cursor: pointer; | ||
20 | margin: 10px auto 45px auto; | ||
21 | } | ||
22 | |||
23 | @mixin avatar-row-responsive ($img-margin, $grey-font-size) { | ||
24 | display: flex; | ||
25 | grid-column: 1; | ||
26 | margin-bottom: 30px; | ||
27 | |||
28 | .channel-avatar { | ||
29 | @include channel-avatar(120px); | ||
30 | } | ||
31 | |||
32 | .account-avatar { | ||
33 | @include avatar(120px); | ||
34 | } | ||
35 | |||
36 | > div { | ||
37 | margin-left: $img-margin; | ||
38 | min-width: 1px; | ||
39 | } | ||
40 | |||
41 | .actor-info { | ||
42 | display: flex; | ||
43 | |||
44 | > div:first-child { | ||
45 | flex-grow: 1; | ||
46 | min-width: 1px; | ||
47 | } | ||
48 | } | ||
49 | |||
50 | .actor-display-name { | ||
51 | display: flex; | ||
52 | flex-wrap: wrap; | ||
53 | } | ||
54 | |||
55 | h1 { | ||
56 | font-size: 28px; | ||
57 | font-weight: $font-bold; | ||
58 | margin: 0; | ||
59 | } | ||
60 | |||
61 | .actor-handle { | ||
62 | @include ellipsis; | ||
63 | } | ||
64 | |||
65 | .actor-handle, | ||
66 | .actor-counters { | ||
67 | color: pvar(--greyForegroundColor); | ||
68 | font-size: $grey-font-size; | ||
69 | } | ||
70 | |||
71 | .actor-counters > *:not(:last-child)::after { | ||
72 | content: '•'; | ||
73 | margin: 0 10px; | ||
74 | color: pvar(--mainColor); | ||
75 | } | ||
76 | |||
77 | @media screen and (max-width: $mobile-view) { | ||
78 | margin-bottom: 15px; | ||
79 | |||
80 | h1 { | ||
81 | font-size: 22px; | ||
82 | } | ||
83 | |||
84 | .channel-avatar { | ||
85 | @include channel-avatar(80px); | ||
86 | } | ||
87 | |||
88 | .account-avatar { | ||
89 | @include avatar(120px); | ||
90 | } | ||
91 | } | ||
92 | } | ||
diff --git a/client/src/sass/include/_miniature.scss b/client/src/sass/include/_miniature.scss index 134b307b1..3b86f29b4 100644 --- a/client/src/sass/include/_miniature.scss +++ b/client/src/sass/include/_miniature.scss | |||
@@ -4,11 +4,11 @@ | |||
4 | @mixin miniature-name { | 4 | @mixin miniature-name { |
5 | @include ellipsis-multiline(1.1em, 2); | 5 | @include ellipsis-multiline(1.1em, 2); |
6 | 6 | ||
7 | word-break: break-all; | ||
8 | word-wrap: break-word; | ||
7 | transition: color 0.2s; | 9 | transition: color 0.2s; |
8 | font-weight: $font-semibold; | 10 | font-weight: $font-semibold; |
9 | color: pvar(--mainForegroundColor); | 11 | color: pvar(--mainForegroundColor); |
10 | margin-top: 10px; | ||
11 | margin-bottom: 5px; | ||
12 | 12 | ||
13 | &:hover { | 13 | &:hover { |
14 | text-decoration: none; | 14 | text-decoration: none; |
@@ -20,20 +20,20 @@ | |||
20 | } | 20 | } |
21 | } | 21 | } |
22 | 22 | ||
23 | $play-overlay-transition: 0.2s ease; | ||
24 | $play-overlay-height: 26px; | ||
25 | $play-overlay-width: 18px; | ||
26 | |||
27 | @mixin miniature-thumbnail { | 23 | @mixin miniature-thumbnail { |
28 | @include disable-outline; | 24 | @include disable-outline; |
29 | 25 | ||
26 | $play-overlay-transition: 0.2s ease; | ||
27 | $play-overlay-height: 26px; | ||
28 | $play-overlay-width: 18px; | ||
29 | |||
30 | display: flex; | 30 | display: flex; |
31 | flex-direction: column; | 31 | flex-direction: column; |
32 | position: relative; | 32 | position: relative; |
33 | border-radius: 3px; | 33 | border-radius: 3px; |
34 | width: 100%; | ||
35 | height: 100%; | ||
34 | overflow: hidden; | 36 | overflow: hidden; |
35 | width: $video-thumbnail-width; | ||
36 | height: $video-thumbnail-height; | ||
37 | background-color: #ececec; | 37 | background-color: #ececec; |
38 | transition: filter $play-overlay-transition; | 38 | transition: filter $play-overlay-transition; |
39 | 39 | ||
@@ -97,154 +97,64 @@ $play-overlay-width: 18px; | |||
97 | color: #fff; | 97 | color: #fff; |
98 | } | 98 | } |
99 | 99 | ||
100 | @mixin miniature-rows { | 100 | // Use margin by default, or padding if $margin is false |
101 | &:first-child { | 101 | @mixin grid-videos-miniature-margins ($margin: true, $min-margin: 0) { |
102 | padding-top: 30px; | 102 | --gridVideosMiniatureMargins: #{pvar(--videosHorizontalMarginContent)}; |
103 | 103 | ||
104 | .section-title { | 104 | @if $margin { |
105 | border-top: none !important; | 105 | margin-left: var(--gridVideosMiniatureMargins) !important; |
106 | } | 106 | margin-right: var(--gridVideosMiniatureMargins) !important; |
107 | } | 107 | } @else { |
108 | 108 | padding-left: var(--gridVideosMiniatureMargins) !important; | |
109 | .section-title { | 109 | padding-right: var(--gridVideosMiniatureMargins) !important; |
110 | font-size: 24px; | ||
111 | font-weight: $font-semibold; | ||
112 | padding-top: 15px; | ||
113 | margin-bottom: 15px; | ||
114 | display: flex; | ||
115 | justify-content: space-between; | ||
116 | |||
117 | &:not(h2) { | ||
118 | border-top: 1px solid $separator-border-color; | ||
119 | } | ||
120 | |||
121 | a { | ||
122 | &:hover, &:focus:not(.focus-visible), &:active { | ||
123 | text-decoration: none; | ||
124 | outline: none; | ||
125 | } | ||
126 | |||
127 | color: pvar(--mainForegroundColor); | ||
128 | } | ||
129 | } | ||
130 | |||
131 | &.channel { | ||
132 | .section-title { | ||
133 | a { | ||
134 | display: flex; | ||
135 | width: fit-content; | ||
136 | align-items: center; | ||
137 | |||
138 | img { | ||
139 | @include avatar(28px); | ||
140 | |||
141 | margin-right: 8px; | ||
142 | } | ||
143 | } | ||
144 | |||
145 | .followers { | ||
146 | color: pvar(--greyForegroundColor); | ||
147 | font-weight: normal; | ||
148 | font-size: 14px; | ||
149 | margin-left: 10px; | ||
150 | position: relative; | ||
151 | top: 2px; | ||
152 | } | ||
153 | } | ||
154 | } | ||
155 | |||
156 | .show-more { | ||
157 | position: relative; | ||
158 | top: -5px; | ||
159 | display: inline-block; | ||
160 | font-size: 16px; | ||
161 | text-transform: uppercase; | ||
162 | color: pvar(--greyForegroundColor); | ||
163 | margin-bottom: 10px; | ||
164 | font-weight: $font-semibold; | ||
165 | text-decoration: none; | ||
166 | } | 110 | } |
167 | 111 | ||
168 | @media screen and (max-width: $mobile-view) { | 112 | @media screen and (max-width: $mobile-view) { |
169 | max-height: initial; | 113 | --gridVideosMiniatureMargins: #{$min-margin}; |
170 | overflow: initial; | ||
171 | |||
172 | .section-title { | ||
173 | font-size: 17px; | ||
174 | margin-left: 10px; | ||
175 | } | ||
176 | } | ||
177 | } | ||
178 | 114 | ||
179 | @mixin fluid-videos-miniature-layout { | ||
180 | margin-left: $not-expanded-horizontal-margins !important; | ||
181 | margin-right: $not-expanded-horizontal-margins !important; | ||
182 | |||
183 | @media screen and (max-width: $mobile-view) { | ||
184 | width: auto; | 115 | width: auto; |
185 | margin: 0 !important; | ||
186 | |||
187 | .videos { | ||
188 | text-align: center; | ||
189 | |||
190 | ::ng-deep .video-miniature { | ||
191 | padding-right: 0; | ||
192 | height: auto; | ||
193 | width: 100%; | ||
194 | margin-bottom: 25px; | ||
195 | |||
196 | .video-miniature-information { | ||
197 | width: 100% !important; | ||
198 | text-align: left; | ||
199 | |||
200 | span { | ||
201 | width: 100%; | ||
202 | } | ||
203 | } | ||
204 | |||
205 | .video-thumbnail { | ||
206 | border-radius: 0; | ||
207 | } | ||
208 | } | ||
209 | } | ||
210 | } | 116 | } |
117 | } | ||
211 | 118 | ||
212 | @media screen and (min-width: #{breakpoint(fhd)}) { | 119 | @mixin grid-videos-miniature-layout { |
213 | margin-left: 6vw !important; | 120 | @include grid-videos-miniature-margins; |
214 | margin-right: 6vw !important; | ||
215 | } | ||
216 | 121 | ||
217 | @media screen and (min-width: $mobile-view) { | 122 | @media screen and (min-width: $mobile-view) { |
218 | 123 | .videos, | |
219 | .videos { | 124 | .playlists { |
220 | --miniature-min-width: #{$video-thumbnail-width - 15px}; | 125 | --miniatureMinWidth: #{$video-thumbnail-width - 25px}; |
221 | --miniature-max-width: #{$video-thumbnail-width}; | 126 | --miniatureMaxWidth: #{$video-thumbnail-width}; |
222 | 127 | ||
223 | display: grid; | 128 | display: grid; |
224 | column-gap: 5px; | 129 | column-gap: 30px; |
225 | grid-template-columns: repeat( | 130 | grid-template-columns: repeat( |
226 | auto-fill, | 131 | auto-fill, |
227 | minmax( | 132 | minmax( |
228 | var(--miniature-min-width), | 133 | var(--miniatureMinWidth), |
229 | 1fr | 134 | 1fr |
230 | ) | 135 | ) |
231 | ); | 136 | ); |
232 | 137 | ||
233 | @media screen and (min-width: #{breakpoint(fhd)}) { | 138 | .video-wrapper, |
234 | column-gap: 1%; | 139 | .playlist-wrapper { |
235 | --miniature-min-width: #{$video-thumbnail-width}; | ||
236 | } | ||
237 | |||
238 | .video-wrapper { | ||
239 | margin: 0 auto; | 140 | margin: 0 auto; |
240 | width: 100%; | 141 | width: 100%; |
241 | 142 | ||
242 | my-video-miniature { | 143 | my-video-miniature, |
144 | my-video-playlist-miniature { | ||
243 | display: block; | 145 | display: block; |
244 | min-width: var(--miniature-min-width); | 146 | min-width: var(--miniatureMinWidth); |
245 | max-width: var(--miniature-max-width); | 147 | max-width: var(--miniatureMaxWidth); |
246 | } | 148 | } |
247 | } | 149 | } |
150 | |||
151 | @media screen and (min-width: #{breakpoint(xm)}) { | ||
152 | column-gap: 15px; | ||
153 | } | ||
154 | |||
155 | @media screen and (min-width: #{breakpoint(fhd)}) { | ||
156 | column-gap: 2%; | ||
157 | } | ||
248 | } | 158 | } |
249 | } | 159 | } |
250 | } | 160 | } |
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index ca11488cb..bf844ac5d 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -23,17 +23,28 @@ | |||
23 | display: block; | 23 | display: block; |
24 | /* Fallback for non-webkit */ | 24 | /* Fallback for non-webkit */ |
25 | display: -webkit-box; | 25 | display: -webkit-box; |
26 | max-height: $font-size * $number-of-lines; | 26 | -webkit-line-clamp: $number-of-lines; |
27 | /* Fallback for non-webkit */ | 27 | /* Fallback for non-webkit */ |
28 | font-size: $font-size; | 28 | font-size: $font-size; |
29 | line-height: $font-size; | 29 | line-height: $font-size; |
30 | overflow: hidden; | 30 | overflow: hidden; |
31 | text-overflow: ellipsis; | 31 | text-overflow: ellipsis; |
32 | max-height: $font-size * $number-of-lines; | ||
32 | } | 33 | } |
33 | 34 | ||
34 | @mixin prefix($property, $parameters...) { | 35 | @mixin fade-text ($fade-after, $background-color) { |
35 | @each $prefix in -webkit-, -moz-, -ms-, -o-, "" { | 36 | position: relative; |
36 | #{$prefix}#{$property}: $parameters; | 37 | overflow: hidden; |
38 | |||
39 | &:after { | ||
40 | content: ''; | ||
41 | pointer-events: none; | ||
42 | width: 100%; | ||
43 | height: 100%; | ||
44 | position: absolute; | ||
45 | left: 0; | ||
46 | top: 0; | ||
47 | background: linear-gradient(transparent $fade-after, $background-color); | ||
37 | } | 48 | } |
38 | } | 49 | } |
39 | 50 | ||
@@ -41,9 +52,6 @@ | |||
41 | word-break: break-word; | 52 | word-break: break-word; |
42 | word-wrap: break-word; | 53 | word-wrap: break-word; |
43 | overflow-wrap: break-word; | 54 | overflow-wrap: break-word; |
44 | -webkit-hyphens: auto; | ||
45 | -ms-hyphens: auto; | ||
46 | -moz-hyphens: auto; | ||
47 | hyphens: auto; | 55 | hyphens: auto; |
48 | } | 56 | } |
49 | 57 | ||
@@ -52,28 +60,6 @@ | |||
52 | ::ng-deep .material { | 60 | ::ng-deep .material { |
53 | color: $color; | 61 | color: $color; |
54 | } | 62 | } |
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 | } | 63 | } |
78 | 64 | ||
79 | @mixin fill-svg-color ($color) { | 65 | @mixin fill-svg-color ($color) { |
@@ -163,6 +149,33 @@ | |||
163 | } | 149 | } |
164 | } | 150 | } |
165 | 151 | ||
152 | @mixin orange-button-inverted { | ||
153 | @include button-focus(pvar(--mainColorLightest)); | ||
154 | |||
155 | border: 2px solid pvar(--mainColor); | ||
156 | font-weight: $font-semibold; | ||
157 | |||
158 | &, &:active, &:focus { | ||
159 | color: pvar(--mainColor); | ||
160 | background-color: pvar(--mainBackgroundColor); | ||
161 | } | ||
162 | |||
163 | &:hover { | ||
164 | color: pvar(--mainColor); | ||
165 | background-color: pvar(--mainColorLightest); | ||
166 | } | ||
167 | |||
168 | &[disabled], &.disabled { | ||
169 | cursor: default; | ||
170 | color: pvar(--mainColor); | ||
171 | background-color: #C6C6C6; | ||
172 | } | ||
173 | |||
174 | my-global-icon { | ||
175 | @include apply-svg-color(pvar(--mainColor)) | ||
176 | } | ||
177 | } | ||
178 | |||
166 | @mixin tertiary-button { | 179 | @mixin tertiary-button { |
167 | @include button-focus($grey-button-outline-color); | 180 | @include button-focus($grey-button-outline-color); |
168 | 181 | ||
@@ -534,6 +547,14 @@ | |||
534 | min-height: $size; | 547 | min-height: $size; |
535 | } | 548 | } |
536 | 549 | ||
550 | @mixin channel-avatar ($size) { | ||
551 | width: $size; | ||
552 | height: $size; | ||
553 | min-width: $size; | ||
554 | min-height: $size; | ||
555 | border-radius: 5px; | ||
556 | } | ||
557 | |||
537 | @mixin chevron ($size, $border-width) { | 558 | @mixin chevron ($size, $border-width) { |
538 | border-style: solid; | 559 | border-style: solid; |
539 | border-width: $border-width $border-width 0 0; | 560 | border-width: $border-width $border-width 0 0; |
@@ -593,103 +614,29 @@ | |||
593 | } | 614 | } |
594 | } | 615 | } |
595 | 616 | ||
596 | @mixin sub-menu-with-actor { | ||
597 | position: initial; | ||
598 | z-index: unset; | ||
599 | height: max-content; | ||
600 | display: flex; | ||
601 | flex-direction: column; | ||
602 | align-items: flex-start; | ||
603 | |||
604 | .actor { | ||
605 | display: flex; | ||
606 | margin-top: 20px; | ||
607 | margin-bottom: 20px; | ||
608 | |||
609 | img { | ||
610 | @include avatar(80px); | ||
611 | |||
612 | margin-right: 20px; | ||
613 | } | ||
614 | |||
615 | .actor-info { | ||
616 | display: flex; | ||
617 | flex-direction: column; | ||
618 | justify-content: center; | ||
619 | |||
620 | .actor-names { | ||
621 | display: flex; | ||
622 | align-items: center; | ||
623 | flex-wrap: wrap; | ||
624 | |||
625 | .actor-display-name { | ||
626 | font-size: 23px; | ||
627 | font-weight: $font-bold; | ||
628 | margin-right: 7px; | ||
629 | } | ||
630 | |||
631 | .actor-name { | ||
632 | position: relative; | ||
633 | top: 3px; | ||
634 | font-size: 14px; | ||
635 | color: $grey-actor-name; | ||
636 | } | ||
637 | } | ||
638 | |||
639 | .actor-lower { | ||
640 | grid-area: lower; | ||
641 | } | ||
642 | |||
643 | .actor-followers { | ||
644 | font-size: 15px; | ||
645 | } | ||
646 | |||
647 | .actor-owner { | ||
648 | @include actor-owner; | ||
649 | } | ||
650 | } | ||
651 | } | ||
652 | |||
653 | .links { | ||
654 | margin-top: 0; | ||
655 | margin-bottom: 15px; | ||
656 | |||
657 | a { | ||
658 | margin-top: 0; | ||
659 | margin-bottom: 0; | ||
660 | text-transform: uppercase; | ||
661 | font-weight: 600; | ||
662 | font-size: 110%; | ||
663 | |||
664 | @media screen and (max-width: $mobile-view) { | ||
665 | font-size: 130%; | ||
666 | } | ||
667 | } | ||
668 | |||
669 | list-overflow { | ||
670 | display: inline-block; | ||
671 | width: max-content; | ||
672 | } | ||
673 | } | ||
674 | } | ||
675 | |||
676 | @mixin create-button { | 617 | @mixin create-button { |
677 | @include peertube-button-link; | 618 | @include peertube-button-link; |
678 | @include orange-button; | 619 | @include orange-button; |
679 | @include button-with-icon(20px, 5px, -1px); | 620 | @include button-with-icon(20px, 5px, -1px); |
680 | } | 621 | } |
681 | 622 | ||
682 | @mixin row-blocks { | 623 | @mixin row-blocks ($column-responsive: true) { |
683 | display: flex; | 624 | display: flex; |
684 | min-height: 130px; | 625 | min-height: 130px; |
685 | padding-bottom: 20px; | 626 | padding-bottom: 20px; |
686 | margin-bottom: 20px; | 627 | margin-bottom: 20px; |
687 | border-bottom: 1px solid #C6C6C6; | 628 | border-bottom: 1px solid #C6C6C6; |
688 | 629 | ||
689 | @media screen and (max-width: 800px) { | 630 | @media screen and (max-width: $small-view) { |
690 | flex-direction: column; | 631 | @if $column-responsive { |
691 | height: auto; | 632 | flex-direction: column; |
692 | align-items: center; | 633 | height: auto; |
634 | align-items: center; | ||
635 | } @else { | ||
636 | min-height: initial; | ||
637 | padding-bottom: 10px; | ||
638 | margin-bottom: 10px; | ||
639 | } | ||
693 | } | 640 | } |
694 | } | 641 | } |
695 | 642 | ||
@@ -756,7 +703,7 @@ | |||
756 | padding: 0.75rem 1rem; | 703 | padding: 0.75rem 1rem; |
757 | margin-bottom: 1rem; | 704 | margin-bottom: 1rem; |
758 | list-style: none; | 705 | list-style: none; |
759 | background-color: pvar(--submenuColor); | 706 | background-color: pvar(--submenuBackgroundColor); |
760 | border-radius: 0.25rem; | 707 | border-radius: 0.25rem; |
761 | 708 | ||
762 | .breadcrumb-item { | 709 | .breadcrumb-item { |
@@ -811,7 +758,7 @@ | |||
811 | & > a, | 758 | & > a, |
812 | & > div { | 759 | & > div { |
813 | padding: 20px; | 760 | padding: 20px; |
814 | background: pvar(--submenuColor); | 761 | background: pvar(--submenuBackgroundColor); |
815 | border-radius: 4px; | 762 | border-radius: 4px; |
816 | box-sizing: border-box; | 763 | box-sizing: border-box; |
817 | height: 100%; | 764 | height: 100%; |
@@ -833,7 +780,7 @@ | |||
833 | } | 780 | } |
834 | } | 781 | } |
835 | 782 | ||
836 | @mixin divider($color: pvar(--submenuColor), $background: pvar(--mainBackgroundColor)) { | 783 | @mixin divider($color: pvar(--submenuBackgroundColor), $background: pvar(--mainBackgroundColor)) { |
837 | width: 95%; | 784 | width: 95%; |
838 | border-top: .05rem solid $color; | 785 | border-top: .05rem solid $color; |
839 | height: .05rem; | 786 | height: .05rem; |
@@ -916,7 +863,7 @@ | |||
916 | } | 863 | } |
917 | } | 864 | } |
918 | 865 | ||
919 | @mixin admin-sub-header-responsive ($horizontal-margins) { | 866 | @mixin admin-sub-header-responsive { |
920 | flex-direction: column; | 867 | flex-direction: column; |
921 | 868 | ||
922 | .form-sub-title { | 869 | .form-sub-title { |
@@ -931,7 +878,7 @@ | |||
931 | white-space: nowrap; | 878 | white-space: nowrap; |
932 | height: 50px; | 879 | height: 50px; |
933 | padding: 10px 0; | 880 | padding: 10px 0; |
934 | width: calc(100vw - #{$horizontal-margins*2}); | 881 | width: 100%; |
935 | 882 | ||
936 | a { | 883 | a { |
937 | margin-left: 5px; | 884 | margin-left: 5px; |
@@ -939,14 +886,16 @@ | |||
939 | } | 886 | } |
940 | } | 887 | } |
941 | 888 | ||
942 | // applies 16:9 ratio to a child element (using $selector) only using | 889 | // applies ratio (default to 16:9) to a child element (using $selector) only using |
943 | // an immediate's parent size. This allows 16:9 ratio without explicit | 890 | // an immediate's parent size. This allows to set a ratio without explicit |
944 | // dimensions, as width/height cannot be computed from each other. | 891 | // dimensions, as width/height cannot be computed from each other. |
945 | @mixin large-screen-ratio ($selector: 'div') { | 892 | @mixin block-ratio ($selector: 'div', $inverted-ratio: 9/16) { |
893 | $padding-percent: percentage($inverted-ratio); | ||
894 | |||
946 | position: relative; | 895 | position: relative; |
947 | height: 0; | 896 | height: 0; |
948 | width: 100%; | 897 | width: 100%; |
949 | padding-top: 56%; | 898 | padding-top: $padding-percent; |
950 | 899 | ||
951 | #{$selector} { | 900 | #{$selector} { |
952 | position: absolute; | 901 | position: absolute; |
@@ -991,3 +940,31 @@ | |||
991 | 940 | ||
992 | border-left: $width solid rgba(255, 255, 255, 0.95); | 941 | border-left: $width solid rgba(255, 255, 255, 0.95); |
993 | } | 942 | } |
943 | |||
944 | @mixin on-small-main-col () { | ||
945 | :host-context(.main-col:not(.expanded)) { | ||
946 | @media screen and (max-width: $small-view + $menu-width) { | ||
947 | @content; | ||
948 | } | ||
949 | } | ||
950 | |||
951 | :host-context(.main-col.expanded) { | ||
952 | @media screen and (max-width: $small-view) { | ||
953 | @content; | ||
954 | } | ||
955 | } | ||
956 | } | ||
957 | |||
958 | @mixin on-mobile-main-col () { | ||
959 | :host-context(.main-col:not(.expanded)) { | ||
960 | @media screen and (max-width: $mobile-view + $menu-width) { | ||
961 | @content; | ||
962 | } | ||
963 | } | ||
964 | |||
965 | :host-context(.main-col.expanded) { | ||
966 | @media screen and (max-width: $mobile-view) { | ||
967 | @content; | ||
968 | } | ||
969 | } | ||
970 | } | ||
diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss index c8316473d..d2a5d2bd9 100644 --- a/client/src/sass/include/_variables.scss +++ b/client/src/sass/include/_variables.scss | |||
@@ -16,9 +16,10 @@ $grey-foreground-hover-color: #303030; | |||
16 | $grey-button-outline-color: scale-color($grey-foreground-color, $alpha: -95%); | 16 | $grey-button-outline-color: scale-color($grey-foreground-color, $alpha: -95%); |
17 | 17 | ||
18 | $main-color: hsl(24, 90%, 50%); | 18 | $main-color: hsl(24, 90%, 50%); |
19 | $main-hover-color: lighten($main-color, 5%); | ||
20 | $main-color-lighter: lighten($main-color, 10%); | 19 | $main-color-lighter: lighten($main-color, 10%); |
21 | $main-color-lightest: lighten($main-color, 40%); | 20 | $main-color-lightest: lighten($main-color, 40%); |
21 | $main-hover-color: lighten($main-color, 5%); | ||
22 | |||
22 | $secondary-color: hsl(187, 77%, 34%); | 23 | $secondary-color: hsl(187, 77%, 34%); |
23 | 24 | ||
24 | $support-button: inherit; | 25 | $support-button: inherit; |
@@ -47,18 +48,34 @@ $menu-bottom-color: #C6C6C6; | |||
47 | $menu-width: 240px; | 48 | $menu-width: 240px; |
48 | $menu-lateral-padding: 26px; | 49 | $menu-lateral-padding: 26px; |
49 | 50 | ||
50 | $sub-menu-color: #F7F7F7; | 51 | $sub-menu-background-color: #F7F7F7; |
51 | $sub-menu-height: 81px; | 52 | $sub-menu-height: 81px; |
52 | 53 | ||
54 | $channel-background-color: #f6ede8; | ||
55 | |||
56 | $banner-inverted-ratio: 1/6; | ||
57 | |||
58 | $max-channels-width: 1200px; | ||
59 | |||
53 | $footer-height: 30px; | 60 | $footer-height: 30px; |
54 | $footer-margin: 30px; | 61 | $footer-margin: 30px; |
55 | 62 | ||
56 | $separator-border-color: rgba(0, 0, 0, 0.10); | 63 | $separator-border-color: rgba(0, 0, 0, 0.10); |
57 | 64 | ||
58 | $video-miniature-width: 238px; | ||
59 | $video-miniature-margin-bottom: 15px; | 65 | $video-miniature-margin-bottom: 15px; |
60 | $video-thumbnail-height: 122px; | 66 | |
61 | $video-thumbnail-width: 223px; | 67 | $video-miniature-row-name-font-size: 1.3em; |
68 | $video-miniature-row-mobile-name-font-size: 14px; | ||
69 | |||
70 | $video-miniature-row-info-font-size: 14px; | ||
71 | $video-miniature-row-mobile-info-font-size: 12px; | ||
72 | |||
73 | $video-thumbnail-height: 153px; | ||
74 | $video-thumbnail-width: 280px; | ||
75 | $video-thumbnail-medium-height: 114px; | ||
76 | $video-thumbnail-medium-width: 201px; | ||
77 | $video-thumbnail-small-height: 71px; | ||
78 | $video-thumbnail-small-width: 125px; | ||
62 | 79 | ||
63 | $theater-bottom-space: 115px; | 80 | $theater-bottom-space: 115px; |
64 | 81 | ||
@@ -98,7 +115,9 @@ $variables: ( | |||
98 | 115 | ||
99 | --menuBackgroundColor: var(--menuBackgroundColor), | 116 | --menuBackgroundColor: var(--menuBackgroundColor), |
100 | --menuForegroundColor: var(--menuForegroundColor), | 117 | --menuForegroundColor: var(--menuForegroundColor), |
101 | --submenuColor: var(--submenuColor), | 118 | |
119 | --submenuBackgroundColor: var(--submenuBackgroundColor), | ||
120 | --channelBackgroundColor: var(--channelBackgroundColor), | ||
102 | 121 | ||
103 | --inputForegroundColor: var(--inputForegroundColor), | 122 | --inputForegroundColor: var(--inputForegroundColor), |
104 | --inputBackgroundColor: var(--inputBackgroundColor), | 123 | --inputBackgroundColor: var(--inputBackgroundColor), |
@@ -116,11 +135,20 @@ $variables: ( | |||
116 | --supportButtonHeartColor: var(--supportButtonHeartColor), | 135 | --supportButtonHeartColor: var(--supportButtonHeartColor), |
117 | 136 | ||
118 | --embedForegroundColor: var(--embedForegroundColor), | 137 | --embedForegroundColor: var(--embedForegroundColor), |
119 | --embedBigPlayBackgroundColor: var(--embedBigPlayBackgroundColor) | 138 | --embedBigPlayBackgroundColor: var(--embedBigPlayBackgroundColor), |
139 | |||
140 | --horizontalMarginContent: var(--horizontalMarginContent), | ||
141 | --videosHorizontalMarginContent: var(--videosHorizontalMarginContent), | ||
142 | --mainColWidth: var(--mainColWidth) | ||
120 | ); | 143 | ); |
121 | 144 | ||
145 | // SASS type check our CSS variables | ||
122 | @function pvar($variable) { | 146 | @function pvar($variable) { |
123 | @return map-get($variables, $variable); | 147 | @if map-has-key($variables, $variable) { |
148 | @return map-get($variables, $variable); | ||
149 | } @else { | ||
150 | @error "ERROR: Variable #{$variable} does not exist"; | ||
151 | } | ||
124 | } | 152 | } |
125 | 153 | ||
126 | /*** z-index groups ***/ | 154 | /*** z-index groups ***/ |
diff --git a/client/src/sass/ng-select.scss b/client/src/sass/ng-select.scss index 54c805ccf..13fc1d6c2 100644 --- a/client/src/sass/ng-select.scss +++ b/client/src/sass/ng-select.scss | |||
@@ -11,7 +11,7 @@ $ng-select-highlight: #f2690d; | |||
11 | $ng-select-box-shadow: #{$focus-box-shadow-form} pvar(--mainColorLightest); | 11 | $ng-select-box-shadow: #{$focus-box-shadow-form} pvar(--mainColorLightest); |
12 | // $ng-select-placeholder: lighten($ng-select-primary-text, 40) !default; | 12 | // $ng-select-placeholder: lighten($ng-select-primary-text, 40) !default; |
13 | $ng-select-height: 30px; | 13 | $ng-select-height: 30px; |
14 | // $ng-select-value-padding-left: 10px !default; | 14 | $ng-select-value-padding-left: 15px; |
15 | // $ng-select-value-font-size: 0.9em !default; | 15 | // $ng-select-value-font-size: 0.9em !default; |
16 | 16 | ||
17 | @import "~@ng-select/ng-select/scss/default.theme.scss"; | 17 | @import "~@ng-select/ng-select/scss/default.theme.scss"; |
@@ -20,11 +20,6 @@ $ng-select-height: 30px; | |||
20 | font-size: .9em; | 20 | font-size: .9em; |
21 | } | 21 | } |
22 | 22 | ||
23 | .ng-input, | ||
24 | .ng-select .ng-select-container .ng-value-container { | ||
25 | padding-left: 15px !important; | ||
26 | } | ||
27 | |||
28 | .ng-select { | 23 | .ng-select { |
29 | &.ng-select-focused { | 24 | &.ng-select-focused { |
30 | &:not(.ng-select-opened) > .ng-select-container { | 25 | &:not(.ng-select-opened) > .ng-select-container { |
@@ -44,4 +39,11 @@ $ng-select-height: 30px; | |||
44 | &.ng-select-single .ng-value-container .ng-value { | 39 | &.ng-select-single .ng-value-container .ng-value { |
45 | color: pvar(--inputForegroundColor); | 40 | color: pvar(--inputForegroundColor); |
46 | } | 41 | } |
42 | |||
43 | &.ng-select-multiple .ng-select-container .ng-value-container { | ||
44 | padding-left: 12px; | ||
45 | .ng-value { | ||
46 | margin-left: 3px; | ||
47 | } | ||
48 | } | ||
47 | } | 49 | } |
diff --git a/client/src/sass/player/context-menu.scss b/client/src/sass/player/context-menu.scss index f3a28ead0..ad673eea7 100644 --- a/client/src/sass/player/context-menu.scss +++ b/client/src/sass/player/context-menu.scss | |||
@@ -14,7 +14,7 @@ $context-menu-width: 350px; | |||
14 | 14 | ||
15 | .vjs-menu-content { | 15 | .vjs-menu-content { |
16 | opacity: $primary-foreground-opacity; | 16 | opacity: $primary-foreground-opacity; |
17 | color: pvar(--embedForegroundCsolor); | 17 | color: pvar(--embedForegroundColor); |
18 | font-size: $font-size !important; | 18 | font-size: $font-size !important; |
19 | font-weight: $font-semibold; | 19 | font-weight: $font-semibold; |
20 | } | 20 | } |
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/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss index afa577819..9c9b5d4fc 100644 --- a/client/src/sass/primeng-custom.scss +++ b/client/src/sass/primeng-custom.scss | |||
@@ -547,7 +547,7 @@ p-table { | |||
547 | height: 46px; | 547 | height: 46px; |
548 | 548 | ||
549 | &.p-highlight { | 549 | &.p-highlight { |
550 | background-color: pvar(--submenuColor) !important; | 550 | background-color: pvar(--submenuBackgroundColor) !important; |
551 | 551 | ||
552 | td, td > a { | 552 | td, td > a { |
553 | color: pvar(--mainForegroundColor) !important; | 553 | color: pvar(--mainForegroundColor) !important; |
@@ -558,7 +558,7 @@ p-table { | |||
558 | .p-datatable-tbody { | 558 | .p-datatable-tbody { |
559 | tr { | 559 | tr { |
560 | &:hover { | 560 | &:hover { |
561 | background-color: pvar(--submenuColor) !important; | 561 | background-color: pvar(--submenuBackgroundColor) !important; |
562 | } | 562 | } |
563 | 563 | ||
564 | td { | 564 | td { |
@@ -590,16 +590,16 @@ p-table { | |||
590 | th { | 590 | th { |
591 | border: none !important; | 591 | border: none !important; |
592 | border-bottom: 1px solid !important; | 592 | border-bottom: 1px solid !important; |
593 | border-color: pvar(--submenuColor) !important; | 593 | border-color: pvar(--submenuBackgroundColor) !important; |
594 | text-align: left !important; | 594 | text-align: left !important; |
595 | padding: 5px 0 5px 15px !important; | 595 | padding: 5px 0 5px 15px !important; |
596 | font-weight: $font-semibold !important; | 596 | font-weight: $font-semibold !important; |
597 | color: pvar(--mainForegroundColor) !important; | 597 | color: pvar(--mainForegroundColor) !important; |
598 | 598 | ||
599 | &.p-sortable-column:hover { | 599 | &.p-sortable-column:hover { |
600 | background-color: pvar(--submenuColor) !important; | 600 | background-color: pvar(--submenuBackgroundColor) !important; |
601 | border: 1px solid !important; | 601 | border: 1px solid !important; |
602 | border-color: pvar(--submenuColor) !important; | 602 | border-color: pvar(--submenuBackgroundColor) !important; |
603 | border-width: 0 1px !important; | 603 | border-width: 0 1px !important; |
604 | 604 | ||
605 | &:first-child { | 605 | &:first-child { |
@@ -608,7 +608,7 @@ p-table { | |||
608 | } | 608 | } |
609 | 609 | ||
610 | &.p-highlight { | 610 | &.p-highlight { |
611 | background-color: pvar(--submenuColor) !important; | 611 | background-color: pvar(--submenuBackgroundColor) !important; |
612 | 612 | ||
613 | .pi { | 613 | .pi { |
614 | @extend .glyphicon; | 614 | @extend .glyphicon; |
@@ -654,7 +654,7 @@ p-table { | |||
654 | position: relative; | 654 | position: relative; |
655 | border: none; | 655 | border: none; |
656 | border-top: 1px solid !important; | 656 | border-top: 1px solid !important; |
657 | border-color: pvar(--submenuColor) !important; | 657 | border-color: pvar(--submenuBackgroundColor) !important; |
658 | height: 40px; | 658 | height: 40px; |
659 | display: flex; | 659 | display: flex; |
660 | justify-content: center; | 660 | justify-content: center; |
@@ -753,29 +753,32 @@ p-table { | |||
753 | } | 753 | } |
754 | 754 | ||
755 | // overflow data table | 755 | // overflow data table |
756 | @mixin overflow-datatable ($table-min-width, $horizontal-margins, $mobile-paginator: true) { | 756 | p-table { |
757 | p-table { | 757 | .p-datatable-wrapper { |
758 | .p-datatable-wrapper { | 758 | overflow-x: auto; |
759 | overflow-x: auto; | 759 | max-width: 100%; |
760 | max-width: calc(100vw - #{$horizontal-margins * 2}); | ||
761 | |||
762 | table { | ||
763 | min-width: $table-min-width; | ||
764 | } | ||
765 | } | ||
766 | 760 | ||
767 | @if $mobile-paginator { | 761 | table { |
768 | p-paginator .p-paginator-bottom { | 762 | min-width: breakpoint(lg); |
769 | display: block; | 763 | } |
764 | } | ||
770 | 765 | ||
771 | .p-paginator-current { | 766 | @media screen and (max-width: #{breakpoint(lg)}) { |
772 | position: relative; | 767 | // Prevent overflow |
773 | display: block; | 768 | p-paginator { |
774 | } | 769 | .p-paginator-current, |
770 | .p-dropdown { | ||
771 | top: 0; | ||
772 | margin-top: 30px; | ||
773 | } | ||
774 | } | ||
775 | } | ||
775 | 776 | ||
776 | a, .p-paginator-pages { | 777 | @media screen and (max-width: $mobile-view) { |
777 | vertical-align: middle; | 778 | // Prevent overflow |
778 | } | 779 | p-paginator { |
780 | .p-paginator-pages > .p-paginator-page:not(.p-highlight) { | ||
781 | display: none; | ||
779 | } | 782 | } |
780 | } | 783 | } |
781 | } | 784 | } |
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index cf4bc6f03..ae8f176b7 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts | |||
@@ -531,6 +531,7 @@ export class PeerTubeEmbed { | |||
531 | videoCaptions, | 531 | videoCaptions, |
532 | inactivityTimeout: 2500, | 532 | inactivityTimeout: 2500, |
533 | videoViewUrl: this.getVideoUrl(videoInfo.uuid) + '/views', | 533 | videoViewUrl: this.getVideoUrl(videoInfo.uuid) + '/views', |
534 | videoUUID: videoInfo.uuid, | ||
534 | 535 | ||
535 | isLive: videoInfo.isLive, | 536 | isLive: videoInfo.isLive, |
536 | 537 | ||
@@ -545,7 +546,8 @@ export class PeerTubeEmbed { | |||
545 | 546 | ||
546 | serverUrl: window.location.origin, | 547 | serverUrl: window.location.origin, |
547 | language: navigator.language, | 548 | language: navigator.language, |
548 | embedUrl: window.location.origin + videoInfo.embedPath | 549 | embedUrl: window.location.origin + videoInfo.embedPath, |
550 | embedTitle: videoInfo.name | ||
549 | }, | 551 | }, |
550 | 552 | ||
551 | webtorrent: { | 553 | webtorrent: { |
@@ -783,6 +785,8 @@ export class PeerTubeEmbed { | |||
783 | 785 | ||
784 | showModal: unimplemented, | 786 | showModal: unimplemented, |
785 | 787 | ||
788 | getServerConfig: unimplemented, | ||
789 | |||
786 | markdownRenderer: { | 790 | markdownRenderer: { |
787 | textMarkdownToHTML: unimplemented, | 791 | textMarkdownToHTML: unimplemented, |
788 | enhancedMarkdownToHTML: unimplemented | 792 | 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 @@ | |||
1 | import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model' | 1 | import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model' |
2 | import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model' | 2 | import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model' |
3 | import { ServerConfig } from '@shared/models/server' | ||
3 | 4 | ||
4 | export type RegisterClientOptions = { | 5 | export 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, |
diff --git a/client/yarn.lock b/client/yarn.lock index 79ab1e2a8..75548e83f 100644 --- a/client/yarn.lock +++ b/client/yarn.lock | |||
@@ -2,23 +2,23 @@ | |||
2 | # yarn lockfile v1 | 2 | # yarn lockfile v1 |
3 | 3 | ||
4 | 4 | ||
5 | "@angular-devkit/architect@0.1102.2": | 5 | "@angular-devkit/architect@0.1102.5": |
6 | version "0.1102.2" | 6 | version "0.1102.5" |
7 | resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1102.2.tgz#3b3eb654ae7c8c204b248bba76982ce8de2f7b6c" | 7 | resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1102.5.tgz#431df157af0c6477e5951f64ff12f3d5d5f075ee" |
8 | integrity sha512-FE7DeT13elqDlELF23QqvEFnT2BkxeC5t31/QW85IN/OR5Tf/q7XEpj7giJXyzKFQ60M3ZzbznZyRz0EqtfaBQ== | 8 | integrity sha512-lVc6NmEAZZPzvc18GzMFLoxqKKvPlNOg4vEtFsFldZmrydLJJGFi4KAs2WaJd8qVR1XuY4el841cjDQAJSq6sQ== |
9 | dependencies: | 9 | dependencies: |
10 | "@angular-devkit/core" "11.2.2" | 10 | "@angular-devkit/core" "11.2.5" |
11 | rxjs "6.6.3" | 11 | rxjs "6.6.3" |
12 | 12 | ||
13 | "@angular-devkit/build-angular@^0.1102.2": | 13 | "@angular-devkit/build-angular@^0.1102.2": |
14 | version "0.1102.2" | 14 | version "0.1102.5" |
15 | resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.1102.2.tgz#c850818fd8bb4dd4fda6288390868475c4b3236e" | 15 | resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.1102.5.tgz#7db51dfc33a8683458fa714d434f8c09fdc1f648" |
16 | integrity sha512-AjnvHrzkYTzDGzp0r5RmGoP9fyZXtaVFo0598PRusi1oWp1sW6B5FKPWw896iREOlotRXw3dsjqrGwbMcz0qyg== | 16 | integrity sha512-iAq/KbRq6kuA17rQZ67/0zQHEzpC9RzvtMZQ3wiiFsOmW5AIV5scjP7e6dn+F6vXZA44X4gCH5AUUkOLXyEtfg== |
17 | dependencies: | 17 | dependencies: |
18 | "@angular-devkit/architect" "0.1102.2" | 18 | "@angular-devkit/architect" "0.1102.5" |
19 | "@angular-devkit/build-optimizer" "0.1102.2" | 19 | "@angular-devkit/build-optimizer" "0.1102.5" |
20 | "@angular-devkit/build-webpack" "0.1102.2" | 20 | "@angular-devkit/build-webpack" "0.1102.5" |
21 | "@angular-devkit/core" "11.2.2" | 21 | "@angular-devkit/core" "11.2.5" |
22 | "@babel/core" "7.12.10" | 22 | "@babel/core" "7.12.10" |
23 | "@babel/generator" "7.12.11" | 23 | "@babel/generator" "7.12.11" |
24 | "@babel/plugin-transform-async-to-generator" "7.12.1" | 24 | "@babel/plugin-transform-async-to-generator" "7.12.1" |
@@ -26,8 +26,9 @@ | |||
26 | "@babel/preset-env" "7.12.11" | 26 | "@babel/preset-env" "7.12.11" |
27 | "@babel/runtime" "7.12.5" | 27 | "@babel/runtime" "7.12.5" |
28 | "@babel/template" "7.12.7" | 28 | "@babel/template" "7.12.7" |
29 | "@discoveryjs/json-ext" "0.5.2" | ||
29 | "@jsdevtools/coverage-istanbul-loader" "3.0.5" | 30 | "@jsdevtools/coverage-istanbul-loader" "3.0.5" |
30 | "@ngtools/webpack" "11.2.2" | 31 | "@ngtools/webpack" "11.2.5" |
31 | ansi-colors "4.1.1" | 32 | ansi-colors "4.1.1" |
32 | autoprefixer "10.2.4" | 33 | autoprefixer "10.2.4" |
33 | babel-loader "8.2.2" | 34 | babel-loader "8.2.2" |
@@ -88,30 +89,30 @@ | |||
88 | webpack-subresource-integrity "1.5.2" | 89 | webpack-subresource-integrity "1.5.2" |
89 | worker-plugin "5.0.0" | 90 | worker-plugin "5.0.0" |
90 | 91 | ||
91 | "@angular-devkit/build-optimizer@0.1102.2": | 92 | "@angular-devkit/build-optimizer@0.1102.5": |
92 | version "0.1102.2" | 93 | version "0.1102.5" |
93 | resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.1102.2.tgz#a306fee0bc648983405320953f05ad1fc60b6b84" | 94 | resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.1102.5.tgz#5c17d82a8c4f03ec0a14110838c2c3da6cb24dfd" |
94 | integrity sha512-TCWWqAe+pWZzLp/g2gG8Z5NC8JSgDNfyEuMBWxEUfo1Sm3BluXoz0BbmnietuhXJZ+fPAp9rLLzEGZlHvOlmOA== | 95 | integrity sha512-ujTwrevgMRNyWir4IdnJEdDRkVSLqugRpL6cU9OeqGn6Bu+zEzZQokLkMZvbw00eEKlf5Siej4hEeF1Hnx+LUA== |
95 | dependencies: | 96 | dependencies: |
96 | loader-utils "2.0.0" | 97 | loader-utils "2.0.0" |
97 | source-map "0.7.3" | 98 | source-map "0.7.3" |
98 | tslib "2.1.0" | 99 | tslib "2.1.0" |
99 | typescript "4.1.3" | 100 | typescript "4.1.5" |
100 | webpack-sources "2.2.0" | 101 | webpack-sources "2.2.0" |
101 | 102 | ||
102 | "@angular-devkit/build-webpack@0.1102.2": | 103 | "@angular-devkit/build-webpack@0.1102.5": |
103 | version "0.1102.2" | 104 | version "0.1102.5" |
104 | resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1102.2.tgz#f48501426a5d01b0610dafce33b4eb84d07181e6" | 105 | resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1102.5.tgz#e111acf7c0cbed761ae382089052a5c2dee71d96" |
105 | integrity sha512-59CBbwbdN8lI5/whuNeAZHRJxPlOmDc5ux8aJJNwWI9w54fz0ut/MLT3iuPk+WZuKlGdpS1sGkObfZwWen5kIQ== | 106 | integrity sha512-VMsi+mFwgPUQi7eEc2oKcf7X0xD0R1xfoguLS/+HGy3sfh+b7oJy3BU4+TRzDPBtGj6vWvENK2rwHFN3cBWvxA== |
106 | dependencies: | 107 | dependencies: |
107 | "@angular-devkit/architect" "0.1102.2" | 108 | "@angular-devkit/architect" "0.1102.5" |
108 | "@angular-devkit/core" "11.2.2" | 109 | "@angular-devkit/core" "11.2.5" |
109 | rxjs "6.6.3" | 110 | rxjs "6.6.3" |
110 | 111 | ||
111 | "@angular-devkit/core@11.2.2": | 112 | "@angular-devkit/core@11.2.5": |
112 | version "11.2.2" | 113 | version "11.2.5" |
113 | resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-11.2.2.tgz#c6b40f941b24d2af447831fc958b744316cd7d87" | 114 | resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-11.2.5.tgz#f9ba8288a6cc388808ee639c383dada50d64d06a" |
114 | integrity sha512-LUDO1AdIjereiMh0j5p9xJcdr9ifhbWCPxlZqfu5wHzUfhCx9gO2Lvjp6rZXQ3OedXg5IZUnyxHlzkszQOsgiw== | 115 | integrity sha512-DRFvEHRKoC+hTwcOAJqLe6UQa+bpXc/1IGCMHWEbuply0KIFIGQOlmaYwFZKixz3HdFZlmoCMcAVkAXvyaWVsQ== |
115 | dependencies: | 116 | dependencies: |
116 | ajv "6.12.6" | 117 | ajv "6.12.6" |
117 | fast-json-stable-stringify "2.1.0" | 118 | fast-json-stable-stringify "2.1.0" |
@@ -119,41 +120,41 @@ | |||
119 | rxjs "6.6.3" | 120 | rxjs "6.6.3" |
120 | source-map "0.7.3" | 121 | source-map "0.7.3" |
121 | 122 | ||
122 | "@angular-devkit/schematics@11.2.2": | 123 | "@angular-devkit/schematics@11.2.5": |
123 | version "11.2.2" | 124 | version "11.2.5" |
124 | resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-11.2.2.tgz#0c8c4b98a30f00649dcbb7794d3783b9a067209f" | 125 | resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-11.2.5.tgz#ddcb966f3f1dc910e55f03067036f1f6a01b8222" |
125 | integrity sha512-6bIxMwafz/+lwdtcshwOuFfhxTMU4RLma1uxBS34DXupMauPGl0IIXAy5cK9dXPlHLxuGsjeBiOM6eq033RLgw== | 126 | integrity sha512-7RoWgpMvhljPhW9CMz1EtqkwNnGpnsPyy0N29ClHPUq+o8wLR0hvbLBDz1fKSF7j1AwRccaQSNTj8KWsjzQJLQ== |
126 | dependencies: | 127 | dependencies: |
127 | "@angular-devkit/core" "11.2.2" | 128 | "@angular-devkit/core" "11.2.5" |
128 | ora "5.3.0" | 129 | ora "5.3.0" |
129 | rxjs "6.6.3" | 130 | rxjs "6.6.3" |
130 | 131 | ||
131 | "@angular/animations@^11.1.1": | 132 | "@angular/animations@^11.1.1": |
132 | version "11.2.3" | 133 | version "11.2.6" |
133 | resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-11.2.3.tgz#518183e5f7b8c3b304020ea86d12cc3216142cc9" | 134 | resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-11.2.6.tgz#36935bc0fe33f1486ed889f8b5e12915858ccf5a" |
134 | integrity sha512-Z6sHIeTeeZrRAW83NI7FO7THF50cPCFkkuvVah3qmCqopY6FuoHKUBEENyGzQGH69LbGFYhEppY8KM/6JtVF6Q== | 135 | integrity sha512-fci034QakkoIrFeY/uOmDvf6AupZ7ziU1FlBMs/wn4HOqwsPCofpawvFQnfj5nez1+KM5JOJ1VHmZKJupkWfgw== |
135 | dependencies: | 136 | dependencies: |
136 | tslib "^2.0.0" | 137 | tslib "^2.0.0" |
137 | 138 | ||
138 | "@angular/cdk@^11.0.0": | 139 | "@angular/cdk@^11.0.0": |
139 | version "11.2.2" | 140 | version "11.2.5" |
140 | resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-11.2.2.tgz#f541069db3f5705d8c064138f6cd94568fe1b658" | 141 | resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-11.2.5.tgz#e0cce8b28ca635b6151b834c6e1c4bc0a8dd7c04" |
141 | integrity sha512-p3lRDPlnOuJtLWEd020QOyn0ERyc1LF7OLi90hTdzMMxe9fT3v6sQJVRs8jIY3NTmpIm/pNDGi77+1/vKerLPQ== | 142 | integrity sha512-ugalSDLME5E9JlxcRR8RGlOYlaV6rIzxOVQrGRBzY2tdhMT4Ng+BFtCkq1K88AU1sTLHq54xg9Xkfn7b5W2kiA== |
142 | dependencies: | 143 | dependencies: |
143 | tslib "^2.0.0" | 144 | tslib "^2.0.0" |
144 | optionalDependencies: | 145 | optionalDependencies: |
145 | parse5 "^5.0.0" | 146 | parse5 "^5.0.0" |
146 | 147 | ||
147 | "@angular/cli@^11.1.2": | 148 | "@angular/cli@^11.1.2": |
148 | version "11.2.2" | 149 | version "11.2.5" |
149 | resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-11.2.2.tgz#ca56894f1a4d1f4e411408b8185b711614c3195a" | 150 | resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-11.2.5.tgz#3cf3e6432db41cebb364da2dcf3d44588535a34a" |
150 | integrity sha512-rOVBzDzrMuOgJY43O46/7yYbncx0egGfr+DMJDQdazePGH1H3INN/eA9gkVcVK53ztCYb9X1sbZKOs9TUhF6nw== | 151 | integrity sha512-GIwK8l6wtg/++8aDYW++LSf7v1uqDtB6so2rPjNlOm7oYk5iqM73KaorQb/1A52oxWE3IRSJLNQaSyUlWvHvSA== |
151 | dependencies: | 152 | dependencies: |
152 | "@angular-devkit/architect" "0.1102.2" | 153 | "@angular-devkit/architect" "0.1102.5" |
153 | "@angular-devkit/core" "11.2.2" | 154 | "@angular-devkit/core" "11.2.5" |
154 | "@angular-devkit/schematics" "11.2.2" | 155 | "@angular-devkit/schematics" "11.2.5" |
155 | "@schematics/angular" "11.2.2" | 156 | "@schematics/angular" "11.2.5" |
156 | "@schematics/update" "0.1102.2" | 157 | "@schematics/update" "0.1102.5" |
157 | "@yarnpkg/lockfile" "1.1.0" | 158 | "@yarnpkg/lockfile" "1.1.0" |
158 | ansi-colors "4.1.1" | 159 | ansi-colors "4.1.1" |
159 | debug "4.3.1" | 160 | debug "4.3.1" |
@@ -173,16 +174,16 @@ | |||
173 | uuid "8.3.2" | 174 | uuid "8.3.2" |
174 | 175 | ||
175 | "@angular/common@^11.1.1": | 176 | "@angular/common@^11.1.1": |
176 | version "11.2.3" | 177 | version "11.2.6" |
177 | resolved "https://registry.yarnpkg.com/@angular/common/-/common-11.2.3.tgz#e71d645fb6bdef9463f23a551cc072ef276c1d84" | 178 | resolved "https://registry.yarnpkg.com/@angular/common/-/common-11.2.6.tgz#9985b9f1b3d82588f85bb74b1967749b0134d017" |
178 | integrity sha512-51gVmr942SZtAFmhVfp7/3fcTQ+Tia7UxWjv6iUtYF3oCvTWbo/J1zki2VNSfmMNKJV8MaMq6XUw8UWbHA0sgQ== | 179 | integrity sha512-q1yR6bktd5p987gLEKiFY4CrHcmBxks9R6GcdgzGneQsucDtGESzEKdcJ0uaMXE+9teS+fQy5GvXel6DlA/J+w== |
179 | dependencies: | 180 | dependencies: |
180 | tslib "^2.0.0" | 181 | tslib "^2.0.0" |
181 | 182 | ||
182 | "@angular/compiler-cli@^11.1.1": | 183 | "@angular/compiler-cli@^11.1.1": |
183 | version "11.2.3" | 184 | version "11.2.6" |
184 | resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-11.2.3.tgz#5307215b9aa6e32d772906fd3b2960ba03a7565d" | 185 | resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-11.2.6.tgz#456844d71079df3ca3f025aaa9d9df9ed5a79006" |
185 | integrity sha512-ObQVI6q2c0VTWbsDnWJDdUZv2Jz/u1jiQNcrdtu/rjtJARaldEno9dMakN838Q6Nw4FzKUO6uYZXmnvKCUjfxQ== | 186 | integrity sha512-1OC8UkySaLzaw3aSrm8A6SA88CxQAdA4ffaOhBLE/Ee6CxpneVxn3ORlnccqnS8zWyEpschbootPJV56U3Azeg== |
186 | dependencies: | 187 | dependencies: |
187 | "@babel/core" "^7.8.6" | 188 | "@babel/core" "^7.8.6" |
188 | "@babel/types" "^7.8.6" | 189 | "@babel/types" "^7.8.6" |
@@ -206,9 +207,9 @@ | |||
206 | integrity sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ== | 207 | integrity sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ== |
207 | 208 | ||
208 | "@angular/compiler@^11.1.1": | 209 | "@angular/compiler@^11.1.1": |
209 | version "11.2.3" | 210 | version "11.2.6" |
210 | resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-11.2.3.tgz#72427d57b992bf6840fb7268357a466095caf8eb" | 211 | resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-11.2.6.tgz#8b69cd2f2c3bb0fbc6f95ded1ccbe20e6858daed" |
211 | integrity sha512-De8BwtSwPVYGdvQa6CDq2C1SLmB78YjS0t/KNlvfp85cl4Gb3BdjTDsKMkJXkm/3ubnIXi1BaRIsFNVTCCF70Q== | 212 | integrity sha512-3ijsCxnCLU1V1hy4UMf9qtMz5LR+wCdVFDqktEQccN9YEkN0cNtOc8Nu9EV9/mc2tqd1Q4xSBpb2o2mvpy7AhQ== |
212 | dependencies: | 213 | dependencies: |
213 | tslib "^2.0.0" | 214 | tslib "^2.0.0" |
214 | 215 | ||
@@ -218,53 +219,53 @@ | |||
218 | integrity sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w== | 219 | integrity sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w== |
219 | 220 | ||
220 | "@angular/core@^11.1.1": | 221 | "@angular/core@^11.1.1": |
221 | version "11.2.3" | 222 | version "11.2.6" |
222 | resolved "https://registry.yarnpkg.com/@angular/core/-/core-11.2.3.tgz#7dd59f35e0b2410543a61be6048c474c18a43f40" | 223 | resolved "https://registry.yarnpkg.com/@angular/core/-/core-11.2.6.tgz#c38ee7834519d3c94e51be62156784a984cd93d2" |
223 | integrity sha512-+G7rZj21Mcmf6nWjQ79EwomwEOVQ1WLqw6YvCXWzgJ9ZlVjLi/Sti0/jIzUpgK0E0Fn86yuXw/vgYq5kjGeOcQ== | 224 | integrity sha512-lS5JOQ/Y9gbk5WiMnCp5Zyz2pRIoZ+IWLOXHU5rkQeXy0zE3eMJhw0FfpEK+X5CeSNl2EPVSPLT0MtDtbNPodg== |
224 | dependencies: | 225 | dependencies: |
225 | tslib "^2.0.0" | 226 | tslib "^2.0.0" |
226 | 227 | ||
227 | "@angular/forms@^11.1.1": | 228 | "@angular/forms@^11.1.1": |
228 | version "11.2.3" | 229 | version "11.2.6" |
229 | resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-11.2.3.tgz#57460a110e6601b50362f878fc0f67701c76dc24" | 230 | resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-11.2.6.tgz#d82a1c655754d48ec861b9b3af370e6ee1e841cb" |
230 | integrity sha512-VfyKV8IxHTclcHQmt5gjGFmKC1kGz7sdNLYsEM+M0y88Bsufh3VIhK4kspfO4nhJxVfh6HFOt1JVQ5bvo6PDlQ== | 231 | integrity sha512-0xxayXCNc8lPQhDj5q/hAcG55cmDXPSBn2cxX4V+uDSGwKU1+h2CQID6gJdBJBh5wOaeMe6h8dK2s1pRgok66A== |
231 | dependencies: | 232 | dependencies: |
232 | tslib "^2.0.0" | 233 | tslib "^2.0.0" |
233 | 234 | ||
234 | "@angular/localize@^11.1.1": | 235 | "@angular/localize@^11.1.1": |
235 | version "11.2.3" | 236 | version "11.2.6" |
236 | resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-11.2.3.tgz#df2e605341be53c2d4cead2d8b274415af8b3136" | 237 | resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-11.2.6.tgz#465f2541c5bcdc396725504becaec3b96c718ec8" |
237 | integrity sha512-SCpum70G+MuoRitbv+u92fjDlKEbYizTosukxryh56QNa47iO3/rkVp8P2R75FDYJVJrxqoTiMGl0Q9tKdrEGA== | 238 | integrity sha512-8K+SdqKqIaRlNRegDBy//VAtf2rlwoZAmqoFfiM5ujuB4SFt32NAduxDUlFGWdZD5V3iPorFBrceq04bt695AA== |
238 | dependencies: | 239 | dependencies: |
239 | "@babel/core" "7.8.3" | 240 | "@babel/core" "7.8.3" |
240 | glob "7.1.2" | 241 | glob "7.1.2" |
241 | yargs "^16.1.1" | 242 | yargs "^16.1.1" |
242 | 243 | ||
243 | "@angular/platform-browser-dynamic@^11.1.1": | 244 | "@angular/platform-browser-dynamic@^11.1.1": |
244 | version "11.2.3" | 245 | version "11.2.6" |
245 | resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-11.2.3.tgz#3d7eb15ba4bcc9e227f68f13bf20258fa16efad1" | 246 | resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-11.2.6.tgz#26acbe4de315019ebe1e925ee826eda20c95d881" |
246 | integrity sha512-QUPCvack7De6u5AqWcW8O6FzczwqoL858R1NlnqojnNbcnN/dCtXtKvvETEEgp/9VMwLfcuLd1BWdBJSah7f6A== | 247 | integrity sha512-B56b8yPW3vAmPe4VONiBYEMZ6B1i5CUkJvit8qWWK3y7t5XrYOihIiGC0UqEDaw/uAg72GXjixspcxZWan5e9w== |
247 | dependencies: | 248 | dependencies: |
248 | tslib "^2.0.0" | 249 | tslib "^2.0.0" |
249 | 250 | ||
250 | "@angular/platform-browser@^11.1.1": | 251 | "@angular/platform-browser@^11.1.1": |
251 | version "11.2.3" | 252 | version "11.2.6" |
252 | resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-11.2.3.tgz#0c6b537500a1c6304829fab19cf8c12daa2b48b9" | 253 | resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-11.2.6.tgz#d2af4323275f501e279ee2aa821ac5599c11feae" |
253 | integrity sha512-S0IP/kGinIH18+gfnX0gLFLbP0Euw1RBceDt/WipYhUeFZZryQHvot/6KFLFtO+8rVunfrg+UyBiaK65/TT9Og== | 254 | integrity sha512-xnYpfoqWyQOUngfbHefsZMyelCSAaxpopu/WYP0gpbYh9qJiVhsN9s6zRMqOIPueq9lmvlEuGBMgaJjeD6Ei7Q== |
254 | dependencies: | 255 | dependencies: |
255 | tslib "^2.0.0" | 256 | tslib "^2.0.0" |
256 | 257 | ||
257 | "@angular/router@^11.1.1": | 258 | "@angular/router@^11.1.1": |
258 | version "11.2.3" | 259 | version "11.2.6" |
259 | resolved "https://registry.yarnpkg.com/@angular/router/-/router-11.2.3.tgz#407a0797845c1cac963663537b30872e39e4b229" | 260 | resolved "https://registry.yarnpkg.com/@angular/router/-/router-11.2.6.tgz#5845ef37e85400aeeaf0ffe670802a58569638cc" |
260 | integrity sha512-lRuEIlNj2BcBZ17mt5SZY7v80PsvlS4J6EbKSOFeSYhALM/AQnaaCdrrMlQ1WyEa5bBUabxGT9/zvahBosy2yA== | 261 | integrity sha512-n/3Sp36slXzRXUcUO9nVs3CkgFxa6U9A8GENeyxq9XQtcE912jOP4dzjDi3hlaNKbX9ijOyEh505KpqmiSYATg== |
261 | dependencies: | 262 | dependencies: |
262 | tslib "^2.0.0" | 263 | tslib "^2.0.0" |
263 | 264 | ||
264 | "@angular/service-worker@^11.1.1": | 265 | "@angular/service-worker@^11.1.1": |
265 | version "11.2.3" | 266 | version "11.2.6" |
266 | resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-11.2.3.tgz#316bfc07ccebdc5af1a9cbc825082880c551c0b9" | 267 | resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-11.2.6.tgz#65e895a7a1dc309c9365ea801806549f7572646c" |
267 | integrity sha512-/JgA4rCH2SyIK/v0+sCqNgiBEV/pXQUcUoqfm//2zfc3VwerehvF3RtRBfabtLBpdwdO5a9DZ4nX+djvTJypvw== | 268 | integrity sha512-nZGwVhHZ6eLptnPzIjiFiktnl4ImC+4kejR3AaElTX8PgS9TykhYhgENB+ILU49bZOGMe3RVnNthgx/JkIEgjQ== |
268 | dependencies: | 269 | dependencies: |
269 | tslib "^2.0.0" | 270 | tslib "^2.0.0" |
270 | 271 | ||
@@ -275,10 +276,10 @@ | |||
275 | dependencies: | 276 | dependencies: |
276 | "@babel/highlight" "^7.12.13" | 277 | "@babel/highlight" "^7.12.13" |
277 | 278 | ||
278 | "@babel/compat-data@^7.12.7", "@babel/compat-data@^7.13.0": | 279 | "@babel/compat-data@^7.12.7", "@babel/compat-data@^7.13.8": |
279 | version "7.13.6" | 280 | version "7.13.12" |
280 | resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.6.tgz#11972d07db4c2317afdbf41d6feb3a730301ef4e" | 281 | resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.12.tgz#a8a5ccac19c200f9dd49624cac6e19d7be1236a1" |
281 | integrity sha512-VhgqKOWYVm7lQXlvbJnWOzwfAQATd2nV52koT0HZ/LdDH0m4DUDwkKYsH+IwpXb+bKPyBJzawA4I6nBKqZcpQw== | 282 | integrity sha512-3eJJ841uKxeV8dcN/2yGEUy+RfgQspPEgQat85umsE1rotuquQ2AbIub4S6j7c50a2d+4myc+zSlnXeIHrOnhQ== |
282 | 283 | ||
283 | "@babel/core@7.12.10": | 284 | "@babel/core@7.12.10": |
284 | version "7.12.10" | 285 | version "7.12.10" |
@@ -323,16 +324,16 @@ | |||
323 | source-map "^0.5.0" | 324 | source-map "^0.5.0" |
324 | 325 | ||
325 | "@babel/core@^7.7.5", "@babel/core@^7.8.6": | 326 | "@babel/core@^7.7.5", "@babel/core@^7.8.6": |
326 | version "7.13.1" | 327 | version "7.13.10" |
327 | resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.1.tgz#7ddd027176debe40f13bb88bac0c21218c5b1ecf" | 328 | resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.10.tgz#07de050bbd8193fcd8a3c27918c0890613a94559" |
328 | integrity sha512-FzeKfFBG2rmFtGiiMdXZPFt/5R5DXubVi82uYhjGX4Msf+pgYQMCFIqFXZWs5vbIYbf14VeBIgdGI03CDOOM1w== | 329 | integrity sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw== |
329 | dependencies: | 330 | dependencies: |
330 | "@babel/code-frame" "^7.12.13" | 331 | "@babel/code-frame" "^7.12.13" |
331 | "@babel/generator" "^7.13.0" | 332 | "@babel/generator" "^7.13.9" |
332 | "@babel/helper-compilation-targets" "^7.13.0" | 333 | "@babel/helper-compilation-targets" "^7.13.10" |
333 | "@babel/helper-module-transforms" "^7.13.0" | 334 | "@babel/helper-module-transforms" "^7.13.0" |
334 | "@babel/helpers" "^7.13.0" | 335 | "@babel/helpers" "^7.13.10" |
335 | "@babel/parser" "^7.13.0" | 336 | "@babel/parser" "^7.13.10" |
336 | "@babel/template" "^7.12.13" | 337 | "@babel/template" "^7.12.13" |
337 | "@babel/traverse" "^7.13.0" | 338 | "@babel/traverse" "^7.13.0" |
338 | "@babel/types" "^7.13.0" | 339 | "@babel/types" "^7.13.0" |
@@ -341,7 +342,7 @@ | |||
341 | gensync "^1.0.0-beta.2" | 342 | gensync "^1.0.0-beta.2" |
342 | json5 "^2.1.2" | 343 | json5 "^2.1.2" |
343 | lodash "^4.17.19" | 344 | lodash "^4.17.19" |
344 | semver "7.0.0" | 345 | semver "^6.3.0" |
345 | source-map "^0.5.0" | 346 | source-map "^0.5.0" |
346 | 347 | ||
347 | "@babel/generator@7.12.11": | 348 | "@babel/generator@7.12.11": |
@@ -353,10 +354,10 @@ | |||
353 | jsesc "^2.5.1" | 354 | jsesc "^2.5.1" |
354 | source-map "^0.5.0" | 355 | source-map "^0.5.0" |
355 | 356 | ||
356 | "@babel/generator@^7.12.10", "@babel/generator@^7.13.0", "@babel/generator@^7.8.3": | 357 | "@babel/generator@^7.12.10", "@babel/generator@^7.13.0", "@babel/generator@^7.13.9", "@babel/generator@^7.8.3": |
357 | version "7.13.0" | 358 | version "7.13.9" |
358 | resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.0.tgz#bd00d4394ca22f220390c56a0b5b85568ec1ec0c" | 359 | resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39" |
359 | integrity sha512-zBZfgvBB/ywjx0Rgc2+BwoH/3H+lDtlgD4hBOpEv5LxRnYsm/753iRuLepqnYlynpjC3AdQxtxsoeHJoEEwOAw== | 360 | integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw== |
360 | dependencies: | 361 | dependencies: |
361 | "@babel/types" "^7.13.0" | 362 | "@babel/types" "^7.13.0" |
362 | jsesc "^2.5.1" | 363 | jsesc "^2.5.1" |
@@ -377,20 +378,20 @@ | |||
377 | "@babel/helper-explode-assignable-expression" "^7.12.13" | 378 | "@babel/helper-explode-assignable-expression" "^7.12.13" |
378 | "@babel/types" "^7.12.13" | 379 | "@babel/types" "^7.12.13" |
379 | 380 | ||
380 | "@babel/helper-compilation-targets@^7.12.5", "@babel/helper-compilation-targets@^7.13.0": | 381 | "@babel/helper-compilation-targets@^7.12.5", "@babel/helper-compilation-targets@^7.13.10", "@babel/helper-compilation-targets@^7.13.8": |
381 | version "7.13.0" | 382 | version "7.13.10" |
382 | resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.0.tgz#c9cf29b82a76fd637f0faa35544c4ace60a155a1" | 383 | resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.10.tgz#1310a1678cb8427c07a753750da4f8ce442bdd0c" |
383 | integrity sha512-SOWD0JK9+MMIhTQiUVd4ng8f3NXhPVQvTv7D3UN4wbp/6cAHnB2EmMaU1zZA2Hh1gwme+THBrVSqTFxHczTh0Q== | 384 | integrity sha512-/Xju7Qg1GQO4mHZ/Kcs6Au7gfafgZnwm+a7sy/ow/tV1sHeraRUHbjdat8/UvDor4Tez+siGKDk6zIKtCPKVJA== |
384 | dependencies: | 385 | dependencies: |
385 | "@babel/compat-data" "^7.13.0" | 386 | "@babel/compat-data" "^7.13.8" |
386 | "@babel/helper-validator-option" "^7.12.17" | 387 | "@babel/helper-validator-option" "^7.12.17" |
387 | browserslist "^4.14.5" | 388 | browserslist "^4.14.5" |
388 | semver "7.0.0" | 389 | semver "^6.3.0" |
389 | 390 | ||
390 | "@babel/helper-create-class-features-plugin@^7.13.0": | 391 | "@babel/helper-create-class-features-plugin@^7.13.0": |
391 | version "7.13.0" | 392 | version "7.13.11" |
392 | resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.0.tgz#28d04ad9cfbd1ed1d8b988c9ea7b945263365846" | 393 | resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.11.tgz#30d30a005bca2c953f5653fc25091a492177f4f6" |
393 | integrity sha512-twwzhthM4/+6o9766AW2ZBHpIHPSGrPGk1+WfHiu13u/lBnggXGNYCpeAyVfNwGDKfkhEDp+WOD/xafoJ2iLjA== | 394 | integrity sha512-ays0I7XYq9xbjCSvT+EvysLgfc3tOkwCULHjrnscGT3A9qD4sk3wXnJ3of0MAWsWGjdinFvajHU2smYuqXKMrw== |
394 | dependencies: | 395 | dependencies: |
395 | "@babel/helper-function-name" "^7.12.13" | 396 | "@babel/helper-function-name" "^7.12.13" |
396 | "@babel/helper-member-expression-to-functions" "^7.13.0" | 397 | "@babel/helper-member-expression-to-functions" "^7.13.0" |
@@ -429,7 +430,7 @@ | |||
429 | dependencies: | 430 | dependencies: |
430 | "@babel/types" "^7.12.13" | 431 | "@babel/types" "^7.12.13" |
431 | 432 | ||
432 | "@babel/helper-hoist-variables@^7.12.13": | 433 | "@babel/helper-hoist-variables@^7.13.0": |
433 | version "7.13.0" | 434 | version "7.13.0" |
434 | resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.13.0.tgz#5d5882e855b5c5eda91e0cadc26c6e7a2c8593d8" | 435 | resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.13.0.tgz#5d5882e855b5c5eda91e0cadc26c6e7a2c8593d8" |
435 | integrity sha512-0kBzvXiIKfsCA0y6cFEIJf4OdzfpRuNk4+YTeHZpGGc666SATFKTz6sRncwFnQk7/ugJ4dSrCj6iJuvW4Qwr2g== | 436 | integrity sha512-0kBzvXiIKfsCA0y6cFEIJf4OdzfpRuNk4+YTeHZpGGc666SATFKTz6sRncwFnQk7/ugJ4dSrCj6iJuvW4Qwr2g== |
@@ -437,34 +438,33 @@ | |||
437 | "@babel/traverse" "^7.13.0" | 438 | "@babel/traverse" "^7.13.0" |
438 | "@babel/types" "^7.13.0" | 439 | "@babel/types" "^7.13.0" |
439 | 440 | ||
440 | "@babel/helper-member-expression-to-functions@^7.13.0": | 441 | "@babel/helper-member-expression-to-functions@^7.13.0", "@babel/helper-member-expression-to-functions@^7.13.12": |
441 | version "7.13.0" | 442 | version "7.13.12" |
442 | resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.0.tgz#6aa4bb678e0f8c22f58cdb79451d30494461b091" | 443 | resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz#dfe368f26d426a07299d8d6513821768216e6d72" |
443 | integrity sha512-yvRf8Ivk62JwisqV1rFRMxiSMDGnN6KH1/mDMmIrij4jztpQNRoHqqMG3U6apYbGRPJpgPalhva9Yd06HlUxJQ== | 444 | integrity sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw== |
444 | dependencies: | 445 | dependencies: |
445 | "@babel/types" "^7.13.0" | 446 | "@babel/types" "^7.13.12" |
446 | 447 | ||
447 | "@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.12.5": | 448 | "@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.12.5", "@babel/helper-module-imports@^7.13.12": |
448 | version "7.12.13" | 449 | version "7.13.12" |
449 | resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz#ec67e4404f41750463e455cc3203f6a32e93fcb0" | 450 | resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977" |
450 | integrity sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g== | 451 | integrity sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA== |
451 | dependencies: | 452 | dependencies: |
452 | "@babel/types" "^7.12.13" | 453 | "@babel/types" "^7.13.12" |
453 | 454 | ||
454 | "@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.12.13", "@babel/helper-module-transforms@^7.13.0": | 455 | "@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.13.0": |
455 | version "7.13.0" | 456 | version "7.13.12" |
456 | resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.13.0.tgz#42eb4bd8eea68bab46751212c357bfed8b40f6f1" | 457 | resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.13.12.tgz#600e58350490828d82282631a1422268e982ba96" |
457 | integrity sha512-Ls8/VBwH577+pw7Ku1QkUWIyRRNHpYlts7+qSqBBFCW3I8QteB9DxfcZ5YJpOwH6Ihe/wn8ch7fMGOP1OhEIvw== | 458 | integrity sha512-7zVQqMO3V+K4JOOj40kxiCrMf6xlQAkewBB0eu2b03OO/Q21ZutOzjpfD79A5gtE/2OWi1nv625MrDlGlkbknQ== |
458 | dependencies: | 459 | dependencies: |
459 | "@babel/helper-module-imports" "^7.12.13" | 460 | "@babel/helper-module-imports" "^7.13.12" |
460 | "@babel/helper-replace-supers" "^7.13.0" | 461 | "@babel/helper-replace-supers" "^7.13.12" |
461 | "@babel/helper-simple-access" "^7.12.13" | 462 | "@babel/helper-simple-access" "^7.13.12" |
462 | "@babel/helper-split-export-declaration" "^7.12.13" | 463 | "@babel/helper-split-export-declaration" "^7.12.13" |
463 | "@babel/helper-validator-identifier" "^7.12.11" | 464 | "@babel/helper-validator-identifier" "^7.12.11" |
464 | "@babel/template" "^7.12.13" | 465 | "@babel/template" "^7.12.13" |
465 | "@babel/traverse" "^7.13.0" | 466 | "@babel/traverse" "^7.13.0" |
466 | "@babel/types" "^7.13.0" | 467 | "@babel/types" "^7.13.12" |
467 | lodash "^4.17.19" | ||
468 | 468 | ||
469 | "@babel/helper-optimise-call-expression@^7.12.13": | 469 | "@babel/helper-optimise-call-expression@^7.12.13": |
470 | version "7.12.13" | 470 | version "7.12.13" |
@@ -487,22 +487,22 @@ | |||
487 | "@babel/helper-wrap-function" "^7.13.0" | 487 | "@babel/helper-wrap-function" "^7.13.0" |
488 | "@babel/types" "^7.13.0" | 488 | "@babel/types" "^7.13.0" |
489 | 489 | ||
490 | "@babel/helper-replace-supers@^7.12.13", "@babel/helper-replace-supers@^7.13.0": | 490 | "@babel/helper-replace-supers@^7.12.13", "@babel/helper-replace-supers@^7.13.0", "@babel/helper-replace-supers@^7.13.12": |
491 | version "7.13.0" | 491 | version "7.13.12" |
492 | resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.13.0.tgz#6034b7b51943094cb41627848cb219cb02be1d24" | 492 | resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz#6442f4c1ad912502481a564a7386de0c77ff3804" |
493 | integrity sha512-Segd5me1+Pz+rmN/NFBOplMbZG3SqRJOBlY+mA0SxAv6rjj7zJqr1AVr3SfzUVTLCv7ZLU5FycOM/SBGuLPbZw== | 493 | integrity sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw== |
494 | dependencies: | 494 | dependencies: |
495 | "@babel/helper-member-expression-to-functions" "^7.13.0" | 495 | "@babel/helper-member-expression-to-functions" "^7.13.12" |
496 | "@babel/helper-optimise-call-expression" "^7.12.13" | 496 | "@babel/helper-optimise-call-expression" "^7.12.13" |
497 | "@babel/traverse" "^7.13.0" | 497 | "@babel/traverse" "^7.13.0" |
498 | "@babel/types" "^7.13.0" | 498 | "@babel/types" "^7.13.12" |
499 | 499 | ||
500 | "@babel/helper-simple-access@^7.12.13": | 500 | "@babel/helper-simple-access@^7.12.13", "@babel/helper-simple-access@^7.13.12": |
501 | version "7.12.13" | 501 | version "7.13.12" |
502 | resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz#8478bcc5cacf6aa1672b251c1d2dde5ccd61a6c4" | 502 | resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz#dd6c538afb61819d205a012c31792a39c7a5eaf6" |
503 | integrity sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA== | 503 | integrity sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA== |
504 | dependencies: | 504 | dependencies: |
505 | "@babel/types" "^7.12.13" | 505 | "@babel/types" "^7.13.12" |
506 | 506 | ||
507 | "@babel/helper-skip-transparent-expression-wrappers@^7.12.1": | 507 | "@babel/helper-skip-transparent-expression-wrappers@^7.12.1": |
508 | version "7.12.1" | 508 | version "7.12.1" |
@@ -538,37 +538,37 @@ | |||
538 | "@babel/traverse" "^7.13.0" | 538 | "@babel/traverse" "^7.13.0" |
539 | "@babel/types" "^7.13.0" | 539 | "@babel/types" "^7.13.0" |
540 | 540 | ||
541 | "@babel/helpers@^7.12.5", "@babel/helpers@^7.13.0", "@babel/helpers@^7.8.3": | 541 | "@babel/helpers@^7.12.5", "@babel/helpers@^7.13.10", "@babel/helpers@^7.8.3": |
542 | version "7.13.0" | 542 | version "7.13.10" |
543 | resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.0.tgz#7647ae57377b4f0408bf4f8a7af01c42e41badc0" | 543 | resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.10.tgz#fd8e2ba7488533cdeac45cc158e9ebca5e3c7df8" |
544 | integrity sha512-aan1MeFPxFacZeSz6Ld7YZo5aPuqnKlD7+HZY75xQsueczFccP9A7V05+oe0XpLwHK3oLorPe9eaAUljL7WEaQ== | 544 | integrity sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ== |
545 | dependencies: | 545 | dependencies: |
546 | "@babel/template" "^7.12.13" | 546 | "@babel/template" "^7.12.13" |
547 | "@babel/traverse" "^7.13.0" | 547 | "@babel/traverse" "^7.13.0" |
548 | "@babel/types" "^7.13.0" | 548 | "@babel/types" "^7.13.0" |
549 | 549 | ||
550 | "@babel/highlight@^7.12.13": | 550 | "@babel/highlight@^7.12.13": |
551 | version "7.12.13" | 551 | version "7.13.10" |
552 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.12.13.tgz#8ab538393e00370b26271b01fa08f7f27f2e795c" | 552 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1" |
553 | integrity sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww== | 553 | integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg== |
554 | dependencies: | 554 | dependencies: |
555 | "@babel/helper-validator-identifier" "^7.12.11" | 555 | "@babel/helper-validator-identifier" "^7.12.11" |
556 | chalk "^2.0.0" | 556 | chalk "^2.0.0" |
557 | js-tokens "^4.0.0" | 557 | js-tokens "^4.0.0" |
558 | 558 | ||
559 | "@babel/parser@^7.12.10", "@babel/parser@^7.12.13", "@babel/parser@^7.12.7", "@babel/parser@^7.13.0", "@babel/parser@^7.8.3": | 559 | "@babel/parser@^7.12.10", "@babel/parser@^7.12.13", "@babel/parser@^7.12.7", "@babel/parser@^7.13.0", "@babel/parser@^7.13.10", "@babel/parser@^7.8.3": |
560 | version "7.13.4" | 560 | version "7.13.12" |
561 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.4.tgz#340211b0da94a351a6f10e63671fa727333d13ab" | 561 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.12.tgz#ba320059420774394d3b0c0233ba40e4250b81d1" |
562 | integrity sha512-uvoOulWHhI+0+1f9L4BoozY7U5cIkZ9PgJqvb041d6vypgUmtVPG4vmGm4pSggjl8BELzvHyUeJSUyEMY6b+qA== | 562 | integrity sha512-4T7Pb244rxH24yR116LAuJ+adxXXnHhZaLJjegJVKSdoNCe4x1eDBaud5YIcQFcqzsaD5BHvJw5BQ0AZapdCRw== |
563 | 563 | ||
564 | "@babel/plugin-proposal-async-generator-functions@^7.12.1": | 564 | "@babel/plugin-proposal-async-generator-functions@^7.12.1": |
565 | version "7.13.5" | 565 | version "7.13.8" |
566 | resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.5.tgz#69e3fbb9958949b09036e27b26eba1aafa1ba3db" | 566 | resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.8.tgz#87aacb574b3bc4b5603f6fe41458d72a5a2ec4b1" |
567 | integrity sha512-8cErJEDzhZgNKzYyjCKsHuyPqtWxG8gc9h4OFSUDJu0vCAOsObPU2LcECnW0kJwh/b+uUz46lObVzIXw0fzAbA== | 567 | integrity sha512-rPBnhj+WgoSmgq+4gQUtXx/vOcU+UYtjy1AA/aeD61Hwj410fwYyqfUcRP3lR8ucgliVJL/G7sXcNUecC75IXA== |
568 | dependencies: | 568 | dependencies: |
569 | "@babel/helper-plugin-utils" "^7.13.0" | 569 | "@babel/helper-plugin-utils" "^7.13.0" |
570 | "@babel/helper-remap-async-to-generator" "^7.13.0" | 570 | "@babel/helper-remap-async-to-generator" "^7.13.0" |
571 | "@babel/plugin-syntax-async-generators" "^7.8.0" | 571 | "@babel/plugin-syntax-async-generators" "^7.8.4" |
572 | 572 | ||
573 | "@babel/plugin-proposal-class-properties@^7.12.1": | 573 | "@babel/plugin-proposal-class-properties@^7.12.1": |
574 | version "7.13.0" | 574 | version "7.13.0" |
@@ -579,12 +579,12 @@ | |||
579 | "@babel/helper-plugin-utils" "^7.13.0" | 579 | "@babel/helper-plugin-utils" "^7.13.0" |
580 | 580 | ||
581 | "@babel/plugin-proposal-dynamic-import@^7.12.1": | 581 | "@babel/plugin-proposal-dynamic-import@^7.12.1": |
582 | version "7.12.17" | 582 | version "7.13.8" |
583 | resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.17.tgz#e0ebd8db65acc37eac518fa17bead2174e224512" | 583 | resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.13.8.tgz#876a1f6966e1dec332e8c9451afda3bebcdf2e1d" |
584 | integrity sha512-ZNGoFZqrnuy9H2izB2jLlnNDAfVPlGl5NhFEiFe4D84ix9GQGygF+CWMGHKuE+bpyS/AOuDQCnkiRNqW2IzS1Q== | 584 | integrity sha512-ONWKj0H6+wIRCkZi9zSbZtE/r73uOhMVHh256ys0UzfM7I3d4n+spZNWjOnJv2gzopumP2Wxi186vI8N0Y2JyQ== |
585 | dependencies: | 585 | dependencies: |
586 | "@babel/helper-plugin-utils" "^7.12.13" | 586 | "@babel/helper-plugin-utils" "^7.13.0" |
587 | "@babel/plugin-syntax-dynamic-import" "^7.8.0" | 587 | "@babel/plugin-syntax-dynamic-import" "^7.8.3" |
588 | 588 | ||
589 | "@babel/plugin-proposal-export-namespace-from@^7.12.1": | 589 | "@babel/plugin-proposal-export-namespace-from@^7.12.1": |
590 | version "7.12.13" | 590 | version "7.12.13" |
@@ -595,28 +595,28 @@ | |||
595 | "@babel/plugin-syntax-export-namespace-from" "^7.8.3" | 595 | "@babel/plugin-syntax-export-namespace-from" "^7.8.3" |
596 | 596 | ||
597 | "@babel/plugin-proposal-json-strings@^7.12.1": | 597 | "@babel/plugin-proposal-json-strings@^7.12.1": |
598 | version "7.12.13" | 598 | version "7.13.8" |
599 | resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.13.tgz#ced7888a2db92a3d520a2e35eb421fdb7fcc9b5d" | 599 | resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.13.8.tgz#bf1fb362547075afda3634ed31571c5901afef7b" |
600 | integrity sha512-v9eEi4GiORDg8x+Dmi5r8ibOe0VXoKDeNPYcTTxdGN4eOWikrJfDJCJrr1l5gKGvsNyGJbrfMftC2dTL6oz7pg== | 600 | integrity sha512-w4zOPKUFPX1mgvTmL/fcEqy34hrQ1CRcGxdphBc6snDnnqJ47EZDIyop6IwXzAC8G916hsIuXB2ZMBCExC5k7Q== |
601 | dependencies: | 601 | dependencies: |
602 | "@babel/helper-plugin-utils" "^7.12.13" | 602 | "@babel/helper-plugin-utils" "^7.13.0" |
603 | "@babel/plugin-syntax-json-strings" "^7.8.0" | 603 | "@babel/plugin-syntax-json-strings" "^7.8.3" |
604 | 604 | ||
605 | "@babel/plugin-proposal-logical-assignment-operators@^7.12.1": | 605 | "@babel/plugin-proposal-logical-assignment-operators@^7.12.1": |
606 | version "7.12.13" | 606 | version "7.13.8" |
607 | resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.13.tgz#575b5d9a08d8299eeb4db6430da6e16e5cf14350" | 607 | resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.13.8.tgz#93fa78d63857c40ce3c8c3315220fd00bfbb4e1a" |
608 | integrity sha512-fqmiD3Lz7jVdK6kabeSr1PZlWSUVqSitmHEe3Z00dtGTKieWnX9beafvavc32kjORa5Bai4QNHgFDwWJP+WtSQ== | 608 | integrity sha512-aul6znYB4N4HGweImqKn59Su9RS8lbUIqxtXTOcAGtNIDczoEFv+l1EhmX8rUBp3G1jMjKJm8m0jXVp63ZpS4A== |
609 | dependencies: | 609 | dependencies: |
610 | "@babel/helper-plugin-utils" "^7.12.13" | 610 | "@babel/helper-plugin-utils" "^7.13.0" |
611 | "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" | 611 | "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" |
612 | 612 | ||
613 | "@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1": | 613 | "@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1": |
614 | version "7.13.0" | 614 | version "7.13.8" |
615 | resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.13.0.tgz#1a96fdf2c43109cfe5568513c5379015a23f5380" | 615 | resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.13.8.tgz#3730a31dafd3c10d8ccd10648ed80a2ac5472ef3" |
616 | integrity sha512-UkAvFA/9+lBBL015gjA68NvKiCReNxqFLm3SdNKaM3XXoDisA7tMAIX4PmIwatFoFqMxxT3WyG9sK3MO0Kting== | 616 | integrity sha512-iePlDPBn//UhxExyS9KyeYU7RM9WScAG+D3Hhno0PLJebAEpDZMocbDe64eqynhNAnwz/vZoL/q/QB2T1OH39A== |
617 | dependencies: | 617 | dependencies: |
618 | "@babel/helper-plugin-utils" "^7.13.0" | 618 | "@babel/helper-plugin-utils" "^7.13.0" |
619 | "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" | 619 | "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" |
620 | 620 | ||
621 | "@babel/plugin-proposal-numeric-separator@^7.12.7": | 621 | "@babel/plugin-proposal-numeric-separator@^7.12.7": |
622 | version "7.12.13" | 622 | version "7.12.13" |
@@ -627,30 +627,32 @@ | |||
627 | "@babel/plugin-syntax-numeric-separator" "^7.10.4" | 627 | "@babel/plugin-syntax-numeric-separator" "^7.10.4" |
628 | 628 | ||
629 | "@babel/plugin-proposal-object-rest-spread@^7.12.1": | 629 | "@babel/plugin-proposal-object-rest-spread@^7.12.1": |
630 | version "7.13.0" | 630 | version "7.13.8" |
631 | resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.13.0.tgz#8f19ad247bb96bd5ad2d4107e6eddfe0a789937b" | 631 | resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.13.8.tgz#5d210a4d727d6ce3b18f9de82cc99a3964eed60a" |
632 | integrity sha512-B4qphdSTp0nLsWcuei07JPKeZej4+Hd22MdnulJXQa1nCcGSBlk8FiqenGERaPZ+PuYhz4Li2Wjc8yfJvHgUMw== | 632 | integrity sha512-DhB2EuB1Ih7S3/IRX5AFVgZ16k3EzfRbq97CxAVI1KSYcW+lexV8VZb7G7L8zuPVSdQMRn0kiBpf/Yzu9ZKH0g== |
633 | dependencies: | 633 | dependencies: |
634 | "@babel/compat-data" "^7.13.8" | ||
635 | "@babel/helper-compilation-targets" "^7.13.8" | ||
634 | "@babel/helper-plugin-utils" "^7.13.0" | 636 | "@babel/helper-plugin-utils" "^7.13.0" |
635 | "@babel/plugin-syntax-object-rest-spread" "^7.8.0" | 637 | "@babel/plugin-syntax-object-rest-spread" "^7.8.3" |
636 | "@babel/plugin-transform-parameters" "^7.13.0" | 638 | "@babel/plugin-transform-parameters" "^7.13.0" |
637 | 639 | ||
638 | "@babel/plugin-proposal-optional-catch-binding@^7.12.1": | 640 | "@babel/plugin-proposal-optional-catch-binding@^7.12.1": |
639 | version "7.12.13" | 641 | version "7.13.8" |
640 | resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.13.tgz#4640520afe57728af14b4d1574ba844f263bcae5" | 642 | resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.13.8.tgz#3ad6bd5901506ea996fc31bdcf3ccfa2bed71107" |
641 | integrity sha512-9+MIm6msl9sHWg58NvqpNpLtuFbmpFYk37x8kgnGzAHvX35E1FyAwSUt5hIkSoWJFSAH+iwU8bJ4fcD1zKXOzg== | 643 | integrity sha512-0wS/4DUF1CuTmGo+NiaHfHcVSeSLj5S3e6RivPTg/2k3wOv3jO35tZ6/ZWsQhQMvdgI7CwphjQa/ccarLymHVA== |
642 | dependencies: | 644 | dependencies: |
643 | "@babel/helper-plugin-utils" "^7.12.13" | 645 | "@babel/helper-plugin-utils" "^7.13.0" |
644 | "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" | 646 | "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" |
645 | 647 | ||
646 | "@babel/plugin-proposal-optional-chaining@^7.12.7": | 648 | "@babel/plugin-proposal-optional-chaining@^7.12.7": |
647 | version "7.13.0" | 649 | version "7.13.12" |
648 | resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.0.tgz#75b41ce0d883d19e8fe635fc3f846be3b1664f4d" | 650 | resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.12.tgz#ba9feb601d422e0adea6760c2bd6bbb7bfec4866" |
649 | integrity sha512-OVRQOZEBP2luZrvEbNSX5FfWDousthhdEoAOpej+Tpe58HFLvqRClT89RauIvBuCDFEip7GW1eT86/5lMy2RNA== | 651 | integrity sha512-fcEdKOkIB7Tf4IxrgEVeFC4zeJSTr78no9wTdBuZZbqF64kzllU0ybo2zrzm7gUQfxGhBgq4E39oRs8Zx/RMYQ== |
650 | dependencies: | 652 | dependencies: |
651 | "@babel/helper-plugin-utils" "^7.13.0" | 653 | "@babel/helper-plugin-utils" "^7.13.0" |
652 | "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" | 654 | "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" |
653 | "@babel/plugin-syntax-optional-chaining" "^7.8.0" | 655 | "@babel/plugin-syntax-optional-chaining" "^7.8.3" |
654 | 656 | ||
655 | "@babel/plugin-proposal-private-methods@^7.12.1": | 657 | "@babel/plugin-proposal-private-methods@^7.12.1": |
656 | version "7.13.0" | 658 | version "7.13.0" |
@@ -668,7 +670,7 @@ | |||
668 | "@babel/helper-create-regexp-features-plugin" "^7.12.13" | 670 | "@babel/helper-create-regexp-features-plugin" "^7.12.13" |
669 | "@babel/helper-plugin-utils" "^7.12.13" | 671 | "@babel/helper-plugin-utils" "^7.12.13" |
670 | 672 | ||
671 | "@babel/plugin-syntax-async-generators@^7.8.0": | 673 | "@babel/plugin-syntax-async-generators@^7.8.0", "@babel/plugin-syntax-async-generators@^7.8.4": |
672 | version "7.8.4" | 674 | version "7.8.4" |
673 | resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" | 675 | resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" |
674 | integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== | 676 | integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== |
@@ -682,7 +684,7 @@ | |||
682 | dependencies: | 684 | dependencies: |
683 | "@babel/helper-plugin-utils" "^7.12.13" | 685 | "@babel/helper-plugin-utils" "^7.12.13" |
684 | 686 | ||
685 | "@babel/plugin-syntax-dynamic-import@^7.8.0": | 687 | "@babel/plugin-syntax-dynamic-import@^7.8.0", "@babel/plugin-syntax-dynamic-import@^7.8.3": |
686 | version "7.8.3" | 688 | version "7.8.3" |
687 | resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" | 689 | resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" |
688 | integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== | 690 | integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== |
@@ -696,7 +698,7 @@ | |||
696 | dependencies: | 698 | dependencies: |
697 | "@babel/helper-plugin-utils" "^7.8.3" | 699 | "@babel/helper-plugin-utils" "^7.8.3" |
698 | 700 | ||
699 | "@babel/plugin-syntax-json-strings@^7.8.0": | 701 | "@babel/plugin-syntax-json-strings@^7.8.0", "@babel/plugin-syntax-json-strings@^7.8.3": |
700 | version "7.8.3" | 702 | version "7.8.3" |
701 | resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" | 703 | resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" |
702 | integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== | 704 | integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== |
@@ -710,7 +712,7 @@ | |||
710 | dependencies: | 712 | dependencies: |
711 | "@babel/helper-plugin-utils" "^7.10.4" | 713 | "@babel/helper-plugin-utils" "^7.10.4" |
712 | 714 | ||
713 | "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0": | 715 | "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": |
714 | version "7.8.3" | 716 | version "7.8.3" |
715 | resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" | 717 | resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" |
716 | integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== | 718 | integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== |
@@ -724,21 +726,21 @@ | |||
724 | dependencies: | 726 | dependencies: |
725 | "@babel/helper-plugin-utils" "^7.10.4" | 727 | "@babel/helper-plugin-utils" "^7.10.4" |
726 | 728 | ||
727 | "@babel/plugin-syntax-object-rest-spread@^7.8.0": | 729 | "@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": |
728 | version "7.8.3" | 730 | version "7.8.3" |
729 | resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" | 731 | resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" |
730 | integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== | 732 | integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== |
731 | dependencies: | 733 | dependencies: |
732 | "@babel/helper-plugin-utils" "^7.8.0" | 734 | "@babel/helper-plugin-utils" "^7.8.0" |
733 | 735 | ||
734 | "@babel/plugin-syntax-optional-catch-binding@^7.8.0": | 736 | "@babel/plugin-syntax-optional-catch-binding@^7.8.0", "@babel/plugin-syntax-optional-catch-binding@^7.8.3": |
735 | version "7.8.3" | 737 | version "7.8.3" |
736 | resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" | 738 | resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" |
737 | integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== | 739 | integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== |
738 | dependencies: | 740 | dependencies: |
739 | "@babel/helper-plugin-utils" "^7.8.0" | 741 | "@babel/helper-plugin-utils" "^7.8.0" |
740 | 742 | ||
741 | "@babel/plugin-syntax-optional-chaining@^7.8.0": | 743 | "@babel/plugin-syntax-optional-chaining@^7.8.0", "@babel/plugin-syntax-optional-chaining@^7.8.3": |
742 | version "7.8.3" | 744 | version "7.8.3" |
743 | resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" | 745 | resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" |
744 | integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== | 746 | integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== |
@@ -880,9 +882,9 @@ | |||
880 | babel-plugin-dynamic-import-node "^2.3.3" | 882 | babel-plugin-dynamic-import-node "^2.3.3" |
881 | 883 | ||
882 | "@babel/plugin-transform-modules-commonjs@^7.12.1": | 884 | "@babel/plugin-transform-modules-commonjs@^7.12.1": |
883 | version "7.13.0" | 885 | version "7.13.8" |
884 | resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.13.0.tgz#276932693a20d12c9776093fdc99c0d9995e34c6" | 886 | resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.13.8.tgz#7b01ad7c2dcf2275b06fa1781e00d13d420b3e1b" |
885 | integrity sha512-j7397PkIB4lcn25U2dClK6VLC6pr2s3q+wbE8R3vJvY6U1UTBBj0n6F+5v6+Fd/UwfDPAorMOs2TV+T4M+owpQ== | 887 | integrity sha512-9QiOx4MEGglfYZ4XOnU79OHr6vIWUakIj9b4mioN8eQIoEh+pf5p/zEB36JpDFWA12nNMiRf7bfoRvl9Rn79Bw== |
886 | dependencies: | 888 | dependencies: |
887 | "@babel/helper-module-transforms" "^7.13.0" | 889 | "@babel/helper-module-transforms" "^7.13.0" |
888 | "@babel/helper-plugin-utils" "^7.13.0" | 890 | "@babel/helper-plugin-utils" "^7.13.0" |
@@ -890,13 +892,13 @@ | |||
890 | babel-plugin-dynamic-import-node "^2.3.3" | 892 | babel-plugin-dynamic-import-node "^2.3.3" |
891 | 893 | ||
892 | "@babel/plugin-transform-modules-systemjs@^7.12.1": | 894 | "@babel/plugin-transform-modules-systemjs@^7.12.1": |
893 | version "7.12.13" | 895 | version "7.13.8" |
894 | resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.13.tgz#351937f392c7f07493fc79b2118201d50404a3c5" | 896 | resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.13.8.tgz#6d066ee2bff3c7b3d60bf28dec169ad993831ae3" |
895 | integrity sha512-aHfVjhZ8QekaNF/5aNdStCGzwTbU7SI5hUybBKlMzqIMC7w7Ho8hx5a4R/DkTHfRfLwHGGxSpFt9BfxKCoXKoA== | 897 | integrity sha512-hwqctPYjhM6cWvVIlOIe27jCIBgHCsdH2xCJVAYQm7V5yTMoilbVMi9f6wKg0rpQAOn6ZG4AOyvCqFF/hUh6+A== |
896 | dependencies: | 898 | dependencies: |
897 | "@babel/helper-hoist-variables" "^7.12.13" | 899 | "@babel/helper-hoist-variables" "^7.13.0" |
898 | "@babel/helper-module-transforms" "^7.12.13" | 900 | "@babel/helper-module-transforms" "^7.13.0" |
899 | "@babel/helper-plugin-utils" "^7.12.13" | 901 | "@babel/helper-plugin-utils" "^7.13.0" |
900 | "@babel/helper-validator-identifier" "^7.12.11" | 902 | "@babel/helper-validator-identifier" "^7.12.11" |
901 | babel-plugin-dynamic-import-node "^2.3.3" | 903 | babel-plugin-dynamic-import-node "^2.3.3" |
902 | 904 | ||
@@ -1109,9 +1111,9 @@ | |||
1109 | regenerator-runtime "^0.13.4" | 1111 | regenerator-runtime "^0.13.4" |
1110 | 1112 | ||
1111 | "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": | 1113 | "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": |
1112 | version "7.13.7" | 1114 | version "7.13.10" |
1113 | resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.7.tgz#d494e39d198ee9ca04f4dcb76d25d9d7a1dc961a" | 1115 | resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d" |
1114 | integrity sha512-h+ilqoX998mRVM5FtB5ijRuHUDVt5l3yfoOi2uh18Z/O3hvyaHQ39NpxVkCIG5yFs+mLq/ewFp8Bss6zmWv6ZA== | 1116 | integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw== |
1115 | dependencies: | 1117 | dependencies: |
1116 | regenerator-runtime "^0.13.4" | 1118 | regenerator-runtime "^0.13.4" |
1117 | 1119 | ||
@@ -1148,16 +1150,16 @@ | |||
1148 | globals "^11.1.0" | 1150 | globals "^11.1.0" |
1149 | lodash "^4.17.19" | 1151 | lodash "^4.17.19" |
1150 | 1152 | ||
1151 | "@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.13", "@babel/types@^7.12.7", "@babel/types@^7.13.0", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6": | 1153 | "@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.13", "@babel/types@^7.12.7", "@babel/types@^7.13.0", "@babel/types@^7.13.12", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6": |
1152 | version "7.13.0" | 1154 | version "7.13.12" |
1153 | resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80" | 1155 | resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.12.tgz#edbf99208ef48852acdff1c8a681a1e4ade580cd" |
1154 | integrity sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA== | 1156 | integrity sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA== |
1155 | dependencies: | 1157 | dependencies: |
1156 | "@babel/helper-validator-identifier" "^7.12.11" | 1158 | "@babel/helper-validator-identifier" "^7.12.11" |
1157 | lodash "^4.17.19" | 1159 | lodash "^4.17.19" |
1158 | to-fast-properties "^2.0.0" | 1160 | to-fast-properties "^2.0.0" |
1159 | 1161 | ||
1160 | "@discoveryjs/json-ext@^0.5.0": | 1162 | "@discoveryjs/json-ext@0.5.2", "@discoveryjs/json-ext@^0.5.0": |
1161 | version "0.5.2" | 1163 | version "0.5.2" |
1162 | resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz#8f03a22a04de437254e8ce8cc84ba39689288752" | 1164 | resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz#8f03a22a04de437254e8ce8cc84ba39689288752" |
1163 | integrity sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg== | 1165 | integrity sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg== |
@@ -1197,12 +1199,12 @@ | |||
1197 | dependencies: | 1199 | dependencies: |
1198 | tslib "^2.0.0" | 1200 | tslib "^2.0.0" |
1199 | 1201 | ||
1200 | "@ngtools/webpack@11.2.2": | 1202 | "@ngtools/webpack@11.2.5": |
1201 | version "11.2.2" | 1203 | version "11.2.5" |
1202 | resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-11.2.2.tgz#647862ed19761796c7f84d5fb3305661d2a3af67" | 1204 | resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-11.2.5.tgz#3e2265145d19fcdda9ec2894ccded83658b1fa66" |
1203 | integrity sha512-X1M/Xs0kLi9FrOIU6yJ74q3pCzhgwPQowO1XjJ68KLOoMbj/DM6Qm0Hi9N0Ay8h0s7BIdjKEu/C3pCdGu1Q54w== | 1205 | integrity sha512-7fhg8hvqTiTS5ESiEN4xR2qRnOVX0rhVSckMXbAFvNYTwQOuS865RiBrYCJ4CsKhGJ9P7XS5i2EIwA3/aLSivg== |
1204 | dependencies: | 1206 | dependencies: |
1205 | "@angular-devkit/core" "11.2.2" | 1207 | "@angular-devkit/core" "11.2.5" |
1206 | enhanced-resolve "5.7.0" | 1208 | enhanced-resolve "5.7.0" |
1207 | webpack-sources "2.2.0" | 1209 | webpack-sources "2.2.0" |
1208 | 1210 | ||
@@ -1331,15 +1333,14 @@ | |||
1331 | infer-owner "^1.0.4" | 1333 | infer-owner "^1.0.4" |
1332 | 1334 | ||
1333 | "@npmcli/run-script@^1.3.0": | 1335 | "@npmcli/run-script@^1.3.0": |
1334 | version "1.8.3" | 1336 | version "1.8.4" |
1335 | resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-1.8.3.tgz#07f440ed492400bb1114369bc37315eeaaae2bb3" | 1337 | resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-1.8.4.tgz#03ced92503a6fe948cbc0975ce39210bc5e824d6" |
1336 | integrity sha512-ELPGWAVU/xyU+A+H3pEPj0QOvYwLTX71RArXcClFzeiyJ/b/McsZ+d0QxpznvfFtZzxGN/gz/1cvlqICR4/suQ== | 1338 | integrity sha512-Yd9HXTtF1JGDXZw0+SOn+mWLYS0e7bHBHVC/2C8yqs4wUrs/k8rwBSinD7rfk+3WG/MFGRZKxjyoD34Pch2E/A== |
1337 | dependencies: | 1339 | dependencies: |
1338 | "@npmcli/node-gyp" "^1.0.2" | 1340 | "@npmcli/node-gyp" "^1.0.2" |
1339 | "@npmcli/promise-spawn" "^1.3.2" | 1341 | "@npmcli/promise-spawn" "^1.3.2" |
1340 | infer-owner "^1.0.4" | 1342 | infer-owner "^1.0.4" |
1341 | node-gyp "^7.1.0" | 1343 | node-gyp "^7.1.0" |
1342 | puka "^1.0.1" | ||
1343 | read-package-json-fast "^2.0.1" | 1344 | read-package-json-fast "^2.0.1" |
1344 | 1345 | ||
1345 | "@polka/url@^1.0.0-next.9": | 1346 | "@polka/url@^1.0.0-next.9": |
@@ -1347,22 +1348,22 @@ | |||
1347 | resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.11.tgz#aeb16f50649a91af79dbe36574b66d0f9e4d9f71" | 1348 | resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.11.tgz#aeb16f50649a91af79dbe36574b66d0f9e4d9f71" |
1348 | integrity sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA== | 1349 | integrity sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA== |
1349 | 1350 | ||
1350 | "@schematics/angular@11.2.2": | 1351 | "@schematics/angular@11.2.5": |
1351 | version "11.2.2" | 1352 | version "11.2.5" |
1352 | resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-11.2.2.tgz#ff69a66b6e1acf5aa36ed0795973f3f57d893d0b" | 1353 | resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-11.2.5.tgz#c984687c95be32d3fa6016faa8b5a61715a830f5" |
1353 | integrity sha512-TcxPy58adUnkirGXyZVVSMuKkA0eIz2PWSQWEgB9l7kO+5LvDOn+RMoc6AVx0s/bU9nH+eozBUJ1XAD/E8QnYQ== | 1354 | integrity sha512-pjaK0gZyqhzgAVxMKElG6cDpAvNZ3adVCTA8dhEixpH+JaQdoczl59hMn7rH75yQW0PApe+8g7HMwVK6bLRmxQ== |
1354 | dependencies: | 1355 | dependencies: |
1355 | "@angular-devkit/core" "11.2.2" | 1356 | "@angular-devkit/core" "11.2.5" |
1356 | "@angular-devkit/schematics" "11.2.2" | 1357 | "@angular-devkit/schematics" "11.2.5" |
1357 | jsonc-parser "3.0.0" | 1358 | jsonc-parser "3.0.0" |
1358 | 1359 | ||
1359 | "@schematics/update@0.1102.2": | 1360 | "@schematics/update@0.1102.5": |
1360 | version "0.1102.2" | 1361 | version "0.1102.5" |
1361 | resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.1102.2.tgz#f8aed68bbcefdc8633c7804e47ff891ef06bd5ef" | 1362 | resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.1102.5.tgz#538493f0a7d06d794d521cca4f2ff588f05cc733" |
1362 | integrity sha512-Nz8kjeixzDnOw00bnZznq3qrbIv8yWEWNb9eDkRBqgOUXQwlhKJY/sYBK58JF2D+conaRVuEqMsBlX08GlFtIA== | 1363 | integrity sha512-iz9pM8mabieqQnPZjrqP5jfRFvPm81/uIg46kY3KjtDtSBi4GAF2dnFyX1dC2mG1rq+e+8zeQLvOvhdLifYlEA== |
1363 | dependencies: | 1364 | dependencies: |
1364 | "@angular-devkit/core" "11.2.2" | 1365 | "@angular-devkit/core" "11.2.5" |
1365 | "@angular-devkit/schematics" "11.2.2" | 1366 | "@angular-devkit/schematics" "11.2.5" |
1366 | "@yarnpkg/lockfile" "1.1.0" | 1367 | "@yarnpkg/lockfile" "1.1.0" |
1367 | ini "2.0.0" | 1368 | ini "2.0.0" |
1368 | npm-package-arg "^8.0.0" | 1369 | npm-package-arg "^8.0.0" |
@@ -1388,9 +1389,9 @@ | |||
1388 | "@types/node" "*" | 1389 | "@types/node" "*" |
1389 | 1390 | ||
1390 | "@types/chart.js@^2.9.16": | 1391 | "@types/chart.js@^2.9.16": |
1391 | version "2.9.30" | 1392 | version "2.9.31" |
1392 | resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.30.tgz#34b99897f4f5ef0f74c8fe4ced70ac52b4d752dd" | 1393 | resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.31.tgz#e8ebc7ed18eb0e5114c69bd46ef8e0037c89d39d" |
1393 | integrity sha512-EgjxUUZFvf6ls3kW2CwyrnSJhgyKxgwrlp/W5G9wqyPEO9iFatO63zAA7L24YqgMxiDjQ+tG7ODU+2yWH91lPg== | 1394 | integrity sha512-hzS6phN/kx3jClk3iYqEHNnYIRSi4RZrIGJ8CDLjgatpHoftCezvC44uqB3o3OUm9ftU1m7sHG8+RLyPTlACrA== |
1394 | dependencies: | 1395 | dependencies: |
1395 | moment "^2.10.2" | 1396 | moment "^2.10.2" |
1396 | 1397 | ||
@@ -1443,9 +1444,9 @@ | |||
1443 | integrity sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA== | 1444 | integrity sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA== |
1444 | 1445 | ||
1445 | "@types/jasmine@*", "@types/jasmine@^3.3.15": | 1446 | "@types/jasmine@*", "@types/jasmine@^3.3.15": |
1446 | version "3.6.4" | 1447 | version "3.6.7" |
1447 | resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.6.4.tgz#22ade1b692d5656f859ef9bc6c62d88632cc27e0" | 1448 | resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.6.7.tgz#e762d3ead78538efb7900ab932d7daf334acb0b4" |
1448 | integrity sha512-CTdMERA4iGNcxeqzD7pavb4WLIFq6bGnx6nIJD+1D4Knx24GE6QBPrWVhO8UlIy7gf7rbIt3ZD7iIzryRD2TgA== | 1449 | integrity sha512-8dtfiykrpe4Ysn6ONj0tOjmpDIh1vWxPk80eutSeWmyaJvAZXZ84219fS4gLrvz05eidhp7BP17WVQBaXHSyXQ== |
1449 | 1450 | ||
1450 | "@types/jasminewd2@^2.0.3": | 1451 | "@types/jasminewd2@^2.0.3": |
1451 | version "2.0.8" | 1452 | version "2.0.8" |
@@ -1465,9 +1466,9 @@ | |||
1465 | integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== | 1466 | integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== |
1466 | 1467 | ||
1467 | "@types/linkify-it@*": | 1468 | "@types/linkify-it@*": |
1468 | version "3.0.0" | 1469 | version "3.0.1" |
1469 | resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.0.tgz#c0ca4c253664492dbf47a646f31cfd483a6bbc95" | 1470 | resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.1.tgz#4d26a9efe3aa2caf829234ec5a39580fc88b6001" |
1470 | integrity sha512-x9OaQQTb1N2hPZ/LWJsqushexDvz7NgzuZxiRmZio44WPuolTZNHDBCrOxCzRVOMwamJRO2dWax5NbygOf1OTQ== | 1471 | integrity sha512-pQv3Sygwxxh6jYQzXaiyWDAHevJqWtqDUv6t11Sa9CPGiXny66II7Pl6PR8QO5OVysD6HYOkHMeBgIjLnk9SkQ== |
1471 | 1472 | ||
1472 | "@types/linkifyjs@^2.1.2": | 1473 | "@types/linkifyjs@^2.1.2": |
1473 | version "2.1.3" | 1474 | version "2.1.3" |
@@ -1519,10 +1520,10 @@ | |||
1519 | resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.3.tgz#3159a01a2b21c9155a3d8f85588885d725dc987d" | 1520 | resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.3.tgz#3159a01a2b21c9155a3d8f85588885d725dc987d" |
1520 | integrity sha512-13gmo3M2qVvjQrWNseqM3+cR6S2Ss3grbR2NZltgMq94wOwqJYQdgn8qzwDshzgXqMlSUtyPZjysImmktu22ew== | 1521 | integrity sha512-13gmo3M2qVvjQrWNseqM3+cR6S2Ss3grbR2NZltgMq94wOwqJYQdgn8qzwDshzgXqMlSUtyPZjysImmktu22ew== |
1521 | 1522 | ||
1522 | "@types/node@*", "@types/node@^14.0.14", "@types/node@^14.14.10": | 1523 | "@types/node@*", "@types/node@>=10.0.0", "@types/node@^14.0.14": |
1523 | version "14.14.31" | 1524 | version "14.14.35" |
1524 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055" | 1525 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.35.tgz#42c953a4e2b18ab931f72477e7012172f4ffa313" |
1525 | integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g== | 1526 | integrity sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag== |
1526 | 1527 | ||
1527 | "@types/parse-json@^4.0.0": | 1528 | "@types/parse-json@^4.0.0": |
1528 | version "4.0.0" | 1529 | version "4.0.0" |
@@ -1561,11 +1562,12 @@ | |||
1561 | integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== | 1562 | integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== |
1562 | 1563 | ||
1563 | "@types/react@*": | 1564 | "@types/react@*": |
1564 | version "17.0.2" | 1565 | version "17.0.3" |
1565 | resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.2.tgz#3de24c4efef902dd9795a49c75f760cbe4f7a5a8" | 1566 | resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79" |
1566 | integrity sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA== | 1567 | integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg== |
1567 | dependencies: | 1568 | dependencies: |
1568 | "@types/prop-types" "*" | 1569 | "@types/prop-types" "*" |
1570 | "@types/scheduler" "*" | ||
1569 | csstype "^3.0.2" | 1571 | csstype "^3.0.2" |
1570 | 1572 | ||
1571 | "@types/sanitize-html@1.27.1": | 1573 | "@types/sanitize-html@1.27.1": |
@@ -1575,6 +1577,11 @@ | |||
1575 | dependencies: | 1577 | dependencies: |
1576 | htmlparser2 "^4.1.0" | 1578 | htmlparser2 "^4.1.0" |
1577 | 1579 | ||
1580 | "@types/scheduler@*": | ||
1581 | version "0.16.1" | ||
1582 | resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" | ||
1583 | integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== | ||
1584 | |||
1578 | "@types/selenium-webdriver@^3.0.0": | 1585 | "@types/selenium-webdriver@^3.0.0": |
1579 | version "3.0.17" | 1586 | version "3.0.17" |
1580 | resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.17.tgz#50bea0c3c2acc31c959c5b1e747798b3b3d06d4b" | 1587 | resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.17.tgz#50bea0c3c2acc31c959c5b1e747798b3b3d06d4b" |
@@ -1605,9 +1612,9 @@ | |||
1605 | integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA== | 1612 | integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA== |
1606 | 1613 | ||
1607 | "@types/uglify-js@*": | 1614 | "@types/uglify-js@*": |
1608 | version "3.12.0" | 1615 | version "3.13.0" |
1609 | resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.12.0.tgz#2bb061c269441620d46b946350c8f16d52ef37c5" | 1616 | resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.0.tgz#1cad8df1fb0b143c5aba08de5712ea9d1ff71124" |
1610 | integrity sha512-sYAF+CF9XZ5cvEBkI7RtrG9g2GtMBkviTnBxYYyq+8BWvO4QtXfwwR6a2LFwCi4evMKZfpv6U43ViYvv17Wz3Q== | 1617 | integrity sha512-EGkrJD5Uy+Pg0NUR8uA4bJ5WMfljyad0G+784vLCNUkD+QwOJXUbBYExXfVGf7YtyzdQp3L/XMYcliB987kL5Q== |
1611 | dependencies: | 1618 | dependencies: |
1612 | source-map "^0.6.1" | 1619 | source-map "^0.6.1" |
1613 | 1620 | ||
@@ -1925,9 +1932,9 @@ acorn@^6.4.1: | |||
1925 | integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== | 1932 | integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== |
1926 | 1933 | ||
1927 | acorn@^8.0.4: | 1934 | acorn@^8.0.4: |
1928 | version "8.0.5" | 1935 | version "8.1.0" |
1929 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.0.5.tgz#a3bfb872a74a6a7f661bc81b9849d9cac12601b7" | 1936 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.0.tgz#52311fd7037ae119cbb134309e901aa46295b3fe" |
1930 | integrity sha512-v+DieK/HJkJOpFBETDJioequtc3PfxsWMaxIdIwujtF7FEV/MAyDQLlm6/zPvr7Mix07mLh6ccVwIsloceodlg== | 1937 | integrity sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA== |
1931 | 1938 | ||
1932 | addr-to-ip-port@^1.0.1, addr-to-ip-port@^1.5.1: | 1939 | addr-to-ip-port@^1.0.1, addr-to-ip-port@^1.5.1: |
1933 | version "1.5.1" | 1940 | version "1.5.1" |
@@ -2793,7 +2800,7 @@ bytes@3.1.0: | |||
2793 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" | 2800 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" |
2794 | integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== | 2801 | integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== |
2795 | 2802 | ||
2796 | cacache@15.0.5, cacache@^15.0.5: | 2803 | cacache@15.0.5: |
2797 | version "15.0.5" | 2804 | version "15.0.5" |
2798 | resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0" | 2805 | resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0" |
2799 | integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A== | 2806 | integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A== |
@@ -2837,6 +2844,29 @@ cacache@^12.0.2: | |||
2837 | unique-filename "^1.1.1" | 2844 | unique-filename "^1.1.1" |
2838 | y18n "^4.0.0" | 2845 | y18n "^4.0.0" |
2839 | 2846 | ||
2847 | cacache@^15.0.5: | ||
2848 | version "15.0.6" | ||
2849 | resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.6.tgz#65a8c580fda15b59150fb76bf3f3a8e45d583099" | ||
2850 | integrity sha512-g1WYDMct/jzW+JdWEyjaX2zoBkZ6ZT9VpOyp2I/VMtDsNLffNat3kqPFfi1eDRSK9/SuKGyORDHcQMcPF8sQ/w== | ||
2851 | dependencies: | ||
2852 | "@npmcli/move-file" "^1.0.1" | ||
2853 | chownr "^2.0.0" | ||
2854 | fs-minipass "^2.0.0" | ||
2855 | glob "^7.1.4" | ||
2856 | infer-owner "^1.0.4" | ||
2857 | lru-cache "^6.0.0" | ||
2858 | minipass "^3.1.1" | ||
2859 | minipass-collect "^1.0.2" | ||
2860 | minipass-flush "^1.0.5" | ||
2861 | minipass-pipeline "^1.2.2" | ||
2862 | mkdirp "^1.0.3" | ||
2863 | p-map "^4.0.0" | ||
2864 | promise-inflight "^1.0.1" | ||
2865 | rimraf "^3.0.2" | ||
2866 | ssri "^8.0.1" | ||
2867 | tar "^6.0.2" | ||
2868 | unique-filename "^1.1.1" | ||
2869 | |||
2840 | cache-base@^1.0.1: | 2870 | cache-base@^1.0.1: |
2841 | version "1.0.1" | 2871 | version "1.0.1" |
2842 | resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" | 2872 | resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" |
@@ -2937,9 +2967,9 @@ caniuse-api@^3.0.0: | |||
2937 | lodash.uniq "^4.5.0" | 2967 | lodash.uniq "^4.5.0" |
2938 | 2968 | ||
2939 | caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001181: | 2969 | caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001181: |
2940 | version "1.0.30001192" | 2970 | version "1.0.30001204" |
2941 | resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001192.tgz#b848ebc0ab230cf313d194a4775a30155d50ae40" | 2971 | resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001204.tgz#256c85709a348ec4d175e847a3b515c66e79f2aa" |
2942 | integrity sha512-63OrUnwJj5T1rUmoyqYTdRWBqFFxZFlyZnRRjDR8NSUQFB6A+j/uBORU/SyJ5WzDLg4SPiZH40hQCBNdZ/jmAw== | 2972 | integrity sha512-JUdjWpcxfJ9IPamy2f5JaRDCaqJOxDzOSKtbdx4rH9VivMd1vIzoPumsJa9LoMIi4Fx2BV2KZOxWhNkBjaYivQ== |
2943 | 2973 | ||
2944 | canonical-path@1.0.0: | 2974 | canonical-path@1.0.0: |
2945 | version "1.0.0" | 2975 | version "1.0.0" |
@@ -2971,7 +3001,7 @@ chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: | |||
2971 | escape-string-regexp "^1.0.5" | 3001 | escape-string-regexp "^1.0.5" |
2972 | supports-color "^5.3.0" | 3002 | supports-color "^5.3.0" |
2973 | 3003 | ||
2974 | chalk@^4.0.0, chalk@^4.1.0: | 3004 | chalk@^4.1.0: |
2975 | version "4.1.0" | 3005 | version "4.1.0" |
2976 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" | 3006 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" |
2977 | integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== | 3007 | integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== |
@@ -3081,9 +3111,9 @@ chrome-trace-event@^1.0.2: | |||
3081 | tslib "^1.9.0" | 3111 | tslib "^1.9.0" |
3082 | 3112 | ||
3083 | chunk-store-stream@^4.1.1: | 3113 | chunk-store-stream@^4.1.1: |
3084 | version "4.2.0" | 3114 | version "4.3.0" |
3085 | resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.2.0.tgz#18f673c495946c4cdcf14124a3ebd5f31eb0ea35" | 3115 | resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.3.0.tgz#3de5f4dfe19729366c29bb7ed52d139f9af29f0e" |
3086 | integrity sha512-90iueoPoqT2isnmy1fyqwzgFy5FokuaxQuijOQG1VgC/6DaXRfeYN0da8iWENkzqElWhqLxo8pWc7pH9dmxlcA== | 3116 | integrity sha512-qby+/RXoiMoTVtPiylWZt7KFF1jy6M829TzMi2hxZtBIH9ptV19wxcft6zGiXLokJgCbuZPGNGab6DWHqiSEKw== |
3087 | dependencies: | 3117 | dependencies: |
3088 | block-stream2 "^2.0.0" | 3118 | block-stream2 "^2.0.0" |
3089 | readable-stream "^3.6.0" | 3119 | readable-stream "^3.6.0" |
@@ -3143,9 +3173,9 @@ cli-cursor@^3.1.0: | |||
3143 | restore-cursor "^3.1.0" | 3173 | restore-cursor "^3.1.0" |
3144 | 3174 | ||
3145 | cli-spinners@^2.5.0: | 3175 | cli-spinners@^2.5.0: |
3146 | version "2.5.0" | 3176 | version "2.6.0" |
3147 | resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.5.0.tgz#12763e47251bf951cb75c201dfa58ff1bcb2d047" | 3177 | resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939" |
3148 | integrity sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ== | 3178 | integrity sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q== |
3149 | 3179 | ||
3150 | cli-width@^2.0.0: | 3180 | cli-width@^2.0.0: |
3151 | version "2.2.1" | 3181 | version "2.2.1" |
@@ -3279,9 +3309,9 @@ color-name@^1.0.0, color-name@~1.1.4: | |||
3279 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== | 3309 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== |
3280 | 3310 | ||
3281 | color-string@^1.5.4: | 3311 | color-string@^1.5.4: |
3282 | version "1.5.4" | 3312 | version "1.5.5" |
3283 | resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6" | 3313 | resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014" |
3284 | integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw== | 3314 | integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg== |
3285 | dependencies: | 3315 | dependencies: |
3286 | color-name "^1.0.0" | 3316 | color-name "^1.0.0" |
3287 | simple-swizzle "^0.2.2" | 3317 | simple-swizzle "^0.2.2" |
@@ -3294,10 +3324,10 @@ color@^3.0.0: | |||
3294 | color-convert "^1.9.1" | 3324 | color-convert "^1.9.1" |
3295 | color-string "^1.5.4" | 3325 | color-string "^1.5.4" |
3296 | 3326 | ||
3297 | colorette@^1.2.1: | 3327 | colorette@^1.2.1, colorette@^1.2.2: |
3298 | version "1.2.1" | 3328 | version "1.2.2" |
3299 | resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" | 3329 | resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" |
3300 | integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== | 3330 | integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== |
3301 | 3331 | ||
3302 | colors@1.4.0, colors@^1.4.0: | 3332 | colors@1.4.0, colors@^1.4.0: |
3303 | version "1.4.0" | 3333 | version "1.4.0" |
@@ -3327,9 +3357,9 @@ commander@^6.2.0: | |||
3327 | integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== | 3357 | integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== |
3328 | 3358 | ||
3329 | commander@^7.0.0: | 3359 | commander@^7.0.0: |
3330 | version "7.1.0" | 3360 | version "7.2.0" |
3331 | resolved "https://registry.yarnpkg.com/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff" | 3361 | resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" |
3332 | integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg== | 3362 | integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== |
3333 | 3363 | ||
3334 | commondir@^1.0.1: | 3364 | commondir@^1.0.1: |
3335 | version "1.0.1" | 3365 | version "1.0.1" |
@@ -3501,9 +3531,9 @@ copy-webpack-plugin@6.3.2: | |||
3501 | webpack-sources "^1.4.3" | 3531 | webpack-sources "^1.4.3" |
3502 | 3532 | ||
3503 | core-js-compat@^3.8.0: | 3533 | core-js-compat@^3.8.0: |
3504 | version "3.9.0" | 3534 | version "3.9.1" |
3505 | resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.9.0.tgz#29da39385f16b71e1915565aa0385c4e0963ad56" | 3535 | resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.9.1.tgz#4e572acfe90aff69d76d8c37759d21a5c59bb455" |
3506 | integrity sha512-YK6fwFjCOKWwGnjFUR3c544YsnA/7DoLL0ysncuOJ4pwbriAtOpvM2bygdlcXbvQCQZ7bBU9CL4t7tGl7ETRpQ== | 3536 | integrity sha512-jXAirMQxrkbiiLsCx9bQPJFA6llDadKMpYrBJQJ3/c4/vsPP/fAf29h24tviRlvwUL6AmY5CHLu2GvjuYviQqA== |
3507 | dependencies: | 3537 | dependencies: |
3508 | browserslist "^4.16.3" | 3538 | browserslist "^4.16.3" |
3509 | semver "7.0.0" | 3539 | semver "7.0.0" |
@@ -3514,9 +3544,9 @@ core-js@3.8.3: | |||
3514 | integrity sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q== | 3544 | integrity sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q== |
3515 | 3545 | ||
3516 | core-js@^3.1.4: | 3546 | core-js@^3.1.4: |
3517 | version "3.9.0" | 3547 | version "3.9.1" |
3518 | resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.0.tgz#790b1bb11553a2272b36e2625c7179db345492f8" | 3548 | resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.1.tgz#cec8de593db8eb2a85ffb0dbdeb312cb6e5460ae" |
3519 | integrity sha512-PyFBJaLq93FlyYdsndE5VaueA9K5cNB7CGzeCj191YYLhkQM0gdZR2SKihM70oF0wdqKSKClv/tEBOpoRmdOVQ== | 3549 | integrity sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg== |
3520 | 3550 | ||
3521 | core-util-is@1.0.2, core-util-is@~1.0.0: | 3551 | core-util-is@1.0.2, core-util-is@~1.0.0: |
3522 | version "1.0.2" | 3552 | version "1.0.2" |
@@ -3691,15 +3721,15 @@ css-loader@5.0.1: | |||
3691 | semver "^7.3.2" | 3721 | semver "^7.3.2" |
3692 | 3722 | ||
3693 | css-loader@^5.0.1: | 3723 | css-loader@^5.0.1: |
3694 | version "5.0.2" | 3724 | version "5.1.3" |
3695 | resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.0.2.tgz#24f758dae349bad0a440c50d7e2067742e0899cb" | 3725 | resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.3.tgz#87f6fc96816b20debe3cf682f85c7e56a963d0d1" |
3696 | integrity sha512-gbkBigdcHbmNvZ1Cg6aV6qh6k9N6XOr8YWzISLQGrwk2mgOH8LLrizhkxbDhQtaLtktyKHD4970S0xwz5btfTA== | 3726 | integrity sha512-CoPZvyh8sLiGARK3gqczpfdedbM74klGWurF2CsNZ2lhNaXdLIUks+3Mfax3WBeRuHoglU+m7KG/+7gY6G4aag== |
3697 | dependencies: | 3727 | dependencies: |
3698 | camelcase "^6.2.0" | 3728 | camelcase "^6.2.0" |
3699 | cssesc "^3.0.0" | 3729 | cssesc "^3.0.0" |
3700 | icss-utils "^5.1.0" | 3730 | icss-utils "^5.1.0" |
3701 | loader-utils "^2.0.0" | 3731 | loader-utils "^2.0.0" |
3702 | postcss "^8.2.4" | 3732 | postcss "^8.2.8" |
3703 | postcss-modules-extract-imports "^3.0.0" | 3733 | postcss-modules-extract-imports "^3.0.0" |
3704 | postcss-modules-local-by-default "^4.0.0" | 3734 | postcss-modules-local-by-default "^4.0.0" |
3705 | postcss-modules-scope "^3.0.0" | 3735 | postcss-modules-scope "^3.0.0" |
@@ -4081,9 +4111,9 @@ destroy@~1.0.4: | |||
4081 | integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= | 4111 | integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= |
4082 | 4112 | ||
4083 | detect-node@^2.0.4: | 4113 | detect-node@^2.0.4: |
4084 | version "2.0.4" | 4114 | version "2.0.5" |
4085 | resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" | 4115 | resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.5.tgz#9d270aa7eaa5af0b72c4c9d9b814e7f4ce738b79" |
4086 | integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== | 4116 | integrity sha512-qi86tE6hRcFHy8jI1m2VG+LaPUR1LhqDa5G8tVjuUXmOrpuAgqsA1pN0+ldgr3aKUH+QLI9hCY/OcRYisERejw== |
4087 | 4117 | ||
4088 | dexie@^3.0.0: | 4118 | dexie@^3.0.0: |
4089 | version "3.0.3" | 4119 | version "3.0.3" |
@@ -4241,9 +4271,9 @@ domutils@^1.5.1, domutils@^1.7.0: | |||
4241 | domelementtype "1" | 4271 | domelementtype "1" |
4242 | 4272 | ||
4243 | domutils@^2.0.0, domutils@^2.4.4: | 4273 | domutils@^2.0.0, domutils@^2.4.4: |
4244 | version "2.4.4" | 4274 | version "2.5.0" |
4245 | resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3" | 4275 | resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.5.0.tgz#42f49cffdabb92ad243278b331fd761c1c2d3039" |
4246 | integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA== | 4276 | integrity sha512-Ho16rzNMOFk2fPwChGh3D2D9OEHAfG19HgmRR2l+WLSsIstNsAYBzePH412bL0y5T44ejABIVfTHQ8nqi/tBCg== |
4247 | dependencies: | 4277 | dependencies: |
4248 | dom-serializer "^1.0.1" | 4278 | dom-serializer "^1.0.1" |
4249 | domelementtype "^2.0.1" | 4279 | domelementtype "^2.0.1" |
@@ -4293,9 +4323,9 @@ ee-first@1.1.1: | |||
4293 | integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= | 4323 | integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= |
4294 | 4324 | ||
4295 | electron-to-chromium@^1.3.649: | 4325 | electron-to-chromium@^1.3.649: |
4296 | version "1.3.673" | 4326 | version "1.3.695" |
4297 | resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.673.tgz#b4f81c930b388f962b7eba20d0483299aaa40913" | 4327 | resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.695.tgz#955f419cf99137226180cc4cca2e59015a4e248d" |
4298 | integrity sha512-ms+QR2ckfrrpEAjXweLx6kNCbpAl66DcW//3BZD4BV5KhUgr0RZRce1ON/9J3QyA3JO28nzgb5Xv8DnPr05ILg== | 4328 | integrity sha512-lz66RliUqLHU1Ojxx1A4QUxKydjiQ79Y4dZyPobs2Dmxj5aVL2TM3KoQ2Gs7HS703Bfny+ukI3KOxwAB0xceHQ== |
4299 | 4329 | ||
4300 | elliptic@^6.5.3: | 4330 | elliptic@^6.5.3: |
4301 | version "6.5.4" | 4331 | version "6.5.4" |
@@ -4357,9 +4387,9 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: | |||
4357 | once "^1.4.0" | 4387 | once "^1.4.0" |
4358 | 4388 | ||
4359 | engine.io-client@~4.1.0: | 4389 | engine.io-client@~4.1.0: |
4360 | version "4.1.1" | 4390 | version "4.1.2" |
4361 | resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-4.1.1.tgz#109942705079f15a4fcf1090bc86d3a1341c0a61" | 4391 | resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-4.1.2.tgz#823b4f005360321c41445fc23ce8ee028ef2e36b" |
4362 | integrity sha512-iYasV/EttP/2pLrdowe9G3zwlNIFhwny8VSIh+vPlMnYZqSzLsTzSLa9hFy015OrH1s4fzoYxeHjVkO8hSFKwg== | 4392 | integrity sha512-1mwvwKYMa0AaCy+sPgvJ/SnKyO5MJZ1HEeXfA3Rm/KHkHGiYD5bQVq8QzvIrkI01FuVtOdZC5lWdRw1BGXB2NQ== |
4363 | dependencies: | 4393 | dependencies: |
4364 | base64-arraybuffer "0.1.4" | 4394 | base64-arraybuffer "0.1.4" |
4365 | component-emitter "~1.3.0" | 4395 | component-emitter "~1.3.0" |
@@ -4437,9 +4467,9 @@ entities@~2.1.0: | |||
4437 | integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== | 4467 | integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== |
4438 | 4468 | ||
4439 | env-paths@^2.2.0: | 4469 | env-paths@^2.2.0: |
4440 | version "2.2.0" | 4470 | version "2.2.1" |
4441 | resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" | 4471 | resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" |
4442 | integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA== | 4472 | integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== |
4443 | 4473 | ||
4444 | envinfo@^7.7.3: | 4474 | envinfo@^7.7.3: |
4445 | version "7.7.4" | 4475 | version "7.7.4" |
@@ -4470,42 +4500,27 @@ error-ex@^1.2.0, error-ex@^1.3.1: | |||
4470 | dependencies: | 4500 | dependencies: |
4471 | is-arrayish "^0.2.1" | 4501 | is-arrayish "^0.2.1" |
4472 | 4502 | ||
4473 | es-abstract@^1.17.2: | 4503 | es-abstract@^1.17.2, es-abstract@^1.18.0-next.2: |
4474 | version "1.17.7" | 4504 | version "1.18.0" |
4475 | resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" | 4505 | resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4" |
4476 | integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== | 4506 | integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw== |
4477 | dependencies: | ||
4478 | es-to-primitive "^1.2.1" | ||
4479 | function-bind "^1.1.1" | ||
4480 | has "^1.0.3" | ||
4481 | has-symbols "^1.0.1" | ||
4482 | is-callable "^1.2.2" | ||
4483 | is-regex "^1.1.1" | ||
4484 | object-inspect "^1.8.0" | ||
4485 | object-keys "^1.1.1" | ||
4486 | object.assign "^4.1.1" | ||
4487 | string.prototype.trimend "^1.0.1" | ||
4488 | string.prototype.trimstart "^1.0.1" | ||
4489 | |||
4490 | es-abstract@^1.18.0-next.2: | ||
4491 | version "1.18.0-next.2" | ||
4492 | resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.2.tgz#088101a55f0541f595e7e057199e27ddc8f3a5c2" | ||
4493 | integrity sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw== | ||
4494 | dependencies: | 4507 | dependencies: |
4495 | call-bind "^1.0.2" | 4508 | call-bind "^1.0.2" |
4496 | es-to-primitive "^1.2.1" | 4509 | es-to-primitive "^1.2.1" |
4497 | function-bind "^1.1.1" | 4510 | function-bind "^1.1.1" |
4498 | get-intrinsic "^1.0.2" | 4511 | get-intrinsic "^1.1.1" |
4499 | has "^1.0.3" | 4512 | has "^1.0.3" |
4500 | has-symbols "^1.0.1" | 4513 | has-symbols "^1.0.2" |
4501 | is-callable "^1.2.2" | 4514 | is-callable "^1.2.3" |
4502 | is-negative-zero "^2.0.1" | 4515 | is-negative-zero "^2.0.1" |
4503 | is-regex "^1.1.1" | 4516 | is-regex "^1.1.2" |
4517 | is-string "^1.0.5" | ||
4504 | object-inspect "^1.9.0" | 4518 | object-inspect "^1.9.0" |
4505 | object-keys "^1.1.1" | 4519 | object-keys "^1.1.1" |
4506 | object.assign "^4.1.2" | 4520 | object.assign "^4.1.2" |
4507 | string.prototype.trimend "^1.0.3" | 4521 | string.prototype.trimend "^1.0.4" |
4508 | string.prototype.trimstart "^1.0.3" | 4522 | string.prototype.trimstart "^1.0.4" |
4523 | unbox-primitive "^1.0.0" | ||
4509 | 4524 | ||
4510 | es-to-primitive@^1.2.1: | 4525 | es-to-primitive@^1.2.1: |
4511 | version "1.2.1" | 4526 | version "1.2.1" |
@@ -4731,14 +4746,14 @@ eventemitter3@^4.0.0, eventemitter3@^4.0.3: | |||
4731 | integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== | 4746 | integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== |
4732 | 4747 | ||
4733 | events@^3.0.0: | 4748 | events@^3.0.0: |
4734 | version "3.2.0" | 4749 | version "3.3.0" |
4735 | resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" | 4750 | resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" |
4736 | integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== | 4751 | integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== |
4737 | 4752 | ||
4738 | eventsource@^1.0.7: | 4753 | eventsource@^1.0.7: |
4739 | version "1.0.7" | 4754 | version "1.1.0" |
4740 | resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" | 4755 | resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.0.tgz#00e8ca7c92109e94b0ddf32dac677d841028cfaf" |
4741 | integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== | 4756 | integrity sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg== |
4742 | dependencies: | 4757 | dependencies: |
4743 | original "^1.0.0" | 4758 | original "^1.0.0" |
4744 | 4759 | ||
@@ -5109,9 +5124,9 @@ focus-visible@^5.0.2: | |||
5109 | integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ== | 5124 | integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ== |
5110 | 5125 | ||
5111 | follow-redirects@^1.0.0: | 5126 | follow-redirects@^1.0.0: |
5112 | version "1.13.2" | 5127 | version "1.13.3" |
5113 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.2.tgz#dd73c8effc12728ba5cf4259d760ea5fb83e3147" | 5128 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267" |
5114 | integrity sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA== | 5129 | integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA== |
5115 | 5130 | ||
5116 | for-in@^1.0.2: | 5131 | for-in@^1.0.2: |
5117 | version "1.0.2" | 5132 | version "1.0.2" |
@@ -5301,7 +5316,7 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: | |||
5301 | resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" | 5316 | resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" |
5302 | integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== | 5317 | integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== |
5303 | 5318 | ||
5304 | get-intrinsic@^1.0.2: | 5319 | get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: |
5305 | version "1.1.1" | 5320 | version "1.1.1" |
5306 | resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" | 5321 | resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" |
5307 | integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== | 5322 | integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== |
@@ -5353,9 +5368,9 @@ glob-parent@^3.1.0: | |||
5353 | path-dirname "^1.0.0" | 5368 | path-dirname "^1.0.0" |
5354 | 5369 | ||
5355 | glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0: | 5370 | glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0: |
5356 | version "5.1.1" | 5371 | version "5.1.2" |
5357 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" | 5372 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" |
5358 | integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== | 5373 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== |
5359 | dependencies: | 5374 | dependencies: |
5360 | is-glob "^4.0.1" | 5375 | is-glob "^4.0.1" |
5361 | 5376 | ||
@@ -5410,9 +5425,9 @@ globals@^9.2.0: | |||
5410 | integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== | 5425 | integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== |
5411 | 5426 | ||
5412 | globby@^11.0.1: | 5427 | globby@^11.0.1: |
5413 | version "11.0.2" | 5428 | version "11.0.3" |
5414 | resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.2.tgz#1af538b766a3b540ebfb58a32b2e2d5897321d83" | 5429 | resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" |
5415 | integrity sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og== | 5430 | integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== |
5416 | dependencies: | 5431 | dependencies: |
5417 | array-union "^2.1.0" | 5432 | array-union "^2.1.0" |
5418 | dir-glob "^3.0.1" | 5433 | dir-glob "^3.0.1" |
@@ -5497,6 +5512,11 @@ has-ansi@^2.0.0: | |||
5497 | dependencies: | 5512 | dependencies: |
5498 | ansi-regex "^2.0.0" | 5513 | ansi-regex "^2.0.0" |
5499 | 5514 | ||
5515 | has-bigints@^1.0.0: | ||
5516 | version "1.0.1" | ||
5517 | resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" | ||
5518 | integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== | ||
5519 | |||
5500 | has-cors@1.1.0: | 5520 | has-cors@1.1.0: |
5501 | version "1.1.0" | 5521 | version "1.1.0" |
5502 | resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" | 5522 | resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" |
@@ -5512,10 +5532,10 @@ has-flag@^4.0.0: | |||
5512 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" | 5532 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" |
5513 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== | 5533 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== |
5514 | 5534 | ||
5515 | has-symbols@^1.0.1: | 5535 | has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2: |
5516 | version "1.0.1" | 5536 | version "1.0.2" |
5517 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" | 5537 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" |
5518 | integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== | 5538 | integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== |
5519 | 5539 | ||
5520 | has-unicode@^2.0.0: | 5540 | has-unicode@^2.0.0: |
5521 | version "2.0.1" | 5541 | version "2.0.1" |
@@ -5616,6 +5636,13 @@ hosted-git-info@^3.0.6: | |||
5616 | dependencies: | 5636 | dependencies: |
5617 | lru-cache "^6.0.0" | 5637 | lru-cache "^6.0.0" |
5618 | 5638 | ||
5639 | hosted-git-info@^4.0.1: | ||
5640 | version "4.0.1" | ||
5641 | resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.1.tgz#710ef5452ea429a844abc33c981056e7371edab7" | ||
5642 | integrity sha512-eT7NrxAsppPRQEBSwKSosReE+v8OzABwEScQYk5d4uxaEPlzxTIku7LINXtBGalthkLhJnq5lBI89PfK43zAKg== | ||
5643 | dependencies: | ||
5644 | lru-cache "^6.0.0" | ||
5645 | |||
5619 | hpack.js@^2.1.6: | 5646 | hpack.js@^2.1.6: |
5620 | version "2.1.6" | 5647 | version "2.1.6" |
5621 | resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" | 5648 | resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" |
@@ -5712,9 +5739,9 @@ htmlparser2@^4.1.0: | |||
5712 | entities "^2.0.0" | 5739 | entities "^2.0.0" |
5713 | 5740 | ||
5714 | htmlparser2@^6.0.0: | 5741 | htmlparser2@^6.0.0: |
5715 | version "6.0.0" | 5742 | version "6.0.1" |
5716 | resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01" | 5743 | resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.1.tgz#422521231ef6d42e56bd411da8ba40aa36e91446" |
5717 | integrity sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw== | 5744 | integrity sha512-GDKPd+vk4jvSuvCbyuzx/unmXkk090Azec7LovXP8as1Hn8q9p3hbjmDGbUqqhknw0ajwit6LiiWqfiTUPMK7w== |
5718 | dependencies: | 5745 | dependencies: |
5719 | domelementtype "^2.0.1" | 5746 | domelementtype "^2.0.1" |
5720 | domhandler "^4.0.0" | 5747 | domhandler "^4.0.0" |
@@ -6133,6 +6160,11 @@ is-ascii@^1.0.0: | |||
6133 | resolved "https://registry.yarnpkg.com/is-ascii/-/is-ascii-1.0.0.tgz#f02ad0259a0921cd199ff21ce1b09e0f6b4e3929" | 6160 | resolved "https://registry.yarnpkg.com/is-ascii/-/is-ascii-1.0.0.tgz#f02ad0259a0921cd199ff21ce1b09e0f6b4e3929" |
6134 | integrity sha1-8CrQJZoJIc0Zn/Ic4bCeD2tOOSk= | 6161 | integrity sha1-8CrQJZoJIc0Zn/Ic4bCeD2tOOSk= |
6135 | 6162 | ||
6163 | is-bigint@^1.0.1: | ||
6164 | version "1.0.1" | ||
6165 | resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.1.tgz#6923051dfcbc764278540b9ce0e6b3213aa5ebc2" | ||
6166 | integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg== | ||
6167 | |||
6136 | is-binary-path@^1.0.0: | 6168 | is-binary-path@^1.0.0: |
6137 | version "1.0.1" | 6169 | version "1.0.1" |
6138 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" | 6170 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" |
@@ -6147,12 +6179,19 @@ is-binary-path@~2.1.0: | |||
6147 | dependencies: | 6179 | dependencies: |
6148 | binary-extensions "^2.0.0" | 6180 | binary-extensions "^2.0.0" |
6149 | 6181 | ||
6182 | is-boolean-object@^1.1.0: | ||
6183 | version "1.1.0" | ||
6184 | resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0" | ||
6185 | integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA== | ||
6186 | dependencies: | ||
6187 | call-bind "^1.0.0" | ||
6188 | |||
6150 | is-buffer@^1.1.5: | 6189 | is-buffer@^1.1.5: |
6151 | version "1.1.6" | 6190 | version "1.1.6" |
6152 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" | 6191 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" |
6153 | integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== | 6192 | integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== |
6154 | 6193 | ||
6155 | is-callable@^1.1.4, is-callable@^1.2.2: | 6194 | is-callable@^1.1.4, is-callable@^1.2.3: |
6156 | version "1.2.3" | 6195 | version "1.2.3" |
6157 | resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" | 6196 | resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" |
6158 | integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== | 6197 | integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== |
@@ -6312,6 +6351,11 @@ is-negative-zero@^2.0.1: | |||
6312 | resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" | 6351 | resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" |
6313 | integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== | 6352 | integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== |
6314 | 6353 | ||
6354 | is-number-object@^1.0.4: | ||
6355 | version "1.0.4" | ||
6356 | resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" | ||
6357 | integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== | ||
6358 | |||
6315 | is-number@^3.0.0: | 6359 | is-number@^3.0.0: |
6316 | version "3.0.0" | 6360 | version "3.0.0" |
6317 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" | 6361 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" |
@@ -6384,7 +6428,7 @@ is-property@^1.0.0, is-property@^1.0.2: | |||
6384 | resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" | 6428 | resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" |
6385 | integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= | 6429 | integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= |
6386 | 6430 | ||
6387 | is-regex@^1.0.4, is-regex@^1.1.1: | 6431 | is-regex@^1.0.4, is-regex@^1.1.2: |
6388 | version "1.1.2" | 6432 | version "1.1.2" |
6389 | resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" | 6433 | resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" |
6390 | integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== | 6434 | integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== |
@@ -6407,6 +6451,11 @@ is-stream@^2.0.0: | |||
6407 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" | 6451 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" |
6408 | integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== | 6452 | integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== |
6409 | 6453 | ||
6454 | is-string@^1.0.5: | ||
6455 | version "1.0.5" | ||
6456 | resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" | ||
6457 | integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== | ||
6458 | |||
6410 | is-svg@^3.0.0: | 6459 | is-svg@^3.0.0: |
6411 | version "3.0.0" | 6460 | version "3.0.0" |
6412 | resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" | 6461 | resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" |
@@ -6414,7 +6463,7 @@ is-svg@^3.0.0: | |||
6414 | dependencies: | 6463 | dependencies: |
6415 | html-comment-regex "^1.1.0" | 6464 | html-comment-regex "^1.1.0" |
6416 | 6465 | ||
6417 | is-symbol@^1.0.2: | 6466 | is-symbol@^1.0.2, is-symbol@^1.0.3: |
6418 | version "1.0.3" | 6467 | version "1.0.3" |
6419 | resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" | 6468 | resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" |
6420 | integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== | 6469 | integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== |
@@ -6426,6 +6475,11 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: | |||
6426 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" | 6475 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" |
6427 | integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= | 6476 | integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= |
6428 | 6477 | ||
6478 | is-unicode-supported@^0.1.0: | ||
6479 | version "0.1.0" | ||
6480 | resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" | ||
6481 | integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== | ||
6482 | |||
6429 | is-what@^3.12.0: | 6483 | is-what@^3.12.0: |
6430 | version "3.14.1" | 6484 | version "3.14.1" |
6431 | resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" | 6485 | resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" |
@@ -6538,16 +6592,21 @@ istanbul-reports@^3.0.2: | |||
6538 | html-escaper "^2.0.0" | 6592 | html-escaper "^2.0.0" |
6539 | istanbul-lib-report "^3.0.0" | 6593 | istanbul-lib-report "^3.0.0" |
6540 | 6594 | ||
6541 | jasmine-core@^3.6.0, jasmine-core@~3.6.0: | 6595 | jasmine-core@^3.6.0: |
6542 | version "3.6.0" | 6596 | version "3.7.1" |
6543 | resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.6.0.tgz#491f3bb23941799c353ceb7a45b38a950ebc5a20" | 6597 | resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.7.1.tgz#0401327f6249eac993d47bbfa18d4e8efacfb561" |
6544 | integrity sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw== | 6598 | integrity sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ== |
6545 | 6599 | ||
6546 | jasmine-core@~2.8.0: | 6600 | jasmine-core@~2.8.0: |
6547 | version "2.8.0" | 6601 | version "2.8.0" |
6548 | resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" | 6602 | resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" |
6549 | integrity sha1-vMl5rh+f0FcB5F5S5l06XWPxok4= | 6603 | integrity sha1-vMl5rh+f0FcB5F5S5l06XWPxok4= |
6550 | 6604 | ||
6605 | jasmine-core@~3.6.0: | ||
6606 | version "3.6.0" | ||
6607 | resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.6.0.tgz#491f3bb23941799c353ceb7a45b38a950ebc5a20" | ||
6608 | integrity sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw== | ||
6609 | |||
6551 | jasmine-spec-reporter@~6.0.0: | 6610 | jasmine-spec-reporter@~6.0.0: |
6552 | version "6.0.0" | 6611 | version "6.0.0" |
6553 | resolved "https://registry.yarnpkg.com/jasmine-spec-reporter/-/jasmine-spec-reporter-6.0.0.tgz#3b9c85689676a351f343ba8dd6d3957f11a4bf1d" | 6612 | resolved "https://registry.yarnpkg.com/jasmine-spec-reporter/-/jasmine-spec-reporter-6.0.0.tgz#3b9c85689676a351f343ba8dd6d3957f11a4bf1d" |
@@ -6785,9 +6844,9 @@ karma-source-map-support@1.4.0: | |||
6785 | source-map-support "^0.5.5" | 6844 | source-map-support "^0.5.5" |
6786 | 6845 | ||
6787 | karma@~6.1.0: | 6846 | karma@~6.1.0: |
6788 | version "6.1.1" | 6847 | version "6.1.2" |
6789 | resolved "https://registry.yarnpkg.com/karma/-/karma-6.1.1.tgz#a7539618cca0f2cbb26d5497120ec31fe340c2a1" | 6848 | resolved "https://registry.yarnpkg.com/karma/-/karma-6.1.2.tgz#9d7394559f5deb150b3021c1860960281c3a0e50" |
6790 | integrity sha512-vVDFxFGAsclgmFjZA/qGw5xqWdZIWxVD7xLyCukYUYd5xs/uGzYbXGOT5zOruVBQleKEmXIr4H2hzGCTn+M9Cg== | 6849 | integrity sha512-mKbxgsJrt3UHBPdKfCxC2eg3lpqyt6hQRFhNWJ2sk0wUnbnLPEiCpgIgiycuLSra0vC6TaK9OPJiMGATGzgH/A== |
6791 | dependencies: | 6850 | dependencies: |
6792 | body-parser "^1.19.0" | 6851 | body-parser "^1.19.0" |
6793 | braces "^3.0.2" | 6852 | braces "^3.0.2" |
@@ -7042,11 +7101,12 @@ lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.1 | |||
7042 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== | 7101 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== |
7043 | 7102 | ||
7044 | log-symbols@^4.0.0: | 7103 | log-symbols@^4.0.0: |
7045 | version "4.0.0" | 7104 | version "4.1.0" |
7046 | resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" | 7105 | resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" |
7047 | integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== | 7106 | integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== |
7048 | dependencies: | 7107 | dependencies: |
7049 | chalk "^4.0.0" | 7108 | chalk "^4.1.0" |
7109 | is-unicode-supported "^0.1.0" | ||
7050 | 7110 | ||
7051 | log4js@^6.2.1: | 7111 | log4js@^6.2.1: |
7052 | version "6.3.0" | 7112 | version "6.3.0" |
@@ -7110,9 +7170,9 @@ m3u8-parser@4.5.0: | |||
7110 | global "^4.3.2" | 7170 | global "^4.3.2" |
7111 | 7171 | ||
7112 | m3u8-parser@^4.4.0: | 7172 | m3u8-parser@^4.4.0: |
7113 | version "4.5.2" | 7173 | version "4.6.0" |
7114 | resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.5.2.tgz#f7d48a60112466e528324624c4e66d52ed341a75" | 7174 | resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.6.0.tgz#a0e2f5dcf8391c9a6e59895a084fa38f27b52124" |
7115 | integrity sha512-sN/lu3TiRxmG2RFjZxo5c0/7Dr4RrEztl43jXrWwj5gFZ7vfa2iIxGfiPx485dm5QCazaIcKk+vNkUso8Aq0Ag== | 7175 | integrity sha512-dKhhpMcPqDM/KzULVrNyDZ/z766peQjwUghDTcl6TE7DQKAt/vm74/IMUAxpO34f6LDpM+OH/dYGQwW1eM4yWw== |
7116 | dependencies: | 7176 | dependencies: |
7117 | "@babel/runtime" "^7.12.5" | 7177 | "@babel/runtime" "^7.12.5" |
7118 | "@videojs/vhs-utils" "^3.0.0" | 7178 | "@videojs/vhs-utils" "^3.0.0" |
@@ -7381,9 +7441,9 @@ mini-css-extract-plugin@1.3.5: | |||
7381 | webpack-sources "^1.1.0" | 7441 | webpack-sources "^1.1.0" |
7382 | 7442 | ||
7383 | mini-css-extract-plugin@^1.3.1: | 7443 | mini-css-extract-plugin@^1.3.1: |
7384 | version "1.3.8" | 7444 | version "1.3.9" |
7385 | resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.8.tgz#639047b78c2ee728704285aa468d2a5a8d91d566" | 7445 | resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.9.tgz#47a32132b0fd97a119acd530e8421e8f6ab16d5e" |
7386 | integrity sha512-u+2kVov/Gcs74iz+x3phEBWMAGw2djjnKfYez+Pl/b5dyXL7aM4Lp5QQtIq16CDwRHT/woUJki49gBNMhfm1eA== | 7446 | integrity sha512-Ac4s+xhVbqlyhXS5J/Vh/QXUz3ycXlCqoCPpg0vdfhsIBH9eg/It/9L1r1XhSCH737M1lqcWnMuWL13zcygn5A== |
7387 | dependencies: | 7447 | dependencies: |
7388 | loader-utils "^2.0.0" | 7448 | loader-utils "^2.0.0" |
7389 | schema-utils "^3.0.0" | 7449 | schema-utils "^3.0.0" |
@@ -7630,9 +7690,9 @@ nan@^2.12.1: | |||
7630 | integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== | 7690 | integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== |
7631 | 7691 | ||
7632 | nanoid@^3.1.20: | 7692 | nanoid@^3.1.20: |
7633 | version "3.1.20" | 7693 | version "3.1.22" |
7634 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" | 7694 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844" |
7635 | integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== | 7695 | integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ== |
7636 | 7696 | ||
7637 | nanomatch@^1.2.9: | 7697 | nanomatch@^1.2.9: |
7638 | version "1.2.13" | 7698 | version "1.2.13" |
@@ -7830,13 +7890,13 @@ npm-package-arg@8.1.0: | |||
7830 | semver "^7.0.0" | 7890 | semver "^7.0.0" |
7831 | validate-npm-package-name "^3.0.0" | 7891 | validate-npm-package-name "^3.0.0" |
7832 | 7892 | ||
7833 | npm-package-arg@^8.0.0, npm-package-arg@^8.0.1: | 7893 | npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.2: |
7834 | version "8.1.1" | 7894 | version "8.1.2" |
7835 | resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.1.tgz#00ebf16ac395c63318e67ce66780a06db6df1b04" | 7895 | resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.2.tgz#b868016ae7de5619e729993fbd8d11dc3c52ab62" |
7836 | integrity sha512-CsP95FhWQDwNqiYS+Q0mZ7FAEDytDZAkNxQqea6IaAFJTAY9Lhhqyl0irU/6PMc7BGfUmnsbHcqxJD7XuVM/rg== | 7896 | integrity sha512-6Eem455JsSMJY6Kpd3EyWE+n5hC+g9bSyHr9K9U2zqZb7+02+hObQ2c0+8iDk/mNF+8r1MhY44WypKJAkySIYA== |
7837 | dependencies: | 7897 | dependencies: |
7838 | hosted-git-info "^3.0.6" | 7898 | hosted-git-info "^4.0.1" |
7839 | semver "^7.0.0" | 7899 | semver "^7.3.4" |
7840 | validate-npm-package-name "^3.0.0" | 7900 | validate-npm-package-name "^3.0.0" |
7841 | 7901 | ||
7842 | npm-packlist@^2.1.4: | 7902 | npm-packlist@^2.1.4: |
@@ -7849,7 +7909,7 @@ npm-packlist@^2.1.4: | |||
7849 | npm-bundled "^1.1.1" | 7909 | npm-bundled "^1.1.1" |
7850 | npm-normalize-package-bin "^1.0.1" | 7910 | npm-normalize-package-bin "^1.0.1" |
7851 | 7911 | ||
7852 | npm-pick-manifest@6.1.0, npm-pick-manifest@^6.0.0: | 7912 | npm-pick-manifest@6.1.0: |
7853 | version "6.1.0" | 7913 | version "6.1.0" |
7854 | resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz#2befed87b0fce956790f62d32afb56d7539c022a" | 7914 | resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz#2befed87b0fce956790f62d32afb56d7539c022a" |
7855 | integrity sha512-ygs4k6f54ZxJXrzT0x34NybRlLeZ4+6nECAIbr2i0foTnijtS1TJiyzpqtuUAJOps/hO0tNDr8fRV5g+BtRlTw== | 7915 | integrity sha512-ygs4k6f54ZxJXrzT0x34NybRlLeZ4+6nECAIbr2i0foTnijtS1TJiyzpqtuUAJOps/hO0tNDr8fRV5g+BtRlTw== |
@@ -7858,6 +7918,16 @@ npm-pick-manifest@6.1.0, npm-pick-manifest@^6.0.0: | |||
7858 | npm-package-arg "^8.0.0" | 7918 | npm-package-arg "^8.0.0" |
7859 | semver "^7.0.0" | 7919 | semver "^7.0.0" |
7860 | 7920 | ||
7921 | npm-pick-manifest@^6.0.0: | ||
7922 | version "6.1.1" | ||
7923 | resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz#7b5484ca2c908565f43b7f27644f36bb816f5148" | ||
7924 | integrity sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA== | ||
7925 | dependencies: | ||
7926 | npm-install-checks "^4.0.0" | ||
7927 | npm-normalize-package-bin "^1.0.1" | ||
7928 | npm-package-arg "^8.1.2" | ||
7929 | semver "^7.3.4" | ||
7930 | |||
7861 | npm-registry-fetch@^9.0.0: | 7931 | npm-registry-fetch@^9.0.0: |
7862 | version "9.0.0" | 7932 | version "9.0.0" |
7863 | resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-9.0.0.tgz#86f3feb4ce00313bc0b8f1f8f69daae6face1661" | 7933 | resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-9.0.0.tgz#86f3feb4ce00313bc0b8f1f8f69daae6face1661" |
@@ -7927,7 +7997,7 @@ object-copy@^0.1.0: | |||
7927 | define-property "^0.2.5" | 7997 | define-property "^0.2.5" |
7928 | kind-of "^3.0.3" | 7998 | kind-of "^3.0.3" |
7929 | 7999 | ||
7930 | object-inspect@^1.8.0, object-inspect@^1.9.0: | 8000 | object-inspect@^1.9.0: |
7931 | version "1.9.0" | 8001 | version "1.9.0" |
7932 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" | 8002 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" |
7933 | integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== | 8003 | integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== |
@@ -7952,7 +8022,7 @@ object-visit@^1.0.0: | |||
7952 | dependencies: | 8022 | dependencies: |
7953 | isobject "^3.0.0" | 8023 | isobject "^3.0.0" |
7954 | 8024 | ||
7955 | object.assign@^4.1.0, object.assign@^4.1.1, object.assign@^4.1.2: | 8025 | object.assign@^4.1.0, object.assign@^4.1.2: |
7956 | version "4.1.2" | 8026 | version "4.1.2" |
7957 | resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" | 8027 | resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" |
7958 | integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== | 8028 | integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== |
@@ -8896,12 +8966,12 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.27: | |||
8896 | source-map "^0.6.1" | 8966 | source-map "^0.6.1" |
8897 | supports-color "^6.1.0" | 8967 | supports-color "^6.1.0" |
8898 | 8968 | ||
8899 | postcss@^8.0.2, postcss@^8.1.4, postcss@^8.2.4: | 8969 | postcss@^8.0.2, postcss@^8.1.4, postcss@^8.2.8: |
8900 | version "8.2.6" | 8970 | version "8.2.8" |
8901 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.6.tgz#5d69a974543b45f87e464bc4c3e392a97d6be9fe" | 8971 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.8.tgz#0b90f9382efda424c4f0f69a2ead6f6830d08ece" |
8902 | integrity sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg== | 8972 | integrity sha512-1F0Xb2T21xET7oQV9eKuctbM9S7BC0fetoHCc4H13z0PT6haiRLP4T0ZY4XWh7iLP0usgqykT6p9B2RtOf4FPw== |
8903 | dependencies: | 8973 | dependencies: |
8904 | colorette "^1.2.1" | 8974 | colorette "^1.2.2" |
8905 | nanoid "^3.1.20" | 8975 | nanoid "^3.1.20" |
8906 | source-map "^0.6.1" | 8976 | source-map "^0.6.1" |
8907 | 8977 | ||
@@ -8924,9 +8994,9 @@ pretty-error@^2.1.1: | |||
8924 | renderkid "^2.0.4" | 8994 | renderkid "^2.0.4" |
8925 | 8995 | ||
8926 | primeng@^11.0.0-rc.1: | 8996 | primeng@^11.0.0-rc.1: |
8927 | version "11.2.3" | 8997 | version "11.3.1" |
8928 | resolved "https://registry.yarnpkg.com/primeng/-/primeng-11.2.3.tgz#66e3d817fe27c9a7703726537c03ddcc1998bb44" | 8998 | resolved "https://registry.yarnpkg.com/primeng/-/primeng-11.3.1.tgz#644dd59d1f0808227a9529ea6ffaad31bdb5e5df" |
8929 | integrity sha512-8elRAGal8a+qXJ4egRKXU+bUvIyfCxsiCerXgOPbwbo/TU/DBK7WBXGGGi6KJOamFqClAqj/FO3WLAdofKQSRQ== | 8999 | integrity sha512-B86/su/3sNP2GfhyegvZh2MpHcUZHas+13bPL98QmZhoiPBQp2jz3H0iD716+piC00Wee6pi/PPm7e9y9qxGDg== |
8930 | dependencies: | 9000 | dependencies: |
8931 | tslib "^2.0.0" | 9001 | tslib "^2.0.0" |
8932 | 9002 | ||
@@ -9027,11 +9097,6 @@ public-encrypt@^4.0.0: | |||
9027 | randombytes "^2.0.1" | 9097 | randombytes "^2.0.1" |
9028 | safe-buffer "^5.1.2" | 9098 | safe-buffer "^5.1.2" |
9029 | 9099 | ||
9030 | puka@^1.0.1: | ||
9031 | version "1.0.1" | ||
9032 | resolved "https://registry.yarnpkg.com/puka/-/puka-1.0.1.tgz#a2df782b7eb4cf9564e4c93a5da422de0dfacc02" | ||
9033 | integrity sha512-ssjRZxBd7BT3dte1RR3VoeT2cT/ODH8x+h0rUF1rMqB0srHYf48stSDWfiYakTp5UBZMxroZhB2+ExLDHm7W3g== | ||
9034 | |||
9035 | pump@^2.0.0: | 9100 | pump@^2.0.0: |
9036 | version "2.0.1" | 9101 | version "2.0.1" |
9037 | resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" | 9102 | resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" |
@@ -9133,15 +9198,15 @@ querystringify@^2.1.1: | |||
9133 | resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" | 9198 | resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" |
9134 | integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== | 9199 | integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== |
9135 | 9200 | ||
9136 | queue-microtask@^1.1.2, queue-microtask@^1.2.0, queue-microtask@^1.2.2: | 9201 | queue-microtask@^1.2.0, queue-microtask@^1.2.2: |
9137 | version "1.2.2" | 9202 | version "1.2.3" |
9138 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.2.tgz#abf64491e6ecf0f38a6502403d4cda04f372dfd3" | 9203 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" |
9139 | integrity sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg== | 9204 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== |
9140 | 9205 | ||
9141 | random-access-file@^2.0.1: | 9206 | random-access-file@^2.0.1: |
9142 | version "2.1.5" | 9207 | version "2.2.0" |
9143 | resolved "https://registry.yarnpkg.com/random-access-file/-/random-access-file-2.1.5.tgz#27af6115b920a9adabb44559e29ea9944bb35bfe" | 9208 | resolved "https://registry.yarnpkg.com/random-access-file/-/random-access-file-2.2.0.tgz#b49b999efefb374afb7587f219071fec5ce66546" |
9144 | integrity sha512-lqmUGgF9X+LD0XSeWSHcs7U2nSLYp+RQvkDDqKWoxW8jcd13tZ00G6PHV32OZqDIHmS9ewoEUEa6jcvyB7UCvg== | 9209 | integrity sha512-B744003Mj7v3EcuPl9hCiB2Ot4aZjgtU2mV6yFY1THiWU/XfGf1uSadR+SlQdJcwHgAWeG7Lbos0aUqjtj8FQg== |
9145 | dependencies: | 9210 | dependencies: |
9146 | mkdirp-classic "^0.5.2" | 9211 | mkdirp-classic "^0.5.2" |
9147 | random-access-storage "^1.1.1" | 9212 | random-access-storage "^1.1.1" |
@@ -9370,9 +9435,9 @@ regjsgen@^0.5.1: | |||
9370 | integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== | 9435 | integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== |
9371 | 9436 | ||
9372 | regjsparser@^0.6.4: | 9437 | regjsparser@^0.6.4: |
9373 | version "0.6.7" | 9438 | version "0.6.8" |
9374 | resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.7.tgz#c00164e1e6713c2e3ee641f1701c4b7aa0a7f86c" | 9439 | resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.8.tgz#4532c3da36d75d56e3f394ce2ea6842bde7496bd" |
9375 | integrity sha512-ib77G0uxsA2ovgiYbCVGx4Pv3PSttAx2vIwidqQzbL2U5S4Q+j00HdSAneSBuyVcMvEnTXMjiGgB+DlXozVhpQ== | 9440 | integrity sha512-3weFrFQREJhJ2PW+iCGaG6TenyzNSZgsBKZ/oEf6Trme31COSeIWhHw9O6FPkuXktfx+b6Hf/5e6dKPHaROq2g== |
9376 | dependencies: | 9441 | dependencies: |
9377 | jsesc "~0.5.0" | 9442 | jsesc "~0.5.0" |
9378 | 9443 | ||
@@ -9593,9 +9658,9 @@ rework@1.0.1, rework@^1.0.1: | |||
9593 | css "^2.0.0" | 9658 | css "^2.0.0" |
9594 | 9659 | ||
9595 | rfdc@^1.1.4: | 9660 | rfdc@^1.1.4: |
9596 | version "1.2.0" | 9661 | version "1.3.0" |
9597 | resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.2.0.tgz#9e9894258f48f284b43c3143c68070a4f373b949" | 9662 | resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" |
9598 | integrity sha512-ijLyszTMmUrXvjSooucVQwimGUk84eRcmCuLV8Xghe3UO85mjUtRAHRyoMM6XtyqbECaXuBWx18La3523sXINA== | 9663 | integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== |
9599 | 9664 | ||
9600 | rgb-regex@^1.0.1: | 9665 | rgb-regex@^1.0.1: |
9601 | version "1.0.1" | 9666 | version "1.0.1" |
@@ -9681,7 +9746,7 @@ run-series@^1.1.8, run-series@^1.1.9: | |||
9681 | resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.9.tgz#15ba9cb90e6a6c054e67c98e1dc063df0ecc113a" | 9746 | resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.9.tgz#15ba9cb90e6a6c054e67c98e1dc063df0ecc113a" |
9682 | integrity sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g== | 9747 | integrity sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g== |
9683 | 9748 | ||
9684 | rusha@^0.8.1: | 9749 | rusha@^0.8.13: |
9685 | version "0.8.13" | 9750 | version "0.8.13" |
9686 | resolved "https://registry.yarnpkg.com/rusha/-/rusha-0.8.13.tgz#9a084e7b860b17bff3015b92c67a6a336191513a" | 9751 | resolved "https://registry.yarnpkg.com/rusha/-/rusha-0.8.13.tgz#9a084e7b860b17bff3015b92c67a6a336191513a" |
9687 | integrity sha1-mghOe4YLF7/zAVuSxnpqM2GRUTo= | 9752 | integrity sha1-mghOe4YLF7/zAVuSxnpqM2GRUTo= |
@@ -9742,9 +9807,9 @@ safe-regex@^1.1.0: | |||
9742 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== | 9807 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== |
9743 | 9808 | ||
9744 | sanitize-html@^2.1.2: | 9809 | sanitize-html@^2.1.2: |
9745 | version "2.3.2" | 9810 | version "2.3.3" |
9746 | resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.3.2.tgz#a1954aea877a096c408aca7b0c260bef6e4fc402" | 9811 | resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.3.3.tgz#3db382c9a621cce4c46d90f10c64f1e9da9e8353" |
9747 | integrity sha512-p7neuskvC8pSurUjdVmbWPXmc9A4+QpOXIL+4gwFC+av5h+lYCXFT8uEneqsFQg/wEA1IH+cKQA60AaQI6p3cg== | 9812 | integrity sha512-DCFXPt7Di0c6JUnlT90eIgrjs6TsJl/8HYU3KLdmrVclFN4O0heTcVbJiMa23OKVr6aR051XYtsgd8EWwEBwUA== |
9748 | dependencies: | 9813 | dependencies: |
9749 | deepmerge "^4.2.2" | 9814 | deepmerge "^4.2.2" |
9750 | escape-string-regexp "^4.0.0" | 9815 | escape-string-regexp "^4.0.0" |
@@ -9894,7 +9959,7 @@ semver@7.0.0: | |||
9894 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" | 9959 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" |
9895 | integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== | 9960 | integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== |
9896 | 9961 | ||
9897 | semver@7.3.4, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4: | 9962 | semver@7.3.4: |
9898 | version "7.3.4" | 9963 | version "7.3.4" |
9899 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" | 9964 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" |
9900 | integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== | 9965 | integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== |
@@ -9906,6 +9971,13 @@ semver@^6.0.0, semver@^6.3.0: | |||
9906 | resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" | 9971 | resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" |
9907 | integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== | 9972 | integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== |
9908 | 9973 | ||
9974 | semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4: | ||
9975 | version "7.3.5" | ||
9976 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" | ||
9977 | integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== | ||
9978 | dependencies: | ||
9979 | lru-cache "^6.0.0" | ||
9980 | |||
9909 | send@0.17.1: | 9981 | send@0.17.1: |
9910 | version "0.17.1" | 9982 | version "0.17.1" |
9911 | resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" | 9983 | resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" |
@@ -10061,9 +10133,9 @@ simple-get@^4.0.0: | |||
10061 | simple-concat "^1.0.0" | 10133 | simple-concat "^1.0.0" |
10062 | 10134 | ||
10063 | simple-peer@^9.5.0, simple-peer@^9.7.1, simple-peer@^9.9.3: | 10135 | simple-peer@^9.5.0, simple-peer@^9.7.1, simple-peer@^9.9.3: |
10064 | version "9.9.3" | 10136 | version "9.10.0" |
10065 | resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.9.3.tgz#b52c39d1173620d06c8b29ada7ee2ad3384bb469" | 10137 | resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.10.0.tgz#f458444300f635e6fcc2f5a5166c45d71eafb57f" |
10066 | integrity sha512-T3wuv0UqBpDTV0x0pJPPsz4thy0tC0fTOHE4g9+AF43RUxxT+MWeXVtdQcK5Xuzv/XTVrB2NrGzdfO1IFBqOkw== | 10138 | integrity sha512-sKrKtca1UdmwdZIbvuT3iEL05tDGt/xdLP6+ej8rh1ADgtDk44yLaEZjIyPJ6c34zsSih46Ou7zUIT7e4hPK7g== |
10067 | dependencies: | 10139 | dependencies: |
10068 | buffer "^6.0.2" | 10140 | buffer "^6.0.2" |
10069 | debug "^4.2.0" | 10141 | debug "^4.2.0" |
@@ -10074,12 +10146,12 @@ simple-peer@^9.5.0, simple-peer@^9.7.1, simple-peer@^9.9.3: | |||
10074 | readable-stream "^3.6.0" | 10146 | readable-stream "^3.6.0" |
10075 | 10147 | ||
10076 | simple-sha1@^3.0.0, simple-sha1@^3.0.1: | 10148 | simple-sha1@^3.0.0, simple-sha1@^3.0.1: |
10077 | version "3.0.1" | 10149 | version "3.1.0" |
10078 | resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-3.0.1.tgz#b34c3c978d74ac4baf99b6555c1e6736e0d6e700" | 10150 | resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-3.1.0.tgz#40cac8436dfaf9924332fc46a5c7bca45f656131" |
10079 | integrity sha512-q7ehqWfHc1VhOm7sW099YDZ4I0yYX7rqyhqqhHV1IYeUTjPOhHyD3mXvv8k2P+rO7+7c8R4/D+8ffzC9BE7Cqg== | 10151 | integrity sha512-ArTptMRC1v08H8ihPD6l0wesKvMfF9e8XL5rIHPanI7kGOsSsbY514MwVu6X1PITHCTB2F08zB7cyEbfc4wQjg== |
10080 | dependencies: | 10152 | dependencies: |
10081 | queue-microtask "^1.1.2" | 10153 | queue-microtask "^1.2.2" |
10082 | rusha "^0.8.1" | 10154 | rusha "^0.8.13" |
10083 | 10155 | ||
10084 | simple-swizzle@^0.2.2: | 10156 | simple-swizzle@^0.2.2: |
10085 | version "0.2.2" | 10157 | version "0.2.2" |
@@ -10159,9 +10231,9 @@ socket.io-adapter@~2.1.0: | |||
10159 | integrity sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg== | 10231 | integrity sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg== |
10160 | 10232 | ||
10161 | socket.io-client@^3.0.3: | 10233 | socket.io-client@^3.0.3: |
10162 | version "3.1.1" | 10234 | version "3.1.3" |
10163 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-3.1.1.tgz#43dfc3feddbb675b274a724f685d6b6af319b3e3" | 10235 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-3.1.3.tgz#57ddcefea58cfab71f0e94c21124de8e3c5aa3e2" |
10164 | integrity sha512-BLgIuCjI7Sf3mDHunKddX9zKR/pbkP7IACM3sJS3jha+zJ6/pGKRV6Fz5XSBHCfUs9YzT8kYIqNwOOuFNLtnYA== | 10236 | integrity sha512-4sIGOGOmCg3AOgGi7EEr6ZkTZRkrXwub70bBB/F0JSkMOUFpA77WsL87o34DffQQ31PkbMUIadGOk+3tx1KGbw== |
10165 | dependencies: | 10237 | dependencies: |
10166 | "@types/component-emitter" "^1.2.10" | 10238 | "@types/component-emitter" "^1.2.10" |
10167 | backo2 "~1.0.2" | 10239 | backo2 "~1.0.2" |
@@ -10181,13 +10253,13 @@ socket.io-parser@~4.0.3, socket.io-parser@~4.0.4: | |||
10181 | debug "~4.3.1" | 10253 | debug "~4.3.1" |
10182 | 10254 | ||
10183 | socket.io@^3.1.0: | 10255 | socket.io@^3.1.0: |
10184 | version "3.1.1" | 10256 | version "3.1.2" |
10185 | resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-3.1.1.tgz#905e3d4a3b37d8e7970e67a4a6eb81110a5778ba" | 10257 | resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-3.1.2.tgz#06e27caa1c4fc9617547acfbb5da9bc1747da39a" |
10186 | integrity sha512-7cBWdsDC7bbyEF6WbBqffjizc/H4YF1wLdZoOzuYfo2uMNSFjJKuQ36t0H40o9B20DO6p+mSytEd92oP4S15bA== | 10258 | integrity sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw== |
10187 | dependencies: | 10259 | dependencies: |
10188 | "@types/cookie" "^0.4.0" | 10260 | "@types/cookie" "^0.4.0" |
10189 | "@types/cors" "^2.8.8" | 10261 | "@types/cors" "^2.8.8" |
10190 | "@types/node" "^14.14.10" | 10262 | "@types/node" ">=10.0.0" |
10191 | accepts "~1.3.4" | 10263 | accepts "~1.3.4" |
10192 | base64id "~2.0.0" | 10264 | base64id "~2.0.0" |
10193 | debug "~4.3.1" | 10265 | debug "~4.3.1" |
@@ -10226,9 +10298,9 @@ socks-proxy-agent@^5.0.0: | |||
10226 | socks "^2.3.3" | 10298 | socks "^2.3.3" |
10227 | 10299 | ||
10228 | socks@^2.3.3: | 10300 | socks@^2.3.3: |
10229 | version "2.5.1" | 10301 | version "2.6.0" |
10230 | resolved "https://registry.yarnpkg.com/socks/-/socks-2.5.1.tgz#7720640b6b5ec9a07d556419203baa3f0596df5f" | 10302 | resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.0.tgz#6b984928461d39871b3666754b9000ecf39dfac2" |
10231 | integrity sha512-oZCsJJxapULAYJaEYBSzMcz8m3jqgGrHaGhkmU/o/PQfFWYWxkAaA0UMGImb6s6tEXfKi959X6VJjMMQ3P6TTQ== | 10303 | integrity sha512-mNmr9owlinMplev0Wd7UHFlqI4ofnBnNzFuzrm63PPaHgbkqCFe4T5LzwKmtQ/f2tX0NTpcdVLyD/FHxFBstYw== |
10232 | dependencies: | 10304 | dependencies: |
10233 | ip "^1.1.5" | 10305 | ip "^1.1.5" |
10234 | smart-buffer "^4.1.0" | 10306 | smart-buffer "^4.1.0" |
@@ -10416,7 +10488,7 @@ ssri@^6.0.1: | |||
10416 | dependencies: | 10488 | dependencies: |
10417 | figgy-pudding "^3.5.1" | 10489 | figgy-pudding "^3.5.1" |
10418 | 10490 | ||
10419 | ssri@^8.0.0: | 10491 | ssri@^8.0.0, ssri@^8.0.1: |
10420 | version "8.0.1" | 10492 | version "8.0.1" |
10421 | resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" | 10493 | resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" |
10422 | integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== | 10494 | integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== |
@@ -10546,15 +10618,15 @@ string-width@^3.0.0, string-width@^3.1.0: | |||
10546 | strip-ansi "^5.1.0" | 10618 | strip-ansi "^5.1.0" |
10547 | 10619 | ||
10548 | string-width@^4.1.0, string-width@^4.2.0: | 10620 | string-width@^4.1.0, string-width@^4.2.0: |
10549 | version "4.2.0" | 10621 | version "4.2.2" |
10550 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" | 10622 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" |
10551 | integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== | 10623 | integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== |
10552 | dependencies: | 10624 | dependencies: |
10553 | emoji-regex "^8.0.0" | 10625 | emoji-regex "^8.0.0" |
10554 | is-fullwidth-code-point "^3.0.0" | 10626 | is-fullwidth-code-point "^3.0.0" |
10555 | strip-ansi "^6.0.0" | 10627 | strip-ansi "^6.0.0" |
10556 | 10628 | ||
10557 | string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.3: | 10629 | string.prototype.trimend@^1.0.4: |
10558 | version "1.0.4" | 10630 | version "1.0.4" |
10559 | resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" | 10631 | resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" |
10560 | integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== | 10632 | integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== |
@@ -10562,7 +10634,7 @@ string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.3: | |||
10562 | call-bind "^1.0.2" | 10634 | call-bind "^1.0.2" |
10563 | define-properties "^1.1.3" | 10635 | define-properties "^1.1.3" |
10564 | 10636 | ||
10565 | string.prototype.trimstart@^1.0.1, string.prototype.trimstart@^1.0.3: | 10637 | string.prototype.trimstart@^1.0.4: |
10566 | version "1.0.4" | 10638 | version "1.0.4" |
10567 | resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" | 10639 | resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" |
10568 | integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== | 10640 | integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== |
@@ -10815,9 +10887,9 @@ terser@^4.1.2, terser@^4.6.3: | |||
10815 | source-map-support "~0.5.12" | 10887 | source-map-support "~0.5.12" |
10816 | 10888 | ||
10817 | terser@^5.3.4: | 10889 | terser@^5.3.4: |
10818 | version "5.6.0" | 10890 | version "5.6.1" |
10819 | resolved "https://registry.yarnpkg.com/terser/-/terser-5.6.0.tgz#138cdf21c5e3100b1b3ddfddf720962f88badcd2" | 10891 | resolved "https://registry.yarnpkg.com/terser/-/terser-5.6.1.tgz#a48eeac5300c0a09b36854bf90d9c26fb201973c" |
10820 | integrity sha512-vyqLMoqadC1uR0vywqOZzriDYzgEkNJFK4q9GeyOBHIbiECHiWLKcWfbQWAUaPfxkjDhapSlZB9f7fkMrvkVjA== | 10892 | integrity sha512-yv9YLFQQ+3ZqgWCUk+pvNJwgUTdlIxUk1WTN+RnaFJe2L7ipG2csPT0ra2XRm7Cs8cxN7QXmK1rFzEwYEQkzXw== |
10821 | dependencies: | 10893 | dependencies: |
10822 | commander "^2.20.0" | 10894 | commander "^2.20.0" |
10823 | source-map "~0.7.2" | 10895 | source-map "~0.7.2" |
@@ -10976,9 +11048,9 @@ tree-kill@1.2.2: | |||
10976 | integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== | 11048 | integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== |
10977 | 11049 | ||
10978 | ts-loader@^8.0.14: | 11050 | ts-loader@^8.0.14: |
10979 | version "8.0.17" | 11051 | version "8.0.18" |
10980 | resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.0.17.tgz#98f2ccff9130074f4079fd89b946b4c637b1f2fc" | 11052 | resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.0.18.tgz#b2385cbe81c34ad9f997915129cdde3ad92a61ea" |
10981 | integrity sha512-OeVfSshx6ot/TCxRwpBHQ/4lRzfgyTkvi7ghDVrLXOHzTbSK413ROgu/xNqM72i3AFeAIJgQy78FwSMKmOW68w== | 11053 | integrity sha512-hRZzkydPX30XkLaQwJTDcWDoxZHK6IrEMDQpNd7tgcakFruFkeUp/aY+9hBb7BUGb+ZWKI0jiOGMo0MckwzdDQ== |
10982 | dependencies: | 11054 | dependencies: |
10983 | chalk "^4.1.0" | 11055 | chalk "^4.1.0" |
10984 | enhanced-resolve "^4.0.0" | 11056 | enhanced-resolve "^4.0.0" |
@@ -11054,9 +11126,9 @@ tsutils@^2.29.0: | |||
11054 | tslib "^1.8.1" | 11126 | tslib "^1.8.1" |
11055 | 11127 | ||
11056 | tsutils@^3.0.0: | 11128 | tsutils@^3.0.0: |
11057 | version "3.20.0" | 11129 | version "3.21.0" |
11058 | resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.20.0.tgz#ea03ea45462e146b53d70ce0893de453ff24f698" | 11130 | resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" |
11059 | integrity sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg== | 11131 | integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== |
11060 | dependencies: | 11132 | dependencies: |
11061 | tslib "^1.8.1" | 11133 | tslib "^1.8.1" |
11062 | 11134 | ||
@@ -11103,9 +11175,9 @@ type@^1.0.1: | |||
11103 | integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== | 11175 | integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== |
11104 | 11176 | ||
11105 | type@^2.0.0: | 11177 | type@^2.0.0: |
11106 | version "2.3.0" | 11178 | version "2.5.0" |
11107 | resolved "https://registry.yarnpkg.com/type/-/type-2.3.0.tgz#ada7c045f07ead08abf9e2edd29be1a0c0661132" | 11179 | resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d" |
11108 | integrity sha512-rgPIqOdfK/4J9FhiVrZ3cveAjRRo5rsQBAIhnylX874y1DX/kEKSVdLsnuHB6l1KTjHyU01VjiMBHgU2adejyg== | 11180 | integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw== |
11109 | 11181 | ||
11110 | typedarray-to-buffer@^3.0.0: | 11182 | typedarray-to-buffer@^3.0.0: |
11111 | version "3.1.5" | 11183 | version "3.1.5" |
@@ -11119,12 +11191,7 @@ typedarray@^0.0.6: | |||
11119 | resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" | 11191 | resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" |
11120 | integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= | 11192 | integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= |
11121 | 11193 | ||
11122 | typescript@4.1.3: | 11194 | typescript@4.1.5, typescript@~4.1.3: |
11123 | version "4.1.3" | ||
11124 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" | ||
11125 | integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== | ||
11126 | |||
11127 | typescript@~4.1.3: | ||
11128 | version "4.1.5" | 11195 | version "4.1.5" |
11129 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" | 11196 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" |
11130 | integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== | 11197 | integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== |
@@ -11140,9 +11207,9 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: | |||
11140 | integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== | 11207 | integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== |
11141 | 11208 | ||
11142 | uglify-js@^3.0.6: | 11209 | uglify-js@^3.0.6: |
11143 | version "3.12.8" | 11210 | version "3.13.2" |
11144 | resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.12.8.tgz#a82e6e53c9be14f7382de3d068ef1e26e7d4aaf8" | 11211 | resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.2.tgz#fe10319861bccc8682bfe2e8151fbdd8aa921c44" |
11145 | integrity sha512-fvBeuXOsvqjecUtF/l1dwsrrf5y2BCUk9AOJGzGcm6tE7vegku5u/YvqjyDaAGr422PLoLnrxg3EnRvTqsdC1w== | 11212 | integrity sha512-SbMu4D2Vo95LMC/MetNaso1194M1htEA+JrqE9Hk+G2DhI+itfS9TRu9ZKeCahLDNa/J3n4MqUJ/fOHMzQpRWw== |
11146 | 11213 | ||
11147 | uint64be@^2.0.2: | 11214 | uint64be@^2.0.2: |
11148 | version "2.0.2" | 11215 | version "2.0.2" |
@@ -11151,6 +11218,16 @@ uint64be@^2.0.2: | |||
11151 | dependencies: | 11218 | dependencies: |
11152 | buffer-alloc "^1.1.0" | 11219 | buffer-alloc "^1.1.0" |
11153 | 11220 | ||
11221 | unbox-primitive@^1.0.0: | ||
11222 | version "1.0.0" | ||
11223 | resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.0.tgz#eeacbc4affa28e9b3d36b5eaeccc50b3251b1d3f" | ||
11224 | integrity sha512-P/51NX+JXyxK/aigg1/ZgyccdAxm5K1+n8+tvqSntjOivPt19gvm1VC49RWYetsiub8WViUchdxl/KWHHB0kzA== | ||
11225 | dependencies: | ||
11226 | function-bind "^1.1.1" | ||
11227 | has-bigints "^1.0.0" | ||
11228 | has-symbols "^1.0.0" | ||
11229 | which-boxed-primitive "^1.0.1" | ||
11230 | |||
11154 | unicode-canonical-property-names-ecmascript@^1.0.4: | 11231 | unicode-canonical-property-names-ecmascript@^1.0.4: |
11155 | version "1.0.4" | 11232 | version "1.0.4" |
11156 | resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" | 11233 | resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" |
@@ -11402,9 +11479,9 @@ uuid@^3.0.0, uuid@^3.3.2, uuid@^3.4.0: | |||
11402 | integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== | 11479 | integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== |
11403 | 11480 | ||
11404 | v8-compile-cache@^2.2.0: | 11481 | v8-compile-cache@^2.2.0: |
11405 | version "2.2.0" | 11482 | version "2.3.0" |
11406 | resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" | 11483 | resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" |
11407 | integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q== | 11484 | integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== |
11408 | 11485 | ||
11409 | validate-npm-package-license@^3.0.1: | 11486 | validate-npm-package-license@^3.0.1: |
11410 | version "3.0.4" | 11487 | version "3.0.4" |
@@ -11801,15 +11878,26 @@ webtorrent@^0.112.3: | |||
11801 | utp-native "^2.3.0" | 11878 | utp-native "^2.3.0" |
11802 | 11879 | ||
11803 | whatwg-fetch@^3.0.0: | 11880 | whatwg-fetch@^3.0.0: |
11804 | version "3.6.1" | 11881 | version "3.6.2" |
11805 | resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.1.tgz#93bc4005af6c2cc30ba3e42ec3125947c8f54ed3" | 11882 | resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" |
11806 | integrity sha512-IEmN/ZfmMw6G1hgZpVd0LuZXOQDisrMOZrzYd5x3RAK4bMPlJohKUZWZ9t/QsTvH0dV9TbPDcc2OSuIDcihnHA== | 11883 | integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== |
11807 | 11884 | ||
11808 | whatwg-mimetype@^2.3.0: | 11885 | whatwg-mimetype@^2.3.0: |
11809 | version "2.3.0" | 11886 | version "2.3.0" |
11810 | resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" | 11887 | resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" |
11811 | integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== | 11888 | integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== |
11812 | 11889 | ||
11890 | which-boxed-primitive@^1.0.1: | ||
11891 | version "1.0.2" | ||
11892 | resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" | ||
11893 | integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== | ||
11894 | dependencies: | ||
11895 | is-bigint "^1.0.1" | ||
11896 | is-boolean-object "^1.1.0" | ||
11897 | is-number-object "^1.0.4" | ||
11898 | is-string "^1.0.5" | ||
11899 | is-symbol "^1.0.3" | ||
11900 | |||
11813 | which-module@^2.0.0: | 11901 | which-module@^2.0.0: |
11814 | version "2.0.0" | 11902 | version "2.0.0" |
11815 | resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" | 11903 | resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" |
@@ -11915,9 +12003,9 @@ ws@^6.2.1: | |||
11915 | async-limiter "~1.0.0" | 12003 | async-limiter "~1.0.0" |
11916 | 12004 | ||
11917 | ws@^7.3.0, ws@^7.3.1, ws@^7.4.2, ws@~7.4.2: | 12005 | ws@^7.3.0, ws@^7.3.1, ws@^7.4.2, ws@~7.4.2: |
11918 | version "7.4.3" | 12006 | version "7.4.4" |
11919 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd" | 12007 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" |
11920 | integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA== | 12008 | integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== |
11921 | 12009 | ||
11922 | xml2js@^0.4.17: | 12010 | xml2js@^0.4.17: |
11923 | version "0.4.23" | 12011 | version "0.4.23" |
@@ -11978,9 +12066,9 @@ yallist@^4.0.0: | |||
11978 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== | 12066 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== |
11979 | 12067 | ||
11980 | yaml@^1.10.0: | 12068 | yaml@^1.10.0: |
11981 | version "1.10.0" | 12069 | version "1.10.2" |
11982 | resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" | 12070 | resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" |
11983 | integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== | 12071 | integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== |
11984 | 12072 | ||
11985 | yargs-parser@^13.1.2: | 12073 | yargs-parser@^13.1.2: |
11986 | version "13.1.2" | 12074 | version "13.1.2" |
@@ -11999,9 +12087,9 @@ yargs-parser@^18.1.2: | |||
11999 | decamelize "^1.2.0" | 12087 | decamelize "^1.2.0" |
12000 | 12088 | ||
12001 | yargs-parser@^20.2.2: | 12089 | yargs-parser@^20.2.2: |
12002 | version "20.2.6" | 12090 | version "20.2.7" |
12003 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.6.tgz#69f920addf61aafc0b8b89002f5d66e28f2d8b20" | 12091 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" |
12004 | integrity sha512-AP1+fQIWSM/sMiET8fyayjx/J+JmTPt2Mr0FkrgqB4todtfa53sOsrSAcIrJRD5XS20bKUwaDIuMkWKCEiQLKA== | 12092 | integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== |
12005 | 12093 | ||
12006 | yargs-parser@^7.0.0: | 12094 | yargs-parser@^7.0.0: |
12007 | version "7.0.0" | 12095 | version "7.0.0" |
diff --git a/config/default.yaml b/config/default.yaml index a09d20b9d..f9b6c50a3 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -198,6 +198,13 @@ federation: | |||
198 | # We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes | 198 | # We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes |
199 | cleanup_remote_interactions: false | 199 | cleanup_remote_interactions: false |
200 | 200 | ||
201 | peertube: | ||
202 | check_latest_version: | ||
203 | # Check and notify admins of new PeerTube versions | ||
204 | enabled: true | ||
205 | # You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json | ||
206 | url: 'https://joinpeertube.org/api/v1/versions.json' | ||
207 | |||
201 | cache: | 208 | cache: |
202 | previews: | 209 | previews: |
203 | size: 500 # Max number of previews you want to cache | 210 | size: 500 # Max number of previews you want to cache |
@@ -265,7 +272,7 @@ transcoding: | |||
265 | # If you also enabled the hls format, it will multiply videos storage by 2 | 272 | # If you also enabled the hls format, it will multiply videos storage by 2 |
266 | # If disabled, breaks federation with PeerTube instances < 2.1 | 273 | # If disabled, breaks federation with PeerTube instances < 2.1 |
267 | webtorrent: | 274 | webtorrent: |
268 | enabled: true | 275 | enabled: false |
269 | 276 | ||
270 | # /!\ Requires ffmpeg >= 4.1 | 277 | # /!\ Requires ffmpeg >= 4.1 |
271 | # Generate HLS playlists and fragmented MP4 files. Better playback than with WebTorrent: | 278 | # Generate HLS playlists and fragmented MP4 files. Better playback than with WebTorrent: |
@@ -274,7 +281,7 @@ transcoding: | |||
274 | # * More stable playback (less bugs/infinite loading) | 281 | # * More stable playback (less bugs/infinite loading) |
275 | # If you also enabled the webtorrent format, it will multiply videos storage by 2 | 282 | # If you also enabled the webtorrent format, it will multiply videos storage by 2 |
276 | hls: | 283 | hls: |
277 | enabled: false | 284 | enabled: true |
278 | 285 | ||
279 | live: | 286 | live: |
280 | enabled: false | 287 | enabled: false |
diff --git a/config/production.yaml.example b/config/production.yaml.example index 31c0e6b96..f2e75af32 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example | |||
@@ -196,6 +196,12 @@ federation: | |||
196 | # We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes | 196 | # We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes |
197 | cleanup_remote_interactions: false | 197 | cleanup_remote_interactions: false |
198 | 198 | ||
199 | peertube: | ||
200 | check_latest_version: | ||
201 | # Check and notify admins of new PeerTube versions | ||
202 | enabled: true | ||
203 | # You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json | ||
204 | url: 'https://joinpeertube.org/api/v1/versions.json' | ||
199 | 205 | ||
200 | ############################################################################### | 206 | ############################################################################### |
201 | # | 207 | # |
@@ -276,7 +282,7 @@ transcoding: | |||
276 | # If you also enabled the hls format, it will multiply videos storage by 2 | 282 | # If you also enabled the hls format, it will multiply videos storage by 2 |
277 | # If disabled, breaks federation with PeerTube instances < 2.1 | 283 | # If disabled, breaks federation with PeerTube instances < 2.1 |
278 | webtorrent: | 284 | webtorrent: |
279 | enabled: true | 285 | enabled: false |
280 | 286 | ||
281 | # /!\ Requires ffmpeg >= 4.1 | 287 | # /!\ Requires ffmpeg >= 4.1 |
282 | # Generate HLS playlists and fragmented MP4 files. Better playback than with WebTorrent: | 288 | # Generate HLS playlists and fragmented MP4 files. Better playback than with WebTorrent: |
@@ -285,7 +291,7 @@ transcoding: | |||
285 | # * More stable playback (less bugs/infinite loading) | 291 | # * More stable playback (less bugs/infinite loading) |
286 | # If you also enabled the webtorrent format, it will multiply videos storage by 2 | 292 | # If you also enabled the webtorrent format, it will multiply videos storage by 2 |
287 | hls: | 293 | hls: |
288 | enabled: false | 294 | enabled: true |
289 | 295 | ||
290 | live: | 296 | live: |
291 | enabled: false | 297 | enabled: false |
diff --git a/config/test.yaml b/config/test.yaml index 33c11afc3..9a522a983 100644 --- a/config/test.yaml +++ b/config/test.yaml | |||
@@ -38,6 +38,10 @@ log: | |||
38 | contact_form: | 38 | contact_form: |
39 | enabled: true | 39 | enabled: true |
40 | 40 | ||
41 | peertube: | ||
42 | check_latest_version: | ||
43 | enabled: false | ||
44 | |||
41 | redundancy: | 45 | redundancy: |
42 | videos: | 46 | videos: |
43 | check_interval: '1 minute' | 47 | check_interval: '1 minute' |
@@ -83,6 +87,8 @@ transcoding: | |||
83 | 1080p: true | 87 | 1080p: true |
84 | 1440p: true | 88 | 1440p: true |
85 | 2160p: true | 89 | 2160p: true |
90 | webtorrent: | ||
91 | enabled: true | ||
86 | hls: | 92 | hls: |
87 | enabled: true | 93 | enabled: true |
88 | 94 | ||
diff --git a/package.json b/package.json index 5719a116c..e3766e318 100644 --- a/package.json +++ b/package.json | |||
@@ -53,6 +53,7 @@ | |||
53 | "start:server": "node dist/server --no-client", | 53 | "start:server": "node dist/server --no-client", |
54 | "update-host": "node ./dist/scripts/update-host.js", | 54 | "update-host": "node ./dist/scripts/update-host.js", |
55 | "create-transcoding-job": "node ./dist/scripts/create-transcoding-job.js", | 55 | "create-transcoding-job": "node ./dist/scripts/create-transcoding-job.js", |
56 | "regenerate-thumbnails": "node ./dist/scripts/regenerate-thumbnails.js", | ||
56 | "create-import-video-file-job": "node ./dist/scripts/create-import-video-file-job.js", | 57 | "create-import-video-file-job": "node ./dist/scripts/create-import-video-file-job.js", |
57 | "print-transcode-command": "node ./dist/scripts/print-transcode-command.js", | 58 | "print-transcode-command": "node ./dist/scripts/print-transcode-command.js", |
58 | "test": "scripty", | 59 | "test": "scripty", |
@@ -82,10 +83,6 @@ | |||
82 | "swagger-cli": "swagger-cli", | 83 | "swagger-cli": "swagger-cli", |
83 | "sass-lint": "sass-lint" | 84 | "sass-lint": "sass-lint" |
84 | }, | 85 | }, |
85 | "resolutions": { | ||
86 | "oauth2-server": "3.1.0-beta.1", | ||
87 | "http-signature": "1.3.5" | ||
88 | }, | ||
89 | "dependencies": { | 86 | "dependencies": { |
90 | "apicache": "1.6.2", | 87 | "apicache": "1.6.2", |
91 | "async": "^3.0.1", | 88 | "async": "^3.0.1", |
@@ -105,12 +102,12 @@ | |||
105 | "deep-object-diff": "^1.1.0", | 102 | "deep-object-diff": "^1.1.0", |
106 | "email-templates": "^8.0.3", | 103 | "email-templates": "^8.0.3", |
107 | "express": "^4.12.4", | 104 | "express": "^4.12.4", |
108 | "express-oauth-server": "^2.0.0", | ||
109 | "express-rate-limit": "^5.0.0", | 105 | "express-rate-limit": "^5.0.0", |
110 | "express-validator": "^6.4.0", | 106 | "express-validator": "^6.4.0", |
111 | "flat": "^5.0.0", | 107 | "flat": "^5.0.0", |
112 | "fluent-ffmpeg": "^2.1.0", | 108 | "fluent-ffmpeg": "^2.1.0", |
113 | "fs-extra": "^9.0.0", | 109 | "fs-extra": "^9.0.0", |
110 | "got": "^11.8.2", | ||
114 | "helmet": "^4.1.0", | 111 | "helmet": "^4.1.0", |
115 | "http-signature": "1.3.5", | 112 | "http-signature": "1.3.5", |
116 | "ip-anonymize": "^0.1.0", | 113 | "ip-anonymize": "^0.1.0", |
@@ -130,7 +127,7 @@ | |||
130 | "multer": "^1.1.0", | 127 | "multer": "^1.1.0", |
131 | "node-media-server": "^2.1.4", | 128 | "node-media-server": "^2.1.4", |
132 | "nodemailer": "^6.0.0", | 129 | "nodemailer": "^6.0.0", |
133 | "oauth2-server": "3.1.0-beta.1", | 130 | "oauth2-server": "3.1.1", |
134 | "parse-torrent": "^9.1.0", | 131 | "parse-torrent": "^9.1.0", |
135 | "password-generator": "^2.0.2", | 132 | "password-generator": "^2.0.2", |
136 | "pem": "^1.12.3", | 133 | "pem": "^1.12.3", |
@@ -140,7 +137,6 @@ | |||
140 | "pug": "^3.0.0", | 137 | "pug": "^3.0.0", |
141 | "redis": "^3.0.2", | 138 | "redis": "^3.0.2", |
142 | "reflect-metadata": "^0.1.12", | 139 | "reflect-metadata": "^0.1.12", |
143 | "request": "^2.81.0", | ||
144 | "sanitize-html": "2.x", | 140 | "sanitize-html": "2.x", |
145 | "scripty": "^2.0.0", | 141 | "scripty": "^2.0.0", |
146 | "sequelize": "6.5.0", | 142 | "sequelize": "6.5.0", |
@@ -178,7 +174,6 @@ | |||
178 | "@types/express-rate-limit": "^5.0.0", | 174 | "@types/express-rate-limit": "^5.0.0", |
179 | "@types/fluent-ffmpeg": "^2.1.16", | 175 | "@types/fluent-ffmpeg": "^2.1.16", |
180 | "@types/fs-extra": "^9.0.1", | 176 | "@types/fs-extra": "^9.0.1", |
181 | "@types/libxmljs": "^0.18.0", | ||
182 | "@types/lodash": "^4.14.64", | 177 | "@types/lodash": "^4.14.64", |
183 | "@types/lru-cache": "^5.1.0", | 178 | "@types/lru-cache": "^5.1.0", |
184 | "@types/magnet-uri": "^5.1.1", | 179 | "@types/magnet-uri": "^5.1.1", |
@@ -210,7 +205,7 @@ | |||
210 | "eslint-plugin-node": "^11.0.0", | 205 | "eslint-plugin-node": "^11.0.0", |
211 | "eslint-plugin-promise": "^4.2.1", | 206 | "eslint-plugin-promise": "^4.2.1", |
212 | "eslint-plugin-standard": "^5.0.0", | 207 | "eslint-plugin-standard": "^5.0.0", |
213 | "libxmljs": "0.19.7", | 208 | "fast-xml-parser": "^3.19.0", |
214 | "maildev": "^1.0.0-rc3", | 209 | "maildev": "^1.0.0-rc3", |
215 | "marked": "^2.0.1", | 210 | "marked": "^2.0.1", |
216 | "marked-man": "^0.7.0", | 211 | "marked-man": "^0.7.0", |
diff --git a/scripts/benchmark.ts b/scripts/benchmark.ts index 45b2a7a79..0cadb36d9 100644 --- a/scripts/benchmark.ts +++ b/scripts/benchmark.ts | |||
@@ -50,126 +50,102 @@ async function run () { | |||
50 | title: 'AP - account peertube', | 50 | title: 'AP - account peertube', |
51 | path: '/accounts/peertube', | 51 | path: '/accounts/peertube', |
52 | headers: buildAPHeader(), | 52 | headers: buildAPHeader(), |
53 | expecter: (client, statusCode) => { | 53 | expecter: (body, status) => { |
54 | const body = client.resData[0].body | 54 | return status === 200 && body.startsWith('{"type":') |
55 | |||
56 | return statusCode === 200 && body.startsWith('{"type":') | ||
57 | } | 55 | } |
58 | }, | 56 | }, |
59 | { | 57 | { |
60 | title: 'AP - video', | 58 | title: 'AP - video', |
61 | path: '/videos/watch/' + video.uuid, | 59 | path: '/videos/watch/' + video.uuid, |
62 | headers: buildAPHeader(), | 60 | headers: buildAPHeader(), |
63 | expecter: (client, statusCode) => { | 61 | expecter: (body, status) => { |
64 | const body = client.resData[0].body | 62 | return status === 200 && body.startsWith('{"type":"Video"') |
65 | |||
66 | return statusCode === 200 && body.startsWith('{"type":"Video"') | ||
67 | } | 63 | } |
68 | }, | 64 | }, |
69 | { | 65 | { |
70 | title: 'Misc - webfinger peertube', | 66 | title: 'Misc - webfinger peertube', |
71 | path: '/.well-known/webfinger?resource=acct:peertube@' + server.host, | 67 | path: '/.well-known/webfinger?resource=acct:peertube@' + server.host, |
72 | expecter: (client, statusCode) => { | 68 | expecter: (body, status) => { |
73 | const body = client.resData[0].body | 69 | return status === 200 && body.startsWith('{"subject":') |
74 | |||
75 | return statusCode === 200 && body.startsWith('{"subject":') | ||
76 | } | 70 | } |
77 | }, | 71 | }, |
78 | { | 72 | { |
79 | title: 'API - unread notifications', | 73 | title: 'API - unread notifications', |
80 | path: '/api/v1/users/me/notifications?start=0&count=0&unread=true', | 74 | path: '/api/v1/users/me/notifications?start=0&count=0&unread=true', |
81 | headers: buildAuthorizationHeader(), | 75 | headers: buildAuthorizationHeader(), |
82 | expecter: (_client, statusCode) => { | 76 | expecter: (_body, status) => { |
83 | return statusCode === 200 | 77 | return status === 200 |
84 | } | 78 | } |
85 | }, | 79 | }, |
86 | { | 80 | { |
87 | title: 'API - me', | 81 | title: 'API - me', |
88 | path: '/api/v1/users/me', | 82 | path: '/api/v1/users/me', |
89 | headers: buildAuthorizationHeader(), | 83 | headers: buildAuthorizationHeader(), |
90 | expecter: (client, statusCode) => { | 84 | expecter: (body, status) => { |
91 | const body = client.resData[0].body | 85 | return status === 200 && body.startsWith('{"id":') |
92 | |||
93 | return statusCode === 200 && body.startsWith('{"id":') | ||
94 | } | 86 | } |
95 | }, | 87 | }, |
96 | { | 88 | { |
97 | title: 'API - videos list', | 89 | title: 'API - videos list', |
98 | path: '/api/v1/videos', | 90 | path: '/api/v1/videos', |
99 | expecter: (client, statusCode) => { | 91 | expecter: (body, status) => { |
100 | const body = client.resData[0].body | 92 | return status === 200 && body.startsWith('{"total":10') |
101 | |||
102 | return statusCode === 200 && body.startsWith('{"total":10') | ||
103 | } | 93 | } |
104 | }, | 94 | }, |
105 | { | 95 | { |
106 | title: 'API - video get', | 96 | title: 'API - video get', |
107 | path: '/api/v1/videos/' + video.uuid, | 97 | path: '/api/v1/videos/' + video.uuid, |
108 | expecter: (client, statusCode) => { | 98 | expecter: (body, status) => { |
109 | const body = client.resData[0].body | 99 | return status === 200 && body.startsWith('{"id":') |
110 | |||
111 | return statusCode === 200 && body.startsWith('{"id":') | ||
112 | } | 100 | } |
113 | }, | 101 | }, |
114 | { | 102 | { |
115 | title: 'API - video captions', | 103 | title: 'API - video captions', |
116 | path: '/api/v1/videos/' + video.uuid + '/captions', | 104 | path: '/api/v1/videos/' + video.uuid + '/captions', |
117 | expecter: (client, statusCode) => { | 105 | expecter: (body, status) => { |
118 | const body = client.resData[0].body | 106 | return status === 200 && body.startsWith('{"total":4') |
119 | |||
120 | return statusCode === 200 && body.startsWith('{"total":4') | ||
121 | } | 107 | } |
122 | }, | 108 | }, |
123 | { | 109 | { |
124 | title: 'API - video threads', | 110 | title: 'API - video threads', |
125 | path: '/api/v1/videos/' + video.uuid + '/comment-threads', | 111 | path: '/api/v1/videos/' + video.uuid + '/comment-threads', |
126 | expecter: (client, statusCode) => { | 112 | expecter: (body, status) => { |
127 | const body = client.resData[0].body | 113 | return status === 200 && body.startsWith('{"total":10') |
128 | |||
129 | return statusCode === 200 && body.startsWith('{"total":10') | ||
130 | } | 114 | } |
131 | }, | 115 | }, |
132 | { | 116 | { |
133 | title: 'API - video replies', | 117 | title: 'API - video replies', |
134 | path: '/api/v1/videos/' + video.uuid + '/comment-threads/' + threadId, | 118 | path: '/api/v1/videos/' + video.uuid + '/comment-threads/' + threadId, |
135 | expecter: (client, statusCode) => { | 119 | expecter: (body, status) => { |
136 | const body = client.resData[0].body | 120 | return status === 200 && body.startsWith('{"comment":{') |
137 | |||
138 | return statusCode === 200 && body.startsWith('{"comment":{') | ||
139 | } | 121 | } |
140 | }, | 122 | }, |
141 | { | 123 | { |
142 | title: 'HTML - video watch', | 124 | title: 'HTML - video watch', |
143 | path: '/videos/watch/' + video.uuid, | 125 | path: '/videos/watch/' + video.uuid, |
144 | expecter: (client, statusCode) => { | 126 | expecter: (body, status) => { |
145 | const body = client.resData[0].body | 127 | return status === 200 && body.includes('<title>my super') |
146 | |||
147 | return statusCode === 200 && body.includes('<title>my super') | ||
148 | } | 128 | } |
149 | }, | 129 | }, |
150 | { | 130 | { |
151 | title: 'HTML - video embed', | 131 | title: 'HTML - video embed', |
152 | path: '/videos/embed/' + video.uuid, | 132 | path: '/videos/embed/' + video.uuid, |
153 | expecter: (client, statusCode) => { | 133 | expecter: (body, status) => { |
154 | const body = client.resData[0].body | 134 | return status === 200 && body.includes('embed') |
155 | |||
156 | return statusCode === 200 && body.includes('embed') | ||
157 | } | 135 | } |
158 | }, | 136 | }, |
159 | { | 137 | { |
160 | title: 'HTML - homepage', | 138 | title: 'HTML - homepage', |
161 | path: '/', | 139 | path: '/', |
162 | expecter: (_client, statusCode) => { | 140 | expecter: (_body, status) => { |
163 | return statusCode === 200 | 141 | return status === 200 |
164 | } | 142 | } |
165 | }, | 143 | }, |
166 | { | 144 | { |
167 | title: 'API - config', | 145 | title: 'API - config', |
168 | path: '/api/v1/config', | 146 | path: '/api/v1/config', |
169 | expecter: (client, statusCode) => { | 147 | expecter: (body, status) => { |
170 | const body = client.resData[0].body | 148 | return status === 200 && body.startsWith('{"instance":') |
171 | |||
172 | return statusCode === 200 && body.startsWith('{"instance":') | ||
173 | } | 149 | } |
174 | } | 150 | } |
175 | ] | 151 | ] |
@@ -197,24 +173,27 @@ function runBenchmark (options: { | |||
197 | const { path, expecter, headers } = options | 173 | const { path, expecter, headers } = options |
198 | 174 | ||
199 | return new Promise((res, rej) => { | 175 | return new Promise((res, rej) => { |
200 | const instance = autocannon({ | 176 | autocannon({ |
201 | url: server.url + path, | 177 | url: server.url + path, |
202 | connections: 20, | 178 | connections: 20, |
203 | headers, | 179 | headers, |
204 | pipelining: 1, | 180 | pipelining: 1, |
205 | duration: 10 | 181 | duration: 10, |
182 | requests: [ | ||
183 | { | ||
184 | onResponse: (status, body) => { | ||
185 | if (expecter(body, status) !== true) { | ||
186 | console.error('Expected result failed.', { body, status }) | ||
187 | throw new Error('Invalid expectation') | ||
188 | } | ||
189 | } | ||
190 | } | ||
191 | ] | ||
206 | }, (err, result) => { | 192 | }, (err, result) => { |
207 | if (err) return rej(err) | 193 | if (err) return rej(err) |
208 | 194 | ||
209 | return res(result) | 195 | return res(result) |
210 | }) | 196 | }) |
211 | |||
212 | instance.on('response', (client, statusCode) => { | ||
213 | if (expecter(client, statusCode) !== true) { | ||
214 | console.error('Expected result failed.', { data: client.resData }) | ||
215 | process.exit(-1) | ||
216 | } | ||
217 | }) | ||
218 | }) | 197 | }) |
219 | } | 198 | } |
220 | 199 | ||
diff --git a/scripts/parse-log.ts b/scripts/parse-log.ts index 3679dab74..5f4480c88 100755 --- a/scripts/parse-log.ts +++ b/scripts/parse-log.ts | |||
@@ -15,6 +15,8 @@ import { format as sqlFormat } from 'sql-formatter' | |||
15 | program | 15 | program |
16 | .option('-l, --level [level]', 'Level log (debug/info/warn/error)') | 16 | .option('-l, --level [level]', 'Level log (debug/info/warn/error)') |
17 | .option('-f, --files [file...]', 'Files to parse. If not provided, the script will parse the latest log file from config)') | 17 | .option('-f, --files [file...]', 'Files to parse. If not provided, the script will parse the latest log file from config)') |
18 | .option('-t, --tags [tags...]', 'Display only lines with these tags') | ||
19 | .option('-nt, --not-tags [tags...]', 'Donrt display lines containing these tags') | ||
18 | .parse(process.argv) | 20 | .parse(process.argv) |
19 | 21 | ||
20 | const options = program.opts() | 22 | const options = program.opts() |
@@ -24,6 +26,7 @@ const excludedKeys = { | |||
24 | message: true, | 26 | message: true, |
25 | splat: true, | 27 | splat: true, |
26 | timestamp: true, | 28 | timestamp: true, |
29 | tags: true, | ||
27 | label: true, | 30 | label: true, |
28 | sql: true | 31 | sql: true |
29 | } | 32 | } |
@@ -93,6 +96,14 @@ function run () { | |||
93 | rl.on('line', line => { | 96 | rl.on('line', line => { |
94 | try { | 97 | try { |
95 | const log = JSON.parse(line) | 98 | const log = JSON.parse(line) |
99 | if (options.tags && !containsTags(log.tags, options.tags)) { | ||
100 | return | ||
101 | } | ||
102 | |||
103 | if (options.notTags && containsTags(log.tags, options.notTags)) { | ||
104 | return | ||
105 | } | ||
106 | |||
96 | // Don't know why but loggerFormat does not remove splat key | 107 | // Don't know why but loggerFormat does not remove splat key |
97 | Object.assign(log, { splat: undefined }) | 108 | Object.assign(log, { splat: undefined }) |
98 | 109 | ||
@@ -131,3 +142,15 @@ function toTimeFormat (time: string) { | |||
131 | 142 | ||
132 | return new Date(timestamp).toISOString() | 143 | return new Date(timestamp).toISOString() |
133 | } | 144 | } |
145 | |||
146 | function containsTags (loggerTags: string[], optionsTags: string[]) { | ||
147 | if (!loggerTags) return false | ||
148 | |||
149 | for (const lt of loggerTags) { | ||
150 | for (const ot of optionsTags) { | ||
151 | if (lt === ot) return true | ||
152 | } | ||
153 | } | ||
154 | |||
155 | return false | ||
156 | } | ||
diff --git a/scripts/prune-storage.ts b/scripts/prune-storage.ts index dcb1fcf90..bdfb335c6 100755 --- a/scripts/prune-storage.ts +++ b/scripts/prune-storage.ts | |||
@@ -11,7 +11,7 @@ import { VideoRedundancyModel } from '../server/models/redundancy/video-redundan | |||
11 | import * as Bluebird from 'bluebird' | 11 | import * as Bluebird from 'bluebird' |
12 | import { getUUIDFromFilename } from '../server/helpers/utils' | 12 | import { getUUIDFromFilename } from '../server/helpers/utils' |
13 | import { ThumbnailModel } from '../server/models/video/thumbnail' | 13 | import { ThumbnailModel } from '../server/models/video/thumbnail' |
14 | import { AvatarModel } from '../server/models/avatar/avatar' | 14 | import { ActorImageModel } from '../server/models/account/actor-image' |
15 | import { uniq, values } from 'lodash' | 15 | import { uniq, values } from 'lodash' |
16 | import { ThumbnailType } from '@shared/models' | 16 | import { ThumbnailType } from '@shared/models' |
17 | 17 | ||
@@ -43,7 +43,7 @@ async function run () { | |||
43 | await pruneDirectory(CONFIG.STORAGE.PREVIEWS_DIR, doesThumbnailExist(true, ThumbnailType.PREVIEW)), | 43 | await pruneDirectory(CONFIG.STORAGE.PREVIEWS_DIR, doesThumbnailExist(true, ThumbnailType.PREVIEW)), |
44 | await pruneDirectory(CONFIG.STORAGE.THUMBNAILS_DIR, doesThumbnailExist(false, ThumbnailType.MINIATURE)), | 44 | await pruneDirectory(CONFIG.STORAGE.THUMBNAILS_DIR, doesThumbnailExist(false, ThumbnailType.MINIATURE)), |
45 | 45 | ||
46 | await pruneDirectory(CONFIG.STORAGE.AVATARS_DIR, doesAvatarExist) | 46 | await pruneDirectory(CONFIG.STORAGE.ACTOR_IMAGES, doesActorImageExist) |
47 | ) | 47 | ) |
48 | 48 | ||
49 | const tmpFiles = await readdir(CONFIG.STORAGE.TMP_DIR) | 49 | const tmpFiles = await readdir(CONFIG.STORAGE.TMP_DIR) |
@@ -107,10 +107,10 @@ function doesThumbnailExist (keepOnlyOwned: boolean, type: ThumbnailType) { | |||
107 | } | 107 | } |
108 | } | 108 | } |
109 | 109 | ||
110 | async function doesAvatarExist (file: string) { | 110 | async function doesActorImageExist (file: string) { |
111 | const avatar = await AvatarModel.loadByName(file) | 111 | const image = await ActorImageModel.loadByName(file) |
112 | 112 | ||
113 | return !!avatar | 113 | return !!image |
114 | } | 114 | } |
115 | 115 | ||
116 | async function doesRedundancyExist (file: string) { | 116 | async function doesRedundancyExist (file: string) { |
diff --git a/scripts/regenerate-thumbnails.ts b/scripts/regenerate-thumbnails.ts new file mode 100644 index 000000000..b95343c0b --- /dev/null +++ b/scripts/regenerate-thumbnails.ts | |||
@@ -0,0 +1,68 @@ | |||
1 | import { registerTSPaths } from '../server/helpers/register-ts-paths' | ||
2 | registerTSPaths() | ||
3 | |||
4 | import * as Bluebird from 'bluebird' | ||
5 | import * as program from 'commander' | ||
6 | import { pathExists, remove } from 'fs-extra' | ||
7 | import { generateImageFilename, processImage } from '@server/helpers/image-utils' | ||
8 | import { THUMBNAILS_SIZE } from '@server/initializers/constants' | ||
9 | import { VideoModel } from '@server/models/video/video' | ||
10 | import { MVideo } from '@server/types/models' | ||
11 | import { initDatabaseModels } from '@server/initializers/database' | ||
12 | |||
13 | program | ||
14 | .description('Regenerate local thumbnails using preview files') | ||
15 | .parse(process.argv) | ||
16 | |||
17 | run() | ||
18 | .then(() => process.exit(0)) | ||
19 | .catch(err => console.error(err)) | ||
20 | |||
21 | async function run () { | ||
22 | await initDatabaseModels(true) | ||
23 | |||
24 | const videos = await VideoModel.listLocal() | ||
25 | |||
26 | await Bluebird.map(videos, v => { | ||
27 | return processVideo(v) | ||
28 | .catch(err => console.error('Cannot process video %s.', v.url, err)) | ||
29 | }, { concurrency: 20 }) | ||
30 | } | ||
31 | |||
32 | async function processVideo (videoArg: MVideo) { | ||
33 | const video = await VideoModel.loadWithFiles(videoArg.id) | ||
34 | |||
35 | console.log('Processing video %s.', video.name) | ||
36 | |||
37 | const thumbnail = video.getMiniature() | ||
38 | const preview = video.getPreview() | ||
39 | |||
40 | const previewPath = preview.getPath() | ||
41 | |||
42 | if (!await pathExists(previewPath)) { | ||
43 | throw new Error(`Preview ${previewPath} does not exist on disk`) | ||
44 | } | ||
45 | |||
46 | const size = { | ||
47 | width: THUMBNAILS_SIZE.width, | ||
48 | height: THUMBNAILS_SIZE.height | ||
49 | } | ||
50 | |||
51 | const oldPath = thumbnail.getPath() | ||
52 | |||
53 | // Update thumbnail | ||
54 | thumbnail.filename = generateImageFilename() | ||
55 | thumbnail.width = size.width | ||
56 | thumbnail.height = size.height | ||
57 | |||
58 | const thumbnailPath = thumbnail.getPath() | ||
59 | await processImage(previewPath, thumbnailPath, size, true) | ||
60 | |||
61 | // Save new attributes | ||
62 | await thumbnail.save() | ||
63 | |||
64 | // Remove old thumbnail | ||
65 | await remove(oldPath) | ||
66 | |||
67 | // Don't federate, remote instances will refresh the thumbnails after a while | ||
68 | } | ||
diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index f5f3219af..64a7b18fd 100755 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh | |||
@@ -39,8 +39,9 @@ then | |||
39 | DB_PASS=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['password'])") | 39 | DB_PASS=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['password'])") |
40 | DB_HOST=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['hostname'])") | 40 | DB_HOST=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['hostname'])") |
41 | DB_SUFFIX=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['suffix'])") | 41 | DB_SUFFIX=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['suffix'])") |
42 | DB_NAME=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['name'] || '')") | ||
42 | mkdir -p $PEERTUBE_PATH/backup | 43 | mkdir -p $PEERTUBE_PATH/backup |
43 | PGPASSWORD=$DB_PASS pg_dump -U $DB_USER -h $DB_HOST -F c "peertube${DB_SUFFIX}" -f "$SQL_BACKUP_PATH" | 44 | PGPASSWORD=$DB_PASS pg_dump -U $DB_USER -h $DB_HOST -F c "${DB_NAME:-'peertube${DB_SUFFIX}'}" -f "$SQL_BACKUP_PATH" |
44 | else | 45 | else |
45 | echo "pg_dump not found. Cannot make a SQL backup!" | 46 | echo "pg_dump not found. Cannot make a SQL backup!" |
46 | fi | 47 | fi |
@@ -44,7 +44,7 @@ checkFFmpeg(CONFIG) | |||
44 | 44 | ||
45 | checkNodeVersion() | 45 | checkNodeVersion() |
46 | 46 | ||
47 | import { checkConfig, checkActivityPubUrls } from './server/initializers/checker-after-init' | 47 | import { checkConfig, checkActivityPubUrls, checkFFmpegVersion } from './server/initializers/checker-after-init' |
48 | 48 | ||
49 | const errorMessage = checkConfig() | 49 | const errorMessage = checkConfig() |
50 | if (errorMessage !== null) { | 50 | if (errorMessage !== null) { |
@@ -120,6 +120,7 @@ import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto' | |||
120 | import { PeerTubeSocket } from './server/lib/peertube-socket' | 120 | import { PeerTubeSocket } from './server/lib/peertube-socket' |
121 | import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls' | 121 | import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls' |
122 | import { PluginsCheckScheduler } from './server/lib/schedulers/plugins-check-scheduler' | 122 | import { PluginsCheckScheduler } from './server/lib/schedulers/plugins-check-scheduler' |
123 | import { PeerTubeVersionCheckScheduler } from './server/lib/schedulers/peertube-version-check-scheduler' | ||
123 | import { Hooks } from './server/lib/plugins/hooks' | 124 | import { Hooks } from './server/lib/plugins/hooks' |
124 | import { PluginManager } from './server/lib/plugins/plugin-manager' | 125 | import { PluginManager } from './server/lib/plugins/plugin-manager' |
125 | import { LiveManager } from './server/lib/live-manager' | 126 | import { LiveManager } from './server/lib/live-manager' |
@@ -160,7 +161,9 @@ morgan.token('user-agent', (req: express.Request) => { | |||
160 | return req.get('user-agent') | 161 | return req.get('user-agent') |
161 | }) | 162 | }) |
162 | app.use(morgan('combined', { | 163 | app.use(morgan('combined', { |
163 | stream: { write: logger.info.bind(logger) }, | 164 | stream: { |
165 | write: (str: string) => logger.info(str, { tags: [ 'http' ] }) | ||
166 | }, | ||
164 | skip: req => CONFIG.LOG.LOG_PING_REQUESTS === false && req.originalUrl === '/api/v1/ping' | 167 | skip: req => CONFIG.LOG.LOG_PING_REQUESTS === false && req.originalUrl === '/api/v1/ping' |
165 | })) | 168 | })) |
166 | 169 | ||
@@ -250,6 +253,9 @@ async function startApplication () { | |||
250 | process.exit(-1) | 253 | process.exit(-1) |
251 | }) | 254 | }) |
252 | 255 | ||
256 | checkFFmpegVersion() | ||
257 | .catch(err => logger.error('Cannot check ffmpeg version', { err })) | ||
258 | |||
253 | // Email initialization | 259 | // Email initialization |
254 | Emailer.Instance.init() | 260 | Emailer.Instance.init() |
255 | 261 | ||
@@ -272,6 +278,7 @@ async function startApplication () { | |||
272 | RemoveOldHistoryScheduler.Instance.enable() | 278 | RemoveOldHistoryScheduler.Instance.enable() |
273 | RemoveOldViewsScheduler.Instance.enable() | 279 | RemoveOldViewsScheduler.Instance.enable() |
274 | PluginsCheckScheduler.Instance.enable() | 280 | PluginsCheckScheduler.Instance.enable() |
281 | PeerTubeVersionCheckScheduler.Instance.enable() | ||
275 | AutoFollowIndexInstances.Instance.enable() | 282 | AutoFollowIndexInstances.Instance.enable() |
276 | 283 | ||
277 | // Redis initialization | 284 | // Redis initialization |
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index fb108ca1c..e28f7502d 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -158,9 +158,17 @@ async function getConfig (req: express.Request, res: express.Response) { | |||
158 | avatar: { | 158 | avatar: { |
159 | file: { | 159 | file: { |
160 | size: { | 160 | size: { |
161 | max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max | 161 | max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max |
162 | }, | 162 | }, |
163 | extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME | 163 | extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME |
164 | } | ||
165 | }, | ||
166 | banner: { | ||
167 | file: { | ||
168 | size: { | ||
169 | max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max | ||
170 | }, | ||
171 | extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME | ||
164 | } | 172 | } |
165 | }, | 173 | }, |
166 | video: { | 174 | video: { |
diff --git a/server/controllers/api/jobs.ts b/server/controllers/api/jobs.ts index 861cc22b9..d7cee1605 100644 --- a/server/controllers/api/jobs.ts +++ b/server/controllers/api/jobs.ts | |||
@@ -9,10 +9,10 @@ import { | |||
9 | authenticate, | 9 | authenticate, |
10 | ensureUserHasRight, | 10 | ensureUserHasRight, |
11 | jobsSortValidator, | 11 | jobsSortValidator, |
12 | paginationValidatorBuilder, | ||
12 | setDefaultPagination, | 13 | setDefaultPagination, |
13 | setDefaultSort | 14 | setDefaultSort |
14 | } from '../../middlewares' | 15 | } from '../../middlewares' |
15 | import { paginationValidator } from '../../middlewares/validators' | ||
16 | import { listJobsValidator } from '../../middlewares/validators/jobs' | 16 | import { listJobsValidator } from '../../middlewares/validators/jobs' |
17 | 17 | ||
18 | const jobsRouter = express.Router() | 18 | const jobsRouter = express.Router() |
@@ -20,7 +20,7 @@ const jobsRouter = express.Router() | |||
20 | jobsRouter.get('/:state?', | 20 | jobsRouter.get('/:state?', |
21 | authenticate, | 21 | authenticate, |
22 | ensureUserHasRight(UserRight.MANAGE_JOBS), | 22 | ensureUserHasRight(UserRight.MANAGE_JOBS), |
23 | paginationValidator, | 23 | paginationValidatorBuilder([ 'jobs' ]), |
24 | jobsSortValidator, | 24 | jobsSortValidator, |
25 | setDefaultSort, | 25 | setDefaultSort, |
26 | setDefaultPagination, | 26 | setDefaultPagination, |
diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts index 1c0b5edb1..bb69f25a1 100644 --- a/server/controllers/api/plugins.ts +++ b/server/controllers/api/plugins.ts | |||
@@ -205,7 +205,6 @@ async function listAvailablePlugins (req: express.Request, res: express.Response | |||
205 | if (!resultList) { | 205 | if (!resultList) { |
206 | return res.status(HttpStatusCode.SERVICE_UNAVAILABLE_503) | 206 | return res.status(HttpStatusCode.SERVICE_UNAVAILABLE_503) |
207 | .json({ error: 'Plugin index unavailable. Please retry later' }) | 207 | .json({ error: 'Plugin index unavailable. Please retry later' }) |
208 | .end() | ||
209 | } | 208 | } |
210 | 209 | ||
211 | return res.json(resultList) | 210 | return res.json(resultList) |
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts index 7e1b7b230..f0cdf3a89 100644 --- a/server/controllers/api/search.ts +++ b/server/controllers/api/search.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { sanitizeUrl } from '@server/helpers/core-utils' | 2 | import { sanitizeUrl } from '@server/helpers/core-utils' |
3 | import { doRequest } from '@server/helpers/requests' | 3 | import { doJSONRequest } from '@server/helpers/requests' |
4 | import { CONFIG } from '@server/initializers/config' | 4 | import { CONFIG } from '@server/initializers/config' |
5 | import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos' | 5 | import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos' |
6 | import { Hooks } from '@server/lib/plugins/hooks' | ||
6 | import { AccountBlocklistModel } from '@server/models/account/account-blocklist' | 7 | import { AccountBlocklistModel } from '@server/models/account/account-blocklist' |
7 | import { getServerActor } from '@server/models/application/application' | 8 | import { getServerActor } from '@server/models/application/application' |
8 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | 9 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' |
@@ -22,8 +23,8 @@ import { | |||
22 | paginationValidator, | 23 | paginationValidator, |
23 | setDefaultPagination, | 24 | setDefaultPagination, |
24 | setDefaultSearchSort, | 25 | setDefaultSearchSort, |
25 | videoChannelsSearchSortValidator, | ||
26 | videoChannelsListSearchValidator, | 26 | videoChannelsListSearchValidator, |
27 | videoChannelsSearchSortValidator, | ||
27 | videosSearchSortValidator, | 28 | videosSearchSortValidator, |
28 | videosSearchValidator | 29 | videosSearchValidator |
29 | } from '../../middlewares' | 30 | } from '../../middlewares' |
@@ -87,16 +88,17 @@ function searchVideoChannels (req: express.Request, res: express.Response) { | |||
87 | async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) { | 88 | async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) { |
88 | const result = await buildMutedForSearchIndex(res) | 89 | const result = await buildMutedForSearchIndex(res) |
89 | 90 | ||
90 | const body = Object.assign(query, result) | 91 | const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params') |
91 | 92 | ||
92 | const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels' | 93 | const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels' |
93 | 94 | ||
94 | try { | 95 | try { |
95 | logger.debug('Doing video channels search index request on %s.', url, { body }) | 96 | logger.debug('Doing video channels search index request on %s.', url, { body }) |
96 | 97 | ||
97 | const searchIndexResult = await doRequest<ResultList<VideoChannel>>({ uri: url, body, json: true }) | 98 | const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoChannel>>(url, { method: 'POST', json: body }) |
99 | const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result') | ||
98 | 100 | ||
99 | return res.json(searchIndexResult.body) | 101 | return res.json(jsonResult) |
100 | } catch (err) { | 102 | } catch (err) { |
101 | logger.warn('Cannot use search index to make video channels search.', { err }) | 103 | logger.warn('Cannot use search index to make video channels search.', { err }) |
102 | 104 | ||
@@ -107,14 +109,19 @@ async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: e | |||
107 | async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) { | 109 | async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) { |
108 | const serverActor = await getServerActor() | 110 | const serverActor = await getServerActor() |
109 | 111 | ||
110 | const options = { | 112 | const apiOptions = await Hooks.wrapObject({ |
111 | actorId: serverActor.id, | 113 | actorId: serverActor.id, |
112 | search: query.search, | 114 | search: query.search, |
113 | start: query.start, | 115 | start: query.start, |
114 | count: query.count, | 116 | count: query.count, |
115 | sort: query.sort | 117 | sort: query.sort |
116 | } | 118 | }, 'filter:api.search.video-channels.local.list.params') |
117 | const resultList = await VideoChannelModel.searchForApi(options) | 119 | |
120 | const resultList = await Hooks.wrapPromiseFun( | ||
121 | VideoChannelModel.searchForApi, | ||
122 | apiOptions, | ||
123 | 'filter:api.search.video-channels.local.list.result' | ||
124 | ) | ||
118 | 125 | ||
119 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 126 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
120 | } | 127 | } |
@@ -168,7 +175,7 @@ function searchVideos (req: express.Request, res: express.Response) { | |||
168 | async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) { | 175 | async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) { |
169 | const result = await buildMutedForSearchIndex(res) | 176 | const result = await buildMutedForSearchIndex(res) |
170 | 177 | ||
171 | const body: VideosSearchQuery = Object.assign(query, result) | 178 | let body: VideosSearchQuery = Object.assign(query, result) |
172 | 179 | ||
173 | // Use the default instance NSFW policy if not specified | 180 | // Use the default instance NSFW policy if not specified |
174 | if (!body.nsfw) { | 181 | if (!body.nsfw) { |
@@ -181,14 +188,17 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons | |||
181 | : 'both' | 188 | : 'both' |
182 | } | 189 | } |
183 | 190 | ||
191 | body = await Hooks.wrapObject(body, 'filter:api.search.videos.index.list.params') | ||
192 | |||
184 | const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos' | 193 | const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos' |
185 | 194 | ||
186 | try { | 195 | try { |
187 | logger.debug('Doing videos search index request on %s.', url, { body }) | 196 | logger.debug('Doing videos search index request on %s.', url, { body }) |
188 | 197 | ||
189 | const searchIndexResult = await doRequest<ResultList<Video>>({ uri: url, body, json: true }) | 198 | const { body: searchIndexResult } = await doJSONRequest<ResultList<Video>>(url, { method: 'POST', json: body }) |
199 | const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result') | ||
190 | 200 | ||
191 | return res.json(searchIndexResult.body) | 201 | return res.json(jsonResult) |
192 | } catch (err) { | 202 | } catch (err) { |
193 | logger.warn('Cannot use search index to make video search.', { err }) | 203 | logger.warn('Cannot use search index to make video search.', { err }) |
194 | 204 | ||
@@ -197,13 +207,18 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons | |||
197 | } | 207 | } |
198 | 208 | ||
199 | async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { | 209 | async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { |
200 | const options = Object.assign(query, { | 210 | const apiOptions = await Hooks.wrapObject(Object.assign(query, { |
201 | includeLocalVideos: true, | 211 | includeLocalVideos: true, |
202 | nsfw: buildNSFWFilter(res, query.nsfw), | 212 | nsfw: buildNSFWFilter(res, query.nsfw), |
203 | filter: query.filter, | 213 | filter: query.filter, |
204 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined | 214 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined |
205 | }) | 215 | }), 'filter:api.search.videos.local.list.params') |
206 | const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) | 216 | |
217 | const resultList = await Hooks.wrapPromiseFun( | ||
218 | VideoModel.searchAndPopulateAccountAndServer, | ||
219 | apiOptions, | ||
220 | 'filter:api.search.videos.local.list.result' | ||
221 | ) | ||
207 | 222 | ||
208 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 223 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
209 | } | 224 | } |
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index 3be1d55ae..e2b1ea7cd 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -2,8 +2,10 @@ import * as express from 'express' | |||
2 | import * as RateLimit from 'express-rate-limit' | 2 | import * as RateLimit from 'express-rate-limit' |
3 | import { tokensRouter } from '@server/controllers/api/users/token' | 3 | import { tokensRouter } from '@server/controllers/api/users/token' |
4 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | ||
5 | import { MUser, MUserAccountDefault } from '@server/types/models' | 6 | import { MUser, MUserAccountDefault } from '@server/types/models' |
6 | import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' | 7 | import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' |
8 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
7 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' | 9 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' |
8 | import { UserRegister } from '../../../../shared/models/users/user-register.model' | 10 | import { UserRegister } from '../../../../shared/models/users/user-register.model' |
9 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' | 11 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' |
@@ -14,7 +16,6 @@ import { WEBSERVER } from '../../../initializers/constants' | |||
14 | import { sequelizeTypescript } from '../../../initializers/database' | 16 | import { sequelizeTypescript } from '../../../initializers/database' |
15 | import { Emailer } from '../../../lib/emailer' | 17 | import { Emailer } from '../../../lib/emailer' |
16 | import { Notifier } from '../../../lib/notifier' | 18 | import { Notifier } from '../../../lib/notifier' |
17 | import { deleteUserToken } from '../../../lib/oauth-model' | ||
18 | import { Redis } from '../../../lib/redis' | 19 | import { Redis } from '../../../lib/redis' |
19 | import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' | 20 | import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' |
20 | import { | 21 | import { |
@@ -52,7 +53,6 @@ import { myVideosHistoryRouter } from './my-history' | |||
52 | import { myNotificationsRouter } from './my-notifications' | 53 | import { myNotificationsRouter } from './my-notifications' |
53 | import { mySubscriptionsRouter } from './my-subscriptions' | 54 | import { mySubscriptionsRouter } from './my-subscriptions' |
54 | import { myVideoPlaylistsRouter } from './my-video-playlists' | 55 | import { myVideoPlaylistsRouter } from './my-video-playlists' |
55 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
56 | 56 | ||
57 | const auditLogger = auditLoggerFactory('users') | 57 | const auditLogger = auditLoggerFactory('users') |
58 | 58 | ||
@@ -335,7 +335,7 @@ async function updateUser (req: express.Request, res: express.Response) { | |||
335 | const user = await userToUpdate.save() | 335 | const user = await userToUpdate.save() |
336 | 336 | ||
337 | // Destroy user token to refresh rights | 337 | // Destroy user token to refresh rights |
338 | if (roleChanged || body.password !== undefined) await deleteUserToken(userToUpdate.id) | 338 | if (roleChanged || body.password !== undefined) await OAuthTokenModel.deleteUserToken(userToUpdate.id) |
339 | 339 | ||
340 | auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) | 340 | auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) |
341 | 341 | ||
@@ -395,7 +395,7 @@ async function changeUserBlock (res: express.Response, user: MUserAccountDefault | |||
395 | user.blockedReason = reason || null | 395 | user.blockedReason = reason || null |
396 | 396 | ||
397 | await sequelizeTypescript.transaction(async t => { | 397 | await sequelizeTypescript.transaction(async t => { |
398 | await deleteUserToken(user.id, t) | 398 | await OAuthTokenModel.deleteUserToken(user.id, t) |
399 | 399 | ||
400 | await user.save({ transaction: t }) | 400 | await user.save({ transaction: t }) |
401 | }) | 401 | }) |
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 5a3e9e51a..9f9d2d77f 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts | |||
@@ -2,7 +2,7 @@ import 'multer' | |||
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger' | 3 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger' |
4 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared' | 5 | import { ActorImageType, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared' |
6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
7 | import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' | 7 | import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' |
8 | import { createReqFiles } from '../../../helpers/express-utils' | 8 | import { createReqFiles } from '../../../helpers/express-utils' |
@@ -11,7 +11,7 @@ import { CONFIG } from '../../../initializers/config' | |||
11 | import { MIMETYPES } from '../../../initializers/constants' | 11 | import { MIMETYPES } from '../../../initializers/constants' |
12 | import { sequelizeTypescript } from '../../../initializers/database' | 12 | import { sequelizeTypescript } from '../../../initializers/database' |
13 | import { sendUpdateActor } from '../../../lib/activitypub/send' | 13 | import { sendUpdateActor } from '../../../lib/activitypub/send' |
14 | import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../../lib/avatar' | 14 | import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/actor-image' |
15 | import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' | 15 | import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' |
16 | import { | 16 | import { |
17 | asyncMiddleware, | 17 | asyncMiddleware, |
@@ -25,7 +25,7 @@ import { | |||
25 | usersVideoRatingValidator | 25 | usersVideoRatingValidator |
26 | } from '../../../middlewares' | 26 | } from '../../../middlewares' |
27 | import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators' | 27 | import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators' |
28 | import { updateAvatarValidator } from '../../../middlewares/validators/avatar' | 28 | import { updateAvatarValidator } from '../../../middlewares/validators/actor-image' |
29 | import { AccountModel } from '../../../models/account/account' | 29 | import { AccountModel } from '../../../models/account/account' |
30 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 30 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
31 | import { UserModel } from '../../../models/account/user' | 31 | import { UserModel } from '../../../models/account/user' |
@@ -238,7 +238,7 @@ async function updateMyAvatar (req: express.Request, res: express.Response) { | |||
238 | 238 | ||
239 | const userAccount = await AccountModel.load(user.Account.id) | 239 | const userAccount = await AccountModel.load(user.Account.id) |
240 | 240 | ||
241 | const avatar = await updateLocalActorAvatarFile(userAccount, avatarPhysicalFile) | 241 | const avatar = await updateLocalActorImageFile(userAccount, avatarPhysicalFile, ActorImageType.AVATAR) |
242 | 242 | ||
243 | return res.json({ avatar: avatar.toFormattedJSON() }) | 243 | return res.json({ avatar: avatar.toFormattedJSON() }) |
244 | } | 244 | } |
@@ -247,7 +247,7 @@ async function deleteMyAvatar (req: express.Request, res: express.Response) { | |||
247 | const user = res.locals.oauth.token.user | 247 | const user = res.locals.oauth.token.user |
248 | 248 | ||
249 | const userAccount = await AccountModel.load(user.Account.id) | 249 | const userAccount = await AccountModel.load(user.Account.id) |
250 | await deleteLocalActorAvatarFile(userAccount) | 250 | await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR) |
251 | 251 | ||
252 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 252 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) |
253 | } | 253 | } |
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts index 5f5e4c5e6..0a9101a46 100644 --- a/server/controllers/api/users/my-notifications.ts +++ b/server/controllers/api/users/my-notifications.ts | |||
@@ -80,7 +80,9 @@ async function updateNotificationSettings (req: express.Request, res: express.Re | |||
80 | newInstanceFollower: body.newInstanceFollower, | 80 | newInstanceFollower: body.newInstanceFollower, |
81 | autoInstanceFollowing: body.autoInstanceFollowing, | 81 | autoInstanceFollowing: body.autoInstanceFollowing, |
82 | abuseNewMessage: body.abuseNewMessage, | 82 | abuseNewMessage: body.abuseNewMessage, |
83 | abuseStateChange: body.abuseStateChange | 83 | abuseStateChange: body.abuseStateChange, |
84 | newPeerTubeVersion: body.newPeerTubeVersion, | ||
85 | newPluginVersion: body.newPluginVersion | ||
84 | } | 86 | } |
85 | 87 | ||
86 | await UserNotificationSettingModel.update(values, query) | 88 | await UserNotificationSettingModel.update(values, query) |
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts index ec77ddd7a..e8949ee59 100644 --- a/server/controllers/api/users/my-subscriptions.ts +++ b/server/controllers/api/users/my-subscriptions.ts | |||
@@ -1,5 +1,8 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { sendUndoFollow } from '@server/lib/activitypub/send' | ||
4 | import { VideoChannelModel } from '@server/models/video/video-channel' | ||
5 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
3 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' | 6 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' |
4 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' | 7 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' |
5 | import { getFormattedObjects } from '../../../helpers/utils' | 8 | import { getFormattedObjects } from '../../../helpers/utils' |
@@ -26,8 +29,6 @@ import { | |||
26 | } from '../../../middlewares/validators' | 29 | } from '../../../middlewares/validators' |
27 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 30 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
28 | import { VideoModel } from '../../../models/video/video' | 31 | import { VideoModel } from '../../../models/video/video' |
29 | import { sendUndoFollow } from '@server/lib/activitypub/send' | ||
30 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
31 | 32 | ||
32 | const mySubscriptionsRouter = express.Router() | 33 | const mySubscriptionsRouter = express.Router() |
33 | 34 | ||
@@ -66,7 +67,7 @@ mySubscriptionsRouter.post('/me/subscriptions', | |||
66 | mySubscriptionsRouter.get('/me/subscriptions/:uri', | 67 | mySubscriptionsRouter.get('/me/subscriptions/:uri', |
67 | authenticate, | 68 | authenticate, |
68 | userSubscriptionGetValidator, | 69 | userSubscriptionGetValidator, |
69 | getUserSubscription | 70 | asyncMiddleware(getUserSubscription) |
70 | ) | 71 | ) |
71 | 72 | ||
72 | mySubscriptionsRouter.delete('/me/subscriptions/:uri', | 73 | mySubscriptionsRouter.delete('/me/subscriptions/:uri', |
@@ -130,10 +131,11 @@ function addUserSubscription (req: express.Request, res: express.Response) { | |||
130 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | 131 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
131 | } | 132 | } |
132 | 133 | ||
133 | function getUserSubscription (req: express.Request, res: express.Response) { | 134 | async function getUserSubscription (req: express.Request, res: express.Response) { |
134 | const subscription = res.locals.subscription | 135 | const subscription = res.locals.subscription |
136 | const videoChannel = await VideoChannelModel.loadAndPopulateAccount(subscription.ActorFollowing.VideoChannel.id) | ||
135 | 137 | ||
136 | return res.json(subscription.ActorFollowing.VideoChannel.toFormattedJSON()) | 138 | return res.json(videoChannel.toFormattedJSON()) |
137 | } | 139 | } |
138 | 140 | ||
139 | async function deleteUserSubscription (req: express.Request, res: express.Response) { | 141 | async function deleteUserSubscription (req: express.Request, res: express.Response) { |
diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts index 821429358..694bb0a92 100644 --- a/server/controllers/api/users/token.ts +++ b/server/controllers/api/users/token.ts | |||
@@ -1,11 +1,14 @@ | |||
1 | import { handleLogin, handleTokenRevocation } from '@server/lib/auth' | 1 | import * as express from 'express' |
2 | import * as RateLimit from 'express-rate-limit' | 2 | import * as RateLimit from 'express-rate-limit' |
3 | import { v4 as uuidv4 } from 'uuid' | ||
4 | import { logger } from '@server/helpers/logger' | ||
3 | import { CONFIG } from '@server/initializers/config' | 5 | import { CONFIG } from '@server/initializers/config' |
4 | import * as express from 'express' | 6 | import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth' |
7 | import { handleOAuthToken } from '@server/lib/auth/oauth' | ||
8 | import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model' | ||
5 | import { Hooks } from '@server/lib/plugins/hooks' | 9 | import { Hooks } from '@server/lib/plugins/hooks' |
6 | import { asyncMiddleware, authenticate } from '@server/middlewares' | 10 | import { asyncMiddleware, authenticate } from '@server/middlewares' |
7 | import { ScopedToken } from '@shared/models/users/user-scoped-token' | 11 | import { ScopedToken } from '@shared/models/users/user-scoped-token' |
8 | import { v4 as uuidv4 } from 'uuid' | ||
9 | 12 | ||
10 | const tokensRouter = express.Router() | 13 | const tokensRouter = express.Router() |
11 | 14 | ||
@@ -16,8 +19,7 @@ const loginRateLimiter = RateLimit({ | |||
16 | 19 | ||
17 | tokensRouter.post('/token', | 20 | tokensRouter.post('/token', |
18 | loginRateLimiter, | 21 | loginRateLimiter, |
19 | handleLogin, | 22 | asyncMiddleware(handleToken) |
20 | tokenSuccess | ||
21 | ) | 23 | ) |
22 | 24 | ||
23 | tokensRouter.post('/revoke-token', | 25 | tokensRouter.post('/revoke-token', |
@@ -42,10 +44,53 @@ export { | |||
42 | } | 44 | } |
43 | // --------------------------------------------------------------------------- | 45 | // --------------------------------------------------------------------------- |
44 | 46 | ||
45 | function tokenSuccess (req: express.Request) { | 47 | async function handleToken (req: express.Request, res: express.Response, next: express.NextFunction) { |
46 | const username = req.body.username | 48 | const grantType = req.body.grant_type |
49 | |||
50 | try { | ||
51 | const bypassLogin = await buildByPassLogin(req, grantType) | ||
52 | |||
53 | const refreshTokenAuthName = grantType === 'refresh_token' | ||
54 | ? await getAuthNameFromRefreshGrant(req.body.refresh_token) | ||
55 | : undefined | ||
56 | |||
57 | const options = { | ||
58 | refreshTokenAuthName, | ||
59 | bypassLogin | ||
60 | } | ||
61 | |||
62 | const token = await handleOAuthToken(req, options) | ||
63 | |||
64 | res.set('Cache-Control', 'no-store') | ||
65 | res.set('Pragma', 'no-cache') | ||
66 | |||
67 | Hooks.runAction('action:api.user.oauth2-got-token', { username: token.user.username, ip: req.ip }) | ||
68 | |||
69 | return res.json({ | ||
70 | token_type: 'Bearer', | ||
47 | 71 | ||
48 | Hooks.runAction('action:api.user.oauth2-got-token', { username, ip: req.ip }) | 72 | access_token: token.accessToken, |
73 | refresh_token: token.refreshToken, | ||
74 | |||
75 | expires_in: token.accessTokenExpiresIn, | ||
76 | refresh_token_expires_in: token.refreshTokenExpiresIn | ||
77 | }) | ||
78 | } catch (err) { | ||
79 | logger.warn('Login error', { err }) | ||
80 | |||
81 | return res.status(err.code || 400).json({ | ||
82 | code: err.name, | ||
83 | error: err.message | ||
84 | }) | ||
85 | } | ||
86 | } | ||
87 | |||
88 | async function handleTokenRevocation (req: express.Request, res: express.Response) { | ||
89 | const token = res.locals.oauth.token | ||
90 | |||
91 | const result = await revokeToken(token, { req, explicitLogout: true }) | ||
92 | |||
93 | return res.json(result) | ||
49 | } | 94 | } |
50 | 95 | ||
51 | function getScopedTokens (req: express.Request, res: express.Response) { | 96 | function getScopedTokens (req: express.Request, res: express.Response) { |
@@ -66,3 +111,14 @@ async function renewScopedTokens (req: express.Request, res: express.Response) { | |||
66 | feedToken: user.feedToken | 111 | feedToken: user.feedToken |
67 | } as ScopedToken) | 112 | } as ScopedToken) |
68 | } | 113 | } |
114 | |||
115 | async function buildByPassLogin (req: express.Request, grantType: string): Promise<BypassLogin> { | ||
116 | if (grantType !== 'password') return undefined | ||
117 | |||
118 | if (req.body.externalAuthToken) { | ||
119 | // Consistency with the getBypassFromPasswordGrant promise | ||
120 | return getBypassFromExternalAuth(req.body.username, req.body.externalAuthToken) | ||
121 | } | ||
122 | |||
123 | return getBypassFromPasswordGrant(req.body.username, req.body.password) | ||
124 | } | ||
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index 03617dc8d..149d6cfb4 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { Hooks } from '@server/lib/plugins/hooks' | 2 | import { Hooks } from '@server/lib/plugins/hooks' |
3 | import { getServerActor } from '@server/models/application/application' | 3 | import { getServerActor } from '@server/models/application/application' |
4 | import { MChannelAccountDefault } from '@server/types/models' | 4 | import { MChannelBannerAccountDefault } from '@server/types/models' |
5 | import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' | 5 | import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared' |
6 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
7 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' | 7 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' |
8 | import { resetSequelizeInstance } from '../../helpers/database-utils' | 8 | import { resetSequelizeInstance } from '../../helpers/database-utils' |
@@ -13,7 +13,7 @@ import { CONFIG } from '../../initializers/config' | |||
13 | import { MIMETYPES } from '../../initializers/constants' | 13 | import { MIMETYPES } from '../../initializers/constants' |
14 | import { sequelizeTypescript } from '../../initializers/database' | 14 | import { sequelizeTypescript } from '../../initializers/database' |
15 | import { sendUpdateActor } from '../../lib/activitypub/send' | 15 | import { sendUpdateActor } from '../../lib/activitypub/send' |
16 | import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../lib/avatar' | 16 | import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/actor-image' |
17 | import { JobQueue } from '../../lib/job-queue' | 17 | import { JobQueue } from '../../lib/job-queue' |
18 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' | 18 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' |
19 | import { | 19 | import { |
@@ -33,7 +33,7 @@ import { | |||
33 | videoPlaylistsSortValidator | 33 | videoPlaylistsSortValidator |
34 | } from '../../middlewares' | 34 | } from '../../middlewares' |
35 | import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators' | 35 | import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators' |
36 | import { updateAvatarValidator } from '../../middlewares/validators/avatar' | 36 | import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image' |
37 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' | 37 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' |
38 | import { AccountModel } from '../../models/account/account' | 38 | import { AccountModel } from '../../models/account/account' |
39 | import { VideoModel } from '../../models/video/video' | 39 | import { VideoModel } from '../../models/video/video' |
@@ -42,6 +42,7 @@ import { VideoPlaylistModel } from '../../models/video/video-playlist' | |||
42 | 42 | ||
43 | const auditLogger = auditLoggerFactory('channels') | 43 | const auditLogger = auditLoggerFactory('channels') |
44 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) | 44 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) |
45 | const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { bannerfile: CONFIG.STORAGE.TMP_DIR }) | ||
45 | 46 | ||
46 | const videoChannelRouter = express.Router() | 47 | const videoChannelRouter = express.Router() |
47 | 48 | ||
@@ -69,6 +70,15 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick', | |||
69 | asyncMiddleware(updateVideoChannelAvatar) | 70 | asyncMiddleware(updateVideoChannelAvatar) |
70 | ) | 71 | ) |
71 | 72 | ||
73 | videoChannelRouter.post('/:nameWithHost/banner/pick', | ||
74 | authenticate, | ||
75 | reqBannerFile, | ||
76 | // Check the rights | ||
77 | asyncMiddleware(videoChannelsUpdateValidator), | ||
78 | updateBannerValidator, | ||
79 | asyncMiddleware(updateVideoChannelBanner) | ||
80 | ) | ||
81 | |||
72 | videoChannelRouter.delete('/:nameWithHost/avatar', | 82 | videoChannelRouter.delete('/:nameWithHost/avatar', |
73 | authenticate, | 83 | authenticate, |
74 | // Check the rights | 84 | // Check the rights |
@@ -76,6 +86,13 @@ videoChannelRouter.delete('/:nameWithHost/avatar', | |||
76 | asyncMiddleware(deleteVideoChannelAvatar) | 86 | asyncMiddleware(deleteVideoChannelAvatar) |
77 | ) | 87 | ) |
78 | 88 | ||
89 | videoChannelRouter.delete('/:nameWithHost/banner', | ||
90 | authenticate, | ||
91 | // Check the rights | ||
92 | asyncMiddleware(videoChannelsUpdateValidator), | ||
93 | asyncMiddleware(deleteVideoChannelBanner) | ||
94 | ) | ||
95 | |||
79 | videoChannelRouter.put('/:nameWithHost', | 96 | videoChannelRouter.put('/:nameWithHost', |
80 | authenticate, | 97 | authenticate, |
81 | asyncMiddleware(videoChannelsUpdateValidator), | 98 | asyncMiddleware(videoChannelsUpdateValidator), |
@@ -134,26 +151,41 @@ async function listVideoChannels (req: express.Request, res: express.Response) { | |||
134 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 151 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
135 | } | 152 | } |
136 | 153 | ||
154 | async function updateVideoChannelBanner (req: express.Request, res: express.Response) { | ||
155 | const bannerPhysicalFile = req.files['bannerfile'][0] | ||
156 | const videoChannel = res.locals.videoChannel | ||
157 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) | ||
158 | |||
159 | const banner = await updateLocalActorImageFile(videoChannel, bannerPhysicalFile, ActorImageType.BANNER) | ||
160 | |||
161 | auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) | ||
162 | |||
163 | return res.json({ banner: banner.toFormattedJSON() }) | ||
164 | } | ||
137 | async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { | 165 | async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { |
138 | const avatarPhysicalFile = req.files['avatarfile'][0] | 166 | const avatarPhysicalFile = req.files['avatarfile'][0] |
139 | const videoChannel = res.locals.videoChannel | 167 | const videoChannel = res.locals.videoChannel |
140 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) | 168 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) |
141 | 169 | ||
142 | const avatar = await updateLocalActorAvatarFile(videoChannel, avatarPhysicalFile) | 170 | const avatar = await updateLocalActorImageFile(videoChannel, avatarPhysicalFile, ActorImageType.AVATAR) |
143 | 171 | ||
144 | auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) | 172 | auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) |
145 | 173 | ||
146 | return res | 174 | return res.json({ avatar: avatar.toFormattedJSON() }) |
147 | .json({ | ||
148 | avatar: avatar.toFormattedJSON() | ||
149 | }) | ||
150 | .end() | ||
151 | } | 175 | } |
152 | 176 | ||
153 | async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) { | 177 | async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) { |
154 | const videoChannel = res.locals.videoChannel | 178 | const videoChannel = res.locals.videoChannel |
155 | 179 | ||
156 | await deleteLocalActorAvatarFile(videoChannel) | 180 | await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR) |
181 | |||
182 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
183 | } | ||
184 | |||
185 | async function deleteVideoChannelBanner (req: express.Request, res: express.Response) { | ||
186 | const videoChannel = res.locals.videoChannel | ||
187 | |||
188 | await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER) | ||
157 | 189 | ||
158 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 190 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) |
159 | } | 191 | } |
@@ -177,7 +209,7 @@ async function addVideoChannel (req: express.Request, res: express.Response) { | |||
177 | videoChannel: { | 209 | videoChannel: { |
178 | id: videoChannelCreated.id | 210 | id: videoChannelCreated.id |
179 | } | 211 | } |
180 | }).end() | 212 | }) |
181 | } | 213 | } |
182 | 214 | ||
183 | async function updateVideoChannel (req: express.Request, res: express.Response) { | 215 | async function updateVideoChannel (req: express.Request, res: express.Response) { |
@@ -206,7 +238,7 @@ async function updateVideoChannel (req: express.Request, res: express.Response) | |||
206 | } | 238 | } |
207 | } | 239 | } |
208 | 240 | ||
209 | const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelAccountDefault | 241 | const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelBannerAccountDefault |
210 | await sendUpdateActor(videoChannelInstanceUpdated, t) | 242 | await sendUpdateActor(videoChannelInstanceUpdated, t) |
211 | 243 | ||
212 | auditLogger.update( | 244 | auditLogger.update( |
@@ -252,13 +284,13 @@ async function removeVideoChannel (req: express.Request, res: express.Response) | |||
252 | } | 284 | } |
253 | 285 | ||
254 | async function getVideoChannel (req: express.Request, res: express.Response) { | 286 | async function getVideoChannel (req: express.Request, res: express.Response) { |
255 | const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id) | 287 | const videoChannel = res.locals.videoChannel |
256 | 288 | ||
257 | if (videoChannelWithVideos.isOutdated()) { | 289 | if (videoChannel.isOutdated()) { |
258 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannelWithVideos.Actor.url } }) | 290 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannel.Actor.url } }) |
259 | } | 291 | } |
260 | 292 | ||
261 | return res.json(videoChannelWithVideos.toFormattedJSON()) | 293 | return res.json(videoChannel.toFormattedJSON()) |
262 | } | 294 | } |
263 | 295 | ||
264 | async function listVideoChannelPlaylists (req: express.Request, res: express.Response) { | 296 | async function listVideoChannelPlaylists (req: express.Request, res: express.Response) { |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 2447c1288..7fee278f2 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -17,7 +17,7 @@ import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../ | |||
17 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' | 17 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' |
18 | import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' | 18 | import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' |
19 | import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' | 19 | import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' |
20 | import { logger } from '../../../helpers/logger' | 20 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
21 | import { getFormattedObjects } from '../../../helpers/utils' | 21 | import { getFormattedObjects } from '../../../helpers/utils' |
22 | import { CONFIG } from '../../../initializers/config' | 22 | import { CONFIG } from '../../../initializers/config' |
23 | import { | 23 | import { |
@@ -67,6 +67,7 @@ import { ownershipVideoRouter } from './ownership' | |||
67 | import { rateVideoRouter } from './rate' | 67 | import { rateVideoRouter } from './rate' |
68 | import { watchingRouter } from './watching' | 68 | import { watchingRouter } from './watching' |
69 | 69 | ||
70 | const lTags = loggerTagsFactory('api', 'video') | ||
70 | const auditLogger = auditLoggerFactory('videos') | 71 | const auditLogger = auditLoggerFactory('videos') |
71 | const videosRouter = express.Router() | 72 | const videosRouter = express.Router() |
72 | 73 | ||
@@ -257,14 +258,14 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
257 | }) | 258 | }) |
258 | 259 | ||
259 | auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) | 260 | auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) |
260 | logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) | 261 | logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid)) |
261 | 262 | ||
262 | return { videoCreated } | 263 | return { videoCreated } |
263 | }) | 264 | }) |
264 | 265 | ||
265 | // Create the torrent file in async way because it could be long | 266 | // Create the torrent file in async way because it could be long |
266 | createTorrentAndSetInfoHashAsync(video, videoFile) | 267 | createTorrentAndSetInfoHashAsync(video, videoFile) |
267 | .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err })) | 268 | .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) })) |
268 | .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)) | 269 | .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)) |
269 | .then(refreshedVideo => { | 270 | .then(refreshedVideo => { |
270 | if (!refreshedVideo) return | 271 | if (!refreshedVideo) return |
@@ -276,7 +277,7 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
276 | return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t)) | 277 | return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t)) |
277 | }) | 278 | }) |
278 | }) | 279 | }) |
279 | .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err })) | 280 | .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) })) |
280 | 281 | ||
281 | if (video.state === VideoState.TO_TRANSCODE) { | 282 | if (video.state === VideoState.TO_TRANSCODE) { |
282 | await addOptimizeOrMergeAudioJob(videoCreated, videoFile, res.locals.oauth.token.User) | 283 | await addOptimizeOrMergeAudioJob(videoCreated, videoFile, res.locals.oauth.token.User) |
@@ -389,7 +390,7 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
389 | new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), | 390 | new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), |
390 | oldVideoAuditView | 391 | oldVideoAuditView |
391 | ) | 392 | ) |
392 | logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) | 393 | logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid, lTags(videoInstance.uuid)) |
393 | 394 | ||
394 | return videoInstanceUpdated | 395 | return videoInstanceUpdated |
395 | }) | 396 | }) |
diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts index 86adb6c69..a85d7c30b 100644 --- a/server/controllers/api/videos/ownership.ts +++ b/server/controllers/api/videos/ownership.ts | |||
@@ -107,7 +107,7 @@ async function acceptOwnership (req: express.Request, res: express.Response) { | |||
107 | // We need more attributes for federation | 107 | // We need more attributes for federation |
108 | const targetVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoChangeOwnership.Video.id) | 108 | const targetVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoChangeOwnership.Video.id) |
109 | 109 | ||
110 | const oldVideoChannel = await VideoChannelModel.loadByIdAndPopulateAccount(targetVideo.channelId) | 110 | const oldVideoChannel = await VideoChannelModel.loadAndPopulateAccount(targetVideo.channelId) |
111 | 111 | ||
112 | targetVideo.channelId = channel.id | 112 | targetVideo.channelId = channel.id |
113 | 113 | ||
diff --git a/server/controllers/client.ts b/server/controllers/client.ts index 557cbfdfb..022a17ff4 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts | |||
@@ -2,7 +2,9 @@ import * as express from 'express' | |||
2 | import { constants, promises as fs } from 'fs' | 2 | import { constants, promises as fs } from 'fs' |
3 | import { readFile } from 'fs-extra' | 3 | import { readFile } from 'fs-extra' |
4 | import { join } from 'path' | 4 | import { join } from 'path' |
5 | import { logger } from '@server/helpers/logger' | ||
5 | import { CONFIG } from '@server/initializers/config' | 6 | import { CONFIG } from '@server/initializers/config' |
7 | import { Hooks } from '@server/lib/plugins/hooks' | ||
6 | import { HttpStatusCode } from '@shared/core-utils' | 8 | import { HttpStatusCode } from '@shared/core-utils' |
7 | import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n' | 9 | import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n' |
8 | import { root } from '../helpers/core-utils' | 10 | import { root } from '../helpers/core-utils' |
@@ -27,6 +29,7 @@ const embedMiddlewares = [ | |||
27 | ? embedCSP | 29 | ? embedCSP |
28 | : (req: express.Request, res: express.Response, next: express.NextFunction) => next(), | 30 | : (req: express.Request, res: express.Response, next: express.NextFunction) => next(), |
29 | 31 | ||
32 | // Set headers | ||
30 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 33 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
31 | res.removeHeader('X-Frame-Options') | 34 | res.removeHeader('X-Frame-Options') |
32 | 35 | ||
@@ -105,6 +108,24 @@ function serveServerTranslations (req: express.Request, res: express.Response) { | |||
105 | } | 108 | } |
106 | 109 | ||
107 | async function generateEmbedHtmlPage (req: express.Request, res: express.Response) { | 110 | async function generateEmbedHtmlPage (req: express.Request, res: express.Response) { |
111 | const hookName = req.originalUrl.startsWith('/video-playlists/') | ||
112 | ? 'filter:html.embed.video-playlist.allowed.result' | ||
113 | : 'filter:html.embed.video.allowed.result' | ||
114 | |||
115 | const allowParameters = { req } | ||
116 | |||
117 | const allowedResult = await Hooks.wrapFun( | ||
118 | isEmbedAllowed, | ||
119 | allowParameters, | ||
120 | hookName | ||
121 | ) | ||
122 | |||
123 | if (!allowedResult || allowedResult.allowed !== true) { | ||
124 | logger.info('Embed is not allowed.', { allowedResult }) | ||
125 | |||
126 | return sendHTML(allowedResult?.html || '', res) | ||
127 | } | ||
128 | |||
108 | const html = await ClientHtml.getEmbedHTML() | 129 | const html = await ClientHtml.getEmbedHTML() |
109 | 130 | ||
110 | return sendHTML(html, res) | 131 | return sendHTML(html, res) |
@@ -158,3 +179,10 @@ function serveClientOverride (path: string) { | |||
158 | } | 179 | } |
159 | } | 180 | } |
160 | } | 181 | } |
182 | |||
183 | type AllowedResult = { allowed: boolean, html?: string } | ||
184 | function isEmbedAllowed (_object: { | ||
185 | req: express.Request | ||
186 | }): AllowedResult { | ||
187 | return { allowed: true } | ||
188 | } | ||
diff --git a/server/controllers/download.ts b/server/controllers/download.ts index 27caa1518..9a8194c5c 100644 --- a/server/controllers/download.ts +++ b/server/controllers/download.ts | |||
@@ -1,8 +1,10 @@ | |||
1 | import * as cors from 'cors' | 1 | import * as cors from 'cors' |
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { logger } from '@server/helpers/logger' | ||
3 | import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' | 4 | import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' |
5 | import { Hooks } from '@server/lib/plugins/hooks' | ||
4 | import { getVideoFilePath } from '@server/lib/video-paths' | 6 | import { getVideoFilePath } from '@server/lib/video-paths' |
5 | import { MVideoFile, MVideoFullLight } from '@server/types/models' | 7 | import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
6 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 8 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
7 | import { VideoStreamingPlaylistType } from '@shared/models' | 9 | import { VideoStreamingPlaylistType } from '@shared/models' |
8 | import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' | 10 | import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' |
@@ -14,19 +16,19 @@ downloadRouter.use(cors()) | |||
14 | 16 | ||
15 | downloadRouter.use( | 17 | downloadRouter.use( |
16 | STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename', | 18 | STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename', |
17 | downloadTorrent | 19 | asyncMiddleware(downloadTorrent) |
18 | ) | 20 | ) |
19 | 21 | ||
20 | downloadRouter.use( | 22 | downloadRouter.use( |
21 | STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', | 23 | STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', |
22 | asyncMiddleware(videosDownloadValidator), | 24 | asyncMiddleware(videosDownloadValidator), |
23 | downloadVideoFile | 25 | asyncMiddleware(downloadVideoFile) |
24 | ) | 26 | ) |
25 | 27 | ||
26 | downloadRouter.use( | 28 | downloadRouter.use( |
27 | STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension', | 29 | STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension', |
28 | asyncMiddleware(videosDownloadValidator), | 30 | asyncMiddleware(videosDownloadValidator), |
29 | downloadHLSVideoFile | 31 | asyncMiddleware(downloadHLSVideoFile) |
30 | ) | 32 | ) |
31 | 33 | ||
32 | // --------------------------------------------------------------------------- | 34 | // --------------------------------------------------------------------------- |
@@ -41,28 +43,58 @@ async function downloadTorrent (req: express.Request, res: express.Response) { | |||
41 | const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) | 43 | const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) |
42 | if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 44 | if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) |
43 | 45 | ||
46 | const allowParameters = { torrentPath: result.path, downloadName: result.downloadName } | ||
47 | |||
48 | const allowedResult = await Hooks.wrapFun( | ||
49 | isTorrentDownloadAllowed, | ||
50 | allowParameters, | ||
51 | 'filter:api.download.torrent.allowed.result' | ||
52 | ) | ||
53 | |||
54 | if (!checkAllowResult(res, allowParameters, allowedResult)) return | ||
55 | |||
44 | return res.download(result.path, result.downloadName) | 56 | return res.download(result.path, result.downloadName) |
45 | } | 57 | } |
46 | 58 | ||
47 | function downloadVideoFile (req: express.Request, res: express.Response) { | 59 | async function downloadVideoFile (req: express.Request, res: express.Response) { |
48 | const video = res.locals.videoAll | 60 | const video = res.locals.videoAll |
49 | 61 | ||
50 | const videoFile = getVideoFile(req, video.VideoFiles) | 62 | const videoFile = getVideoFile(req, video.VideoFiles) |
51 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() | 63 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
52 | 64 | ||
65 | const allowParameters = { video, videoFile } | ||
66 | |||
67 | const allowedResult = await Hooks.wrapFun( | ||
68 | isVideoDownloadAllowed, | ||
69 | allowParameters, | ||
70 | 'filter:api.download.video.allowed.result' | ||
71 | ) | ||
72 | |||
73 | if (!checkAllowResult(res, allowParameters, allowedResult)) return | ||
74 | |||
53 | return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`) | 75 | return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`) |
54 | } | 76 | } |
55 | 77 | ||
56 | function downloadHLSVideoFile (req: express.Request, res: express.Response) { | 78 | async function downloadHLSVideoFile (req: express.Request, res: express.Response) { |
57 | const video = res.locals.videoAll | 79 | const video = res.locals.videoAll |
58 | const playlist = getHLSPlaylist(video) | 80 | const streamingPlaylist = getHLSPlaylist(video) |
59 | if (!playlist) return res.status(HttpStatusCode.NOT_FOUND_404).end | 81 | if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end |
60 | 82 | ||
61 | const videoFile = getVideoFile(req, playlist.VideoFiles) | 83 | const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles) |
62 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() | 84 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
63 | 85 | ||
64 | const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}` | 86 | const allowParameters = { video, streamingPlaylist, videoFile } |
65 | return res.download(getVideoFilePath(playlist, videoFile), filename) | 87 | |
88 | const allowedResult = await Hooks.wrapFun( | ||
89 | isVideoDownloadAllowed, | ||
90 | allowParameters, | ||
91 | 'filter:api.download.video.allowed.result' | ||
92 | ) | ||
93 | |||
94 | if (!checkAllowResult(res, allowParameters, allowedResult)) return | ||
95 | |||
96 | const filename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}` | ||
97 | return res.download(getVideoFilePath(streamingPlaylist, videoFile), filename) | ||
66 | } | 98 | } |
67 | 99 | ||
68 | function getVideoFile (req: express.Request, files: MVideoFile[]) { | 100 | function getVideoFile (req: express.Request, files: MVideoFile[]) { |
@@ -76,3 +108,34 @@ function getHLSPlaylist (video: MVideoFullLight) { | |||
76 | 108 | ||
77 | return Object.assign(playlist, { Video: video }) | 109 | return Object.assign(playlist, { Video: video }) |
78 | } | 110 | } |
111 | |||
112 | type AllowedResult = { | ||
113 | allowed: boolean | ||
114 | errorMessage?: string | ||
115 | } | ||
116 | |||
117 | function isTorrentDownloadAllowed (_object: { | ||
118 | torrentPath: string | ||
119 | }): AllowedResult { | ||
120 | return { allowed: true } | ||
121 | } | ||
122 | |||
123 | function isVideoDownloadAllowed (_object: { | ||
124 | video: MVideo | ||
125 | videoFile: MVideoFile | ||
126 | streamingPlaylist?: MStreamingPlaylist | ||
127 | }): AllowedResult { | ||
128 | return { allowed: true } | ||
129 | } | ||
130 | |||
131 | function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) { | ||
132 | if (!result || result.allowed !== true) { | ||
133 | logger.info('Download is not allowed.', { result, allowParameters }) | ||
134 | res.status(HttpStatusCode.FORBIDDEN_403) | ||
135 | .json({ error: result?.errorMessage || 'Refused download' }) | ||
136 | |||
137 | return false | ||
138 | } | ||
139 | |||
140 | return true | ||
141 | } | ||
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index e29a8fe1d..921067e65 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import * as Feed from 'pfeed' | 2 | import * as Feed from 'pfeed' |
3 | import { VideoFilter } from '../../shared/models/videos/video-query.type' | ||
3 | import { buildNSFWFilter } from '../helpers/express-utils' | 4 | import { buildNSFWFilter } from '../helpers/express-utils' |
4 | import { CONFIG } from '../initializers/config' | 5 | import { CONFIG } from '../initializers/config' |
5 | import { FEEDS, ROUTE_CACHE_LIFETIME, THUMBNAILS_SIZE, WEBSERVER } from '../initializers/constants' | 6 | import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' |
6 | import { | 7 | import { |
7 | asyncMiddleware, | 8 | asyncMiddleware, |
8 | commonVideosFiltersValidator, | 9 | commonVideosFiltersValidator, |
@@ -17,7 +18,6 @@ import { | |||
17 | import { cacheRoute } from '../middlewares/cache' | 18 | import { cacheRoute } from '../middlewares/cache' |
18 | import { VideoModel } from '../models/video/video' | 19 | import { VideoModel } from '../models/video/video' |
19 | import { VideoCommentModel } from '../models/video/video-comment' | 20 | import { VideoCommentModel } from '../models/video/video-comment' |
20 | import { VideoFilter } from '../../shared/models/videos/video-query.type' | ||
21 | 21 | ||
22 | const feedsRouter = express.Router() | 22 | const feedsRouter = express.Router() |
23 | 23 | ||
@@ -318,9 +318,9 @@ function addVideosToFeed (feed, videos: VideoModel[]) { | |||
318 | }, | 318 | }, |
319 | thumbnail: [ | 319 | thumbnail: [ |
320 | { | 320 | { |
321 | url: WEBSERVER.URL + video.getMiniatureStaticPath(), | 321 | url: WEBSERVER.URL + video.getPreviewStaticPath(), |
322 | height: THUMBNAILS_SIZE.height, | 322 | height: PREVIEWS_SIZE.height, |
323 | width: THUMBNAILS_SIZE.width | 323 | width: PREVIEWS_SIZE.width |
324 | } | 324 | } |
325 | ] | 325 | ] |
326 | }) | 326 | }) |
diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts index 4e553479b..6f71fdb16 100644 --- a/server/controllers/lazy-static.ts +++ b/server/controllers/lazy-static.ts | |||
@@ -4,10 +4,10 @@ import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache | |||
4 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 4 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
5 | import { logger } from '../helpers/logger' | 5 | import { logger } from '../helpers/logger' |
6 | import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' | 6 | import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' |
7 | import { avatarPathUnsafeCache, pushAvatarProcessInQueue } from '../lib/avatar' | 7 | import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/actor-image' |
8 | import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' | 8 | import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' |
9 | import { asyncMiddleware } from '../middlewares' | 9 | import { asyncMiddleware } from '../middlewares' |
10 | import { AvatarModel } from '../models/avatar/avatar' | 10 | import { ActorImageModel } from '../models/account/actor-image' |
11 | 11 | ||
12 | const lazyStaticRouter = express.Router() | 12 | const lazyStaticRouter = express.Router() |
13 | 13 | ||
@@ -15,7 +15,12 @@ lazyStaticRouter.use(cors()) | |||
15 | 15 | ||
16 | lazyStaticRouter.use( | 16 | lazyStaticRouter.use( |
17 | LAZY_STATIC_PATHS.AVATARS + ':filename', | 17 | LAZY_STATIC_PATHS.AVATARS + ':filename', |
18 | asyncMiddleware(getAvatar) | 18 | asyncMiddleware(getActorImage) |
19 | ) | ||
20 | |||
21 | lazyStaticRouter.use( | ||
22 | LAZY_STATIC_PATHS.BANNERS + ':filename', | ||
23 | asyncMiddleware(getActorImage) | ||
19 | ) | 24 | ) |
20 | 25 | ||
21 | lazyStaticRouter.use( | 26 | lazyStaticRouter.use( |
@@ -43,36 +48,36 @@ export { | |||
43 | 48 | ||
44 | // --------------------------------------------------------------------------- | 49 | // --------------------------------------------------------------------------- |
45 | 50 | ||
46 | async function getAvatar (req: express.Request, res: express.Response) { | 51 | async function getActorImage (req: express.Request, res: express.Response) { |
47 | const filename = req.params.filename | 52 | const filename = req.params.filename |
48 | 53 | ||
49 | if (avatarPathUnsafeCache.has(filename)) { | 54 | if (actorImagePathUnsafeCache.has(filename)) { |
50 | return res.sendFile(avatarPathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER }) | 55 | return res.sendFile(actorImagePathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER }) |
51 | } | 56 | } |
52 | 57 | ||
53 | const avatar = await AvatarModel.loadByName(filename) | 58 | const image = await ActorImageModel.loadByName(filename) |
54 | if (!avatar) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 59 | if (!image) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) |
55 | 60 | ||
56 | if (avatar.onDisk === false) { | 61 | if (image.onDisk === false) { |
57 | if (!avatar.fileUrl) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 62 | if (!image.fileUrl) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) |
58 | 63 | ||
59 | logger.info('Lazy serve remote avatar image %s.', avatar.fileUrl) | 64 | logger.info('Lazy serve remote actor image %s.', image.fileUrl) |
60 | 65 | ||
61 | try { | 66 | try { |
62 | await pushAvatarProcessInQueue({ filename: avatar.filename, fileUrl: avatar.fileUrl }) | 67 | await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type }) |
63 | } catch (err) { | 68 | } catch (err) { |
64 | logger.warn('Cannot process remote avatar %s.', avatar.fileUrl, { err }) | 69 | logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err }) |
65 | return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 70 | return res.sendStatus(HttpStatusCode.NOT_FOUND_404) |
66 | } | 71 | } |
67 | 72 | ||
68 | avatar.onDisk = true | 73 | image.onDisk = true |
69 | avatar.save() | 74 | image.save() |
70 | .catch(err => logger.error('Cannot save new avatar disk state.', { err })) | 75 | .catch(err => logger.error('Cannot save new actor image disk state.', { err })) |
71 | } | 76 | } |
72 | 77 | ||
73 | const path = avatar.getPath() | 78 | const path = image.getPath() |
74 | 79 | ||
75 | avatarPathUnsafeCache.set(filename, path) | 80 | actorImagePathUnsafeCache.set(filename, path) |
76 | return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) | 81 | return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) |
77 | } | 82 | } |
78 | 83 | ||
diff --git a/server/controllers/plugins.ts b/server/controllers/plugins.ts index 6a1ccc0bf..105f51518 100644 --- a/server/controllers/plugins.ts +++ b/server/controllers/plugins.ts | |||
@@ -1,15 +1,15 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' | ||
3 | import { join } from 'path' | 2 | import { join } from 'path' |
4 | import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager' | 3 | import { logger } from '@server/helpers/logger' |
5 | import { getPluginValidator, pluginStaticDirectoryValidator, getExternalAuthValidator } from '../middlewares/validators/plugins' | 4 | import { optionalAuthenticate } from '@server/middlewares/auth' |
6 | import { serveThemeCSSValidator } from '../middlewares/validators/themes' | ||
7 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | ||
8 | import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n' | 5 | import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n' |
6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | ||
9 | import { PluginType } from '../../shared/models/plugins/plugin.type' | 7 | import { PluginType } from '../../shared/models/plugins/plugin.type' |
10 | import { isTestInstance } from '../helpers/core-utils' | 8 | import { isTestInstance } from '../helpers/core-utils' |
11 | import { logger } from '@server/helpers/logger' | 9 | import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' |
12 | import { optionalAuthenticate } from '@server/middlewares/oauth' | 10 | import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager' |
11 | import { getExternalAuthValidator, getPluginValidator, pluginStaticDirectoryValidator } from '../middlewares/validators/plugins' | ||
12 | import { serveThemeCSSValidator } from '../middlewares/validators/themes' | ||
13 | 13 | ||
14 | const sendFileOptions = { | 14 | const sendFileOptions = { |
15 | maxAge: '30 days', | 15 | maxAge: '30 days', |
diff --git a/server/controllers/services.ts b/server/controllers/services.ts index d0217c30a..189e1651b 100644 --- a/server/controllers/services.ts +++ b/server/controllers/services.ts | |||
@@ -3,6 +3,7 @@ import { EMBED_SIZE, PREVIEWS_SIZE, WEBSERVER, THUMBNAILS_SIZE } from '../initia | |||
3 | import { asyncMiddleware, oembedValidator } from '../middlewares' | 3 | import { asyncMiddleware, oembedValidator } from '../middlewares' |
4 | import { accountNameWithHostGetValidator } from '../middlewares/validators' | 4 | import { accountNameWithHostGetValidator } from '../middlewares/validators' |
5 | import { MChannelSummary } from '@server/types/models' | 5 | import { MChannelSummary } from '@server/types/models' |
6 | import { escapeHTML } from '@shared/core-utils/renderer' | ||
6 | 7 | ||
7 | const servicesRouter = express.Router() | 8 | const servicesRouter = express.Router() |
8 | 9 | ||
@@ -79,6 +80,7 @@ function buildOEmbed (options: { | |||
79 | const embedUrl = webserverUrl + embedPath | 80 | const embedUrl = webserverUrl + embedPath |
80 | let embedWidth = EMBED_SIZE.width | 81 | let embedWidth = EMBED_SIZE.width |
81 | let embedHeight = EMBED_SIZE.height | 82 | let embedHeight = EMBED_SIZE.height |
83 | const embedTitle = escapeHTML(title) | ||
82 | 84 | ||
83 | let thumbnailUrl = previewPath | 85 | let thumbnailUrl = previewPath |
84 | ? webserverUrl + previewPath | 86 | ? webserverUrl + previewPath |
@@ -96,7 +98,7 @@ function buildOEmbed (options: { | |||
96 | } | 98 | } |
97 | 99 | ||
98 | const html = `<iframe width="${embedWidth}" height="${embedHeight}" sandbox="allow-same-origin allow-scripts" ` + | 100 | const html = `<iframe width="${embedWidth}" height="${embedHeight}" sandbox="allow-same-origin allow-scripts" ` + |
99 | `src="${embedUrl}" frameborder="0" allowfullscreen></iframe>` | 101 | `title="${embedTitle}" src="${embedUrl}" frameborder="0" allowfullscreen></iframe>` |
100 | 102 | ||
101 | const json: any = { | 103 | const json: any = { |
102 | type: 'video', | 104 | type: 'video', |
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 4baa31117..e6a0628e6 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -252,9 +252,9 @@ async function generateNodeinfo (req: express.Request, res: express.Response) { | |||
252 | avatar: { | 252 | avatar: { |
253 | file: { | 253 | file: { |
254 | size: { | 254 | size: { |
255 | max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max | 255 | max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max |
256 | }, | 256 | }, |
257 | extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME | 257 | extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME |
258 | } | 258 | } |
259 | }, | 259 | }, |
260 | video: { | 260 | video: { |
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index 08aef2908..e0754b501 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts | |||
@@ -3,7 +3,6 @@ import { URL } from 'url' | |||
3 | import validator from 'validator' | 3 | import validator from 'validator' |
4 | import { ContextType } from '@shared/models/activitypub/context' | 4 | import { ContextType } from '@shared/models/activitypub/context' |
5 | import { ResultList } from '../../shared/models' | 5 | import { ResultList } from '../../shared/models' |
6 | import { Activity } from '../../shared/models/activitypub' | ||
7 | import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants' | 6 | import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants' |
8 | import { MActor, MVideoWithHost } from '../types/models' | 7 | import { MActor, MVideoWithHost } from '../types/models' |
9 | import { pageToStartAndCount } from './core-utils' | 8 | import { pageToStartAndCount } from './core-utils' |
@@ -182,10 +181,10 @@ async function activityPubCollectionPagination ( | |||
182 | 181 | ||
183 | } | 182 | } |
184 | 183 | ||
185 | function buildSignedActivity (byActor: MActor, data: Object, contextType?: ContextType) { | 184 | function buildSignedActivity <T> (byActor: MActor, data: T, contextType?: ContextType) { |
186 | const activity = activityPubContextify(data, contextType) | 185 | const activity = activityPubContextify(data, contextType) |
187 | 186 | ||
188 | return signJsonLDObject(byActor, activity) as Promise<Activity> | 187 | return signJsonLDObject(byActor, activity) |
189 | } | 188 | } |
190 | 189 | ||
191 | function getAPId (activity: string | { id: string }) { | 190 | function getAPId (activity: string | { id: string }) { |
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index 935fd22d9..b93868c12 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts | |||
@@ -10,7 +10,9 @@ import { BinaryToTextEncoding, createHash, randomBytes } from 'crypto' | |||
10 | import { truncate } from 'lodash' | 10 | import { truncate } from 'lodash' |
11 | import { basename, isAbsolute, join, resolve } from 'path' | 11 | import { basename, isAbsolute, join, resolve } from 'path' |
12 | import * as pem from 'pem' | 12 | import * as pem from 'pem' |
13 | import { pipeline } from 'stream' | ||
13 | import { URL } from 'url' | 14 | import { URL } from 'url' |
15 | import { promisify } from 'util' | ||
14 | 16 | ||
15 | const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => { | 17 | const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => { |
16 | if (!oldObject || typeof oldObject !== 'object') { | 18 | if (!oldObject || typeof oldObject !== 'object') { |
@@ -152,24 +154,6 @@ function root () { | |||
152 | return rootPath | 154 | return rootPath |
153 | } | 155 | } |
154 | 156 | ||
155 | // Thanks: https://stackoverflow.com/a/12034334 | ||
156 | function escapeHTML (stringParam) { | ||
157 | if (!stringParam) return '' | ||
158 | |||
159 | const entityMap = { | ||
160 | '&': '&', | ||
161 | '<': '<', | ||
162 | '>': '>', | ||
163 | '"': '"', | ||
164 | '\'': ''', | ||
165 | '/': '/', | ||
166 | '`': '`', | ||
167 | '=': '=' | ||
168 | } | ||
169 | |||
170 | return String(stringParam).replace(/[&<>"'`=/]/g, s => entityMap[s]) | ||
171 | } | ||
172 | |||
173 | function pageToStartAndCount (page: number, itemsPerPage: number) { | 157 | function pageToStartAndCount (page: number, itemsPerPage: number) { |
174 | const start = (page - 1) * itemsPerPage | 158 | const start = (page - 1) * itemsPerPage |
175 | 159 | ||
@@ -249,11 +233,23 @@ function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) | |||
249 | } | 233 | } |
250 | } | 234 | } |
251 | 235 | ||
236 | type SemVersion = { major: number, minor: number, patch: number } | ||
237 | function parseSemVersion (s: string) { | ||
238 | const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i) | ||
239 | |||
240 | return { | ||
241 | major: parseInt(parsed[1]), | ||
242 | minor: parseInt(parsed[2]), | ||
243 | patch: parseInt(parsed[3]) | ||
244 | } as SemVersion | ||
245 | } | ||
246 | |||
252 | const randomBytesPromise = promisify1<number, Buffer>(randomBytes) | 247 | const randomBytesPromise = promisify1<number, Buffer>(randomBytes) |
253 | const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey) | 248 | const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey) |
254 | const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey) | 249 | const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey) |
255 | const execPromise2 = promisify2<string, any, string>(exec) | 250 | const execPromise2 = promisify2<string, any, string>(exec) |
256 | const execPromise = promisify1<string, string>(exec) | 251 | const execPromise = promisify1<string, string>(exec) |
252 | const pipelinePromise = promisify(pipeline) | ||
257 | 253 | ||
258 | // --------------------------------------------------------------------------- | 254 | // --------------------------------------------------------------------------- |
259 | 255 | ||
@@ -264,7 +260,6 @@ export { | |||
264 | 260 | ||
265 | objectConverter, | 261 | objectConverter, |
266 | root, | 262 | root, |
267 | escapeHTML, | ||
268 | pageToStartAndCount, | 263 | pageToStartAndCount, |
269 | sanitizeUrl, | 264 | sanitizeUrl, |
270 | sanitizeHost, | 265 | sanitizeHost, |
@@ -284,5 +279,8 @@ export { | |||
284 | createPrivateKey, | 279 | createPrivateKey, |
285 | getPublicKey, | 280 | getPublicKey, |
286 | execPromise2, | 281 | execPromise2, |
287 | execPromise | 282 | execPromise, |
283 | pipelinePromise, | ||
284 | |||
285 | parseSemVersion | ||
288 | } | 286 | } |
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts index da79b2782..b5c96f6e7 100644 --- a/server/helpers/custom-validators/activitypub/activity.ts +++ b/server/helpers/custom-validators/activitypub/activity.ts | |||
@@ -1,16 +1,13 @@ | |||
1 | import validator from 'validator' | 1 | import validator from 'validator' |
2 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' | 2 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' |
3 | import { isAbuseReasonValid } from '../abuses' | ||
3 | import { exists } from '../misc' | 4 | import { exists } from '../misc' |
4 | import { sanitizeAndCheckActorObject } from './actor' | 5 | import { sanitizeAndCheckActorObject } from './actor' |
5 | import { isCacheFileObjectValid } from './cache-file' | 6 | import { isCacheFileObjectValid } from './cache-file' |
6 | import { isFlagActivityValid } from './flag' | ||
7 | import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc' | 7 | import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc' |
8 | import { isPlaylistObjectValid } from './playlist' | 8 | import { isPlaylistObjectValid } from './playlist' |
9 | import { isDislikeActivityValid, isLikeActivityValid } from './rate' | ||
10 | import { isShareActivityValid } from './share' | ||
11 | import { sanitizeAndCheckVideoCommentObject } from './video-comments' | 9 | import { sanitizeAndCheckVideoCommentObject } from './video-comments' |
12 | import { sanitizeAndCheckVideoTorrentObject } from './videos' | 10 | import { sanitizeAndCheckVideoTorrentObject } from './videos' |
13 | import { isViewActivityValid } from './view' | ||
14 | 11 | ||
15 | function isRootActivityValid (activity: any) { | 12 | function isRootActivityValid (activity: any) { |
16 | return isCollection(activity) || isActivity(activity) | 13 | return isCollection(activity) || isActivity(activity) |
@@ -29,18 +26,18 @@ function isActivity (activity: any) { | |||
29 | } | 26 | } |
30 | 27 | ||
31 | const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = { | 28 | const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = { |
32 | Create: checkCreateActivity, | 29 | Create: isCreateActivityValid, |
33 | Update: checkUpdateActivity, | 30 | Update: isUpdateActivityValid, |
34 | Delete: checkDeleteActivity, | 31 | Delete: isDeleteActivityValid, |
35 | Follow: checkFollowActivity, | 32 | Follow: isFollowActivityValid, |
36 | Accept: checkAcceptActivity, | 33 | Accept: isAcceptActivityValid, |
37 | Reject: checkRejectActivity, | 34 | Reject: isRejectActivityValid, |
38 | Announce: checkAnnounceActivity, | 35 | Announce: isAnnounceActivityValid, |
39 | Undo: checkUndoActivity, | 36 | Undo: isUndoActivityValid, |
40 | Like: checkLikeActivity, | 37 | Like: isLikeActivityValid, |
41 | View: checkViewActivity, | 38 | View: isViewActivityValid, |
42 | Flag: checkFlagActivity, | 39 | Flag: isFlagActivityValid, |
43 | Dislike: checkDislikeActivity | 40 | Dislike: isDislikeActivityValid |
44 | } | 41 | } |
45 | 42 | ||
46 | function isActivityValid (activity: any) { | 43 | function isActivityValid (activity: any) { |
@@ -51,34 +48,34 @@ function isActivityValid (activity: any) { | |||
51 | return checker(activity) | 48 | return checker(activity) |
52 | } | 49 | } |
53 | 50 | ||
54 | // --------------------------------------------------------------------------- | 51 | function isFlagActivityValid (activity: any) { |
55 | 52 | return isBaseActivityValid(activity, 'Flag') && | |
56 | export { | 53 | isAbuseReasonValid(activity.content) && |
57 | isRootActivityValid, | 54 | isActivityPubUrlValid(activity.object) |
58 | isActivityValid | ||
59 | } | 55 | } |
60 | 56 | ||
61 | // --------------------------------------------------------------------------- | 57 | function isLikeActivityValid (activity: any) { |
62 | 58 | return isBaseActivityValid(activity, 'Like') && | |
63 | function checkViewActivity (activity: any) { | 59 | isObjectValid(activity.object) |
64 | return isBaseActivityValid(activity, 'View') && | ||
65 | isViewActivityValid(activity) | ||
66 | } | 60 | } |
67 | 61 | ||
68 | function checkFlagActivity (activity: any) { | 62 | function isDislikeActivityValid (activity: any) { |
69 | return isBaseActivityValid(activity, 'Flag') && | 63 | return isBaseActivityValid(activity, 'Dislike') && |
70 | isFlagActivityValid(activity) | 64 | isObjectValid(activity.object) |
71 | } | 65 | } |
72 | 66 | ||
73 | function checkDislikeActivity (activity: any) { | 67 | function isAnnounceActivityValid (activity: any) { |
74 | return isDislikeActivityValid(activity) | 68 | return isBaseActivityValid(activity, 'Announce') && |
69 | isObjectValid(activity.object) | ||
75 | } | 70 | } |
76 | 71 | ||
77 | function checkLikeActivity (activity: any) { | 72 | function isViewActivityValid (activity: any) { |
78 | return isLikeActivityValid(activity) | 73 | return isBaseActivityValid(activity, 'View') && |
74 | isActivityPubUrlValid(activity.actor) && | ||
75 | isActivityPubUrlValid(activity.object) | ||
79 | } | 76 | } |
80 | 77 | ||
81 | function checkCreateActivity (activity: any) { | 78 | function isCreateActivityValid (activity: any) { |
82 | return isBaseActivityValid(activity, 'Create') && | 79 | return isBaseActivityValid(activity, 'Create') && |
83 | ( | 80 | ( |
84 | isViewActivityValid(activity.object) || | 81 | isViewActivityValid(activity.object) || |
@@ -92,7 +89,7 @@ function checkCreateActivity (activity: any) { | |||
92 | ) | 89 | ) |
93 | } | 90 | } |
94 | 91 | ||
95 | function checkUpdateActivity (activity: any) { | 92 | function isUpdateActivityValid (activity: any) { |
96 | return isBaseActivityValid(activity, 'Update') && | 93 | return isBaseActivityValid(activity, 'Update') && |
97 | ( | 94 | ( |
98 | isCacheFileObjectValid(activity.object) || | 95 | isCacheFileObjectValid(activity.object) || |
@@ -102,36 +99,51 @@ function checkUpdateActivity (activity: any) { | |||
102 | ) | 99 | ) |
103 | } | 100 | } |
104 | 101 | ||
105 | function checkDeleteActivity (activity: any) { | 102 | function isDeleteActivityValid (activity: any) { |
106 | // We don't really check objects | 103 | // We don't really check objects |
107 | return isBaseActivityValid(activity, 'Delete') && | 104 | return isBaseActivityValid(activity, 'Delete') && |
108 | isObjectValid(activity.object) | 105 | isObjectValid(activity.object) |
109 | } | 106 | } |
110 | 107 | ||
111 | function checkFollowActivity (activity: any) { | 108 | function isFollowActivityValid (activity: any) { |
112 | return isBaseActivityValid(activity, 'Follow') && | 109 | return isBaseActivityValid(activity, 'Follow') && |
113 | isObjectValid(activity.object) | 110 | isObjectValid(activity.object) |
114 | } | 111 | } |
115 | 112 | ||
116 | function checkAcceptActivity (activity: any) { | 113 | function isAcceptActivityValid (activity: any) { |
117 | return isBaseActivityValid(activity, 'Accept') | 114 | return isBaseActivityValid(activity, 'Accept') |
118 | } | 115 | } |
119 | 116 | ||
120 | function checkRejectActivity (activity: any) { | 117 | function isRejectActivityValid (activity: any) { |
121 | return isBaseActivityValid(activity, 'Reject') | 118 | return isBaseActivityValid(activity, 'Reject') |
122 | } | 119 | } |
123 | 120 | ||
124 | function checkAnnounceActivity (activity: any) { | 121 | function isUndoActivityValid (activity: any) { |
125 | return isShareActivityValid(activity) | ||
126 | } | ||
127 | |||
128 | function checkUndoActivity (activity: any) { | ||
129 | return isBaseActivityValid(activity, 'Undo') && | 122 | return isBaseActivityValid(activity, 'Undo') && |
130 | ( | 123 | ( |
131 | checkFollowActivity(activity.object) || | 124 | isFollowActivityValid(activity.object) || |
132 | checkLikeActivity(activity.object) || | 125 | isLikeActivityValid(activity.object) || |
133 | checkDislikeActivity(activity.object) || | 126 | isDislikeActivityValid(activity.object) || |
134 | checkAnnounceActivity(activity.object) || | 127 | isAnnounceActivityValid(activity.object) || |
135 | checkCreateActivity(activity.object) | 128 | isCreateActivityValid(activity.object) |
136 | ) | 129 | ) |
137 | } | 130 | } |
131 | |||
132 | // --------------------------------------------------------------------------- | ||
133 | |||
134 | export { | ||
135 | isRootActivityValid, | ||
136 | isActivityValid, | ||
137 | isFlagActivityValid, | ||
138 | isLikeActivityValid, | ||
139 | isDislikeActivityValid, | ||
140 | isAnnounceActivityValid, | ||
141 | isViewActivityValid, | ||
142 | isCreateActivityValid, | ||
143 | isUpdateActivityValid, | ||
144 | isDeleteActivityValid, | ||
145 | isFollowActivityValid, | ||
146 | isAcceptActivityValid, | ||
147 | isRejectActivityValid, | ||
148 | isUndoActivityValid | ||
149 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/flag.ts b/server/helpers/custom-validators/activitypub/flag.ts deleted file mode 100644 index dc90b3667..000000000 --- a/server/helpers/custom-validators/activitypub/flag.ts +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | import { isActivityPubUrlValid } from './misc' | ||
2 | import { isAbuseReasonValid } from '../abuses' | ||
3 | |||
4 | function isFlagActivityValid (activity: any) { | ||
5 | return activity.type === 'Flag' && | ||
6 | isAbuseReasonValid(activity.content) && | ||
7 | isActivityPubUrlValid(activity.object) | ||
8 | } | ||
9 | |||
10 | // --------------------------------------------------------------------------- | ||
11 | |||
12 | export { | ||
13 | isFlagActivityValid | ||
14 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/rate.ts b/server/helpers/custom-validators/activitypub/rate.ts deleted file mode 100644 index aafdda443..000000000 --- a/server/helpers/custom-validators/activitypub/rate.ts +++ /dev/null | |||
@@ -1,18 +0,0 @@ | |||
1 | import { isBaseActivityValid, isObjectValid } from './misc' | ||
2 | |||
3 | function isLikeActivityValid (activity: any) { | ||
4 | return isBaseActivityValid(activity, 'Like') && | ||
5 | isObjectValid(activity.object) | ||
6 | } | ||
7 | |||
8 | function isDislikeActivityValid (activity: any) { | ||
9 | return isBaseActivityValid(activity, 'Dislike') && | ||
10 | isObjectValid(activity.object) | ||
11 | } | ||
12 | |||
13 | // --------------------------------------------------------------------------- | ||
14 | |||
15 | export { | ||
16 | isDislikeActivityValid, | ||
17 | isLikeActivityValid | ||
18 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/share.ts b/server/helpers/custom-validators/activitypub/share.ts deleted file mode 100644 index fb5e4c05e..000000000 --- a/server/helpers/custom-validators/activitypub/share.ts +++ /dev/null | |||
@@ -1,11 +0,0 @@ | |||
1 | import { isBaseActivityValid, isObjectValid } from './misc' | ||
2 | |||
3 | function isShareActivityValid (activity: any) { | ||
4 | return isBaseActivityValid(activity, 'Announce') && | ||
5 | isObjectValid(activity.object) | ||
6 | } | ||
7 | // --------------------------------------------------------------------------- | ||
8 | |||
9 | export { | ||
10 | isShareActivityValid | ||
11 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/view.ts b/server/helpers/custom-validators/activitypub/view.ts deleted file mode 100644 index 41d16469f..000000000 --- a/server/helpers/custom-validators/activitypub/view.ts +++ /dev/null | |||
@@ -1,13 +0,0 @@ | |||
1 | import { isActivityPubUrlValid } from './misc' | ||
2 | |||
3 | function isViewActivityValid (activity: any) { | ||
4 | return activity.type === 'View' && | ||
5 | isActivityPubUrlValid(activity.actor) && | ||
6 | isActivityPubUrlValid(activity.object) | ||
7 | } | ||
8 | |||
9 | // --------------------------------------------------------------------------- | ||
10 | |||
11 | export { | ||
12 | isViewActivityValid | ||
13 | } | ||
diff --git a/server/helpers/custom-validators/actor-images.ts b/server/helpers/custom-validators/actor-images.ts new file mode 100644 index 000000000..4fb0b7c70 --- /dev/null +++ b/server/helpers/custom-validators/actor-images.ts | |||
@@ -0,0 +1,17 @@ | |||
1 | |||
2 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | ||
3 | import { isFileValid } from './misc' | ||
4 | |||
5 | const imageMimeTypes = CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME | ||
6 | .map(v => v.replace('.', '')) | ||
7 | .join('|') | ||
8 | const imageMimeTypesRegex = `image/(${imageMimeTypes})` | ||
9 | function isActorImageFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], fieldname: string) { | ||
10 | return isFileValid(files, imageMimeTypesRegex, fieldname, CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max) | ||
11 | } | ||
12 | |||
13 | // --------------------------------------------------------------------------- | ||
14 | |||
15 | export { | ||
16 | isActorImageFile | ||
17 | } | ||
diff --git a/server/helpers/custom-validators/user-notifications.ts b/server/helpers/custom-validators/user-notifications.ts index 8a33b895b..252c107db 100644 --- a/server/helpers/custom-validators/user-notifications.ts +++ b/server/helpers/custom-validators/user-notifications.ts | |||
@@ -1,10 +1,9 @@ | |||
1 | import { exists } from './misc' | ||
2 | import validator from 'validator' | 1 | import validator from 'validator' |
3 | import { UserNotificationType } from '../../../shared/models/users' | ||
4 | import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | 2 | import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' |
3 | import { exists } from './misc' | ||
5 | 4 | ||
6 | function isUserNotificationTypeValid (value: any) { | 5 | function isUserNotificationTypeValid (value: any) { |
7 | return exists(value) && validator.isInt('' + value) && UserNotificationType[value] !== undefined | 6 | return exists(value) && validator.isInt('' + value) |
8 | } | 7 | } |
9 | 8 | ||
10 | function isUserNotificationSettingValid (value: any) { | 9 | function isUserNotificationSettingValid (value: any) { |
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts index d6e91ad35..5b21c3529 100644 --- a/server/helpers/custom-validators/users.ts +++ b/server/helpers/custom-validators/users.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import { values } from 'lodash' | ||
1 | import validator from 'validator' | 2 | import validator from 'validator' |
2 | import { UserRole } from '../../../shared' | 3 | import { UserRole } from '../../../shared' |
3 | import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants' | ||
4 | import { exists, isArray, isBooleanValid, isFileValid } from './misc' | ||
5 | import { values } from 'lodash' | ||
6 | import { isEmailEnabled } from '../../initializers/config' | 4 | import { isEmailEnabled } from '../../initializers/config' |
5 | import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants' | ||
6 | import { exists, isArray, isBooleanValid } from './misc' | ||
7 | 7 | ||
8 | const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS | 8 | const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS |
9 | 9 | ||
@@ -97,14 +97,6 @@ function isUserRoleValid (value: any) { | |||
97 | return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined | 97 | return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined |
98 | } | 98 | } |
99 | 99 | ||
100 | const avatarMimeTypes = CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME | ||
101 | .map(v => v.replace('.', '')) | ||
102 | .join('|') | ||
103 | const avatarMimeTypesRegex = `image/(${avatarMimeTypes})` | ||
104 | function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { | ||
105 | return isFileValid(files, avatarMimeTypesRegex, 'avatarfile', CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max) | ||
106 | } | ||
107 | |||
108 | // --------------------------------------------------------------------------- | 100 | // --------------------------------------------------------------------------- |
109 | 101 | ||
110 | export { | 102 | export { |
@@ -128,6 +120,5 @@ export { | |||
128 | isUserDisplayNameValid, | 120 | isUserDisplayNameValid, |
129 | isUserDescriptionValid, | 121 | isUserDescriptionValid, |
130 | isNoInstanceConfigWarningModal, | 122 | isNoInstanceConfigWarningModal, |
131 | isNoWelcomeModal, | 123 | isNoWelcomeModal |
132 | isAvatarFile | ||
133 | } | 124 | } |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 620025966..01c3aa5f7 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -5,7 +5,7 @@ import { dirname, join } from 'path' | |||
5 | import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants' | 5 | import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants' |
6 | import { AvailableEncoders, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos' | 6 | import { AvailableEncoders, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos' |
7 | import { CONFIG } from '../initializers/config' | 7 | import { CONFIG } from '../initializers/config' |
8 | import { promisify0 } from './core-utils' | 8 | import { execPromise, promisify0 } from './core-utils' |
9 | import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' | 9 | import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' |
10 | import { processImage } from './image-utils' | 10 | import { processImage } from './image-utils' |
11 | import { logger } from './logger' | 11 | import { logger } from './logger' |
@@ -649,6 +649,24 @@ function getFFmpeg (input: string, type: 'live' | 'vod') { | |||
649 | return command | 649 | return command |
650 | } | 650 | } |
651 | 651 | ||
652 | function getFFmpegVersion () { | ||
653 | return new Promise<string>((res, rej) => { | ||
654 | (ffmpeg() as any)._getFfmpegPath((err, ffmpegPath) => { | ||
655 | if (err) return rej(err) | ||
656 | if (!ffmpegPath) return rej(new Error('Could not find ffmpeg path')) | ||
657 | |||
658 | return execPromise(`${ffmpegPath} -version`) | ||
659 | .then(stdout => { | ||
660 | const parsed = stdout.match(/ffmpeg version .?(\d+\.\d+\.\d+)/) | ||
661 | if (!parsed || !parsed[1]) return rej(new Error(`Could not find ffmpeg version in ${stdout}`)) | ||
662 | |||
663 | return res(parsed[1]) | ||
664 | }) | ||
665 | .catch(err => rej(err)) | ||
666 | }) | ||
667 | }) | ||
668 | } | ||
669 | |||
652 | async function runCommand (options: { | 670 | async function runCommand (options: { |
653 | command: ffmpeg.FfmpegCommand | 671 | command: ffmpeg.FfmpegCommand |
654 | silent?: boolean // false | 672 | silent?: boolean // false |
@@ -695,6 +713,7 @@ export { | |||
695 | TranscodeOptionsType, | 713 | TranscodeOptionsType, |
696 | transcode, | 714 | transcode, |
697 | runCommand, | 715 | runCommand, |
716 | getFFmpegVersion, | ||
698 | 717 | ||
699 | resetSupportedEncoders, | 718 | resetSupportedEncoders, |
700 | 719 | ||
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts index 9285c12fc..6f6f8d4da 100644 --- a/server/helpers/image-utils.ts +++ b/server/helpers/image-utils.ts | |||
@@ -1,9 +1,14 @@ | |||
1 | import { copy, readFile, remove, rename } from 'fs-extra' | 1 | import { copy, readFile, remove, rename } from 'fs-extra' |
2 | import * as Jimp from 'jimp' | 2 | import * as Jimp from 'jimp' |
3 | import { extname } from 'path' | 3 | import { extname } from 'path' |
4 | import { v4 as uuidv4 } from 'uuid' | ||
4 | import { convertWebPToJPG, processGIF } from './ffmpeg-utils' | 5 | import { convertWebPToJPG, processGIF } from './ffmpeg-utils' |
5 | import { logger } from './logger' | 6 | import { logger } from './logger' |
6 | 7 | ||
8 | function generateImageFilename (extension = '.jpg') { | ||
9 | return uuidv4() + extension | ||
10 | } | ||
11 | |||
7 | async function processImage ( | 12 | async function processImage ( |
8 | path: string, | 13 | path: string, |
9 | destination: string, | 14 | destination: string, |
@@ -31,6 +36,7 @@ async function processImage ( | |||
31 | // --------------------------------------------------------------------------- | 36 | // --------------------------------------------------------------------------- |
32 | 37 | ||
33 | export { | 38 | export { |
39 | generateImageFilename, | ||
34 | processImage | 40 | processImage |
35 | } | 41 | } |
36 | 42 | ||
diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts index 6917a64d9..a112fd300 100644 --- a/server/helpers/logger.ts +++ b/server/helpers/logger.ts | |||
@@ -48,7 +48,7 @@ function getLoggerReplacer () { | |||
48 | } | 48 | } |
49 | 49 | ||
50 | const consoleLoggerFormat = winston.format.printf(info => { | 50 | const consoleLoggerFormat = winston.format.printf(info => { |
51 | const toOmit = [ 'label', 'timestamp', 'level', 'message', 'sql' ] | 51 | const toOmit = [ 'label', 'timestamp', 'level', 'message', 'sql', 'tags' ] |
52 | 52 | ||
53 | const obj = omit(info, ...toOmit) | 53 | const obj = omit(info, ...toOmit) |
54 | 54 | ||
@@ -150,6 +150,13 @@ const bunyanLogger = { | |||
150 | error: bunyanLogFactory('error'), | 150 | error: bunyanLogFactory('error'), |
151 | fatal: bunyanLogFactory('error') | 151 | fatal: bunyanLogFactory('error') |
152 | } | 152 | } |
153 | |||
154 | function loggerTagsFactory (...defaultTags: string[]) { | ||
155 | return (...tags: string[]) => { | ||
156 | return { tags: defaultTags.concat(tags) } | ||
157 | } | ||
158 | } | ||
159 | |||
153 | // --------------------------------------------------------------------------- | 160 | // --------------------------------------------------------------------------- |
154 | 161 | ||
155 | export { | 162 | export { |
@@ -159,5 +166,6 @@ export { | |||
159 | consoleLoggerFormat, | 166 | consoleLoggerFormat, |
160 | jsonLoggerFormat, | 167 | jsonLoggerFormat, |
161 | logger, | 168 | logger, |
169 | loggerTagsFactory, | ||
162 | bunyanLogger | 170 | bunyanLogger |
163 | } | 171 | } |
diff --git a/server/helpers/middlewares/video-channels.ts b/server/helpers/middlewares/video-channels.ts index 05499bb74..e6eab65a2 100644 --- a/server/helpers/middlewares/video-channels.ts +++ b/server/helpers/middlewares/video-channels.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { VideoChannelModel } from '../../models/video/video-channel' | 2 | import { MChannelBannerAccountDefault } from '@server/types/models' |
3 | import { MChannelAccountDefault } from '@server/types/models' | ||
4 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
4 | import { VideoChannelModel } from '../../models/video/video-channel' | ||
5 | 5 | ||
6 | async function doesLocalVideoChannelNameExist (name: string, res: express.Response) { | 6 | async function doesLocalVideoChannelNameExist (name: string, res: express.Response) { |
7 | const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name) | 7 | const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name) |
@@ -29,11 +29,10 @@ export { | |||
29 | doesVideoChannelNameWithHostExist | 29 | doesVideoChannelNameWithHostExist |
30 | } | 30 | } |
31 | 31 | ||
32 | function processVideoChannelExist (videoChannel: MChannelAccountDefault, res: express.Response) { | 32 | function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) { |
33 | if (!videoChannel) { | 33 | if (!videoChannel) { |
34 | res.status(HttpStatusCode.NOT_FOUND_404) | 34 | res.status(HttpStatusCode.NOT_FOUND_404) |
35 | .json({ error: 'Video channel not found' }) | 35 | .json({ error: 'Video channel not found' }) |
36 | .end() | ||
37 | 36 | ||
38 | return false | 37 | return false |
39 | } | 38 | } |
diff --git a/server/helpers/middlewares/videos.ts b/server/helpers/middlewares/videos.ts index c5eb0607a..403cae092 100644 --- a/server/helpers/middlewares/videos.ts +++ b/server/helpers/middlewares/videos.ts | |||
@@ -66,25 +66,24 @@ async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | st | |||
66 | } | 66 | } |
67 | 67 | ||
68 | async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) { | 68 | async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) { |
69 | if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) { | 69 | const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) |
70 | const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) | ||
71 | if (videoChannel === null) { | ||
72 | res.status(HttpStatusCode.BAD_REQUEST_400) | ||
73 | .json({ error: 'Unknown video `video channel` on this instance.' }) | ||
74 | .end() | ||
75 | 70 | ||
76 | return false | 71 | if (videoChannel === null) { |
77 | } | 72 | res.status(HttpStatusCode.BAD_REQUEST_400) |
73 | .json({ error: 'Unknown video "video channel" for this instance.' }) | ||
78 | 74 | ||
75 | return false | ||
76 | } | ||
77 | |||
78 | // Don't check account id if the user can update any video | ||
79 | if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) { | ||
79 | res.locals.videoChannel = videoChannel | 80 | res.locals.videoChannel = videoChannel |
80 | return true | 81 | return true |
81 | } | 82 | } |
82 | 83 | ||
83 | const videoChannel = await VideoChannelModel.loadByIdAndAccount(channelId, user.Account.id) | 84 | if (videoChannel.Account.id !== user.Account.id) { |
84 | if (videoChannel === null) { | ||
85 | res.status(HttpStatusCode.BAD_REQUEST_400) | 85 | res.status(HttpStatusCode.BAD_REQUEST_400) |
86 | .json({ error: 'Unknown video `video channel` for this account.' }) | 86 | .json({ error: 'Unknown video "video channel" for this account.' }) |
87 | .end() | ||
88 | 87 | ||
89 | return false | 88 | return false |
90 | } | 89 | } |
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index 994f725d8..bc6f1d074 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts | |||
@@ -84,7 +84,7 @@ async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument: any) | |||
84 | return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64') | 84 | return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64') |
85 | } | 85 | } |
86 | 86 | ||
87 | async function signJsonLDObject (byActor: MActor, data: any) { | 87 | async function signJsonLDObject <T> (byActor: MActor, data: T) { |
88 | const signature = { | 88 | const signature = { |
89 | type: 'RsaSignature2017', | 89 | type: 'RsaSignature2017', |
90 | creator: byActor.url, | 90 | creator: byActor.url, |
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index b556c392e..fd2a56f30 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts | |||
@@ -1,58 +1,141 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { createWriteStream, remove } from 'fs-extra' | 1 | import { createWriteStream, remove } from 'fs-extra' |
3 | import * as request from 'request' | 2 | import got, { CancelableRequest, Options as GotOptions, RequestError } from 'got' |
3 | import { join } from 'path' | ||
4 | import { CONFIG } from '../initializers/config' | ||
4 | import { ACTIVITY_PUB, PEERTUBE_VERSION, WEBSERVER } from '../initializers/constants' | 5 | import { ACTIVITY_PUB, PEERTUBE_VERSION, WEBSERVER } from '../initializers/constants' |
6 | import { pipelinePromise } from './core-utils' | ||
5 | import { processImage } from './image-utils' | 7 | import { processImage } from './image-utils' |
6 | import { join } from 'path' | ||
7 | import { logger } from './logger' | 8 | import { logger } from './logger' |
8 | import { CONFIG } from '../initializers/config' | ||
9 | 9 | ||
10 | function doRequest <T> ( | 10 | export interface PeerTubeRequestError extends Error { |
11 | requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }, | 11 | statusCode?: number |
12 | bodyKBLimit = 1000 // 1MB | 12 | responseBody?: any |
13 | ): Bluebird<{ response: request.RequestResponse, body: T }> { | 13 | } |
14 | if (!(requestOptions.headers)) requestOptions.headers = {} | ||
15 | requestOptions.headers['User-Agent'] = getUserAgent() | ||
16 | 14 | ||
17 | if (requestOptions.activityPub === true) { | 15 | const httpSignature = require('http-signature') |
18 | requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER | 16 | |
17 | type PeerTubeRequestOptions = { | ||
18 | activityPub?: boolean | ||
19 | bodyKBLimit?: number // 1MB | ||
20 | httpSignature?: { | ||
21 | algorithm: string | ||
22 | authorizationHeaderName: string | ||
23 | keyId: string | ||
24 | key: string | ||
25 | headers: string[] | ||
19 | } | 26 | } |
27 | jsonResponse?: boolean | ||
28 | } & Pick<GotOptions, 'headers' | 'json' | 'method' | 'searchParams'> | ||
29 | |||
30 | const peertubeGot = got.extend({ | ||
31 | headers: { | ||
32 | 'user-agent': getUserAgent() | ||
33 | }, | ||
34 | |||
35 | handlers: [ | ||
36 | (options, next) => { | ||
37 | const promiseOrStream = next(options) as CancelableRequest<any> | ||
38 | const bodyKBLimit = options.context?.bodyKBLimit as number | ||
39 | if (!bodyKBLimit) throw new Error('No KB limit for this request') | ||
40 | |||
41 | const bodyLimit = bodyKBLimit * 1000 | ||
42 | |||
43 | /* eslint-disable @typescript-eslint/no-floating-promises */ | ||
44 | promiseOrStream.on('downloadProgress', progress => { | ||
45 | if (progress.transferred > bodyLimit && progress.percent !== 1) { | ||
46 | const message = `Exceeded the download limit of ${bodyLimit} B` | ||
47 | logger.warn(message) | ||
48 | |||
49 | // CancelableRequest | ||
50 | if (promiseOrStream.cancel) { | ||
51 | promiseOrStream.cancel() | ||
52 | return | ||
53 | } | ||
54 | |||
55 | // Stream | ||
56 | (promiseOrStream as any).destroy() | ||
57 | } | ||
58 | }) | ||
20 | 59 | ||
21 | return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => { | 60 | return promiseOrStream |
22 | request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) | 61 | } |
23 | .on('data', onRequestDataLengthCheck(bodyKBLimit)) | 62 | ], |
24 | }) | 63 | |
64 | hooks: { | ||
65 | beforeRequest: [ | ||
66 | options => { | ||
67 | const headers = options.headers || {} | ||
68 | headers['host'] = options.url.host | ||
69 | }, | ||
70 | |||
71 | options => { | ||
72 | const httpSignatureOptions = options.context?.httpSignature | ||
73 | |||
74 | if (httpSignatureOptions) { | ||
75 | const method = options.method ?? 'GET' | ||
76 | const path = options.path ?? options.url.pathname | ||
77 | |||
78 | if (!method || !path) { | ||
79 | throw new Error(`Cannot sign request without method (${method}) or path (${path}) ${options}`) | ||
80 | } | ||
81 | |||
82 | httpSignature.signRequest({ | ||
83 | getHeader: function (header) { | ||
84 | return options.headers[header] | ||
85 | }, | ||
86 | |||
87 | setHeader: function (header, value) { | ||
88 | options.headers[header] = value | ||
89 | }, | ||
90 | |||
91 | method, | ||
92 | path | ||
93 | }, httpSignatureOptions) | ||
94 | } | ||
95 | } | ||
96 | ] | ||
97 | } | ||
98 | }) | ||
99 | |||
100 | function doRequest (url: string, options: PeerTubeRequestOptions = {}) { | ||
101 | const gotOptions = buildGotOptions(options) | ||
102 | |||
103 | return peertubeGot(url, gotOptions) | ||
104 | .catch(err => { throw buildRequestError(err) }) | ||
25 | } | 105 | } |
26 | 106 | ||
27 | function doRequestAndSaveToFile ( | 107 | function doJSONRequest <T> (url: string, options: PeerTubeRequestOptions = {}) { |
28 | requestOptions: request.CoreOptions & request.UriOptions, | 108 | const gotOptions = buildGotOptions(options) |
109 | |||
110 | return peertubeGot<T>(url, { ...gotOptions, responseType: 'json' }) | ||
111 | .catch(err => { throw buildRequestError(err) }) | ||
112 | } | ||
113 | |||
114 | async function doRequestAndSaveToFile ( | ||
115 | url: string, | ||
29 | destPath: string, | 116 | destPath: string, |
30 | bodyKBLimit = 10000 // 10MB | 117 | options: PeerTubeRequestOptions = {} |
31 | ) { | 118 | ) { |
32 | if (!requestOptions.headers) requestOptions.headers = {} | 119 | const gotOptions = buildGotOptions(options) |
33 | requestOptions.headers['User-Agent'] = getUserAgent() | ||
34 | |||
35 | return new Bluebird<void>((res, rej) => { | ||
36 | const file = createWriteStream(destPath) | ||
37 | file.on('finish', () => res()) | ||
38 | 120 | ||
39 | request(requestOptions) | 121 | const outFile = createWriteStream(destPath) |
40 | .on('data', onRequestDataLengthCheck(bodyKBLimit)) | ||
41 | .on('error', err => { | ||
42 | file.close() | ||
43 | 122 | ||
44 | remove(destPath) | 123 | try { |
45 | .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err })) | 124 | await pipelinePromise( |
125 | peertubeGot.stream(url, gotOptions), | ||
126 | outFile | ||
127 | ) | ||
128 | } catch (err) { | ||
129 | remove(destPath) | ||
130 | .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err })) | ||
46 | 131 | ||
47 | return rej(err) | 132 | throw buildRequestError(err) |
48 | }) | 133 | } |
49 | .pipe(file) | ||
50 | }) | ||
51 | } | 134 | } |
52 | 135 | ||
53 | async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) { | 136 | async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) { |
54 | const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName) | 137 | const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName) |
55 | await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath) | 138 | await doRequestAndSaveToFile(url, tmpPath) |
56 | 139 | ||
57 | const destPath = join(destDir, destName) | 140 | const destPath = join(destDir, destName) |
58 | 141 | ||
@@ -73,24 +156,46 @@ function getUserAgent () { | |||
73 | 156 | ||
74 | export { | 157 | export { |
75 | doRequest, | 158 | doRequest, |
159 | doJSONRequest, | ||
76 | doRequestAndSaveToFile, | 160 | doRequestAndSaveToFile, |
77 | downloadImage | 161 | downloadImage |
78 | } | 162 | } |
79 | 163 | ||
80 | // --------------------------------------------------------------------------- | 164 | // --------------------------------------------------------------------------- |
81 | 165 | ||
82 | // Thanks to https://github.com/request/request/issues/2470#issuecomment-268929907 <3 | 166 | function buildGotOptions (options: PeerTubeRequestOptions) { |
83 | function onRequestDataLengthCheck (bodyKBLimit: number) { | 167 | const { activityPub, bodyKBLimit = 1000 } = options |
84 | let bufferLength = 0 | ||
85 | const bytesLimit = bodyKBLimit * 1000 | ||
86 | 168 | ||
87 | return function (chunk) { | 169 | const context = { bodyKBLimit, httpSignature: options.httpSignature } |
88 | bufferLength += chunk.length | ||
89 | if (bufferLength > bytesLimit) { | ||
90 | this.abort() | ||
91 | 170 | ||
92 | const error = new Error(`Response was too large - aborted after ${bytesLimit} bytes.`) | 171 | let headers = options.headers || {} |
93 | this.emit('error', error) | 172 | |
94 | } | 173 | if (!headers.date) { |
174 | headers = { ...headers, date: new Date().toUTCString() } | ||
175 | } | ||
176 | |||
177 | if (activityPub && !headers.accept) { | ||
178 | headers = { ...headers, accept: ACTIVITY_PUB.ACCEPT_HEADER } | ||
95 | } | 179 | } |
180 | |||
181 | return { | ||
182 | method: options.method, | ||
183 | json: options.json, | ||
184 | searchParams: options.searchParams, | ||
185 | headers, | ||
186 | context | ||
187 | } | ||
188 | } | ||
189 | |||
190 | function buildRequestError (error: RequestError) { | ||
191 | const newError: PeerTubeRequestError = new Error(error.message) | ||
192 | newError.name = error.name | ||
193 | newError.stack = error.stack | ||
194 | |||
195 | if (error.response) { | ||
196 | newError.responseBody = error.response.body | ||
197 | newError.statusCode = error.response.statusCode | ||
198 | } | ||
199 | |||
200 | return newError | ||
96 | } | 201 | } |
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts index 5b46f704a..fac3da6ba 100644 --- a/server/helpers/youtube-dl.ts +++ b/server/helpers/youtube-dl.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import { createWriteStream } from 'fs' | 1 | import { createWriteStream } from 'fs' |
2 | import { ensureDir, move, pathExists, remove, writeFile } from 'fs-extra' | 2 | import { ensureDir, move, pathExists, remove, writeFile } from 'fs-extra' |
3 | import got from 'got' | ||
3 | import { join } from 'path' | 4 | import { join } from 'path' |
4 | import * as request from 'request' | ||
5 | import { CONFIG } from '@server/initializers/config' | 5 | import { CONFIG } from '@server/initializers/config' |
6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
7 | import { VideoResolution } from '../../shared/models/videos' | 7 | import { VideoResolution } from '../../shared/models/videos' |
8 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' | 8 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' |
9 | import { getEnabledResolutions } from '../lib/video-transcoding' | 9 | import { getEnabledResolutions } from '../lib/video-transcoding' |
10 | import { peertubeTruncate, root } from './core-utils' | 10 | import { peertubeTruncate, pipelinePromise, root } from './core-utils' |
11 | import { isVideoFileExtnameValid } from './custom-validators/videos' | 11 | import { isVideoFileExtnameValid } from './custom-validators/videos' |
12 | import { logger } from './logger' | 12 | import { logger } from './logger' |
13 | import { generateVideoImportTmpPath } from './utils' | 13 | import { generateVideoImportTmpPath } from './utils' |
@@ -195,55 +195,32 @@ async function updateYoutubeDLBinary () { | |||
195 | 195 | ||
196 | await ensureDir(binDirectory) | 196 | await ensureDir(binDirectory) |
197 | 197 | ||
198 | return new Promise<void>(res => { | 198 | try { |
199 | request.get(url, { followRedirect: false }, (err, result) => { | 199 | const result = await got(url, { followRedirect: false }) |
200 | if (err) { | ||
201 | logger.error('Cannot update youtube-dl.', { err }) | ||
202 | return res() | ||
203 | } | ||
204 | |||
205 | if (result.statusCode !== HttpStatusCode.FOUND_302) { | ||
206 | logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode) | ||
207 | return res() | ||
208 | } | ||
209 | |||
210 | const url = result.headers.location | ||
211 | const downloadFile = request.get(url) | ||
212 | const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(url)[1] | ||
213 | |||
214 | downloadFile.on('response', result => { | ||
215 | if (result.statusCode !== HttpStatusCode.OK_200) { | ||
216 | logger.error('Cannot update youtube-dl: new version response is not 200, it\'s %d.', result.statusCode) | ||
217 | return res() | ||
218 | } | ||
219 | |||
220 | const writeStream = createWriteStream(bin, { mode: 493 }).on('error', err => { | ||
221 | logger.error('youtube-dl update error in write stream', { err }) | ||
222 | return res() | ||
223 | }) | ||
224 | 200 | ||
225 | downloadFile.pipe(writeStream) | 201 | if (result.statusCode !== HttpStatusCode.FOUND_302) { |
226 | }) | 202 | logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode) |
203 | return | ||
204 | } | ||
227 | 205 | ||
228 | downloadFile.on('error', err => { | 206 | const newUrl = result.headers.location |
229 | logger.error('youtube-dl update error.', { err }) | 207 | const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(newUrl)[1] |
230 | return res() | ||
231 | }) | ||
232 | 208 | ||
233 | downloadFile.on('end', () => { | 209 | const downloadFileStream = got.stream(newUrl) |
234 | const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' }) | 210 | const writeStream = createWriteStream(bin, { mode: 493 }) |
235 | writeFile(detailsPath, details, { encoding: 'utf8' }, err => { | ||
236 | if (err) { | ||
237 | logger.error('youtube-dl update error: cannot write details.', { err }) | ||
238 | return res() | ||
239 | } | ||
240 | 211 | ||
241 | logger.info('youtube-dl updated to version %s.', newVersion) | 212 | await pipelinePromise( |
242 | return res() | 213 | downloadFileStream, |
243 | }) | 214 | writeStream |
244 | }) | 215 | ) |
245 | }) | 216 | |
246 | }) | 217 | const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' }) |
218 | await writeFile(detailsPath, details, { encoding: 'utf8' }) | ||
219 | |||
220 | logger.info('youtube-dl updated to version %s.', newVersion) | ||
221 | } catch (err) { | ||
222 | logger.error('Cannot update youtube-dl.', { err }) | ||
223 | } | ||
247 | } | 224 | } |
248 | 225 | ||
249 | async function safeGetYoutubeDL () { | 226 | async function safeGetYoutubeDL () { |
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts index 2b00e2047..a93c8b7fd 100644 --- a/server/initializers/checker-after-init.ts +++ b/server/initializers/checker-after-init.ts | |||
@@ -1,16 +1,17 @@ | |||
1 | import * as config from 'config' | 1 | import * as config from 'config' |
2 | import { isProdInstance, isTestInstance } from '../helpers/core-utils' | 2 | import { uniq } from 'lodash' |
3 | import { UserModel } from '../models/account/user' | ||
4 | import { getServerActor, ApplicationModel } from '../models/application/application' | ||
5 | import { OAuthClientModel } from '../models/oauth/oauth-client' | ||
6 | import { URL } from 'url' | 3 | import { URL } from 'url' |
7 | import { CONFIG, isEmailEnabled } from './config' | 4 | import { getFFmpegVersion } from '@server/helpers/ffmpeg-utils' |
8 | import { logger } from '../helpers/logger' | 5 | import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' |
9 | import { RecentlyAddedStrategy } from '../../shared/models/redundancy' | 6 | import { RecentlyAddedStrategy } from '../../shared/models/redundancy' |
7 | import { isProdInstance, isTestInstance, parseSemVersion } from '../helpers/core-utils' | ||
10 | import { isArray } from '../helpers/custom-validators/misc' | 8 | import { isArray } from '../helpers/custom-validators/misc' |
11 | import { uniq } from 'lodash' | 9 | import { logger } from '../helpers/logger' |
10 | import { UserModel } from '../models/account/user' | ||
11 | import { ApplicationModel, getServerActor } from '../models/application/application' | ||
12 | import { OAuthClientModel } from '../models/oauth/oauth-client' | ||
13 | import { CONFIG, isEmailEnabled } from './config' | ||
12 | import { WEBSERVER } from './constants' | 14 | import { WEBSERVER } from './constants' |
13 | import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' | ||
14 | 15 | ||
15 | async function checkActivityPubUrls () { | 16 | async function checkActivityPubUrls () { |
16 | const actor = await getServerActor() | 17 | const actor = await getServerActor() |
@@ -176,11 +177,21 @@ async function applicationExist () { | |||
176 | return totalApplication !== 0 | 177 | return totalApplication !== 0 |
177 | } | 178 | } |
178 | 179 | ||
180 | async function checkFFmpegVersion () { | ||
181 | const version = await getFFmpegVersion() | ||
182 | const { major, minor } = parseSemVersion(version) | ||
183 | |||
184 | if (major < 4 || (major === 4 && minor < 1)) { | ||
185 | logger.warn('Your ffmpeg version (%s) is outdated. PeerTube supports ffmpeg >= 4.1. Please upgrade.', version) | ||
186 | } | ||
187 | } | ||
188 | |||
179 | // --------------------------------------------------------------------------- | 189 | // --------------------------------------------------------------------------- |
180 | 190 | ||
181 | export { | 191 | export { |
182 | checkConfig, | 192 | checkConfig, |
183 | clientsExist, | 193 | clientsExist, |
194 | checkFFmpegVersion, | ||
184 | usersExist, | 195 | usersExist, |
185 | applicationExist, | 196 | applicationExist, |
186 | checkActivityPubUrls | 197 | checkActivityPubUrls |
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 565e0d1fa..e92cc4d2c 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import * as config from 'config' | 1 | import * as config from 'config' |
2 | import { promisify0 } from '../helpers/core-utils' | 2 | import { parseSemVersion, promisify0 } from '../helpers/core-utils' |
3 | import { logger } from '../helpers/logger' | 3 | import { logger } from '../helpers/logger' |
4 | 4 | ||
5 | // ONLY USE CORE MODULES IN THIS FILE! | 5 | // ONLY USE CORE MODULES IN THIS FILE! |
@@ -37,6 +37,7 @@ function checkMissedConfig () { | |||
37 | 'theme.default', | 37 | 'theme.default', |
38 | 'remote_redundancy.videos.accept_from', | 38 | 'remote_redundancy.videos.accept_from', |
39 | 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions', | 39 | 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions', |
40 | 'peertube.check_latest_version.enabled', 'peertube.check_latest_version.url', | ||
40 | 'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url', | 41 | 'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url', |
41 | 'search.search_index.disable_local_search', 'search.search_index.is_default_search', | 42 | 'search.search_index.disable_local_search', 'search.search_index.is_default_search', |
42 | 'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives', | 43 | 'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives', |
@@ -102,8 +103,7 @@ async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) { | |||
102 | 103 | ||
103 | function checkNodeVersion () { | 104 | function checkNodeVersion () { |
104 | const v = process.version | 105 | const v = process.version |
105 | const majorString = v.split('.')[0].replace('v', '') | 106 | const { major } = parseSemVersion(v) |
106 | const major = parseInt(majorString, 10) | ||
107 | 107 | ||
108 | logger.debug('Checking NodeJS version %s.', v) | 108 | logger.debug('Checking NodeJS version %s.', v) |
109 | 109 | ||
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index c16b63c33..93dd5ac04 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -59,7 +59,7 @@ const CONFIG = { | |||
59 | }, | 59 | }, |
60 | STORAGE: { | 60 | STORAGE: { |
61 | TMP_DIR: buildPath(config.get<string>('storage.tmp')), | 61 | TMP_DIR: buildPath(config.get<string>('storage.tmp')), |
62 | AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), | 62 | ACTOR_IMAGES: buildPath(config.get<string>('storage.avatars')), |
63 | LOG_DIR: buildPath(config.get<string>('storage.logs')), | 63 | LOG_DIR: buildPath(config.get<string>('storage.logs')), |
64 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), | 64 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), |
65 | STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')), | 65 | STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')), |
@@ -163,6 +163,12 @@ const CONFIG = { | |||
163 | CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions') | 163 | CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions') |
164 | } | 164 | } |
165 | }, | 165 | }, |
166 | PEERTUBE: { | ||
167 | CHECK_LATEST_VERSION: { | ||
168 | ENABLED: config.get<boolean>('peertube.check_latest_version.enabled'), | ||
169 | URL: config.get<string>('peertube.check_latest_version.url') | ||
170 | } | ||
171 | }, | ||
166 | ADMIN: { | 172 | ADMIN: { |
167 | get EMAIL () { return config.get<string>('admin.email') } | 173 | get EMAIL () { return config.get<string>('admin.email') } |
168 | }, | 174 | }, |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 50467f408..1802257df 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -24,12 +24,12 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
24 | 24 | ||
25 | // --------------------------------------------------------------------------- | 25 | // --------------------------------------------------------------------------- |
26 | 26 | ||
27 | const LAST_MIGRATION_VERSION = 612 | 27 | const LAST_MIGRATION_VERSION = 635 |
28 | 28 | ||
29 | // --------------------------------------------------------------------------- | 29 | // --------------------------------------------------------------------------- |
30 | 30 | ||
31 | const API_VERSION = 'v1' | 31 | const API_VERSION = 'v1' |
32 | const PEERTUBE_VERSION = require(join(root(), 'package.json')).version | 32 | const PEERTUBE_VERSION: string = require(join(root(), 'package.json')).version |
33 | 33 | ||
34 | const PAGINATION = { | 34 | const PAGINATION = { |
35 | GLOBAL: { | 35 | GLOBAL: { |
@@ -207,6 +207,7 @@ const SCHEDULER_INTERVALS_MS = { | |||
207 | updateVideos: 60000, // 1 minute | 207 | updateVideos: 60000, // 1 minute |
208 | youtubeDLUpdate: 60000 * 60 * 24, // 1 day | 208 | youtubeDLUpdate: 60000 * 60 * 24, // 1 day |
209 | checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL, | 209 | checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL, |
210 | checkPeerTubeVersion: 60000 * 60 * 24, // 1 day | ||
210 | autoFollowIndexInstances: 60000 * 60 * 24, // 1 day | 211 | autoFollowIndexInstances: 60000 * 60 * 24, // 1 day |
211 | removeOldViews: 60000 * 60 * 24, // 1 day | 212 | removeOldViews: 60000 * 60 * 24, // 1 day |
212 | removeOldHistory: 60000 * 60 * 24, // 1 day | 213 | removeOldHistory: 60000 * 60 * 24, // 1 day |
@@ -304,7 +305,7 @@ const CONSTRAINTS_FIELDS = { | |||
304 | PUBLIC_KEY: { min: 10, max: 5000 }, // Length | 305 | PUBLIC_KEY: { min: 10, max: 5000 }, // Length |
305 | PRIVATE_KEY: { min: 10, max: 5000 }, // Length | 306 | PRIVATE_KEY: { min: 10, max: 5000 }, // Length |
306 | URL: { min: 3, max: 2000 }, // Length | 307 | URL: { min: 3, max: 2000 }, // Length |
307 | AVATAR: { | 308 | IMAGE: { |
308 | EXTNAME: [ '.png', '.jpeg', '.jpg', '.gif', '.webp' ], | 309 | EXTNAME: [ '.png', '.jpeg', '.jpg', '.gif', '.webp' ], |
309 | FILE_SIZE: { | 310 | FILE_SIZE: { |
310 | max: 2 * 1024 * 1024 // 2MB | 311 | max: 2 * 1024 * 1024 // 2MB |
@@ -465,6 +466,8 @@ const MIMETYPES = { | |||
465 | IMAGE: { | 466 | IMAGE: { |
466 | MIMETYPE_EXT: { | 467 | MIMETYPE_EXT: { |
467 | 'image/png': '.png', | 468 | 'image/png': '.png', |
469 | 'image/gif': '.gif', | ||
470 | 'image/webp': '.webp', | ||
468 | 'image/jpg': '.jpg', | 471 | 'image/jpg': '.jpg', |
469 | 'image/jpeg': '.jpg' | 472 | 'image/jpeg': '.jpg' |
470 | }, | 473 | }, |
@@ -579,6 +582,7 @@ const STATIC_DOWNLOAD_PATHS = { | |||
579 | HLS_VIDEOS: '/download/streaming-playlists/hls/videos/' | 582 | HLS_VIDEOS: '/download/streaming-playlists/hls/videos/' |
580 | } | 583 | } |
581 | const LAZY_STATIC_PATHS = { | 584 | const LAZY_STATIC_PATHS = { |
585 | BANNERS: '/lazy-static/banners/', | ||
582 | AVATARS: '/lazy-static/avatars/', | 586 | AVATARS: '/lazy-static/avatars/', |
583 | PREVIEWS: '/lazy-static/previews/', | 587 | PREVIEWS: '/lazy-static/previews/', |
584 | VIDEO_CAPTIONS: '/lazy-static/video-captions/', | 588 | VIDEO_CAPTIONS: '/lazy-static/video-captions/', |
@@ -594,8 +598,8 @@ const STATIC_MAX_AGE = { | |||
594 | 598 | ||
595 | // Videos thumbnail size | 599 | // Videos thumbnail size |
596 | const THUMBNAILS_SIZE = { | 600 | const THUMBNAILS_SIZE = { |
597 | width: 223, | 601 | width: 280, |
598 | height: 122, | 602 | height: 157, |
599 | minWidth: 150 | 603 | minWidth: 150 |
600 | } | 604 | } |
601 | const PREVIEWS_SIZE = { | 605 | const PREVIEWS_SIZE = { |
@@ -603,9 +607,15 @@ const PREVIEWS_SIZE = { | |||
603 | height: 480, | 607 | height: 480, |
604 | minWidth: 400 | 608 | minWidth: 400 |
605 | } | 609 | } |
606 | const AVATARS_SIZE = { | 610 | const ACTOR_IMAGES_SIZE = { |
607 | width: 120, | 611 | AVATARS: { |
608 | height: 120 | 612 | width: 120, |
613 | height: 120 | ||
614 | }, | ||
615 | BANNERS: { | ||
616 | width: 1920, | ||
617 | height: 317 // 6/1 ratio | ||
618 | } | ||
609 | } | 619 | } |
610 | 620 | ||
611 | const EMBED_SIZE = { | 621 | const EMBED_SIZE = { |
@@ -633,7 +643,7 @@ const LRU_CACHE = { | |||
633 | USER_TOKENS: { | 643 | USER_TOKENS: { |
634 | MAX_SIZE: 1000 | 644 | MAX_SIZE: 1000 |
635 | }, | 645 | }, |
636 | AVATAR_STATIC: { | 646 | ACTOR_IMAGE_STATIC: { |
637 | MAX_SIZE: 500 | 647 | MAX_SIZE: 500 |
638 | } | 648 | } |
639 | } | 649 | } |
@@ -670,7 +680,7 @@ const MEMOIZE_LENGTH = { | |||
670 | } | 680 | } |
671 | 681 | ||
672 | const QUEUE_CONCURRENCY = { | 682 | const QUEUE_CONCURRENCY = { |
673 | AVATAR_PROCESS_IMAGE: 3 | 683 | ACTOR_PROCESS_IMAGE: 3 |
674 | } | 684 | } |
675 | 685 | ||
676 | const REDUNDANCY = { | 686 | const REDUNDANCY = { |
@@ -753,7 +763,7 @@ if (isTestInstance() === true) { | |||
753 | ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds | 763 | ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds |
754 | ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL = 10 * 1000 // 10 seconds | 764 | ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL = 10 * 1000 // 10 seconds |
755 | 765 | ||
756 | CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB | 766 | CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max = 100 * 1024 // 100KB |
757 | CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max = 400 * 1024 // 400KB | 767 | CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max = 400 * 1024 // 400KB |
758 | 768 | ||
759 | SCHEDULER_INTERVALS_MS.actorFollowScores = 1000 | 769 | SCHEDULER_INTERVALS_MS.actorFollowScores = 1000 |
@@ -763,6 +773,7 @@ if (isTestInstance() === true) { | |||
763 | SCHEDULER_INTERVALS_MS.updateVideos = 5000 | 773 | SCHEDULER_INTERVALS_MS.updateVideos = 5000 |
764 | SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000 | 774 | SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000 |
765 | SCHEDULER_INTERVALS_MS.updateInboxStats = 5000 | 775 | SCHEDULER_INTERVALS_MS.updateInboxStats = 5000 |
776 | SCHEDULER_INTERVALS_MS.checkPeerTubeVersion = 2000 | ||
766 | REPEAT_JOBS['videos-views'] = { every: 5000 } | 777 | REPEAT_JOBS['videos-views'] = { every: 5000 } |
767 | REPEAT_JOBS['activitypub-cleaner'] = { every: 5000 } | 778 | REPEAT_JOBS['activitypub-cleaner'] = { every: 5000 } |
768 | 779 | ||
@@ -813,7 +824,7 @@ export { | |||
813 | SEARCH_INDEX, | 824 | SEARCH_INDEX, |
814 | HLS_REDUNDANCY_DIRECTORY, | 825 | HLS_REDUNDANCY_DIRECTORY, |
815 | P2P_MEDIA_LOADER_PEER_VERSION, | 826 | P2P_MEDIA_LOADER_PEER_VERSION, |
816 | AVATARS_SIZE, | 827 | ACTOR_IMAGES_SIZE, |
817 | ACCEPT_HEADERS, | 828 | ACCEPT_HEADERS, |
818 | BCRYPT_SALT_SIZE, | 829 | BCRYPT_SALT_SIZE, |
819 | TRACKER_RATE_LIMITS, | 830 | TRACKER_RATE_LIMITS, |
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 1f2b6d521..4c9d7c610 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { TrackerModel } from '@server/models/server/tracker' | ||
2 | import { VideoTrackerModel } from '@server/models/server/video-tracker' | ||
3 | import { QueryTypes, Transaction } from 'sequelize' | 1 | import { QueryTypes, Transaction } from 'sequelize' |
4 | import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' | 2 | import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' |
3 | import { TrackerModel } from '@server/models/server/tracker' | ||
4 | import { VideoTrackerModel } from '@server/models/server/video-tracker' | ||
5 | import { isTestInstance } from '../helpers/core-utils' | 5 | import { isTestInstance } from '../helpers/core-utils' |
6 | import { logger } from '../helpers/logger' | 6 | import { logger } from '../helpers/logger' |
7 | import { AbuseModel } from '../models/abuse/abuse' | 7 | import { AbuseModel } from '../models/abuse/abuse' |
@@ -11,6 +11,7 @@ import { VideoCommentAbuseModel } from '../models/abuse/video-comment-abuse' | |||
11 | import { AccountModel } from '../models/account/account' | 11 | import { AccountModel } from '../models/account/account' |
12 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 12 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
13 | import { AccountVideoRateModel } from '../models/account/account-video-rate' | 13 | import { AccountVideoRateModel } from '../models/account/account-video-rate' |
14 | import { ActorImageModel } from '../models/account/actor-image' | ||
14 | import { UserModel } from '../models/account/user' | 15 | import { UserModel } from '../models/account/user' |
15 | import { UserNotificationModel } from '../models/account/user-notification' | 16 | import { UserNotificationModel } from '../models/account/user-notification' |
16 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' | 17 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' |
@@ -18,7 +19,6 @@ import { UserVideoHistoryModel } from '../models/account/user-video-history' | |||
18 | import { ActorModel } from '../models/activitypub/actor' | 19 | import { ActorModel } from '../models/activitypub/actor' |
19 | import { ActorFollowModel } from '../models/activitypub/actor-follow' | 20 | import { ActorFollowModel } from '../models/activitypub/actor-follow' |
20 | import { ApplicationModel } from '../models/application/application' | 21 | import { ApplicationModel } from '../models/application/application' |
21 | import { AvatarModel } from '../models/avatar/avatar' | ||
22 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 22 | import { OAuthClientModel } from '../models/oauth/oauth-client' |
23 | import { OAuthTokenModel } from '../models/oauth/oauth-token' | 23 | import { OAuthTokenModel } from '../models/oauth/oauth-token' |
24 | import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' | 24 | import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' |
@@ -76,7 +76,7 @@ const sequelizeTypescript = new SequelizeTypescript({ | |||
76 | newMessage += ' in ' + benchmark + 'ms' | 76 | newMessage += ' in ' + benchmark + 'ms' |
77 | } | 77 | } |
78 | 78 | ||
79 | logger.debug(newMessage, { sql: message }) | 79 | logger.debug(newMessage, { sql: message, tags: [ 'sql' ] }) |
80 | } | 80 | } |
81 | }) | 81 | }) |
82 | 82 | ||
@@ -95,7 +95,7 @@ async function initDatabaseModels (silent: boolean) { | |||
95 | ApplicationModel, | 95 | ApplicationModel, |
96 | ActorModel, | 96 | ActorModel, |
97 | ActorFollowModel, | 97 | ActorFollowModel, |
98 | AvatarModel, | 98 | ActorImageModel, |
99 | AccountModel, | 99 | AccountModel, |
100 | OAuthClientModel, | 100 | OAuthClientModel, |
101 | OAuthTokenModel, | 101 | OAuthTokenModel, |
diff --git a/server/initializers/migrations/0610-views-index.ts b/server/initializers/migrations/0610-views-index copy.ts index 02ee21172..02ee21172 100644 --- a/server/initializers/migrations/0610-views-index.ts +++ b/server/initializers/migrations/0610-views-index copy.ts | |||
diff --git a/server/initializers/migrations/0615-latest-versions-notification-settings.ts b/server/initializers/migrations/0615-latest-versions-notification-settings.ts new file mode 100644 index 000000000..86bf56009 --- /dev/null +++ b/server/initializers/migrations/0615-latest-versions-notification-settings.ts | |||
@@ -0,0 +1,44 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | const notificationSettingColumns = [ 'newPeerTubeVersion', 'newPluginVersion' ] | ||
11 | |||
12 | for (const column of notificationSettingColumns) { | ||
13 | const data = { | ||
14 | type: Sequelize.INTEGER, | ||
15 | defaultValue: null, | ||
16 | allowNull: true | ||
17 | } | ||
18 | await utils.queryInterface.addColumn('userNotificationSetting', column, data) | ||
19 | } | ||
20 | |||
21 | { | ||
22 | const query = 'UPDATE "userNotificationSetting" SET "newPeerTubeVersion" = 3, "newPluginVersion" = 1' | ||
23 | await utils.sequelize.query(query) | ||
24 | } | ||
25 | |||
26 | for (const column of notificationSettingColumns) { | ||
27 | const data = { | ||
28 | type: Sequelize.INTEGER, | ||
29 | defaultValue: null, | ||
30 | allowNull: false | ||
31 | } | ||
32 | await utils.queryInterface.changeColumn('userNotificationSetting', column, data) | ||
33 | } | ||
34 | } | ||
35 | } | ||
36 | |||
37 | function down (options) { | ||
38 | throw new Error('Not implemented.') | ||
39 | } | ||
40 | |||
41 | export { | ||
42 | up, | ||
43 | down | ||
44 | } | ||
diff --git a/server/initializers/migrations/0620-latest-versions-application.ts b/server/initializers/migrations/0620-latest-versions-application.ts new file mode 100644 index 000000000..a689b18fc --- /dev/null +++ b/server/initializers/migrations/0620-latest-versions-application.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | |||
10 | { | ||
11 | const data = { | ||
12 | type: Sequelize.STRING, | ||
13 | defaultValue: null, | ||
14 | allowNull: true | ||
15 | } | ||
16 | await utils.queryInterface.addColumn('application', 'latestPeerTubeVersion', data) | ||
17 | } | ||
18 | } | ||
19 | |||
20 | function down (options) { | ||
21 | throw new Error('Not implemented.') | ||
22 | } | ||
23 | |||
24 | export { | ||
25 | up, | ||
26 | down | ||
27 | } | ||
diff --git a/server/initializers/migrations/0625-latest-versions-notification.ts b/server/initializers/migrations/0625-latest-versions-notification.ts new file mode 100644 index 000000000..77f395ce4 --- /dev/null +++ b/server/initializers/migrations/0625-latest-versions-notification.ts | |||
@@ -0,0 +1,26 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | |||
10 | { | ||
11 | await utils.sequelize.query(` | ||
12 | ALTER TABLE "userNotification" | ||
13 | ADD COLUMN "applicationId" INTEGER REFERENCES "application" ("id") ON DELETE SET NULL ON UPDATE CASCADE, | ||
14 | ADD COLUMN "pluginId" INTEGER REFERENCES "plugin" ("id") ON DELETE SET NULL ON UPDATE CASCADE | ||
15 | `) | ||
16 | } | ||
17 | } | ||
18 | |||
19 | function down (options) { | ||
20 | throw new Error('Not implemented.') | ||
21 | } | ||
22 | |||
23 | export { | ||
24 | up, | ||
25 | down | ||
26 | } | ||
diff --git a/server/initializers/migrations/0630-banner.ts b/server/initializers/migrations/0630-banner.ts new file mode 100644 index 000000000..5766bb171 --- /dev/null +++ b/server/initializers/migrations/0630-banner.ts | |||
@@ -0,0 +1,50 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | |||
10 | { | ||
11 | await utils.sequelize.query(`ALTER TABLE "avatar" RENAME to "actorImage"`) | ||
12 | } | ||
13 | |||
14 | { | ||
15 | const data = { | ||
16 | type: Sequelize.INTEGER, | ||
17 | defaultValue: null, | ||
18 | allowNull: true | ||
19 | } | ||
20 | await utils.queryInterface.addColumn('actorImage', 'type', data) | ||
21 | } | ||
22 | |||
23 | { | ||
24 | await utils.sequelize.query(`UPDATE "actorImage" SET "type" = 1`) | ||
25 | } | ||
26 | |||
27 | { | ||
28 | const data = { | ||
29 | type: Sequelize.INTEGER, | ||
30 | defaultValue: null, | ||
31 | allowNull: false | ||
32 | } | ||
33 | await utils.queryInterface.changeColumn('actorImage', 'type', data) | ||
34 | } | ||
35 | |||
36 | { | ||
37 | await utils.sequelize.query( | ||
38 | `ALTER TABLE "actor" ADD COLUMN "bannerId" INTEGER REFERENCES "actorImage" ("id") ON DELETE SET NULL ON UPDATE CASCADE` | ||
39 | ) | ||
40 | } | ||
41 | } | ||
42 | |||
43 | function down (options) { | ||
44 | throw new Error('Not implemented.') | ||
45 | } | ||
46 | |||
47 | export { | ||
48 | up, | ||
49 | down | ||
50 | } | ||
diff --git a/server/initializers/migrations/0635-actor-image-size.ts b/server/initializers/migrations/0635-actor-image-size.ts new file mode 100644 index 000000000..d7c5da8c3 --- /dev/null +++ b/server/initializers/migrations/0635-actor-image-size.ts | |||
@@ -0,0 +1,35 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | const data = { | ||
11 | type: Sequelize.INTEGER, | ||
12 | defaultValue: null, | ||
13 | allowNull: true | ||
14 | } | ||
15 | await utils.queryInterface.addColumn('actorImage', 'height', data) | ||
16 | } | ||
17 | |||
18 | { | ||
19 | const data = { | ||
20 | type: Sequelize.INTEGER, | ||
21 | defaultValue: null, | ||
22 | allowNull: true | ||
23 | } | ||
24 | await utils.queryInterface.addColumn('actorImage', 'width', data) | ||
25 | } | ||
26 | } | ||
27 | |||
28 | function down (options) { | ||
29 | throw new Error('Not implemented.') | ||
30 | } | ||
31 | |||
32 | export { | ||
33 | up, | ||
34 | down | ||
35 | } | ||
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index a726f9e20..eec951d4e 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts | |||
@@ -1,26 +1,29 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { extname } from 'path' | ||
2 | import { Op, Transaction } from 'sequelize' | 3 | import { Op, Transaction } from 'sequelize' |
3 | import { URL } from 'url' | 4 | import { URL } from 'url' |
4 | import { v4 as uuidv4 } from 'uuid' | 5 | import { v4 as uuidv4 } from 'uuid' |
6 | import { getServerActor } from '@server/models/application/application' | ||
7 | import { ActorImageType } from '@shared/models' | ||
8 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
5 | import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | 9 | import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub' |
6 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' | 10 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' |
7 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | 11 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
12 | import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor' | ||
8 | import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor' | 13 | import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor' |
9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 14 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
10 | import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' | 15 | import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' |
11 | import { logger } from '../../helpers/logger' | 16 | import { logger } from '../../helpers/logger' |
12 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' | 17 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' |
13 | import { doRequest } from '../../helpers/requests' | 18 | import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests' |
14 | import { getUrlFromWebfinger } from '../../helpers/webfinger' | 19 | import { getUrlFromWebfinger } from '../../helpers/webfinger' |
15 | import { MIMETYPES, WEBSERVER } from '../../initializers/constants' | 20 | import { MIMETYPES, WEBSERVER } from '../../initializers/constants' |
21 | import { sequelizeTypescript } from '../../initializers/database' | ||
16 | import { AccountModel } from '../../models/account/account' | 22 | import { AccountModel } from '../../models/account/account' |
23 | import { ActorImageModel } from '../../models/account/actor-image' | ||
17 | import { ActorModel } from '../../models/activitypub/actor' | 24 | import { ActorModel } from '../../models/activitypub/actor' |
18 | import { AvatarModel } from '../../models/avatar/avatar' | ||
19 | import { ServerModel } from '../../models/server/server' | 25 | import { ServerModel } from '../../models/server/server' |
20 | import { VideoChannelModel } from '../../models/video/video-channel' | 26 | import { VideoChannelModel } from '../../models/video/video-channel' |
21 | import { JobQueue } from '../job-queue' | ||
22 | import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor' | ||
23 | import { sequelizeTypescript } from '../../initializers/database' | ||
24 | import { | 27 | import { |
25 | MAccount, | 28 | MAccount, |
26 | MAccountDefault, | 29 | MAccountDefault, |
@@ -28,15 +31,14 @@ import { | |||
28 | MActorAccountChannelId, | 31 | MActorAccountChannelId, |
29 | MActorAccountChannelIdActor, | 32 | MActorAccountChannelIdActor, |
30 | MActorAccountId, | 33 | MActorAccountId, |
31 | MActorDefault, | ||
32 | MActorFull, | 34 | MActorFull, |
33 | MActorFullActor, | 35 | MActorFullActor, |
34 | MActorId, | 36 | MActorId, |
37 | MActorImage, | ||
38 | MActorImages, | ||
35 | MChannel | 39 | MChannel |
36 | } from '../../types/models' | 40 | } from '../../types/models' |
37 | import { extname } from 'path' | 41 | import { JobQueue } from '../job-queue' |
38 | import { getServerActor } from '@server/models/application/application' | ||
39 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
40 | 42 | ||
41 | // Set account keys, this could be long so process after the account creation and do not block the client | 43 | // Set account keys, this could be long so process after the account creation and do not block the client |
42 | async function generateAndSaveActorKeys <T extends MActor> (actor: T) { | 44 | async function generateAndSaveActorKeys <T extends MActor> (actor: T) { |
@@ -168,66 +170,83 @@ async function updateActorInstance (actorInstance: ActorModel, attributes: Activ | |||
168 | } | 170 | } |
169 | } | 171 | } |
170 | 172 | ||
171 | type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string } | 173 | type ImageInfo = { |
172 | async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo, t: Transaction) { | 174 | name: string |
173 | if (!info.name) return actor | 175 | fileUrl: string |
176 | height: number | ||
177 | width: number | ||
178 | onDisk?: boolean | ||
179 | } | ||
180 | async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) { | ||
181 | const oldImageModel = type === ActorImageType.AVATAR | ||
182 | ? actor.Avatar | ||
183 | : actor.Banner | ||
174 | 184 | ||
175 | if (actor.Avatar) { | 185 | if (oldImageModel) { |
176 | // Don't update the avatar if the file URL did not change | 186 | // Don't update the avatar if the file URL did not change |
177 | if (info.fileUrl && actor.Avatar.fileUrl === info.fileUrl) return actor | 187 | if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) return actor |
178 | 188 | ||
179 | try { | 189 | try { |
180 | await actor.Avatar.destroy({ transaction: t }) | 190 | await oldImageModel.destroy({ transaction: t }) |
191 | |||
192 | setActorImage(actor, type, null) | ||
181 | } catch (err) { | 193 | } catch (err) { |
182 | logger.error('Cannot remove old avatar of actor %s.', actor.url, { err }) | 194 | logger.error('Cannot remove old actor image of actor %s.', actor.url, { err }) |
183 | } | 195 | } |
184 | } | 196 | } |
185 | 197 | ||
186 | const avatar = await AvatarModel.create({ | 198 | if (imageInfo) { |
187 | filename: info.name, | 199 | const imageModel = await ActorImageModel.create({ |
188 | onDisk: info.onDisk, | 200 | filename: imageInfo.name, |
189 | fileUrl: info.fileUrl | 201 | onDisk: imageInfo.onDisk ?? false, |
190 | }, { transaction: t }) | 202 | fileUrl: imageInfo.fileUrl, |
191 | 203 | height: imageInfo.height, | |
192 | actor.avatarId = avatar.id | 204 | width: imageInfo.width, |
193 | actor.Avatar = avatar | 205 | type |
206 | }, { transaction: t }) | ||
207 | |||
208 | setActorImage(actor, type, imageModel) | ||
209 | } | ||
194 | 210 | ||
195 | return actor | 211 | return actor |
196 | } | 212 | } |
197 | 213 | ||
198 | async function deleteActorAvatarInstance (actor: MActorDefault, t: Transaction) { | 214 | async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) { |
199 | try { | 215 | try { |
200 | await actor.Avatar.destroy({ transaction: t }) | 216 | if (type === ActorImageType.AVATAR) { |
217 | await actor.Avatar.destroy({ transaction: t }) | ||
218 | |||
219 | actor.avatarId = null | ||
220 | actor.Avatar = null | ||
221 | } else { | ||
222 | await actor.Banner.destroy({ transaction: t }) | ||
223 | |||
224 | actor.bannerId = null | ||
225 | actor.Banner = null | ||
226 | } | ||
201 | } catch (err) { | 227 | } catch (err) { |
202 | logger.error('Cannot remove old avatar of actor %s.', actor.url, { err }) | 228 | logger.error('Cannot remove old image of actor %s.', actor.url, { err }) |
203 | } | 229 | } |
204 | 230 | ||
205 | actor.avatarId = null | ||
206 | actor.Avatar = null | ||
207 | |||
208 | return actor | 231 | return actor |
209 | } | 232 | } |
210 | 233 | ||
211 | async function fetchActorTotalItems (url: string) { | 234 | async function fetchActorTotalItems (url: string) { |
212 | const options = { | ||
213 | uri: url, | ||
214 | method: 'GET', | ||
215 | json: true, | ||
216 | activityPub: true | ||
217 | } | ||
218 | |||
219 | try { | 235 | try { |
220 | const { body } = await doRequest<ActivityPubOrderedCollection<unknown>>(options) | 236 | const { body } = await doJSONRequest<ActivityPubOrderedCollection<unknown>>(url, { activityPub: true }) |
221 | return body.totalItems ? body.totalItems : 0 | 237 | |
238 | return body.totalItems || 0 | ||
222 | } catch (err) { | 239 | } catch (err) { |
223 | logger.warn('Cannot fetch remote actor count %s.', url, { err }) | 240 | logger.warn('Cannot fetch remote actor count %s.', url, { err }) |
224 | return 0 | 241 | return 0 |
225 | } | 242 | } |
226 | } | 243 | } |
227 | 244 | ||
228 | function getAvatarInfoIfExists (actorJSON: ActivityPubActor) { | 245 | function getImageInfoIfExists (actorJSON: ActivityPubActor, type: ActorImageType) { |
229 | const mimetypes = MIMETYPES.IMAGE | 246 | const mimetypes = MIMETYPES.IMAGE |
230 | const icon = actorJSON.icon | 247 | const icon = type === ActorImageType.AVATAR |
248 | ? actorJSON.icon | ||
249 | : actorJSON.image | ||
231 | 250 | ||
232 | if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined | 251 | if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined |
233 | 252 | ||
@@ -245,7 +264,10 @@ function getAvatarInfoIfExists (actorJSON: ActivityPubActor) { | |||
245 | 264 | ||
246 | return { | 265 | return { |
247 | name: uuidv4() + extension, | 266 | name: uuidv4() + extension, |
248 | fileUrl: icon.url | 267 | fileUrl: icon.url, |
268 | height: icon.height, | ||
269 | width: icon.width, | ||
270 | type | ||
249 | } | 271 | } |
250 | } | 272 | } |
251 | 273 | ||
@@ -285,16 +307,7 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel | |||
285 | actorUrl = actor.url | 307 | actorUrl = actor.url |
286 | } | 308 | } |
287 | 309 | ||
288 | const { result, statusCode } = await fetchRemoteActor(actorUrl) | 310 | const { result } = await fetchRemoteActor(actorUrl) |
289 | |||
290 | if (statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
291 | logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) | ||
292 | actor.Account | ||
293 | ? await actor.Account.destroy() | ||
294 | : await actor.VideoChannel.destroy() | ||
295 | |||
296 | return { actor: undefined, refreshed: false } | ||
297 | } | ||
298 | 311 | ||
299 | if (result === undefined) { | 312 | if (result === undefined) { |
300 | logger.warn('Cannot fetch remote actor in refresh actor.') | 313 | logger.warn('Cannot fetch remote actor in refresh actor.') |
@@ -304,15 +317,8 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel | |||
304 | return sequelizeTypescript.transaction(async t => { | 317 | return sequelizeTypescript.transaction(async t => { |
305 | updateInstanceWithAnother(actor, result.actor) | 318 | updateInstanceWithAnother(actor, result.actor) |
306 | 319 | ||
307 | if (result.avatar !== undefined) { | 320 | await updateActorImageInstance(actor, ActorImageType.AVATAR, result.avatar, t) |
308 | const avatarInfo = { | 321 | await updateActorImageInstance(actor, ActorImageType.BANNER, result.banner, t) |
309 | name: result.avatar.name, | ||
310 | fileUrl: result.avatar.fileUrl, | ||
311 | onDisk: false | ||
312 | } | ||
313 | |||
314 | await updateActorAvatarInstance(actor, avatarInfo, t) | ||
315 | } | ||
316 | 322 | ||
317 | // Force update | 323 | // Force update |
318 | actor.setDataValue('updatedAt', new Date()) | 324 | actor.setDataValue('updatedAt', new Date()) |
@@ -334,6 +340,15 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel | |||
334 | return { refreshed: true, actor } | 340 | return { refreshed: true, actor } |
335 | }) | 341 | }) |
336 | } catch (err) { | 342 | } catch (err) { |
343 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
344 | logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) | ||
345 | actor.Account | ||
346 | ? await actor.Account.destroy() | ||
347 | : await actor.VideoChannel.destroy() | ||
348 | |||
349 | return { actor: undefined, refreshed: false } | ||
350 | } | ||
351 | |||
337 | logger.warn('Cannot refresh actor %s.', actor.url, { err }) | 352 | logger.warn('Cannot refresh actor %s.', actor.url, { err }) |
338 | return { actor, refreshed: false } | 353 | return { actor, refreshed: false } |
339 | } | 354 | } |
@@ -344,16 +359,32 @@ export { | |||
344 | buildActorInstance, | 359 | buildActorInstance, |
345 | generateAndSaveActorKeys, | 360 | generateAndSaveActorKeys, |
346 | fetchActorTotalItems, | 361 | fetchActorTotalItems, |
347 | getAvatarInfoIfExists, | 362 | getImageInfoIfExists, |
348 | updateActorInstance, | 363 | updateActorInstance, |
349 | deleteActorAvatarInstance, | 364 | deleteActorImageInstance, |
350 | refreshActorIfNeeded, | 365 | refreshActorIfNeeded, |
351 | updateActorAvatarInstance, | 366 | updateActorImageInstance, |
352 | addFetchOutboxJob | 367 | addFetchOutboxJob |
353 | } | 368 | } |
354 | 369 | ||
355 | // --------------------------------------------------------------------------- | 370 | // --------------------------------------------------------------------------- |
356 | 371 | ||
372 | function setActorImage (actorModel: MActorImages, type: ActorImageType, imageModel: MActorImage) { | ||
373 | const id = imageModel | ||
374 | ? imageModel.id | ||
375 | : null | ||
376 | |||
377 | if (type === ActorImageType.AVATAR) { | ||
378 | actorModel.avatarId = id | ||
379 | actorModel.Avatar = imageModel | ||
380 | } else { | ||
381 | actorModel.bannerId = id | ||
382 | actorModel.Banner = imageModel | ||
383 | } | ||
384 | |||
385 | return actorModel | ||
386 | } | ||
387 | |||
357 | function saveActorAndServerAndModelIfNotExist ( | 388 | function saveActorAndServerAndModelIfNotExist ( |
358 | result: FetchRemoteActorResult, | 389 | result: FetchRemoteActorResult, |
359 | ownerActor?: MActorFullActor, | 390 | ownerActor?: MActorFullActor, |
@@ -384,15 +415,32 @@ function saveActorAndServerAndModelIfNotExist ( | |||
384 | 415 | ||
385 | // Avatar? | 416 | // Avatar? |
386 | if (result.avatar) { | 417 | if (result.avatar) { |
387 | const avatar = await AvatarModel.create({ | 418 | const avatar = await ActorImageModel.create({ |
388 | filename: result.avatar.name, | 419 | filename: result.avatar.name, |
389 | fileUrl: result.avatar.fileUrl, | 420 | fileUrl: result.avatar.fileUrl, |
390 | onDisk: false | 421 | width: result.avatar.width, |
422 | height: result.avatar.height, | ||
423 | onDisk: false, | ||
424 | type: ActorImageType.AVATAR | ||
391 | }, { transaction: t }) | 425 | }, { transaction: t }) |
392 | 426 | ||
393 | actor.avatarId = avatar.id | 427 | actor.avatarId = avatar.id |
394 | } | 428 | } |
395 | 429 | ||
430 | // Banner? | ||
431 | if (result.banner) { | ||
432 | const banner = await ActorImageModel.create({ | ||
433 | filename: result.banner.name, | ||
434 | fileUrl: result.banner.fileUrl, | ||
435 | width: result.banner.width, | ||
436 | height: result.banner.height, | ||
437 | onDisk: false, | ||
438 | type: ActorImageType.BANNER | ||
439 | }, { transaction: t }) | ||
440 | |||
441 | actor.bannerId = banner.id | ||
442 | } | ||
443 | |||
396 | // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists | 444 | // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists |
397 | // (which could be false in a retried query) | 445 | // (which could be false in a retried query) |
398 | const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({ | 446 | const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({ |
@@ -436,39 +484,37 @@ function saveActorAndServerAndModelIfNotExist ( | |||
436 | } | 484 | } |
437 | } | 485 | } |
438 | 486 | ||
487 | type ImageResult = { | ||
488 | name: string | ||
489 | fileUrl: string | ||
490 | height: number | ||
491 | width: number | ||
492 | } | ||
493 | |||
439 | type FetchRemoteActorResult = { | 494 | type FetchRemoteActorResult = { |
440 | actor: MActor | 495 | actor: MActor |
441 | name: string | 496 | name: string |
442 | summary: string | 497 | summary: string |
443 | support?: string | 498 | support?: string |
444 | playlists?: string | 499 | playlists?: string |
445 | avatar?: { | 500 | avatar?: ImageResult |
446 | name: string | 501 | banner?: ImageResult |
447 | fileUrl: string | ||
448 | } | ||
449 | attributedTo: ActivityPubAttributedTo[] | 502 | attributedTo: ActivityPubAttributedTo[] |
450 | } | 503 | } |
451 | async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { | 504 | async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { |
452 | const options = { | ||
453 | uri: actorUrl, | ||
454 | method: 'GET', | ||
455 | json: true, | ||
456 | activityPub: true | ||
457 | } | ||
458 | |||
459 | logger.info('Fetching remote actor %s.', actorUrl) | 505 | logger.info('Fetching remote actor %s.', actorUrl) |
460 | 506 | ||
461 | const requestResult = await doRequest<ActivityPubActor>(options) | 507 | const requestResult = await doJSONRequest<ActivityPubActor>(actorUrl, { activityPub: true }) |
462 | const actorJSON = requestResult.body | 508 | const actorJSON = requestResult.body |
463 | 509 | ||
464 | if (sanitizeAndCheckActorObject(actorJSON) === false) { | 510 | if (sanitizeAndCheckActorObject(actorJSON) === false) { |
465 | logger.debug('Remote actor JSON is not valid.', { actorJSON }) | 511 | logger.debug('Remote actor JSON is not valid.', { actorJSON }) |
466 | return { result: undefined, statusCode: requestResult.response.statusCode } | 512 | return { result: undefined, statusCode: requestResult.statusCode } |
467 | } | 513 | } |
468 | 514 | ||
469 | if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) { | 515 | if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) { |
470 | logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id) | 516 | logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id) |
471 | return { result: undefined, statusCode: requestResult.response.statusCode } | 517 | return { result: undefined, statusCode: requestResult.statusCode } |
472 | } | 518 | } |
473 | 519 | ||
474 | const followersCount = await fetchActorTotalItems(actorJSON.followers) | 520 | const followersCount = await fetchActorTotalItems(actorJSON.followers) |
@@ -492,15 +538,17 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe | |||
492 | : null | 538 | : null |
493 | }) | 539 | }) |
494 | 540 | ||
495 | const avatarInfo = await getAvatarInfoIfExists(actorJSON) | 541 | const avatarInfo = getImageInfoIfExists(actorJSON, ActorImageType.AVATAR) |
542 | const bannerInfo = getImageInfoIfExists(actorJSON, ActorImageType.BANNER) | ||
496 | 543 | ||
497 | const name = actorJSON.name || actorJSON.preferredUsername | 544 | const name = actorJSON.name || actorJSON.preferredUsername |
498 | return { | 545 | return { |
499 | statusCode: requestResult.response.statusCode, | 546 | statusCode: requestResult.statusCode, |
500 | result: { | 547 | result: { |
501 | actor, | 548 | actor, |
502 | name, | 549 | name, |
503 | avatar: avatarInfo, | 550 | avatar: avatarInfo, |
551 | banner: bannerInfo, | ||
504 | summary: actorJSON.summary, | 552 | summary: actorJSON.summary, |
505 | support: actorJSON.support, | 553 | support: actorJSON.support, |
506 | playlists: actorJSON.playlists, | 554 | playlists: actorJSON.playlists, |
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts index 1ed105bbe..278abf7de 100644 --- a/server/lib/activitypub/crawl.ts +++ b/server/lib/activitypub/crawl.ts | |||
@@ -1,27 +1,26 @@ | |||
1 | import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants' | ||
2 | import { doRequest } from '../../helpers/requests' | ||
3 | import { logger } from '../../helpers/logger' | ||
4 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
5 | import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | ||
6 | import { URL } from 'url' | 2 | import { URL } from 'url' |
3 | import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | ||
4 | import { logger } from '../../helpers/logger' | ||
5 | import { doJSONRequest } from '../../helpers/requests' | ||
6 | import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants' | ||
7 | 7 | ||
8 | type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) | 8 | type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) |
9 | type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) | 9 | type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) |
10 | 10 | ||
11 | async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) { | 11 | async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) { |
12 | logger.info('Crawling ActivityPub data on %s.', uri) | 12 | let url = argUrl |
13 | |||
14 | logger.info('Crawling ActivityPub data on %s.', url) | ||
13 | 15 | ||
14 | const options = { | 16 | const options = { |
15 | method: 'GET', | ||
16 | uri, | ||
17 | json: true, | ||
18 | activityPub: true, | 17 | activityPub: true, |
19 | timeout: REQUEST_TIMEOUT | 18 | timeout: REQUEST_TIMEOUT |
20 | } | 19 | } |
21 | 20 | ||
22 | const startDate = new Date() | 21 | const startDate = new Date() |
23 | 22 | ||
24 | const response = await doRequest<ActivityPubOrderedCollection<T>>(options) | 23 | const response = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options) |
25 | const firstBody = response.body | 24 | const firstBody = response.body |
26 | 25 | ||
27 | const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT | 26 | const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT |
@@ -35,9 +34,9 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T> | |||
35 | const remoteHost = new URL(nextLink).host | 34 | const remoteHost = new URL(nextLink).host |
36 | if (remoteHost === WEBSERVER.HOST) continue | 35 | if (remoteHost === WEBSERVER.HOST) continue |
37 | 36 | ||
38 | options.uri = nextLink | 37 | url = nextLink |
39 | 38 | ||
40 | const res = await doRequest<ActivityPubOrderedCollection<T>>(options) | 39 | const res = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options) |
41 | body = res.body | 40 | body = res.body |
42 | } else { | 41 | } else { |
43 | // nextLink is already the object we want | 42 | // nextLink is already the object we want |
@@ -49,7 +48,7 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T> | |||
49 | 48 | ||
50 | if (Array.isArray(body.orderedItems)) { | 49 | if (Array.isArray(body.orderedItems)) { |
51 | const items = body.orderedItems | 50 | const items = body.orderedItems |
52 | logger.info('Processing %i ActivityPub items for %s.', items.length, options.uri) | 51 | logger.info('Processing %i ActivityPub items for %s.', items.length, url) |
53 | 52 | ||
54 | await handler(items) | 53 | await handler(items) |
55 | } | 54 | } |
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts index d5a3ef7c8..7166c68a6 100644 --- a/server/lib/activitypub/playlist.ts +++ b/server/lib/activitypub/playlist.ts | |||
@@ -1,24 +1,24 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
3 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | ||
1 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | 4 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' |
2 | import { crawlCollectionPage } from './crawl' | 5 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' |
3 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | 6 | import { checkUrlsSameHost } from '../../helpers/activitypub' |
7 | import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist' | ||
4 | import { isArray } from '../../helpers/custom-validators/misc' | 8 | import { isArray } from '../../helpers/custom-validators/misc' |
5 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
6 | import { logger } from '../../helpers/logger' | 9 | import { logger } from '../../helpers/logger' |
10 | import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests' | ||
11 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | ||
12 | import { sequelizeTypescript } from '../../initializers/database' | ||
7 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | 13 | import { VideoPlaylistModel } from '../../models/video/video-playlist' |
8 | import { doRequest } from '../../helpers/requests' | ||
9 | import { checkUrlsSameHost } from '../../helpers/activitypub' | ||
10 | import * as Bluebird from 'bluebird' | ||
11 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | ||
12 | import { getOrCreateVideoAndAccountAndChannel } from './videos' | ||
13 | import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist' | ||
14 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' | 14 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' |
15 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | ||
16 | import { sequelizeTypescript } from '../../initializers/database' | ||
17 | import { createPlaylistMiniatureFromUrl } from '../thumbnail' | ||
18 | import { FilteredModelAttributes } from '../../types/sequelize' | ||
19 | import { MAccountDefault, MAccountId, MVideoId } from '../../types/models' | 15 | import { MAccountDefault, MAccountId, MVideoId } from '../../types/models' |
20 | import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist' | 16 | import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist' |
21 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 17 | import { FilteredModelAttributes } from '../../types/sequelize' |
18 | import { createPlaylistMiniatureFromUrl } from '../thumbnail' | ||
19 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
20 | import { crawlCollectionPage } from './crawl' | ||
21 | import { getOrCreateVideoAndAccountAndChannel } from './videos' | ||
22 | 22 | ||
23 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { | 23 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { |
24 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) | 24 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) |
@@ -56,11 +56,7 @@ async function createAccountPlaylists (playlistUrls: string[], account: MAccount | |||
56 | if (exists === true) return | 56 | if (exists === true) return |
57 | 57 | ||
58 | // Fetch url | 58 | // Fetch url |
59 | const { body } = await doRequest<PlaylistObject>({ | 59 | const { body } = await doJSONRequest<PlaylistObject>(playlistUrl, { activityPub: true }) |
60 | uri: playlistUrl, | ||
61 | json: true, | ||
62 | activityPub: true | ||
63 | }) | ||
64 | 60 | ||
65 | if (!isPlaylistObjectValid(body)) { | 61 | if (!isPlaylistObjectValid(body)) { |
66 | throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`) | 62 | throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`) |
@@ -120,13 +116,7 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner) | |||
120 | if (!videoPlaylist.isOutdated()) return videoPlaylist | 116 | if (!videoPlaylist.isOutdated()) return videoPlaylist |
121 | 117 | ||
122 | try { | 118 | try { |
123 | const { statusCode, playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url) | 119 | const { playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url) |
124 | if (statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
125 | logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url) | ||
126 | |||
127 | await videoPlaylist.destroy() | ||
128 | return undefined | ||
129 | } | ||
130 | 120 | ||
131 | if (playlistObject === undefined) { | 121 | if (playlistObject === undefined) { |
132 | logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url) | 122 | logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url) |
@@ -140,6 +130,13 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner) | |||
140 | 130 | ||
141 | return videoPlaylist | 131 | return videoPlaylist |
142 | } catch (err) { | 132 | } catch (err) { |
133 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
134 | logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url) | ||
135 | |||
136 | await videoPlaylist.destroy() | ||
137 | return undefined | ||
138 | } | ||
139 | |||
143 | logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err }) | 140 | logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err }) |
144 | 141 | ||
145 | await videoPlaylist.setAsRefreshed() | 142 | await videoPlaylist.setAsRefreshed() |
@@ -164,12 +161,7 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid | |||
164 | 161 | ||
165 | await Bluebird.map(elementUrls, async elementUrl => { | 162 | await Bluebird.map(elementUrls, async elementUrl => { |
166 | try { | 163 | try { |
167 | // Fetch url | 164 | const { body } = await doJSONRequest<PlaylistElementObject>(elementUrl, { activityPub: true }) |
168 | const { body } = await doRequest<PlaylistElementObject>({ | ||
169 | uri: elementUrl, | ||
170 | json: true, | ||
171 | activityPub: true | ||
172 | }) | ||
173 | 165 | ||
174 | if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`) | 166 | if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`) |
175 | 167 | ||
@@ -199,21 +191,14 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid | |||
199 | } | 191 | } |
200 | 192 | ||
201 | async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { | 193 | async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { |
202 | const options = { | ||
203 | uri: playlistUrl, | ||
204 | method: 'GET', | ||
205 | json: true, | ||
206 | activityPub: true | ||
207 | } | ||
208 | |||
209 | logger.info('Fetching remote playlist %s.', playlistUrl) | 194 | logger.info('Fetching remote playlist %s.', playlistUrl) |
210 | 195 | ||
211 | const { response, body } = await doRequest<any>(options) | 196 | const { body, statusCode } = await doJSONRequest<any>(playlistUrl, { activityPub: true }) |
212 | 197 | ||
213 | if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { | 198 | if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { |
214 | logger.debug('Remote video playlist JSON is not valid.', { body }) | 199 | logger.debug('Remote video playlist JSON is not valid.', { body }) |
215 | return { statusCode: response.statusCode, playlistObject: undefined } | 200 | return { statusCode, playlistObject: undefined } |
216 | } | 201 | } |
217 | 202 | ||
218 | return { statusCode: response.statusCode, playlistObject: body } | 203 | return { statusCode, playlistObject: body } |
219 | } | 204 | } |
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts index a86def936..070ee0f1d 100644 --- a/server/lib/activitypub/process/process-delete.ts +++ b/server/lib/activitypub/process/process-delete.ts | |||
@@ -7,7 +7,7 @@ import { VideoModel } from '../../../models/video/video' | |||
7 | import { VideoCommentModel } from '../../../models/video/video-comment' | 7 | import { VideoCommentModel } from '../../../models/video/video-comment' |
8 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' | 8 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' |
9 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 9 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
10 | import { MAccountActor, MActor, MActorSignature, MChannelActor, MChannelActorAccountActor, MCommentOwnerVideo } from '../../../types/models' | 10 | import { MAccountActor, MActor, MActorSignature, MChannelActor, MCommentOwnerVideo } from '../../../types/models' |
11 | import { markCommentAsDeleted } from '../../video-comment' | 11 | import { markCommentAsDeleted } from '../../video-comment' |
12 | import { forwardVideoRelatedActivity } from '../send/utils' | 12 | import { forwardVideoRelatedActivity } from '../send/utils' |
13 | 13 | ||
@@ -30,9 +30,7 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete | |||
30 | } else if (byActorFull.type === 'Group') { | 30 | } else if (byActorFull.type === 'Group') { |
31 | if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.') | 31 | if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.') |
32 | 32 | ||
33 | const channelToDelete = byActorFull.VideoChannel as MChannelActorAccountActor | 33 | const channelToDelete = Object.assign({}, byActorFull.VideoChannel, { Actor: byActorFull }) |
34 | channelToDelete.Actor = byActorFull | ||
35 | |||
36 | return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete) | 34 | return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete) |
37 | } | 35 | } |
38 | } | 36 | } |
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index 849f70b94..6df9b93b2 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts | |||
@@ -6,7 +6,7 @@ import { sequelizeTypescript } from '../../../initializers/database' | |||
6 | import { AccountModel } from '../../../models/account/account' | 6 | import { AccountModel } from '../../../models/account/account' |
7 | import { ActorModel } from '../../../models/activitypub/actor' | 7 | import { ActorModel } from '../../../models/activitypub/actor' |
8 | import { VideoChannelModel } from '../../../models/video/video-channel' | 8 | import { VideoChannelModel } from '../../../models/video/video-channel' |
9 | import { getAvatarInfoIfExists, updateActorAvatarInstance, updateActorInstance } from '../actor' | 9 | import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor' |
10 | import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos' | 10 | import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos' |
11 | import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' | 11 | import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' |
12 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' | 12 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' |
@@ -17,6 +17,7 @@ import { createOrUpdateVideoPlaylist } from '../playlist' | |||
17 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 17 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
18 | import { MActorSignature, MAccountIdActor } from '../../../types/models' | 18 | import { MActorSignature, MAccountIdActor } from '../../../types/models' |
19 | import { isRedundancyAccepted } from '@server/lib/redundancy' | 19 | import { isRedundancyAccepted } from '@server/lib/redundancy' |
20 | import { ActorImageType } from '@shared/models' | ||
20 | 21 | ||
21 | async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { | 22 | async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { |
22 | const { activity, byActor } = options | 23 | const { activity, byActor } = options |
@@ -119,7 +120,8 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate) | |||
119 | let accountOrChannelFieldsSave: object | 120 | let accountOrChannelFieldsSave: object |
120 | 121 | ||
121 | // Fetch icon? | 122 | // Fetch icon? |
122 | const avatarInfo = await getAvatarInfoIfExists(actorAttributesToUpdate) | 123 | const avatarInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.AVATAR) |
124 | const bannerInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.BANNER) | ||
123 | 125 | ||
124 | try { | 126 | try { |
125 | await sequelizeTypescript.transaction(async t => { | 127 | await sequelizeTypescript.transaction(async t => { |
@@ -132,11 +134,8 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate) | |||
132 | 134 | ||
133 | await updateActorInstance(actor, actorAttributesToUpdate) | 135 | await updateActorInstance(actor, actorAttributesToUpdate) |
134 | 136 | ||
135 | if (avatarInfo !== undefined) { | 137 | await updateActorImageInstance(actor, ActorImageType.AVATAR, avatarInfo, t) |
136 | const avatarOptions = Object.assign({}, avatarInfo, { onDisk: false }) | 138 | await updateActorImageInstance(actor, ActorImageType.BANNER, bannerInfo, t) |
137 | |||
138 | await updateActorAvatarInstance(actor, avatarOptions, t) | ||
139 | } | ||
140 | 139 | ||
141 | await actor.save({ transaction: t }) | 140 | await actor.save({ transaction: t }) |
142 | 141 | ||
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index 9fb218224..baded642a 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts | |||
@@ -4,7 +4,7 @@ import { VideoPrivacy } from '../../../../shared/models/videos' | |||
4 | import { VideoCommentModel } from '../../../models/video/video-comment' | 4 | import { VideoCommentModel } from '../../../models/video/video-comment' |
5 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' | 5 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' |
6 | import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' | 6 | import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' |
7 | import { logger } from '../../../helpers/logger' | 7 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
8 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' | 8 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' |
9 | import { | 9 | import { |
10 | MActorLight, | 10 | MActorLight, |
@@ -18,10 +18,12 @@ import { | |||
18 | import { getServerActor } from '@server/models/application/application' | 18 | import { getServerActor } from '@server/models/application/application' |
19 | import { ContextType } from '@shared/models/activitypub/context' | 19 | import { ContextType } from '@shared/models/activitypub/context' |
20 | 20 | ||
21 | const lTags = loggerTagsFactory('ap', 'create') | ||
22 | |||
21 | async function sendCreateVideo (video: MVideoAP, t: Transaction) { | 23 | async function sendCreateVideo (video: MVideoAP, t: Transaction) { |
22 | if (!video.hasPrivacyForFederation()) return undefined | 24 | if (!video.hasPrivacyForFederation()) return undefined |
23 | 25 | ||
24 | logger.info('Creating job to send video creation of %s.', video.url) | 26 | logger.info('Creating job to send video creation of %s.', video.url, lTags(video.uuid)) |
25 | 27 | ||
26 | const byActor = video.VideoChannel.Account.Actor | 28 | const byActor = video.VideoChannel.Account.Actor |
27 | const videoObject = video.toActivityPubObject() | 29 | const videoObject = video.toActivityPubObject() |
@@ -37,7 +39,7 @@ async function sendCreateCacheFile ( | |||
37 | video: MVideoAccountLight, | 39 | video: MVideoAccountLight, |
38 | fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo | 40 | fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo |
39 | ) { | 41 | ) { |
40 | logger.info('Creating job to send file cache of %s.', fileRedundancy.url) | 42 | logger.info('Creating job to send file cache of %s.', fileRedundancy.url, lTags(video.uuid)) |
41 | 43 | ||
42 | return sendVideoRelatedCreateActivity({ | 44 | return sendVideoRelatedCreateActivity({ |
43 | byActor, | 45 | byActor, |
@@ -51,7 +53,7 @@ async function sendCreateCacheFile ( | |||
51 | async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) { | 53 | async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) { |
52 | if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined | 54 | if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined |
53 | 55 | ||
54 | logger.info('Creating job to send create video playlist of %s.', playlist.url) | 56 | logger.info('Creating job to send create video playlist of %s.', playlist.url, lTags(playlist.uuid)) |
55 | 57 | ||
56 | const byActor = playlist.OwnerAccount.Actor | 58 | const byActor = playlist.OwnerAccount.Actor |
57 | const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC) | 59 | const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC) |
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts index 1f8a8f3c4..c22fa0893 100644 --- a/server/lib/activitypub/share.ts +++ b/server/lib/activitypub/share.ts | |||
@@ -1,15 +1,17 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
1 | import { Transaction } from 'sequelize' | 2 | import { Transaction } from 'sequelize' |
3 | import { getServerActor } from '@server/models/application/application' | ||
4 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
5 | import { logger, loggerTagsFactory } from '../../helpers/logger' | ||
6 | import { doJSONRequest } from '../../helpers/requests' | ||
7 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | ||
2 | import { VideoShareModel } from '../../models/video/video-share' | 8 | import { VideoShareModel } from '../../models/video/video-share' |
9 | import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video' | ||
10 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
3 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' | 11 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' |
4 | import { getLocalVideoAnnounceActivityPubUrl } from './url' | 12 | import { getLocalVideoAnnounceActivityPubUrl } from './url' |
5 | import * as Bluebird from 'bluebird' | 13 | |
6 | import { doRequest } from '../../helpers/requests' | 14 | const lTags = loggerTagsFactory('share') |
7 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
8 | import { logger } from '../../helpers/logger' | ||
9 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | ||
10 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
11 | import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video' | ||
12 | import { getServerActor } from '@server/models/application/application' | ||
13 | 15 | ||
14 | async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { | 16 | async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { |
15 | if (!video.hasPrivacyForFederation()) return undefined | 17 | if (!video.hasPrivacyForFederation()) return undefined |
@@ -25,7 +27,10 @@ async function changeVideoChannelShare ( | |||
25 | oldVideoChannel: MChannelActorLight, | 27 | oldVideoChannel: MChannelActorLight, |
26 | t: Transaction | 28 | t: Transaction |
27 | ) { | 29 | ) { |
28 | logger.info('Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name) | 30 | logger.info( |
31 | 'Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name, | ||
32 | lTags(video.uuid) | ||
33 | ) | ||
29 | 34 | ||
30 | await undoShareByVideoChannel(video, oldVideoChannel, t) | 35 | await undoShareByVideoChannel(video, oldVideoChannel, t) |
31 | 36 | ||
@@ -35,12 +40,7 @@ async function changeVideoChannelShare ( | |||
35 | async function addVideoShares (shareUrls: string[], video: MVideoId) { | 40 | async function addVideoShares (shareUrls: string[], video: MVideoId) { |
36 | await Bluebird.map(shareUrls, async shareUrl => { | 41 | await Bluebird.map(shareUrls, async shareUrl => { |
37 | try { | 42 | try { |
38 | // Fetch url | 43 | const { body } = await doJSONRequest<any>(shareUrl, { activityPub: true }) |
39 | const { body } = await doRequest<any>({ | ||
40 | uri: shareUrl, | ||
41 | json: true, | ||
42 | activityPub: true | ||
43 | }) | ||
44 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | 44 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') |
45 | 45 | ||
46 | const actorUrl = getAPId(body.actor) | 46 | const actorUrl = getAPId(body.actor) |
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts index d025ed7f1..e23e0c0e7 100644 --- a/server/lib/activitypub/video-comments.ts +++ b/server/lib/activitypub/video-comments.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { checkUrlsSameHost } from '../../helpers/activitypub' | ||
1 | import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments' | 3 | import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments' |
2 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
3 | import { doRequest } from '../../helpers/requests' | 5 | import { doJSONRequest } from '../../helpers/requests' |
4 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | 6 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' |
5 | import { VideoCommentModel } from '../../models/video/video-comment' | 7 | import { VideoCommentModel } from '../../models/video/video-comment' |
8 | import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video' | ||
6 | import { getOrCreateActorAndServerAndModel } from './actor' | 9 | import { getOrCreateActorAndServerAndModel } from './actor' |
7 | import { getOrCreateVideoAndAccountAndChannel } from './videos' | 10 | import { getOrCreateVideoAndAccountAndChannel } from './videos' |
8 | import * as Bluebird from 'bluebird' | ||
9 | import { checkUrlsSameHost } from '../../helpers/activitypub' | ||
10 | import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video' | ||
11 | 11 | ||
12 | type ResolveThreadParams = { | 12 | type ResolveThreadParams = { |
13 | url: string | 13 | url: string |
@@ -18,8 +18,12 @@ type ResolveThreadParams = { | |||
18 | type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> | 18 | type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> |
19 | 19 | ||
20 | async function addVideoComments (commentUrls: string[]) { | 20 | async function addVideoComments (commentUrls: string[]) { |
21 | return Bluebird.map(commentUrls, commentUrl => { | 21 | return Bluebird.map(commentUrls, async commentUrl => { |
22 | return resolveThread({ url: commentUrl, isVideo: false }) | 22 | try { |
23 | await resolveThread({ url: commentUrl, isVideo: false }) | ||
24 | } catch (err) { | ||
25 | logger.warn('Cannot resolve thread %s.', commentUrl, { err }) | ||
26 | } | ||
23 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | 27 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) |
24 | } | 28 | } |
25 | 29 | ||
@@ -126,11 +130,7 @@ async function resolveRemoteParentComment (params: ResolveThreadParams) { | |||
126 | throw new Error('Recursion limit reached when resolving a thread') | 130 | throw new Error('Recursion limit reached when resolving a thread') |
127 | } | 131 | } |
128 | 132 | ||
129 | const { body } = await doRequest<any>({ | 133 | const { body } = await doJSONRequest<any>(url, { activityPub: true }) |
130 | uri: url, | ||
131 | json: true, | ||
132 | activityPub: true | ||
133 | }) | ||
134 | 134 | ||
135 | if (sanitizeAndCheckVideoCommentObject(body) === false) { | 135 | if (sanitizeAndCheckVideoCommentObject(body) === false) { |
136 | throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body)) | 136 | throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body)) |
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts index e246b1313..f40c07fea 100644 --- a/server/lib/activitypub/video-rates.ts +++ b/server/lib/activitypub/video-rates.ts | |||
@@ -1,26 +1,22 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
1 | import { Transaction } from 'sequelize' | 2 | import { Transaction } from 'sequelize' |
2 | import { sendLike, sendUndoDislike, sendUndoLike } from './send' | 3 | import { doJSONRequest } from '@server/helpers/requests' |
3 | import { VideoRateType } from '../../../shared/models/videos' | 4 | import { VideoRateType } from '../../../shared/models/videos' |
4 | import * as Bluebird from 'bluebird' | 5 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
5 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
6 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | ||
7 | import { logger } from '../../helpers/logger' | 6 | import { logger } from '../../helpers/logger' |
8 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | 7 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' |
9 | import { doRequest } from '../../helpers/requests' | 8 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' |
10 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
11 | import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url' | ||
12 | import { sendDislike } from './send/send-dislike' | ||
13 | import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models' | 9 | import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models' |
10 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
11 | import { sendLike, sendUndoDislike, sendUndoLike } from './send' | ||
12 | import { sendDislike } from './send/send-dislike' | ||
13 | import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url' | ||
14 | 14 | ||
15 | async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) { | 15 | async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) { |
16 | await Bluebird.map(ratesUrl, async rateUrl => { | 16 | await Bluebird.map(ratesUrl, async rateUrl => { |
17 | try { | 17 | try { |
18 | // Fetch url | 18 | // Fetch url |
19 | const { body } = await doRequest<any>({ | 19 | const { body } = await doJSONRequest<any>(rateUrl, { activityPub: true }) |
20 | uri: rateUrl, | ||
21 | json: true, | ||
22 | activityPub: true | ||
23 | }) | ||
24 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | 20 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') |
25 | 21 | ||
26 | const actorUrl = getAPId(body.actor) | 22 | const actorUrl = getAPId(body.actor) |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index c02578aad..9014791c0 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -1,8 +1,7 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { maxBy, minBy } from 'lodash' | 2 | import { maxBy, minBy } from 'lodash' |
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import { basename, join } from 'path' | 4 | import { basename } from 'path' |
5 | import * as request from 'request' | ||
6 | import { Transaction } from 'sequelize/types' | 5 | import { Transaction } from 'sequelize/types' |
7 | import { TrackerModel } from '@server/models/server/tracker' | 6 | import { TrackerModel } from '@server/models/server/tracker' |
8 | import { VideoLiveModel } from '@server/models/video/video-live' | 7 | import { VideoLiveModel } from '@server/models/video/video-live' |
@@ -17,7 +16,7 @@ import { | |||
17 | ActivityUrlObject, | 16 | ActivityUrlObject, |
18 | ActivityVideoUrlObject | 17 | ActivityVideoUrlObject |
19 | } from '../../../shared/index' | 18 | } from '../../../shared/index' |
20 | import { ActivityIconObject, ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' | 19 | import { ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' |
21 | import { VideoPrivacy } from '../../../shared/models/videos' | 20 | import { VideoPrivacy } from '../../../shared/models/videos' |
22 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | 21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' |
23 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | 22 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' |
@@ -31,11 +30,10 @@ import { isArray } from '../../helpers/custom-validators/misc' | |||
31 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 30 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
32 | import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' | 31 | import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' |
33 | import { logger } from '../../helpers/logger' | 32 | import { logger } from '../../helpers/logger' |
34 | import { doRequest } from '../../helpers/requests' | 33 | import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests' |
35 | import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video' | 34 | import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video' |
36 | import { | 35 | import { |
37 | ACTIVITY_PUB, | 36 | ACTIVITY_PUB, |
38 | LAZY_STATIC_PATHS, | ||
39 | MIMETYPES, | 37 | MIMETYPES, |
40 | P2P_MEDIA_LOADER_PEER_VERSION, | 38 | P2P_MEDIA_LOADER_PEER_VERSION, |
41 | PREVIEWS_SIZE, | 39 | PREVIEWS_SIZE, |
@@ -115,36 +113,26 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid | |||
115 | } | 113 | } |
116 | } | 114 | } |
117 | 115 | ||
118 | async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.RequestResponse, videoObject: VideoObject }> { | 116 | async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> { |
119 | const options = { | ||
120 | uri: videoUrl, | ||
121 | method: 'GET', | ||
122 | json: true, | ||
123 | activityPub: true | ||
124 | } | ||
125 | |||
126 | logger.info('Fetching remote video %s.', videoUrl) | 117 | logger.info('Fetching remote video %s.', videoUrl) |
127 | 118 | ||
128 | const { response, body } = await doRequest<any>(options) | 119 | const { statusCode, body } = await doJSONRequest<any>(videoUrl, { activityPub: true }) |
129 | 120 | ||
130 | if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { | 121 | if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { |
131 | logger.debug('Remote video JSON is not valid.', { body }) | 122 | logger.debug('Remote video JSON is not valid.', { body }) |
132 | return { response, videoObject: undefined } | 123 | return { statusCode, videoObject: undefined } |
133 | } | 124 | } |
134 | 125 | ||
135 | return { response, videoObject: body } | 126 | return { statusCode, videoObject: body } |
136 | } | 127 | } |
137 | 128 | ||
138 | async function fetchRemoteVideoDescription (video: MVideoAccountLight) { | 129 | async function fetchRemoteVideoDescription (video: MVideoAccountLight) { |
139 | const host = video.VideoChannel.Account.Actor.Server.host | 130 | const host = video.VideoChannel.Account.Actor.Server.host |
140 | const path = video.getDescriptionAPIPath() | 131 | const path = video.getDescriptionAPIPath() |
141 | const options = { | 132 | const url = REMOTE_SCHEME.HTTP + '://' + host + path |
142 | uri: REMOTE_SCHEME.HTTP + '://' + host + path, | ||
143 | json: true | ||
144 | } | ||
145 | 133 | ||
146 | const { body } = await doRequest<any>(options) | 134 | const { body } = await doJSONRequest<any>(url) |
147 | return body.description ? body.description : '' | 135 | return body.description || '' |
148 | } | 136 | } |
149 | 137 | ||
150 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) { | 138 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) { |
@@ -378,13 +366,13 @@ async function updateVideoFromAP (options: { | |||
378 | 366 | ||
379 | if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) | 367 | if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) |
380 | 368 | ||
381 | if (videoUpdated.getPreview()) { | 369 | const previewIcon = getPreviewFromIcons(videoObject) |
382 | const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), video) | 370 | if (videoUpdated.getPreview() && previewIcon) { |
383 | const previewModel = createPlaceholderThumbnail({ | 371 | const previewModel = createPlaceholderThumbnail({ |
384 | fileUrl: previewUrl, | 372 | fileUrl: previewIcon.url, |
385 | video, | 373 | video, |
386 | type: ThumbnailType.PREVIEW, | 374 | type: ThumbnailType.PREVIEW, |
387 | size: PREVIEWS_SIZE | 375 | size: previewIcon |
388 | }) | 376 | }) |
389 | await videoUpdated.addAndSaveThumbnail(previewModel, t) | 377 | await videoUpdated.addAndSaveThumbnail(previewModel, t) |
390 | } | 378 | } |
@@ -534,14 +522,7 @@ async function refreshVideoIfNeeded (options: { | |||
534 | : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) | 522 | : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) |
535 | 523 | ||
536 | try { | 524 | try { |
537 | const { response, videoObject } = await fetchRemoteVideo(video.url) | 525 | const { videoObject } = await fetchRemoteVideo(video.url) |
538 | if (response.statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
539 | logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url) | ||
540 | |||
541 | // Video does not exist anymore | ||
542 | await video.destroy() | ||
543 | return undefined | ||
544 | } | ||
545 | 526 | ||
546 | if (videoObject === undefined) { | 527 | if (videoObject === undefined) { |
547 | logger.warn('Cannot refresh remote video %s: invalid body.', video.url) | 528 | logger.warn('Cannot refresh remote video %s: invalid body.', video.url) |
@@ -565,6 +546,14 @@ async function refreshVideoIfNeeded (options: { | |||
565 | 546 | ||
566 | return video | 547 | return video |
567 | } catch (err) { | 548 | } catch (err) { |
549 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
550 | logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url) | ||
551 | |||
552 | // Video does not exist anymore | ||
553 | await video.destroy() | ||
554 | return undefined | ||
555 | } | ||
556 | |||
568 | logger.warn('Cannot refresh video %s.', options.video.url, { err }) | 557 | logger.warn('Cannot refresh video %s.', options.video.url, { err }) |
569 | 558 | ||
570 | ActorFollowScoreCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId) | 559 | ActorFollowScoreCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId) |
@@ -638,15 +627,17 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi | |||
638 | 627 | ||
639 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) | 628 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) |
640 | 629 | ||
641 | const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), videoCreated) | 630 | const previewIcon = getPreviewFromIcons(videoObject) |
642 | const previewModel = createPlaceholderThumbnail({ | 631 | if (previewIcon) { |
643 | fileUrl: previewUrl, | 632 | const previewModel = createPlaceholderThumbnail({ |
644 | video: videoCreated, | 633 | fileUrl: previewIcon.url, |
645 | type: ThumbnailType.PREVIEW, | 634 | video: videoCreated, |
646 | size: PREVIEWS_SIZE | 635 | type: ThumbnailType.PREVIEW, |
647 | }) | 636 | size: previewIcon |
637 | }) | ||
648 | 638 | ||
649 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) | 639 | await videoCreated.addAndSaveThumbnail(previewModel, t) |
640 | } | ||
650 | 641 | ||
651 | // Process files | 642 | // Process files |
652 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url) | 643 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url) |
@@ -906,12 +897,6 @@ function getPreviewFromIcons (videoObject: VideoObject) { | |||
906 | return maxBy(validIcons, 'width') | 897 | return maxBy(validIcons, 'width') |
907 | } | 898 | } |
908 | 899 | ||
909 | function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoWithHost) { | ||
910 | return previewIcon | ||
911 | ? previewIcon.url | ||
912 | : buildRemoteVideoBaseUrl(video, join(LAZY_STATIC_PATHS.PREVIEWS, video.generatePreviewName())) | ||
913 | } | ||
914 | |||
915 | function getTrackerUrls (object: VideoObject, video: MVideoWithHost) { | 900 | function getTrackerUrls (object: VideoObject, video: MVideoWithHost) { |
916 | let wsFound = false | 901 | let wsFound = false |
917 | 902 | ||
diff --git a/server/lib/actor-image.ts b/server/lib/actor-image.ts new file mode 100644 index 000000000..f271f0b5b --- /dev/null +++ b/server/lib/actor-image.ts | |||
@@ -0,0 +1,97 @@ | |||
1 | import 'multer' | ||
2 | import { queue } from 'async' | ||
3 | import * as LRUCache from 'lru-cache' | ||
4 | import { extname, join } from 'path' | ||
5 | import { v4 as uuidv4 } from 'uuid' | ||
6 | import { ActorImageType } from '@shared/models' | ||
7 | import { retryTransactionWrapper } from '../helpers/database-utils' | ||
8 | import { processImage } from '../helpers/image-utils' | ||
9 | import { downloadImage } from '../helpers/requests' | ||
10 | import { CONFIG } from '../initializers/config' | ||
11 | import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' | ||
12 | import { sequelizeTypescript } from '../initializers/database' | ||
13 | import { MAccountDefault, MChannelDefault } from '../types/models' | ||
14 | import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actor' | ||
15 | import { sendUpdateActor } from './activitypub/send' | ||
16 | |||
17 | async function updateLocalActorImageFile ( | ||
18 | accountOrChannel: MAccountDefault | MChannelDefault, | ||
19 | imagePhysicalFile: Express.Multer.File, | ||
20 | type: ActorImageType | ||
21 | ) { | ||
22 | const imageSize = type === ActorImageType.AVATAR | ||
23 | ? ACTOR_IMAGES_SIZE.AVATARS | ||
24 | : ACTOR_IMAGES_SIZE.BANNERS | ||
25 | |||
26 | const extension = extname(imagePhysicalFile.filename) | ||
27 | |||
28 | const imageName = uuidv4() + extension | ||
29 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) | ||
30 | await processImage(imagePhysicalFile.path, destination, imageSize) | ||
31 | |||
32 | return retryTransactionWrapper(() => { | ||
33 | return sequelizeTypescript.transaction(async t => { | ||
34 | const actorImageInfo = { | ||
35 | name: imageName, | ||
36 | fileUrl: null, | ||
37 | height: imageSize.height, | ||
38 | width: imageSize.width, | ||
39 | onDisk: true | ||
40 | } | ||
41 | |||
42 | const updatedActor = await updateActorImageInstance(accountOrChannel.Actor, type, actorImageInfo, t) | ||
43 | await updatedActor.save({ transaction: t }) | ||
44 | |||
45 | await sendUpdateActor(accountOrChannel, t) | ||
46 | |||
47 | return type === ActorImageType.AVATAR | ||
48 | ? updatedActor.Avatar | ||
49 | : updatedActor.Banner | ||
50 | }) | ||
51 | }) | ||
52 | } | ||
53 | |||
54 | async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) { | ||
55 | return retryTransactionWrapper(() => { | ||
56 | return sequelizeTypescript.transaction(async t => { | ||
57 | const updatedActor = await deleteActorImageInstance(accountOrChannel.Actor, type, t) | ||
58 | await updatedActor.save({ transaction: t }) | ||
59 | |||
60 | await sendUpdateActor(accountOrChannel, t) | ||
61 | |||
62 | return updatedActor.Avatar | ||
63 | }) | ||
64 | }) | ||
65 | } | ||
66 | |||
67 | type DownloadImageQueueTask = { fileUrl: string, filename: string, type: ActorImageType } | ||
68 | |||
69 | const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { | ||
70 | const size = task.type === ActorImageType.AVATAR | ||
71 | ? ACTOR_IMAGES_SIZE.AVATARS | ||
72 | : ACTOR_IMAGES_SIZE.BANNERS | ||
73 | |||
74 | downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, size) | ||
75 | .then(() => cb()) | ||
76 | .catch(err => cb(err)) | ||
77 | }, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE) | ||
78 | |||
79 | function pushActorImageProcessInQueue (task: DownloadImageQueueTask) { | ||
80 | return new Promise<void>((res, rej) => { | ||
81 | downloadImageQueue.push(task, err => { | ||
82 | if (err) return rej(err) | ||
83 | |||
84 | return res() | ||
85 | }) | ||
86 | }) | ||
87 | } | ||
88 | |||
89 | // Unsafe so could returns paths that does not exist anymore | ||
90 | const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.ACTOR_IMAGE_STATIC.MAX_SIZE }) | ||
91 | |||
92 | export { | ||
93 | actorImagePathUnsafeCache, | ||
94 | updateLocalActorImageFile, | ||
95 | deleteLocalActorImageFile, | ||
96 | pushActorImageProcessInQueue | ||
97 | } | ||
diff --git a/server/lib/auth.ts b/server/lib/auth/external-auth.ts index dbd421a7b..80f5064b6 100644 --- a/server/lib/auth.ts +++ b/server/lib/auth/external-auth.ts | |||
@@ -1,28 +1,16 @@ | |||
1 | |||
1 | import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' | 2 | import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' |
2 | import { logger } from '@server/helpers/logger' | 3 | import { logger } from '@server/helpers/logger' |
3 | import { generateRandomString } from '@server/helpers/utils' | 4 | import { generateRandomString } from '@server/helpers/utils' |
4 | import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' | 5 | import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' |
5 | import { revokeToken } from '@server/lib/oauth-model' | ||
6 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 6 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
7 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | 7 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' |
8 | import { UserRole } from '@shared/models' | ||
9 | import { | 8 | import { |
10 | RegisterServerAuthenticatedResult, | 9 | RegisterServerAuthenticatedResult, |
11 | RegisterServerAuthPassOptions, | 10 | RegisterServerAuthPassOptions, |
12 | RegisterServerExternalAuthenticatedResult | 11 | RegisterServerExternalAuthenticatedResult |
13 | } from '@server/types/plugins/register-server-auth.model' | 12 | } from '@server/types/plugins/register-server-auth.model' |
14 | import * as express from 'express' | 13 | import { UserRole } from '@shared/models' |
15 | import * as OAuthServer from 'express-oauth-server' | ||
16 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
17 | |||
18 | const oAuthServer = new OAuthServer({ | ||
19 | useErrorHandler: true, | ||
20 | accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN, | ||
21 | refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN, | ||
22 | allowExtendedTokenAttributes: true, | ||
23 | continueMiddleware: true, | ||
24 | model: require('./oauth-model') | ||
25 | }) | ||
26 | 14 | ||
27 | // Token is the key, expiration date is the value | 15 | // Token is the key, expiration date is the value |
28 | const authBypassTokens = new Map<string, { | 16 | const authBypassTokens = new Map<string, { |
@@ -37,42 +25,6 @@ const authBypassTokens = new Map<string, { | |||
37 | npmName: string | 25 | npmName: string |
38 | }>() | 26 | }>() |
39 | 27 | ||
40 | async function handleLogin (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
41 | const grantType = req.body.grant_type | ||
42 | |||
43 | if (grantType === 'password') { | ||
44 | if (req.body.externalAuthToken) proxifyExternalAuthBypass(req, res) | ||
45 | else await proxifyPasswordGrant(req, res) | ||
46 | } else if (grantType === 'refresh_token') { | ||
47 | await proxifyRefreshGrant(req, res) | ||
48 | } | ||
49 | |||
50 | return forwardTokenReq(req, res, next) | ||
51 | } | ||
52 | |||
53 | async function handleTokenRevocation (req: express.Request, res: express.Response) { | ||
54 | const token = res.locals.oauth.token | ||
55 | |||
56 | res.locals.explicitLogout = true | ||
57 | const result = await revokeToken(token) | ||
58 | |||
59 | // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released | ||
60 | // oAuthServer.revoke(req, res, err => { | ||
61 | // if (err) { | ||
62 | // logger.warn('Error in revoke token handler.', { err }) | ||
63 | // | ||
64 | // return res.status(err.status) | ||
65 | // .json({ | ||
66 | // error: err.message, | ||
67 | // code: err.name | ||
68 | // }) | ||
69 | // .end() | ||
70 | // } | ||
71 | // }) | ||
72 | |||
73 | return res.json(result) | ||
74 | } | ||
75 | |||
76 | async function onExternalUserAuthenticated (options: { | 28 | async function onExternalUserAuthenticated (options: { |
77 | npmName: string | 29 | npmName: string |
78 | authName: string | 30 | authName: string |
@@ -107,7 +59,7 @@ async function onExternalUserAuthenticated (options: { | |||
107 | authName | 59 | authName |
108 | }) | 60 | }) |
109 | 61 | ||
110 | // Cleanup | 62 | // Cleanup expired tokens |
111 | const now = new Date() | 63 | const now = new Date() |
112 | for (const [ key, value ] of authBypassTokens) { | 64 | for (const [ key, value ] of authBypassTokens) { |
113 | if (value.expires.getTime() < now.getTime()) { | 65 | if (value.expires.getTime() < now.getTime()) { |
@@ -118,37 +70,15 @@ async function onExternalUserAuthenticated (options: { | |||
118 | res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`) | 70 | res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`) |
119 | } | 71 | } |
120 | 72 | ||
121 | // --------------------------------------------------------------------------- | 73 | async function getAuthNameFromRefreshGrant (refreshToken?: string) { |
122 | 74 | if (!refreshToken) return undefined | |
123 | export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation } | ||
124 | |||
125 | // --------------------------------------------------------------------------- | ||
126 | |||
127 | function forwardTokenReq (req: express.Request, res: express.Response, next?: express.NextFunction) { | ||
128 | return oAuthServer.token()(req, res, err => { | ||
129 | if (err) { | ||
130 | logger.warn('Login error.', { err }) | ||
131 | |||
132 | return res.status(err.status) | ||
133 | .json({ | ||
134 | error: err.message, | ||
135 | code: err.name | ||
136 | }) | ||
137 | } | ||
138 | |||
139 | if (next) return next() | ||
140 | }) | ||
141 | } | ||
142 | |||
143 | async function proxifyRefreshGrant (req: express.Request, res: express.Response) { | ||
144 | const refreshToken = req.body.refresh_token | ||
145 | if (!refreshToken) return | ||
146 | 75 | ||
147 | const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken) | 76 | const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken) |
148 | if (tokenModel?.authName) res.locals.refreshTokenAuthName = tokenModel.authName | 77 | |
78 | return tokenModel?.authName | ||
149 | } | 79 | } |
150 | 80 | ||
151 | async function proxifyPasswordGrant (req: express.Request, res: express.Response) { | 81 | async function getBypassFromPasswordGrant (username: string, password: string) { |
152 | const plugins = PluginManager.Instance.getIdAndPassAuths() | 82 | const plugins = PluginManager.Instance.getIdAndPassAuths() |
153 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] | 83 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] |
154 | 84 | ||
@@ -174,8 +104,8 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response | |||
174 | }) | 104 | }) |
175 | 105 | ||
176 | const loginOptions = { | 106 | const loginOptions = { |
177 | id: req.body.username, | 107 | id: username, |
178 | password: req.body.password | 108 | password |
179 | } | 109 | } |
180 | 110 | ||
181 | for (const pluginAuth of pluginAuths) { | 111 | for (const pluginAuth of pluginAuths) { |
@@ -199,49 +129,41 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response | |||
199 | authName, npmName, loginOptions.id | 129 | authName, npmName, loginOptions.id |
200 | ) | 130 | ) |
201 | 131 | ||
202 | res.locals.bypassLogin = { | 132 | return { |
203 | bypass: true, | 133 | bypass: true, |
204 | pluginName: pluginAuth.npmName, | 134 | pluginName: pluginAuth.npmName, |
205 | authName: authOptions.authName, | 135 | authName: authOptions.authName, |
206 | user: buildUserResult(loginResult) | 136 | user: buildUserResult(loginResult) |
207 | } | 137 | } |
208 | |||
209 | return | ||
210 | } catch (err) { | 138 | } catch (err) { |
211 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) | 139 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) |
212 | } | 140 | } |
213 | } | 141 | } |
142 | |||
143 | return undefined | ||
214 | } | 144 | } |
215 | 145 | ||
216 | function proxifyExternalAuthBypass (req: express.Request, res: express.Response) { | 146 | function getBypassFromExternalAuth (username: string, externalAuthToken: string) { |
217 | const obj = authBypassTokens.get(req.body.externalAuthToken) | 147 | const obj = authBypassTokens.get(externalAuthToken) |
218 | if (!obj) { | 148 | if (!obj) throw new Error('Cannot authenticate user with unknown bypass token') |
219 | logger.error('Cannot authenticate user with unknown bypass token') | ||
220 | return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) | ||
221 | } | ||
222 | 149 | ||
223 | const { expires, user, authName, npmName } = obj | 150 | const { expires, user, authName, npmName } = obj |
224 | 151 | ||
225 | const now = new Date() | 152 | const now = new Date() |
226 | if (now.getTime() > expires.getTime()) { | 153 | if (now.getTime() > expires.getTime()) { |
227 | logger.error('Cannot authenticate user with an expired external auth token') | 154 | throw new Error('Cannot authenticate user with an expired external auth token') |
228 | return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) | ||
229 | } | 155 | } |
230 | 156 | ||
231 | if (user.username !== req.body.username) { | 157 | if (user.username !== username) { |
232 | logger.error('Cannot authenticate user %s with invalid username %s.', req.body.username) | 158 | throw new Error(`Cannot authenticate user ${user.username} with invalid username ${username}`) |
233 | return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) | ||
234 | } | 159 | } |
235 | 160 | ||
236 | // Bypass oauth library validation | ||
237 | req.body.password = 'fake' | ||
238 | |||
239 | logger.info( | 161 | logger.info( |
240 | 'Auth success with external auth method %s of plugin %s for %s.', | 162 | 'Auth success with external auth method %s of plugin %s for %s.', |
241 | authName, npmName, user.email | 163 | authName, npmName, user.email |
242 | ) | 164 | ) |
243 | 165 | ||
244 | res.locals.bypassLogin = { | 166 | return { |
245 | bypass: true, | 167 | bypass: true, |
246 | pluginName: npmName, | 168 | pluginName: npmName, |
247 | authName: authName, | 169 | authName: authName, |
@@ -286,3 +208,12 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) { | |||
286 | displayName: pluginResult.displayName || pluginResult.username | 208 | displayName: pluginResult.displayName || pluginResult.username |
287 | } | 209 | } |
288 | } | 210 | } |
211 | |||
212 | // --------------------------------------------------------------------------- | ||
213 | |||
214 | export { | ||
215 | onExternalUserAuthenticated, | ||
216 | getBypassFromExternalAuth, | ||
217 | getAuthNameFromRefreshGrant, | ||
218 | getBypassFromPasswordGrant | ||
219 | } | ||
diff --git a/server/lib/oauth-model.ts b/server/lib/auth/oauth-model.ts index a2c53a2c9..b9c69eb2d 100644 --- a/server/lib/oauth-model.ts +++ b/server/lib/auth/oauth-model.ts | |||
@@ -1,49 +1,36 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import * as LRUCache from 'lru-cache' | ||
3 | import { AccessDeniedError } from 'oauth2-server' | 2 | import { AccessDeniedError } from 'oauth2-server' |
4 | import { Transaction } from 'sequelize' | ||
5 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 3 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
6 | import { ActorModel } from '@server/models/activitypub/actor' | 4 | import { ActorModel } from '@server/models/activitypub/actor' |
5 | import { MOAuthClient } from '@server/types/models' | ||
7 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 6 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
8 | import { MUser } from '@server/types/models/user/user' | 7 | import { MUser } from '@server/types/models/user/user' |
9 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' | 8 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' |
10 | import { UserRole } from '@shared/models/users/user-role' | 9 | import { UserRole } from '@shared/models/users/user-role' |
11 | import { logger } from '../helpers/logger' | 10 | import { logger } from '../../helpers/logger' |
12 | import { CONFIG } from '../initializers/config' | 11 | import { CONFIG } from '../../initializers/config' |
13 | import { LRU_CACHE } from '../initializers/constants' | 12 | import { UserModel } from '../../models/account/user' |
14 | import { UserModel } from '../models/account/user' | 13 | import { OAuthClientModel } from '../../models/oauth/oauth-client' |
15 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 14 | import { OAuthTokenModel } from '../../models/oauth/oauth-token' |
16 | import { OAuthTokenModel } from '../models/oauth/oauth-token' | 15 | import { createUserAccountAndChannelAndPlaylist } from '../user' |
17 | import { createUserAccountAndChannelAndPlaylist } from './user' | 16 | import { TokensCache } from './tokens-cache' |
18 | 17 | ||
19 | type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } | 18 | type TokenInfo = { |
20 | 19 | accessToken: string | |
21 | const accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) | 20 | refreshToken: string |
22 | const userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) | 21 | accessTokenExpiresAt: Date |
23 | 22 | refreshTokenExpiresAt: Date | |
24 | // --------------------------------------------------------------------------- | ||
25 | |||
26 | function deleteUserToken (userId: number, t?: Transaction) { | ||
27 | clearCacheByUserId(userId) | ||
28 | |||
29 | return OAuthTokenModel.deleteUserToken(userId, t) | ||
30 | } | 23 | } |
31 | 24 | ||
32 | function clearCacheByUserId (userId: number) { | 25 | export type BypassLogin = { |
33 | const token = userHavingToken.get(userId) | 26 | bypass: boolean |
34 | 27 | pluginName: string | |
35 | if (token !== undefined) { | 28 | authName?: string |
36 | accessTokenCache.del(token) | 29 | user: { |
37 | userHavingToken.del(userId) | 30 | username: string |
38 | } | 31 | email: string |
39 | } | 32 | displayName: string |
40 | 33 | role: UserRole | |
41 | function clearCacheByToken (token: string) { | ||
42 | const tokenModel = accessTokenCache.get(token) | ||
43 | |||
44 | if (tokenModel !== undefined) { | ||
45 | userHavingToken.del(tokenModel.userId) | ||
46 | accessTokenCache.del(token) | ||
47 | } | 34 | } |
48 | } | 35 | } |
49 | 36 | ||
@@ -54,15 +41,12 @@ async function getAccessToken (bearerToken: string) { | |||
54 | 41 | ||
55 | let tokenModel: MOAuthTokenUser | 42 | let tokenModel: MOAuthTokenUser |
56 | 43 | ||
57 | if (accessTokenCache.has(bearerToken)) { | 44 | if (TokensCache.Instance.hasToken(bearerToken)) { |
58 | tokenModel = accessTokenCache.get(bearerToken) | 45 | tokenModel = TokensCache.Instance.getByToken(bearerToken) |
59 | } else { | 46 | } else { |
60 | tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) | 47 | tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) |
61 | 48 | ||
62 | if (tokenModel) { | 49 | if (tokenModel) TokensCache.Instance.setToken(tokenModel) |
63 | accessTokenCache.set(bearerToken, tokenModel) | ||
64 | userHavingToken.set(tokenModel.userId, tokenModel.accessToken) | ||
65 | } | ||
66 | } | 50 | } |
67 | 51 | ||
68 | if (!tokenModel) return undefined | 52 | if (!tokenModel) return undefined |
@@ -99,16 +83,13 @@ async function getRefreshToken (refreshToken: string) { | |||
99 | return tokenInfo | 83 | return tokenInfo |
100 | } | 84 | } |
101 | 85 | ||
102 | async function getUser (usernameOrEmail?: string, password?: string) { | 86 | async function getUser (usernameOrEmail?: string, password?: string, bypassLogin?: BypassLogin) { |
103 | const res: express.Response = this.request.res | ||
104 | |||
105 | // Special treatment coming from a plugin | 87 | // Special treatment coming from a plugin |
106 | if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) { | 88 | if (bypassLogin && bypassLogin.bypass === true) { |
107 | const obj = res.locals.bypassLogin | 89 | logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) |
108 | logger.info('Bypassing oauth login by plugin %s.', obj.pluginName) | ||
109 | 90 | ||
110 | let user = await UserModel.loadByEmail(obj.user.email) | 91 | let user = await UserModel.loadByEmail(bypassLogin.user.email) |
111 | if (!user) user = await createUserFromExternal(obj.pluginName, obj.user) | 92 | if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user) |
112 | 93 | ||
113 | // Cannot create a user | 94 | // Cannot create a user |
114 | if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') | 95 | if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') |
@@ -117,7 +98,7 @@ async function getUser (usernameOrEmail?: string, password?: string) { | |||
117 | // Then we just go through a regular login process | 98 | // Then we just go through a regular login process |
118 | if (user.pluginAuth !== null) { | 99 | if (user.pluginAuth !== null) { |
119 | // This user does not belong to this plugin, skip it | 100 | // This user does not belong to this plugin, skip it |
120 | if (user.pluginAuth !== obj.pluginName) return null | 101 | if (user.pluginAuth !== bypassLogin.pluginName) return null |
121 | 102 | ||
122 | checkUserValidityOrThrow(user) | 103 | checkUserValidityOrThrow(user) |
123 | 104 | ||
@@ -143,18 +124,25 @@ async function getUser (usernameOrEmail?: string, password?: string) { | |||
143 | return user | 124 | return user |
144 | } | 125 | } |
145 | 126 | ||
146 | async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ success: boolean, redirectUrl?: string }> { | 127 | async function revokeToken ( |
147 | const res: express.Response = this.request.res | 128 | tokenInfo: { refreshToken: string }, |
129 | options: { | ||
130 | req?: express.Request | ||
131 | explicitLogout?: boolean | ||
132 | } = {} | ||
133 | ): Promise<{ success: boolean, redirectUrl?: string }> { | ||
134 | const { req, explicitLogout } = options | ||
135 | |||
148 | const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) | 136 | const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) |
149 | 137 | ||
150 | if (token) { | 138 | if (token) { |
151 | let redirectUrl: string | 139 | let redirectUrl: string |
152 | 140 | ||
153 | if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) { | 141 | if (explicitLogout === true && token.User.pluginAuth && token.authName) { |
154 | redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, this.request) | 142 | redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, req) |
155 | } | 143 | } |
156 | 144 | ||
157 | clearCacheByToken(token.accessToken) | 145 | TokensCache.Instance.clearCacheByToken(token.accessToken) |
158 | 146 | ||
159 | token.destroy() | 147 | token.destroy() |
160 | .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) | 148 | .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) |
@@ -165,14 +153,22 @@ async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ succ | |||
165 | return { success: false } | 153 | return { success: false } |
166 | } | 154 | } |
167 | 155 | ||
168 | async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { | 156 | async function saveToken ( |
169 | const res: express.Response = this.request.res | 157 | token: TokenInfo, |
170 | 158 | client: MOAuthClient, | |
159 | user: MUser, | ||
160 | options: { | ||
161 | refreshTokenAuthName?: string | ||
162 | bypassLogin?: BypassLogin | ||
163 | } = {} | ||
164 | ) { | ||
165 | const { refreshTokenAuthName, bypassLogin } = options | ||
171 | let authName: string = null | 166 | let authName: string = null |
172 | if (res.locals.bypassLogin?.bypass === true) { | 167 | |
173 | authName = res.locals.bypassLogin.authName | 168 | if (bypassLogin?.bypass === true) { |
174 | } else if (res.locals.refreshTokenAuthName) { | 169 | authName = bypassLogin.authName |
175 | authName = res.locals.refreshTokenAuthName | 170 | } else if (refreshTokenAuthName) { |
171 | authName = refreshTokenAuthName | ||
176 | } | 172 | } |
177 | 173 | ||
178 | logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') | 174 | logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') |
@@ -199,17 +195,12 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User | |||
199 | refreshTokenExpiresAt: tokenCreated.refreshTokenExpiresAt, | 195 | refreshTokenExpiresAt: tokenCreated.refreshTokenExpiresAt, |
200 | client, | 196 | client, |
201 | user, | 197 | user, |
202 | refresh_token_expires_in: Math.floor((tokenCreated.refreshTokenExpiresAt.getTime() - new Date().getTime()) / 1000) | 198 | accessTokenExpiresIn: buildExpiresIn(tokenCreated.accessTokenExpiresAt), |
199 | refreshTokenExpiresIn: buildExpiresIn(tokenCreated.refreshTokenExpiresAt) | ||
203 | } | 200 | } |
204 | } | 201 | } |
205 | 202 | ||
206 | // --------------------------------------------------------------------------- | ||
207 | |||
208 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications | ||
209 | export { | 203 | export { |
210 | deleteUserToken, | ||
211 | clearCacheByUserId, | ||
212 | clearCacheByToken, | ||
213 | getAccessToken, | 204 | getAccessToken, |
214 | getClient, | 205 | getClient, |
215 | getRefreshToken, | 206 | getRefreshToken, |
@@ -218,6 +209,8 @@ export { | |||
218 | saveToken | 209 | saveToken |
219 | } | 210 | } |
220 | 211 | ||
212 | // --------------------------------------------------------------------------- | ||
213 | |||
221 | async function createUserFromExternal (pluginAuth: string, options: { | 214 | async function createUserFromExternal (pluginAuth: string, options: { |
222 | username: string | 215 | username: string |
223 | email: string | 216 | email: string |
@@ -252,3 +245,7 @@ async function createUserFromExternal (pluginAuth: string, options: { | |||
252 | function checkUserValidityOrThrow (user: MUser) { | 245 | function checkUserValidityOrThrow (user: MUser) { |
253 | if (user.blocked) throw new AccessDeniedError('User is blocked.') | 246 | if (user.blocked) throw new AccessDeniedError('User is blocked.') |
254 | } | 247 | } |
248 | |||
249 | function buildExpiresIn (expiresAt: Date) { | ||
250 | return Math.floor((expiresAt.getTime() - new Date().getTime()) / 1000) | ||
251 | } | ||
diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts new file mode 100644 index 000000000..5b6130d56 --- /dev/null +++ b/server/lib/auth/oauth.ts | |||
@@ -0,0 +1,180 @@ | |||
1 | import * as express from 'express' | ||
2 | import { | ||
3 | InvalidClientError, | ||
4 | InvalidGrantError, | ||
5 | InvalidRequestError, | ||
6 | Request, | ||
7 | Response, | ||
8 | UnauthorizedClientError, | ||
9 | UnsupportedGrantTypeError | ||
10 | } from 'oauth2-server' | ||
11 | import { randomBytesPromise, sha1 } from '@server/helpers/core-utils' | ||
12 | import { MOAuthClient } from '@server/types/models' | ||
13 | import { OAUTH_LIFETIME } from '../../initializers/constants' | ||
14 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' | ||
15 | |||
16 | /** | ||
17 | * | ||
18 | * Reimplement some functions of OAuth2Server to inject external auth methods | ||
19 | * | ||
20 | */ | ||
21 | |||
22 | const oAuthServer = new (require('oauth2-server'))({ | ||
23 | accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN, | ||
24 | refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN, | ||
25 | |||
26 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications | ||
27 | model: require('./oauth-model') | ||
28 | }) | ||
29 | |||
30 | // --------------------------------------------------------------------------- | ||
31 | |||
32 | async function handleOAuthToken (req: express.Request, options: { refreshTokenAuthName?: string, bypassLogin?: BypassLogin }) { | ||
33 | const request = new Request(req) | ||
34 | const { refreshTokenAuthName, bypassLogin } = options | ||
35 | |||
36 | if (request.method !== 'POST') { | ||
37 | throw new InvalidRequestError('Invalid request: method must be POST') | ||
38 | } | ||
39 | |||
40 | if (!request.is([ 'application/x-www-form-urlencoded' ])) { | ||
41 | throw new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded') | ||
42 | } | ||
43 | |||
44 | const clientId = request.body.client_id | ||
45 | const clientSecret = request.body.client_secret | ||
46 | |||
47 | if (!clientId || !clientSecret) { | ||
48 | throw new InvalidClientError('Invalid client: cannot retrieve client credentials') | ||
49 | } | ||
50 | |||
51 | const client = await getClient(clientId, clientSecret) | ||
52 | if (!client) { | ||
53 | throw new InvalidClientError('Invalid client: client is invalid') | ||
54 | } | ||
55 | |||
56 | const grantType = request.body.grant_type | ||
57 | if (!grantType) { | ||
58 | throw new InvalidRequestError('Missing parameter: `grant_type`') | ||
59 | } | ||
60 | |||
61 | if (![ 'password', 'refresh_token' ].includes(grantType)) { | ||
62 | throw new UnsupportedGrantTypeError('Unsupported grant type: `grant_type` is invalid') | ||
63 | } | ||
64 | |||
65 | if (!client.grants.includes(grantType)) { | ||
66 | throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid') | ||
67 | } | ||
68 | |||
69 | if (grantType === 'password') { | ||
70 | return handlePasswordGrant({ | ||
71 | request, | ||
72 | client, | ||
73 | bypassLogin | ||
74 | }) | ||
75 | } | ||
76 | |||
77 | return handleRefreshGrant({ | ||
78 | request, | ||
79 | client, | ||
80 | refreshTokenAuthName | ||
81 | }) | ||
82 | } | ||
83 | |||
84 | async function handleOAuthAuthenticate ( | ||
85 | req: express.Request, | ||
86 | res: express.Response, | ||
87 | authenticateInQuery = false | ||
88 | ) { | ||
89 | const options = authenticateInQuery | ||
90 | ? { allowBearerTokensInQueryString: true } | ||
91 | : {} | ||
92 | |||
93 | return oAuthServer.authenticate(new Request(req), new Response(res), options) | ||
94 | } | ||
95 | |||
96 | export { | ||
97 | handleOAuthToken, | ||
98 | handleOAuthAuthenticate | ||
99 | } | ||
100 | |||
101 | // --------------------------------------------------------------------------- | ||
102 | |||
103 | async function handlePasswordGrant (options: { | ||
104 | request: Request | ||
105 | client: MOAuthClient | ||
106 | bypassLogin?: BypassLogin | ||
107 | }) { | ||
108 | const { request, client, bypassLogin } = options | ||
109 | |||
110 | if (!request.body.username) { | ||
111 | throw new InvalidRequestError('Missing parameter: `username`') | ||
112 | } | ||
113 | |||
114 | if (!bypassLogin && !request.body.password) { | ||
115 | throw new InvalidRequestError('Missing parameter: `password`') | ||
116 | } | ||
117 | |||
118 | const user = await getUser(request.body.username, request.body.password, bypassLogin) | ||
119 | if (!user) throw new InvalidGrantError('Invalid grant: user credentials are invalid') | ||
120 | |||
121 | const token = await buildToken() | ||
122 | |||
123 | return saveToken(token, client, user, { bypassLogin }) | ||
124 | } | ||
125 | |||
126 | async function handleRefreshGrant (options: { | ||
127 | request: Request | ||
128 | client: MOAuthClient | ||
129 | refreshTokenAuthName: string | ||
130 | }) { | ||
131 | const { request, client, refreshTokenAuthName } = options | ||
132 | |||
133 | if (!request.body.refresh_token) { | ||
134 | throw new InvalidRequestError('Missing parameter: `refresh_token`') | ||
135 | } | ||
136 | |||
137 | const refreshToken = await getRefreshToken(request.body.refresh_token) | ||
138 | |||
139 | if (!refreshToken) { | ||
140 | throw new InvalidGrantError('Invalid grant: refresh token is invalid') | ||
141 | } | ||
142 | |||
143 | if (refreshToken.client.id !== client.id) { | ||
144 | throw new InvalidGrantError('Invalid grant: refresh token is invalid') | ||
145 | } | ||
146 | |||
147 | if (refreshToken.refreshTokenExpiresAt && refreshToken.refreshTokenExpiresAt < new Date()) { | ||
148 | throw new InvalidGrantError('Invalid grant: refresh token has expired') | ||
149 | } | ||
150 | |||
151 | await revokeToken({ refreshToken: refreshToken.refreshToken }) | ||
152 | |||
153 | const token = await buildToken() | ||
154 | |||
155 | return saveToken(token, client, refreshToken.user, { refreshTokenAuthName }) | ||
156 | } | ||
157 | |||
158 | function generateRandomToken () { | ||
159 | return randomBytesPromise(256) | ||
160 | .then(buffer => sha1(buffer)) | ||
161 | } | ||
162 | |||
163 | function getTokenExpiresAt (type: 'access' | 'refresh') { | ||
164 | const lifetime = type === 'access' | ||
165 | ? OAUTH_LIFETIME.ACCESS_TOKEN | ||
166 | : OAUTH_LIFETIME.REFRESH_TOKEN | ||
167 | |||
168 | return new Date(Date.now() + lifetime * 1000) | ||
169 | } | ||
170 | |||
171 | async function buildToken () { | ||
172 | const [ accessToken, refreshToken ] = await Promise.all([ generateRandomToken(), generateRandomToken() ]) | ||
173 | |||
174 | return { | ||
175 | accessToken, | ||
176 | refreshToken, | ||
177 | accessTokenExpiresAt: getTokenExpiresAt('access'), | ||
178 | refreshTokenExpiresAt: getTokenExpiresAt('refresh') | ||
179 | } | ||
180 | } | ||
diff --git a/server/lib/auth/tokens-cache.ts b/server/lib/auth/tokens-cache.ts new file mode 100644 index 000000000..b027ce69a --- /dev/null +++ b/server/lib/auth/tokens-cache.ts | |||
@@ -0,0 +1,52 @@ | |||
1 | import * as LRUCache from 'lru-cache' | ||
2 | import { MOAuthTokenUser } from '@server/types/models' | ||
3 | import { LRU_CACHE } from '../../initializers/constants' | ||
4 | |||
5 | export class TokensCache { | ||
6 | |||
7 | private static instance: TokensCache | ||
8 | |||
9 | private readonly accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) | ||
10 | private readonly userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) | ||
11 | |||
12 | private constructor () { } | ||
13 | |||
14 | static get Instance () { | ||
15 | return this.instance || (this.instance = new this()) | ||
16 | } | ||
17 | |||
18 | hasToken (token: string) { | ||
19 | return this.accessTokenCache.has(token) | ||
20 | } | ||
21 | |||
22 | getByToken (token: string) { | ||
23 | return this.accessTokenCache.get(token) | ||
24 | } | ||
25 | |||
26 | setToken (token: MOAuthTokenUser) { | ||
27 | this.accessTokenCache.set(token.accessToken, token) | ||
28 | this.userHavingToken.set(token.userId, token.accessToken) | ||
29 | } | ||
30 | |||
31 | deleteUserToken (userId: number) { | ||
32 | this.clearCacheByUserId(userId) | ||
33 | } | ||
34 | |||
35 | clearCacheByUserId (userId: number) { | ||
36 | const token = this.userHavingToken.get(userId) | ||
37 | |||
38 | if (token !== undefined) { | ||
39 | this.accessTokenCache.del(token) | ||
40 | this.userHavingToken.del(userId) | ||
41 | } | ||
42 | } | ||
43 | |||
44 | clearCacheByToken (token: string) { | ||
45 | const tokenModel = this.accessTokenCache.get(token) | ||
46 | |||
47 | if (tokenModel !== undefined) { | ||
48 | this.userHavingToken.del(tokenModel.userId) | ||
49 | this.accessTokenCache.del(token) | ||
50 | } | ||
51 | } | ||
52 | } | ||
diff --git a/server/lib/avatar.ts b/server/lib/avatar.ts deleted file mode 100644 index 86f1e7bdb..000000000 --- a/server/lib/avatar.ts +++ /dev/null | |||
@@ -1,85 +0,0 @@ | |||
1 | import 'multer' | ||
2 | import { sendUpdateActor } from './activitypub/send' | ||
3 | import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' | ||
4 | import { updateActorAvatarInstance, deleteActorAvatarInstance } from './activitypub/actor' | ||
5 | import { processImage } from '../helpers/image-utils' | ||
6 | import { extname, join } from 'path' | ||
7 | import { retryTransactionWrapper } from '../helpers/database-utils' | ||
8 | import { v4 as uuidv4 } from 'uuid' | ||
9 | import { CONFIG } from '../initializers/config' | ||
10 | import { sequelizeTypescript } from '../initializers/database' | ||
11 | import * as LRUCache from 'lru-cache' | ||
12 | import { queue } from 'async' | ||
13 | import { downloadImage } from '../helpers/requests' | ||
14 | import { MAccountDefault, MChannelDefault } from '../types/models' | ||
15 | |||
16 | async function updateLocalActorAvatarFile ( | ||
17 | accountOrChannel: MAccountDefault | MChannelDefault, | ||
18 | avatarPhysicalFile: Express.Multer.File | ||
19 | ) { | ||
20 | const extension = extname(avatarPhysicalFile.filename) | ||
21 | |||
22 | const avatarName = uuidv4() + extension | ||
23 | const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) | ||
24 | await processImage(avatarPhysicalFile.path, destination, AVATARS_SIZE) | ||
25 | |||
26 | return retryTransactionWrapper(() => { | ||
27 | return sequelizeTypescript.transaction(async t => { | ||
28 | const avatarInfo = { | ||
29 | name: avatarName, | ||
30 | fileUrl: null, | ||
31 | onDisk: true | ||
32 | } | ||
33 | |||
34 | const updatedActor = await updateActorAvatarInstance(accountOrChannel.Actor, avatarInfo, t) | ||
35 | await updatedActor.save({ transaction: t }) | ||
36 | |||
37 | await sendUpdateActor(accountOrChannel, t) | ||
38 | |||
39 | return updatedActor.Avatar | ||
40 | }) | ||
41 | }) | ||
42 | } | ||
43 | |||
44 | async function deleteLocalActorAvatarFile ( | ||
45 | accountOrChannel: MAccountDefault | MChannelDefault | ||
46 | ) { | ||
47 | return retryTransactionWrapper(() => { | ||
48 | return sequelizeTypescript.transaction(async t => { | ||
49 | const updatedActor = await deleteActorAvatarInstance(accountOrChannel.Actor, t) | ||
50 | await updatedActor.save({ transaction: t }) | ||
51 | |||
52 | await sendUpdateActor(accountOrChannel, t) | ||
53 | |||
54 | return updatedActor.Avatar | ||
55 | }) | ||
56 | }) | ||
57 | } | ||
58 | |||
59 | type DownloadImageQueueTask = { fileUrl: string, filename: string } | ||
60 | |||
61 | const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { | ||
62 | downloadImage(task.fileUrl, CONFIG.STORAGE.AVATARS_DIR, task.filename, AVATARS_SIZE) | ||
63 | .then(() => cb()) | ||
64 | .catch(err => cb(err)) | ||
65 | }, QUEUE_CONCURRENCY.AVATAR_PROCESS_IMAGE) | ||
66 | |||
67 | function pushAvatarProcessInQueue (task: DownloadImageQueueTask) { | ||
68 | return new Promise<void>((res, rej) => { | ||
69 | downloadImageQueue.push(task, err => { | ||
70 | if (err) return rej(err) | ||
71 | |||
72 | return res() | ||
73 | }) | ||
74 | }) | ||
75 | } | ||
76 | |||
77 | // Unsafe so could returns paths that does not exist anymore | ||
78 | const avatarPathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.AVATAR_STATIC.MAX_SIZE }) | ||
79 | |||
80 | export { | ||
81 | avatarPathUnsafeCache, | ||
82 | updateLocalActorAvatarFile, | ||
83 | deleteLocalActorAvatarFile, | ||
84 | pushAvatarProcessInQueue | ||
85 | } | ||
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index f19ec7df0..6ddaa82c8 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -5,12 +5,13 @@ import validator from 'validator' | |||
5 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n' | 5 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n' |
6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
7 | import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' | 7 | import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' |
8 | import { escapeHTML, isTestInstance, sha256 } from '../helpers/core-utils' | 8 | import { isTestInstance, sha256 } from '../helpers/core-utils' |
9 | import { escapeHTML } from '@shared/core-utils/renderer' | ||
9 | import { logger } from '../helpers/logger' | 10 | import { logger } from '../helpers/logger' |
10 | import { CONFIG } from '../initializers/config' | 11 | import { CONFIG } from '../initializers/config' |
11 | import { | 12 | import { |
12 | ACCEPT_HEADERS, | 13 | ACCEPT_HEADERS, |
13 | AVATARS_SIZE, | 14 | ACTOR_IMAGES_SIZE, |
14 | CUSTOM_HTML_TAG_COMMENTS, | 15 | CUSTOM_HTML_TAG_COMMENTS, |
15 | EMBED_SIZE, | 16 | EMBED_SIZE, |
16 | FILES_CONTENT_HASH, | 17 | FILES_CONTENT_HASH, |
@@ -245,8 +246,8 @@ class ClientHtml { | |||
245 | 246 | ||
246 | const image = { | 247 | const image = { |
247 | url: entity.Actor.getAvatarUrl(), | 248 | url: entity.Actor.getAvatarUrl(), |
248 | width: AVATARS_SIZE.width, | 249 | width: ACTOR_IMAGES_SIZE.AVATARS.width, |
249 | height: AVATARS_SIZE.height | 250 | height: ACTOR_IMAGES_SIZE.AVATARS.height |
250 | } | 251 | } |
251 | 252 | ||
252 | const ogType = 'website' | 253 | const ogType = 'website' |
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 969eae77b..9ca0d5d5b 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -7,12 +7,12 @@ import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/m | |||
7 | import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' | 7 | import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' |
8 | import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils' | 8 | import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils' |
9 | import { AbuseState, EmailPayload, UserAbuse } from '@shared/models' | 9 | import { AbuseState, EmailPayload, UserAbuse } from '@shared/models' |
10 | import { SendEmailOptions } from '../../shared/models/server/emailer.model' | 10 | import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model' |
11 | import { isTestInstance, root } from '../helpers/core-utils' | 11 | import { isTestInstance, root } from '../helpers/core-utils' |
12 | import { bunyanLogger, logger } from '../helpers/logger' | 12 | import { bunyanLogger, logger } from '../helpers/logger' |
13 | import { CONFIG, isEmailEnabled } from '../initializers/config' | 13 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
14 | import { WEBSERVER } from '../initializers/constants' | 14 | import { WEBSERVER } from '../initializers/constants' |
15 | import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' | 15 | import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MPlugin, MUser } from '../types/models' |
16 | import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' | 16 | import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' |
17 | import { JobQueue } from './job-queue' | 17 | import { JobQueue } from './job-queue' |
18 | 18 | ||
@@ -403,9 +403,9 @@ class Emailer { | |||
403 | } | 403 | } |
404 | 404 | ||
405 | async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { | 405 | async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { |
406 | const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' | 406 | const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' |
407 | const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() | 407 | const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() |
408 | const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON() | 408 | const channel = (await VideoChannelModel.loadAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON() |
409 | 409 | ||
410 | const emailPayload: EmailPayload = { | 410 | const emailPayload: EmailPayload = { |
411 | template: 'video-auto-blacklist-new', | 411 | template: 'video-auto-blacklist-new', |
@@ -417,7 +417,7 @@ class Emailer { | |||
417 | videoName: videoBlacklist.Video.name, | 417 | videoName: videoBlacklist.Video.name, |
418 | action: { | 418 | action: { |
419 | text: 'Review autoblacklist', | 419 | text: 'Review autoblacklist', |
420 | url: VIDEO_AUTO_BLACKLIST_URL | 420 | url: videoAutoBlacklistUrl |
421 | } | 421 | } |
422 | } | 422 | } |
423 | } | 423 | } |
@@ -472,6 +472,36 @@ class Emailer { | |||
472 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 472 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
473 | } | 473 | } |
474 | 474 | ||
475 | addNewPeerTubeVersionNotification (to: string[], latestVersion: string) { | ||
476 | const emailPayload: EmailPayload = { | ||
477 | to, | ||
478 | template: 'peertube-version-new', | ||
479 | subject: `A new PeerTube version is available: ${latestVersion}`, | ||
480 | locals: { | ||
481 | latestVersion | ||
482 | } | ||
483 | } | ||
484 | |||
485 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
486 | } | ||
487 | |||
488 | addNewPlugionVersionNotification (to: string[], plugin: MPlugin) { | ||
489 | const pluginUrl = WEBSERVER.URL + '/admin/plugins/list-installed?pluginType=' + plugin.type | ||
490 | |||
491 | const emailPayload: EmailPayload = { | ||
492 | to, | ||
493 | template: 'plugin-version-new', | ||
494 | subject: `A new plugin/theme version is available: ${plugin.name}@${plugin.latestVersion}`, | ||
495 | locals: { | ||
496 | pluginName: plugin.name, | ||
497 | latestVersion: plugin.latestVersion, | ||
498 | pluginUrl | ||
499 | } | ||
500 | } | ||
501 | |||
502 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
503 | } | ||
504 | |||
475 | addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) { | 505 | addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) { |
476 | const emailPayload: EmailPayload = { | 506 | const emailPayload: EmailPayload = { |
477 | template: 'password-reset', | 507 | template: 'password-reset', |
@@ -569,26 +599,27 @@ class Emailer { | |||
569 | }) | 599 | }) |
570 | 600 | ||
571 | for (const to of options.to) { | 601 | for (const to of options.to) { |
572 | await email | 602 | const baseOptions: SendEmailDefaultOptions = { |
573 | .send(merge( | 603 | template: 'common', |
574 | { | 604 | message: { |
575 | template: 'common', | 605 | to, |
576 | message: { | 606 | from: options.from, |
577 | to, | 607 | subject: options.subject, |
578 | from: options.from, | 608 | replyTo: options.replyTo |
579 | subject: options.subject, | 609 | }, |
580 | replyTo: options.replyTo | 610 | locals: { // default variables available in all templates |
581 | }, | 611 | WEBSERVER, |
582 | locals: { // default variables available in all templates | 612 | EMAIL: CONFIG.EMAIL, |
583 | WEBSERVER, | 613 | instanceName: CONFIG.INSTANCE.NAME, |
584 | EMAIL: CONFIG.EMAIL, | 614 | text: options.text, |
585 | instanceName: CONFIG.INSTANCE.NAME, | 615 | subject: options.subject |
586 | text: options.text, | 616 | } |
587 | subject: options.subject | 617 | } |
588 | } | 618 | |
589 | }, | 619 | // overriden/new variables given for a specific template in the payload |
590 | options // overriden/new variables given for a specific template in the payload | 620 | const sendOptions = merge(baseOptions, options) |
591 | ) as SendEmailOptions) | 621 | |
622 | await email.send(sendOptions) | ||
592 | .then(res => logger.debug('Sent email.', { res })) | 623 | .then(res => logger.debug('Sent email.', { res })) |
593 | .catch(err => logger.error('Error in email sender.', { err })) | 624 | .catch(err => logger.error('Error in email sender.', { err })) |
594 | } | 625 | } |
diff --git a/server/lib/emails/peertube-version-new/html.pug b/server/lib/emails/peertube-version-new/html.pug new file mode 100644 index 000000000..2f4d9399d --- /dev/null +++ b/server/lib/emails/peertube-version-new/html.pug | |||
@@ -0,0 +1,9 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | New PeerTube version available | ||
5 | |||
6 | block content | ||
7 | p | ||
8 | | A new version of PeerTube is available: #{latestVersion}. | ||
9 | | You can check the latest news on #[a(href="https://joinpeertube.org/news") JoinPeerTube]. | ||
diff --git a/server/lib/emails/plugin-version-new/html.pug b/server/lib/emails/plugin-version-new/html.pug new file mode 100644 index 000000000..86d3d87e8 --- /dev/null +++ b/server/lib/emails/plugin-version-new/html.pug | |||
@@ -0,0 +1,9 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | New plugin version available | ||
5 | |||
6 | block content | ||
7 | p | ||
8 | | A new version of the plugin/theme #{pluginName} is available: #{latestVersion}. | ||
9 | | You might want to upgrade it on #[a(href=pluginUrl) the PeerTube admin interface]. | ||
diff --git a/server/lib/files-cache/videos-caption-cache.ts b/server/lib/files-cache/videos-caption-cache.ts index ee0447010..58e2260b6 100644 --- a/server/lib/files-cache/videos-caption-cache.ts +++ b/server/lib/files-cache/videos-caption-cache.ts | |||
@@ -41,7 +41,7 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <string> { | |||
41 | const remoteUrl = videoCaption.getFileUrl(video) | 41 | const remoteUrl = videoCaption.getFileUrl(video) |
42 | const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.filename) | 42 | const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.filename) |
43 | 43 | ||
44 | await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) | 44 | await doRequestAndSaveToFile(remoteUrl, destPath) |
45 | 45 | ||
46 | return { isOwned: false, path: destPath } | 46 | return { isOwned: false, path: destPath } |
47 | } | 47 | } |
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts index ee72cd3f9..dd3a84aca 100644 --- a/server/lib/files-cache/videos-preview-cache.ts +++ b/server/lib/files-cache/videos-preview-cache.ts | |||
@@ -39,7 +39,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { | |||
39 | const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename) | 39 | const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename) |
40 | 40 | ||
41 | const remoteUrl = preview.getFileUrl(video) | 41 | const remoteUrl = preview.getFileUrl(video) |
42 | await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) | 42 | await doRequestAndSaveToFile(remoteUrl, destPath) |
43 | 43 | ||
44 | logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath) | 44 | logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath) |
45 | 45 | ||
diff --git a/server/lib/files-cache/videos-torrent-cache.ts b/server/lib/files-cache/videos-torrent-cache.ts index ca0e1770d..23217f140 100644 --- a/server/lib/files-cache/videos-torrent-cache.ts +++ b/server/lib/files-cache/videos-torrent-cache.ts | |||
@@ -5,6 +5,7 @@ import { CONFIG } from '../../initializers/config' | |||
5 | import { FILES_CACHE } from '../../initializers/constants' | 5 | import { FILES_CACHE } from '../../initializers/constants' |
6 | import { VideoModel } from '../../models/video/video' | 6 | import { VideoModel } from '../../models/video/video' |
7 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' | 7 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' |
8 | import { MVideo, MVideoFile } from '@server/types/models' | ||
8 | 9 | ||
9 | class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { | 10 | class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { |
10 | 11 | ||
@@ -22,7 +23,11 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { | |||
22 | const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename) | 23 | const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename) |
23 | if (!file) return undefined | 24 | if (!file) return undefined |
24 | 25 | ||
25 | if (file.getVideo().isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename) } | 26 | if (file.getVideo().isOwned()) { |
27 | const downloadName = this.buildDownloadName(file.getVideo(), file) | ||
28 | |||
29 | return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename), downloadName } | ||
30 | } | ||
26 | 31 | ||
27 | return this.loadRemoteFile(filename) | 32 | return this.loadRemoteFile(filename) |
28 | } | 33 | } |
@@ -41,12 +46,16 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { | |||
41 | const remoteUrl = file.getRemoteTorrentUrl(video) | 46 | const remoteUrl = file.getRemoteTorrentUrl(video) |
42 | const destPath = join(FILES_CACHE.TORRENTS.DIRECTORY, file.torrentFilename) | 47 | const destPath = join(FILES_CACHE.TORRENTS.DIRECTORY, file.torrentFilename) |
43 | 48 | ||
44 | await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) | 49 | await doRequestAndSaveToFile(remoteUrl, destPath) |
45 | 50 | ||
46 | const downloadName = `${video.name}-${file.resolution}p.torrent` | 51 | const downloadName = this.buildDownloadName(video, file) |
47 | 52 | ||
48 | return { isOwned: false, path: destPath, downloadName } | 53 | return { isOwned: false, path: destPath, downloadName } |
49 | } | 54 | } |
55 | |||
56 | private buildDownloadName (video: MVideo, file: MVideoFile) { | ||
57 | return `${video.name}-${file.resolution}p.torrent` | ||
58 | } | ||
50 | } | 59 | } |
51 | 60 | ||
52 | export { | 61 | export { |
diff --git a/server/lib/hls.ts b/server/lib/hls.ts index 04187668c..84539e2c1 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts | |||
@@ -135,7 +135,7 @@ function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, | |||
135 | const destPath = join(tmpDirectory, basename(fileUrl)) | 135 | const destPath = join(tmpDirectory, basename(fileUrl)) |
136 | 136 | ||
137 | const bodyKBLimit = 10 * 1000 * 1000 // 10GB | 137 | const bodyKBLimit = 10 * 1000 * 1000 // 10GB |
138 | await doRequestAndSaveToFile({ uri: fileUrl }, destPath, bodyKBLimit) | 138 | await doRequestAndSaveToFile(fileUrl, destPath, { bodyKBLimit }) |
139 | } | 139 | } |
140 | 140 | ||
141 | clearTimeout(timer) | 141 | clearTimeout(timer) |
@@ -156,7 +156,7 @@ function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, | |||
156 | } | 156 | } |
157 | 157 | ||
158 | async function fetchUniqUrls (playlistUrl: string) { | 158 | async function fetchUniqUrls (playlistUrl: string) { |
159 | const { body } = await doRequest<string>({ uri: playlistUrl }) | 159 | const { body } = await doRequest(playlistUrl) |
160 | 160 | ||
161 | if (!body) return [] | 161 | if (!body) return [] |
162 | 162 | ||
diff --git a/server/lib/job-queue/handlers/activitypub-cleaner.ts b/server/lib/job-queue/handlers/activitypub-cleaner.ts index b58bbc983..1caca1dcc 100644 --- a/server/lib/job-queue/handlers/activitypub-cleaner.ts +++ b/server/lib/job-queue/handlers/activitypub-cleaner.ts | |||
@@ -1,10 +1,13 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import * as Bull from 'bull' | 2 | import * as Bull from 'bull' |
3 | import { checkUrlsSameHost } from '@server/helpers/activitypub' | 3 | import { checkUrlsSameHost } from '@server/helpers/activitypub' |
4 | import { isDislikeActivityValid, isLikeActivityValid } from '@server/helpers/custom-validators/activitypub/rate' | 4 | import { |
5 | import { isShareActivityValid } from '@server/helpers/custom-validators/activitypub/share' | 5 | isAnnounceActivityValid, |
6 | isDislikeActivityValid, | ||
7 | isLikeActivityValid | ||
8 | } from '@server/helpers/custom-validators/activitypub/activity' | ||
6 | import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments' | 9 | import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments' |
7 | import { doRequest } from '@server/helpers/requests' | 10 | import { doJSONRequest, PeerTubeRequestError } from '@server/helpers/requests' |
8 | import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants' | 11 | import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants' |
9 | import { VideoModel } from '@server/models/video/video' | 12 | import { VideoModel } from '@server/models/video/video' |
10 | import { VideoCommentModel } from '@server/models/video/video-comment' | 13 | import { VideoCommentModel } from '@server/models/video/video-comment' |
@@ -78,44 +81,44 @@ async function updateObjectIfNeeded <T> ( | |||
78 | updater: (url: string, newUrl: string) => Promise<T>, | 81 | updater: (url: string, newUrl: string) => Promise<T>, |
79 | deleter: (url: string) => Promise<T> | 82 | deleter: (url: string) => Promise<T> |
80 | ): Promise<{ data: T, status: 'deleted' | 'updated' } | null> { | 83 | ): Promise<{ data: T, status: 'deleted' | 'updated' } | null> { |
81 | // Fetch url | 84 | const on404OrTombstone = async () => { |
82 | const { response, body } = await doRequest<any>({ | ||
83 | uri: url, | ||
84 | json: true, | ||
85 | activityPub: true | ||
86 | }) | ||
87 | |||
88 | // Does not exist anymore, remove entry | ||
89 | if (response.statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
90 | logger.info('Removing remote AP object %s.', url) | 85 | logger.info('Removing remote AP object %s.', url) |
91 | const data = await deleter(url) | 86 | const data = await deleter(url) |
92 | 87 | ||
93 | return { status: 'deleted', data } | 88 | return { status: 'deleted' as 'deleted', data } |
94 | } | 89 | } |
95 | 90 | ||
96 | // If not same id, check same host and update | 91 | try { |
97 | if (!body || !body.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`) | 92 | const { body } = await doJSONRequest<any>(url, { activityPub: true }) |
98 | 93 | ||
99 | if (body.type === 'Tombstone') { | 94 | // If not same id, check same host and update |
100 | logger.info('Removing remote AP object %s.', url) | 95 | if (!body || !body.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`) |
101 | const data = await deleter(url) | ||
102 | 96 | ||
103 | return { status: 'deleted', data } | 97 | if (body.type === 'Tombstone') { |
104 | } | 98 | return on404OrTombstone() |
99 | } | ||
105 | 100 | ||
106 | const newUrl = body.id | 101 | const newUrl = body.id |
107 | if (newUrl !== url) { | 102 | if (newUrl !== url) { |
108 | if (checkUrlsSameHost(newUrl, url) !== true) { | 103 | if (checkUrlsSameHost(newUrl, url) !== true) { |
109 | throw new Error(`New url ${newUrl} has not the same host than old url ${url}`) | 104 | throw new Error(`New url ${newUrl} has not the same host than old url ${url}`) |
105 | } | ||
106 | |||
107 | logger.info('Updating remote AP object %s.', url) | ||
108 | const data = await updater(url, newUrl) | ||
109 | |||
110 | return { status: 'updated', data } | ||
110 | } | 111 | } |
111 | 112 | ||
112 | logger.info('Updating remote AP object %s.', url) | 113 | return null |
113 | const data = await updater(url, newUrl) | 114 | } catch (err) { |
115 | // Does not exist anymore, remove entry | ||
116 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
117 | return on404OrTombstone() | ||
118 | } | ||
114 | 119 | ||
115 | return { status: 'updated', data } | 120 | throw err |
116 | } | 121 | } |
117 | |||
118 | return null | ||
119 | } | 122 | } |
120 | 123 | ||
121 | function rateOptionsFactory () { | 124 | function rateOptionsFactory () { |
@@ -149,7 +152,7 @@ function rateOptionsFactory () { | |||
149 | 152 | ||
150 | function shareOptionsFactory () { | 153 | function shareOptionsFactory () { |
151 | return { | 154 | return { |
152 | bodyValidator: (body: any) => isShareActivityValid(body), | 155 | bodyValidator: (body: any) => isAnnounceActivityValid(body), |
153 | 156 | ||
154 | updater: async (url: string, newUrl: string) => { | 157 | updater: async (url: string, newUrl: string) => { |
155 | const share = await VideoShareModel.loadByUrl(url, undefined) | 158 | const share = await VideoShareModel.loadByUrl(url, undefined) |
diff --git a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts index 7174786d6..c69ff9e83 100644 --- a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts +++ b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts | |||
@@ -16,8 +16,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) { | |||
16 | const httpSignatureOptions = await buildSignedRequestOptions(payload) | 16 | const httpSignatureOptions = await buildSignedRequestOptions(payload) |
17 | 17 | ||
18 | const options = { | 18 | const options = { |
19 | method: 'POST', | 19 | method: 'POST' as 'POST', |
20 | uri: '', | ||
21 | json: body, | 20 | json: body, |
22 | httpSignature: httpSignatureOptions, | 21 | httpSignature: httpSignatureOptions, |
23 | timeout: REQUEST_TIMEOUT, | 22 | timeout: REQUEST_TIMEOUT, |
@@ -28,7 +27,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) { | |||
28 | const goodUrls: string[] = [] | 27 | const goodUrls: string[] = [] |
29 | 28 | ||
30 | await Bluebird.map(payload.uris, uri => { | 29 | await Bluebird.map(payload.uris, uri => { |
31 | return doRequest(Object.assign({}, options, { uri })) | 30 | return doRequest(uri, options) |
32 | .then(() => goodUrls.push(uri)) | 31 | .then(() => goodUrls.push(uri)) |
33 | .catch(() => badUrls.push(uri)) | 32 | .catch(() => badUrls.push(uri)) |
34 | }, { concurrency: BROADCAST_CONCURRENCY }) | 33 | }, { concurrency: BROADCAST_CONCURRENCY }) |
diff --git a/server/lib/job-queue/handlers/activitypub-http-unicast.ts b/server/lib/job-queue/handlers/activitypub-http-unicast.ts index 74989d62e..585dad671 100644 --- a/server/lib/job-queue/handlers/activitypub-http-unicast.ts +++ b/server/lib/job-queue/handlers/activitypub-http-unicast.ts | |||
@@ -16,8 +16,7 @@ async function processActivityPubHttpUnicast (job: Bull.Job) { | |||
16 | const httpSignatureOptions = await buildSignedRequestOptions(payload) | 16 | const httpSignatureOptions = await buildSignedRequestOptions(payload) |
17 | 17 | ||
18 | const options = { | 18 | const options = { |
19 | method: 'POST', | 19 | method: 'POST' as 'POST', |
20 | uri, | ||
21 | json: body, | 20 | json: body, |
22 | httpSignature: httpSignatureOptions, | 21 | httpSignature: httpSignatureOptions, |
23 | timeout: REQUEST_TIMEOUT, | 22 | timeout: REQUEST_TIMEOUT, |
@@ -25,7 +24,7 @@ async function processActivityPubHttpUnicast (job: Bull.Job) { | |||
25 | } | 24 | } |
26 | 25 | ||
27 | try { | 26 | try { |
28 | await doRequest(options) | 27 | await doRequest(uri, options) |
29 | ActorFollowScoreCache.Instance.updateActorFollowsScore([ uri ], []) | 28 | ActorFollowScoreCache.Instance.updateActorFollowsScore([ uri ], []) |
30 | } catch (err) { | 29 | } catch (err) { |
31 | ActorFollowScoreCache.Instance.updateActorFollowsScore([], [ uri ]) | 30 | ActorFollowScoreCache.Instance.updateActorFollowsScore([], [ uri ]) |
diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts index c030d31ef..e8a91450d 100644 --- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts +++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts | |||
@@ -6,21 +6,24 @@ import { getServerActor } from '@server/models/application/application' | |||
6 | import { buildDigest } from '@server/helpers/peertube-crypto' | 6 | import { buildDigest } from '@server/helpers/peertube-crypto' |
7 | import { ContextType } from '@shared/models/activitypub/context' | 7 | import { ContextType } from '@shared/models/activitypub/context' |
8 | 8 | ||
9 | type Payload = { body: any, contextType?: ContextType, signatureActorId?: number } | 9 | type Payload <T> = { body: T, contextType?: ContextType, signatureActorId?: number } |
10 | 10 | ||
11 | async function computeBody (payload: Payload) { | 11 | async function computeBody <T> ( |
12 | payload: Payload<T> | ||
13 | ): Promise<T | T & { type: 'RsaSignature2017', creator: string, created: string }> { | ||
12 | let body = payload.body | 14 | let body = payload.body |
13 | 15 | ||
14 | if (payload.signatureActorId) { | 16 | if (payload.signatureActorId) { |
15 | const actorSignature = await ActorModel.load(payload.signatureActorId) | 17 | const actorSignature = await ActorModel.load(payload.signatureActorId) |
16 | if (!actorSignature) throw new Error('Unknown signature actor id.') | 18 | if (!actorSignature) throw new Error('Unknown signature actor id.') |
19 | |||
17 | body = await buildSignedActivity(actorSignature, payload.body, payload.contextType) | 20 | body = await buildSignedActivity(actorSignature, payload.body, payload.contextType) |
18 | } | 21 | } |
19 | 22 | ||
20 | return body | 23 | return body |
21 | } | 24 | } |
22 | 25 | ||
23 | async function buildSignedRequestOptions (payload: Payload) { | 26 | async function buildSignedRequestOptions (payload: Payload<any>) { |
24 | let actor: MActor | null | 27 | let actor: MActor | null |
25 | 28 | ||
26 | if (payload.signatureActorId) { | 29 | if (payload.signatureActorId) { |
@@ -43,9 +46,9 @@ async function buildSignedRequestOptions (payload: Payload) { | |||
43 | 46 | ||
44 | function buildGlobalHeaders (body: any) { | 47 | function buildGlobalHeaders (body: any) { |
45 | return { | 48 | return { |
46 | 'Digest': buildDigest(body), | 49 | 'digest': buildDigest(body), |
47 | 'Content-Type': 'application/activity+json', | 50 | 'content-type': 'application/activity+json', |
48 | 'Accept': ACTIVITY_PUB.ACCEPT_HEADER | 51 | 'accept': ACTIVITY_PUB.ACCEPT_HEADER |
49 | } | 52 | } |
50 | } | 53 | } |
51 | 54 | ||
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index 740c274d7..da7f7cc05 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts | |||
@@ -19,7 +19,7 @@ import { CONFIG } from '../initializers/config' | |||
19 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 19 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
20 | import { UserModel } from '../models/account/user' | 20 | import { UserModel } from '../models/account/user' |
21 | import { UserNotificationModel } from '../models/account/user-notification' | 21 | import { UserNotificationModel } from '../models/account/user-notification' |
22 | import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull } from '../types/models' | 22 | import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models' |
23 | import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' | 23 | import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' |
24 | import { isBlockedByServerOrAccount } from './blocklist' | 24 | import { isBlockedByServerOrAccount } from './blocklist' |
25 | import { Emailer } from './emailer' | 25 | import { Emailer } from './emailer' |
@@ -144,6 +144,20 @@ class Notifier { | |||
144 | }) | 144 | }) |
145 | } | 145 | } |
146 | 146 | ||
147 | notifyOfNewPeerTubeVersion (application: MApplication, latestVersion: string) { | ||
148 | this.notifyAdminsOfNewPeerTubeVersion(application, latestVersion) | ||
149 | .catch(err => { | ||
150 | logger.error('Cannot notify on new PeerTubeb version %s.', latestVersion, { err }) | ||
151 | }) | ||
152 | } | ||
153 | |||
154 | notifyOfNewPluginVersion (plugin: MPlugin) { | ||
155 | this.notifyAdminsOfNewPluginVersion(plugin) | ||
156 | .catch(err => { | ||
157 | logger.error('Cannot notify on new plugin version %s.', plugin.name, { err }) | ||
158 | }) | ||
159 | } | ||
160 | |||
147 | private async notifySubscribersOfNewVideo (video: MVideoAccountLight) { | 161 | private async notifySubscribersOfNewVideo (video: MVideoAccountLight) { |
148 | // List all followers that are users | 162 | // List all followers that are users |
149 | const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) | 163 | const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) |
@@ -667,6 +681,64 @@ class Notifier { | |||
667 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) | 681 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) |
668 | } | 682 | } |
669 | 683 | ||
684 | private async notifyAdminsOfNewPeerTubeVersion (application: MApplication, latestVersion: string) { | ||
685 | // Use the debug right to know who is an administrator | ||
686 | const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG) | ||
687 | if (admins.length === 0) return | ||
688 | |||
689 | logger.info('Notifying %s admins of new PeerTube version %s.', admins.length, latestVersion) | ||
690 | |||
691 | function settingGetter (user: MUserWithNotificationSetting) { | ||
692 | return user.NotificationSetting.newPeerTubeVersion | ||
693 | } | ||
694 | |||
695 | async function notificationCreator (user: MUserWithNotificationSetting) { | ||
696 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ | ||
697 | type: UserNotificationType.NEW_PEERTUBE_VERSION, | ||
698 | userId: user.id, | ||
699 | applicationId: application.id | ||
700 | }) | ||
701 | notification.Application = application | ||
702 | |||
703 | return notification | ||
704 | } | ||
705 | |||
706 | function emailSender (emails: string[]) { | ||
707 | return Emailer.Instance.addNewPeerTubeVersionNotification(emails, latestVersion) | ||
708 | } | ||
709 | |||
710 | return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) | ||
711 | } | ||
712 | |||
713 | private async notifyAdminsOfNewPluginVersion (plugin: MPlugin) { | ||
714 | // Use the debug right to know who is an administrator | ||
715 | const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG) | ||
716 | if (admins.length === 0) return | ||
717 | |||
718 | logger.info('Notifying %s admins of new plugin version %s@%s.', admins.length, plugin.name, plugin.latestVersion) | ||
719 | |||
720 | function settingGetter (user: MUserWithNotificationSetting) { | ||
721 | return user.NotificationSetting.newPluginVersion | ||
722 | } | ||
723 | |||
724 | async function notificationCreator (user: MUserWithNotificationSetting) { | ||
725 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ | ||
726 | type: UserNotificationType.NEW_PLUGIN_VERSION, | ||
727 | userId: user.id, | ||
728 | pluginId: plugin.id | ||
729 | }) | ||
730 | notification.Plugin = plugin | ||
731 | |||
732 | return notification | ||
733 | } | ||
734 | |||
735 | function emailSender (emails: string[]) { | ||
736 | return Emailer.Instance.addNewPlugionVersionNotification(emails, plugin) | ||
737 | } | ||
738 | |||
739 | return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) | ||
740 | } | ||
741 | |||
670 | private async notify<T extends MUserWithNotificationSetting> (options: { | 742 | private async notify<T extends MUserWithNotificationSetting> (options: { |
671 | users: T[] | 743 | users: T[] |
672 | notificationCreator: (user: T) => Promise<UserNotificationModelForApi> | 744 | notificationCreator: (user: T) => Promise<UserNotificationModelForApi> |
diff --git a/server/lib/plugins/plugin-index.ts b/server/lib/plugins/plugin-index.ts index 7bcb6ed4c..624f5da1d 100644 --- a/server/lib/plugins/plugin-index.ts +++ b/server/lib/plugins/plugin-index.ts | |||
@@ -1,22 +1,22 @@ | |||
1 | import { doRequest } from '../../helpers/requests' | 1 | import { sanitizeUrl } from '@server/helpers/core-utils' |
2 | import { CONFIG } from '../../initializers/config' | 2 | import { ResultList } from '../../../shared/models' |
3 | import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model' | ||
4 | import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model' | ||
3 | import { | 5 | import { |
4 | PeertubePluginLatestVersionRequest, | 6 | PeertubePluginLatestVersionRequest, |
5 | PeertubePluginLatestVersionResponse | 7 | PeertubePluginLatestVersionResponse |
6 | } from '../../../shared/models/plugins/peertube-plugin-latest-version.model' | 8 | } from '../../../shared/models/plugins/peertube-plugin-latest-version.model' |
7 | import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model' | ||
8 | import { ResultList } from '../../../shared/models' | ||
9 | import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model' | ||
10 | import { PluginModel } from '../../models/server/plugin' | ||
11 | import { PluginManager } from './plugin-manager' | ||
12 | import { logger } from '../../helpers/logger' | 9 | import { logger } from '../../helpers/logger' |
10 | import { doJSONRequest } from '../../helpers/requests' | ||
11 | import { CONFIG } from '../../initializers/config' | ||
13 | import { PEERTUBE_VERSION } from '../../initializers/constants' | 12 | import { PEERTUBE_VERSION } from '../../initializers/constants' |
14 | import { sanitizeUrl } from '@server/helpers/core-utils' | 13 | import { PluginModel } from '../../models/server/plugin' |
14 | import { PluginManager } from './plugin-manager' | ||
15 | 15 | ||
16 | async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) { | 16 | async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) { |
17 | const { start = 0, count = 20, search, sort = 'npmName', pluginType } = options | 17 | const { start = 0, count = 20, search, sort = 'npmName', pluginType } = options |
18 | 18 | ||
19 | const qs: PeertubePluginIndexList = { | 19 | const searchParams: PeertubePluginIndexList & Record<string, string | number> = { |
20 | start, | 20 | start, |
21 | count, | 21 | count, |
22 | sort, | 22 | sort, |
@@ -28,7 +28,7 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) | |||
28 | const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' | 28 | const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' |
29 | 29 | ||
30 | try { | 30 | try { |
31 | const { body } = await doRequest<any>({ uri, qs, json: true }) | 31 | const { body } = await doJSONRequest<any>(uri, { searchParams }) |
32 | 32 | ||
33 | logger.debug('Got result from PeerTube index.', { body }) | 33 | logger.debug('Got result from PeerTube index.', { body }) |
34 | 34 | ||
@@ -58,7 +58,11 @@ async function getLatestPluginsVersion (npmNames: string[]): Promise<PeertubePlu | |||
58 | 58 | ||
59 | const uri = sanitizeUrl(CONFIG.PLUGINS.INDEX.URL) + '/api/v1/plugins/latest-version' | 59 | const uri = sanitizeUrl(CONFIG.PLUGINS.INDEX.URL) + '/api/v1/plugins/latest-version' |
60 | 60 | ||
61 | const { body } = await doRequest<any>({ uri, body: bodyRequest, json: true, method: 'POST' }) | 61 | const options = { |
62 | json: bodyRequest, | ||
63 | method: 'POST' as 'POST' | ||
64 | } | ||
65 | const { body } = await doJSONRequest<PeertubePluginLatestVersionResponse>(uri, options) | ||
62 | 66 | ||
63 | return body | 67 | return body |
64 | } | 68 | } |
diff --git a/server/lib/plugins/register-helpers.ts b/server/lib/plugins/register-helpers.ts index 1f2a88c27..9b5e1a546 100644 --- a/server/lib/plugins/register-helpers.ts +++ b/server/lib/plugins/register-helpers.ts | |||
@@ -7,7 +7,7 @@ import { | |||
7 | VIDEO_PLAYLIST_PRIVACIES, | 7 | VIDEO_PLAYLIST_PRIVACIES, |
8 | VIDEO_PRIVACIES | 8 | VIDEO_PRIVACIES |
9 | } from '@server/initializers/constants' | 9 | } from '@server/initializers/constants' |
10 | import { onExternalUserAuthenticated } from '@server/lib/auth' | 10 | import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth' |
11 | import { PluginModel } from '@server/models/server/plugin' | 11 | import { PluginModel } from '@server/models/server/plugin' |
12 | import { | 12 | import { |
13 | RegisterServerAuthExternalOptions, | 13 | RegisterServerAuthExternalOptions, |
diff --git a/server/lib/schedulers/auto-follow-index-instances.ts b/server/lib/schedulers/auto-follow-index-instances.ts index f62f52f9c..0b8cd1389 100644 --- a/server/lib/schedulers/auto-follow-index-instances.ts +++ b/server/lib/schedulers/auto-follow-index-instances.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { chunk } from 'lodash' | 1 | import { chunk } from 'lodash' |
2 | import { doRequest } from '@server/helpers/requests' | 2 | import { doJSONRequest } from '@server/helpers/requests' |
3 | import { JobQueue } from '@server/lib/job-queue' | 3 | import { JobQueue } from '@server/lib/job-queue' |
4 | import { ActorFollowModel } from '@server/models/activitypub/actor-follow' | 4 | import { ActorFollowModel } from '@server/models/activitypub/actor-follow' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
@@ -34,12 +34,12 @@ export class AutoFollowIndexInstances extends AbstractScheduler { | |||
34 | try { | 34 | try { |
35 | const serverActor = await getServerActor() | 35 | const serverActor = await getServerActor() |
36 | 36 | ||
37 | const qs = { count: 1000 } | 37 | const searchParams = { count: 1000 } |
38 | if (this.lastCheck) Object.assign(qs, { since: this.lastCheck.toISOString() }) | 38 | if (this.lastCheck) Object.assign(searchParams, { since: this.lastCheck.toISOString() }) |
39 | 39 | ||
40 | this.lastCheck = new Date() | 40 | this.lastCheck = new Date() |
41 | 41 | ||
42 | const { body } = await doRequest<any>({ uri: indexUrl, qs, json: true }) | 42 | const { body } = await doJSONRequest<any>(indexUrl, { searchParams }) |
43 | if (!body.data || Array.isArray(body.data) === false) { | 43 | if (!body.data || Array.isArray(body.data) === false) { |
44 | logger.error('Cannot auto follow instances of index %s. Please check the auto follow URL.', indexUrl, { body }) | 44 | logger.error('Cannot auto follow instances of index %s. Please check the auto follow URL.', indexUrl, { body }) |
45 | return | 45 | return |
diff --git a/server/lib/schedulers/peertube-version-check-scheduler.ts b/server/lib/schedulers/peertube-version-check-scheduler.ts new file mode 100644 index 000000000..c8960465c --- /dev/null +++ b/server/lib/schedulers/peertube-version-check-scheduler.ts | |||
@@ -0,0 +1,55 @@ | |||
1 | |||
2 | import { doJSONRequest } from '@server/helpers/requests' | ||
3 | import { ApplicationModel } from '@server/models/application/application' | ||
4 | import { compareSemVer } from '@shared/core-utils' | ||
5 | import { JoinPeerTubeVersions } from '@shared/models' | ||
6 | import { logger } from '../../helpers/logger' | ||
7 | import { CONFIG } from '../../initializers/config' | ||
8 | import { PEERTUBE_VERSION, SCHEDULER_INTERVALS_MS } from '../../initializers/constants' | ||
9 | import { Notifier } from '../notifier' | ||
10 | import { AbstractScheduler } from './abstract-scheduler' | ||
11 | |||
12 | export class PeerTubeVersionCheckScheduler extends AbstractScheduler { | ||
13 | |||
14 | private static instance: AbstractScheduler | ||
15 | |||
16 | protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.checkPeerTubeVersion | ||
17 | |||
18 | private constructor () { | ||
19 | super() | ||
20 | } | ||
21 | |||
22 | protected async internalExecute () { | ||
23 | return this.checkLatestVersion() | ||
24 | } | ||
25 | |||
26 | private async checkLatestVersion () { | ||
27 | if (CONFIG.PEERTUBE.CHECK_LATEST_VERSION.ENABLED === false) return | ||
28 | |||
29 | logger.info('Checking latest PeerTube version.') | ||
30 | |||
31 | const { body } = await doJSONRequest<JoinPeerTubeVersions>(CONFIG.PEERTUBE.CHECK_LATEST_VERSION.URL) | ||
32 | |||
33 | if (!body?.peertube?.latestVersion) { | ||
34 | logger.warn('Cannot check latest PeerTube version: body is invalid.', { body }) | ||
35 | return | ||
36 | } | ||
37 | |||
38 | const latestVersion = body.peertube.latestVersion | ||
39 | const application = await ApplicationModel.load() | ||
40 | |||
41 | // Already checked this version | ||
42 | if (application.latestPeerTubeVersion === latestVersion) return | ||
43 | |||
44 | if (compareSemVer(PEERTUBE_VERSION, latestVersion) < 0) { | ||
45 | application.latestPeerTubeVersion = latestVersion | ||
46 | await application.save() | ||
47 | |||
48 | Notifier.Instance.notifyOfNewPeerTubeVersion(application, latestVersion) | ||
49 | } | ||
50 | } | ||
51 | |||
52 | static get Instance () { | ||
53 | return this.instance || (this.instance = new this()) | ||
54 | } | ||
55 | } | ||
diff --git a/server/lib/schedulers/plugins-check-scheduler.ts b/server/lib/schedulers/plugins-check-scheduler.ts index 014993e94..9a1ae3ec5 100644 --- a/server/lib/schedulers/plugins-check-scheduler.ts +++ b/server/lib/schedulers/plugins-check-scheduler.ts | |||
@@ -6,6 +6,7 @@ import { PluginModel } from '../../models/server/plugin' | |||
6 | import { chunk } from 'lodash' | 6 | import { chunk } from 'lodash' |
7 | import { getLatestPluginsVersion } from '../plugins/plugin-index' | 7 | import { getLatestPluginsVersion } from '../plugins/plugin-index' |
8 | import { compareSemVer } from '../../../shared/core-utils/miscs/miscs' | 8 | import { compareSemVer } from '../../../shared/core-utils/miscs/miscs' |
9 | import { Notifier } from '../notifier' | ||
9 | 10 | ||
10 | export class PluginsCheckScheduler extends AbstractScheduler { | 11 | export class PluginsCheckScheduler extends AbstractScheduler { |
11 | 12 | ||
@@ -53,6 +54,11 @@ export class PluginsCheckScheduler extends AbstractScheduler { | |||
53 | plugin.latestVersion = result.latestVersion | 54 | plugin.latestVersion = result.latestVersion |
54 | await plugin.save() | 55 | await plugin.save() |
55 | 56 | ||
57 | // Notify if there is an higher plugin version available | ||
58 | if (compareSemVer(plugin.version, result.latestVersion) < 0) { | ||
59 | Notifier.Instance.notifyOfNewPluginVersion(plugin) | ||
60 | } | ||
61 | |||
56 | logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion) | 62 | logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion) |
57 | } | 63 | } |
58 | } | 64 | } |
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index 106f5fdaa..cfee69cfc 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | |||
2 | import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' | 3 | import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' |
3 | import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' | 4 | import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' |
4 | import { processImage } from '../helpers/image-utils' | 5 | import { generateImageFilename, processImage } from '../helpers/image-utils' |
5 | import { downloadImage } from '../helpers/requests' | 6 | import { downloadImage } from '../helpers/requests' |
6 | import { CONFIG } from '../initializers/config' | 7 | import { CONFIG } from '../initializers/config' |
7 | import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' | 8 | import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' |
@@ -11,7 +12,7 @@ import { MThumbnail } from '../types/models/video/thumbnail' | |||
11 | import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' | 12 | import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' |
12 | import { getVideoFilePath } from './video-paths' | 13 | import { getVideoFilePath } from './video-paths' |
13 | 14 | ||
14 | type ImageSize = { height: number, width: number } | 15 | type ImageSize = { height?: number, width?: number } |
15 | 16 | ||
16 | function createPlaylistMiniatureFromExisting (options: { | 17 | function createPlaylistMiniatureFromExisting (options: { |
17 | inputPath: string | 18 | inputPath: string |
@@ -200,7 +201,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si | |||
200 | : undefined | 201 | : undefined |
201 | 202 | ||
202 | if (type === ThumbnailType.MINIATURE) { | 203 | if (type === ThumbnailType.MINIATURE) { |
203 | const filename = video.generateThumbnailName() | 204 | const filename = generateImageFilename() |
204 | const basePath = CONFIG.STORAGE.THUMBNAILS_DIR | 205 | const basePath = CONFIG.STORAGE.THUMBNAILS_DIR |
205 | 206 | ||
206 | return { | 207 | return { |
@@ -214,7 +215,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si | |||
214 | } | 215 | } |
215 | 216 | ||
216 | if (type === ThumbnailType.PREVIEW) { | 217 | if (type === ThumbnailType.PREVIEW) { |
217 | const filename = video.generatePreviewName() | 218 | const filename = generateImageFilename() |
218 | const basePath = CONFIG.STORAGE.PREVIEWS_DIR | 219 | const basePath = CONFIG.STORAGE.PREVIEWS_DIR |
219 | 220 | ||
220 | return { | 221 | return { |
diff --git a/server/lib/user.ts b/server/lib/user.ts index e1892f22c..9b0a0a2f1 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts | |||
@@ -193,7 +193,9 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction | | |||
193 | newInstanceFollower: UserNotificationSettingValue.WEB, | 193 | newInstanceFollower: UserNotificationSettingValue.WEB, |
194 | abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 194 | abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
195 | abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 195 | abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
196 | autoInstanceFollowing: UserNotificationSettingValue.WEB | 196 | autoInstanceFollowing: UserNotificationSettingValue.WEB, |
197 | newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
198 | newPluginVersion: UserNotificationSettingValue.WEB | ||
197 | } | 199 | } |
198 | 200 | ||
199 | return UserNotificationSettingModel.create(values, { transaction: t }) | 201 | return UserNotificationSettingModel.create(values, { transaction: t }) |
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts index dbb37e0b2..37c43c3b0 100644 --- a/server/lib/video-blacklist.ts +++ b/server/lib/video-blacklist.ts | |||
@@ -11,7 +11,7 @@ import { | |||
11 | } from '@server/types/models' | 11 | } from '@server/types/models' |
12 | import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models' | 12 | import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models' |
13 | import { UserAdminFlag } from '../../shared/models/users/user-flag.model' | 13 | import { UserAdminFlag } from '../../shared/models/users/user-flag.model' |
14 | import { logger } from '../helpers/logger' | 14 | import { logger, loggerTagsFactory } from '../helpers/logger' |
15 | import { CONFIG } from '../initializers/config' | 15 | import { CONFIG } from '../initializers/config' |
16 | import { VideoBlacklistModel } from '../models/video/video-blacklist' | 16 | import { VideoBlacklistModel } from '../models/video/video-blacklist' |
17 | import { sendDeleteVideo } from './activitypub/send' | 17 | import { sendDeleteVideo } from './activitypub/send' |
@@ -20,6 +20,8 @@ import { LiveManager } from './live-manager' | |||
20 | import { Notifier } from './notifier' | 20 | import { Notifier } from './notifier' |
21 | import { Hooks } from './plugins/hooks' | 21 | import { Hooks } from './plugins/hooks' |
22 | 22 | ||
23 | const lTags = loggerTagsFactory('blacklist') | ||
24 | |||
23 | async function autoBlacklistVideoIfNeeded (parameters: { | 25 | async function autoBlacklistVideoIfNeeded (parameters: { |
24 | video: MVideoWithBlacklistLight | 26 | video: MVideoWithBlacklistLight |
25 | user?: MUser | 27 | user?: MUser |
@@ -60,7 +62,7 @@ async function autoBlacklistVideoIfNeeded (parameters: { | |||
60 | }) | 62 | }) |
61 | } | 63 | } |
62 | 64 | ||
63 | logger.info('Video %s auto-blacklisted.', video.uuid) | 65 | logger.info('Video %s auto-blacklisted.', video.uuid, lTags(video.uuid)) |
64 | 66 | ||
65 | return true | 67 | return true |
66 | } | 68 | } |
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts index 49bdf4869..0476cb2d5 100644 --- a/server/lib/video-channel.ts +++ b/server/lib/video-channel.ts | |||
@@ -3,18 +3,12 @@ import { v4 as uuidv4 } from 'uuid' | |||
3 | import { VideoChannelCreate } from '../../shared/models' | 3 | import { VideoChannelCreate } from '../../shared/models' |
4 | import { VideoModel } from '../models/video/video' | 4 | import { VideoModel } from '../models/video/video' |
5 | import { VideoChannelModel } from '../models/video/video-channel' | 5 | import { VideoChannelModel } from '../models/video/video-channel' |
6 | import { MAccountId, MChannelDefault, MChannelId } from '../types/models' | 6 | import { MAccountId, MChannelId } from '../types/models' |
7 | import { buildActorInstance } from './activitypub/actor' | 7 | import { buildActorInstance } from './activitypub/actor' |
8 | import { getLocalVideoChannelActivityPubUrl } from './activitypub/url' | 8 | import { getLocalVideoChannelActivityPubUrl } from './activitypub/url' |
9 | import { federateVideoIfNeeded } from './activitypub/videos' | 9 | import { federateVideoIfNeeded } from './activitypub/videos' |
10 | 10 | ||
11 | type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & { Account?: T } | 11 | async function createLocalVideoChannel (videoChannelInfo: VideoChannelCreate, account: MAccountId, t: Sequelize.Transaction) { |
12 | |||
13 | async function createLocalVideoChannel <T extends MAccountId> ( | ||
14 | videoChannelInfo: VideoChannelCreate, | ||
15 | account: T, | ||
16 | t: Sequelize.Transaction | ||
17 | ): Promise<CustomVideoChannelModelAccount<T>> { | ||
18 | const uuid = uuidv4() | 12 | const uuid = uuidv4() |
19 | const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name) | 13 | const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name) |
20 | const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid) | 14 | const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid) |
@@ -32,13 +26,11 @@ async function createLocalVideoChannel <T extends MAccountId> ( | |||
32 | const videoChannel = new VideoChannelModel(videoChannelData) | 26 | const videoChannel = new VideoChannelModel(videoChannelData) |
33 | 27 | ||
34 | const options = { transaction: t } | 28 | const options = { transaction: t } |
35 | const videoChannelCreated: CustomVideoChannelModelAccount<T> = await videoChannel.save(options) as MChannelDefault | 29 | const videoChannelCreated = await videoChannel.save(options) |
36 | 30 | ||
37 | // Do not forget to add Account/Actor information to the created video channel | ||
38 | videoChannelCreated.Account = account | ||
39 | videoChannelCreated.Actor = actorInstanceCreated | 31 | videoChannelCreated.Actor = actorInstanceCreated |
40 | 32 | ||
41 | // No need to seed this empty video channel to followers | 33 | // No need to send this empty video channel to followers |
42 | return videoChannelCreated | 34 | return videoChannelCreated |
43 | } | 35 | } |
44 | 36 | ||
diff --git a/server/middlewares/oauth.ts b/server/middlewares/auth.ts index 280595acc..f38373624 100644 --- a/server/middlewares/oauth.ts +++ b/server/middlewares/auth.ts | |||
@@ -1,15 +1,19 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { Socket } from 'socket.io' | 2 | import { Socket } from 'socket.io' |
3 | import { oAuthServer } from '@server/lib/auth' | 3 | import { getAccessToken } from '@server/lib/auth/oauth-model' |
4 | import { logger } from '../helpers/logger' | ||
5 | import { getAccessToken } from '../lib/oauth-model' | ||
6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 4 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
5 | import { logger } from '../helpers/logger' | ||
6 | import { handleOAuthAuthenticate } from '../lib/auth/oauth' | ||
7 | 7 | ||
8 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) { | 8 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) { |
9 | const options = authenticateInQuery ? { allowBearerTokensInQueryString: true } : {} | 9 | handleOAuthAuthenticate(req, res, authenticateInQuery) |
10 | .then((token: any) => { | ||
11 | res.locals.oauth = { token } | ||
12 | res.locals.authenticated = true | ||
10 | 13 | ||
11 | oAuthServer.authenticate(options)(req, res, err => { | 14 | return next() |
12 | if (err) { | 15 | }) |
16 | .catch(err => { | ||
13 | logger.warn('Cannot authenticate.', { err }) | 17 | logger.warn('Cannot authenticate.', { err }) |
14 | 18 | ||
15 | return res.status(err.status) | 19 | return res.status(err.status) |
@@ -17,13 +21,7 @@ function authenticate (req: express.Request, res: express.Response, next: expres | |||
17 | error: 'Token is invalid.', | 21 | error: 'Token is invalid.', |
18 | code: err.name | 22 | code: err.name |
19 | }) | 23 | }) |
20 | .end() | 24 | }) |
21 | } | ||
22 | |||
23 | res.locals.authenticated = true | ||
24 | |||
25 | return next() | ||
26 | }) | ||
27 | } | 25 | } |
28 | 26 | ||
29 | function authenticateSocket (socket: Socket, next: (err?: any) => void) { | 27 | function authenticateSocket (socket: Socket, next: (err?: any) => void) { |
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index b758a8586..3e280e16f 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | export * from './validators' | 1 | export * from './validators' |
2 | export * from './activitypub' | 2 | export * from './activitypub' |
3 | export * from './async' | 3 | export * from './async' |
4 | export * from './oauth' | 4 | export * from './auth' |
5 | export * from './pagination' | 5 | export * from './pagination' |
6 | export * from './servers' | 6 | export * from './servers' |
7 | export * from './sort' | 7 | export * from './sort' |
diff --git a/server/middlewares/validators/activitypub/signature.ts b/server/middlewares/validators/activitypub/signature.ts index 02b191480..7c4e49463 100644 --- a/server/middlewares/validators/activitypub/signature.ts +++ b/server/middlewares/validators/activitypub/signature.ts | |||
@@ -23,7 +23,7 @@ const signatureValidator = [ | |||
23 | .custom(isSignatureValueValid).withMessage('Should have a valid signature value'), | 23 | .custom(isSignatureValueValid).withMessage('Should have a valid signature value'), |
24 | 24 | ||
25 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 25 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
26 | logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } }) | 26 | logger.debug('Checking Linked Data Signature parameter', { parameters: { signature: req.body.signature } }) |
27 | 27 | ||
28 | if (areValidationErrors(req, res)) return | 28 | if (areValidationErrors(req, res)) return |
29 | 29 | ||
diff --git a/server/middlewares/validators/actor-image.ts b/server/middlewares/validators/actor-image.ts new file mode 100644 index 000000000..961d7a7e5 --- /dev/null +++ b/server/middlewares/validators/actor-image.ts | |||
@@ -0,0 +1,30 @@ | |||
1 | import * as express from 'express' | ||
2 | import { body } from 'express-validator' | ||
3 | import { isActorImageFile } from '@server/helpers/custom-validators/actor-images' | ||
4 | import { cleanUpReqFiles } from '../../helpers/express-utils' | ||
5 | import { logger } from '../../helpers/logger' | ||
6 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | ||
7 | import { areValidationErrors } from './utils' | ||
8 | |||
9 | const updateActorImageValidatorFactory = (fieldname: string) => ([ | ||
10 | body(fieldname).custom((value, { req }) => isActorImageFile(req.files, fieldname)).withMessage( | ||
11 | 'This file is not supported or too large. Please, make sure it is of the following type : ' + | ||
12 | CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME.join(', ') | ||
13 | ), | ||
14 | |||
15 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
16 | logger.debug('Checking updateActorImageValidator parameters', { files: req.files }) | ||
17 | |||
18 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | ||
19 | |||
20 | return next() | ||
21 | } | ||
22 | ]) | ||
23 | |||
24 | const updateAvatarValidator = updateActorImageValidatorFactory('avatarfile') | ||
25 | const updateBannerValidator = updateActorImageValidatorFactory('bannerfile') | ||
26 | |||
27 | export { | ||
28 | updateAvatarValidator, | ||
29 | updateBannerValidator | ||
30 | } | ||
diff --git a/server/middlewares/validators/avatar.ts b/server/middlewares/validators/avatar.ts deleted file mode 100644 index 2acb97483..000000000 --- a/server/middlewares/validators/avatar.ts +++ /dev/null | |||
@@ -1,26 +0,0 @@ | |||
1 | import * as express from 'express' | ||
2 | import { body } from 'express-validator' | ||
3 | import { isAvatarFile } from '../../helpers/custom-validators/users' | ||
4 | import { areValidationErrors } from './utils' | ||
5 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | ||
6 | import { logger } from '../../helpers/logger' | ||
7 | import { cleanUpReqFiles } from '../../helpers/express-utils' | ||
8 | |||
9 | const updateAvatarValidator = [ | ||
10 | body('avatarfile').custom((value, { req }) => isAvatarFile(req.files)).withMessage( | ||
11 | 'This file is not supported or too large. Please, make sure it is of the following type : ' + | ||
12 | CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME.join(', ') | ||
13 | ), | ||
14 | |||
15 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
16 | logger.debug('Checking updateAvatarValidator parameters', { files: req.files }) | ||
17 | |||
18 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | ||
19 | |||
20 | return next() | ||
21 | } | ||
22 | ] | ||
23 | |||
24 | export { | ||
25 | updateAvatarValidator | ||
26 | } | ||
diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts index a590aca99..bb849dc72 100644 --- a/server/middlewares/validators/follows.ts +++ b/server/middlewares/validators/follows.ts | |||
@@ -68,7 +68,6 @@ const removeFollowingValidator = [ | |||
68 | .json({ | 68 | .json({ |
69 | error: `Following ${req.params.host} not found.` | 69 | error: `Following ${req.params.host} not found.` |
70 | }) | 70 | }) |
71 | .end() | ||
72 | } | 71 | } |
73 | 72 | ||
74 | res.locals.follow = follow | 73 | res.locals.follow = follow |
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts index 4086d77aa..24faeea3e 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | export * from './abuse' | 1 | export * from './abuse' |
2 | export * from './account' | 2 | export * from './account' |
3 | export * from './actor-image' | ||
3 | export * from './blocklist' | 4 | export * from './blocklist' |
4 | export * from './oembed' | 5 | export * from './oembed' |
5 | export * from './activitypub' | 6 | export * from './activitypub' |
diff --git a/server/middlewares/validators/jobs.ts b/server/middlewares/validators/jobs.ts index 99ef25e0a..d87b28c06 100644 --- a/server/middlewares/validators/jobs.ts +++ b/server/middlewares/validators/jobs.ts | |||
@@ -1,9 +1,11 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { param, query } from 'express-validator' | 2 | import { param, query } from 'express-validator' |
3 | import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs' | 3 | import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs' |
4 | import { logger } from '../../helpers/logger' | 4 | import { logger, loggerTagsFactory } from '../../helpers/logger' |
5 | import { areValidationErrors } from './utils' | 5 | import { areValidationErrors } from './utils' |
6 | 6 | ||
7 | const lTags = loggerTagsFactory('validators', 'jobs') | ||
8 | |||
7 | const listJobsValidator = [ | 9 | const listJobsValidator = [ |
8 | param('state') | 10 | param('state') |
9 | .optional() | 11 | .optional() |
@@ -14,7 +16,7 @@ const listJobsValidator = [ | |||
14 | .custom(isValidJobType).withMessage('Should have a valid job state'), | 16 | .custom(isValidJobType).withMessage('Should have a valid job state'), |
15 | 17 | ||
16 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 18 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
17 | logger.debug('Checking listJobsValidator parameters.', { parameters: req.params }) | 19 | logger.debug('Checking listJobsValidator parameters.', { parameters: req.params, ...lTags() }) |
18 | 20 | ||
19 | if (areValidationErrors(req, res)) return | 21 | if (areValidationErrors(req, res)) return |
20 | 22 | ||
diff --git a/server/middlewares/validators/pagination.ts b/server/middlewares/validators/pagination.ts index 1cae7848c..6b0a83d80 100644 --- a/server/middlewares/validators/pagination.ts +++ b/server/middlewares/validators/pagination.ts | |||
@@ -4,25 +4,30 @@ import { logger } from '../../helpers/logger' | |||
4 | import { areValidationErrors } from './utils' | 4 | import { areValidationErrors } from './utils' |
5 | import { PAGINATION } from '@server/initializers/constants' | 5 | import { PAGINATION } from '@server/initializers/constants' |
6 | 6 | ||
7 | const paginationValidator = [ | 7 | const paginationValidator = paginationValidatorBuilder() |
8 | query('start') | ||
9 | .optional() | ||
10 | .isInt({ min: 0 }).withMessage('Should have a number start'), | ||
11 | query('count') | ||
12 | .optional() | ||
13 | .isInt({ min: 0, max: PAGINATION.GLOBAL.COUNT.MAX }).withMessage(`Should have a number count (max: ${PAGINATION.GLOBAL.COUNT.MAX})`), | ||
14 | 8 | ||
15 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 9 | function paginationValidatorBuilder (tags: string[] = []) { |
16 | logger.debug('Checking pagination parameters', { parameters: req.query }) | 10 | return [ |
11 | query('start') | ||
12 | .optional() | ||
13 | .isInt({ min: 0 }).withMessage('Should have a number start'), | ||
14 | query('count') | ||
15 | .optional() | ||
16 | .isInt({ min: 0, max: PAGINATION.GLOBAL.COUNT.MAX }).withMessage(`Should have a number count (max: ${PAGINATION.GLOBAL.COUNT.MAX})`), | ||
17 | 17 | ||
18 | if (areValidationErrors(req, res)) return | 18 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
19 | logger.debug('Checking pagination parameters', { parameters: req.query, tags }) | ||
19 | 20 | ||
20 | return next() | 21 | if (areValidationErrors(req, res)) return |
21 | } | 22 | |
22 | ] | 23 | return next() |
24 | } | ||
25 | ] | ||
26 | } | ||
23 | 27 | ||
24 | // --------------------------------------------------------------------------- | 28 | // --------------------------------------------------------------------------- |
25 | 29 | ||
26 | export { | 30 | export { |
27 | paginationValidator | 31 | paginationValidator, |
32 | paginationValidatorBuilder | ||
28 | } | 33 | } |
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index e93ceb200..beecc155b 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -28,7 +28,7 @@ const SORTABLE_VIDEO_REDUNDANCIES_COLUMNS = createSortableColumns(SORTABLE_COLUM | |||
28 | 28 | ||
29 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) | 29 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) |
30 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) | 30 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) |
31 | const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) | 31 | const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS, [ 'jobs' ]) |
32 | const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS) | 32 | const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS) |
33 | const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) | 33 | const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) |
34 | const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) | 34 | const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) |
diff --git a/server/middlewares/validators/utils.ts b/server/middlewares/validators/utils.ts index 2899bed6f..4167f6d43 100644 --- a/server/middlewares/validators/utils.ts +++ b/server/middlewares/validators/utils.ts | |||
@@ -17,12 +17,12 @@ function areValidationErrors (req: express.Request, res: express.Response) { | |||
17 | return false | 17 | return false |
18 | } | 18 | } |
19 | 19 | ||
20 | function checkSort (sortableColumns: string[]) { | 20 | function checkSort (sortableColumns: string[], tags: string[] = []) { |
21 | return [ | 21 | return [ |
22 | query('sort').optional().isIn(sortableColumns).withMessage('Should have correct sortable column'), | 22 | query('sort').optional().isIn(sortableColumns).withMessage('Should have correct sortable column'), |
23 | 23 | ||
24 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 24 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
25 | logger.debug('Checking sort parameters', { parameters: req.query }) | 25 | logger.debug('Checking sort parameters', { parameters: req.query, tags }) |
26 | 26 | ||
27 | if (areValidationErrors(req, res)) return | 27 | if (areValidationErrors(req, res)) return |
28 | 28 | ||
diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts index 57ac548b9..2463d281c 100644 --- a/server/middlewares/validators/videos/video-channels.ts +++ b/server/middlewares/validators/videos/video-channels.ts | |||
@@ -73,13 +73,11 @@ const videoChannelsUpdateValidator = [ | |||
73 | if (res.locals.videoChannel.Actor.isOwned() === false) { | 73 | if (res.locals.videoChannel.Actor.isOwned() === false) { |
74 | return res.status(HttpStatusCode.FORBIDDEN_403) | 74 | return res.status(HttpStatusCode.FORBIDDEN_403) |
75 | .json({ error: 'Cannot update video channel of another server' }) | 75 | .json({ error: 'Cannot update video channel of another server' }) |
76 | .end() | ||
77 | } | 76 | } |
78 | 77 | ||
79 | if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) { | 78 | if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) { |
80 | return res.status(HttpStatusCode.FORBIDDEN_403) | 79 | return res.status(HttpStatusCode.FORBIDDEN_403) |
81 | .json({ error: 'Cannot update video channel of another user' }) | 80 | .json({ error: 'Cannot update video channel of another user' }) |
82 | .end() | ||
83 | } | 81 | } |
84 | 82 | ||
85 | return next() | 83 | return next() |
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts index 226c9d436..1afacfed8 100644 --- a/server/middlewares/validators/videos/video-comments.ts +++ b/server/middlewares/validators/videos/video-comments.ts | |||
@@ -216,7 +216,7 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon | |||
216 | if (!acceptedResult || acceptedResult.accepted !== true) { | 216 | if (!acceptedResult || acceptedResult.accepted !== true) { |
217 | logger.info('Refused local comment.', { acceptedResult, acceptParameters }) | 217 | logger.info('Refused local comment.', { acceptedResult, acceptParameters }) |
218 | res.status(HttpStatusCode.FORBIDDEN_403) | 218 | res.status(HttpStatusCode.FORBIDDEN_403) |
219 | .json({ error: acceptedResult.errorMessage || 'Refused local comment' }) | 219 | .json({ error: acceptedResult?.errorMessage || 'Refused local comment' }) |
220 | 220 | ||
221 | return false | 221 | return false |
222 | } | 222 | } |
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts index 0fba4f5fd..c872d045e 100644 --- a/server/middlewares/validators/videos/video-playlists.ts +++ b/server/middlewares/validators/videos/video-playlists.ts | |||
@@ -29,7 +29,7 @@ import { doesVideoChannelIdExist, doesVideoExist, doesVideoPlaylistExist, VideoP | |||
29 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' | 29 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' |
30 | import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' | 30 | import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' |
31 | import { MVideoPlaylist } from '../../../types/models/video/video-playlist' | 31 | import { MVideoPlaylist } from '../../../types/models/video/video-playlist' |
32 | import { authenticatePromiseIfNeeded } from '../../oauth' | 32 | import { authenticatePromiseIfNeeded } from '../../auth' |
33 | import { areValidationErrors } from '../utils' | 33 | import { areValidationErrors } from '../utils' |
34 | 34 | ||
35 | const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ | 35 | const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ |
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 37cc07b94..4d31d3dcb 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -54,7 +54,7 @@ import { isLocalVideoAccepted } from '../../../lib/moderation' | |||
54 | import { Hooks } from '../../../lib/plugins/hooks' | 54 | import { Hooks } from '../../../lib/plugins/hooks' |
55 | import { AccountModel } from '../../../models/account/account' | 55 | import { AccountModel } from '../../../models/account/account' |
56 | import { VideoModel } from '../../../models/video/video' | 56 | import { VideoModel } from '../../../models/video/video' |
57 | import { authenticatePromiseIfNeeded } from '../../oauth' | 57 | import { authenticatePromiseIfNeeded } from '../../auth' |
58 | import { areValidationErrors } from '../utils' | 58 | import { areValidationErrors } from '../utils' |
59 | 59 | ||
60 | const videosAddValidator = getCommonVideoEditAttributes().concat([ | 60 | const videosAddValidator = getCommonVideoEditAttributes().concat([ |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index c72f9c63d..312451abe 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -33,7 +33,7 @@ import { | |||
33 | import { ActorModel } from '../activitypub/actor' | 33 | import { ActorModel } from '../activitypub/actor' |
34 | import { ActorFollowModel } from '../activitypub/actor-follow' | 34 | import { ActorFollowModel } from '../activitypub/actor-follow' |
35 | import { ApplicationModel } from '../application/application' | 35 | import { ApplicationModel } from '../application/application' |
36 | import { AvatarModel } from '../avatar/avatar' | 36 | import { ActorImageModel } from './actor-image' |
37 | import { ServerModel } from '../server/server' | 37 | import { ServerModel } from '../server/server' |
38 | import { ServerBlocklistModel } from '../server/server-blocklist' | 38 | import { ServerBlocklistModel } from '../server/server-blocklist' |
39 | import { getSort, throwIfNotValid } from '../utils' | 39 | import { getSort, throwIfNotValid } from '../utils' |
@@ -82,7 +82,8 @@ export type SummaryOptions = { | |||
82 | serverInclude, | 82 | serverInclude, |
83 | 83 | ||
84 | { | 84 | { |
85 | model: AvatarModel.unscoped(), | 85 | model: ActorImageModel.unscoped(), |
86 | as: 'Avatar', | ||
86 | required: false | 87 | required: false |
87 | } | 88 | } |
88 | ] | 89 | ] |
diff --git a/server/models/account/actor-image.ts b/server/models/account/actor-image.ts new file mode 100644 index 000000000..ae05b4969 --- /dev/null +++ b/server/models/account/actor-image.ts | |||
@@ -0,0 +1,100 @@ | |||
1 | import { remove } from 'fs-extra' | ||
2 | import { join } from 'path' | ||
3 | import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
4 | import { MActorImageFormattable } from '@server/types/models' | ||
5 | import { ActorImageType } from '@shared/models' | ||
6 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' | ||
7 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
8 | import { logger } from '../../helpers/logger' | ||
9 | import { CONFIG } from '../../initializers/config' | ||
10 | import { LAZY_STATIC_PATHS } from '../../initializers/constants' | ||
11 | import { throwIfNotValid } from '../utils' | ||
12 | |||
13 | @Table({ | ||
14 | tableName: 'actorImage', | ||
15 | indexes: [ | ||
16 | { | ||
17 | fields: [ 'filename' ], | ||
18 | unique: true | ||
19 | } | ||
20 | ] | ||
21 | }) | ||
22 | export class ActorImageModel extends Model { | ||
23 | |||
24 | @AllowNull(false) | ||
25 | @Column | ||
26 | filename: string | ||
27 | |||
28 | @AllowNull(true) | ||
29 | @Default(null) | ||
30 | @Column | ||
31 | height: number | ||
32 | |||
33 | @AllowNull(true) | ||
34 | @Default(null) | ||
35 | @Column | ||
36 | width: number | ||
37 | |||
38 | @AllowNull(true) | ||
39 | @Is('ActorImageFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true)) | ||
40 | @Column | ||
41 | fileUrl: string | ||
42 | |||
43 | @AllowNull(false) | ||
44 | @Column | ||
45 | onDisk: boolean | ||
46 | |||
47 | @AllowNull(false) | ||
48 | @Column | ||
49 | type: ActorImageType | ||
50 | |||
51 | @CreatedAt | ||
52 | createdAt: Date | ||
53 | |||
54 | @UpdatedAt | ||
55 | updatedAt: Date | ||
56 | |||
57 | @AfterDestroy | ||
58 | static removeFilesAndSendDelete (instance: ActorImageModel) { | ||
59 | logger.info('Removing actor image file %s.', instance.filename) | ||
60 | |||
61 | // Don't block the transaction | ||
62 | instance.removeImage() | ||
63 | .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, err)) | ||
64 | } | ||
65 | |||
66 | static loadByName (filename: string) { | ||
67 | const query = { | ||
68 | where: { | ||
69 | filename | ||
70 | } | ||
71 | } | ||
72 | |||
73 | return ActorImageModel.findOne(query) | ||
74 | } | ||
75 | |||
76 | toFormattedJSON (this: MActorImageFormattable): ActorImage { | ||
77 | return { | ||
78 | path: this.getStaticPath(), | ||
79 | createdAt: this.createdAt, | ||
80 | updatedAt: this.updatedAt | ||
81 | } | ||
82 | } | ||
83 | |||
84 | getStaticPath () { | ||
85 | if (this.type === ActorImageType.AVATAR) { | ||
86 | return join(LAZY_STATIC_PATHS.AVATARS, this.filename) | ||
87 | } | ||
88 | |||
89 | return join(LAZY_STATIC_PATHS.BANNERS, this.filename) | ||
90 | } | ||
91 | |||
92 | getPath () { | ||
93 | return join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) | ||
94 | } | ||
95 | |||
96 | removeImage () { | ||
97 | const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) | ||
98 | return remove(imagePath) | ||
99 | } | ||
100 | } | ||
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts index ebab8b6d2..138051528 100644 --- a/server/models/account/user-notification-setting.ts +++ b/server/models/account/user-notification-setting.ts | |||
@@ -12,10 +12,10 @@ import { | |||
12 | Table, | 12 | Table, |
13 | UpdatedAt | 13 | UpdatedAt |
14 | } from 'sequelize-typescript' | 14 | } from 'sequelize-typescript' |
15 | import { TokensCache } from '@server/lib/auth/tokens-cache' | ||
15 | import { MNotificationSettingFormattable } from '@server/types/models' | 16 | import { MNotificationSettingFormattable } from '@server/types/models' |
16 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | 17 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' |
17 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | 18 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' |
18 | import { clearCacheByUserId } from '../../lib/oauth-model' | ||
19 | import { throwIfNotValid } from '../utils' | 19 | import { throwIfNotValid } from '../utils' |
20 | import { UserModel } from './user' | 20 | import { UserModel } from './user' |
21 | 21 | ||
@@ -156,6 +156,24 @@ export class UserNotificationSettingModel extends Model { | |||
156 | @Column | 156 | @Column |
157 | abuseNewMessage: UserNotificationSettingValue | 157 | abuseNewMessage: UserNotificationSettingValue |
158 | 158 | ||
159 | @AllowNull(false) | ||
160 | @Default(null) | ||
161 | @Is( | ||
162 | 'UserNotificationSettingNewPeerTubeVersion', | ||
163 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPeerTubeVersion') | ||
164 | ) | ||
165 | @Column | ||
166 | newPeerTubeVersion: UserNotificationSettingValue | ||
167 | |||
168 | @AllowNull(false) | ||
169 | @Default(null) | ||
170 | @Is( | ||
171 | 'UserNotificationSettingNewPeerPluginVersion', | ||
172 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPluginVersion') | ||
173 | ) | ||
174 | @Column | ||
175 | newPluginVersion: UserNotificationSettingValue | ||
176 | |||
159 | @ForeignKey(() => UserModel) | 177 | @ForeignKey(() => UserModel) |
160 | @Column | 178 | @Column |
161 | userId: number | 179 | userId: number |
@@ -177,7 +195,7 @@ export class UserNotificationSettingModel extends Model { | |||
177 | @AfterUpdate | 195 | @AfterUpdate |
178 | @AfterDestroy | 196 | @AfterDestroy |
179 | static removeTokenCache (instance: UserNotificationSettingModel) { | 197 | static removeTokenCache (instance: UserNotificationSettingModel) { |
180 | return clearCacheByUserId(instance.userId) | 198 | return TokensCache.Instance.clearCacheByUserId(instance.userId) |
181 | } | 199 | } |
182 | 200 | ||
183 | toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting { | 201 | toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting { |
@@ -195,7 +213,9 @@ export class UserNotificationSettingModel extends Model { | |||
195 | newInstanceFollower: this.newInstanceFollower, | 213 | newInstanceFollower: this.newInstanceFollower, |
196 | autoInstanceFollowing: this.autoInstanceFollowing, | 214 | autoInstanceFollowing: this.autoInstanceFollowing, |
197 | abuseNewMessage: this.abuseNewMessage, | 215 | abuseNewMessage: this.abuseNewMessage, |
198 | abuseStateChange: this.abuseStateChange | 216 | abuseStateChange: this.abuseStateChange, |
217 | newPeerTubeVersion: this.newPeerTubeVersion, | ||
218 | newPluginVersion: this.newPluginVersion | ||
199 | } | 219 | } |
200 | } | 220 | } |
201 | } | 221 | } |
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts index add129644..805095002 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts | |||
@@ -9,7 +9,8 @@ import { VideoAbuseModel } from '../abuse/video-abuse' | |||
9 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | 9 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' |
10 | import { ActorModel } from '../activitypub/actor' | 10 | import { ActorModel } from '../activitypub/actor' |
11 | import { ActorFollowModel } from '../activitypub/actor-follow' | 11 | import { ActorFollowModel } from '../activitypub/actor-follow' |
12 | import { AvatarModel } from '../avatar/avatar' | 12 | import { ApplicationModel } from '../application/application' |
13 | import { PluginModel } from '../server/plugin' | ||
13 | import { ServerModel } from '../server/server' | 14 | import { ServerModel } from '../server/server' |
14 | import { getSort, throwIfNotValid } from '../utils' | 15 | import { getSort, throwIfNotValid } from '../utils' |
15 | import { VideoModel } from '../video/video' | 16 | import { VideoModel } from '../video/video' |
@@ -18,6 +19,7 @@ import { VideoChannelModel } from '../video/video-channel' | |||
18 | import { VideoCommentModel } from '../video/video-comment' | 19 | import { VideoCommentModel } from '../video/video-comment' |
19 | import { VideoImportModel } from '../video/video-import' | 20 | import { VideoImportModel } from '../video/video-import' |
20 | import { AccountModel } from './account' | 21 | import { AccountModel } from './account' |
22 | import { ActorImageModel } from './actor-image' | ||
21 | import { UserModel } from './user' | 23 | import { UserModel } from './user' |
22 | 24 | ||
23 | enum ScopeNames { | 25 | enum ScopeNames { |
@@ -32,7 +34,8 @@ function buildActorWithAvatarInclude () { | |||
32 | include: [ | 34 | include: [ |
33 | { | 35 | { |
34 | attributes: [ 'filename' ], | 36 | attributes: [ 'filename' ], |
35 | model: AvatarModel.unscoped(), | 37 | as: 'Avatar', |
38 | model: ActorImageModel.unscoped(), | ||
36 | required: false | 39 | required: false |
37 | }, | 40 | }, |
38 | { | 41 | { |
@@ -96,7 +99,7 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
96 | attributes: [ 'id' ], | 99 | attributes: [ 'id' ], |
97 | model: VideoAbuseModel.unscoped(), | 100 | model: VideoAbuseModel.unscoped(), |
98 | required: false, | 101 | required: false, |
99 | include: [ buildVideoInclude(true) ] | 102 | include: [ buildVideoInclude(false) ] |
100 | }, | 103 | }, |
101 | { | 104 | { |
102 | attributes: [ 'id' ], | 105 | attributes: [ 'id' ], |
@@ -106,12 +109,12 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
106 | { | 109 | { |
107 | attributes: [ 'id', 'originCommentId' ], | 110 | attributes: [ 'id', 'originCommentId' ], |
108 | model: VideoCommentModel.unscoped(), | 111 | model: VideoCommentModel.unscoped(), |
109 | required: true, | 112 | required: false, |
110 | include: [ | 113 | include: [ |
111 | { | 114 | { |
112 | attributes: [ 'id', 'name', 'uuid' ], | 115 | attributes: [ 'id', 'name', 'uuid' ], |
113 | model: VideoModel.unscoped(), | 116 | model: VideoModel.unscoped(), |
114 | required: true | 117 | required: false |
115 | } | 118 | } |
116 | ] | 119 | ] |
117 | } | 120 | } |
@@ -120,7 +123,7 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
120 | { | 123 | { |
121 | model: AccountModel, | 124 | model: AccountModel, |
122 | as: 'FlaggedAccount', | 125 | as: 'FlaggedAccount', |
123 | required: true, | 126 | required: false, |
124 | include: [ buildActorWithAvatarInclude() ] | 127 | include: [ buildActorWithAvatarInclude() ] |
125 | } | 128 | } |
126 | ] | 129 | ] |
@@ -141,6 +144,18 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
141 | }, | 144 | }, |
142 | 145 | ||
143 | { | 146 | { |
147 | attributes: [ 'id', 'name', 'type', 'latestVersion' ], | ||
148 | model: PluginModel.unscoped(), | ||
149 | required: false | ||
150 | }, | ||
151 | |||
152 | { | ||
153 | attributes: [ 'id', 'latestPeerTubeVersion' ], | ||
154 | model: ApplicationModel.unscoped(), | ||
155 | required: false | ||
156 | }, | ||
157 | |||
158 | { | ||
144 | attributes: [ 'id', 'state' ], | 159 | attributes: [ 'id', 'state' ], |
145 | model: ActorFollowModel.unscoped(), | 160 | model: ActorFollowModel.unscoped(), |
146 | required: false, | 161 | required: false, |
@@ -158,7 +173,8 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
158 | }, | 173 | }, |
159 | { | 174 | { |
160 | attributes: [ 'filename' ], | 175 | attributes: [ 'filename' ], |
161 | model: AvatarModel.unscoped(), | 176 | as: 'Avatar', |
177 | model: ActorImageModel.unscoped(), | ||
162 | required: false | 178 | required: false |
163 | }, | 179 | }, |
164 | { | 180 | { |
@@ -251,6 +267,22 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
251 | [Op.ne]: null | 267 | [Op.ne]: null |
252 | } | 268 | } |
253 | } | 269 | } |
270 | }, | ||
271 | { | ||
272 | fields: [ 'pluginId' ], | ||
273 | where: { | ||
274 | pluginId: { | ||
275 | [Op.ne]: null | ||
276 | } | ||
277 | } | ||
278 | }, | ||
279 | { | ||
280 | fields: [ 'applicationId' ], | ||
281 | where: { | ||
282 | applicationId: { | ||
283 | [Op.ne]: null | ||
284 | } | ||
285 | } | ||
254 | } | 286 | } |
255 | ] as (ModelIndexesOptions & { where?: WhereOptions })[] | 287 | ] as (ModelIndexesOptions & { where?: WhereOptions })[] |
256 | }) | 288 | }) |
@@ -370,6 +402,30 @@ export class UserNotificationModel extends Model { | |||
370 | }) | 402 | }) |
371 | ActorFollow: ActorFollowModel | 403 | ActorFollow: ActorFollowModel |
372 | 404 | ||
405 | @ForeignKey(() => PluginModel) | ||
406 | @Column | ||
407 | pluginId: number | ||
408 | |||
409 | @BelongsTo(() => PluginModel, { | ||
410 | foreignKey: { | ||
411 | allowNull: true | ||
412 | }, | ||
413 | onDelete: 'cascade' | ||
414 | }) | ||
415 | Plugin: PluginModel | ||
416 | |||
417 | @ForeignKey(() => ApplicationModel) | ||
418 | @Column | ||
419 | applicationId: number | ||
420 | |||
421 | @BelongsTo(() => ApplicationModel, { | ||
422 | foreignKey: { | ||
423 | allowNull: true | ||
424 | }, | ||
425 | onDelete: 'cascade' | ||
426 | }) | ||
427 | Application: ApplicationModel | ||
428 | |||
373 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { | 429 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { |
374 | const where = { userId } | 430 | const where = { userId } |
375 | 431 | ||
@@ -524,6 +580,18 @@ export class UserNotificationModel extends Model { | |||
524 | } | 580 | } |
525 | : undefined | 581 | : undefined |
526 | 582 | ||
583 | const plugin = this.Plugin | ||
584 | ? { | ||
585 | name: this.Plugin.name, | ||
586 | type: this.Plugin.type, | ||
587 | latestVersion: this.Plugin.latestVersion | ||
588 | } | ||
589 | : undefined | ||
590 | |||
591 | const peertube = this.Application | ||
592 | ? { latestVersion: this.Application.latestPeerTubeVersion } | ||
593 | : undefined | ||
594 | |||
527 | return { | 595 | return { |
528 | id: this.id, | 596 | id: this.id, |
529 | type: this.type, | 597 | type: this.type, |
@@ -535,6 +603,8 @@ export class UserNotificationModel extends Model { | |||
535 | videoBlacklist, | 603 | videoBlacklist, |
536 | account, | 604 | account, |
537 | actorFollow, | 605 | actorFollow, |
606 | plugin, | ||
607 | peertube, | ||
538 | createdAt: this.createdAt.toISOString(), | 608 | createdAt: this.createdAt.toISOString(), |
539 | updatedAt: this.updatedAt.toISOString() | 609 | updatedAt: this.updatedAt.toISOString() |
540 | } | 610 | } |
@@ -553,17 +623,19 @@ export class UserNotificationModel extends Model { | |||
553 | ? { | 623 | ? { |
554 | threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), | 624 | threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), |
555 | 625 | ||
556 | video: { | 626 | video: abuse.VideoCommentAbuse.VideoComment.Video |
557 | id: abuse.VideoCommentAbuse.VideoComment.Video.id, | 627 | ? { |
558 | name: abuse.VideoCommentAbuse.VideoComment.Video.name, | 628 | id: abuse.VideoCommentAbuse.VideoComment.Video.id, |
559 | uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid | 629 | name: abuse.VideoCommentAbuse.VideoComment.Video.name, |
560 | } | 630 | uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid |
631 | } | ||
632 | : undefined | ||
561 | } | 633 | } |
562 | : undefined | 634 | : undefined |
563 | 635 | ||
564 | const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined | 636 | const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined |
565 | 637 | ||
566 | const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined | 638 | const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined |
567 | 639 | ||
568 | return { | 640 | return { |
569 | id: abuse.id, | 641 | id: abuse.id, |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index c1f22b76a..00c6d73aa 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -21,6 +21,7 @@ import { | |||
21 | Table, | 21 | Table, |
22 | UpdatedAt | 22 | UpdatedAt |
23 | } from 'sequelize-typescript' | 23 | } from 'sequelize-typescript' |
24 | import { TokensCache } from '@server/lib/auth/tokens-cache' | ||
24 | import { | 25 | import { |
25 | MMyUserFormattable, | 26 | MMyUserFormattable, |
26 | MUser, | 27 | MUser, |
@@ -58,7 +59,6 @@ import { | |||
58 | } from '../../helpers/custom-validators/users' | 59 | } from '../../helpers/custom-validators/users' |
59 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' | 60 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' |
60 | import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' | 61 | import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' |
61 | import { clearCacheByUserId } from '../../lib/oauth-model' | ||
62 | import { getThemeOrDefault } from '../../lib/plugins/theme-utils' | 62 | import { getThemeOrDefault } from '../../lib/plugins/theme-utils' |
63 | import { ActorModel } from '../activitypub/actor' | 63 | import { ActorModel } from '../activitypub/actor' |
64 | import { ActorFollowModel } from '../activitypub/actor-follow' | 64 | import { ActorFollowModel } from '../activitypub/actor-follow' |
@@ -71,6 +71,7 @@ import { VideoLiveModel } from '../video/video-live' | |||
71 | import { VideoPlaylistModel } from '../video/video-playlist' | 71 | import { VideoPlaylistModel } from '../video/video-playlist' |
72 | import { AccountModel } from './account' | 72 | import { AccountModel } from './account' |
73 | import { UserNotificationSettingModel } from './user-notification-setting' | 73 | import { UserNotificationSettingModel } from './user-notification-setting' |
74 | import { ActorImageModel } from './actor-image' | ||
74 | 75 | ||
75 | enum ScopeNames { | 76 | enum ScopeNames { |
76 | FOR_ME_API = 'FOR_ME_API', | 77 | FOR_ME_API = 'FOR_ME_API', |
@@ -97,7 +98,20 @@ enum ScopeNames { | |||
97 | model: AccountModel, | 98 | model: AccountModel, |
98 | include: [ | 99 | include: [ |
99 | { | 100 | { |
100 | model: VideoChannelModel | 101 | model: VideoChannelModel.unscoped(), |
102 | include: [ | ||
103 | { | ||
104 | model: ActorModel, | ||
105 | required: true, | ||
106 | include: [ | ||
107 | { | ||
108 | model: ActorImageModel, | ||
109 | as: 'Banner', | ||
110 | required: false | ||
111 | } | ||
112 | ] | ||
113 | } | ||
114 | ] | ||
101 | }, | 115 | }, |
102 | { | 116 | { |
103 | attributes: [ 'id', 'name', 'type' ], | 117 | attributes: [ 'id', 'name', 'type' ], |
@@ -411,7 +425,7 @@ export class UserModel extends Model { | |||
411 | @AfterUpdate | 425 | @AfterUpdate |
412 | @AfterDestroy | 426 | @AfterDestroy |
413 | static removeTokenCache (instance: UserModel) { | 427 | static removeTokenCache (instance: UserModel) { |
414 | return clearCacheByUserId(instance.id) | 428 | return TokensCache.Instance.clearCacheByUserId(instance.id) |
415 | } | 429 | } |
416 | 430 | ||
417 | static countTotal () { | 431 | static countTotal () { |
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index ce6a4e267..4c5f37620 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts | |||
@@ -248,13 +248,6 @@ export class ActorFollowModel extends Model { | |||
248 | } | 248 | } |
249 | 249 | ||
250 | return ActorFollowModel.findOne(query) | 250 | return ActorFollowModel.findOne(query) |
251 | .then(result => { | ||
252 | if (result?.ActorFollowing.VideoChannel) { | ||
253 | result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing | ||
254 | } | ||
255 | |||
256 | return result | ||
257 | }) | ||
258 | } | 251 | } |
259 | 252 | ||
260 | static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> { | 253 | static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> { |
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 3b98e8841..a6c724f26 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts | |||
@@ -19,7 +19,7 @@ import { | |||
19 | } from 'sequelize-typescript' | 19 | } from 'sequelize-typescript' |
20 | import { ModelCache } from '@server/models/model-cache' | 20 | import { ModelCache } from '@server/models/model-cache' |
21 | import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' | 21 | import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' |
22 | import { Avatar } from '../../../shared/models/avatars/avatar.model' | 22 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' |
23 | import { activityPubContextify } from '../../helpers/activitypub' | 23 | import { activityPubContextify } from '../../helpers/activitypub' |
24 | import { | 24 | import { |
25 | isActorFollowersCountValid, | 25 | isActorFollowersCountValid, |
@@ -29,11 +29,19 @@ import { | |||
29 | isActorPublicKeyValid | 29 | isActorPublicKeyValid |
30 | } from '../../helpers/custom-validators/activitypub/actor' | 30 | } from '../../helpers/custom-validators/activitypub/actor' |
31 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 31 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
32 | import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' | 32 | import { |
33 | ACTIVITY_PUB, | ||
34 | ACTIVITY_PUB_ACTOR_TYPES, | ||
35 | CONSTRAINTS_FIELDS, | ||
36 | MIMETYPES, | ||
37 | SERVER_ACTOR_NAME, | ||
38 | WEBSERVER | ||
39 | } from '../../initializers/constants' | ||
33 | import { | 40 | import { |
34 | MActor, | 41 | MActor, |
35 | MActorAccountChannelId, | 42 | MActorAccountChannelId, |
36 | MActorAP, | 43 | MActorAPAccount, |
44 | MActorAPChannel, | ||
37 | MActorFormattable, | 45 | MActorFormattable, |
38 | MActorFull, | 46 | MActorFull, |
39 | MActorHost, | 47 | MActorHost, |
@@ -43,7 +51,7 @@ import { | |||
43 | MActorWithInboxes | 51 | MActorWithInboxes |
44 | } from '../../types/models' | 52 | } from '../../types/models' |
45 | import { AccountModel } from '../account/account' | 53 | import { AccountModel } from '../account/account' |
46 | import { AvatarModel } from '../avatar/avatar' | 54 | import { ActorImageModel } from '../account/actor-image' |
47 | import { ServerModel } from '../server/server' | 55 | import { ServerModel } from '../server/server' |
48 | import { isOutdated, throwIfNotValid } from '../utils' | 56 | import { isOutdated, throwIfNotValid } from '../utils' |
49 | import { VideoModel } from '../video/video' | 57 | import { VideoModel } from '../video/video' |
@@ -73,7 +81,8 @@ export const unusedActorAttributesForAPI = [ | |||
73 | required: false | 81 | required: false |
74 | }, | 82 | }, |
75 | { | 83 | { |
76 | model: AvatarModel, | 84 | model: ActorImageModel, |
85 | as: 'Avatar', | ||
77 | required: false | 86 | required: false |
78 | } | 87 | } |
79 | ] | 88 | ] |
@@ -100,7 +109,13 @@ export const unusedActorAttributesForAPI = [ | |||
100 | required: false | 109 | required: false |
101 | }, | 110 | }, |
102 | { | 111 | { |
103 | model: AvatarModel, | 112 | model: ActorImageModel, |
113 | as: 'Avatar', | ||
114 | required: false | ||
115 | }, | ||
116 | { | ||
117 | model: ActorImageModel, | ||
118 | as: 'Banner', | ||
104 | required: false | 119 | required: false |
105 | } | 120 | } |
106 | ] | 121 | ] |
@@ -213,18 +228,35 @@ export class ActorModel extends Model { | |||
213 | @UpdatedAt | 228 | @UpdatedAt |
214 | updatedAt: Date | 229 | updatedAt: Date |
215 | 230 | ||
216 | @ForeignKey(() => AvatarModel) | 231 | @ForeignKey(() => ActorImageModel) |
217 | @Column | 232 | @Column |
218 | avatarId: number | 233 | avatarId: number |
219 | 234 | ||
220 | @BelongsTo(() => AvatarModel, { | 235 | @ForeignKey(() => ActorImageModel) |
236 | @Column | ||
237 | bannerId: number | ||
238 | |||
239 | @BelongsTo(() => ActorImageModel, { | ||
221 | foreignKey: { | 240 | foreignKey: { |
241 | name: 'avatarId', | ||
222 | allowNull: true | 242 | allowNull: true |
223 | }, | 243 | }, |
244 | as: 'Avatar', | ||
224 | onDelete: 'set null', | 245 | onDelete: 'set null', |
225 | hooks: true | 246 | hooks: true |
226 | }) | 247 | }) |
227 | Avatar: AvatarModel | 248 | Avatar: ActorImageModel |
249 | |||
250 | @BelongsTo(() => ActorImageModel, { | ||
251 | foreignKey: { | ||
252 | name: 'bannerId', | ||
253 | allowNull: true | ||
254 | }, | ||
255 | as: 'Banner', | ||
256 | onDelete: 'set null', | ||
257 | hooks: true | ||
258 | }) | ||
259 | Banner: ActorImageModel | ||
228 | 260 | ||
229 | @HasMany(() => ActorFollowModel, { | 261 | @HasMany(() => ActorFollowModel, { |
230 | foreignKey: { | 262 | foreignKey: { |
@@ -496,7 +528,7 @@ export class ActorModel extends Model { | |||
496 | } | 528 | } |
497 | 529 | ||
498 | toFormattedSummaryJSON (this: MActorSummaryFormattable) { | 530 | toFormattedSummaryJSON (this: MActorSummaryFormattable) { |
499 | let avatar: Avatar = null | 531 | let avatar: ActorImage = null |
500 | if (this.Avatar) { | 532 | if (this.Avatar) { |
501 | avatar = this.Avatar.toFormattedJSON() | 533 | avatar = this.Avatar.toFormattedJSON() |
502 | } | 534 | } |
@@ -512,29 +544,51 @@ export class ActorModel extends Model { | |||
512 | toFormattedJSON (this: MActorFormattable) { | 544 | toFormattedJSON (this: MActorFormattable) { |
513 | const base = this.toFormattedSummaryJSON() | 545 | const base = this.toFormattedSummaryJSON() |
514 | 546 | ||
547 | let banner: ActorImage = null | ||
548 | if (this.bannerId) { | ||
549 | banner = this.Banner.toFormattedJSON() | ||
550 | } | ||
551 | |||
515 | return Object.assign(base, { | 552 | return Object.assign(base, { |
516 | id: this.id, | 553 | id: this.id, |
517 | hostRedundancyAllowed: this.getRedundancyAllowed(), | 554 | hostRedundancyAllowed: this.getRedundancyAllowed(), |
518 | followingCount: this.followingCount, | 555 | followingCount: this.followingCount, |
519 | followersCount: this.followersCount, | 556 | followersCount: this.followersCount, |
557 | banner, | ||
520 | createdAt: this.createdAt, | 558 | createdAt: this.createdAt, |
521 | updatedAt: this.updatedAt | 559 | updatedAt: this.updatedAt |
522 | }) | 560 | }) |
523 | } | 561 | } |
524 | 562 | ||
525 | toActivityPubObject (this: MActorAP, name: string) { | 563 | toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) { |
526 | let icon: ActivityIconObject | 564 | let icon: ActivityIconObject |
565 | let image: ActivityIconObject | ||
527 | 566 | ||
528 | if (this.avatarId) { | 567 | if (this.avatarId) { |
529 | const extension = extname(this.Avatar.filename) | 568 | const extension = extname(this.Avatar.filename) |
530 | 569 | ||
531 | icon = { | 570 | icon = { |
532 | type: 'Image', | 571 | type: 'Image', |
533 | mediaType: extension === '.png' ? 'image/png' : 'image/jpeg', | 572 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], |
573 | height: this.Avatar.height, | ||
574 | width: this.Avatar.width, | ||
534 | url: this.getAvatarUrl() | 575 | url: this.getAvatarUrl() |
535 | } | 576 | } |
536 | } | 577 | } |
537 | 578 | ||
579 | if (this.bannerId) { | ||
580 | const banner = (this as MActorAPChannel).Banner | ||
581 | const extension = extname(banner.filename) | ||
582 | |||
583 | image = { | ||
584 | type: 'Image', | ||
585 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], | ||
586 | height: banner.height, | ||
587 | width: banner.width, | ||
588 | url: this.getBannerUrl() | ||
589 | } | ||
590 | } | ||
591 | |||
538 | const json = { | 592 | const json = { |
539 | type: this.type, | 593 | type: this.type, |
540 | id: this.url, | 594 | id: this.url, |
@@ -554,7 +608,8 @@ export class ActorModel extends Model { | |||
554 | owner: this.url, | 608 | owner: this.url, |
555 | publicKeyPem: this.publicKey | 609 | publicKeyPem: this.publicKey |
556 | }, | 610 | }, |
557 | icon | 611 | icon, |
612 | image | ||
558 | } | 613 | } |
559 | 614 | ||
560 | return activityPubContextify(json) | 615 | return activityPubContextify(json) |
@@ -624,6 +679,12 @@ export class ActorModel extends Model { | |||
624 | return WEBSERVER.URL + this.Avatar.getStaticPath() | 679 | return WEBSERVER.URL + this.Avatar.getStaticPath() |
625 | } | 680 | } |
626 | 681 | ||
682 | getBannerUrl () { | ||
683 | if (!this.bannerId) return undefined | ||
684 | |||
685 | return WEBSERVER.URL + this.Banner.getStaticPath() | ||
686 | } | ||
687 | |||
627 | isOutdated () { | 688 | isOutdated () { |
628 | if (this.isOwned()) return false | 689 | if (this.isOwned()) return false |
629 | 690 | ||
diff --git a/server/models/application/application.ts b/server/models/application/application.ts index 909569de1..21f8b1cbc 100644 --- a/server/models/application/application.ts +++ b/server/models/application/application.ts | |||
@@ -32,6 +32,10 @@ export class ApplicationModel extends Model { | |||
32 | @Column | 32 | @Column |
33 | migrationVersion: number | 33 | migrationVersion: number |
34 | 34 | ||
35 | @AllowNull(true) | ||
36 | @Column | ||
37 | latestPeerTubeVersion: string | ||
38 | |||
35 | @HasOne(() => AccountModel, { | 39 | @HasOne(() => AccountModel, { |
36 | foreignKey: { | 40 | foreignKey: { |
37 | allowNull: true | 41 | allowNull: true |
diff --git a/server/models/avatar/avatar.ts b/server/models/avatar/avatar.ts deleted file mode 100644 index 0d246a144..000000000 --- a/server/models/avatar/avatar.ts +++ /dev/null | |||
@@ -1,81 +0,0 @@ | |||
1 | import { join } from 'path' | ||
2 | import { AfterDestroy, AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
3 | import { Avatar } from '../../../shared/models/avatars/avatar.model' | ||
4 | import { LAZY_STATIC_PATHS } from '../../initializers/constants' | ||
5 | import { logger } from '../../helpers/logger' | ||
6 | import { remove } from 'fs-extra' | ||
7 | import { CONFIG } from '../../initializers/config' | ||
8 | import { throwIfNotValid } from '../utils' | ||
9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
10 | import { MAvatarFormattable } from '@server/types/models' | ||
11 | |||
12 | @Table({ | ||
13 | tableName: 'avatar', | ||
14 | indexes: [ | ||
15 | { | ||
16 | fields: [ 'filename' ], | ||
17 | unique: true | ||
18 | } | ||
19 | ] | ||
20 | }) | ||
21 | export class AvatarModel extends Model { | ||
22 | |||
23 | @AllowNull(false) | ||
24 | @Column | ||
25 | filename: string | ||
26 | |||
27 | @AllowNull(true) | ||
28 | @Is('AvatarFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true)) | ||
29 | @Column | ||
30 | fileUrl: string | ||
31 | |||
32 | @AllowNull(false) | ||
33 | @Column | ||
34 | onDisk: boolean | ||
35 | |||
36 | @CreatedAt | ||
37 | createdAt: Date | ||
38 | |||
39 | @UpdatedAt | ||
40 | updatedAt: Date | ||
41 | |||
42 | @AfterDestroy | ||
43 | static removeFilesAndSendDelete (instance: AvatarModel) { | ||
44 | logger.info('Removing avatar file %s.', instance.filename) | ||
45 | |||
46 | // Don't block the transaction | ||
47 | instance.removeAvatar() | ||
48 | .catch(err => logger.error('Cannot remove avatar file %s.', instance.filename, err)) | ||
49 | } | ||
50 | |||
51 | static loadByName (filename: string) { | ||
52 | const query = { | ||
53 | where: { | ||
54 | filename | ||
55 | } | ||
56 | } | ||
57 | |||
58 | return AvatarModel.findOne(query) | ||
59 | } | ||
60 | |||
61 | toFormattedJSON (this: MAvatarFormattable): Avatar { | ||
62 | return { | ||
63 | path: this.getStaticPath(), | ||
64 | createdAt: this.createdAt, | ||
65 | updatedAt: this.updatedAt | ||
66 | } | ||
67 | } | ||
68 | |||
69 | getStaticPath () { | ||
70 | return join(LAZY_STATIC_PATHS.AVATARS, this.filename) | ||
71 | } | ||
72 | |||
73 | getPath () { | ||
74 | return join(CONFIG.STORAGE.AVATARS_DIR, this.filename) | ||
75 | } | ||
76 | |||
77 | removeAvatar () { | ||
78 | const avatarPath = join(CONFIG.STORAGE.AVATARS_DIR, this.filename) | ||
79 | return remove(avatarPath) | ||
80 | } | ||
81 | } | ||
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts index 6bc6cf27c..27e643aa7 100644 --- a/server/models/oauth/oauth-token.ts +++ b/server/models/oauth/oauth-token.ts | |||
@@ -12,9 +12,10 @@ import { | |||
12 | Table, | 12 | Table, |
13 | UpdatedAt | 13 | UpdatedAt |
14 | } from 'sequelize-typescript' | 14 | } from 'sequelize-typescript' |
15 | import { TokensCache } from '@server/lib/auth/tokens-cache' | ||
16 | import { MUserAccountId } from '@server/types/models' | ||
15 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 17 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
16 | import { logger } from '../../helpers/logger' | 18 | import { logger } from '../../helpers/logger' |
17 | import { clearCacheByToken } from '../../lib/oauth-model' | ||
18 | import { AccountModel } from '../account/account' | 19 | import { AccountModel } from '../account/account' |
19 | import { UserModel } from '../account/user' | 20 | import { UserModel } from '../account/user' |
20 | import { ActorModel } from '../activitypub/actor' | 21 | import { ActorModel } from '../activitypub/actor' |
@@ -26,9 +27,7 @@ export type OAuthTokenInfo = { | |||
26 | client: { | 27 | client: { |
27 | id: number | 28 | id: number |
28 | } | 29 | } |
29 | user: { | 30 | user: MUserAccountId |
30 | id: number | ||
31 | } | ||
32 | token: MOAuthTokenUser | 31 | token: MOAuthTokenUser |
33 | } | 32 | } |
34 | 33 | ||
@@ -133,7 +132,7 @@ export class OAuthTokenModel extends Model { | |||
133 | @AfterUpdate | 132 | @AfterUpdate |
134 | @AfterDestroy | 133 | @AfterDestroy |
135 | static removeTokenCache (token: OAuthTokenModel) { | 134 | static removeTokenCache (token: OAuthTokenModel) { |
136 | return clearCacheByToken(token.accessToken) | 135 | return TokensCache.Instance.clearCacheByToken(token.accessToken) |
137 | } | 136 | } |
138 | 137 | ||
139 | static loadByRefreshToken (refreshToken: string) { | 138 | static loadByRefreshToken (refreshToken: string) { |
@@ -206,6 +205,8 @@ export class OAuthTokenModel extends Model { | |||
206 | } | 205 | } |
207 | 206 | ||
208 | static deleteUserToken (userId: number, t?: Transaction) { | 207 | static deleteUserToken (userId: number, t?: Transaction) { |
208 | TokensCache.Instance.deleteUserToken(userId) | ||
209 | |||
209 | const query = { | 210 | const query = { |
210 | where: { | 211 | where: { |
211 | userId | 212 | userId |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 53293df37..53ebadeaf 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -32,6 +32,7 @@ import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' | |||
32 | import { ActorModel } from '../activitypub/actor' | 32 | import { ActorModel } from '../activitypub/actor' |
33 | import { ServerModel } from '../server/server' | 33 | import { ServerModel } from '../server/server' |
34 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' | 34 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' |
35 | import { ScheduleVideoUpdateModel } from '../video/schedule-video-update' | ||
35 | import { VideoModel } from '../video/video' | 36 | import { VideoModel } from '../video/video' |
36 | import { VideoChannelModel } from '../video/video-channel' | 37 | import { VideoChannelModel } from '../video/video-channel' |
37 | import { VideoFileModel } from '../video/video-file' | 38 | import { VideoFileModel } from '../video/video-file' |
@@ -374,7 +375,13 @@ export class VideoRedundancyModel extends Model { | |||
374 | ...this.buildVideoIdsForDuplication(peertubeActor) | 375 | ...this.buildVideoIdsForDuplication(peertubeActor) |
375 | }, | 376 | }, |
376 | include: [ | 377 | include: [ |
377 | VideoRedundancyModel.buildServerRedundancyInclude() | 378 | VideoRedundancyModel.buildServerRedundancyInclude(), |
379 | |||
380 | // Required by publishedAt sort | ||
381 | { | ||
382 | model: ScheduleVideoUpdateModel.unscoped(), | ||
383 | required: false | ||
384 | } | ||
378 | ] | 385 | ] |
379 | } | 386 | } |
380 | 387 | ||
diff --git a/server/models/utils.ts b/server/models/utils.ts index 5337ae75d..ec51c66bf 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -56,6 +56,14 @@ function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): Or | |||
56 | 56 | ||
57 | lastSort | 57 | lastSort |
58 | ] | 58 | ] |
59 | } else if (field === 'publishedAt') { | ||
60 | return [ | ||
61 | [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ], | ||
62 | |||
63 | [ Sequelize.col('VideoModel.publishedAt'), direction ], | ||
64 | |||
65 | lastSort | ||
66 | ] | ||
59 | } | 67 | } |
60 | 68 | ||
61 | let finalField: string | Col | 69 | let finalField: string | Col |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 178878c55..d2a055f5b 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -28,17 +28,16 @@ import { | |||
28 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | 28 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
29 | import { sendDeleteActor } from '../../lib/activitypub/send' | 29 | import { sendDeleteActor } from '../../lib/activitypub/send' |
30 | import { | 30 | import { |
31 | MChannelAccountDefault, | ||
32 | MChannelActor, | 31 | MChannelActor, |
33 | MChannelActorAccountDefaultVideos, | ||
34 | MChannelAP, | 32 | MChannelAP, |
33 | MChannelBannerAccountDefault, | ||
35 | MChannelFormattable, | 34 | MChannelFormattable, |
36 | MChannelSummaryFormattable | 35 | MChannelSummaryFormattable |
37 | } from '../../types/models/video' | 36 | } from '../../types/models/video' |
38 | import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' | 37 | import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' |
38 | import { ActorImageModel } from '../account/actor-image' | ||
39 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' | 39 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' |
40 | import { ActorFollowModel } from '../activitypub/actor-follow' | 40 | import { ActorFollowModel } from '../activitypub/actor-follow' |
41 | import { AvatarModel } from '../avatar/avatar' | ||
42 | import { ServerModel } from '../server/server' | 41 | import { ServerModel } from '../server/server' |
43 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' | 42 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' |
44 | import { VideoModel } from './video' | 43 | import { VideoModel } from './video' |
@@ -49,6 +48,7 @@ export enum ScopeNames { | |||
49 | SUMMARY = 'SUMMARY', | 48 | SUMMARY = 'SUMMARY', |
50 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 49 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
51 | WITH_ACTOR = 'WITH_ACTOR', | 50 | WITH_ACTOR = 'WITH_ACTOR', |
51 | WITH_ACTOR_BANNER = 'WITH_ACTOR_BANNER', | ||
52 | WITH_VIDEOS = 'WITH_VIDEOS', | 52 | WITH_VIDEOS = 'WITH_VIDEOS', |
53 | WITH_STATS = 'WITH_STATS' | 53 | WITH_STATS = 'WITH_STATS' |
54 | } | 54 | } |
@@ -99,7 +99,14 @@ export type SummaryOptions = { | |||
99 | } | 99 | } |
100 | } | 100 | } |
101 | ] | 101 | ] |
102 | } | 102 | }, |
103 | include: [ | ||
104 | { | ||
105 | model: ActorImageModel, | ||
106 | as: 'Banner', | ||
107 | required: false | ||
108 | } | ||
109 | ] | ||
103 | }, | 110 | }, |
104 | { | 111 | { |
105 | model: AccountModel, | 112 | model: AccountModel, |
@@ -130,7 +137,8 @@ export type SummaryOptions = { | |||
130 | required: false | 137 | required: false |
131 | }, | 138 | }, |
132 | { | 139 | { |
133 | model: AvatarModel.unscoped(), | 140 | model: ActorImageModel.unscoped(), |
141 | as: 'Avatar', | ||
134 | required: false | 142 | required: false |
135 | } | 143 | } |
136 | ] | 144 | ] |
@@ -167,6 +175,20 @@ export type SummaryOptions = { | |||
167 | ActorModel | 175 | ActorModel |
168 | ] | 176 | ] |
169 | }, | 177 | }, |
178 | [ScopeNames.WITH_ACTOR_BANNER]: { | ||
179 | include: [ | ||
180 | { | ||
181 | model: ActorModel, | ||
182 | include: [ | ||
183 | { | ||
184 | model: ActorImageModel, | ||
185 | required: false, | ||
186 | as: 'Banner' | ||
187 | } | ||
188 | ] | ||
189 | } | ||
190 | ] | ||
191 | }, | ||
170 | [ScopeNames.WITH_VIDEOS]: { | 192 | [ScopeNames.WITH_VIDEOS]: { |
171 | include: [ | 193 | include: [ |
172 | VideoModel | 194 | VideoModel |
@@ -441,7 +463,7 @@ export class VideoChannelModel extends Model { | |||
441 | where | 463 | where |
442 | } | 464 | } |
443 | 465 | ||
444 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ] | 466 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ] |
445 | 467 | ||
446 | if (options.withStats === true) { | 468 | if (options.withStats === true) { |
447 | scopes.push({ | 469 | scopes.push({ |
@@ -457,32 +479,13 @@ export class VideoChannelModel extends Model { | |||
457 | }) | 479 | }) |
458 | } | 480 | } |
459 | 481 | ||
460 | static loadByIdAndPopulateAccount (id: number): Promise<MChannelAccountDefault> { | 482 | static loadAndPopulateAccount (id: number): Promise<MChannelBannerAccountDefault> { |
461 | return VideoChannelModel.unscoped() | 483 | return VideoChannelModel.unscoped() |
462 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | 484 | .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ]) |
463 | .findByPk(id) | 485 | .findByPk(id) |
464 | } | 486 | } |
465 | 487 | ||
466 | static loadByIdAndAccount (id: number, accountId: number): Promise<MChannelAccountDefault> { | 488 | static loadByUrlAndPopulateAccount (url: string): Promise<MChannelBannerAccountDefault> { |
467 | const query = { | ||
468 | where: { | ||
469 | id, | ||
470 | accountId | ||
471 | } | ||
472 | } | ||
473 | |||
474 | return VideoChannelModel.unscoped() | ||
475 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | ||
476 | .findOne(query) | ||
477 | } | ||
478 | |||
479 | static loadAndPopulateAccount (id: number): Promise<MChannelAccountDefault> { | ||
480 | return VideoChannelModel.unscoped() | ||
481 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | ||
482 | .findByPk(id) | ||
483 | } | ||
484 | |||
485 | static loadByUrlAndPopulateAccount (url: string): Promise<MChannelAccountDefault> { | ||
486 | const query = { | 489 | const query = { |
487 | include: [ | 490 | include: [ |
488 | { | 491 | { |
@@ -490,7 +493,14 @@ export class VideoChannelModel extends Model { | |||
490 | required: true, | 493 | required: true, |
491 | where: { | 494 | where: { |
492 | url | 495 | url |
493 | } | 496 | }, |
497 | include: [ | ||
498 | { | ||
499 | model: ActorImageModel, | ||
500 | required: false, | ||
501 | as: 'Banner' | ||
502 | } | ||
503 | ] | ||
494 | } | 504 | } |
495 | ] | 505 | ] |
496 | } | 506 | } |
@@ -508,7 +518,7 @@ export class VideoChannelModel extends Model { | |||
508 | return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host) | 518 | return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host) |
509 | } | 519 | } |
510 | 520 | ||
511 | static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelAccountDefault> { | 521 | static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelBannerAccountDefault> { |
512 | const query = { | 522 | const query = { |
513 | include: [ | 523 | include: [ |
514 | { | 524 | { |
@@ -517,17 +527,24 @@ export class VideoChannelModel extends Model { | |||
517 | where: { | 527 | where: { |
518 | preferredUsername: name, | 528 | preferredUsername: name, |
519 | serverId: null | 529 | serverId: null |
520 | } | 530 | }, |
531 | include: [ | ||
532 | { | ||
533 | model: ActorImageModel, | ||
534 | required: false, | ||
535 | as: 'Banner' | ||
536 | } | ||
537 | ] | ||
521 | } | 538 | } |
522 | ] | 539 | ] |
523 | } | 540 | } |
524 | 541 | ||
525 | return VideoChannelModel.unscoped() | 542 | return VideoChannelModel.unscoped() |
526 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | 543 | .scope([ ScopeNames.WITH_ACCOUNT ]) |
527 | .findOne(query) | 544 | .findOne(query) |
528 | } | 545 | } |
529 | 546 | ||
530 | static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelAccountDefault> { | 547 | static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelBannerAccountDefault> { |
531 | const query = { | 548 | const query = { |
532 | include: [ | 549 | include: [ |
533 | { | 550 | { |
@@ -541,6 +558,11 @@ export class VideoChannelModel extends Model { | |||
541 | model: ServerModel, | 558 | model: ServerModel, |
542 | required: true, | 559 | required: true, |
543 | where: { host } | 560 | where: { host } |
561 | }, | ||
562 | { | ||
563 | model: ActorImageModel, | ||
564 | required: false, | ||
565 | as: 'Banner' | ||
544 | } | 566 | } |
545 | ] | 567 | ] |
546 | } | 568 | } |
@@ -548,22 +570,10 @@ export class VideoChannelModel extends Model { | |||
548 | } | 570 | } |
549 | 571 | ||
550 | return VideoChannelModel.unscoped() | 572 | return VideoChannelModel.unscoped() |
551 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | 573 | .scope([ ScopeNames.WITH_ACCOUNT ]) |
552 | .findOne(query) | 574 | .findOne(query) |
553 | } | 575 | } |
554 | 576 | ||
555 | static loadAndPopulateAccountAndVideos (id: number): Promise<MChannelActorAccountDefaultVideos> { | ||
556 | const options = { | ||
557 | include: [ | ||
558 | VideoModel | ||
559 | ] | ||
560 | } | ||
561 | |||
562 | return VideoChannelModel.unscoped() | ||
563 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ]) | ||
564 | .findByPk(id, options) | ||
565 | } | ||
566 | |||
567 | toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary { | 577 | toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary { |
568 | const actor = this.Actor.toFormattedSummaryJSON() | 578 | const actor = this.Actor.toFormattedSummaryJSON() |
569 | 579 | ||
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts index 96df0a7f8..4d95ddee2 100644 --- a/server/models/video/video-query-builder.ts +++ b/server/models/video/video-query-builder.ts | |||
@@ -490,12 +490,13 @@ function wrapForAPIResults (baseQuery: string, replacements: any, options: Build | |||
490 | 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"', | 490 | 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"', |
491 | 491 | ||
492 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"', | 492 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"', |
493 | 'LEFT OUTER JOIN "avatar" AS "VideoChannel->Actor->Avatar" ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"', | 493 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' + |
494 | 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"', | ||
494 | 495 | ||
495 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' + | 496 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' + |
496 | 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"', | 497 | 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"', |
497 | 498 | ||
498 | 'LEFT OUTER JOIN "avatar" AS "VideoChannel->Account->Actor->Avatar" ' + | 499 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' + |
499 | 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"', | 500 | 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"', |
500 | 501 | ||
501 | 'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"' | 502 | 'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"' |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 3c4f3d3df..e9afb2c18 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -24,7 +24,6 @@ import { | |||
24 | Table, | 24 | Table, |
25 | UpdatedAt | 25 | UpdatedAt |
26 | } from 'sequelize-typescript' | 26 | } from 'sequelize-typescript' |
27 | import { v4 as uuidv4 } from 'uuid' | ||
28 | import { buildNSFWFilter } from '@server/helpers/express-utils' | 27 | import { buildNSFWFilter } from '@server/helpers/express-utils' |
29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' | 28 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' |
30 | import { LiveManager } from '@server/lib/live-manager' | 29 | import { LiveManager } from '@server/lib/live-manager' |
@@ -100,10 +99,10 @@ import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models | |||
100 | import { VideoAbuseModel } from '../abuse/video-abuse' | 99 | import { VideoAbuseModel } from '../abuse/video-abuse' |
101 | import { AccountModel } from '../account/account' | 100 | import { AccountModel } from '../account/account' |
102 | import { AccountVideoRateModel } from '../account/account-video-rate' | 101 | import { AccountVideoRateModel } from '../account/account-video-rate' |
102 | import { ActorImageModel } from '../account/actor-image' | ||
103 | import { UserModel } from '../account/user' | 103 | import { UserModel } from '../account/user' |
104 | import { UserVideoHistoryModel } from '../account/user-video-history' | 104 | import { UserVideoHistoryModel } from '../account/user-video-history' |
105 | import { ActorModel } from '../activitypub/actor' | 105 | import { ActorModel } from '../activitypub/actor' |
106 | import { AvatarModel } from '../avatar/avatar' | ||
107 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 106 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
108 | import { ServerModel } from '../server/server' | 107 | import { ServerModel } from '../server/server' |
109 | import { TrackerModel } from '../server/tracker' | 108 | import { TrackerModel } from '../server/tracker' |
@@ -286,7 +285,8 @@ export type AvailableForListIDsOptions = { | |||
286 | required: false | 285 | required: false |
287 | }, | 286 | }, |
288 | { | 287 | { |
289 | model: AvatarModel.unscoped(), | 288 | model: ActorImageModel.unscoped(), |
289 | as: 'Avatar', | ||
290 | required: false | 290 | required: false |
291 | } | 291 | } |
292 | ] | 292 | ] |
@@ -308,7 +308,8 @@ export type AvailableForListIDsOptions = { | |||
308 | required: false | 308 | required: false |
309 | }, | 309 | }, |
310 | { | 310 | { |
311 | model: AvatarModel.unscoped(), | 311 | model: ActorImageModel.unscoped(), |
312 | as: 'Avatar', | ||
312 | required: false | 313 | required: false |
313 | } | 314 | } |
314 | ] | 315 | ] |
@@ -1703,7 +1704,7 @@ export class VideoModel extends Model { | |||
1703 | 1704 | ||
1704 | function buildActor (rowActor: any) { | 1705 | function buildActor (rowActor: any) { |
1705 | const avatarModel = rowActor.Avatar.id !== null | 1706 | const avatarModel = rowActor.Avatar.id !== null |
1706 | ? new AvatarModel(pick(rowActor.Avatar, avatarKeys), buildOpts) | 1707 | ? new ActorImageModel(pick(rowActor.Avatar, avatarKeys), buildOpts) |
1707 | : null | 1708 | : null |
1708 | 1709 | ||
1709 | const serverModel = rowActor.Server.id !== null | 1710 | const serverModel = rowActor.Server.id !== null |
@@ -1869,20 +1870,12 @@ export class VideoModel extends Model { | |||
1869 | this.Thumbnails.push(savedThumbnail) | 1870 | this.Thumbnails.push(savedThumbnail) |
1870 | } | 1871 | } |
1871 | 1872 | ||
1872 | generateThumbnailName () { | ||
1873 | return uuidv4() + '.jpg' | ||
1874 | } | ||
1875 | |||
1876 | getMiniature () { | 1873 | getMiniature () { |
1877 | if (Array.isArray(this.Thumbnails) === false) return undefined | 1874 | if (Array.isArray(this.Thumbnails) === false) return undefined |
1878 | 1875 | ||
1879 | return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE) | 1876 | return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE) |
1880 | } | 1877 | } |
1881 | 1878 | ||
1882 | generatePreviewName () { | ||
1883 | return uuidv4() + '.jpg' | ||
1884 | } | ||
1885 | |||
1886 | hasPreview () { | 1879 | hasPreview () { |
1887 | return !!this.getPreview() | 1880 | return !!this.getPreview() |
1888 | } | 1881 | } |
diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts index 8bde54a40..364b53e0f 100644 --- a/server/tests/api/activitypub/security.ts +++ b/server/tests/api/activitypub/security.ts | |||
@@ -8,6 +8,8 @@ import { | |||
8 | cleanupTests, | 8 | cleanupTests, |
9 | closeAllSequelize, | 9 | closeAllSequelize, |
10 | flushAndRunMultipleServers, | 10 | flushAndRunMultipleServers, |
11 | killallServers, | ||
12 | reRunServer, | ||
11 | ServerInfo, | 13 | ServerInfo, |
12 | setActorField, | 14 | setActorField, |
13 | wait | 15 | wait |
@@ -20,21 +22,32 @@ import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activi | |||
20 | const expect = chai.expect | 22 | const expect = chai.expect |
21 | 23 | ||
22 | function setKeysOfServer (onServer: ServerInfo, ofServer: ServerInfo, publicKey: string, privateKey: string) { | 24 | function setKeysOfServer (onServer: ServerInfo, ofServer: ServerInfo, publicKey: string, privateKey: string) { |
25 | const url = 'http://localhost:' + ofServer.port + '/accounts/peertube' | ||
26 | |||
23 | return Promise.all([ | 27 | return Promise.all([ |
24 | setActorField(onServer.internalServerNumber, 'http://localhost:' + ofServer.port + '/accounts/peertube', 'publicKey', publicKey), | 28 | setActorField(onServer.internalServerNumber, url, 'publicKey', publicKey), |
25 | setActorField(onServer.internalServerNumber, 'http://localhost:' + ofServer.port + '/accounts/peertube', 'privateKey', privateKey) | 29 | setActorField(onServer.internalServerNumber, url, 'privateKey', privateKey) |
26 | ]) | 30 | ]) |
27 | } | 31 | } |
28 | 32 | ||
29 | function getAnnounceWithoutContext (server2: ServerInfo) { | 33 | function setUpdatedAtOfServer (onServer: ServerInfo, ofServer: ServerInfo, updatedAt: string) { |
34 | const url = 'http://localhost:' + ofServer.port + '/accounts/peertube' | ||
35 | |||
36 | return Promise.all([ | ||
37 | setActorField(onServer.internalServerNumber, url, 'createdAt', updatedAt), | ||
38 | setActorField(onServer.internalServerNumber, url, 'updatedAt', updatedAt) | ||
39 | ]) | ||
40 | } | ||
41 | |||
42 | function getAnnounceWithoutContext (server: ServerInfo) { | ||
30 | const json = require('./json/peertube/announce-without-context.json') | 43 | const json = require('./json/peertube/announce-without-context.json') |
31 | const result: typeof json = {} | 44 | const result: typeof json = {} |
32 | 45 | ||
33 | for (const key of Object.keys(json)) { | 46 | for (const key of Object.keys(json)) { |
34 | if (Array.isArray(json[key])) { | 47 | if (Array.isArray(json[key])) { |
35 | result[key] = json[key].map(v => v.replace(':9002', `:${server2.port}`)) | 48 | result[key] = json[key].map(v => v.replace(':9002', `:${server.port}`)) |
36 | } else { | 49 | } else { |
37 | result[key] = json[key].replace(':9002', `:${server2.port}`) | 50 | result[key] = json[key].replace(':9002', `:${server.port}`) |
38 | } | 51 | } |
39 | } | 52 | } |
40 | 53 | ||
@@ -64,7 +77,8 @@ describe('Test ActivityPub security', function () { | |||
64 | 77 | ||
65 | url = servers[0].url + '/inbox' | 78 | url = servers[0].url + '/inbox' |
66 | 79 | ||
67 | await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey) | 80 | await setKeysOfServer(servers[0], servers[1], keys.publicKey, null) |
81 | await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey) | ||
68 | 82 | ||
69 | const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' } | 83 | const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' } |
70 | const by = { url: 'http://localhost:' + servers[1].port + '/accounts/peertube', privateKey: keys.privateKey } | 84 | const by = { url: 'http://localhost:' + servers[1].port + '/accounts/peertube', privateKey: keys.privateKey } |
@@ -79,9 +93,12 @@ describe('Test ActivityPub security', function () { | |||
79 | Digest: buildDigest({ hello: 'coucou' }) | 93 | Digest: buildDigest({ hello: 'coucou' }) |
80 | } | 94 | } |
81 | 95 | ||
82 | const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | 96 | try { |
83 | 97 | await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | |
84 | expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | 98 | expect(true, 'Did not throw').to.be.false |
99 | } catch (err) { | ||
100 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
101 | } | ||
85 | }) | 102 | }) |
86 | 103 | ||
87 | it('Should fail with an invalid date', async function () { | 104 | it('Should fail with an invalid date', async function () { |
@@ -89,9 +106,12 @@ describe('Test ActivityPub security', function () { | |||
89 | const headers = buildGlobalHeaders(body) | 106 | const headers = buildGlobalHeaders(body) |
90 | headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT' | 107 | headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT' |
91 | 108 | ||
92 | const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | 109 | try { |
93 | 110 | await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | |
94 | expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | 111 | expect(true, 'Did not throw').to.be.false |
112 | } catch (err) { | ||
113 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
114 | } | ||
95 | }) | 115 | }) |
96 | 116 | ||
97 | it('Should fail with bad keys', async function () { | 117 | it('Should fail with bad keys', async function () { |
@@ -101,9 +121,12 @@ describe('Test ActivityPub security', function () { | |||
101 | const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) | 121 | const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) |
102 | const headers = buildGlobalHeaders(body) | 122 | const headers = buildGlobalHeaders(body) |
103 | 123 | ||
104 | const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | 124 | try { |
105 | 125 | await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | |
106 | expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | 126 | expect(true, 'Did not throw').to.be.false |
127 | } catch (err) { | ||
128 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
129 | } | ||
107 | }) | 130 | }) |
108 | 131 | ||
109 | it('Should reject requests without appropriate signed headers', async function () { | 132 | it('Should reject requests without appropriate signed headers', async function () { |
@@ -123,8 +146,12 @@ describe('Test ActivityPub security', function () { | |||
123 | for (const badHeaders of badHeadersMatrix) { | 146 | for (const badHeaders of badHeadersMatrix) { |
124 | signatureOptions.headers = badHeaders | 147 | signatureOptions.headers = badHeaders |
125 | 148 | ||
126 | const { response } = await makePOSTAPRequest(url, body, signatureOptions, headers) | 149 | try { |
127 | expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | 150 | await makePOSTAPRequest(url, body, signatureOptions, headers) |
151 | expect(true, 'Did not throw').to.be.false | ||
152 | } catch (err) { | ||
153 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
154 | } | ||
128 | } | 155 | } |
129 | }) | 156 | }) |
130 | 157 | ||
@@ -132,27 +159,32 @@ describe('Test ActivityPub security', function () { | |||
132 | const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) | 159 | const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) |
133 | const headers = buildGlobalHeaders(body) | 160 | const headers = buildGlobalHeaders(body) |
134 | 161 | ||
135 | const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | 162 | const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) |
136 | 163 | expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204) | |
137 | expect(response.statusCode).to.equal(HttpStatusCode.NO_CONTENT_204) | ||
138 | }) | 164 | }) |
139 | 165 | ||
140 | it('Should refresh the actor keys', async function () { | 166 | it('Should refresh the actor keys', async function () { |
141 | this.timeout(20000) | 167 | this.timeout(20000) |
142 | 168 | ||
143 | // Wait refresh invalidation | ||
144 | await wait(10000) | ||
145 | |||
146 | // Update keys of server 2 to invalid keys | 169 | // Update keys of server 2 to invalid keys |
147 | // Server 1 should refresh the actor and fail | 170 | // Server 1 should refresh the actor and fail |
148 | await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey) | 171 | await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey) |
172 | await setUpdatedAtOfServer(servers[0], servers[1], '2015-07-17 22:00:00+00') | ||
173 | |||
174 | // Invalid peertube actor cache | ||
175 | killallServers([ servers[1] ]) | ||
176 | await reRunServer(servers[1]) | ||
149 | 177 | ||
150 | const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) | 178 | const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) |
151 | const headers = buildGlobalHeaders(body) | 179 | const headers = buildGlobalHeaders(body) |
152 | 180 | ||
153 | const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | 181 | try { |
154 | 182 | await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | |
155 | expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | 183 | expect(true, 'Did not throw').to.be.false |
184 | } catch (err) { | ||
185 | console.error(err) | ||
186 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
187 | } | ||
156 | }) | 188 | }) |
157 | }) | 189 | }) |
158 | 190 | ||
@@ -183,9 +215,12 @@ describe('Test ActivityPub security', function () { | |||
183 | 215 | ||
184 | const headers = buildGlobalHeaders(signedBody) | 216 | const headers = buildGlobalHeaders(signedBody) |
185 | 217 | ||
186 | const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) | 218 | try { |
187 | 219 | await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) | |
188 | expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | 220 | expect(true, 'Did not throw').to.be.false |
221 | } catch (err) { | ||
222 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
223 | } | ||
189 | }) | 224 | }) |
190 | 225 | ||
191 | it('Should fail with an altered body', async function () { | 226 | it('Should fail with an altered body', async function () { |
@@ -204,9 +239,12 @@ describe('Test ActivityPub security', function () { | |||
204 | 239 | ||
205 | const headers = buildGlobalHeaders(signedBody) | 240 | const headers = buildGlobalHeaders(signedBody) |
206 | 241 | ||
207 | const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) | 242 | try { |
208 | 243 | await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) | |
209 | expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | 244 | expect(true, 'Did not throw').to.be.false |
245 | } catch (err) { | ||
246 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
247 | } | ||
210 | }) | 248 | }) |
211 | 249 | ||
212 | it('Should succeed with a valid signature', async function () { | 250 | it('Should succeed with a valid signature', async function () { |
@@ -220,9 +258,8 @@ describe('Test ActivityPub security', function () { | |||
220 | 258 | ||
221 | const headers = buildGlobalHeaders(signedBody) | 259 | const headers = buildGlobalHeaders(signedBody) |
222 | 260 | ||
223 | const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) | 261 | const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) |
224 | 262 | expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204) | |
225 | expect(response.statusCode).to.equal(HttpStatusCode.NO_CONTENT_204) | ||
226 | }) | 263 | }) |
227 | 264 | ||
228 | it('Should refresh the actor keys', async function () { | 265 | it('Should refresh the actor keys', async function () { |
@@ -243,9 +280,12 @@ describe('Test ActivityPub security', function () { | |||
243 | 280 | ||
244 | const headers = buildGlobalHeaders(signedBody) | 281 | const headers = buildGlobalHeaders(signedBody) |
245 | 282 | ||
246 | const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) | 283 | try { |
247 | 284 | await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) | |
248 | expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | 285 | expect(true, 'Did not throw').to.be.false |
286 | } catch (err) { | ||
287 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
288 | } | ||
249 | }) | 289 | }) |
250 | }) | 290 | }) |
251 | 291 | ||
diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts index 05a78b0ad..26d4423f9 100644 --- a/server/tests/api/check-params/user-notifications.ts +++ b/server/tests/api/check-params/user-notifications.ts | |||
@@ -176,7 +176,9 @@ describe('Test user notifications API validators', function () { | |||
176 | newInstanceFollower: UserNotificationSettingValue.WEB, | 176 | newInstanceFollower: UserNotificationSettingValue.WEB, |
177 | autoInstanceFollowing: UserNotificationSettingValue.WEB, | 177 | autoInstanceFollowing: UserNotificationSettingValue.WEB, |
178 | abuseNewMessage: UserNotificationSettingValue.WEB, | 178 | abuseNewMessage: UserNotificationSettingValue.WEB, |
179 | abuseStateChange: UserNotificationSettingValue.WEB | 179 | abuseStateChange: UserNotificationSettingValue.WEB, |
180 | newPeerTubeVersion: UserNotificationSettingValue.WEB, | ||
181 | newPluginVersion: UserNotificationSettingValue.WEB | ||
180 | } | 182 | } |
181 | 183 | ||
182 | it('Should fail with missing fields', async function () { | 184 | it('Should fail with missing fields', async function () { |
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index 0a13f5b67..2b03fde2d 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -241,7 +241,7 @@ describe('Test users API validators', function () { | |||
241 | }) | 241 | }) |
242 | 242 | ||
243 | it('Should succeed with no password on a server with smtp enabled', async function () { | 243 | it('Should succeed with no password on a server with smtp enabled', async function () { |
244 | this.timeout(10000) | 244 | this.timeout(20000) |
245 | 245 | ||
246 | killallServers([ server ]) | 246 | killallServers([ server ]) |
247 | 247 | ||
diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts index 0dd436426..bc2e6192e 100644 --- a/server/tests/api/check-params/video-channels.ts +++ b/server/tests/api/check-params/video-channels.ts | |||
@@ -234,7 +234,8 @@ describe('Test video channels API validator', function () { | |||
234 | }) | 234 | }) |
235 | }) | 235 | }) |
236 | 236 | ||
237 | describe('When updating video channel avatar', function () { | 237 | describe('When updating video channel avatar/banner', function () { |
238 | const types = [ 'avatar', 'banner' ] | ||
238 | let path: string | 239 | let path: string |
239 | 240 | ||
240 | before(async function () { | 241 | before(async function () { |
@@ -242,48 +243,57 @@ describe('Test video channels API validator', function () { | |||
242 | }) | 243 | }) |
243 | 244 | ||
244 | it('Should fail with an incorrect input file', async function () { | 245 | it('Should fail with an incorrect input file', async function () { |
245 | const fields = {} | 246 | for (const type of types) { |
246 | const attaches = { | 247 | const fields = {} |
247 | avatarfile: join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') | 248 | const attaches = { |
249 | [type + 'file']: join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') | ||
250 | } | ||
251 | |||
252 | await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches }) | ||
248 | } | 253 | } |
249 | await makeUploadRequest({ url: server.url, path: path + '/avatar/pick', token: server.accessToken, fields, attaches }) | ||
250 | }) | 254 | }) |
251 | 255 | ||
252 | it('Should fail with a big file', async function () { | 256 | it('Should fail with a big file', async function () { |
253 | const fields = {} | 257 | for (const type of types) { |
254 | const attaches = { | 258 | const fields = {} |
255 | avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') | 259 | const attaches = { |
260 | [type + 'file']: join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') | ||
261 | } | ||
262 | await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches }) | ||
256 | } | 263 | } |
257 | await makeUploadRequest({ url: server.url, path: path + '/avatar/pick', token: server.accessToken, fields, attaches }) | ||
258 | }) | 264 | }) |
259 | 265 | ||
260 | it('Should fail with an unauthenticated user', async function () { | 266 | it('Should fail with an unauthenticated user', async function () { |
261 | const fields = {} | 267 | for (const type of types) { |
262 | const attaches = { | 268 | const fields = {} |
263 | avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar.png') | 269 | const attaches = { |
270 | [type + 'file']: join(__dirname, '..', '..', 'fixtures', 'avatar.png') | ||
271 | } | ||
272 | await makeUploadRequest({ | ||
273 | url: server.url, | ||
274 | path: `${path}/${type}/pick`, | ||
275 | fields, | ||
276 | attaches, | ||
277 | statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 | ||
278 | }) | ||
264 | } | 279 | } |
265 | await makeUploadRequest({ | ||
266 | url: server.url, | ||
267 | path: path + '/avatar/pick', | ||
268 | fields, | ||
269 | attaches, | ||
270 | statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 | ||
271 | }) | ||
272 | }) | 280 | }) |
273 | 281 | ||
274 | it('Should succeed with the correct params', async function () { | 282 | it('Should succeed with the correct params', async function () { |
275 | const fields = {} | 283 | for (const type of types) { |
276 | const attaches = { | 284 | const fields = {} |
277 | avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar.png') | 285 | const attaches = { |
286 | [type + 'file']: join(__dirname, '..', '..', 'fixtures', 'avatar.png') | ||
287 | } | ||
288 | await makeUploadRequest({ | ||
289 | url: server.url, | ||
290 | path: `${path}/${type}/pick`, | ||
291 | token: server.accessToken, | ||
292 | fields, | ||
293 | attaches, | ||
294 | statusCodeExpected: HttpStatusCode.OK_200 | ||
295 | }) | ||
278 | } | 296 | } |
279 | await makeUploadRequest({ | ||
280 | url: server.url, | ||
281 | path: path + '/avatar/pick', | ||
282 | token: server.accessToken, | ||
283 | fields, | ||
284 | attaches, | ||
285 | statusCodeExpected: HttpStatusCode.OK_200 | ||
286 | }) | ||
287 | }) | 297 | }) |
288 | }) | 298 | }) |
289 | 299 | ||
diff --git a/server/tests/api/notifications/admin-notifications.ts b/server/tests/api/notifications/admin-notifications.ts new file mode 100644 index 000000000..e07327d74 --- /dev/null +++ b/server/tests/api/notifications/admin-notifications.ts | |||
@@ -0,0 +1,165 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import { expect } from 'chai' | ||
5 | import { MockJoinPeerTubeVersions } from '@shared/extra-utils/mock-servers/joinpeertube-versions' | ||
6 | import { cleanupTests, installPlugin, setPluginLatestVersion, setPluginVersion, wait } from '../../../../shared/extra-utils' | ||
7 | import { ServerInfo } from '../../../../shared/extra-utils/index' | ||
8 | import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' | ||
9 | import { | ||
10 | CheckerBaseParams, | ||
11 | checkNewPeerTubeVersion, | ||
12 | checkNewPluginVersion, | ||
13 | prepareNotificationsTest | ||
14 | } from '../../../../shared/extra-utils/users/user-notifications' | ||
15 | import { UserNotification, UserNotificationType } from '../../../../shared/models/users' | ||
16 | import { PluginType } from '@shared/models' | ||
17 | |||
18 | describe('Test admin notifications', function () { | ||
19 | let server: ServerInfo | ||
20 | let userNotifications: UserNotification[] = [] | ||
21 | let adminNotifications: UserNotification[] = [] | ||
22 | let emails: object[] = [] | ||
23 | let baseParams: CheckerBaseParams | ||
24 | let joinPeerTubeServer: MockJoinPeerTubeVersions | ||
25 | |||
26 | before(async function () { | ||
27 | this.timeout(120000) | ||
28 | |||
29 | const config = { | ||
30 | peertube: { | ||
31 | check_latest_version: { | ||
32 | enabled: true, | ||
33 | url: 'http://localhost:42102/versions.json' | ||
34 | } | ||
35 | }, | ||
36 | plugins: { | ||
37 | index: { | ||
38 | enabled: true, | ||
39 | check_latest_versions_interval: '5 seconds' | ||
40 | } | ||
41 | } | ||
42 | } | ||
43 | |||
44 | const res = await prepareNotificationsTest(1, config) | ||
45 | emails = res.emails | ||
46 | server = res.servers[0] | ||
47 | |||
48 | userNotifications = res.userNotifications | ||
49 | adminNotifications = res.adminNotifications | ||
50 | |||
51 | baseParams = { | ||
52 | server: server, | ||
53 | emails, | ||
54 | socketNotifications: adminNotifications, | ||
55 | token: server.accessToken | ||
56 | } | ||
57 | |||
58 | await installPlugin({ | ||
59 | url: server.url, | ||
60 | accessToken: server.accessToken, | ||
61 | npmName: 'peertube-plugin-hello-world' | ||
62 | }) | ||
63 | |||
64 | await installPlugin({ | ||
65 | url: server.url, | ||
66 | accessToken: server.accessToken, | ||
67 | npmName: 'peertube-theme-background-red' | ||
68 | }) | ||
69 | |||
70 | joinPeerTubeServer = new MockJoinPeerTubeVersions() | ||
71 | await joinPeerTubeServer.initialize() | ||
72 | }) | ||
73 | |||
74 | describe('Latest PeerTube version notification', function () { | ||
75 | |||
76 | it('Should not send a notification to admins if there is not a new version', async function () { | ||
77 | this.timeout(30000) | ||
78 | |||
79 | joinPeerTubeServer.setLatestVersion('1.4.2') | ||
80 | |||
81 | await wait(3000) | ||
82 | await checkNewPeerTubeVersion(baseParams, '1.4.2', 'absence') | ||
83 | }) | ||
84 | |||
85 | it('Should send a notification to admins on new plugin version', async function () { | ||
86 | this.timeout(30000) | ||
87 | |||
88 | joinPeerTubeServer.setLatestVersion('15.4.2') | ||
89 | |||
90 | await wait(3000) | ||
91 | await checkNewPeerTubeVersion(baseParams, '15.4.2', 'presence') | ||
92 | }) | ||
93 | |||
94 | it('Should not send the same notification to admins', async function () { | ||
95 | this.timeout(30000) | ||
96 | |||
97 | await wait(3000) | ||
98 | expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(1) | ||
99 | }) | ||
100 | |||
101 | it('Should not have sent a notification to users', async function () { | ||
102 | this.timeout(30000) | ||
103 | |||
104 | expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(0) | ||
105 | }) | ||
106 | |||
107 | it('Should send a new notification after a new release', async function () { | ||
108 | this.timeout(30000) | ||
109 | |||
110 | joinPeerTubeServer.setLatestVersion('15.4.3') | ||
111 | |||
112 | await wait(3000) | ||
113 | await checkNewPeerTubeVersion(baseParams, '15.4.3', 'presence') | ||
114 | expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2) | ||
115 | }) | ||
116 | }) | ||
117 | |||
118 | describe('Latest plugin version notification', function () { | ||
119 | |||
120 | it('Should not send a notification to admins if there is no new plugin version', async function () { | ||
121 | this.timeout(30000) | ||
122 | |||
123 | await wait(6000) | ||
124 | await checkNewPluginVersion(baseParams, PluginType.PLUGIN, 'hello-world', 'absence') | ||
125 | }) | ||
126 | |||
127 | it('Should send a notification to admins on new plugin version', async function () { | ||
128 | this.timeout(30000) | ||
129 | |||
130 | await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1') | ||
131 | await setPluginLatestVersion(server.internalServerNumber, 'hello-world', '0.0.1') | ||
132 | await wait(6000) | ||
133 | |||
134 | await checkNewPluginVersion(baseParams, PluginType.PLUGIN, 'hello-world', 'presence') | ||
135 | }) | ||
136 | |||
137 | it('Should not send the same notification to admins', async function () { | ||
138 | this.timeout(30000) | ||
139 | |||
140 | await wait(6000) | ||
141 | |||
142 | expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(1) | ||
143 | }) | ||
144 | |||
145 | it('Should not have sent a notification to users', async function () { | ||
146 | expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(0) | ||
147 | }) | ||
148 | |||
149 | it('Should send a new notification after a new plugin release', async function () { | ||
150 | this.timeout(30000) | ||
151 | |||
152 | await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1') | ||
153 | await setPluginLatestVersion(server.internalServerNumber, 'hello-world', '0.0.1') | ||
154 | await wait(6000) | ||
155 | |||
156 | expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2) | ||
157 | }) | ||
158 | }) | ||
159 | |||
160 | after(async function () { | ||
161 | MockSmtpServer.Instance.kill() | ||
162 | |||
163 | await cleanupTests([ server ]) | ||
164 | }) | ||
165 | }) | ||
diff --git a/server/tests/api/notifications/index.ts b/server/tests/api/notifications/index.ts index bd07a339e..8caa30a3d 100644 --- a/server/tests/api/notifications/index.ts +++ b/server/tests/api/notifications/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import './admin-notifications' | ||
1 | import './comments-notifications' | 2 | import './comments-notifications' |
2 | import './moderation-notifications' | 3 | import './moderation-notifications' |
3 | import './notifications-api' | 4 | import './notifications-api' |
diff --git a/server/tests/api/server/handle-down.ts b/server/tests/api/server/handle-down.ts index 043754e70..f3ba11950 100644 --- a/server/tests/api/server/handle-down.ts +++ b/server/tests/api/server/handle-down.ts | |||
@@ -348,8 +348,8 @@ describe('Test handle downs', function () { | |||
348 | 348 | ||
349 | for (let i = 0; i < 3; i++) { | 349 | for (let i = 0; i < 3; i++) { |
350 | await getVideo(servers[1].url, videoIdsServer1[i]) | 350 | await getVideo(servers[1].url, videoIdsServer1[i]) |
351 | await wait(1000) | ||
352 | await waitJobs([ servers[1] ]) | 351 | await waitJobs([ servers[1] ]) |
352 | await wait(1500) | ||
353 | } | 353 | } |
354 | 354 | ||
355 | for (const id of videoIdsServer1) { | 355 | for (const id of videoIdsServer1) { |
diff --git a/server/tests/api/server/services.ts b/server/tests/api/server/services.ts index df910c111..f0fa91674 100644 --- a/server/tests/api/server/services.ts +++ b/server/tests/api/server/services.ts | |||
@@ -20,6 +20,7 @@ const expect = chai.expect | |||
20 | describe('Test services', function () { | 20 | describe('Test services', function () { |
21 | let server: ServerInfo = null | 21 | let server: ServerInfo = null |
22 | let playlistUUID: string | 22 | let playlistUUID: string |
23 | let playlistDisplayName: string | ||
23 | let video: Video | 24 | let video: Video |
24 | 25 | ||
25 | before(async function () { | 26 | before(async function () { |
@@ -52,6 +53,7 @@ describe('Test services', function () { | |||
52 | }) | 53 | }) |
53 | 54 | ||
54 | playlistUUID = res.body.videoPlaylist.uuid | 55 | playlistUUID = res.body.videoPlaylist.uuid |
56 | playlistDisplayName = 'The Life and Times of Scrooge McDuck' | ||
55 | 57 | ||
56 | await addVideoInPlaylist({ | 58 | await addVideoInPlaylist({ |
57 | url: server.url, | 59 | url: server.url, |
@@ -69,7 +71,7 @@ describe('Test services', function () { | |||
69 | 71 | ||
70 | const res = await getOEmbed(server.url, oembedUrl) | 72 | const res = await getOEmbed(server.url, oembedUrl) |
71 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' + | 73 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' + |
72 | `src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + | 74 | `title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + |
73 | 'frameborder="0" allowfullscreen></iframe>' | 75 | 'frameborder="0" allowfullscreen></iframe>' |
74 | const expectedThumbnailUrl = 'http://localhost:' + server.port + video.previewPath | 76 | const expectedThumbnailUrl = 'http://localhost:' + server.port + video.previewPath |
75 | 77 | ||
@@ -88,7 +90,7 @@ describe('Test services', function () { | |||
88 | 90 | ||
89 | const res = await getOEmbed(server.url, oembedUrl) | 91 | const res = await getOEmbed(server.url, oembedUrl) |
90 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' + | 92 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' + |
91 | `src="http://localhost:${server.port}/video-playlists/embed/${playlistUUID}" ` + | 93 | `title="${playlistDisplayName}" src="http://localhost:${server.port}/video-playlists/embed/${playlistUUID}" ` + |
92 | 'frameborder="0" allowfullscreen></iframe>' | 94 | 'frameborder="0" allowfullscreen></iframe>' |
93 | 95 | ||
94 | expect(res.body.html).to.equal(expectedHtml) | 96 | expect(res.body.html).to.equal(expectedHtml) |
@@ -97,8 +99,8 @@ describe('Test services', function () { | |||
97 | expect(res.body.width).to.equal(560) | 99 | expect(res.body.width).to.equal(560) |
98 | expect(res.body.height).to.equal(315) | 100 | expect(res.body.height).to.equal(315) |
99 | expect(res.body.thumbnail_url).exist | 101 | expect(res.body.thumbnail_url).exist |
100 | expect(res.body.thumbnail_width).to.equal(223) | 102 | expect(res.body.thumbnail_width).to.equal(280) |
101 | expect(res.body.thumbnail_height).to.equal(122) | 103 | expect(res.body.thumbnail_height).to.equal(157) |
102 | }) | 104 | }) |
103 | 105 | ||
104 | it('Should have a valid oEmbed response with small max height query', async function () { | 106 | it('Should have a valid oEmbed response with small max height query', async function () { |
@@ -109,7 +111,7 @@ describe('Test services', function () { | |||
109 | 111 | ||
110 | const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth) | 112 | const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth) |
111 | const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts" ' + | 113 | const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts" ' + |
112 | `src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + | 114 | `title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + |
113 | 'frameborder="0" allowfullscreen></iframe>' | 115 | 'frameborder="0" allowfullscreen></iframe>' |
114 | 116 | ||
115 | expect(res.body.html).to.equal(expectedHtml) | 117 | expect(res.body.html).to.equal(expectedHtml) |
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 62a59033f..cea98aac7 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -4,10 +4,12 @@ import 'mocha' | |||
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models' | 5 | import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models' |
6 | import { CustomConfig } from '@shared/models/server' | 6 | import { CustomConfig } from '@shared/models/server' |
7 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
7 | import { | 8 | import { |
8 | addVideoCommentThread, | 9 | addVideoCommentThread, |
9 | blockUser, | 10 | blockUser, |
10 | cleanupTests, | 11 | cleanupTests, |
12 | closeAllSequelize, | ||
11 | createUser, | 13 | createUser, |
12 | deleteMe, | 14 | deleteMe, |
13 | flushAndRunServer, | 15 | flushAndRunServer, |
@@ -24,6 +26,7 @@ import { | |||
24 | getVideoChannel, | 26 | getVideoChannel, |
25 | getVideosList, | 27 | getVideosList, |
26 | installPlugin, | 28 | installPlugin, |
29 | killallServers, | ||
27 | login, | 30 | login, |
28 | makePutBodyRequest, | 31 | makePutBodyRequest, |
29 | rateVideo, | 32 | rateVideo, |
@@ -31,7 +34,9 @@ import { | |||
31 | removeUser, | 34 | removeUser, |
32 | removeVideo, | 35 | removeVideo, |
33 | reportAbuse, | 36 | reportAbuse, |
37 | reRunServer, | ||
34 | ServerInfo, | 38 | ServerInfo, |
39 | setTokenField, | ||
35 | testImage, | 40 | testImage, |
36 | unblockUser, | 41 | unblockUser, |
37 | updateAbuse, | 42 | updateAbuse, |
@@ -44,10 +49,9 @@ import { | |||
44 | waitJobs | 49 | waitJobs |
45 | } from '../../../../shared/extra-utils' | 50 | } from '../../../../shared/extra-utils' |
46 | import { follow } from '../../../../shared/extra-utils/server/follows' | 51 | import { follow } from '../../../../shared/extra-utils/server/follows' |
47 | import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' | 52 | import { logout, refreshToken, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' |
48 | import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' | 53 | import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' |
49 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' | 54 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' |
50 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
51 | 55 | ||
52 | const expect = chai.expect | 56 | const expect = chai.expect |
53 | 57 | ||
@@ -89,6 +93,7 @@ describe('Test users', function () { | |||
89 | const client = { id: 'client', secret: server.client.secret } | 93 | const client = { id: 'client', secret: server.client.secret } |
90 | const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) | 94 | const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) |
91 | 95 | ||
96 | expect(res.body.code).to.equal('invalid_client') | ||
92 | expect(res.body.error).to.contain('client is invalid') | 97 | expect(res.body.error).to.contain('client is invalid') |
93 | }) | 98 | }) |
94 | 99 | ||
@@ -96,6 +101,7 @@ describe('Test users', function () { | |||
96 | const client = { id: server.client.id, secret: 'coucou' } | 101 | const client = { id: server.client.id, secret: 'coucou' } |
97 | const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) | 102 | const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) |
98 | 103 | ||
104 | expect(res.body.code).to.equal('invalid_client') | ||
99 | expect(res.body.error).to.contain('client is invalid') | 105 | expect(res.body.error).to.contain('client is invalid') |
100 | }) | 106 | }) |
101 | }) | 107 | }) |
@@ -106,6 +112,7 @@ describe('Test users', function () { | |||
106 | const user = { username: 'captain crochet', password: server.user.password } | 112 | const user = { username: 'captain crochet', password: server.user.password } |
107 | const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) | 113 | const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) |
108 | 114 | ||
115 | expect(res.body.code).to.equal('invalid_grant') | ||
109 | expect(res.body.error).to.contain('credentials are invalid') | 116 | expect(res.body.error).to.contain('credentials are invalid') |
110 | }) | 117 | }) |
111 | 118 | ||
@@ -113,6 +120,7 @@ describe('Test users', function () { | |||
113 | const user = { username: server.user.username, password: 'mew_three' } | 120 | const user = { username: server.user.username, password: 'mew_three' } |
114 | const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) | 121 | const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) |
115 | 122 | ||
123 | expect(res.body.code).to.equal('invalid_grant') | ||
116 | expect(res.body.error).to.contain('credentials are invalid') | 124 | expect(res.body.error).to.contain('credentials are invalid') |
117 | }) | 125 | }) |
118 | 126 | ||
@@ -245,12 +253,44 @@ describe('Test users', function () { | |||
245 | }) | 253 | }) |
246 | 254 | ||
247 | it('Should be able to login again', async function () { | 255 | it('Should be able to login again', async function () { |
248 | server.accessToken = await serverLogin(server) | 256 | const res = await login(server.url, server.client, server.user) |
257 | server.accessToken = res.body.access_token | ||
258 | server.refreshToken = res.body.refresh_token | ||
259 | }) | ||
260 | |||
261 | it('Should be able to get my user information again', async function () { | ||
262 | await getMyUserInformation(server.url, server.accessToken) | ||
263 | }) | ||
264 | |||
265 | it('Should have an expired access token', async function () { | ||
266 | this.timeout(15000) | ||
267 | |||
268 | await setTokenField(server.internalServerNumber, server.accessToken, 'accessTokenExpiresAt', new Date().toISOString()) | ||
269 | await setTokenField(server.internalServerNumber, server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString()) | ||
270 | |||
271 | killallServers([ server ]) | ||
272 | await reRunServer(server) | ||
273 | |||
274 | await getMyUserInformation(server.url, server.accessToken, 401) | ||
275 | }) | ||
276 | |||
277 | it('Should not be able to refresh an access token with an expired refresh token', async function () { | ||
278 | await refreshToken(server, server.refreshToken, 400) | ||
249 | }) | 279 | }) |
250 | 280 | ||
251 | it('Should have an expired access token') | 281 | it('Should refresh the token', async function () { |
282 | this.timeout(15000) | ||
283 | |||
284 | const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString() | ||
285 | await setTokenField(server.internalServerNumber, server.accessToken, 'refreshTokenExpiresAt', futureDate) | ||
252 | 286 | ||
253 | it('Should refresh the token') | 287 | killallServers([ server ]) |
288 | await reRunServer(server) | ||
289 | |||
290 | const res = await refreshToken(server, server.refreshToken) | ||
291 | server.accessToken = res.body.access_token | ||
292 | server.refreshToken = res.body.refresh_token | ||
293 | }) | ||
254 | 294 | ||
255 | it('Should be able to get my user information again', async function () { | 295 | it('Should be able to get my user information again', async function () { |
256 | await getMyUserInformation(server.url, server.accessToken) | 296 | await getMyUserInformation(server.url, server.accessToken) |
@@ -976,6 +1016,7 @@ describe('Test users', function () { | |||
976 | }) | 1016 | }) |
977 | 1017 | ||
978 | after(async function () { | 1018 | after(async function () { |
1019 | await closeAllSequelize([ server ]) | ||
979 | await cleanupTests([ server ]) | 1020 | await cleanupTests([ server ]) |
980 | }) | 1021 | }) |
981 | }) | 1022 | }) |
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts index 367f99fdd..d12d58e75 100644 --- a/server/tests/api/videos/video-channels.ts +++ b/server/tests/api/videos/video-channels.ts | |||
@@ -2,16 +2,20 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { basename } from 'path' | ||
5 | import { | 6 | import { |
6 | cleanupTests, | 7 | cleanupTests, |
7 | createUser, | 8 | createUser, |
9 | deleteVideoChannelImage, | ||
8 | doubleFollow, | 10 | doubleFollow, |
9 | flushAndRunMultipleServers, | 11 | flushAndRunMultipleServers, |
12 | getActorImage, | ||
10 | getVideo, | 13 | getVideo, |
14 | getVideoChannel, | ||
11 | getVideoChannelVideos, | 15 | getVideoChannelVideos, |
12 | testImage, | 16 | testImage, |
13 | updateVideo, | 17 | updateVideo, |
14 | updateVideoChannelAvatar, | 18 | updateVideoChannelImage, |
15 | uploadVideo, | 19 | uploadVideo, |
16 | userLogin, | 20 | userLogin, |
17 | wait | 21 | wait |
@@ -21,7 +25,6 @@ import { | |||
21 | deleteVideoChannel, | 25 | deleteVideoChannel, |
22 | getAccountVideoChannelsList, | 26 | getAccountVideoChannelsList, |
23 | getMyUserInformation, | 27 | getMyUserInformation, |
24 | getVideoChannel, | ||
25 | getVideoChannelsList, | 28 | getVideoChannelsList, |
26 | ServerInfo, | 29 | ServerInfo, |
27 | setAccessTokensToServers, | 30 | setAccessTokensToServers, |
@@ -30,9 +33,17 @@ import { | |||
30 | } from '../../../../shared/extra-utils/index' | 33 | } from '../../../../shared/extra-utils/index' |
31 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 34 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
32 | import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index' | 35 | import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index' |
36 | import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants' | ||
33 | 37 | ||
34 | const expect = chai.expect | 38 | const expect = chai.expect |
35 | 39 | ||
40 | async function findChannel (server: ServerInfo, channelId: number) { | ||
41 | const res = await getVideoChannelsList(server.url, 0, 5, '-name') | ||
42 | const videoChannel = res.body.data.find(c => c.id === channelId) | ||
43 | |||
44 | return videoChannel as VideoChannel | ||
45 | } | ||
46 | |||
36 | describe('Test video channels', function () { | 47 | describe('Test video channels', function () { |
37 | let servers: ServerInfo[] | 48 | let servers: ServerInfo[] |
38 | let userInfo: User | 49 | let userInfo: User |
@@ -262,38 +273,94 @@ describe('Test video channels', function () { | |||
262 | }) | 273 | }) |
263 | 274 | ||
264 | it('Should update video channel avatar', async function () { | 275 | it('Should update video channel avatar', async function () { |
265 | this.timeout(5000) | 276 | this.timeout(15000) |
266 | 277 | ||
267 | const fixture = 'avatar.png' | 278 | const fixture = 'avatar.png' |
268 | 279 | ||
269 | await updateVideoChannelAvatar({ | 280 | await updateVideoChannelImage({ |
270 | url: servers[0].url, | 281 | url: servers[0].url, |
271 | accessToken: servers[0].accessToken, | 282 | accessToken: servers[0].accessToken, |
272 | videoChannelName: 'second_video_channel', | 283 | videoChannelName: 'second_video_channel', |
273 | fixture | 284 | fixture, |
285 | type: 'avatar' | ||
274 | }) | 286 | }) |
275 | 287 | ||
276 | await waitJobs(servers) | 288 | await waitJobs(servers) |
289 | |||
290 | for (const server of servers) { | ||
291 | const videoChannel = await findChannel(server, secondVideoChannelId) | ||
292 | |||
293 | await testImage(server.url, 'avatar-resized', videoChannel.avatar.path, '.png') | ||
294 | |||
295 | const row = await getActorImage(server.internalServerNumber, basename(videoChannel.avatar.path)) | ||
296 | expect(row.height).to.equal(ACTOR_IMAGES_SIZE.AVATARS.height) | ||
297 | expect(row.width).to.equal(ACTOR_IMAGES_SIZE.AVATARS.width) | ||
298 | } | ||
277 | }) | 299 | }) |
278 | 300 | ||
279 | it('Should have video channel avatar updated', async function () { | 301 | it('Should update video channel banner', async function () { |
302 | this.timeout(15000) | ||
303 | |||
304 | const fixture = 'banner.jpg' | ||
305 | |||
306 | await updateVideoChannelImage({ | ||
307 | url: servers[0].url, | ||
308 | accessToken: servers[0].accessToken, | ||
309 | videoChannelName: 'second_video_channel', | ||
310 | fixture, | ||
311 | type: 'banner' | ||
312 | }) | ||
313 | |||
314 | await waitJobs(servers) | ||
315 | |||
280 | for (const server of servers) { | 316 | for (const server of servers) { |
281 | const res = await getVideoChannelsList(server.url, 0, 1, '-name') | 317 | const res = await getVideoChannel(server.url, 'second_video_channel@' + servers[0].host) |
318 | const videoChannel = res.body | ||
282 | 319 | ||
283 | const videoChannel = res.body.data.find(c => c.id === secondVideoChannelId) | 320 | await testImage(server.url, 'banner-resized', videoChannel.banner.path) |
284 | 321 | ||
285 | await testImage(server.url, 'avatar-resized', videoChannel.avatar.path, '.png') | 322 | const row = await getActorImage(server.internalServerNumber, basename(videoChannel.banner.path)) |
323 | expect(row.height).to.equal(ACTOR_IMAGES_SIZE.BANNERS.height) | ||
324 | expect(row.width).to.equal(ACTOR_IMAGES_SIZE.BANNERS.width) | ||
325 | } | ||
326 | }) | ||
327 | |||
328 | it('Should delete the video channel avatar', async function () { | ||
329 | this.timeout(15000) | ||
330 | |||
331 | await deleteVideoChannelImage({ | ||
332 | url: servers[0].url, | ||
333 | accessToken: servers[0].accessToken, | ||
334 | videoChannelName: 'second_video_channel', | ||
335 | type: 'avatar' | ||
336 | }) | ||
337 | |||
338 | await waitJobs(servers) | ||
339 | |||
340 | for (const server of servers) { | ||
341 | const videoChannel = await findChannel(server, secondVideoChannelId) | ||
342 | |||
343 | expect(videoChannel.avatar).to.be.null | ||
286 | } | 344 | } |
287 | }) | 345 | }) |
288 | 346 | ||
289 | it('Should get video channel', async function () { | 347 | it('Should delete the video channel banner', async function () { |
290 | const res = await getVideoChannel(servers[0].url, 'second_video_channel') | 348 | this.timeout(15000) |
349 | |||
350 | await deleteVideoChannelImage({ | ||
351 | url: servers[0].url, | ||
352 | accessToken: servers[0].accessToken, | ||
353 | videoChannelName: 'second_video_channel', | ||
354 | type: 'banner' | ||
355 | }) | ||
356 | |||
357 | await waitJobs(servers) | ||
358 | |||
359 | for (const server of servers) { | ||
360 | const videoChannel = await findChannel(server, secondVideoChannelId) | ||
291 | 361 | ||
292 | const videoChannel = res.body | 362 | expect(videoChannel.banner).to.be.null |
293 | expect(videoChannel.name).to.equal('second_video_channel') | 363 | } |
294 | expect(videoChannel.displayName).to.equal('video channel updated') | ||
295 | expect(videoChannel.description).to.equal('video channel description updated') | ||
296 | expect(videoChannel.support).to.equal('video channel support text updated') | ||
297 | }) | 364 | }) |
298 | 365 | ||
299 | it('Should list the second video channel videos', async function () { | 366 | it('Should list the second video channel videos', async function () { |
diff --git a/server/tests/cli/index.ts b/server/tests/cli/index.ts index 242589010..7e6eebd17 100644 --- a/server/tests/cli/index.ts +++ b/server/tests/cli/index.ts | |||
@@ -6,5 +6,6 @@ import './peertube' | |||
6 | import './plugins' | 6 | import './plugins' |
7 | import './print-transcode-command' | 7 | import './print-transcode-command' |
8 | import './prune-storage' | 8 | import './prune-storage' |
9 | import './regenerate-thumbnails' | ||
9 | import './reset-password' | 10 | import './reset-password' |
10 | import './update-host' | 11 | import './update-host' |
diff --git a/server/tests/cli/regenerate-thumbnails.ts b/server/tests/cli/regenerate-thumbnails.ts new file mode 100644 index 000000000..8acb9f263 --- /dev/null +++ b/server/tests/cli/regenerate-thumbnails.ts | |||
@@ -0,0 +1,124 @@ | |||
1 | import 'mocha' | ||
2 | import { expect } from 'chai' | ||
3 | import { writeFile } from 'fs-extra' | ||
4 | import { basename, join } from 'path' | ||
5 | import { Video, VideoDetails } from '@shared/models' | ||
6 | import { | ||
7 | buildServerDirectory, | ||
8 | cleanupTests, | ||
9 | doubleFollow, | ||
10 | execCLI, | ||
11 | flushAndRunMultipleServers, | ||
12 | getEnvCli, | ||
13 | getVideo, | ||
14 | makeRawRequest, | ||
15 | ServerInfo, | ||
16 | setAccessTokensToServers, | ||
17 | uploadVideoAndGetId, | ||
18 | waitJobs | ||
19 | } from '../../../shared/extra-utils' | ||
20 | import { HttpStatusCode } from '@shared/core-utils' | ||
21 | |||
22 | async function testThumbnail (server: ServerInfo, videoId: number | string) { | ||
23 | const res = await getVideo(server.url, videoId) | ||
24 | const video: VideoDetails = res.body | ||
25 | |||
26 | const res1 = await makeRawRequest(join(server.url, video.thumbnailPath), HttpStatusCode.OK_200) | ||
27 | expect(res1.body).to.not.have.lengthOf(0) | ||
28 | |||
29 | const res2 = await makeRawRequest(join(server.url, video.thumbnailPath), HttpStatusCode.OK_200) | ||
30 | expect(res2.body).to.not.have.lengthOf(0) | ||
31 | } | ||
32 | |||
33 | describe('Test regenerate thumbnails script', function () { | ||
34 | let servers: ServerInfo[] | ||
35 | |||
36 | let video1: Video | ||
37 | let video2: Video | ||
38 | let remoteVideo: Video | ||
39 | |||
40 | let thumbnail1Path: string | ||
41 | let thumbnailRemotePath: string | ||
42 | |||
43 | before(async function () { | ||
44 | this.timeout(60000) | ||
45 | |||
46 | servers = await flushAndRunMultipleServers(2) | ||
47 | await setAccessTokensToServers(servers) | ||
48 | |||
49 | await doubleFollow(servers[0], servers[1]) | ||
50 | |||
51 | { | ||
52 | const videoUUID1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 1' })).uuid | ||
53 | video1 = await (getVideo(servers[0].url, videoUUID1).then(res => res.body)) | ||
54 | |||
55 | thumbnail1Path = join(buildServerDirectory(servers[0], 'thumbnails'), basename(video1.thumbnailPath)) | ||
56 | |||
57 | const videoUUID2 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 2' })).uuid | ||
58 | video2 = await (getVideo(servers[0].url, videoUUID2).then(res => res.body)) | ||
59 | } | ||
60 | |||
61 | { | ||
62 | const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 3' })).uuid | ||
63 | await waitJobs(servers) | ||
64 | |||
65 | remoteVideo = await (getVideo(servers[0].url, videoUUID).then(res => res.body)) | ||
66 | |||
67 | thumbnailRemotePath = join(buildServerDirectory(servers[0], 'thumbnails'), basename(remoteVideo.thumbnailPath)) | ||
68 | } | ||
69 | |||
70 | await writeFile(thumbnail1Path, '') | ||
71 | await writeFile(thumbnailRemotePath, '') | ||
72 | }) | ||
73 | |||
74 | it('Should have empty thumbnails', async function () { | ||
75 | { | ||
76 | const res = await makeRawRequest(join(servers[0].url, video1.thumbnailPath), HttpStatusCode.OK_200) | ||
77 | expect(res.body).to.have.lengthOf(0) | ||
78 | } | ||
79 | |||
80 | { | ||
81 | const res = await makeRawRequest(join(servers[0].url, video2.thumbnailPath), HttpStatusCode.OK_200) | ||
82 | expect(res.body).to.not.have.lengthOf(0) | ||
83 | } | ||
84 | |||
85 | { | ||
86 | const res = await makeRawRequest(join(servers[0].url, remoteVideo.thumbnailPath), HttpStatusCode.OK_200) | ||
87 | expect(res.body).to.have.lengthOf(0) | ||
88 | } | ||
89 | }) | ||
90 | |||
91 | it('Should regenerate local thumbnails from the CLI', async function () { | ||
92 | this.timeout(15000) | ||
93 | |||
94 | const env = getEnvCli(servers[0]) | ||
95 | await execCLI(`${env} npm run regenerate-thumbnails`) | ||
96 | }) | ||
97 | |||
98 | it('Should have generated new thumbnail files', async function () { | ||
99 | await testThumbnail(servers[0], video1.uuid) | ||
100 | await testThumbnail(servers[0], video2.uuid) | ||
101 | |||
102 | const res = await makeRawRequest(join(servers[0].url, remoteVideo.thumbnailPath), HttpStatusCode.OK_200) | ||
103 | expect(res.body).to.have.lengthOf(0) | ||
104 | }) | ||
105 | |||
106 | it('Should have deleted old thumbnail files', async function () { | ||
107 | { | ||
108 | await makeRawRequest(join(servers[0].url, video1.thumbnailPath), HttpStatusCode.NOT_FOUND_404) | ||
109 | } | ||
110 | |||
111 | { | ||
112 | await makeRawRequest(join(servers[0].url, video2.thumbnailPath), HttpStatusCode.NOT_FOUND_404) | ||
113 | } | ||
114 | |||
115 | { | ||
116 | const res = await makeRawRequest(join(servers[0].url, remoteVideo.thumbnailPath), HttpStatusCode.OK_200) | ||
117 | expect(res.body).to.have.lengthOf(0) | ||
118 | } | ||
119 | }) | ||
120 | |||
121 | after(async function () { | ||
122 | await cleanupTests(servers) | ||
123 | }) | ||
124 | }) | ||
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts index f1055ea44..7bad81751 100644 --- a/server/tests/feeds/feeds.ts +++ b/server/tests/feeds/feeds.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import * as libxmljs from 'libxmljs' | 5 | import * as xmlParser from 'fast-xml-parser' |
6 | import { | 6 | import { |
7 | addAccountToAccountBlocklist, | 7 | addAccountToAccountBlocklist, |
8 | addAccountToServerBlocklist, | 8 | addAccountToServerBlocklist, |
@@ -139,12 +139,15 @@ describe('Test syndication feeds', () => { | |||
139 | it('Should contain a valid enclosure (covers RSS 2.0 endpoint)', async function () { | 139 | it('Should contain a valid enclosure (covers RSS 2.0 endpoint)', async function () { |
140 | for (const server of servers) { | 140 | for (const server of servers) { |
141 | const rss = await getXMLfeed(server.url, 'videos') | 141 | const rss = await getXMLfeed(server.url, 'videos') |
142 | const xmlDoc = libxmljs.parseXmlString(rss.text) | 142 | expect(xmlParser.validate(rss.text)).to.be.true |
143 | const xmlEnclosure = xmlDoc.get('/rss/channel/item/enclosure') | 143 | |
144 | expect(xmlEnclosure).to.exist | 144 | const xmlDoc = xmlParser.parse(rss.text, { parseAttributeValue: true, ignoreAttributes: false }) |
145 | expect(xmlEnclosure.attr('type').value()).to.be.equal('application/x-bittorrent') | 145 | |
146 | expect(xmlEnclosure.attr('length').value()).to.be.equal('218910') | 146 | const enclosure = xmlDoc.rss.channel.item[0].enclosure |
147 | expect(xmlEnclosure.attr('url').value()).to.contain('720.torrent') | 147 | expect(enclosure).to.exist |
148 | expect(enclosure['@_type']).to.equal('application/x-bittorrent') | ||
149 | expect(enclosure['@_length']).to.equal(218910) | ||
150 | expect(enclosure['@_url']).to.contain('720.torrent') | ||
148 | } | 151 | } |
149 | }) | 152 | }) |
150 | 153 | ||
diff --git a/server/tests/fixtures/banner-resized.jpg b/server/tests/fixtures/banner-resized.jpg new file mode 100644 index 000000000..13ea422cb --- /dev/null +++ b/server/tests/fixtures/banner-resized.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/banner.jpg b/server/tests/fixtures/banner.jpg new file mode 100644 index 000000000..e5f284f59 --- /dev/null +++ b/server/tests/fixtures/banner.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/peertube-plugin-test/main.js b/server/tests/fixtures/peertube-plugin-test/main.js index 305d92002..ee0bc39f3 100644 --- a/server/tests/fixtures/peertube-plugin-test/main.js +++ b/server/tests/fixtures/peertube-plugin-test/main.js | |||
@@ -184,6 +184,76 @@ async function register ({ registerHook, registerSetting, settingsManager, stora | |||
184 | return result | 184 | return result |
185 | } | 185 | } |
186 | }) | 186 | }) |
187 | |||
188 | registerHook({ | ||
189 | target: 'filter:api.download.torrent.allowed.result', | ||
190 | handler: (result, params) => { | ||
191 | if (params && params.downloadName.includes('bad torrent')) { | ||
192 | return { allowed: false, errorMessage: 'Liu Bei' } | ||
193 | } | ||
194 | |||
195 | return result | ||
196 | } | ||
197 | }) | ||
198 | |||
199 | registerHook({ | ||
200 | target: 'filter:api.download.video.allowed.result', | ||
201 | handler: (result, params) => { | ||
202 | if (params && !params.streamingPlaylist && params.video.name.includes('bad file')) { | ||
203 | return { allowed: false, errorMessage: 'Cao Cao' } | ||
204 | } | ||
205 | |||
206 | if (params && params.streamingPlaylist && params.video.name.includes('bad playlist file')) { | ||
207 | return { allowed: false, errorMessage: 'Sun Jian' } | ||
208 | } | ||
209 | |||
210 | return result | ||
211 | } | ||
212 | }) | ||
213 | |||
214 | registerHook({ | ||
215 | target: 'filter:html.embed.video.allowed.result', | ||
216 | handler: (result, params) => { | ||
217 | return { | ||
218 | allowed: false, | ||
219 | html: 'Lu Bu' | ||
220 | } | ||
221 | } | ||
222 | }) | ||
223 | |||
224 | registerHook({ | ||
225 | target: 'filter:html.embed.video-playlist.allowed.result', | ||
226 | handler: (result, params) => { | ||
227 | return { | ||
228 | allowed: false, | ||
229 | html: 'Diao Chan' | ||
230 | } | ||
231 | } | ||
232 | }) | ||
233 | |||
234 | { | ||
235 | const searchHooks = [ | ||
236 | 'filter:api.search.videos.local.list.params', | ||
237 | 'filter:api.search.videos.local.list.result', | ||
238 | 'filter:api.search.videos.index.list.params', | ||
239 | 'filter:api.search.videos.index.list.result', | ||
240 | 'filter:api.search.video-channels.local.list.params', | ||
241 | 'filter:api.search.video-channels.local.list.result', | ||
242 | 'filter:api.search.video-channels.index.list.params', | ||
243 | 'filter:api.search.video-channels.index.list.result', | ||
244 | ] | ||
245 | |||
246 | for (const h of searchHooks) { | ||
247 | registerHook({ | ||
248 | target: h, | ||
249 | handler: (obj) => { | ||
250 | peertubeHelpers.logger.debug('Run hook %s.', h) | ||
251 | |||
252 | return obj | ||
253 | } | ||
254 | }) | ||
255 | } | ||
256 | } | ||
187 | } | 257 | } |
188 | 258 | ||
189 | async function unregister () { | 259 | async function unregister () { |
diff --git a/server/tests/fixtures/thumbnail-playlist.jpg b/server/tests/fixtures/thumbnail-playlist.jpg index 19db4f18c..62cd77435 100644 --- a/server/tests/fixtures/thumbnail-playlist.jpg +++ b/server/tests/fixtures/thumbnail-playlist.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_import_thumbnail.jpg b/server/tests/fixtures/video_import_thumbnail.jpg index fcc50b75f..9ee1bc382 100644 --- a/server/tests/fixtures/video_import_thumbnail.jpg +++ b/server/tests/fixtures/video_import_thumbnail.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short.mp4.jpg b/server/tests/fixtures/video_short.mp4.jpg index 48790ffec..62cd77435 100644 --- a/server/tests/fixtures/video_short.mp4.jpg +++ b/server/tests/fixtures/video_short.mp4.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short.ogv.jpg b/server/tests/fixtures/video_short.ogv.jpg index c4c1d00e5..62cd77435 100644 --- a/server/tests/fixtures/video_short.ogv.jpg +++ b/server/tests/fixtures/video_short.ogv.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short.webm.jpg b/server/tests/fixtures/video_short.webm.jpg index 7f8047516..62cd77435 100644 --- a/server/tests/fixtures/video_short.webm.jpg +++ b/server/tests/fixtures/video_short.webm.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short1.webm.jpg b/server/tests/fixtures/video_short1.webm.jpg index 582eb9ea3..615cb2a5d 100644 --- a/server/tests/fixtures/video_short1.webm.jpg +++ b/server/tests/fixtures/video_short1.webm.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short2.webm.jpg b/server/tests/fixtures/video_short2.webm.jpg index b331aba3b..aa3126381 100644 --- a/server/tests/fixtures/video_short2.webm.jpg +++ b/server/tests/fixtures/video_short2.webm.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short3.webm.jpg b/server/tests/fixtures/video_short3.webm.jpg index ec8652167..62cd77435 100644 --- a/server/tests/fixtures/video_short3.webm.jpg +++ b/server/tests/fixtures/video_short3.webm.jpg | |||
Binary files differ | |||
diff --git a/server/tests/helpers/request.ts b/server/tests/helpers/request.ts index f8b2d599b..5e77f129e 100644 --- a/server/tests/helpers/request.ts +++ b/server/tests/helpers/request.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' | ||
5 | import { get4KFileUrl, root, wait } from '../../../shared/extra-utils' | ||
6 | import { join } from 'path' | ||
7 | import { pathExists, remove } from 'fs-extra' | ||
8 | import { expect } from 'chai' | 4 | import { expect } from 'chai' |
5 | import { pathExists, remove } from 'fs-extra' | ||
6 | import { join } from 'path' | ||
7 | import { get4KFileUrl, root, wait } from '../../../shared/extra-utils' | ||
8 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' | ||
9 | 9 | ||
10 | describe('Request helpers', function () { | 10 | describe('Request helpers', function () { |
11 | const destPath1 = join(root(), 'test-output-1.txt') | 11 | const destPath1 = join(root(), 'test-output-1.txt') |
@@ -13,7 +13,7 @@ describe('Request helpers', function () { | |||
13 | 13 | ||
14 | it('Should throw an error when the bytes limit is exceeded for request', async function () { | 14 | it('Should throw an error when the bytes limit is exceeded for request', async function () { |
15 | try { | 15 | try { |
16 | await doRequest({ uri: get4KFileUrl() }, 3) | 16 | await doRequest(get4KFileUrl(), { bodyKBLimit: 3 }) |
17 | } catch { | 17 | } catch { |
18 | return | 18 | return |
19 | } | 19 | } |
@@ -23,7 +23,7 @@ describe('Request helpers', function () { | |||
23 | 23 | ||
24 | it('Should throw an error when the bytes limit is exceeded for request and save file', async function () { | 24 | it('Should throw an error when the bytes limit is exceeded for request and save file', async function () { |
25 | try { | 25 | try { |
26 | await doRequestAndSaveToFile({ uri: get4KFileUrl() }, destPath1, 3) | 26 | await doRequestAndSaveToFile(get4KFileUrl(), destPath1, { bodyKBLimit: 3 }) |
27 | } catch { | 27 | } catch { |
28 | 28 | ||
29 | await wait(500) | 29 | await wait(500) |
@@ -35,8 +35,8 @@ describe('Request helpers', function () { | |||
35 | }) | 35 | }) |
36 | 36 | ||
37 | it('Should succeed if the file is below the limit', async function () { | 37 | it('Should succeed if the file is below the limit', async function () { |
38 | await doRequest({ uri: get4KFileUrl() }, 5) | 38 | await doRequest(get4KFileUrl(), { bodyKBLimit: 5 }) |
39 | await doRequestAndSaveToFile({ uri: get4KFileUrl() }, destPath2, 5) | 39 | await doRequestAndSaveToFile(get4KFileUrl(), destPath2, { bodyKBLimit: 5 }) |
40 | 40 | ||
41 | expect(await pathExists(destPath2)).to.be.true | 41 | expect(await pathExists(destPath2)).to.be.true |
42 | }) | 42 | }) |
diff --git a/server/tests/plugins/external-auth.ts b/server/tests/plugins/external-auth.ts index a1b5e8f5d..5addb45c7 100644 --- a/server/tests/plugins/external-auth.ts +++ b/server/tests/plugins/external-auth.ts | |||
@@ -137,7 +137,7 @@ describe('Test external auth plugins', function () { | |||
137 | 137 | ||
138 | await loginUsingExternalToken(server, 'cyan', externalAuthToken, HttpStatusCode.BAD_REQUEST_400) | 138 | await loginUsingExternalToken(server, 'cyan', externalAuthToken, HttpStatusCode.BAD_REQUEST_400) |
139 | 139 | ||
140 | await waitUntilLog(server, 'expired external auth token') | 140 | await waitUntilLog(server, 'expired external auth token', 2) |
141 | }) | 141 | }) |
142 | 142 | ||
143 | it('Should auto login Cyan, create the user and use the token', async function () { | 143 | it('Should auto login Cyan, create the user and use the token', async function () { |
diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts index d88170201..ac958c5f5 100644 --- a/server/tests/plugins/filter-hooks.ts +++ b/server/tests/plugins/filter-hooks.ts | |||
@@ -2,11 +2,15 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { advancedVideoChannelSearch } from '@shared/extra-utils/search/video-channels' | ||
5 | import { ServerConfig } from '@shared/models' | 6 | import { ServerConfig } from '@shared/models' |
7 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
6 | import { | 8 | import { |
7 | addVideoCommentReply, | 9 | addVideoCommentReply, |
8 | addVideoCommentThread, | 10 | addVideoCommentThread, |
11 | advancedVideosSearch, | ||
9 | createLive, | 12 | createLive, |
13 | createVideoPlaylist, | ||
10 | doubleFollow, | 14 | doubleFollow, |
11 | getAccountVideos, | 15 | getAccountVideos, |
12 | getConfig, | 16 | getConfig, |
@@ -15,24 +19,33 @@ import { | |||
15 | getVideo, | 19 | getVideo, |
16 | getVideoChannelVideos, | 20 | getVideoChannelVideos, |
17 | getVideoCommentThreads, | 21 | getVideoCommentThreads, |
22 | getVideoPlaylist, | ||
18 | getVideosList, | 23 | getVideosList, |
19 | getVideosListPagination, | 24 | getVideosListPagination, |
20 | getVideoThreadComments, | 25 | getVideoThreadComments, |
21 | getVideoWithToken, | 26 | getVideoWithToken, |
22 | installPlugin, | 27 | installPlugin, |
28 | makeRawRequest, | ||
23 | registerUser, | 29 | registerUser, |
24 | setAccessTokensToServers, | 30 | setAccessTokensToServers, |
25 | setDefaultVideoChannel, | 31 | setDefaultVideoChannel, |
26 | updateCustomSubConfig, | 32 | updateCustomSubConfig, |
27 | updateVideo, | 33 | updateVideo, |
28 | uploadVideo, | 34 | uploadVideo, |
35 | uploadVideoAndGetId, | ||
29 | waitJobs | 36 | waitJobs |
30 | } from '../../../shared/extra-utils' | 37 | } from '../../../shared/extra-utils' |
31 | import { cleanupTests, flushAndRunMultipleServers, ServerInfo } from '../../../shared/extra-utils/server/servers' | 38 | import { cleanupTests, flushAndRunMultipleServers, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers' |
32 | import { getGoodVideoUrl, getMyVideoImports, importVideo } from '../../../shared/extra-utils/videos/video-imports' | 39 | import { getGoodVideoUrl, getMyVideoImports, importVideo } from '../../../shared/extra-utils/videos/video-imports' |
33 | import { VideoDetails, VideoImport, VideoImportState, VideoPrivacy } from '../../../shared/models/videos' | 40 | import { |
41 | VideoDetails, | ||
42 | VideoImport, | ||
43 | VideoImportState, | ||
44 | VideoPlaylist, | ||
45 | VideoPlaylistPrivacy, | ||
46 | VideoPrivacy | ||
47 | } from '../../../shared/models/videos' | ||
34 | import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model' | 48 | import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model' |
35 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
36 | 49 | ||
37 | const expect = chai.expect | 50 | const expect = chai.expect |
38 | 51 | ||
@@ -355,6 +368,165 @@ describe('Test plugin filter hooks', function () { | |||
355 | }) | 368 | }) |
356 | }) | 369 | }) |
357 | 370 | ||
371 | describe('Download hooks', function () { | ||
372 | const downloadVideos: VideoDetails[] = [] | ||
373 | |||
374 | before(async function () { | ||
375 | this.timeout(60000) | ||
376 | |||
377 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { | ||
378 | transcoding: { | ||
379 | webtorrent: { | ||
380 | enabled: true | ||
381 | }, | ||
382 | hls: { | ||
383 | enabled: true | ||
384 | } | ||
385 | } | ||
386 | }) | ||
387 | |||
388 | const uuids: string[] = [] | ||
389 | |||
390 | for (const name of [ 'bad torrent', 'bad file', 'bad playlist file' ]) { | ||
391 | const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: name })).uuid | ||
392 | uuids.push(uuid) | ||
393 | } | ||
394 | |||
395 | await waitJobs(servers) | ||
396 | |||
397 | for (const uuid of uuids) { | ||
398 | const res = await getVideo(servers[0].url, uuid) | ||
399 | downloadVideos.push(res.body) | ||
400 | } | ||
401 | }) | ||
402 | |||
403 | it('Should run filter:api.download.torrent.allowed.result', async function () { | ||
404 | const res = await makeRawRequest(downloadVideos[0].files[0].torrentDownloadUrl, 403) | ||
405 | expect(res.body.error).to.equal('Liu Bei') | ||
406 | |||
407 | await makeRawRequest(downloadVideos[1].files[0].torrentDownloadUrl, 200) | ||
408 | await makeRawRequest(downloadVideos[2].files[0].torrentDownloadUrl, 200) | ||
409 | }) | ||
410 | |||
411 | it('Should run filter:api.download.video.allowed.result', async function () { | ||
412 | { | ||
413 | const res = await makeRawRequest(downloadVideos[1].files[0].fileDownloadUrl, 403) | ||
414 | expect(res.body.error).to.equal('Cao Cao') | ||
415 | |||
416 | await makeRawRequest(downloadVideos[0].files[0].fileDownloadUrl, 200) | ||
417 | await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200) | ||
418 | } | ||
419 | |||
420 | { | ||
421 | const res = await makeRawRequest(downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl, 403) | ||
422 | expect(res.body.error).to.equal('Sun Jian') | ||
423 | |||
424 | await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200) | ||
425 | |||
426 | await makeRawRequest(downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl, 200) | ||
427 | await makeRawRequest(downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl, 200) | ||
428 | } | ||
429 | }) | ||
430 | }) | ||
431 | |||
432 | describe('Embed filters', function () { | ||
433 | const embedVideos: VideoDetails[] = [] | ||
434 | const embedPlaylists: VideoPlaylist[] = [] | ||
435 | |||
436 | before(async function () { | ||
437 | this.timeout(60000) | ||
438 | |||
439 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { | ||
440 | transcoding: { | ||
441 | enabled: false | ||
442 | } | ||
443 | }) | ||
444 | |||
445 | for (const name of [ 'bad embed', 'good embed' ]) { | ||
446 | { | ||
447 | const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: name })).uuid | ||
448 | const res = await getVideo(servers[0].url, uuid) | ||
449 | embedVideos.push(res.body) | ||
450 | } | ||
451 | |||
452 | { | ||
453 | const playlistAttrs = { displayName: name, videoChannelId: servers[0].videoChannel.id, privacy: VideoPlaylistPrivacy.PUBLIC } | ||
454 | const res = await createVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistAttrs }) | ||
455 | |||
456 | const resPlaylist = await getVideoPlaylist(servers[0].url, res.body.videoPlaylist.id) | ||
457 | embedPlaylists.push(resPlaylist.body) | ||
458 | } | ||
459 | } | ||
460 | }) | ||
461 | |||
462 | it('Should run filter:html.embed.video.allowed.result', async function () { | ||
463 | const res = await makeRawRequest(servers[0].url + embedVideos[0].embedPath, 200) | ||
464 | expect(res.text).to.equal('Lu Bu') | ||
465 | }) | ||
466 | |||
467 | it('Should run filter:html.embed.video-playlist.allowed.result', async function () { | ||
468 | const res = await makeRawRequest(servers[0].url + embedPlaylists[0].embedPath, 200) | ||
469 | expect(res.text).to.equal('Diao Chan') | ||
470 | }) | ||
471 | }) | ||
472 | |||
473 | describe('Search filters', function () { | ||
474 | |||
475 | before(async function () { | ||
476 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { | ||
477 | search: { | ||
478 | searchIndex: { | ||
479 | enabled: true, | ||
480 | isDefaultSearch: false, | ||
481 | disableLocalSearch: false | ||
482 | } | ||
483 | } | ||
484 | }) | ||
485 | }) | ||
486 | |||
487 | it('Should run filter:api.search.videos.local.list.{params,result}', async function () { | ||
488 | await advancedVideosSearch(servers[0].url, { | ||
489 | search: 'Sun Quan' | ||
490 | }) | ||
491 | |||
492 | await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.params', 1) | ||
493 | await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.result', 1) | ||
494 | }) | ||
495 | |||
496 | it('Should run filter:api.search.videos.index.list.{params,result}', async function () { | ||
497 | await advancedVideosSearch(servers[0].url, { | ||
498 | search: 'Sun Quan', | ||
499 | searchTarget: 'search-index' | ||
500 | }) | ||
501 | |||
502 | await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.params', 1) | ||
503 | await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.result', 1) | ||
504 | await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.index.list.params', 1) | ||
505 | await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.index.list.result', 1) | ||
506 | }) | ||
507 | |||
508 | it('Should run filter:api.search.video-channels.local.list.{params,result}', async function () { | ||
509 | await advancedVideoChannelSearch(servers[0].url, { | ||
510 | search: 'Sun Ce' | ||
511 | }) | ||
512 | |||
513 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.params', 1) | ||
514 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.result', 1) | ||
515 | }) | ||
516 | |||
517 | it('Should run filter:api.search.video-channels.index.list.{params,result}', async function () { | ||
518 | await advancedVideoChannelSearch(servers[0].url, { | ||
519 | search: 'Sun Ce', | ||
520 | searchTarget: 'search-index' | ||
521 | }) | ||
522 | |||
523 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.params', 1) | ||
524 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.result', 1) | ||
525 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.params', 1) | ||
526 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.result', 1) | ||
527 | }) | ||
528 | }) | ||
529 | |||
358 | after(async function () { | 530 | after(async function () { |
359 | await cleanupTests(servers) | 531 | await cleanupTests(servers) |
360 | }) | 532 | }) |
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts index 9be0834ba..915995031 100644 --- a/server/tools/peertube-import-videos.ts +++ b/server/tools/peertube-import-videos.ts | |||
@@ -202,10 +202,7 @@ async function uploadVideoOnPeerTube (parameters: { | |||
202 | if (videoInfo.thumbnail) { | 202 | if (videoInfo.thumbnail) { |
203 | thumbnailfile = join(cwd, sha256(videoInfo.thumbnail) + '.jpg') | 203 | thumbnailfile = join(cwd, sha256(videoInfo.thumbnail) + '.jpg') |
204 | 204 | ||
205 | await doRequestAndSaveToFile({ | 205 | await doRequestAndSaveToFile(videoInfo.thumbnail, thumbnailfile) |
206 | method: 'GET', | ||
207 | uri: videoInfo.thumbnail | ||
208 | }, thumbnailfile) | ||
209 | } | 206 | } |
210 | 207 | ||
211 | const originallyPublishedAt = buildOriginallyPublishedAt(videoInfo) | 208 | const originallyPublishedAt = buildOriginallyPublishedAt(videoInfo) |
diff --git a/server/types/models/account/account.ts b/server/types/models/account/account.ts index d2add9810..9513acad8 100644 --- a/server/types/models/account/account.ts +++ b/server/types/models/account/account.ts | |||
@@ -1,7 +1,10 @@ | |||
1 | import { FunctionProperties, PickWith } from '@shared/core-utils' | ||
1 | import { AccountModel } from '../../../models/account/account' | 2 | import { AccountModel } from '../../../models/account/account' |
3 | import { MChannelDefault } from '../video/video-channels' | ||
4 | import { MAccountBlocklistId } from './account-blocklist' | ||
2 | import { | 5 | import { |
3 | MActor, | 6 | MActor, |
4 | MActorAP, | 7 | MActorAPAccount, |
5 | MActorAPI, | 8 | MActorAPI, |
6 | MActorAudience, | 9 | MActorAudience, |
7 | MActorDefault, | 10 | MActorDefault, |
@@ -13,9 +16,6 @@ import { | |||
13 | MActorSummaryFormattable, | 16 | MActorSummaryFormattable, |
14 | MActorUrl | 17 | MActorUrl |
15 | } from './actor' | 18 | } from './actor' |
16 | import { FunctionProperties, PickWith } from '@shared/core-utils' | ||
17 | import { MAccountBlocklistId } from './account-blocklist' | ||
18 | import { MChannelDefault } from '../video/video-channels' | ||
19 | 19 | ||
20 | type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M> | 20 | type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M> |
21 | 21 | ||
@@ -106,4 +106,4 @@ export type MAccountFormattable = | |||
106 | 106 | ||
107 | export type MAccountAP = | 107 | export type MAccountAP = |
108 | Pick<MAccount, 'name' | 'description'> & | 108 | Pick<MAccount, 'name' | 'description'> & |
109 | Use<'Actor', MActorAP> | 109 | Use<'Actor', MActorAPAccount> |
diff --git a/server/types/models/account/actor-follow.ts b/server/types/models/account/actor-follow.ts index 8c213d09c..8e19c6140 100644 --- a/server/types/models/account/actor-follow.ts +++ b/server/types/models/account/actor-follow.ts | |||
@@ -1,16 +1,15 @@ | |||
1 | import { PickWith } from '@shared/core-utils' | ||
1 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 2 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
2 | import { | 3 | import { |
3 | MActor, | 4 | MActor, |
4 | MActorChannelAccountActor, | 5 | MActorChannelAccountActor, |
5 | MActorDefault, | 6 | MActorDefault, |
6 | MActorDefaultAccountChannel, | 7 | MActorDefaultAccountChannel, |
8 | MActorDefaultChannelId, | ||
7 | MActorFormattable, | 9 | MActorFormattable, |
8 | MActorHost, | 10 | MActorHost, |
9 | MActorUsername | 11 | MActorUsername |
10 | } from './actor' | 12 | } from './actor' |
11 | import { PickWith } from '@shared/core-utils' | ||
12 | import { ActorModel } from '@server/models/activitypub/actor' | ||
13 | import { MChannelDefault } from '../video/video-channels' | ||
14 | 13 | ||
15 | type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M> | 14 | type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M> |
16 | 15 | ||
@@ -47,14 +46,10 @@ export type MActorFollowFull = | |||
47 | 46 | ||
48 | // For subscriptions | 47 | // For subscriptions |
49 | 48 | ||
50 | type SubscriptionFollowing = | ||
51 | MActorDefault & | ||
52 | PickWith<ActorModel, 'VideoChannel', MChannelDefault> | ||
53 | |||
54 | export type MActorFollowActorsDefaultSubscription = | 49 | export type MActorFollowActorsDefaultSubscription = |
55 | MActorFollow & | 50 | MActorFollow & |
56 | Use<'ActorFollower', MActorDefault> & | 51 | Use<'ActorFollower', MActorDefault> & |
57 | Use<'ActorFollowing', SubscriptionFollowing> | 52 | Use<'ActorFollowing', MActorDefaultChannelId> |
58 | 53 | ||
59 | export type MActorFollowSubscriptions = | 54 | export type MActorFollowSubscriptions = |
60 | MActorFollow & | 55 | MActorFollow & |
diff --git a/server/types/models/account/actor-image.ts b/server/types/models/account/actor-image.ts new file mode 100644 index 000000000..e59f8b141 --- /dev/null +++ b/server/types/models/account/actor-image.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | import { ActorImageModel } from '../../../models/account/actor-image' | ||
2 | import { FunctionProperties } from '@shared/core-utils' | ||
3 | |||
4 | export type MActorImage = ActorImageModel | ||
5 | |||
6 | // ############################################################################ | ||
7 | |||
8 | // Format for API or AP object | ||
9 | |||
10 | export type MActorImageFormattable = | ||
11 | FunctionProperties<MActorImage> & | ||
12 | Pick<MActorImage, 'filename' | 'createdAt' | 'updatedAt'> | ||
diff --git a/server/types/models/account/actor.ts b/server/types/models/account/actor.ts index ee0d05f4e..8f3f30074 100644 --- a/server/types/models/account/actor.ts +++ b/server/types/models/account/actor.ts | |||
@@ -1,15 +1,17 @@ | |||
1 | import { ActorModel } from '../../../models/activitypub/actor' | 1 | |
2 | import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils' | 2 | import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils' |
3 | import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account' | 3 | import { ActorModel } from '../../../models/activitypub/actor' |
4 | import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server' | 4 | import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server' |
5 | import { MAvatar, MAvatarFormattable } from './avatar' | ||
6 | import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video' | 5 | import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video' |
6 | import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account' | ||
7 | import { MActorImage, MActorImageFormattable } from './actor-image' | ||
7 | 8 | ||
8 | type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M> | 9 | type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M> |
10 | type UseOpt<K extends keyof ActorModel, M> = PickWithOpt<ActorModel, K, M> | ||
9 | 11 | ||
10 | // ############################################################################ | 12 | // ############################################################################ |
11 | 13 | ||
12 | export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server'> | 14 | export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server' | 'Banner'> |
13 | 15 | ||
14 | // ############################################################################ | 16 | // ############################################################################ |
15 | 17 | ||
@@ -34,7 +36,7 @@ export type MActorRedundancyAllowedOpt = PickWithOpt<ActorModel, 'Server', MServ | |||
34 | export type MActorDefaultLight = | 36 | export type MActorDefaultLight = |
35 | MActorLight & | 37 | MActorLight & |
36 | Use<'Server', MServerHost> & | 38 | Use<'Server', MServerHost> & |
37 | Use<'Avatar', MAvatar> | 39 | Use<'Avatar', MActorImage> |
38 | 40 | ||
39 | export type MActorAccountId = | 41 | export type MActorAccountId = |
40 | MActor & | 42 | MActor & |
@@ -75,10 +77,25 @@ export type MActorServer = | |||
75 | 77 | ||
76 | // Complex actor associations | 78 | // Complex actor associations |
77 | 79 | ||
80 | export type MActorImages = | ||
81 | MActor & | ||
82 | Use<'Avatar', MActorImage> & | ||
83 | UseOpt<'Banner', MActorImage> | ||
84 | |||
78 | export type MActorDefault = | 85 | export type MActorDefault = |
79 | MActor & | 86 | MActor & |
80 | Use<'Server', MServer> & | 87 | Use<'Server', MServer> & |
81 | Use<'Avatar', MAvatar> | 88 | Use<'Avatar', MActorImage> |
89 | |||
90 | export type MActorDefaultChannelId = | ||
91 | MActorDefault & | ||
92 | Use<'VideoChannel', MChannelId> | ||
93 | |||
94 | export type MActorDefaultBanner = | ||
95 | MActor & | ||
96 | Use<'Server', MServer> & | ||
97 | Use<'Avatar', MActorImage> & | ||
98 | Use<'Banner', MActorImage> | ||
82 | 99 | ||
83 | // Actor with channel that is associated to an account and its actor | 100 | // Actor with channel that is associated to an account and its actor |
84 | // Actor -> VideoChannel -> Account -> Actor | 101 | // Actor -> VideoChannel -> Account -> Actor |
@@ -89,7 +106,8 @@ export type MActorChannelAccountActor = | |||
89 | export type MActorFull = | 106 | export type MActorFull = |
90 | MActor & | 107 | MActor & |
91 | Use<'Server', MServer> & | 108 | Use<'Server', MServer> & |
92 | Use<'Avatar', MAvatar> & | 109 | Use<'Avatar', MActorImage> & |
110 | Use<'Banner', MActorImage> & | ||
93 | Use<'Account', MAccount> & | 111 | Use<'Account', MAccount> & |
94 | Use<'VideoChannel', MChannelAccountActor> | 112 | Use<'VideoChannel', MChannelAccountActor> |
95 | 113 | ||
@@ -97,7 +115,8 @@ export type MActorFull = | |||
97 | export type MActorFullActor = | 115 | export type MActorFullActor = |
98 | MActor & | 116 | MActor & |
99 | Use<'Server', MServer> & | 117 | Use<'Server', MServer> & |
100 | Use<'Avatar', MAvatar> & | 118 | Use<'Avatar', MActorImage> & |
119 | Use<'Banner', MActorImage> & | ||
101 | Use<'Account', MAccountDefault> & | 120 | Use<'Account', MAccountDefault> & |
102 | Use<'VideoChannel', MChannelAccountDefault> | 121 | Use<'VideoChannel', MChannelAccountDefault> |
103 | 122 | ||
@@ -109,7 +128,7 @@ export type MActorSummary = | |||
109 | FunctionProperties<MActor> & | 128 | FunctionProperties<MActor> & |
110 | Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId' | 'avatarId'> & | 129 | Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId' | 'avatarId'> & |
111 | Use<'Server', MServerHost> & | 130 | Use<'Server', MServerHost> & |
112 | Use<'Avatar', MAvatar> | 131 | Use<'Avatar', MActorImage> |
113 | 132 | ||
114 | export type MActorSummaryBlocks = | 133 | export type MActorSummaryBlocks = |
115 | MActorSummary & | 134 | MActorSummary & |
@@ -127,13 +146,21 @@ export type MActorSummaryFormattable = | |||
127 | FunctionProperties<MActor> & | 146 | FunctionProperties<MActor> & |
128 | Pick<MActor, 'url' | 'preferredUsername'> & | 147 | Pick<MActor, 'url' | 'preferredUsername'> & |
129 | Use<'Server', MServerHost> & | 148 | Use<'Server', MServerHost> & |
130 | Use<'Avatar', MAvatarFormattable> | 149 | Use<'Avatar', MActorImageFormattable> |
131 | 150 | ||
132 | export type MActorFormattable = | 151 | export type MActorFormattable = |
133 | MActorSummaryFormattable & | 152 | MActorSummaryFormattable & |
134 | Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt'> & | 153 | Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt' | 'bannerId' | 'avatarId'> & |
135 | Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>> | 154 | Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>> & |
155 | UseOpt<'Banner', MActorImageFormattable> | ||
136 | 156 | ||
137 | export type MActorAP = | 157 | type MActorAPBase = |
138 | MActor & | 158 | MActor & |
139 | Use<'Avatar', MAvatar> | 159 | Use<'Avatar', MActorImage> |
160 | |||
161 | export type MActorAPAccount = | ||
162 | MActorAPBase | ||
163 | |||
164 | export type MActorAPChannel = | ||
165 | MActorAPBase & | ||
166 | Use<'Banner', MActorImage> | ||
diff --git a/server/types/models/account/avatar.ts b/server/types/models/account/avatar.ts deleted file mode 100644 index 0489a8599..000000000 --- a/server/types/models/account/avatar.ts +++ /dev/null | |||
@@ -1,12 +0,0 @@ | |||
1 | import { AvatarModel } from '../../../models/avatar/avatar' | ||
2 | import { FunctionProperties } from '@shared/core-utils' | ||
3 | |||
4 | export type MAvatar = AvatarModel | ||
5 | |||
6 | // ############################################################################ | ||
7 | |||
8 | // Format for API or AP object | ||
9 | |||
10 | export type MAvatarFormattable = | ||
11 | FunctionProperties<MAvatar> & | ||
12 | Pick<MAvatar, 'filename' | 'createdAt' | 'updatedAt'> | ||
diff --git a/server/types/models/account/index.ts b/server/types/models/account/index.ts index 513c09c40..e3fc00f94 100644 --- a/server/types/models/account/index.ts +++ b/server/types/models/account/index.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | export * from './account' | 1 | export * from './account' |
2 | export * from './account-blocklist' | 2 | export * from './account-blocklist' |
3 | export * from './actor' | ||
4 | export * from './actor-follow' | 3 | export * from './actor-follow' |
5 | export * from './avatar' | 4 | export * from './actor-image' |
5 | export * from './actor' | ||
diff --git a/server/types/models/application/application.ts b/server/types/models/application/application.ts new file mode 100644 index 000000000..9afb9ad70 --- /dev/null +++ b/server/types/models/application/application.ts | |||
@@ -0,0 +1,5 @@ | |||
1 | import { ApplicationModel } from '@server/models/application/application' | ||
2 | |||
3 | // ############################################################################ | ||
4 | |||
5 | export type MApplication = Omit<ApplicationModel, 'Account'> | ||
diff --git a/server/types/models/application/index.ts b/server/types/models/application/index.ts new file mode 100644 index 000000000..26e4b031f --- /dev/null +++ b/server/types/models/application/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './application' | |||
diff --git a/server/types/models/index.ts b/server/types/models/index.ts index affa17425..b4fdb1ff3 100644 --- a/server/types/models/index.ts +++ b/server/types/models/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export * from './account' | 1 | export * from './account' |
2 | export * from './application' | ||
2 | export * from './moderation' | 3 | export * from './moderation' |
3 | export * from './oauth' | 4 | export * from './oauth' |
4 | export * from './server' | 5 | export * from './server' |
diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts index 58764a748..7ebb0485d 100644 --- a/server/types/models/user/user-notification.ts +++ b/server/types/models/user/user-notification.ts | |||
@@ -1,12 +1,14 @@ | |||
1 | import { VideoAbuseModel } from '@server/models/abuse/video-abuse' | 1 | import { VideoAbuseModel } from '@server/models/abuse/video-abuse' |
2 | import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' | 2 | import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' |
3 | import { ApplicationModel } from '@server/models/application/application' | ||
4 | import { PluginModel } from '@server/models/server/plugin' | ||
3 | import { PickWith, PickWithOpt } from '@shared/core-utils' | 5 | import { PickWith, PickWithOpt } from '@shared/core-utils' |
4 | import { AbuseModel } from '../../../models/abuse/abuse' | 6 | import { AbuseModel } from '../../../models/abuse/abuse' |
5 | import { AccountModel } from '../../../models/account/account' | 7 | import { AccountModel } from '../../../models/account/account' |
8 | import { ActorImageModel } from '../../../models/account/actor-image' | ||
6 | import { UserNotificationModel } from '../../../models/account/user-notification' | 9 | import { UserNotificationModel } from '../../../models/account/user-notification' |
7 | import { ActorModel } from '../../../models/activitypub/actor' | 10 | import { ActorModel } from '../../../models/activitypub/actor' |
8 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 11 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
9 | import { AvatarModel } from '../../../models/avatar/avatar' | ||
10 | import { ServerModel } from '../../../models/server/server' | 12 | import { ServerModel } from '../../../models/server/server' |
11 | import { VideoModel } from '../../../models/video/video' | 13 | import { VideoModel } from '../../../models/video/video' |
12 | import { VideoBlacklistModel } from '../../../models/video/video-blacklist' | 14 | import { VideoBlacklistModel } from '../../../models/video/video-blacklist' |
@@ -27,7 +29,7 @@ export module UserNotificationIncludes { | |||
27 | 29 | ||
28 | export type ActorInclude = | 30 | export type ActorInclude = |
29 | Pick<ActorModel, 'preferredUsername' | 'getHost'> & | 31 | Pick<ActorModel, 'preferredUsername' | 'getHost'> & |
30 | PickWith<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> & | 32 | PickWith<ActorModel, 'Avatar', Pick<ActorImageModel, 'filename' | 'getStaticPath'>> & |
31 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> | 33 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> |
32 | 34 | ||
33 | export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'> | 35 | export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'> |
@@ -73,7 +75,7 @@ export module UserNotificationIncludes { | |||
73 | Pick<ActorModel, 'preferredUsername' | 'getHost'> & | 75 | Pick<ActorModel, 'preferredUsername' | 'getHost'> & |
74 | PickWith<ActorModel, 'Account', AccountInclude> & | 76 | PickWith<ActorModel, 'Account', AccountInclude> & |
75 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> & | 77 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> & |
76 | PickWithOpt<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> | 78 | PickWithOpt<ActorModel, 'Avatar', Pick<ActorImageModel, 'filename' | 'getStaticPath'>> |
77 | 79 | ||
78 | export type ActorFollowing = | 80 | export type ActorFollowing = |
79 | Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> & | 81 | Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> & |
@@ -85,13 +87,19 @@ export module UserNotificationIncludes { | |||
85 | Pick<ActorFollowModel, 'id' | 'state'> & | 87 | Pick<ActorFollowModel, 'id' | 'state'> & |
86 | PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> & | 88 | PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> & |
87 | PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing> | 89 | PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing> |
90 | |||
91 | export type PluginInclude = | ||
92 | Pick<PluginModel, 'id' | 'name' | 'type' | 'latestVersion'> | ||
93 | |||
94 | export type ApplicationInclude = | ||
95 | Pick<ApplicationModel, 'latestPeerTubeVersion'> | ||
88 | } | 96 | } |
89 | 97 | ||
90 | // ############################################################################ | 98 | // ############################################################################ |
91 | 99 | ||
92 | export type MUserNotification = | 100 | export type MUserNotification = |
93 | Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' | | 101 | Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' | |
94 | 'VideoImport' | 'Account' | 'ActorFollow'> | 102 | 'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application'> |
95 | 103 | ||
96 | // ############################################################################ | 104 | // ############################################################################ |
97 | 105 | ||
@@ -103,4 +111,6 @@ export type UserNotificationModelForApi = | |||
103 | Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> & | 111 | Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> & |
104 | Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> & | 112 | Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> & |
105 | Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> & | 113 | Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> & |
114 | Use<'Plugin', UserNotificationIncludes.PluginInclude> & | ||
115 | Use<'Application', UserNotificationIncludes.ApplicationInclude> & | ||
106 | Use<'Account', UserNotificationIncludes.AccountIncludeActor> | 116 | Use<'Account', UserNotificationIncludes.AccountIncludeActor> |
diff --git a/server/types/models/user/user.ts b/server/types/models/user/user.ts index 12a68accf..fa7de9c52 100644 --- a/server/types/models/user/user.ts +++ b/server/types/models/user/user.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | import { UserModel } from '../../../models/account/user' | 1 | import { AccountModel } from '@server/models/account/account' |
2 | import { MVideoPlaylist } from '@server/types/models' | ||
2 | import { PickWith, PickWithOpt } from '@shared/core-utils' | 3 | import { PickWith, PickWithOpt } from '@shared/core-utils' |
4 | import { UserModel } from '../../../models/account/user' | ||
3 | import { | 5 | import { |
4 | MAccount, | 6 | MAccount, |
5 | MAccountDefault, | 7 | MAccountDefault, |
@@ -9,10 +11,8 @@ import { | |||
9 | MAccountIdActorId, | 11 | MAccountIdActorId, |
10 | MAccountUrl | 12 | MAccountUrl |
11 | } from '../account' | 13 | } from '../account' |
12 | import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting' | ||
13 | import { AccountModel } from '@server/models/account/account' | ||
14 | import { MChannelFormattable } from '../video/video-channels' | 14 | import { MChannelFormattable } from '../video/video-channels' |
15 | import { MVideoPlaylist } from '@server/types/models' | 15 | import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting' |
16 | 16 | ||
17 | type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M> | 17 | type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M> |
18 | 18 | ||
diff --git a/server/types/models/video/video-channels.ts b/server/types/models/video/video-channels.ts index 77790daa4..f577807ca 100644 --- a/server/types/models/video/video-channels.ts +++ b/server/types/models/video/video-channels.ts | |||
@@ -12,15 +12,17 @@ import { | |||
12 | MAccountUserId, | 12 | MAccountUserId, |
13 | MActor, | 13 | MActor, |
14 | MActorAccountChannelId, | 14 | MActorAccountChannelId, |
15 | MActorAP, | 15 | MActorAPChannel, |
16 | MActorAPI, | 16 | MActorAPI, |
17 | MActorDefault, | 17 | MActorDefault, |
18 | MActorDefaultBanner, | ||
18 | MActorDefaultLight, | 19 | MActorDefaultLight, |
19 | MActorFormattable, | 20 | MActorFormattable, |
20 | MActorHost, | 21 | MActorHost, |
21 | MActorLight, | 22 | MActorLight, |
22 | MActorSummary, | 23 | MActorSummary, |
23 | MActorSummaryFormattable, MActorUrl | 24 | MActorSummaryFormattable, |
25 | MActorUrl | ||
24 | } from '../account' | 26 | } from '../account' |
25 | import { MVideo } from './video' | 27 | import { MVideo } from './video' |
26 | 28 | ||
@@ -55,14 +57,14 @@ export type MChannelDefault = | |||
55 | MChannel & | 57 | MChannel & |
56 | Use<'Actor', MActorDefault> | 58 | Use<'Actor', MActorDefault> |
57 | 59 | ||
60 | export type MChannelBannerDefault = | ||
61 | MChannel & | ||
62 | Use<'Actor', MActorDefaultBanner> | ||
63 | |||
58 | // ############################################################################ | 64 | // ############################################################################ |
59 | 65 | ||
60 | // Not all association attributes | 66 | // Not all association attributes |
61 | 67 | ||
62 | export type MChannelLight = | ||
63 | MChannel & | ||
64 | Use<'Actor', MActorDefaultLight> | ||
65 | |||
66 | export type MChannelActorLight = | 68 | export type MChannelActorLight = |
67 | MChannel & | 69 | MChannel & |
68 | Use<'Actor', MActorLight> | 70 | Use<'Actor', MActorLight> |
@@ -84,29 +86,23 @@ export type MChannelAccountActor = | |||
84 | MChannel & | 86 | MChannel & |
85 | Use<'Account', MAccountActor> | 87 | Use<'Account', MAccountActor> |
86 | 88 | ||
87 | export type MChannelAccountDefault = | 89 | export type MChannelBannerAccountDefault = |
88 | MChannel & | 90 | MChannel & |
89 | Use<'Actor', MActorDefault> & | 91 | Use<'Actor', MActorDefaultBanner> & |
90 | Use<'Account', MAccountDefault> | 92 | Use<'Account', MAccountDefault> |
91 | 93 | ||
92 | export type MChannelActorAccountActor = | 94 | export type MChannelAccountDefault = |
93 | MChannel & | 95 | MChannel & |
94 | Use<'Account', MAccountActor> & | 96 | Use<'Actor', MActorDefault> & |
95 | Use<'Actor', MActor> | 97 | Use<'Account', MAccountDefault> |
96 | 98 | ||
97 | // ############################################################################ | 99 | // ############################################################################ |
98 | 100 | ||
99 | // Videos associations | 101 | // Videos associations |
100 | export type MChannelVideos = | 102 | export type MChannelVideos = |
101 | MChannel & | 103 | MChannel & |
102 | Use<'Videos', MVideo[]> | 104 | Use<'Videos', MVideo[]> |
103 | 105 | ||
104 | export type MChannelActorAccountDefaultVideos = | ||
105 | MChannel & | ||
106 | Use<'Actor', MActorDefault> & | ||
107 | Use<'Account', MAccountDefault> & | ||
108 | Use<'Videos', MVideo[]> | ||
109 | |||
110 | // ############################################################################ | 106 | // ############################################################################ |
111 | 107 | ||
112 | // For API | 108 | // For API |
@@ -146,5 +142,5 @@ export type MChannelFormattable = | |||
146 | 142 | ||
147 | export type MChannelAP = | 143 | export type MChannelAP = |
148 | Pick<MChannel, 'name' | 'description' | 'support'> & | 144 | Pick<MChannel, 'name' | 'description' | 'support'> & |
149 | Use<'Actor', MActorAP> & | 145 | Use<'Actor', MActorAPChannel> & |
150 | Use<'Account', MAccountUrl> | 146 | Use<'Account', MAccountUrl> |
diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts index 66acfb3f5..cf3e7ae34 100644 --- a/server/typings/express/index.d.ts +++ b/server/typings/express/index.d.ts | |||
@@ -3,7 +3,9 @@ import { | |||
3 | MAbuseMessage, | 3 | MAbuseMessage, |
4 | MAbuseReporter, | 4 | MAbuseReporter, |
5 | MAccountBlocklist, | 5 | MAccountBlocklist, |
6 | MActorFollowActorsDefault, | ||
6 | MActorUrl, | 7 | MActorUrl, |
8 | MChannelBannerAccountDefault, | ||
7 | MStreamingPlaylist, | 9 | MStreamingPlaylist, |
8 | MVideoChangeOwnershipFull, | 10 | MVideoChangeOwnershipFull, |
9 | MVideoFile, | 11 | MVideoFile, |
@@ -17,15 +19,12 @@ import { MPlugin, MServer, MServerBlocklist } from '@server/types/models/server' | |||
17 | import { MVideoImportDefault } from '@server/types/models/video/video-import' | 19 | import { MVideoImportDefault } from '@server/types/models/video/video-import' |
18 | import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' | 20 | import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' |
19 | import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' | 21 | import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' |
20 | import { UserRole } from '@shared/models' | ||
21 | import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' | 22 | import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' |
22 | import { | 23 | import { |
23 | MAccountDefault, | 24 | MAccountDefault, |
24 | MActorAccountChannelId, | 25 | MActorAccountChannelId, |
25 | MActorFollowActorsDefault, | ||
26 | MActorFollowActorsDefaultSubscription, | 26 | MActorFollowActorsDefaultSubscription, |
27 | MActorFull, | 27 | MActorFull, |
28 | MChannelAccountDefault, | ||
29 | MComment, | 28 | MComment, |
30 | MCommentOwnerVideoReply, | 29 | MCommentOwnerVideoReply, |
31 | MUserDefault, | 30 | MUserDefault, |
@@ -49,22 +48,6 @@ declare module 'express' { | |||
49 | } | 48 | } |
50 | 49 | ||
51 | interface PeerTubeLocals { | 50 | interface PeerTubeLocals { |
52 | bypassLogin?: { | ||
53 | bypass: boolean | ||
54 | pluginName: string | ||
55 | authName?: string | ||
56 | user: { | ||
57 | username: string | ||
58 | email: string | ||
59 | displayName: string | ||
60 | role: UserRole | ||
61 | } | ||
62 | } | ||
63 | |||
64 | refreshTokenAuthName?: string | ||
65 | |||
66 | explicitLogout?: boolean | ||
67 | |||
68 | videoAll?: MVideoFullLight | 51 | videoAll?: MVideoFullLight |
69 | onlyImmutableVideo?: MVideoImmutable | 52 | onlyImmutableVideo?: MVideoImmutable |
70 | onlyVideo?: MVideoThumbnail | 53 | onlyVideo?: MVideoThumbnail |
@@ -88,7 +71,7 @@ interface PeerTubeLocals { | |||
88 | 71 | ||
89 | videoStreamingPlaylist?: MStreamingPlaylist | 72 | videoStreamingPlaylist?: MStreamingPlaylist |
90 | 73 | ||
91 | videoChannel?: MChannelAccountDefault | 74 | videoChannel?: MChannelBannerAccountDefault |
92 | 75 | ||
93 | videoPlaylistFull?: MVideoPlaylistFull | 76 | videoPlaylistFull?: MVideoPlaylistFull |
94 | videoPlaylistSummary?: MVideoPlaylistFullSummary | 77 | videoPlaylistSummary?: MVideoPlaylistFullSummary |
diff --git a/shared/core-utils/renderer/html.ts b/shared/core-utils/renderer/html.ts index 1220848a0..de4ad47ac 100644 --- a/shared/core-utils/renderer/html.ts +++ b/shared/core-utils/renderer/html.ts | |||
@@ -19,3 +19,21 @@ export const SANITIZE_OPTIONS = { | |||
19 | } | 19 | } |
20 | } | 20 | } |
21 | } | 21 | } |
22 | |||
23 | // Thanks: https://stackoverflow.com/a/12034334 | ||
24 | export function escapeHTML (stringParam: string) { | ||
25 | if (!stringParam) return '' | ||
26 | |||
27 | const entityMap = { | ||
28 | '&': '&', | ||
29 | '<': '<', | ||
30 | '>': '>', | ||
31 | '"': '"', | ||
32 | '\'': ''', | ||
33 | '/': '/', | ||
34 | '`': '`', | ||
35 | '=': '=' | ||
36 | } | ||
37 | |||
38 | return String(stringParam).replace(/[&<>"'`=/]/g, s => entityMap[s]) | ||
39 | } | ||
diff --git a/shared/extra-utils/index.ts b/shared/extra-utils/index.ts index 5c95a1b3e..898a92d43 100644 --- a/shared/extra-utils/index.ts +++ b/shared/extra-utils/index.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | export * from './bulk/bulk' | 1 | export * from './bulk/bulk' |
2 | export * from './cli/cli' | 2 | export * from './cli/cli' |
3 | export * from './feeds/feeds' | 3 | export * from './feeds/feeds' |
4 | export * from './instances-index/mock-instances-index' | 4 | export * from './mock-servers/mock-instances-index' |
5 | export * from './miscs/miscs' | 5 | export * from './miscs/miscs' |
6 | export * from './miscs/sql' | 6 | export * from './miscs/sql' |
7 | export * from './miscs/stubs' | 7 | export * from './miscs/stubs' |
diff --git a/shared/extra-utils/miscs/sql.ts b/shared/extra-utils/miscs/sql.ts index 740f0c2d6..65a0aa5fe 100644 --- a/shared/extra-utils/miscs/sql.ts +++ b/shared/extra-utils/miscs/sql.ts | |||
@@ -82,6 +82,11 @@ async function countVideoViewsOf (internalServerNumber: number, uuid: string) { | |||
82 | return parseInt(total + '', 10) | 82 | return parseInt(total + '', 10) |
83 | } | 83 | } |
84 | 84 | ||
85 | function getActorImage (internalServerNumber: number, filename: string) { | ||
86 | return selectQuery(internalServerNumber, `SELECT * FROM "actorImage" WHERE filename = '${filename}'`) | ||
87 | .then(rows => rows[0]) | ||
88 | } | ||
89 | |||
85 | function selectQuery (internalServerNumber: number, query: string) { | 90 | function selectQuery (internalServerNumber: number, query: string) { |
86 | const seq = getSequelize(internalServerNumber) | 91 | const seq = getSequelize(internalServerNumber) |
87 | const options = { type: QueryTypes.SELECT as QueryTypes.SELECT } | 92 | const options = { type: QueryTypes.SELECT as QueryTypes.SELECT } |
@@ -106,12 +111,20 @@ async function closeAllSequelize (servers: ServerInfo[]) { | |||
106 | } | 111 | } |
107 | } | 112 | } |
108 | 113 | ||
109 | function setPluginVersion (internalServerNumber: number, pluginName: string, newVersion: string) { | 114 | function setPluginField (internalServerNumber: number, pluginName: string, field: string, value: string) { |
110 | const seq = getSequelize(internalServerNumber) | 115 | const seq = getSequelize(internalServerNumber) |
111 | 116 | ||
112 | const options = { type: QueryTypes.UPDATE } | 117 | const options = { type: QueryTypes.UPDATE } |
113 | 118 | ||
114 | return seq.query(`UPDATE "plugin" SET "version" = '${newVersion}' WHERE "name" = '${pluginName}'`, options) | 119 | return seq.query(`UPDATE "plugin" SET "${field}" = '${value}' WHERE "name" = '${pluginName}'`, options) |
120 | } | ||
121 | |||
122 | function setPluginVersion (internalServerNumber: number, pluginName: string, newVersion: string) { | ||
123 | return setPluginField(internalServerNumber, pluginName, 'version', newVersion) | ||
124 | } | ||
125 | |||
126 | function setPluginLatestVersion (internalServerNumber: number, pluginName: string, newVersion: string) { | ||
127 | return setPluginField(internalServerNumber, pluginName, 'latestVersion', newVersion) | ||
115 | } | 128 | } |
116 | 129 | ||
117 | function setActorFollowScores (internalServerNumber: number, newScore: number) { | 130 | function setActorFollowScores (internalServerNumber: number, newScore: number) { |
@@ -122,14 +135,25 @@ function setActorFollowScores (internalServerNumber: number, newScore: number) { | |||
122 | return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options) | 135 | return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options) |
123 | } | 136 | } |
124 | 137 | ||
138 | function setTokenField (internalServerNumber: number, accessToken: string, field: string, value: string) { | ||
139 | const seq = getSequelize(internalServerNumber) | ||
140 | |||
141 | const options = { type: QueryTypes.UPDATE } | ||
142 | |||
143 | return seq.query(`UPDATE "oAuthToken" SET "${field}" = '${value}' WHERE "accessToken" = '${accessToken}'`, options) | ||
144 | } | ||
145 | |||
125 | export { | 146 | export { |
126 | setVideoField, | 147 | setVideoField, |
127 | setPlaylistField, | 148 | setPlaylistField, |
128 | setActorField, | 149 | setActorField, |
129 | countVideoViewsOf, | 150 | countVideoViewsOf, |
130 | setPluginVersion, | 151 | setPluginVersion, |
152 | setPluginLatestVersion, | ||
131 | selectQuery, | 153 | selectQuery, |
154 | getActorImage, | ||
132 | deleteAll, | 155 | deleteAll, |
156 | setTokenField, | ||
133 | updateQuery, | 157 | updateQuery, |
134 | setActorFollowScores, | 158 | setActorFollowScores, |
135 | closeAllSequelize, | 159 | closeAllSequelize, |
diff --git a/shared/extra-utils/mock-servers/joinpeertube-versions.ts b/shared/extra-utils/mock-servers/joinpeertube-versions.ts new file mode 100644 index 000000000..d7d5b2c49 --- /dev/null +++ b/shared/extra-utils/mock-servers/joinpeertube-versions.ts | |||
@@ -0,0 +1,31 @@ | |||
1 | import * as express from 'express' | ||
2 | |||
3 | export class MockJoinPeerTubeVersions { | ||
4 | private latestVersion: string | ||
5 | |||
6 | initialize () { | ||
7 | return new Promise<void>(res => { | ||
8 | const app = express() | ||
9 | |||
10 | app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
11 | if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url) | ||
12 | |||
13 | return next() | ||
14 | }) | ||
15 | |||
16 | app.get('/versions.json', (req: express.Request, res: express.Response) => { | ||
17 | return res.json({ | ||
18 | peertube: { | ||
19 | latestVersion: this.latestVersion | ||
20 | } | ||
21 | }) | ||
22 | }) | ||
23 | |||
24 | app.listen(42102, () => res()) | ||
25 | }) | ||
26 | } | ||
27 | |||
28 | setLatestVersion (latestVersion: string) { | ||
29 | this.latestVersion = latestVersion | ||
30 | } | ||
31 | } | ||
diff --git a/shared/extra-utils/instances-index/mock-instances-index.ts b/shared/extra-utils/mock-servers/mock-instances-index.ts index 2604eda03..2604eda03 100644 --- a/shared/extra-utils/instances-index/mock-instances-index.ts +++ b/shared/extra-utils/mock-servers/mock-instances-index.ts | |||
diff --git a/shared/extra-utils/requests/activitypub.ts b/shared/extra-utils/requests/activitypub.ts index 4762a8665..ecd8ce823 100644 --- a/shared/extra-utils/requests/activitypub.ts +++ b/shared/extra-utils/requests/activitypub.ts | |||
@@ -5,20 +5,19 @@ import { activityPubContextify } from '../../../server/helpers/activitypub' | |||
5 | 5 | ||
6 | function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) { | 6 | function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) { |
7 | const options = { | 7 | const options = { |
8 | method: 'POST', | 8 | method: 'POST' as 'POST', |
9 | uri: url, | ||
10 | json: body, | 9 | json: body, |
11 | httpSignature, | 10 | httpSignature, |
12 | headers | 11 | headers |
13 | } | 12 | } |
14 | 13 | ||
15 | return doRequest(options) | 14 | return doRequest(url, options) |
16 | } | 15 | } |
17 | 16 | ||
18 | async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) { | 17 | async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) { |
19 | const follow = { | 18 | const follow = { |
20 | type: 'Follow', | 19 | type: 'Follow', |
21 | id: by.url + '/toto', | 20 | id: by.url + '/' + new Date().getTime(), |
22 | actor: by.url, | 21 | actor: by.url, |
23 | object: to.url | 22 | object: to.url |
24 | } | 23 | } |
@@ -34,7 +33,7 @@ async function makeFollowRequest (to: { url: string }, by: { url: string, privat | |||
34 | } | 33 | } |
35 | const headers = buildGlobalHeaders(body) | 34 | const headers = buildGlobalHeaders(body) |
36 | 35 | ||
37 | return makePOSTAPRequest(to.url, body, httpSignature, headers) | 36 | return makePOSTAPRequest(to.url + '/inbox', body, httpSignature, headers) |
38 | } | 37 | } |
39 | 38 | ||
40 | export { | 39 | export { |
diff --git a/shared/extra-utils/requests/requests.ts b/shared/extra-utils/requests/requests.ts index 3e773ee03..8b5cddf4a 100644 --- a/shared/extra-utils/requests/requests.ts +++ b/shared/extra-utils/requests/requests.ts | |||
@@ -152,11 +152,12 @@ function makeHTMLRequest (url: string, path: string) { | |||
152 | .expect(HttpStatusCode.OK_200) | 152 | .expect(HttpStatusCode.OK_200) |
153 | } | 153 | } |
154 | 154 | ||
155 | function updateAvatarRequest (options: { | 155 | function updateImageRequest (options: { |
156 | url: string | 156 | url: string |
157 | path: string | 157 | path: string |
158 | accessToken: string | 158 | accessToken: string |
159 | fixture: string | 159 | fixture: string |
160 | fieldname: string | ||
160 | }) { | 161 | }) { |
161 | let filePath = '' | 162 | let filePath = '' |
162 | if (isAbsolute(options.fixture)) { | 163 | if (isAbsolute(options.fixture)) { |
@@ -170,7 +171,7 @@ function updateAvatarRequest (options: { | |||
170 | path: options.path, | 171 | path: options.path, |
171 | token: options.accessToken, | 172 | token: options.accessToken, |
172 | fields: {}, | 173 | fields: {}, |
173 | attaches: { avatarfile: filePath }, | 174 | attaches: { [options.fieldname]: filePath }, |
174 | statusCodeExpected: HttpStatusCode.OK_200 | 175 | statusCodeExpected: HttpStatusCode.OK_200 |
175 | }) | 176 | }) |
176 | } | 177 | } |
@@ -191,5 +192,5 @@ export { | |||
191 | makePutBodyRequest, | 192 | makePutBodyRequest, |
192 | makeDeleteRequest, | 193 | makeDeleteRequest, |
193 | makeRawRequest, | 194 | makeRawRequest, |
194 | updateAvatarRequest | 195 | updateImageRequest |
195 | } | 196 | } |
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts index 08d05ef36..779a3cc36 100644 --- a/shared/extra-utils/server/servers.ts +++ b/shared/extra-utils/server/servers.ts | |||
@@ -37,6 +37,7 @@ interface ServerInfo { | |||
37 | customConfigFile?: string | 37 | customConfigFile?: string |
38 | 38 | ||
39 | accessToken?: string | 39 | accessToken?: string |
40 | refreshToken?: string | ||
40 | videoChannel?: VideoChannel | 41 | videoChannel?: VideoChannel |
41 | 42 | ||
42 | video?: { | 43 | video?: { |
diff --git a/shared/extra-utils/users/user-notifications.ts b/shared/extra-utils/users/user-notifications.ts index 467a3d959..249e82925 100644 --- a/shared/extra-utils/users/user-notifications.ts +++ b/shared/extra-utils/users/user-notifications.ts | |||
@@ -2,7 +2,8 @@ | |||
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { inspect } from 'util' | 4 | import { inspect } from 'util' |
5 | import { AbuseState } from '@shared/models' | 5 | import { AbuseState, PluginType } from '@shared/models' |
6 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
6 | import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users' | 7 | import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users' |
7 | import { MockSmtpServer } from '../miscs/email' | 8 | import { MockSmtpServer } from '../miscs/email' |
8 | import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests' | 9 | import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests' |
@@ -11,7 +12,6 @@ import { flushAndRunMultipleServers, ServerInfo } from '../server/servers' | |||
11 | import { getUserNotificationSocket } from '../socket/socket-io' | 12 | import { getUserNotificationSocket } from '../socket/socket-io' |
12 | import { setAccessTokensToServers, userLogin } from './login' | 13 | import { setAccessTokensToServers, userLogin } from './login' |
13 | import { createUser, getMyUserInformation } from './users' | 14 | import { createUser, getMyUserInformation } from './users' |
14 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
15 | 15 | ||
16 | function updateMyNotificationSettings ( | 16 | function updateMyNotificationSettings ( |
17 | url: string, | 17 | url: string, |
@@ -629,7 +629,59 @@ async function checkNewBlacklistOnMyVideo ( | |||
629 | await checkNotification(base, notificationChecker, emailNotificationFinder, 'presence') | 629 | await checkNotification(base, notificationChecker, emailNotificationFinder, 'presence') |
630 | } | 630 | } |
631 | 631 | ||
632 | function getAllNotificationsSettings () { | 632 | async function checkNewPeerTubeVersion (base: CheckerBaseParams, latestVersion: string, type: CheckerType) { |
633 | const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION | ||
634 | |||
635 | function notificationChecker (notification: UserNotification, type: CheckerType) { | ||
636 | if (type === 'presence') { | ||
637 | expect(notification).to.not.be.undefined | ||
638 | expect(notification.type).to.equal(notificationType) | ||
639 | |||
640 | expect(notification.peertube).to.exist | ||
641 | expect(notification.peertube.latestVersion).to.equal(latestVersion) | ||
642 | } else { | ||
643 | expect(notification).to.satisfy((n: UserNotification) => { | ||
644 | return n === undefined || n.peertube === undefined || n.peertube.latestVersion !== latestVersion | ||
645 | }) | ||
646 | } | ||
647 | } | ||
648 | |||
649 | function emailNotificationFinder (email: object) { | ||
650 | const text = email['text'] | ||
651 | |||
652 | return text.includes(latestVersion) | ||
653 | } | ||
654 | |||
655 | await checkNotification(base, notificationChecker, emailNotificationFinder, type) | ||
656 | } | ||
657 | |||
658 | async function checkNewPluginVersion (base: CheckerBaseParams, pluginType: PluginType, pluginName: string, type: CheckerType) { | ||
659 | const notificationType = UserNotificationType.NEW_PLUGIN_VERSION | ||
660 | |||
661 | function notificationChecker (notification: UserNotification, type: CheckerType) { | ||
662 | if (type === 'presence') { | ||
663 | expect(notification).to.not.be.undefined | ||
664 | expect(notification.type).to.equal(notificationType) | ||
665 | |||
666 | expect(notification.plugin.name).to.equal(pluginName) | ||
667 | expect(notification.plugin.type).to.equal(pluginType) | ||
668 | } else { | ||
669 | expect(notification).to.satisfy((n: UserNotification) => { | ||
670 | return n === undefined || n.plugin === undefined || n.plugin.name !== pluginName | ||
671 | }) | ||
672 | } | ||
673 | } | ||
674 | |||
675 | function emailNotificationFinder (email: object) { | ||
676 | const text = email['text'] | ||
677 | |||
678 | return text.includes(pluginName) | ||
679 | } | ||
680 | |||
681 | await checkNotification(base, notificationChecker, emailNotificationFinder, type) | ||
682 | } | ||
683 | |||
684 | function getAllNotificationsSettings (): UserNotificationSetting { | ||
633 | return { | 685 | return { |
634 | newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 686 | newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
635 | newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 687 | newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
@@ -644,11 +696,13 @@ function getAllNotificationsSettings () { | |||
644 | newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 696 | newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
645 | abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 697 | abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
646 | abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 698 | abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
647 | autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL | 699 | autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
648 | } as UserNotificationSetting | 700 | newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
701 | newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL | ||
702 | } | ||
649 | } | 703 | } |
650 | 704 | ||
651 | async function prepareNotificationsTest (serversCount = 3) { | 705 | async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) { |
652 | const userNotifications: UserNotification[] = [] | 706 | const userNotifications: UserNotification[] = [] |
653 | const adminNotifications: UserNotification[] = [] | 707 | const adminNotifications: UserNotification[] = [] |
654 | const adminNotificationsServer2: UserNotification[] = [] | 708 | const adminNotificationsServer2: UserNotification[] = [] |
@@ -665,7 +719,7 @@ async function prepareNotificationsTest (serversCount = 3) { | |||
665 | limit: 20 | 719 | limit: 20 |
666 | } | 720 | } |
667 | } | 721 | } |
668 | const servers = await flushAndRunMultipleServers(serversCount, overrideConfig) | 722 | const servers = await flushAndRunMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg)) |
669 | 723 | ||
670 | await setAccessTokensToServers(servers) | 724 | await setAccessTokensToServers(servers) |
671 | 725 | ||
@@ -749,5 +803,7 @@ export { | |||
749 | checkNewInstanceFollower, | 803 | checkNewInstanceFollower, |
750 | prepareNotificationsTest, | 804 | prepareNotificationsTest, |
751 | checkNewCommentAbuseForModerators, | 805 | checkNewCommentAbuseForModerators, |
752 | checkNewAccountAbuseForModerators | 806 | checkNewAccountAbuseForModerators, |
807 | checkNewPeerTubeVersion, | ||
808 | checkNewPluginVersion | ||
753 | } | 809 | } |
diff --git a/shared/extra-utils/users/users.ts b/shared/extra-utils/users/users.ts index db532dbb0..6040dd9c0 100644 --- a/shared/extra-utils/users/users.ts +++ b/shared/extra-utils/users/users.ts | |||
@@ -4,7 +4,7 @@ import { UserUpdateMe } from '../../models/users' | |||
4 | import { UserAdminFlag } from '../../models/users/user-flag.model' | 4 | import { UserAdminFlag } from '../../models/users/user-flag.model' |
5 | import { UserRegister } from '../../models/users/user-register.model' | 5 | import { UserRegister } from '../../models/users/user-register.model' |
6 | import { UserRole } from '../../models/users/user-role' | 6 | import { UserRole } from '../../models/users/user-role' |
7 | import { makeGetRequest, makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests' | 7 | import { makeGetRequest, makePostBodyRequest, makePutBodyRequest, updateImageRequest } from '../requests/requests' |
8 | import { ServerInfo } from '../server/servers' | 8 | import { ServerInfo } from '../server/servers' |
9 | import { userLogin } from './login' | 9 | import { userLogin } from './login' |
10 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 10 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
@@ -275,7 +275,7 @@ function updateMyAvatar (options: { | |||
275 | }) { | 275 | }) { |
276 | const path = '/api/v1/users/me/avatar/pick' | 276 | const path = '/api/v1/users/me/avatar/pick' |
277 | 277 | ||
278 | return updateAvatarRequest(Object.assign(options, { path })) | 278 | return updateImageRequest({ ...options, path, fieldname: 'avatarfile' }) |
279 | } | 279 | } |
280 | 280 | ||
281 | function updateUser (options: { | 281 | function updateUser (options: { |
diff --git a/shared/extra-utils/videos/video-channels.ts b/shared/extra-utils/videos/video-channels.ts index 3ff445c2a..d0dfb5856 100644 --- a/shared/extra-utils/videos/video-channels.ts +++ b/shared/extra-utils/videos/video-channels.ts | |||
@@ -3,7 +3,7 @@ | |||
3 | import * as request from 'supertest' | 3 | import * as request from 'supertest' |
4 | import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model' | 4 | import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model' |
5 | import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model' | 5 | import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model' |
6 | import { makeGetRequest, updateAvatarRequest } from '../requests/requests' | 6 | import { makeDeleteRequest, makeGetRequest, updateImageRequest } from '../requests/requests' |
7 | import { ServerInfo } from '../server/servers' | 7 | import { ServerInfo } from '../server/servers' |
8 | import { User } from '../../models/users/user.model' | 8 | import { User } from '../../models/users/user.model' |
9 | import { getMyUserInformation } from '../users/users' | 9 | import { getMyUserInformation } from '../users/users' |
@@ -129,16 +129,32 @@ function getVideoChannel (url: string, channelName: string) { | |||
129 | .expect('Content-Type', /json/) | 129 | .expect('Content-Type', /json/) |
130 | } | 130 | } |
131 | 131 | ||
132 | function updateVideoChannelAvatar (options: { | 132 | function updateVideoChannelImage (options: { |
133 | url: string | 133 | url: string |
134 | accessToken: string | 134 | accessToken: string |
135 | fixture: string | 135 | fixture: string |
136 | videoChannelName: string | number | 136 | videoChannelName: string | number |
137 | type: 'avatar' | 'banner' | ||
137 | }) { | 138 | }) { |
139 | const path = `/api/v1/video-channels/${options.videoChannelName}/${options.type}/pick` | ||
138 | 140 | ||
139 | const path = '/api/v1/video-channels/' + options.videoChannelName + '/avatar/pick' | 141 | return updateImageRequest({ ...options, path, fieldname: options.type + 'file' }) |
142 | } | ||
143 | |||
144 | function deleteVideoChannelImage (options: { | ||
145 | url: string | ||
146 | accessToken: string | ||
147 | videoChannelName: string | number | ||
148 | type: 'avatar' | 'banner' | ||
149 | }) { | ||
150 | const path = `/api/v1/video-channels/${options.videoChannelName}/${options.type}` | ||
140 | 151 | ||
141 | return updateAvatarRequest(Object.assign(options, { path })) | 152 | return makeDeleteRequest({ |
153 | url: options.url, | ||
154 | token: options.accessToken, | ||
155 | path, | ||
156 | statusCodeExpected: 204 | ||
157 | }) | ||
142 | } | 158 | } |
143 | 159 | ||
144 | function setDefaultVideoChannel (servers: ServerInfo[]) { | 160 | function setDefaultVideoChannel (servers: ServerInfo[]) { |
@@ -157,12 +173,13 @@ function setDefaultVideoChannel (servers: ServerInfo[]) { | |||
157 | // --------------------------------------------------------------------------- | 173 | // --------------------------------------------------------------------------- |
158 | 174 | ||
159 | export { | 175 | export { |
160 | updateVideoChannelAvatar, | 176 | updateVideoChannelImage, |
161 | getVideoChannelsList, | 177 | getVideoChannelsList, |
162 | getAccountVideoChannelsList, | 178 | getAccountVideoChannelsList, |
163 | addVideoChannel, | 179 | addVideoChannel, |
164 | updateVideoChannel, | 180 | updateVideoChannel, |
165 | deleteVideoChannel, | 181 | deleteVideoChannel, |
166 | getVideoChannel, | 182 | getVideoChannel, |
167 | setDefaultVideoChannel | 183 | setDefaultVideoChannel, |
184 | deleteVideoChannelImage | ||
168 | } | 185 | } |
diff --git a/shared/models/activitypub/activitypub-actor.ts b/shared/models/activitypub/activitypub-actor.ts index f022f3d02..c59be3f3b 100644 --- a/shared/models/activitypub/activitypub-actor.ts +++ b/shared/models/activitypub/activitypub-actor.ts | |||
@@ -27,5 +27,6 @@ export interface ActivityPubActor { | |||
27 | publicKeyPem: string | 27 | publicKeyPem: string |
28 | } | 28 | } |
29 | 29 | ||
30 | icon: ActivityIconObject | 30 | icon?: ActivityIconObject |
31 | image?: ActivityIconObject | ||
31 | } | 32 | } |
diff --git a/shared/models/activitypub/objects/common-objects.ts b/shared/models/activitypub/objects/common-objects.ts index 76f0e3bcf..43d7f7f74 100644 --- a/shared/models/activitypub/objects/common-objects.ts +++ b/shared/models/activitypub/objects/common-objects.ts | |||
@@ -9,7 +9,7 @@ export interface ActivityIdentifierObject { | |||
9 | export interface ActivityIconObject { | 9 | export interface ActivityIconObject { |
10 | type: 'Image' | 10 | type: 'Image' |
11 | url: string | 11 | url: string |
12 | mediaType: 'image/jpeg' | 'image/png' | 12 | mediaType: string |
13 | width?: number | 13 | width?: number |
14 | height?: number | 14 | height?: number |
15 | } | 15 | } |
diff --git a/shared/models/actors/account.model.ts b/shared/models/actors/account.model.ts index 2ff4b9f5e..120dec271 100644 --- a/shared/models/actors/account.model.ts +++ b/shared/models/actors/account.model.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { ActorImage } from './actor-image.model' | ||
1 | import { Actor } from './actor.model' | 2 | import { Actor } from './actor.model' |
2 | import { Avatar } from '../avatars' | ||
3 | 3 | ||
4 | export interface Account extends Actor { | 4 | export interface Account extends Actor { |
5 | displayName: string | 5 | displayName: string |
@@ -14,5 +14,5 @@ export interface AccountSummary { | |||
14 | displayName: string | 14 | displayName: string |
15 | url: string | 15 | url: string |
16 | host: string | 16 | host: string |
17 | avatar?: Avatar | 17 | avatar?: ActorImage |
18 | } | 18 | } |
diff --git a/shared/models/avatars/avatar.model.ts b/shared/models/actors/actor-image.model.ts index f7fa16f49..ad5eab627 100644 --- a/shared/models/avatars/avatar.model.ts +++ b/shared/models/actors/actor-image.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | export interface Avatar { | 1 | export interface ActorImage { |
2 | path: string | 2 | path: string |
3 | 3 | ||
4 | url?: string | 4 | url?: string |
diff --git a/shared/models/actors/actor-image.type.ts b/shared/models/actors/actor-image.type.ts new file mode 100644 index 000000000..ac8eb6bf2 --- /dev/null +++ b/shared/models/actors/actor-image.type.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | export const enum ActorImageType { | ||
2 | AVATAR = 1, | ||
3 | BANNER = 2 | ||
4 | } | ||
diff --git a/shared/models/actors/actor.model.ts b/shared/models/actors/actor.model.ts index 1dbf5f638..7d9f35b10 100644 --- a/shared/models/actors/actor.model.ts +++ b/shared/models/actors/actor.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { Avatar } from '../avatars/avatar.model' | 1 | import { ActorImage } from './actor-image.model' |
2 | 2 | ||
3 | export interface Actor { | 3 | export interface Actor { |
4 | id: number | 4 | id: number |
@@ -9,5 +9,5 @@ export interface Actor { | |||
9 | followersCount: number | 9 | followersCount: number |
10 | createdAt: Date | string | 10 | createdAt: Date | string |
11 | updatedAt: Date | string | 11 | updatedAt: Date | string |
12 | avatar?: Avatar | 12 | avatar?: ActorImage |
13 | } | 13 | } |
diff --git a/shared/models/actors/index.ts b/shared/models/actors/index.ts index c7a92399d..156f83248 100644 --- a/shared/models/actors/index.ts +++ b/shared/models/actors/index.ts | |||
@@ -1,3 +1,5 @@ | |||
1 | export * from './account.model' | 1 | export * from './account.model' |
2 | export * from './actor-image.model' | ||
3 | export * from './actor-image.type' | ||
2 | export * from './actor.model' | 4 | export * from './actor.model' |
3 | export * from './follow.model' | 5 | export * from './follow.model' |
diff --git a/shared/models/avatars/index.ts b/shared/models/avatars/index.ts deleted file mode 100644 index 65e8e0882..000000000 --- a/shared/models/avatars/index.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export * from './avatar.model' | ||
diff --git a/shared/models/index.ts b/shared/models/index.ts index 2214f7ca3..dff5fdf0e 100644 --- a/shared/models/index.ts +++ b/shared/models/index.ts | |||
@@ -1,12 +1,12 @@ | |||
1 | export * from './activitypub' | 1 | export * from './activitypub' |
2 | export * from './actors' | 2 | export * from './actors' |
3 | export * from './avatars' | ||
4 | export * from './moderation' | 3 | export * from './moderation' |
5 | export * from './bulk' | 4 | export * from './bulk' |
6 | export * from './redundancy' | 5 | export * from './redundancy' |
7 | export * from './users' | 6 | export * from './users' |
8 | export * from './videos' | 7 | export * from './videos' |
9 | export * from './feeds' | 8 | export * from './feeds' |
9 | export * from './joinpeertube' | ||
10 | export * from './overviews' | 10 | export * from './overviews' |
11 | export * from './plugins' | 11 | export * from './plugins' |
12 | export * from './search' | 12 | export * from './search' |
diff --git a/shared/models/joinpeertube/index.ts b/shared/models/joinpeertube/index.ts new file mode 100644 index 000000000..9681c35ad --- /dev/null +++ b/shared/models/joinpeertube/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './versions.model' | |||
diff --git a/shared/models/joinpeertube/versions.model.ts b/shared/models/joinpeertube/versions.model.ts new file mode 100644 index 000000000..60a769150 --- /dev/null +++ b/shared/models/joinpeertube/versions.model.ts | |||
@@ -0,0 +1,5 @@ | |||
1 | export interface JoinPeerTubeVersions { | ||
2 | peertube: { | ||
3 | latestVersion: string | ||
4 | } | ||
5 | } | ||
diff --git a/shared/models/plugins/client-hook.model.ts b/shared/models/plugins/client-hook.model.ts index 7b7144676..f8ca32771 100644 --- a/shared/models/plugins/client-hook.model.ts +++ b/shared/models/plugins/client-hook.model.ts | |||
@@ -85,8 +85,27 @@ export const clientActionHookObject = { | |||
85 | // Fired when the registration page is being initialized | 85 | // Fired when the registration page is being initialized |
86 | 'action:signup.register.init': true, | 86 | 'action:signup.register.init': true, |
87 | 87 | ||
88 | // Fired when the video upload page is being initalized | ||
89 | 'action:video-upload.init': true, | ||
90 | // Fired when the video import by URL page is being initalized | ||
91 | 'action:video-url-import.init': true, | ||
92 | // Fired when the video import by torrent/magnet URI page is being initalized | ||
93 | 'action:video-torrent-import.init': true, | ||
94 | // Fired when the "Go Live" page is being initalized | ||
95 | 'action:go-live.init': true, | ||
96 | |||
97 | // Fired when the user explicitely logged in/logged out | ||
98 | 'action:auth-user.logged-in': true, | ||
99 | 'action:auth-user.logged-out': true, | ||
100 | // Fired when the application loaded user information (using tokens from the local storage or after a successful login) | ||
101 | 'action:auth-user.information-loaded': true, | ||
102 | |||
103 | // Fired when the modal to download a video/caption is shown | ||
104 | 'action:modal.video-download.shown': true, | ||
105 | |||
88 | // ####### Embed hooks ####### | 106 | // ####### Embed hooks ####### |
89 | // In embed scope, peertube helpers are not available | 107 | // /!\ In embed scope, peertube helpers are not available |
108 | // ########################### | ||
90 | 109 | ||
91 | // Fired when the embed loaded the player | 110 | // Fired when the embed loaded the player |
92 | 'action:embed.player.loaded': true | 111 | 'action:embed.player.loaded': true |
diff --git a/shared/models/plugins/server-hook.model.ts b/shared/models/plugins/server-hook.model.ts index 082b4b591..88277af5a 100644 --- a/shared/models/plugins/server-hook.model.ts +++ b/shared/models/plugins/server-hook.model.ts | |||
@@ -18,6 +18,16 @@ export const serverFilterHookObject = { | |||
18 | 'filter:api.user.me.videos.list.params': true, | 18 | 'filter:api.user.me.videos.list.params': true, |
19 | 'filter:api.user.me.videos.list.result': true, | 19 | 'filter:api.user.me.videos.list.result': true, |
20 | 20 | ||
21 | // Filter params/results to search videos/channels in the DB or on the remote index | ||
22 | 'filter:api.search.videos.local.list.params': true, | ||
23 | 'filter:api.search.videos.local.list.result': true, | ||
24 | 'filter:api.search.videos.index.list.params': true, | ||
25 | 'filter:api.search.videos.index.list.result': true, | ||
26 | 'filter:api.search.video-channels.local.list.params': true, | ||
27 | 'filter:api.search.video-channels.local.list.result': true, | ||
28 | 'filter:api.search.video-channels.index.list.params': true, | ||
29 | 'filter:api.search.video-channels.index.list.result': true, | ||
30 | |||
21 | // Filter the result of the get function | 31 | // Filter the result of the get function |
22 | // Used to get detailed video information (video watch page for example) | 32 | // Used to get detailed video information (video watch page for example) |
23 | 'filter:api.video.get.result': true, | 33 | 'filter:api.video.get.result': true, |
@@ -50,7 +60,15 @@ export const serverFilterHookObject = { | |||
50 | 'filter:video.auto-blacklist.result': true, | 60 | 'filter:video.auto-blacklist.result': true, |
51 | 61 | ||
52 | // Filter result used to check if a user can register on the instance | 62 | // Filter result used to check if a user can register on the instance |
53 | 'filter:api.user.signup.allowed.result': true | 63 | 'filter:api.user.signup.allowed.result': true, |
64 | |||
65 | // Filter result used to check if video/torrent download is allowed | ||
66 | 'filter:api.download.video.allowed.result': true, | ||
67 | 'filter:api.download.torrent.allowed.result': true, | ||
68 | |||
69 | // Filter result to check if the embed is allowed for a particular request | ||
70 | 'filter:html.embed.video.allowed.result': true, | ||
71 | 'filter:html.embed.video-playlist.allowed.result': true | ||
54 | } | 72 | } |
55 | 73 | ||
56 | export type ServerFilterHookName = keyof typeof serverFilterHookObject | 74 | export type ServerFilterHookName = keyof typeof serverFilterHookObject |
diff --git a/shared/models/server/emailer.model.ts b/shared/models/server/emailer.model.ts index 069ef0bab..39512d306 100644 --- a/shared/models/server/emailer.model.ts +++ b/shared/models/server/emailer.model.ts | |||
@@ -1,12 +1,49 @@ | |||
1 | export type SendEmailOptions = { | 1 | type From = string | { name?: string, address: string } |
2 | to: string[] | ||
3 | 2 | ||
4 | template?: string | 3 | interface Base extends Partial<SendEmailDefaultMessageOptions> { |
4 | to: string[] | string | ||
5 | } | ||
6 | |||
7 | interface MailTemplate extends Base { | ||
8 | template: string | ||
5 | locals?: { [key: string]: any } | 9 | locals?: { [key: string]: any } |
10 | text?: undefined | ||
11 | } | ||
12 | |||
13 | interface MailText extends Base { | ||
14 | text: string | ||
6 | 15 | ||
7 | // override defaults | 16 | locals?: Partial<SendEmailDefaultLocalsOptions> & { |
8 | subject?: string | 17 | title?: string |
9 | text?: string | 18 | action?: { |
10 | from?: string | { name?: string, address: string } | 19 | url: string |
11 | replyTo?: string | 20 | text: string |
21 | } | ||
22 | } | ||
12 | } | 23 | } |
24 | |||
25 | interface SendEmailDefaultLocalsOptions { | ||
26 | instanceName: string | ||
27 | text: string | ||
28 | subject: string | ||
29 | } | ||
30 | |||
31 | interface SendEmailDefaultMessageOptions { | ||
32 | to: string[] | string | ||
33 | from: From | ||
34 | subject: string | ||
35 | replyTo: string | ||
36 | } | ||
37 | |||
38 | export type SendEmailDefaultOptions = { | ||
39 | template: 'common' | ||
40 | |||
41 | message: SendEmailDefaultMessageOptions | ||
42 | |||
43 | locals: SendEmailDefaultLocalsOptions & { | ||
44 | WEBSERVER: any | ||
45 | EMAIL: any | ||
46 | } | ||
47 | } | ||
48 | |||
49 | export type SendEmailOptions = MailTemplate | MailText | ||
diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts index 83ef84457..e4acfee8d 100644 --- a/shared/models/server/job.model.ts +++ b/shared/models/server/job.model.ts | |||
@@ -59,7 +59,7 @@ export type ActivitypubHttpFetcherPayload = { | |||
59 | export type ActivitypubHttpUnicastPayload = { | 59 | export type ActivitypubHttpUnicastPayload = { |
60 | uri: string | 60 | uri: string |
61 | signatureActorId?: number | 61 | signatureActorId?: number |
62 | body: any | 62 | body: object |
63 | contextType?: ContextType | 63 | contextType?: ContextType |
64 | } | 64 | } |
65 | 65 | ||
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts index efde4ad9d..85d84af44 100644 --- a/shared/models/server/server-config.model.ts +++ b/shared/models/server/server-config.model.ts | |||
@@ -151,6 +151,15 @@ export interface ServerConfig { | |||
151 | } | 151 | } |
152 | } | 152 | } |
153 | 153 | ||
154 | banner: { | ||
155 | file: { | ||
156 | size: { | ||
157 | max: number | ||
158 | } | ||
159 | extensions: string[] | ||
160 | } | ||
161 | } | ||
162 | |||
154 | video: { | 163 | video: { |
155 | image: { | 164 | image: { |
156 | size: { | 165 | size: { |
diff --git a/shared/models/users/user-notification-setting.model.ts b/shared/models/users/user-notification-setting.model.ts index 473148062..977e6b985 100644 --- a/shared/models/users/user-notification-setting.model.ts +++ b/shared/models/users/user-notification-setting.model.ts | |||
@@ -24,4 +24,7 @@ export interface UserNotificationSetting { | |||
24 | 24 | ||
25 | abuseStateChange: UserNotificationSettingValue | 25 | abuseStateChange: UserNotificationSettingValue |
26 | abuseNewMessage: UserNotificationSettingValue | 26 | abuseNewMessage: UserNotificationSettingValue |
27 | |||
28 | newPeerTubeVersion: UserNotificationSettingValue | ||
29 | newPluginVersion: UserNotificationSettingValue | ||
27 | } | 30 | } |
diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts index e2f2234e4..8b33e3fbd 100644 --- a/shared/models/users/user-notification.model.ts +++ b/shared/models/users/user-notification.model.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { FollowState } from '../actors' | 1 | import { FollowState } from '../actors' |
2 | import { AbuseState } from '../moderation' | 2 | import { AbuseState } from '../moderation' |
3 | import { PluginType } from '../plugins' | ||
3 | 4 | ||
4 | export enum UserNotificationType { | 5 | export const enum UserNotificationType { |
5 | NEW_VIDEO_FROM_SUBSCRIPTION = 1, | 6 | NEW_VIDEO_FROM_SUBSCRIPTION = 1, |
6 | NEW_COMMENT_ON_MY_VIDEO = 2, | 7 | NEW_COMMENT_ON_MY_VIDEO = 2, |
7 | NEW_ABUSE_FOR_MODERATORS = 3, | 8 | NEW_ABUSE_FOR_MODERATORS = 3, |
@@ -26,7 +27,10 @@ export enum UserNotificationType { | |||
26 | 27 | ||
27 | ABUSE_STATE_CHANGE = 15, | 28 | ABUSE_STATE_CHANGE = 15, |
28 | 29 | ||
29 | ABUSE_NEW_MESSAGE = 16 | 30 | ABUSE_NEW_MESSAGE = 16, |
31 | |||
32 | NEW_PLUGIN_VERSION = 17, | ||
33 | NEW_PEERTUBE_VERSION = 18 | ||
30 | } | 34 | } |
31 | 35 | ||
32 | export interface VideoInfo { | 36 | export interface VideoInfo { |
@@ -108,6 +112,16 @@ export interface UserNotification { | |||
108 | } | 112 | } |
109 | } | 113 | } |
110 | 114 | ||
115 | plugin?: { | ||
116 | name: string | ||
117 | type: PluginType | ||
118 | latestVersion: string | ||
119 | } | ||
120 | |||
121 | peertube?: { | ||
122 | latestVersion: string | ||
123 | } | ||
124 | |||
111 | createdAt: string | 125 | createdAt: string |
112 | updatedAt: string | 126 | updatedAt: string |
113 | } | 127 | } |
diff --git a/shared/models/videos/channel/video-channel.model.ts b/shared/models/videos/channel/video-channel.model.ts index 32829e92a..56517972d 100644 --- a/shared/models/videos/channel/video-channel.model.ts +++ b/shared/models/videos/channel/video-channel.model.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { Actor } from '../../actors/actor.model' | 1 | import { Actor } from '../../actors/actor.model' |
2 | import { Account } from '../../actors/index' | 2 | import { Account, ActorImage } from '../../actors' |
3 | import { Avatar } from '../../avatars' | ||
4 | 3 | ||
5 | export type ViewsPerDate = { | 4 | export type ViewsPerDate = { |
6 | date: Date | 5 | date: Date |
@@ -16,6 +15,8 @@ export interface VideoChannel extends Actor { | |||
16 | 15 | ||
17 | videosCount?: number | 16 | videosCount?: number |
18 | viewsPerDay?: ViewsPerDate[] // chronologically ordered | 17 | viewsPerDay?: ViewsPerDate[] // chronologically ordered |
18 | |||
19 | banner?: ActorImage | ||
19 | } | 20 | } |
20 | 21 | ||
21 | export interface VideoChannelSummary { | 22 | export interface VideoChannelSummary { |
@@ -24,5 +25,5 @@ export interface VideoChannelSummary { | |||
24 | displayName: string | 25 | displayName: string |
25 | url: string | 26 | url: string |
26 | host: string | 27 | host: string |
27 | avatar?: Avatar | 28 | avatar?: ActorImage |
28 | } | 29 | } |
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index a47654f69..373b17ddf 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml | |||
@@ -974,7 +974,10 @@ paths: | |||
974 | content: | 974 | content: |
975 | application/json: | 975 | application/json: |
976 | schema: | 976 | schema: |
977 | $ref: '#/components/schemas/Avatar' | 977 | type: object |
978 | properties: | ||
979 | avatar: | ||
980 | $ref: '#/components/schemas/ActorImage' | ||
978 | '413': | 981 | '413': |
979 | description: image file too large | 982 | description: image file too large |
980 | headers: | 983 | headers: |
@@ -996,6 +999,17 @@ paths: | |||
996 | encoding: | 999 | encoding: |
997 | avatarfile: | 1000 | avatarfile: |
998 | contentType: image/png, image/jpeg | 1001 | contentType: image/png, image/jpeg |
1002 | /users/me/avatar: | ||
1003 | delete: | ||
1004 | summary: Delete my avatar | ||
1005 | security: | ||
1006 | - OAuth2: [] | ||
1007 | tags: | ||
1008 | - My User | ||
1009 | responses: | ||
1010 | '204': | ||
1011 | description: successful operation | ||
1012 | |||
999 | /videos/ownership: | 1013 | /videos/ownership: |
1000 | get: | 1014 | get: |
1001 | summary: List video ownership changes | 1015 | summary: List video ownership changes |
@@ -2185,6 +2199,112 @@ paths: | |||
2185 | application/json: | 2199 | application/json: |
2186 | schema: | 2200 | schema: |
2187 | $ref: '#/components/schemas/VideoListResponse' | 2201 | $ref: '#/components/schemas/VideoListResponse' |
2202 | '/video-channels/{channelHandle}/avatar/pick': | ||
2203 | post: | ||
2204 | summary: Update channel avatar | ||
2205 | security: | ||
2206 | - OAuth2: [] | ||
2207 | tags: | ||
2208 | - Video Channels | ||
2209 | parameters: | ||
2210 | - $ref: '#/components/parameters/channelHandle' | ||
2211 | responses: | ||
2212 | '200': | ||
2213 | description: successful operation | ||
2214 | content: | ||
2215 | application/json: | ||
2216 | schema: | ||
2217 | type: object | ||
2218 | properties: | ||
2219 | avatar: | ||
2220 | $ref: '#/components/schemas/ActorImage' | ||
2221 | '413': | ||
2222 | description: image file too large | ||
2223 | headers: | ||
2224 | X-File-Maximum-Size: | ||
2225 | schema: | ||
2226 | type: string | ||
2227 | format: Nginx size | ||
2228 | description: Maximum file size for the avatar | ||
2229 | requestBody: | ||
2230 | content: | ||
2231 | multipart/form-data: | ||
2232 | schema: | ||
2233 | type: object | ||
2234 | properties: | ||
2235 | avatarfile: | ||
2236 | description: The file to upload. | ||
2237 | type: string | ||
2238 | format: binary | ||
2239 | encoding: | ||
2240 | avatarfile: | ||
2241 | contentType: image/png, image/jpeg | ||
2242 | '/video-channels/{channelHandle}/avatar': | ||
2243 | delete: | ||
2244 | summary: Delete channel avatar | ||
2245 | security: | ||
2246 | - OAuth2: [] | ||
2247 | tags: | ||
2248 | - Video Channels | ||
2249 | parameters: | ||
2250 | - $ref: '#/components/parameters/channelHandle' | ||
2251 | responses: | ||
2252 | '204': | ||
2253 | description: successful operation | ||
2254 | |||
2255 | |||
2256 | '/video-channels/{channelHandle}/banner/pick': | ||
2257 | post: | ||
2258 | summary: Update channel banner | ||
2259 | security: | ||
2260 | - OAuth2: [] | ||
2261 | tags: | ||
2262 | - Video Channels | ||
2263 | parameters: | ||
2264 | - $ref: '#/components/parameters/channelHandle' | ||
2265 | responses: | ||
2266 | '200': | ||
2267 | description: successful operation | ||
2268 | content: | ||
2269 | application/json: | ||
2270 | schema: | ||
2271 | type: object | ||
2272 | properties: | ||
2273 | banner: | ||
2274 | $ref: '#/components/schemas/ActorImage' | ||
2275 | '413': | ||
2276 | description: image file too large | ||
2277 | headers: | ||
2278 | X-File-Maximum-Size: | ||
2279 | schema: | ||
2280 | type: string | ||
2281 | format: Nginx size | ||
2282 | description: Maximum file size for the banner | ||
2283 | requestBody: | ||
2284 | content: | ||
2285 | multipart/form-data: | ||
2286 | schema: | ||
2287 | type: object | ||
2288 | properties: | ||
2289 | bannerfile: | ||
2290 | description: The file to upload. | ||
2291 | type: string | ||
2292 | format: binary | ||
2293 | encoding: | ||
2294 | bannerfile: | ||
2295 | contentType: image/png, image/jpeg | ||
2296 | '/video-channels/{channelHandle}/banner': | ||
2297 | delete: | ||
2298 | summary: Delete channel banner | ||
2299 | security: | ||
2300 | - OAuth2: [] | ||
2301 | tags: | ||
2302 | - Video Channels | ||
2303 | parameters: | ||
2304 | - $ref: '#/components/parameters/channelHandle' | ||
2305 | responses: | ||
2306 | '204': | ||
2307 | description: successful operation | ||
2188 | 2308 | ||
2189 | /video-playlists/privacies: | 2309 | /video-playlists/privacies: |
2190 | get: | 2310 | get: |
@@ -3989,7 +4109,7 @@ components: | |||
3989 | avatar: | 4109 | avatar: |
3990 | nullable: true | 4110 | nullable: true |
3991 | allOf: | 4111 | allOf: |
3992 | - $ref: '#/components/schemas/Avatar' | 4112 | - $ref: '#/components/schemas/ActorImage' |
3993 | VideoChannelSummary: | 4113 | VideoChannelSummary: |
3994 | properties: | 4114 | properties: |
3995 | id: | 4115 | id: |
@@ -4007,7 +4127,7 @@ components: | |||
4007 | avatar: | 4127 | avatar: |
4008 | nullable: true | 4128 | nullable: true |
4009 | allOf: | 4129 | allOf: |
4010 | - $ref: '#/components/schemas/Avatar' | 4130 | - $ref: '#/components/schemas/ActorImage' |
4011 | PlaylistElement: | 4131 | PlaylistElement: |
4012 | properties: | 4132 | properties: |
4013 | position: | 4133 | position: |
@@ -4460,7 +4580,7 @@ components: | |||
4460 | $ref: '#/components/schemas/VideoConstantString' | 4580 | $ref: '#/components/schemas/VideoConstantString' |
4461 | captionPath: | 4581 | captionPath: |
4462 | type: string | 4582 | type: string |
4463 | Avatar: | 4583 | ActorImage: |
4464 | properties: | 4584 | properties: |
4465 | path: | 4585 | path: |
4466 | type: string | 4586 | type: string |
@@ -4512,7 +4632,7 @@ components: | |||
4512 | type: string | 4632 | type: string |
4513 | format: date-time | 4633 | format: date-time |
4514 | avatar: | 4634 | avatar: |
4515 | $ref: '#/components/schemas/Avatar' | 4635 | $ref: '#/components/schemas/ActorImage' |
4516 | Account: | 4636 | Account: |
4517 | allOf: | 4637 | allOf: |
4518 | - $ref: '#/components/schemas/Actor' | 4638 | - $ref: '#/components/schemas/Actor' |
@@ -5694,6 +5814,8 @@ components: | |||
5694 | description: User can stream multiple times in a permanent live | 5814 | description: User can stream multiple times in a permanent live |
5695 | type: boolean | 5815 | type: boolean |
5696 | 5816 | ||
5817 | |||
5818 | |||
5697 | callbacks: | 5819 | callbacks: |
5698 | searchIndex: | 5820 | searchIndex: |
5699 | 'https://search.example.org/api/v1/search/videos': | 5821 | 'https://search.example.org/api/v1/search/videos': |
diff --git a/support/doc/dependencies.md b/support/doc/dependencies.md index 0fdbdfc82..9666d72af 100644 --- a/support/doc/dependencies.md +++ b/support/doc/dependencies.md | |||
@@ -281,17 +281,34 @@ service nginx start | |||
281 | 281 | ||
282 | 1. Add the packages: | 282 | 1. Add the packages: |
283 | 283 | ||
284 | ```sh | ||
285 | brew install bash ffmpeg nginx postgresql openssl gcc make redis git yarn | ||
284 | ``` | 286 | ``` |
285 | brew install ffmpeg nginx postgresql openssl gcc make redis git yarn | 287 | |
288 | You may need to update your default version of bash. | ||
289 | |||
290 | **How to change your default shell** | ||
291 | |||
292 | ```sh | ||
293 | which -a bash # Check where bash is installed | ||
294 | bash --version # You need a version at least as recent as 4.0 | ||
295 | sudo vim /etc/shells # Add in this file : /usr/local/bin/bash | ||
296 | chsh -s /usr/local/bin/bash # To set the brew-installed bash as default bash | ||
286 | ``` | 297 | ``` |
287 | 298 | ||
299 | In a new shell, type `bash --version` to assert your changes took effect and | ||
300 | correctly modified your default bash version. | ||
301 | |||
288 | 2. Run the services: | 302 | 2. Run the services: |
289 | 303 | ||
290 | ``` | 304 | ```sh |
291 | brew services run postgresql | 305 | brew services run postgresql |
292 | brew services run redis | 306 | brew services run redis |
293 | ``` | 307 | ``` |
294 | 308 | ||
309 | On macOS, the `postgresql` user can be `_postgres` instead of `postgres`. | ||
310 | If `sudo -u postgres createuser -P peertube` gives you an error, you can try `sudo -u _postgres createuser -U peertube`. | ||
311 | |||
295 | ## Gentoo | 312 | ## Gentoo |
296 | 313 | ||
297 | 1. Add this to ``/etc/portage/sets/peertube``: | 314 | 1. Add this to ``/etc/portage/sets/peertube``: |
diff --git a/support/doc/development/release.md b/support/doc/development/release.md index 39c2c5608..5cd735eda 100644 --- a/support/doc/development/release.md +++ b/support/doc/development/release.md | |||
@@ -19,4 +19,4 @@ NODE_APP_INSTANCE=6 NODE_ENV=test npm run start | |||
19 | * Check the release is okay: https://github.com/Chocobozzz/PeerTube/releases | 19 | * Check the release is okay: https://github.com/Chocobozzz/PeerTube/releases |
20 | * Update https://peertube3.cpy.re and check it works correctly | 20 | * Update https://peertube3.cpy.re and check it works correctly |
21 | * Update all other instances and check it works correctly | 21 | * Update all other instances and check it works correctly |
22 | * Communicate | 22 | * After a couple of days, update https://joinpeertube.org/api/v1/versions.json |
diff --git a/support/doc/plugins/guide.md b/support/doc/plugins/guide.md index bc10e624d..20cbec5c7 100644 --- a/support/doc/plugins/guide.md +++ b/support/doc/plugins/guide.md | |||
@@ -22,6 +22,7 @@ | |||
22 | - [Custom Modal](#custom-modal) | 22 | - [Custom Modal](#custom-modal) |
23 | - [Translate](#translate) | 23 | - [Translate](#translate) |
24 | - [Get public settings](#get-public-settings) | 24 | - [Get public settings](#get-public-settings) |
25 | - [Get server config](#get-server-config) | ||
25 | - [Add custom fields to video form](#add-custom-fields-to-video-form) | 26 | - [Add custom fields to video form](#add-custom-fields-to-video-form) |
26 | - [Publishing](#publishing) | 27 | - [Publishing](#publishing) |
27 | - [Write a plugin/theme](#write-a-plugintheme) | 28 | - [Write a plugin/theme](#write-a-plugintheme) |
@@ -470,6 +471,15 @@ peertubeHelpers.getSettings() | |||
470 | }) | 471 | }) |
471 | ``` | 472 | ``` |
472 | 473 | ||
474 | #### Get server config | ||
475 | |||
476 | ```js | ||
477 | peertubeHelpers.getServerConfig() | ||
478 | .then(config => { | ||
479 | console.log('Fetched server config.', config) | ||
480 | }) | ||
481 | ``` | ||
482 | |||
473 | #### Add custom fields to video form | 483 | #### Add custom fields to video form |
474 | 484 | ||
475 | To add custom fields in the video form (in *Plugin settings* tab): | 485 | To add custom fields in the video form (in *Plugin settings* tab): |
diff --git a/support/doc/tools.md b/support/doc/tools.md index 452b3d039..175c22cd8 100644 --- a/support/doc/tools.md +++ b/support/doc/tools.md | |||
@@ -15,6 +15,7 @@ | |||
15 | - [peertube-redundancy.js](#peertube-redundancyjs) | 15 | - [peertube-redundancy.js](#peertube-redundancyjs) |
16 | - [Server tools](#server-tools) | 16 | - [Server tools](#server-tools) |
17 | - [parse-log](#parse-log) | 17 | - [parse-log](#parse-log) |
18 | - [regenerate-thumbnails.js](#regenerate-thumbnailsjs) | ||
18 | - [create-transcoding-job.js](#create-transcoding-jobjs) | 19 | - [create-transcoding-job.js](#create-transcoding-jobjs) |
19 | - [create-import-video-file-job.js](#create-import-video-file-jobjs) | 20 | - [create-import-video-file-job.js](#create-import-video-file-jobjs) |
20 | - [prune-storage.js](#prune-storagejs) | 21 | - [prune-storage.js](#prune-storagejs) |
@@ -244,6 +245,22 @@ $ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production | |||
244 | 245 | ||
245 | `--level` is optional and could be `info`/`warn`/`error` | 246 | `--level` is optional and could be `info`/`warn`/`error` |
246 | 247 | ||
248 | You can also remove SQL or HTTP logs using `--not-tags`: | ||
249 | |||
250 | ``` | ||
251 | $ cd /var/www/peertube/peertube-latest | ||
252 | $ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run parse-log -- --level debug --not-tags http sql | ||
253 | ``` | ||
254 | |||
255 | ### regenerate-thumbnails.js | ||
256 | |||
257 | Regenerating local video thumbnails could be useful because new PeerTube releases may increase thumbnail sizes: | ||
258 | |||
259 | ``` | ||
260 | $ cd /var/www/peertube/peertube-latest | ||
261 | $ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run regenerate-thumbnails | ||
262 | ``` | ||
263 | |||
247 | ### create-transcoding-job.js | 264 | ### create-transcoding-job.js |
248 | 265 | ||
249 | You can use this script to force transcoding of an existing video. PeerTube needs to be running. | 266 | You can use this script to force transcoding of an existing video. PeerTube needs to be running. |
diff --git a/support/docker/production/config/custom-environment-variables.yaml b/support/docker/production/config/custom-environment-variables.yaml index 63459d8a0..8226715e0 100644 --- a/support/docker/production/config/custom-environment-variables.yaml +++ b/support/docker/production/config/custom-environment-variables.yaml | |||
@@ -117,6 +117,10 @@ transcoding: | |||
117 | 2160p: | 117 | 2160p: |
118 | __name: "PEERTUBE_TRANSCODING_2160P" | 118 | __name: "PEERTUBE_TRANSCODING_2160P" |
119 | __format: "json" | 119 | __format: "json" |
120 | webtorrent: | ||
121 | enabled: | ||
122 | __name: "PEERTUBE_TRANSCODING_WEBTORRENT_ENABLED" | ||
123 | __format: "json" | ||
120 | hls: | 124 | hls: |
121 | enabled: | 125 | enabled: |
122 | __name: "PEERTUBE_TRANSCODING_HLS_ENABLED" | 126 | __name: "PEERTUBE_TRANSCODING_HLS_ENABLED" |
diff --git a/support/systemd/peertube.service b/support/systemd/peertube.service index cf4e7b417..bdeb76b51 100644 --- a/support/systemd/peertube.service +++ b/support/systemd/peertube.service | |||
@@ -8,7 +8,7 @@ Environment=NODE_ENV=production | |||
8 | Environment=NODE_CONFIG_DIR=/var/www/peertube/config | 8 | Environment=NODE_CONFIG_DIR=/var/www/peertube/config |
9 | User=peertube | 9 | User=peertube |
10 | Group=peertube | 10 | Group=peertube |
11 | ExecStart=/usr/bin/npm start | 11 | ExecStart=/usr/bin/node dist/server |
12 | WorkingDirectory=/var/www/peertube/peertube-latest | 12 | WorkingDirectory=/var/www/peertube/peertube-latest |
13 | StandardOutput=syslog | 13 | StandardOutput=syslog |
14 | StandardError=syslog | 14 | StandardError=syslog |
@@ -68,23 +68,23 @@ | |||
68 | integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== | 68 | integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== |
69 | 69 | ||
70 | "@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13": | 70 | "@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13": |
71 | version "7.13.8" | 71 | version "7.13.10" |
72 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.8.tgz#10b2dac78526424dfc1f47650d0e415dfd9dc481" | 72 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1" |
73 | integrity sha512-4vrIhfJyfNf+lCtXC2ck1rKSzDwciqF7IWFhXXrSOUC2O5DrVp+w4c6ed4AllTxhTkUP5x2tYj41VaxdVMMRDw== | 73 | integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg== |
74 | dependencies: | 74 | dependencies: |
75 | "@babel/helper-validator-identifier" "^7.12.11" | 75 | "@babel/helper-validator-identifier" "^7.12.11" |
76 | chalk "^2.0.0" | 76 | chalk "^2.0.0" |
77 | js-tokens "^4.0.0" | 77 | js-tokens "^4.0.0" |
78 | 78 | ||
79 | "@babel/parser@^7.6.0", "@babel/parser@^7.9.6": | 79 | "@babel/parser@^7.6.0", "@babel/parser@^7.9.6": |
80 | version "7.13.9" | 80 | version "7.13.10" |
81 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.9.tgz#ca34cb95e1c2dd126863a84465ae8ef66114be99" | 81 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.10.tgz#8f8f9bf7b3afa3eabd061f7a5bcdf4fec3c48409" |
82 | integrity sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw== | 82 | integrity sha512-0s7Mlrw9uTWkYua7xWr99Wpk2bnGa0ANleKfksYAES8LpWH4gW1OUr42vqKNf0us5UQNfru2wPqMqRITzq/SIQ== |
83 | 83 | ||
84 | "@babel/runtime@^7.7.2": | 84 | "@babel/runtime@^7.7.2": |
85 | version "7.13.9" | 85 | version "7.13.10" |
86 | resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.9.tgz#97dbe2116e2630c489f22e0656decd60aaa1fcee" | 86 | resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d" |
87 | integrity sha512-aY2kU+xgJ3dJ1eU6FMB9EH8dIe8dmusF1xEku52joLvw6eAFN0AI+WxCLDnpev2LEejWBAy2sBvBOBAjI3zmvA== | 87 | integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw== |
88 | dependencies: | 88 | dependencies: |
89 | regenerator-runtime "^0.13.4" | 89 | regenerator-runtime "^0.13.4" |
90 | 90 | ||
@@ -447,13 +447,13 @@ | |||
447 | tlds "^1.218.0" | 447 | tlds "^1.218.0" |
448 | 448 | ||
449 | "@mapbox/node-pre-gyp@^1.0.0": | 449 | "@mapbox/node-pre-gyp@^1.0.0": |
450 | version "1.0.0" | 450 | version "1.0.1" |
451 | resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.0.tgz#2b809e701da0f6729b47fe78ad4b9dc187a7d2e5" | 451 | resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.1.tgz#1b23a8decb5e6356b04770d586067d2bff2703dd" |
452 | integrity sha512-mEaiD1CURETR/dBIiJAwz0M0Q0mH3gCW4pPMaIlNt97mdzYUVeqGcTJSamgJpS6Tg4tBHDrOJpjdh5fJTLnyNQ== | 452 | integrity sha512-CUBdThIZMoLEQQxACwhLsPg/puxBca0abTH3ixuvBQkhjJ80Hdp99jmVjxFCOa52/tZqN9d70IbGUf+OuKDHGA== |
453 | dependencies: | 453 | dependencies: |
454 | detect-libc "^1.0.3" | 454 | detect-libc "^1.0.3" |
455 | http-proxy-agent "^4.0.1" | 455 | http-proxy-agent "^4.0.1" |
456 | mkdirp "^1.0.4" | 456 | make-dir "^3.1.0" |
457 | node-fetch "^2.6.1" | 457 | node-fetch "^2.6.1" |
458 | nopt "^5.0.0" | 458 | nopt "^5.0.0" |
459 | npmlog "^4.1.2" | 459 | npmlog "^4.1.2" |
@@ -461,20 +461,20 @@ | |||
461 | semver "^7.3.4" | 461 | semver "^7.3.4" |
462 | tar "^6.1.0" | 462 | tar "^6.1.0" |
463 | 463 | ||
464 | "@nestjs/common@7.6.13": | 464 | "@nestjs/common@7.6.14": |
465 | version "7.6.13" | 465 | version "7.6.14" |
466 | resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-7.6.13.tgz#597558afedfddeb5021fe8a154327ee082279ab8" | 466 | resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-7.6.14.tgz#abdad360ef107482345b111eeee74fbef00620c9" |
467 | integrity sha512-xijw6so4yA8Ywi8mnrA7Kz97ZC36u20Eyb5/XvmokdLcgTcTyHVdE39r44JYnjHPf8SKZoJ965zdu/fKl4s4GQ== | 467 | integrity sha512-XJrGoGttCsIOvG2+EXl09pl9iCmYXnhPjx3ndPPigMRdXQGLVpF38OdzroWTD7aYU5rHo3Co21G9cYl8aqdt2Q== |
468 | dependencies: | 468 | dependencies: |
469 | axios "0.21.1" | 469 | axios "0.21.1" |
470 | iterare "1.2.1" | 470 | iterare "1.2.1" |
471 | tslib "2.1.0" | 471 | tslib "2.1.0" |
472 | uuid "8.3.2" | 472 | uuid "8.3.2" |
473 | 473 | ||
474 | "@nestjs/core@7.6.13": | 474 | "@nestjs/core@7.6.14": |
475 | version "7.6.13" | 475 | version "7.6.14" |
476 | resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-7.6.13.tgz#b7518dceb436e6ed2c1fad2cff86ddf69b143e73" | 476 | resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-7.6.14.tgz#b3be15506aee33b847abce993a7371439b292dd9" |
477 | integrity sha512-8oY8yZSgri2DngqmvBMtwYw1GIAaXbUXS7Y0mp/iSZ6jP7CQqYCybdcMPneunrt5PG8rtJsq6n+4JNRvxXrVmA== | 477 | integrity sha512-iAeQIsC79xcLTpga3he48ROX4g561VFsfbksicqotrFy0k9czKxVtHxevsnwo8KzFsYXQqOCO6XYI8MsuAjMcg== |
478 | dependencies: | 478 | dependencies: |
479 | "@nuxtjs/opencollective" "0.3.2" | 479 | "@nuxtjs/opencollective" "0.3.2" |
480 | fast-safe-stringify "2.0.7" | 480 | fast-safe-stringify "2.0.7" |
@@ -515,12 +515,12 @@ | |||
515 | node-fetch "^2.6.1" | 515 | node-fetch "^2.6.1" |
516 | 516 | ||
517 | "@openapitools/openapi-generator-cli@^2.1.4": | 517 | "@openapitools/openapi-generator-cli@^2.1.4": |
518 | version "2.1.26" | 518 | version "2.2.2" |
519 | resolved "https://registry.yarnpkg.com/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.1.26.tgz#69108458c0c1a0a3964d9b3e2f0360b195c8ea5f" | 519 | resolved "https://registry.yarnpkg.com/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.2.2.tgz#12b2171a0404731e35aa89a2e0c146186480f51c" |
520 | integrity sha512-wr4LHQCoZCvLhf0/UY/9AZYTVi3nWvvOT+/JFjZYWDA/TIqC4eWxPjzM5tnSzGed6gBTuNHEh8gUonDz6WOZDw== | 520 | integrity sha512-Hl0/5bvv/ETYFuPpTPXqAtChHE2+lLrH0ATl8MtNDxtdXRLoQGCeT8jdT600VvCqJToRkNvQ1JPHbcg/hehyBw== |
521 | dependencies: | 521 | dependencies: |
522 | "@nestjs/common" "7.6.13" | 522 | "@nestjs/common" "7.6.14" |
523 | "@nestjs/core" "7.6.13" | 523 | "@nestjs/core" "7.6.14" |
524 | "@nuxtjs/opencollective" "0.3.2" | 524 | "@nuxtjs/opencollective" "0.3.2" |
525 | chalk "4.1.0" | 525 | chalk "4.1.0" |
526 | commander "6.2.1" | 526 | commander "6.2.1" |
@@ -763,13 +763,6 @@ | |||
763 | dependencies: | 763 | dependencies: |
764 | "@types/node" "*" | 764 | "@types/node" "*" |
765 | 765 | ||
766 | "@types/libxmljs@^0.18.0": | ||
767 | version "0.18.6" | ||
768 | resolved "https://registry.yarnpkg.com/@types/libxmljs/-/libxmljs-0.18.6.tgz#06cb685b791568c56224486338a50c00dbfbf629" | ||
769 | integrity sha512-xVUs71CwL5wYYfx5oH344DYWdoE2hVWlnRxlXFYyA8BcueN+Ey/h4FyhzEikbIJSXBKyPpJKhGu5c3NOx15nww== | ||
770 | dependencies: | ||
771 | "@types/node" "*" | ||
772 | |||
773 | "@types/lodash@^4.14.64": | 766 | "@types/lodash@^4.14.64": |
774 | version "4.14.168" | 767 | version "4.14.168" |
775 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008" | 768 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008" |
@@ -836,9 +829,9 @@ | |||
836 | "@types/express" "*" | 829 | "@types/express" "*" |
837 | 830 | ||
838 | "@types/node@*", "@types/node@>=10.0.0", "@types/node@^14.14.28", "@types/node@^14.14.31": | 831 | "@types/node@*", "@types/node@>=10.0.0", "@types/node@^14.14.28", "@types/node@^14.14.31": |
839 | version "14.14.31" | 832 | version "14.14.34" |
840 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055" | 833 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.34.tgz#07935194fc049069a1c56c0c274265abeddf88da" |
841 | integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g== | 834 | integrity sha512-dBPaxocOK6UVyvhbnpFIj2W+S+1cBTkHQbFQfeeJhoKFbzYcVUGHvddeWPSucKATb3F0+pgDq0i6ghEaZjsugA== |
842 | 835 | ||
843 | "@types/nodemailer@^6.2.0": | 836 | "@types/nodemailer@^6.2.0": |
844 | version "6.4.0" | 837 | version "6.4.0" |
@@ -883,9 +876,9 @@ | |||
883 | "@types/node" "*" | 876 | "@types/node" "*" |
884 | 877 | ||
885 | "@types/qs@*": | 878 | "@types/qs@*": |
886 | version "6.9.5" | 879 | version "6.9.6" |
887 | resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" | 880 | resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.6.tgz#df9c3c8b31a247ec315e6996566be3171df4b3b1" |
888 | integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== | 881 | integrity sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA== |
889 | 882 | ||
890 | "@types/range-parser@*": | 883 | "@types/range-parser@*": |
891 | version "1.2.3" | 884 | version "1.2.3" |
@@ -986,12 +979,12 @@ | |||
986 | "@types/node" "*" | 979 | "@types/node" "*" |
987 | 980 | ||
988 | "@typescript-eslint/eslint-plugin@^4.8.1": | 981 | "@typescript-eslint/eslint-plugin@^4.8.1": |
989 | version "4.16.1" | 982 | version "4.17.0" |
990 | resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.16.1.tgz#2caf6a79dd19c3853b8d39769a27fccb24e4e651" | 983 | resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.17.0.tgz#6f856eca4e6a52ce9cf127dfd349096ad936aa2d" |
991 | integrity sha512-SK777klBdlkUZpZLC1mPvyOWk9yAFCWmug13eAjVQ4/Q1LATE/NbcQL1xDHkptQkZOLnPmLUA1Y54m8dqYwnoQ== | 984 | integrity sha512-/fKFDcoHg8oNan39IKFOb5WmV7oWhQe1K6CDaAVfJaNWEhmfqlA24g+u1lqU5bMH7zuNasfMId4LaYWC5ijRLw== |
992 | dependencies: | 985 | dependencies: |
993 | "@typescript-eslint/experimental-utils" "4.16.1" | 986 | "@typescript-eslint/experimental-utils" "4.17.0" |
994 | "@typescript-eslint/scope-manager" "4.16.1" | 987 | "@typescript-eslint/scope-manager" "4.17.0" |
995 | debug "^4.1.1" | 988 | debug "^4.1.1" |
996 | functional-red-black-tree "^1.0.1" | 989 | functional-red-black-tree "^1.0.1" |
997 | lodash "^4.17.15" | 990 | lodash "^4.17.15" |
@@ -999,60 +992,60 @@ | |||
999 | semver "^7.3.2" | 992 | semver "^7.3.2" |
1000 | tsutils "^3.17.1" | 993 | tsutils "^3.17.1" |
1001 | 994 | ||
1002 | "@typescript-eslint/experimental-utils@4.16.1": | 995 | "@typescript-eslint/experimental-utils@4.17.0": |
1003 | version "4.16.1" | 996 | version "4.17.0" |
1004 | resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.16.1.tgz#da7a396dc7d0e01922acf102b76efff17320b328" | 997 | resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.17.0.tgz#762c44aaa1a6a3c05b6d63a8648fb89b89f84c80" |
1005 | integrity sha512-0Hm3LSlMYFK17jO4iY3un1Ve9x1zLNn4EM50Lia+0EV99NdbK+cn0er7HC7IvBA23mBg3P+8dUkMXy4leL33UQ== | 998 | integrity sha512-ZR2NIUbnIBj+LGqCFGQ9yk2EBQrpVVFOh9/Kd0Lm6gLpSAcCuLLe5lUCibKGCqyH9HPwYC0GIJce2O1i8VYmWA== |
1006 | dependencies: | 999 | dependencies: |
1007 | "@types/json-schema" "^7.0.3" | 1000 | "@types/json-schema" "^7.0.3" |
1008 | "@typescript-eslint/scope-manager" "4.16.1" | 1001 | "@typescript-eslint/scope-manager" "4.17.0" |
1009 | "@typescript-eslint/types" "4.16.1" | 1002 | "@typescript-eslint/types" "4.17.0" |
1010 | "@typescript-eslint/typescript-estree" "4.16.1" | 1003 | "@typescript-eslint/typescript-estree" "4.17.0" |
1011 | eslint-scope "^5.0.0" | 1004 | eslint-scope "^5.0.0" |
1012 | eslint-utils "^2.0.0" | 1005 | eslint-utils "^2.0.0" |
1013 | 1006 | ||
1014 | "@typescript-eslint/parser@^4.0.0": | 1007 | "@typescript-eslint/parser@^4.0.0": |
1015 | version "4.16.1" | 1008 | version "4.17.0" |
1016 | resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.16.1.tgz#3bbd3234dd3c5b882b2bcd9899bc30e1e1586d2a" | 1009 | resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.17.0.tgz#141b647ffc72ebebcbf9b0fe6087f65b706d3215" |
1017 | integrity sha512-/c0LEZcDL5y8RyI1zLcmZMvJrsR6SM1uetskFkoh3dvqDKVXPsXI+wFB/CbVw7WkEyyTKobC1mUNp/5y6gRvXg== | 1010 | integrity sha512-KYdksiZQ0N1t+6qpnl6JeK9ycCFprS9xBAiIrw4gSphqONt8wydBw4BXJi3C11ywZmyHulvMaLjWsxDjUSDwAw== |
1018 | dependencies: | 1011 | dependencies: |
1019 | "@typescript-eslint/scope-manager" "4.16.1" | 1012 | "@typescript-eslint/scope-manager" "4.17.0" |
1020 | "@typescript-eslint/types" "4.16.1" | 1013 | "@typescript-eslint/types" "4.17.0" |
1021 | "@typescript-eslint/typescript-estree" "4.16.1" | 1014 | "@typescript-eslint/typescript-estree" "4.17.0" |
1022 | debug "^4.1.1" | 1015 | debug "^4.1.1" |
1023 | 1016 | ||
1024 | "@typescript-eslint/scope-manager@4.16.1": | 1017 | "@typescript-eslint/scope-manager@4.17.0": |
1025 | version "4.16.1" | 1018 | version "4.17.0" |
1026 | resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.16.1.tgz#244e2006bc60cfe46987e9987f4ff49c9e3f00d5" | 1019 | resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.17.0.tgz#f4edf94eff3b52a863180f7f89581bf963e3d37d" |
1027 | integrity sha512-6IlZv9JaurqV0jkEg923cV49aAn8V6+1H1DRfhRcvZUrptQ+UtSKHb5kwTayzOYTJJ/RsYZdcvhOEKiBLyc0Cw== | 1020 | integrity sha512-OJ+CeTliuW+UZ9qgULrnGpPQ1bhrZNFpfT/Bc0pzNeyZwMik7/ykJ0JHnQ7krHanFN9wcnPK89pwn84cRUmYjw== |
1028 | dependencies: | 1021 | dependencies: |
1029 | "@typescript-eslint/types" "4.16.1" | 1022 | "@typescript-eslint/types" "4.17.0" |
1030 | "@typescript-eslint/visitor-keys" "4.16.1" | 1023 | "@typescript-eslint/visitor-keys" "4.17.0" |
1031 | 1024 | ||
1032 | "@typescript-eslint/types@4.16.1": | 1025 | "@typescript-eslint/types@4.17.0": |
1033 | version "4.16.1" | 1026 | version "4.17.0" |
1034 | resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.16.1.tgz#5ba2d3e38b1a67420d2487519e193163054d9c15" | 1027 | resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.17.0.tgz#f57d8fc7f31b348db946498a43050083d25f40ad" |
1035 | integrity sha512-nnKqBwMgRlhzmJQF8tnFDZWfunXmJyuXj55xc8Kbfup4PbkzdoDXZvzN8//EiKR27J6vUSU8j4t37yUuYPiLqA== | 1028 | integrity sha512-RN5z8qYpJ+kXwnLlyzZkiJwfW2AY458Bf8WqllkondQIcN2ZxQowAToGSd9BlAUZDB5Ea8I6mqL2quGYCLT+2g== |
1036 | 1029 | ||
1037 | "@typescript-eslint/typescript-estree@4.16.1": | 1030 | "@typescript-eslint/typescript-estree@4.17.0": |
1038 | version "4.16.1" | 1031 | version "4.17.0" |
1039 | resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.16.1.tgz#c2fc46b05a48fbf8bbe8b66a63f0a9ba04b356f1" | 1032 | resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.17.0.tgz#b835d152804f0972b80dbda92477f9070a72ded1" |
1040 | integrity sha512-m8I/DKHa8YbeHt31T+UGd/l8Kwr0XCTCZL3H4HMvvLCT7HU9V7yYdinTOv1gf/zfqNeDcCgaFH2BMsS8x6NvJg== | 1033 | integrity sha512-lRhSFIZKUEPPWpWfwuZBH9trYIEJSI0vYsrxbvVvNyIUDoKWaklOAelsSkeh3E2VBSZiNe9BZ4E5tYBZbUczVQ== |
1041 | dependencies: | 1034 | dependencies: |
1042 | "@typescript-eslint/types" "4.16.1" | 1035 | "@typescript-eslint/types" "4.17.0" |
1043 | "@typescript-eslint/visitor-keys" "4.16.1" | 1036 | "@typescript-eslint/visitor-keys" "4.17.0" |
1044 | debug "^4.1.1" | 1037 | debug "^4.1.1" |
1045 | globby "^11.0.1" | 1038 | globby "^11.0.1" |
1046 | is-glob "^4.0.1" | 1039 | is-glob "^4.0.1" |
1047 | semver "^7.3.2" | 1040 | semver "^7.3.2" |
1048 | tsutils "^3.17.1" | 1041 | tsutils "^3.17.1" |
1049 | 1042 | ||
1050 | "@typescript-eslint/visitor-keys@4.16.1": | 1043 | "@typescript-eslint/visitor-keys@4.17.0": |
1051 | version "4.16.1" | 1044 | version "4.17.0" |
1052 | resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.16.1.tgz#d7571fb580749fae621520deeb134370bbfc7293" | 1045 | resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.17.0.tgz#9c304cfd20287c14a31d573195a709111849b14d" |
1053 | integrity sha512-s/aIP1XcMkEqCNcPQtl60ogUYjSM8FU2mq1O7y5cFf3Xcob1z1iXWNB6cC43Op+NGRTFgGolri6s8z/efA9i1w== | 1046 | integrity sha512-WfuMN8mm5SSqXuAr9NM+fItJ0SVVphobWYkWOwQ1odsfC014Vdxk/92t4JwS1Q6fCA/ABfCKpa3AVtpUKTNKGQ== |
1054 | dependencies: | 1047 | dependencies: |
1055 | "@typescript-eslint/types" "4.16.1" | 1048 | "@typescript-eslint/types" "4.17.0" |
1056 | eslint-visitor-keys "^2.0.0" | 1049 | eslint-visitor-keys "^2.0.0" |
1057 | 1050 | ||
1058 | "@ungap/promise-all-settled@1.1.2": | 1051 | "@ungap/promise-all-settled@1.1.2": |
@@ -1116,9 +1109,9 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: | |||
1116 | uri-js "^4.2.2" | 1109 | uri-js "^4.2.2" |
1117 | 1110 | ||
1118 | ajv@^7.0.2: | 1111 | ajv@^7.0.2: |
1119 | version "7.1.1" | 1112 | version "7.2.1" |
1120 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.1.1.tgz#1e6b37a454021fa9941713f38b952fc1c8d32a84" | 1113 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.2.1.tgz#a5ac226171912447683524fa2f1248fcf8bac83d" |
1121 | integrity sha512-ga/aqDYnUy/o7vbsRTFhhTsNeXiYb5JWDIcRIeZfwRNCefwjNTVYCGdGSUrEmiu3yDK3vFvNbgJxvrQW4JXrYQ== | 1114 | integrity sha512-+nu0HDv7kNSOua9apAVc979qd932rrZeb3WOvoiD31A/p1mIE5/9bN2027pE2rOPYEdS3UHzsvof4hY+lM9/WQ== |
1122 | dependencies: | 1115 | dependencies: |
1123 | fast-deep-equal "^3.1.1" | 1116 | fast-deep-equal "^3.1.1" |
1124 | json-schema-traverse "^1.0.0" | 1117 | json-schema-traverse "^1.0.0" |
@@ -1373,11 +1366,12 @@ at-least-node@^1.0.0: | |||
1373 | integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== | 1366 | integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== |
1374 | 1367 | ||
1375 | autocannon@^7.0.4: | 1368 | autocannon@^7.0.4: |
1376 | version "7.0.4" | 1369 | version "7.0.5" |
1377 | resolved "https://registry.yarnpkg.com/autocannon/-/autocannon-7.0.4.tgz#c812c11af283254bff4bd75cce8383e79550c882" | 1370 | resolved "https://registry.yarnpkg.com/autocannon/-/autocannon-7.0.5.tgz#7c195ba09ae3b299d6f7532950d1e07041538b29" |
1378 | integrity sha512-+A+kSsVrx9F9fFbPAD7YytGQfCKgkaCIut4KrnYBbY2hmboAT065ClxqBsVqstokvFfdBAfSMPh0VSc6ktiimg== | 1371 | integrity sha512-VMOfWf0e9EB5Crr7/snXTb64oC7I3lofpAjBcPWvHGet94DKjHCsbj05iIt2WTenPKub++6PETb/H9qleV9yJg== |
1379 | dependencies: | 1372 | dependencies: |
1380 | chalk "^4.1.0" | 1373 | chalk "^4.1.0" |
1374 | char-spinner "^1.0.1" | ||
1381 | cli-table3 "^0.6.0" | 1375 | cli-table3 "^0.6.0" |
1382 | clone "^2.1.2" | 1376 | clone "^2.1.2" |
1383 | color-support "^1.1.1" | 1377 | color-support "^1.1.1" |
@@ -1391,11 +1385,10 @@ autocannon@^7.0.4: | |||
1391 | manage-path "^2.0.0" | 1385 | manage-path "^2.0.0" |
1392 | minimist "^1.2.0" | 1386 | minimist "^1.2.0" |
1393 | on-net-listen "^1.1.1" | 1387 | on-net-listen "^1.1.1" |
1394 | ora "^5.1.0" | ||
1395 | pretty-bytes "^5.4.1" | 1388 | pretty-bytes "^5.4.1" |
1396 | progress "^2.0.3" | 1389 | progress "^2.0.3" |
1397 | reinterval "^1.1.0" | 1390 | reinterval "^1.1.0" |
1398 | retimer "^2.0.0" | 1391 | retimer "^3.0.0" |
1399 | semver "^7.3.2" | 1392 | semver "^7.3.2" |
1400 | timestring "^6.0.0" | 1393 | timestring "^6.0.0" |
1401 | 1394 | ||
@@ -1468,7 +1461,7 @@ basic-auth-connect@^1.0.0: | |||
1468 | resolved "https://registry.yarnpkg.com/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz#fdb0b43962ca7b40456a7c2bb48fe173da2d2122" | 1461 | resolved "https://registry.yarnpkg.com/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz#fdb0b43962ca7b40456a7c2bb48fe173da2d2122" |
1469 | integrity sha1-/bC0OWLKe0BFanwrtI/hc9otISI= | 1462 | integrity sha1-/bC0OWLKe0BFanwrtI/hc9otISI= |
1470 | 1463 | ||
1471 | basic-auth@^2.0.0, basic-auth@~2.0.1: | 1464 | basic-auth@2.0.1, basic-auth@~2.0.1: |
1472 | version "2.0.1" | 1465 | version "2.0.1" |
1473 | resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" | 1466 | resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" |
1474 | integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== | 1467 | integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== |
@@ -1545,11 +1538,6 @@ binary-search@^1.3.4: | |||
1545 | resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c" | 1538 | resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c" |
1546 | integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA== | 1539 | integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA== |
1547 | 1540 | ||
1548 | bindings@~1.3.0: | ||
1549 | version "1.3.1" | ||
1550 | resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.1.tgz#21fc7c6d67c18516ec5aaa2815b145ff77b26ea5" | ||
1551 | integrity sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew== | ||
1552 | |||
1553 | bitfield@^4.0.0: | 1541 | bitfield@^4.0.0: |
1554 | version "4.0.0" | 1542 | version "4.0.0" |
1555 | resolved "https://registry.yarnpkg.com/bitfield/-/bitfield-4.0.0.tgz#3094123c870030dc6198a283d779639bd2a8e256" | 1543 | resolved "https://registry.yarnpkg.com/bitfield/-/bitfield-4.0.0.tgz#3094123c870030dc6198a283d779639bd2a8e256" |
@@ -1626,15 +1614,6 @@ bittorrent-tracker@^9.0.0: | |||
1626 | bufferutil "^4.0.1" | 1614 | bufferutil "^4.0.1" |
1627 | utf-8-validate "^5.0.2" | 1615 | utf-8-validate "^5.0.2" |
1628 | 1616 | ||
1629 | bl@^4.0.3: | ||
1630 | version "4.1.0" | ||
1631 | resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" | ||
1632 | integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== | ||
1633 | dependencies: | ||
1634 | buffer "^5.5.0" | ||
1635 | inherits "^2.0.4" | ||
1636 | readable-stream "^3.4.0" | ||
1637 | |||
1638 | blob-to-buffer@^1.2.9: | 1617 | blob-to-buffer@^1.2.9: |
1639 | version "1.2.9" | 1618 | version "1.2.9" |
1640 | resolved "https://registry.yarnpkg.com/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz#a17fd6c1c564011408f8971e451544245daaa84a" | 1619 | resolved "https://registry.yarnpkg.com/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz#a17fd6c1c564011408f8971e451544245daaa84a" |
@@ -1652,16 +1631,16 @@ block-stream2@^2.0.0, block-stream2@^2.1.0: | |||
1652 | dependencies: | 1631 | dependencies: |
1653 | readable-stream "^3.4.0" | 1632 | readable-stream "^3.4.0" |
1654 | 1633 | ||
1634 | bluebird@3.7.2, bluebird@^3.5.0, bluebird@^3.7.2: | ||
1635 | version "3.7.2" | ||
1636 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" | ||
1637 | integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== | ||
1638 | |||
1655 | bluebird@^2.10.0: | 1639 | bluebird@^2.10.0: |
1656 | version "2.11.0" | 1640 | version "2.11.0" |
1657 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" | 1641 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" |
1658 | integrity sha1-U0uQM8AiyVecVro7Plpcqvu2UOE= | 1642 | integrity sha1-U0uQM8AiyVecVro7Plpcqvu2UOE= |
1659 | 1643 | ||
1660 | bluebird@^3.0.5, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.7.2: | ||
1661 | version "3.7.2" | ||
1662 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" | ||
1663 | integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== | ||
1664 | |||
1665 | bmp-js@^0.1.0: | 1644 | bmp-js@^0.1.0: |
1666 | version "0.1.0" | 1645 | version "0.1.0" |
1667 | resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.1.0.tgz#e05a63f796a6c1ff25f4771ec7adadc148c07233" | 1646 | resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.1.0.tgz#e05a63f796a6c1ff25f4771ec7adadc148c07233" |
@@ -1770,7 +1749,7 @@ buffer-writer@2.0.0: | |||
1770 | resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" | 1749 | resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" |
1771 | integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== | 1750 | integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== |
1772 | 1751 | ||
1773 | buffer@^5.2.0, buffer@^5.5.0: | 1752 | buffer@^5.2.0: |
1774 | version "5.7.1" | 1753 | version "5.7.1" |
1775 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" | 1754 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" |
1776 | integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== | 1755 | integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== |
@@ -1926,9 +1905,9 @@ chai-xml@^0.4.0: | |||
1926 | xml2js "^0.4.23" | 1905 | xml2js "^0.4.23" |
1927 | 1906 | ||
1928 | chai@^4.1.1: | 1907 | chai@^4.1.1: |
1929 | version "4.3.1" | 1908 | version "4.3.3" |
1930 | resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.1.tgz#6fc6af447610709818e5c45116207d60b8a49cfd" | 1909 | resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.3.tgz#f2b2ad9736999d07a7ff95cf1e7086c43a76f72d" |
1931 | integrity sha512-JClPZFGRcSl7X8dYzlCJY7v+X1fBA+9Y339Y8EqhBVfp0QC1hTnaf7nMfR+XZ74clkBC64b0iEw2cWKHt3EVqA== | 1910 | integrity sha512-MPSLOZwxxnA0DhLE84klnGPojWFK5KuhP7/j5dTsxpr2S3XlkqJP5WbyYl1gCTWvG2Z5N+HD4F472WsbEZL6Pw== |
1932 | dependencies: | 1911 | dependencies: |
1933 | assertion-error "^1.1.0" | 1912 | assertion-error "^1.1.0" |
1934 | check-error "^1.0.2" | 1913 | check-error "^1.0.2" |
@@ -1962,6 +1941,11 @@ chalk@^3.0.0: | |||
1962 | ansi-styles "^4.1.0" | 1941 | ansi-styles "^4.1.0" |
1963 | supports-color "^7.1.0" | 1942 | supports-color "^7.1.0" |
1964 | 1943 | ||
1944 | char-spinner@^1.0.1: | ||
1945 | version "1.0.1" | ||
1946 | resolved "https://registry.yarnpkg.com/char-spinner/-/char-spinner-1.0.1.tgz#e6ea67bd247e107112983b7ab0479ed362800081" | ||
1947 | integrity sha1-5upnvSR+EHESmDt6sEee02KAAIE= | ||
1948 | |||
1965 | character-parser@^2.2.0: | 1949 | character-parser@^2.2.0: |
1966 | version "2.2.0" | 1950 | version "2.2.0" |
1967 | resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0" | 1951 | resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0" |
@@ -2028,11 +2012,6 @@ chokidar@3.5.1, chokidar@^3.2.2, chokidar@^3.4.2: | |||
2028 | optionalDependencies: | 2012 | optionalDependencies: |
2029 | fsevents "~2.3.1" | 2013 | fsevents "~2.3.1" |
2030 | 2014 | ||
2031 | chownr@^1.1.1: | ||
2032 | version "1.1.4" | ||
2033 | resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" | ||
2034 | integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== | ||
2035 | |||
2036 | chownr@^2.0.0: | 2015 | chownr@^2.0.0: |
2037 | version "2.0.0" | 2016 | version "2.0.0" |
2038 | resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" | 2017 | resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" |
@@ -2061,9 +2040,9 @@ chrome-net@^3.3.2, chrome-net@^3.3.3, chrome-net@^3.3.4: | |||
2061 | inherits "^2.0.1" | 2040 | inherits "^2.0.1" |
2062 | 2041 | ||
2063 | chunk-store-stream@^4.2.0: | 2042 | chunk-store-stream@^4.2.0: |
2064 | version "4.2.0" | 2043 | version "4.3.0" |
2065 | resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.2.0.tgz#18f673c495946c4cdcf14124a3ebd5f31eb0ea35" | 2044 | resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.3.0.tgz#3de5f4dfe19729366c29bb7ed52d139f9af29f0e" |
2066 | integrity sha512-90iueoPoqT2isnmy1fyqwzgFy5FokuaxQuijOQG1VgC/6DaXRfeYN0da8iWENkzqElWhqLxo8pWc7pH9dmxlcA== | 2045 | integrity sha512-qby+/RXoiMoTVtPiylWZt7KFF1jy6M829TzMi2hxZtBIH9ptV19wxcft6zGiXLokJgCbuZPGNGab6DWHqiSEKw== |
2067 | dependencies: | 2046 | dependencies: |
2068 | block-stream2 "^2.0.0" | 2047 | block-stream2 "^2.0.0" |
2069 | readable-stream "^3.6.0" | 2048 | readable-stream "^3.6.0" |
@@ -2092,11 +2071,6 @@ cli-cursor@^3.1.0: | |||
2092 | dependencies: | 2071 | dependencies: |
2093 | restore-cursor "^3.1.0" | 2072 | restore-cursor "^3.1.0" |
2094 | 2073 | ||
2095 | cli-spinners@^2.5.0: | ||
2096 | version "2.5.0" | ||
2097 | resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.5.0.tgz#12763e47251bf951cb75c201dfa58ff1bcb2d047" | ||
2098 | integrity sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ== | ||
2099 | |||
2100 | cli-table3@^0.6.0: | 2074 | cli-table3@^0.6.0: |
2101 | version "0.6.0" | 2075 | version "0.6.0" |
2102 | resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" | 2076 | resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" |
@@ -2204,9 +2178,9 @@ color-name@^1.0.0, color-name@~1.1.4: | |||
2204 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== | 2178 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== |
2205 | 2179 | ||
2206 | color-string@^1.5.2: | 2180 | color-string@^1.5.2: |
2207 | version "1.5.4" | 2181 | version "1.5.5" |
2208 | resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6" | 2182 | resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014" |
2209 | integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw== | 2183 | integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg== |
2210 | dependencies: | 2184 | dependencies: |
2211 | color-name "^1.0.0" | 2185 | color-name "^1.0.0" |
2212 | simple-swizzle "^0.2.2" | 2186 | simple-swizzle "^0.2.2" |
@@ -2224,7 +2198,7 @@ color@3.0.x: | |||
2224 | color-convert "^1.9.1" | 2198 | color-convert "^1.9.1" |
2225 | color-string "^1.5.2" | 2199 | color-string "^1.5.2" |
2226 | 2200 | ||
2227 | colorette@^1.2.1: | 2201 | colorette@^1.2.2: |
2228 | version "1.2.2" | 2202 | version "1.2.2" |
2229 | resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" | 2203 | resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" |
2230 | integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== | 2204 | integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== |
@@ -2352,9 +2326,9 @@ concurrently@^6.0.0: | |||
2352 | yargs "^16.2.0" | 2326 | yargs "^16.2.0" |
2353 | 2327 | ||
2354 | config@^3.0.0: | 2328 | config@^3.0.0: |
2355 | version "3.3.4" | 2329 | version "3.3.6" |
2356 | resolved "https://registry.yarnpkg.com/config/-/config-3.3.4.tgz#55811abc2752b38a7b806cbdbc2da79c428312b7" | 2330 | resolved "https://registry.yarnpkg.com/config/-/config-3.3.6.tgz#b87799db7399cc34988f55379b5f43465b1b065c" |
2357 | integrity sha512-URO0m6z+rtENGHqtzO7W7C35iF+H9KVe7JJFps+3TIqZEOHl83NqTAgp5h8ah96m4NPQnx08nPBfbtDU+PgjVA== | 2331 | integrity sha512-Hj5916C5HFawjYJat1epbyY2PlAgLpBtDUlr0MxGLgo3p5+7kylyvnRY18PqJHgnNWXcdd0eWDemT7eYWuFgwg== |
2358 | dependencies: | 2332 | dependencies: |
2359 | json5 "^2.1.1" | 2333 | json5 "^2.1.1" |
2360 | 2334 | ||
@@ -2584,9 +2558,9 @@ dashdash@^1.12.0: | |||
2584 | assert-plus "^1.0.0" | 2558 | assert-plus "^1.0.0" |
2585 | 2559 | ||
2586 | date-fns@^2.0.1, date-fns@^2.16.1: | 2560 | date-fns@^2.0.1, date-fns@^2.16.1: |
2587 | version "2.18.0" | 2561 | version "2.19.0" |
2588 | resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.18.0.tgz#08e50aee300ad0d2c5e054e3f0d10d8f9cdfe09e" | 2562 | resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1" |
2589 | integrity sha512-NYyAg4wRmGVU4miKq5ivRACOODdZRY3q5WLmOJSq8djyzftYphU7dTHLcEtLqEvfqMKQ0jVv91P4BAwIjsXIcw== | 2563 | integrity sha512-X3bf2iTPgCAQp9wvjOQytnf5vO5rESYRXlPIVcgSbtT5OTScPcsf9eZU+B/YIkKAtYr5WeCii58BgATrNitlWg== |
2590 | 2564 | ||
2591 | dateformat@^3.0.3: | 2565 | dateformat@^3.0.3: |
2592 | version "3.0.3" | 2566 | version "3.0.3" |
@@ -2756,7 +2730,7 @@ destroy@~1.0.4: | |||
2756 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" | 2730 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" |
2757 | integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= | 2731 | integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= |
2758 | 2732 | ||
2759 | detect-libc@^1.0.2, detect-libc@^1.0.3: | 2733 | detect-libc@^1.0.3: |
2760 | version "1.0.3" | 2734 | version "1.0.3" |
2761 | resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" | 2735 | resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" |
2762 | integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= | 2736 | integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= |
@@ -2840,9 +2814,9 @@ domhandler@^4.0.0: | |||
2840 | domelementtype "^2.1.0" | 2814 | domelementtype "^2.1.0" |
2841 | 2815 | ||
2842 | domutils@^2.0.0, domutils@^2.4.3, domutils@^2.4.4: | 2816 | domutils@^2.0.0, domutils@^2.4.3, domutils@^2.4.4: |
2843 | version "2.4.4" | 2817 | version "2.5.0" |
2844 | resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3" | 2818 | resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.5.0.tgz#42f49cffdabb92ad243278b331fd761c1c2d3039" |
2845 | integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA== | 2819 | integrity sha512-Ho16rzNMOFk2fPwChGh3D2D9OEHAfG19HgmRR2l+WLSsIstNsAYBzePH412bL0y5T44ejABIVfTHQ8nqi/tBCg== |
2846 | dependencies: | 2820 | dependencies: |
2847 | dom-serializer "^1.0.1" | 2821 | dom-serializer "^1.0.1" |
2848 | domelementtype "^2.0.1" | 2822 | domelementtype "^2.0.1" |
@@ -3047,27 +3021,10 @@ error-ex@^1.2.0, error-ex@^1.3.1: | |||
3047 | dependencies: | 3021 | dependencies: |
3048 | is-arrayish "^0.2.1" | 3022 | is-arrayish "^0.2.1" |
3049 | 3023 | ||
3050 | es-abstract@^1.17.0-next.0: | 3024 | es-abstract@^1.17.0-next.0, es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2: |
3051 | version "1.17.7" | 3025 | version "1.18.0" |
3052 | resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" | 3026 | resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4" |
3053 | integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== | 3027 | integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw== |
3054 | dependencies: | ||
3055 | es-to-primitive "^1.2.1" | ||
3056 | function-bind "^1.1.1" | ||
3057 | has "^1.0.3" | ||
3058 | has-symbols "^1.0.1" | ||
3059 | is-callable "^1.2.2" | ||
3060 | is-regex "^1.1.1" | ||
3061 | object-inspect "^1.8.0" | ||
3062 | object-keys "^1.1.1" | ||
3063 | object.assign "^4.1.1" | ||
3064 | string.prototype.trimend "^1.0.1" | ||
3065 | string.prototype.trimstart "^1.0.1" | ||
3066 | |||
3067 | es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2: | ||
3068 | version "1.18.0-next.3" | ||
3069 | resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.3.tgz#56bc8b5cc36b2cca25a13be07f3c02c2343db6b7" | ||
3070 | integrity sha512-VMzHx/Bczjg59E6jZOQjHeN3DEoptdhejpARgflAViidlqSpjdq9zA6lKwlhRRs/lOw1gHJv2xkkSFRgvEwbQg== | ||
3071 | dependencies: | 3028 | dependencies: |
3072 | call-bind "^1.0.2" | 3029 | call-bind "^1.0.2" |
3073 | es-to-primitive "^1.2.1" | 3030 | es-to-primitive "^1.2.1" |
@@ -3401,15 +3358,6 @@ exif-parser@^0.1.12: | |||
3401 | resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922" | 3358 | resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922" |
3402 | integrity sha1-WKnS1ywCwfbwKg70qRZicrd2CSI= | 3359 | integrity sha1-WKnS1ywCwfbwKg70qRZicrd2CSI= |
3403 | 3360 | ||
3404 | express-oauth-server@^2.0.0: | ||
3405 | version "2.0.0" | ||
3406 | resolved "https://registry.yarnpkg.com/express-oauth-server/-/express-oauth-server-2.0.0.tgz#57b08665c1201532f52c4c02f19709238b99a48d" | ||
3407 | integrity sha1-V7CGZcEgFTL1LEwC8ZcJI4uZpI0= | ||
3408 | dependencies: | ||
3409 | bluebird "^3.0.5" | ||
3410 | express "^4.13.3" | ||
3411 | oauth2-server "3.0.0" | ||
3412 | |||
3413 | express-rate-limit@^5.0.0: | 3361 | express-rate-limit@^5.0.0: |
3414 | version "5.2.6" | 3362 | version "5.2.6" |
3415 | resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.2.6.tgz#b454e1be8a252081bda58460e0a25bf43ee0f7b0" | 3363 | resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.2.6.tgz#b454e1be8a252081bda58460e0a25bf43ee0f7b0" |
@@ -3423,7 +3371,7 @@ express-validator@^6.4.0: | |||
3423 | lodash "^4.17.20" | 3371 | lodash "^4.17.20" |
3424 | validator "^13.5.2" | 3372 | validator "^13.5.2" |
3425 | 3373 | ||
3426 | express@^4.12.4, express@^4.13.3, express@^4.16.4, express@^4.17.1: | 3374 | express@^4.12.4, express@^4.16.4, express@^4.17.1: |
3427 | version "4.17.1" | 3375 | version "4.17.1" |
3428 | resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" | 3376 | resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" |
3429 | integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== | 3377 | integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== |
@@ -3527,6 +3475,11 @@ fast-safe-stringify@2.0.7, fast-safe-stringify@^2.0.4, fast-safe-stringify@^2.0. | |||
3527 | resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" | 3475 | resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" |
3528 | integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== | 3476 | integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== |
3529 | 3477 | ||
3478 | fast-xml-parser@^3.19.0: | ||
3479 | version "3.19.0" | ||
3480 | resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz#cb637ec3f3999f51406dd8ff0e6fc4d83e520d01" | ||
3481 | integrity sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg== | ||
3482 | |||
3530 | fastq@^1.6.0: | 3483 | fastq@^1.6.0: |
3531 | version "1.11.0" | 3484 | version "1.11.0" |
3532 | resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" | 3485 | resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" |
@@ -3749,13 +3702,6 @@ fs-extra@9.1.0, fs-extra@^9.0.0: | |||
3749 | jsonfile "^6.0.1" | 3702 | jsonfile "^6.0.1" |
3750 | universalify "^2.0.0" | 3703 | universalify "^2.0.0" |
3751 | 3704 | ||
3752 | fs-minipass@^1.2.5: | ||
3753 | version "1.2.7" | ||
3754 | resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" | ||
3755 | integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== | ||
3756 | dependencies: | ||
3757 | minipass "^2.6.0" | ||
3758 | |||
3759 | fs-minipass@^2.0.0: | 3705 | fs-minipass@^2.0.0: |
3760 | version "2.1.0" | 3706 | version "2.1.0" |
3761 | resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" | 3707 | resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" |
@@ -3873,9 +3819,9 @@ gifwrap@^0.9.2: | |||
3873 | omggif "^1.0.10" | 3819 | omggif "^1.0.10" |
3874 | 3820 | ||
3875 | glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0: | 3821 | glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0: |
3876 | version "5.1.1" | 3822 | version "5.1.2" |
3877 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" | 3823 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" |
3878 | integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== | 3824 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== |
3879 | dependencies: | 3825 | dependencies: |
3880 | is-glob "^4.0.1" | 3826 | is-glob "^4.0.1" |
3881 | 3827 | ||
@@ -3925,6 +3871,23 @@ globby@^11.0.1: | |||
3925 | merge2 "^1.3.0" | 3871 | merge2 "^1.3.0" |
3926 | slash "^3.0.0" | 3872 | slash "^3.0.0" |
3927 | 3873 | ||
3874 | got@^11.8.2, got@~11.8.1: | ||
3875 | version "11.8.2" | ||
3876 | resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599" | ||
3877 | integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ== | ||
3878 | dependencies: | ||
3879 | "@sindresorhus/is" "^4.0.0" | ||
3880 | "@szmarczak/http-timer" "^4.0.5" | ||
3881 | "@types/cacheable-request" "^6.0.1" | ||
3882 | "@types/responselike" "^1.0.0" | ||
3883 | cacheable-lookup "^5.0.3" | ||
3884 | cacheable-request "^7.0.1" | ||
3885 | decompress-response "^6.0.0" | ||
3886 | http2-wrapper "^1.0.0-beta.5.2" | ||
3887 | lowercase-keys "^2.0.0" | ||
3888 | p-cancelable "^2.0.0" | ||
3889 | responselike "^2.0.0" | ||
3890 | |||
3928 | got@^9.6.0: | 3891 | got@^9.6.0: |
3929 | version "9.6.0" | 3892 | version "9.6.0" |
3930 | resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" | 3893 | resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" |
@@ -3942,23 +3905,6 @@ got@^9.6.0: | |||
3942 | to-readable-stream "^1.0.0" | 3905 | to-readable-stream "^1.0.0" |
3943 | url-parse-lax "^3.0.0" | 3906 | url-parse-lax "^3.0.0" |
3944 | 3907 | ||
3945 | got@~11.8.1: | ||
3946 | version "11.8.2" | ||
3947 | resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599" | ||
3948 | integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ== | ||
3949 | dependencies: | ||
3950 | "@sindresorhus/is" "^4.0.0" | ||
3951 | "@szmarczak/http-timer" "^4.0.5" | ||
3952 | "@types/cacheable-request" "^6.0.1" | ||
3953 | "@types/responselike" "^1.0.0" | ||
3954 | cacheable-lookup "^5.0.3" | ||
3955 | cacheable-request "^7.0.1" | ||
3956 | decompress-response "^6.0.0" | ||
3957 | http2-wrapper "^1.0.0-beta.5.2" | ||
3958 | lowercase-keys "^2.0.0" | ||
3959 | p-cancelable "^2.0.0" | ||
3960 | responselike "^2.0.0" | ||
3961 | |||
3962 | graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: | 3908 | graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: |
3963 | version "4.2.6" | 3909 | version "4.2.6" |
3964 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" | 3910 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" |
@@ -4104,9 +4050,9 @@ htmlparser2@^4.0.0, htmlparser2@^4.1.0: | |||
4104 | entities "^2.0.0" | 4050 | entities "^2.0.0" |
4105 | 4051 | ||
4106 | htmlparser2@^6.0.0: | 4052 | htmlparser2@^6.0.0: |
4107 | version "6.0.0" | 4053 | version "6.0.1" |
4108 | resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01" | 4054 | resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.1.tgz#422521231ef6d42e56bd411da8ba40aa36e91446" |
4109 | integrity sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw== | 4055 | integrity sha512-GDKPd+vk4jvSuvCbyuzx/unmXkk090Azec7LovXP8as1Hn8q9p3hbjmDGbUqqhknw0ajwit6LiiWqfiTUPMK7w== |
4110 | dependencies: | 4056 | dependencies: |
4111 | domelementtype "^2.0.1" | 4057 | domelementtype "^2.0.1" |
4112 | domhandler "^4.0.0" | 4058 | domhandler "^4.0.0" |
@@ -4167,7 +4113,7 @@ http-proxy-agent@^4.0.1: | |||
4167 | agent-base "6" | 4113 | agent-base "6" |
4168 | debug "4" | 4114 | debug "4" |
4169 | 4115 | ||
4170 | http-signature@1.3.5, http-signature@~1.2.0: | 4116 | http-signature@1.3.5: |
4171 | version "1.3.5" | 4117 | version "1.3.5" |
4172 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.5.tgz#9f19496ffbf3227298d7b5f156e0e1a948678683" | 4118 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.5.tgz#9f19496ffbf3227298d7b5f156e0e1a948678683" |
4173 | integrity sha512-NwoTQYSJoFt34jSBbwzDHDofoA61NGXzu6wXh95o1Ry62EnmKjXb/nR/RknLeZ3G/uGwrlKNY2z7uPt+Cdl7Tw== | 4119 | integrity sha512-NwoTQYSJoFt34jSBbwzDHDofoA61NGXzu6wXh95o1Ry62EnmKjXb/nR/RknLeZ3G/uGwrlKNY2z7uPt+Cdl7Tw== |
@@ -4176,6 +4122,15 @@ http-signature@1.3.5, http-signature@~1.2.0: | |||
4176 | jsprim "^1.2.2" | 4122 | jsprim "^1.2.2" |
4177 | sshpk "^1.14.1" | 4123 | sshpk "^1.14.1" |
4178 | 4124 | ||
4125 | http-signature@~1.2.0: | ||
4126 | version "1.2.0" | ||
4127 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" | ||
4128 | integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= | ||
4129 | dependencies: | ||
4130 | assert-plus "^1.0.0" | ||
4131 | jsprim "^1.2.2" | ||
4132 | sshpk "^1.7.0" | ||
4133 | |||
4179 | http2-wrapper@^1.0.0-beta.5.2: | 4134 | http2-wrapper@^1.0.0-beta.5.2: |
4180 | version "1.0.3" | 4135 | version "1.0.3" |
4181 | resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" | 4136 | resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" |
@@ -4245,13 +4200,6 @@ ignore-by-default@^1.0.1: | |||
4245 | resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" | 4200 | resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" |
4246 | integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= | 4201 | integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= |
4247 | 4202 | ||
4248 | ignore-walk@^3.0.1: | ||
4249 | version "3.0.3" | ||
4250 | resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" | ||
4251 | integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== | ||
4252 | dependencies: | ||
4253 | minimatch "^3.0.4" | ||
4254 | |||
4255 | ignore@^4.0.6: | 4203 | ignore@^4.0.6: |
4256 | version "4.0.6" | 4204 | version "4.0.6" |
4257 | resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" | 4205 | resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" |
@@ -4446,7 +4394,7 @@ is-buffer@~1.1.6: | |||
4446 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" | 4394 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" |
4447 | integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== | 4395 | integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== |
4448 | 4396 | ||
4449 | is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.2, is-callable@^1.2.3: | 4397 | is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.3: |
4450 | version "1.2.3" | 4398 | version "1.2.3" |
4451 | resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" | 4399 | resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" |
4452 | integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== | 4400 | integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== |
@@ -4542,11 +4490,6 @@ is-installed-globally@^0.3.1: | |||
4542 | global-dirs "^2.0.1" | 4490 | global-dirs "^2.0.1" |
4543 | is-path-inside "^3.0.1" | 4491 | is-path-inside "^3.0.1" |
4544 | 4492 | ||
4545 | is-interactive@^1.0.0: | ||
4546 | version "1.0.0" | ||
4547 | resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" | ||
4548 | integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== | ||
4549 | |||
4550 | is-nan@^1.3.0: | 4493 | is-nan@^1.3.0: |
4551 | version "1.3.2" | 4494 | version "1.3.2" |
4552 | resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" | 4495 | resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" |
@@ -4581,9 +4524,9 @@ is-obj@^2.0.0: | |||
4581 | integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== | 4524 | integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== |
4582 | 4525 | ||
4583 | is-path-inside@^3.0.1: | 4526 | is-path-inside@^3.0.1: |
4584 | version "3.0.2" | 4527 | version "3.0.3" |
4585 | resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" | 4528 | resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" |
4586 | integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== | 4529 | integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== |
4587 | 4530 | ||
4588 | is-plain-obj@^1.1.0: | 4531 | is-plain-obj@^1.1.0: |
4589 | version "1.1.0" | 4532 | version "1.1.0" |
@@ -4605,7 +4548,7 @@ is-promise@^2.0.0, is-promise@^2.2.2: | |||
4605 | resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" | 4548 | resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" |
4606 | integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== | 4549 | integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== |
4607 | 4550 | ||
4608 | is-regex@^1.0.3, is-regex@^1.1.1, is-regex@^1.1.2: | 4551 | is-regex@^1.0.3, is-regex@^1.1.2: |
4609 | version "1.1.2" | 4552 | version "1.1.2" |
4610 | resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" | 4553 | resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" |
4611 | integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== | 4554 | integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== |
@@ -4947,15 +4890,6 @@ libqp@1.1.0: | |||
4947 | resolved "https://registry.yarnpkg.com/libqp/-/libqp-1.1.0.tgz#f5e6e06ad74b794fb5b5b66988bf728ef1dedbe8" | 4890 | resolved "https://registry.yarnpkg.com/libqp/-/libqp-1.1.0.tgz#f5e6e06ad74b794fb5b5b66988bf728ef1dedbe8" |
4948 | integrity sha1-9ebgatdLeU+1tbZpiL9yjvHe2+g= | 4891 | integrity sha1-9ebgatdLeU+1tbZpiL9yjvHe2+g= |
4949 | 4892 | ||
4950 | libxmljs@0.19.7: | ||
4951 | version "0.19.7" | ||
4952 | resolved "https://registry.yarnpkg.com/libxmljs/-/libxmljs-0.19.7.tgz#96c2151b0b73f33dd29917edec82902587004e5a" | ||
4953 | integrity sha512-lFJyG9T1mVwTzNTw6ZkvIt0O+NsIR+FTE+RcC2QDFGU8YMnQrnyEOGrj6HWSe1AdwQK7s37BOp4NL+pcAqfK2g== | ||
4954 | dependencies: | ||
4955 | bindings "~1.3.0" | ||
4956 | nan "~2.14.0" | ||
4957 | node-pre-gyp "~0.11.0" | ||
4958 | |||
4959 | lines-and-columns@^1.1.6: | 4893 | lines-and-columns@^1.1.6: |
4960 | version "1.1.6" | 4894 | version "1.1.6" |
4961 | resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" | 4895 | resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" |
@@ -5063,12 +4997,17 @@ lodash.isequal@^4.5.0: | |||
5063 | resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" | 4997 | resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" |
5064 | integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= | 4998 | integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= |
5065 | 4999 | ||
5000 | lodash@4.17.19: | ||
5001 | version "4.17.19" | ||
5002 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" | ||
5003 | integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== | ||
5004 | |||
5066 | lodash@4.17.21, lodash@>=4.17.13, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: | 5005 | lodash@4.17.21, lodash@>=4.17.13, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: |
5067 | version "4.17.21" | 5006 | version "4.17.21" |
5068 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" | 5007 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" |
5069 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== | 5008 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== |
5070 | 5009 | ||
5071 | log-symbols@4.0.0, log-symbols@^4.0.0: | 5010 | log-symbols@4.0.0: |
5072 | version "4.0.0" | 5011 | version "4.0.0" |
5073 | resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" | 5012 | resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" |
5074 | integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== | 5013 | integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== |
@@ -5199,7 +5138,7 @@ mailsplit@5.0.1: | |||
5199 | libmime "5.0.0" | 5138 | libmime "5.0.0" |
5200 | libqp "1.1.0" | 5139 | libqp "1.1.0" |
5201 | 5140 | ||
5202 | make-dir@^3.0.0: | 5141 | make-dir@^3.0.0, make-dir@^3.1.0: |
5203 | version "3.1.0" | 5142 | version "3.1.0" |
5204 | resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" | 5143 | resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" |
5205 | integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== | 5144 | integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== |
@@ -5449,14 +5388,6 @@ minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.5: | |||
5449 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" | 5388 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" |
5450 | integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== | 5389 | integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== |
5451 | 5390 | ||
5452 | minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: | ||
5453 | version "2.9.0" | ||
5454 | resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" | ||
5455 | integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== | ||
5456 | dependencies: | ||
5457 | safe-buffer "^5.1.2" | ||
5458 | yallist "^3.0.0" | ||
5459 | |||
5460 | minipass@^3.0.0: | 5391 | minipass@^3.0.0: |
5461 | version "3.1.3" | 5392 | version "3.1.3" |
5462 | resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" | 5393 | resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" |
@@ -5464,13 +5395,6 @@ minipass@^3.0.0: | |||
5464 | dependencies: | 5395 | dependencies: |
5465 | yallist "^4.0.0" | 5396 | yallist "^4.0.0" |
5466 | 5397 | ||
5467 | minizlib@^1.2.1: | ||
5468 | version "1.3.3" | ||
5469 | resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" | ||
5470 | integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== | ||
5471 | dependencies: | ||
5472 | minipass "^2.9.0" | ||
5473 | |||
5474 | minizlib@^2.1.1: | 5398 | minizlib@^2.1.1: |
5475 | version "2.1.2" | 5399 | version "2.1.2" |
5476 | resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" | 5400 | resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" |
@@ -5484,7 +5408,7 @@ mkdirp-classic@^0.5.2: | |||
5484 | resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" | 5408 | resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" |
5485 | integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== | 5409 | integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== |
5486 | 5410 | ||
5487 | mkdirp@0.x.x, mkdirp@^0.5.0, mkdirp@^0.5.1: | 5411 | mkdirp@0.x.x, mkdirp@^0.5.1: |
5488 | version "0.5.5" | 5412 | version "0.5.5" |
5489 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" | 5413 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" |
5490 | integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== | 5414 | integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== |
@@ -5496,15 +5420,15 @@ mkdirp@1.0.3: | |||
5496 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea" | 5420 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea" |
5497 | integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g== | 5421 | integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g== |
5498 | 5422 | ||
5499 | mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.4: | 5423 | mkdirp@^1.0.3, mkdirp@~1.0.4: |
5500 | version "1.0.4" | 5424 | version "1.0.4" |
5501 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" | 5425 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" |
5502 | integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== | 5426 | integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== |
5503 | 5427 | ||
5504 | mocha@^8.0.1: | 5428 | mocha@^8.0.1: |
5505 | version "8.3.0" | 5429 | version "8.3.2" |
5506 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.3.0.tgz#a83a7432d382ae1ca29686062d7fdc2c36f63fe5" | 5430 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.3.2.tgz#53406f195fa86fbdebe71f8b1c6fb23221d69fcc" |
5507 | integrity sha512-TQqyC89V1J/Vxx0DhJIXlq9gbbL9XFNdeLQ1+JsnZsVaSOV1z3tWfw0qZmQJGQRIfkvZcs7snQnZnOCKoldq1Q== | 5431 | integrity sha512-UdmISwr/5w+uXLPKspgoV7/RXZwKRTiTjJ2/AC5ZiEztIoOYdfKb19+9jNmEInzx5pBsCyJQzarAxqIGBNYJhg== |
5508 | dependencies: | 5432 | dependencies: |
5509 | "@ungap/promise-all-settled" "1.1.2" | 5433 | "@ungap/promise-all-settled" "1.1.2" |
5510 | ansi-colors "4.1.1" | 5434 | ansi-colors "4.1.1" |
@@ -5635,16 +5559,16 @@ mute-stream@0.0.8, mute-stream@~0.0.4: | |||
5635 | resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" | 5559 | resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" |
5636 | integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== | 5560 | integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== |
5637 | 5561 | ||
5638 | nan@~2.14.0: | 5562 | nanoid@3.1.20: |
5639 | version "2.14.2" | ||
5640 | resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" | ||
5641 | integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== | ||
5642 | |||
5643 | nanoid@3.1.20, nanoid@^3.1.20: | ||
5644 | version "3.1.20" | 5563 | version "3.1.20" |
5645 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" | 5564 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" |
5646 | integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== | 5565 | integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== |
5647 | 5566 | ||
5567 | nanoid@^3.1.20: | ||
5568 | version "3.1.21" | ||
5569 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.21.tgz#25bfee7340ac4185866fbfb2c9006d299da1be7f" | ||
5570 | integrity sha512-A6oZraK4DJkAOICstsGH98dvycPr/4GGDH7ZWKmMdd3vGcOurZ6JmWFUt0DA5bzrrn2FrUjmv6mFNWvv8jpppA== | ||
5571 | |||
5648 | napi-macros@^2.0.0: | 5572 | napi-macros@^2.0.0: |
5649 | version "2.0.0" | 5573 | version "2.0.0" |
5650 | resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b" | 5574 | resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b" |
@@ -5660,15 +5584,6 @@ ncp@1.0.x: | |||
5660 | resolved "https://registry.yarnpkg.com/ncp/-/ncp-1.0.1.tgz#d15367e5cb87432ba117d2bf80fdf45aecfb4246" | 5584 | resolved "https://registry.yarnpkg.com/ncp/-/ncp-1.0.1.tgz#d15367e5cb87432ba117d2bf80fdf45aecfb4246" |
5661 | integrity sha1-0VNn5cuHQyuhF9K/gP30Wuz7QkY= | 5585 | integrity sha1-0VNn5cuHQyuhF9K/gP30Wuz7QkY= |
5662 | 5586 | ||
5663 | needle@^2.2.1: | ||
5664 | version "2.6.0" | ||
5665 | resolved "https://registry.yarnpkg.com/needle/-/needle-2.6.0.tgz#24dbb55f2509e2324b4a99d61f413982013ccdbe" | ||
5666 | integrity sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg== | ||
5667 | dependencies: | ||
5668 | debug "^3.2.6" | ||
5669 | iconv-lite "^0.4.4" | ||
5670 | sax "^1.2.4" | ||
5671 | |||
5672 | negotiator@0.6.2: | 5587 | negotiator@0.6.2: |
5673 | version "0.6.2" | 5588 | version "0.6.2" |
5674 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" | 5589 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" |
@@ -5727,22 +5642,6 @@ node-media-server@^2.1.4: | |||
5727 | mkdirp "1.0.3" | 5642 | mkdirp "1.0.3" |
5728 | ws "^5.2.2" | 5643 | ws "^5.2.2" |
5729 | 5644 | ||
5730 | node-pre-gyp@~0.11.0: | ||
5731 | version "0.11.0" | ||
5732 | resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054" | ||
5733 | integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q== | ||
5734 | dependencies: | ||
5735 | detect-libc "^1.0.2" | ||
5736 | mkdirp "^0.5.1" | ||
5737 | needle "^2.2.1" | ||
5738 | nopt "^4.0.1" | ||
5739 | npm-packlist "^1.1.6" | ||
5740 | npmlog "^4.0.2" | ||
5741 | rc "^1.2.7" | ||
5742 | rimraf "^2.6.1" | ||
5743 | semver "^5.3.0" | ||
5744 | tar "^4" | ||
5745 | |||
5746 | nodemailer@5.0.0: | 5645 | nodemailer@5.0.0: |
5747 | version "5.0.0" | 5646 | version "5.0.0" |
5748 | resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-5.0.0.tgz#bcb409eca613114e85de42646d0ce7f1fa70b716" | 5647 | resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-5.0.0.tgz#bcb409eca613114e85de42646d0ce7f1fa70b716" |
@@ -5779,14 +5678,6 @@ nodemon@^2.0.1: | |||
5779 | undefsafe "^2.0.3" | 5678 | undefsafe "^2.0.3" |
5780 | update-notifier "^4.1.0" | 5679 | update-notifier "^4.1.0" |
5781 | 5680 | ||
5782 | nopt@^4.0.1: | ||
5783 | version "4.0.3" | ||
5784 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" | ||
5785 | integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== | ||
5786 | dependencies: | ||
5787 | abbrev "1" | ||
5788 | osenv "^0.1.4" | ||
5789 | |||
5790 | nopt@^5.0.0: | 5681 | nopt@^5.0.0: |
5791 | version "5.0.0" | 5682 | version "5.0.0" |
5792 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" | 5683 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" |
@@ -5821,27 +5712,6 @@ normalize-url@^4.1.0: | |||
5821 | resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" | 5712 | resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" |
5822 | integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== | 5713 | integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== |
5823 | 5714 | ||
5824 | npm-bundled@^1.0.1: | ||
5825 | version "1.1.1" | ||
5826 | resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" | ||
5827 | integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA== | ||
5828 | dependencies: | ||
5829 | npm-normalize-package-bin "^1.0.1" | ||
5830 | |||
5831 | npm-normalize-package-bin@^1.0.1: | ||
5832 | version "1.0.1" | ||
5833 | resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" | ||
5834 | integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== | ||
5835 | |||
5836 | npm-packlist@^1.1.6: | ||
5837 | version "1.4.8" | ||
5838 | resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" | ||
5839 | integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== | ||
5840 | dependencies: | ||
5841 | ignore-walk "^3.0.1" | ||
5842 | npm-bundled "^1.0.1" | ||
5843 | npm-normalize-package-bin "^1.0.1" | ||
5844 | |||
5845 | npm-run-path@^2.0.0: | 5715 | npm-run-path@^2.0.0: |
5846 | version "2.0.2" | 5716 | version "2.0.2" |
5847 | resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" | 5717 | resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" |
@@ -5856,7 +5726,7 @@ npm-run-path@^4.0.1: | |||
5856 | dependencies: | 5726 | dependencies: |
5857 | path-key "^3.0.0" | 5727 | path-key "^3.0.0" |
5858 | 5728 | ||
5859 | npmlog@^4.0.2, npmlog@^4.1.2: | 5729 | npmlog@^4.1.2: |
5860 | version "4.1.2" | 5730 | version "4.1.2" |
5861 | resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" | 5731 | resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" |
5862 | integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== | 5732 | integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== |
@@ -5883,17 +5753,17 @@ oauth-sign@~0.9.0: | |||
5883 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" | 5753 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" |
5884 | integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== | 5754 | integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== |
5885 | 5755 | ||
5886 | oauth2-server@3.0.0, oauth2-server@3.1.0-beta.1: | 5756 | oauth2-server@3.1.1: |
5887 | version "3.1.0-beta.1" | 5757 | version "3.1.1" |
5888 | resolved "https://registry.yarnpkg.com/oauth2-server/-/oauth2-server-3.1.0-beta.1.tgz#159ee4d32d148c2dc7a39f7b1ce872e039b91a41" | 5758 | resolved "https://registry.yarnpkg.com/oauth2-server/-/oauth2-server-3.1.1.tgz#be291da840a307a50368736ab766bd68f2eeb3a9" |
5889 | integrity sha512-FWLl/YC5NGvGzAtclhmlY9fG0nKwDP7xPiPOi5fZ4APO34BmF/vxsEp22spJNuSOrGEsp9W7jKtFCI3UBSvx5w== | 5759 | integrity sha512-4dv+fE9hrK+xTaCygOLh/kQeFzbFr7UqSyHvBDbrQq8Hg52sAkV2vTsyH3Z42hoeaKpbhM7udhL8Y4GYbl6TGQ== |
5890 | dependencies: | 5760 | dependencies: |
5891 | basic-auth "^2.0.0" | 5761 | basic-auth "2.0.1" |
5892 | bluebird "^3.5.1" | 5762 | bluebird "3.7.2" |
5893 | lodash "^4.17.10" | 5763 | lodash "4.17.19" |
5894 | promisify-any "^2.0.1" | 5764 | promisify-any "2.0.1" |
5895 | statuses "^1.5.0" | 5765 | statuses "1.5.0" |
5896 | type-is "^1.6.16" | 5766 | type-is "1.6.18" |
5897 | 5767 | ||
5898 | object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: | 5768 | object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: |
5899 | version "4.1.1" | 5769 | version "4.1.1" |
@@ -5910,7 +5780,7 @@ object-hash@2.1.1: | |||
5910 | resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.1.1.tgz#9447d0279b4fcf80cff3259bf66a1dc73afabe09" | 5780 | resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.1.1.tgz#9447d0279b4fcf80cff3259bf66a1dc73afabe09" |
5911 | integrity sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ== | 5781 | integrity sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ== |
5912 | 5782 | ||
5913 | object-inspect@^1.8.0, object-inspect@^1.9.0: | 5783 | object-inspect@^1.9.0: |
5914 | version "1.9.0" | 5784 | version "1.9.0" |
5915 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" | 5785 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" |
5916 | integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== | 5786 | integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== |
@@ -5920,7 +5790,7 @@ object-keys@^1.0.12, object-keys@^1.1.1: | |||
5920 | resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" | 5790 | resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" |
5921 | integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== | 5791 | integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== |
5922 | 5792 | ||
5923 | object.assign@^4.1.1, object.assign@^4.1.2: | 5793 | object.assign@^4.1.2: |
5924 | version "4.1.2" | 5794 | version "4.1.2" |
5925 | resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" | 5795 | resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" |
5926 | integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== | 5796 | integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== |
@@ -6029,47 +5899,20 @@ optionator@^0.9.1: | |||
6029 | type-check "^0.4.0" | 5899 | type-check "^0.4.0" |
6030 | word-wrap "^1.2.3" | 5900 | word-wrap "^1.2.3" |
6031 | 5901 | ||
6032 | ora@^5.1.0: | 5902 | os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: |
6033 | version "5.3.0" | ||
6034 | resolved "https://registry.yarnpkg.com/ora/-/ora-5.3.0.tgz#fb832899d3a1372fe71c8b2c534bbfe74961bb6f" | ||
6035 | integrity sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g== | ||
6036 | dependencies: | ||
6037 | bl "^4.0.3" | ||
6038 | chalk "^4.1.0" | ||
6039 | cli-cursor "^3.1.0" | ||
6040 | cli-spinners "^2.5.0" | ||
6041 | is-interactive "^1.0.0" | ||
6042 | log-symbols "^4.0.0" | ||
6043 | strip-ansi "^6.0.0" | ||
6044 | wcwidth "^1.0.1" | ||
6045 | |||
6046 | os-homedir@^1.0.0: | ||
6047 | version "1.0.2" | ||
6048 | resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" | ||
6049 | integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= | ||
6050 | |||
6051 | os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: | ||
6052 | version "1.0.2" | 5903 | version "1.0.2" |
6053 | resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" | 5904 | resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" |
6054 | integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= | 5905 | integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= |
6055 | 5906 | ||
6056 | osenv@^0.1.4: | ||
6057 | version "0.1.5" | ||
6058 | resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" | ||
6059 | integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== | ||
6060 | dependencies: | ||
6061 | os-homedir "^1.0.0" | ||
6062 | os-tmpdir "^1.0.0" | ||
6063 | |||
6064 | p-cancelable@^1.0.0: | 5907 | p-cancelable@^1.0.0: |
6065 | version "1.1.0" | 5908 | version "1.1.0" |
6066 | resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" | 5909 | resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" |
6067 | integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== | 5910 | integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== |
6068 | 5911 | ||
6069 | p-cancelable@^2.0.0: | 5912 | p-cancelable@^2.0.0: |
6070 | version "2.0.0" | 5913 | version "2.1.0" |
6071 | resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e" | 5914 | resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.0.tgz#4d51c3b91f483d02a0d300765321fca393d758dd" |
6072 | integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== | 5915 | integrity sha512-HAZyB3ZodPo+BDpb4/Iu7Jv4P6cSazBz9ZM0ChhEXp70scx834aWCEjQRwgt41UzzejUAPdbqqONfRWTPYrPAQ== |
6073 | 5916 | ||
6074 | p-finally@^1.0.0: | 5917 | p-finally@^1.0.0: |
6075 | version "1.0.0" | 5918 | version "1.0.0" |
@@ -6490,11 +6333,11 @@ pngjs@^3.0.0, pngjs@^3.3.3: | |||
6490 | integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== | 6333 | integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== |
6491 | 6334 | ||
6492 | postcss@^8.0.2: | 6335 | postcss@^8.0.2: |
6493 | version "8.2.6" | 6336 | version "8.2.8" |
6494 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.6.tgz#5d69a974543b45f87e464bc4c3e392a97d6be9fe" | 6337 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.8.tgz#0b90f9382efda424c4f0f69a2ead6f6830d08ece" |
6495 | integrity sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg== | 6338 | integrity sha512-1F0Xb2T21xET7oQV9eKuctbM9S7BC0fetoHCc4H13z0PT6haiRLP4T0ZY4XWh7iLP0usgqykT6p9B2RtOf4FPw== |
6496 | dependencies: | 6339 | dependencies: |
6497 | colorette "^1.2.1" | 6340 | colorette "^1.2.2" |
6498 | nanoid "^3.1.20" | 6341 | nanoid "^3.1.20" |
6499 | source-map "^0.6.1" | 6342 | source-map "^0.6.1" |
6500 | 6343 | ||
@@ -6579,7 +6422,7 @@ promise@^7.0.1: | |||
6579 | dependencies: | 6422 | dependencies: |
6580 | asap "~2.0.3" | 6423 | asap "~2.0.3" |
6581 | 6424 | ||
6582 | promisify-any@^2.0.1: | 6425 | promisify-any@2.0.1: |
6583 | version "2.0.1" | 6426 | version "2.0.1" |
6584 | resolved "https://registry.yarnpkg.com/promisify-any/-/promisify-any-2.0.1.tgz#403e00a8813f175242ab50fe33a69f8eece47305" | 6427 | resolved "https://registry.yarnpkg.com/promisify-any/-/promisify-any-2.0.1.tgz#403e00a8813f175242ab50fe33a69f8eece47305" |
6585 | integrity sha1-QD4AqIE/F1JCq1D+M6afjuzkcwU= | 6428 | integrity sha1-QD4AqIE/F1JCq1D+M6afjuzkcwU= |
@@ -6841,7 +6684,7 @@ raw-body@2.4.0: | |||
6841 | iconv-lite "0.4.24" | 6684 | iconv-lite "0.4.24" |
6842 | unpipe "1.0.0" | 6685 | unpipe "1.0.0" |
6843 | 6686 | ||
6844 | rc@^1.2.7, rc@^1.2.8: | 6687 | rc@^1.2.8: |
6845 | version "1.2.8" | 6688 | version "1.2.8" |
6846 | resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" | 6689 | resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" |
6847 | integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== | 6690 | integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== |
@@ -7060,7 +6903,7 @@ render-media@^4.1.0: | |||
7060 | stream-to-blob-url "^3.0.2" | 6903 | stream-to-blob-url "^3.0.2" |
7061 | videostream "^3.2.2" | 6904 | videostream "^3.2.2" |
7062 | 6905 | ||
7063 | request@^2.81.0, request@^2.88.0: | 6906 | request@^2.88.0: |
7064 | version "2.88.2" | 6907 | version "2.88.2" |
7065 | resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" | 6908 | resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" |
7066 | integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== | 6909 | integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== |
@@ -7153,10 +6996,10 @@ restore-cursor@^3.1.0: | |||
7153 | onetime "^5.1.0" | 6996 | onetime "^5.1.0" |
7154 | signal-exit "^3.0.2" | 6997 | signal-exit "^3.0.2" |
7155 | 6998 | ||
7156 | retimer@^2.0.0: | 6999 | retimer@^3.0.0: |
7157 | version "2.0.0" | 7000 | version "3.0.0" |
7158 | resolved "https://registry.yarnpkg.com/retimer/-/retimer-2.0.0.tgz#e8bd68c5e5a8ec2f49ccb5c636db84c04063bbca" | 7001 | resolved "https://registry.yarnpkg.com/retimer/-/retimer-3.0.0.tgz#98b751b1feaf1af13eb0228f8ea68b8f9da530df" |
7159 | integrity sha512-KLXY85WkEq2V2bKex/LOO1ViXVn2KGYe4PYysAdYdjmraYIUsVkXu8O4am+8+5UbaaGl1qho4aqAAPHNQ4GSbg== | 7002 | integrity sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA== |
7160 | 7003 | ||
7161 | retry-as-promised@^3.2.0: | 7004 | retry-as-promised@^3.2.0: |
7162 | version "3.2.0" | 7005 | version "3.2.0" |
@@ -7175,7 +7018,7 @@ revalidator@0.1.x: | |||
7175 | resolved "https://registry.yarnpkg.com/revalidator/-/revalidator-0.1.8.tgz#fece61bfa0c1b52a206bd6b18198184bdd523a3b" | 7018 | resolved "https://registry.yarnpkg.com/revalidator/-/revalidator-0.1.8.tgz#fece61bfa0c1b52a206bd6b18198184bdd523a3b" |
7176 | integrity sha1-/s5hv6DBtSoga9axgZgYS91SOjs= | 7019 | integrity sha1-/s5hv6DBtSoga9axgZgYS91SOjs= |
7177 | 7020 | ||
7178 | rimraf@2.x.x, rimraf@^2.6.1, rimraf@^2.6.3: | 7021 | rimraf@2.x.x, rimraf@^2.6.3: |
7179 | version "2.7.1" | 7022 | version "2.7.1" |
7180 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" | 7023 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" |
7181 | integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== | 7024 | integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== |
@@ -7544,9 +7387,9 @@ socket.io-client@2.2.0: | |||
7544 | to-array "0.1.4" | 7387 | to-array "0.1.4" |
7545 | 7388 | ||
7546 | socket.io-client@^3.0.2: | 7389 | socket.io-client@^3.0.2: |
7547 | version "3.1.2" | 7390 | version "3.1.3" |
7548 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-3.1.2.tgz#77be8c180cef29121970856e8f48e5463631020a" | 7391 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-3.1.3.tgz#57ddcefea58cfab71f0e94c21124de8e3c5aa3e2" |
7549 | integrity sha512-fXhF8plHrd7U14A7K0JPOmZzpmGkLpIS6623DzrBZqYzI/yvlP4fA3LnxwthEVgiHmn2uJ4KjdnQD8A03PuBWQ== | 7392 | integrity sha512-4sIGOGOmCg3AOgGi7EEr6ZkTZRkrXwub70bBB/F0JSkMOUFpA77WsL87o34DffQQ31PkbMUIadGOk+3tx1KGbw== |
7550 | dependencies: | 7393 | dependencies: |
7551 | "@types/component-emitter" "^1.2.10" | 7394 | "@types/component-emitter" "^1.2.10" |
7552 | backo2 "~1.0.2" | 7395 | backo2 "~1.0.2" |
@@ -7700,7 +7543,7 @@ srt-to-vtt@^1.1.2: | |||
7700 | through2 "^0.6.3" | 7543 | through2 "^0.6.3" |
7701 | to-utf-8 "^1.2.0" | 7544 | to-utf-8 "^1.2.0" |
7702 | 7545 | ||
7703 | sshpk@^1.14.1: | 7546 | sshpk@^1.14.1, sshpk@^1.7.0: |
7704 | version "1.16.1" | 7547 | version "1.16.1" |
7705 | resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" | 7548 | resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" |
7706 | integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== | 7549 | integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== |
@@ -7725,7 +7568,7 @@ standard-as-callback@^2.0.1: | |||
7725 | resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.0.1.tgz#ed8bb25648e15831759b6023bdb87e6b60b38126" | 7568 | resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.0.1.tgz#ed8bb25648e15831759b6023bdb87e6b60b38126" |
7726 | integrity sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg== | 7569 | integrity sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg== |
7727 | 7570 | ||
7728 | "statuses@>= 1.5.0 < 2", statuses@^1.5.0, statuses@~1.5.0: | 7571 | statuses@1.5.0, "statuses@>= 1.5.0 < 2", statuses@~1.5.0: |
7729 | version "1.5.0" | 7572 | version "1.5.0" |
7730 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" | 7573 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" |
7731 | integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= | 7574 | integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= |
@@ -7811,7 +7654,7 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: | |||
7811 | is-fullwidth-code-point "^3.0.0" | 7654 | is-fullwidth-code-point "^3.0.0" |
7812 | strip-ansi "^6.0.0" | 7655 | strip-ansi "^6.0.0" |
7813 | 7656 | ||
7814 | string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.4: | 7657 | string.prototype.trimend@^1.0.4: |
7815 | version "1.0.4" | 7658 | version "1.0.4" |
7816 | resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" | 7659 | resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" |
7817 | integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== | 7660 | integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== |
@@ -7819,7 +7662,7 @@ string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.4: | |||
7819 | call-bind "^1.0.2" | 7662 | call-bind "^1.0.2" |
7820 | define-properties "^1.1.3" | 7663 | define-properties "^1.1.3" |
7821 | 7664 | ||
7822 | string.prototype.trimstart@^1.0.1, string.prototype.trimstart@^1.0.4: | 7665 | string.prototype.trimstart@^1.0.4: |
7823 | version "1.0.4" | 7666 | version "1.0.4" |
7824 | resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" | 7667 | resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" |
7825 | integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== | 7668 | integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== |
@@ -7982,19 +7825,6 @@ table@^6.0.4: | |||
7982 | slice-ansi "^4.0.0" | 7825 | slice-ansi "^4.0.0" |
7983 | string-width "^4.2.0" | 7826 | string-width "^4.2.0" |
7984 | 7827 | ||
7985 | tar@^4: | ||
7986 | version "4.4.13" | ||
7987 | resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" | ||
7988 | integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== | ||
7989 | dependencies: | ||
7990 | chownr "^1.1.1" | ||
7991 | fs-minipass "^1.2.5" | ||
7992 | minipass "^2.8.6" | ||
7993 | minizlib "^1.2.1" | ||
7994 | mkdirp "^0.5.0" | ||
7995 | safe-buffer "^5.1.2" | ||
7996 | yallist "^3.0.3" | ||
7997 | |||
7998 | tar@^6.1.0: | 7828 | tar@^6.1.0: |
7999 | version "6.1.0" | 7829 | version "6.1.0" |
8000 | resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83" | 7830 | resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83" |
@@ -8248,9 +8078,9 @@ tslib@^1.8.1, tslib@^1.9.0: | |||
8248 | integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== | 8078 | integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== |
8249 | 8079 | ||
8250 | tsutils@^3.17.1: | 8080 | tsutils@^3.17.1: |
8251 | version "3.20.0" | 8081 | version "3.21.0" |
8252 | resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.20.0.tgz#ea03ea45462e146b53d70ce0893de453ff24f698" | 8082 | resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" |
8253 | integrity sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg== | 8083 | integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== |
8254 | dependencies: | 8084 | dependencies: |
8255 | tslib "^1.8.1" | 8085 | tslib "^1.8.1" |
8256 | 8086 | ||
@@ -8298,7 +8128,7 @@ type-fest@^0.8.1: | |||
8298 | resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" | 8128 | resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" |
8299 | integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== | 8129 | integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== |
8300 | 8130 | ||
8301 | type-is@^1.6.16, type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18: | 8131 | type-is@1.6.18, type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18: |
8302 | version "1.6.18" | 8132 | version "1.6.18" |
8303 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" | 8133 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" |
8304 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== | 8134 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== |
@@ -8312,9 +8142,9 @@ type@^1.0.1: | |||
8312 | integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== | 8142 | integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== |
8313 | 8143 | ||
8314 | type@^2.0.0: | 8144 | type@^2.0.0: |
8315 | version "2.3.0" | 8145 | version "2.5.0" |
8316 | resolved "https://registry.yarnpkg.com/type/-/type-2.3.0.tgz#ada7c045f07ead08abf9e2edd29be1a0c0661132" | 8146 | resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d" |
8317 | integrity sha512-rgPIqOdfK/4J9FhiVrZ3cveAjRRo5rsQBAIhnylX874y1DX/kEKSVdLsnuHB6l1KTjHyU01VjiMBHgU2adejyg== | 8147 | integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw== |
8318 | 8148 | ||
8319 | typedarray-to-buffer@^3.0.0, typedarray-to-buffer@^3.1.5: | 8149 | typedarray-to-buffer@^3.0.0, typedarray-to-buffer@^3.1.5: |
8320 | version "3.1.5" | 8150 | version "3.1.5" |
@@ -8329,9 +8159,9 @@ typedarray@^0.0.6: | |||
8329 | integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= | 8159 | integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= |
8330 | 8160 | ||
8331 | typescript@^4.0.5: | 8161 | typescript@^4.0.5: |
8332 | version "4.2.2" | 8162 | version "4.2.3" |
8333 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.2.tgz#1450f020618f872db0ea17317d16d8da8ddb8c4c" | 8163 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" |
8334 | integrity sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ== | 8164 | integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== |
8335 | 8165 | ||
8336 | uc.micro@^1.0.1, uc.micro@^1.0.5: | 8166 | uc.micro@^1.0.1, uc.micro@^1.0.5: |
8337 | version "1.0.6" | 8167 | version "1.0.6" |
@@ -8541,9 +8371,9 @@ uuid@^3.3.2, uuid@^3.4.0: | |||
8541 | integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== | 8371 | integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== |
8542 | 8372 | ||
8543 | v8-compile-cache@^2.0.3: | 8373 | v8-compile-cache@^2.0.3: |
8544 | version "2.2.0" | 8374 | version "2.3.0" |
8545 | resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" | 8375 | resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" |
8546 | integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q== | 8376 | integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== |
8547 | 8377 | ||
8548 | valid-data-url@^3.0.0: | 8378 | valid-data-url@^3.0.0: |
8549 | version "3.0.1" | 8379 | version "3.0.1" |
@@ -8604,7 +8434,7 @@ void-elements@^3.1.0: | |||
8604 | resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" | 8434 | resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" |
8605 | integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk= | 8435 | integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk= |
8606 | 8436 | ||
8607 | wcwidth@>=1.0.1, wcwidth@^1.0.1: | 8437 | wcwidth@>=1.0.1: |
8608 | version "1.0.1" | 8438 | version "1.0.1" |
8609 | resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" | 8439 | resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" |
8610 | integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= | 8440 | integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= |
@@ -8631,9 +8461,9 @@ webfinger.js@^2.6.6: | |||
8631 | xhr2 "^0.1.4" | 8461 | xhr2 "^0.1.4" |
8632 | 8462 | ||
8633 | webtorrent@^0.115.1: | 8463 | webtorrent@^0.115.1: |
8634 | version "0.115.1" | 8464 | version "0.115.3" |
8635 | resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-0.115.1.tgz#3984e6b17fdb8ad68b5cdbd42c46a2288e1b3bb2" | 8465 | resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-0.115.3.tgz#2d0a53b65326ffd0124b3592950c4c75e299730a" |
8636 | integrity sha512-8kq498EMUjYu18wlfoZ42wvz9oUAJrobJbHQGRHl0sbrPVBt17H4FVoAc502XSMCbFzhMx5Vqd7Wz4JTTCPvuQ== | 8466 | integrity sha512-DNryTNoAHse+zxArBZg25U8B97KNPeVjGzrjRB+oDnGROuKfQcvLh8/9K79FDfQTYVpInMmr9l0ksIsEjz/L2g== |
8637 | dependencies: | 8467 | dependencies: |
8638 | addr-to-ip-port "^1.5.1" | 8468 | addr-to-ip-port "^1.5.1" |
8639 | bitfield "^4.0.0" | 8469 | bitfield "^4.0.0" |
@@ -8839,9 +8669,9 @@ ws@^5.2.2: | |||
8839 | async-limiter "~1.0.0" | 8669 | async-limiter "~1.0.0" |
8840 | 8670 | ||
8841 | ws@^7.0.0, ws@^7.3.0, ws@^7.4.2, ws@~7.4.2: | 8671 | ws@^7.0.0, ws@^7.3.0, ws@^7.4.2, ws@~7.4.2: |
8842 | version "7.4.3" | 8672 | version "7.4.4" |
8843 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd" | 8673 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" |
8844 | integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA== | 8674 | integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== |
8845 | 8675 | ||
8846 | ws@~6.1.0: | 8676 | ws@~6.1.0: |
8847 | version "6.1.4" | 8677 | version "6.1.4" |
@@ -8918,7 +8748,7 @@ yallist@^2.1.2: | |||
8918 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" | 8748 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" |
8919 | integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= | 8749 | integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= |
8920 | 8750 | ||
8921 | yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: | 8751 | yallist@^3.0.2: |
8922 | version "3.1.1" | 8752 | version "3.1.1" |
8923 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" | 8753 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" |
8924 | integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== | 8754 | integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== |
@@ -8957,9 +8787,9 @@ yargs-parser@^18.1.2: | |||
8957 | decamelize "^1.2.0" | 8787 | decamelize "^1.2.0" |
8958 | 8788 | ||
8959 | yargs-parser@^20.2.2: | 8789 | yargs-parser@^20.2.2: |
8960 | version "20.2.6" | 8790 | version "20.2.7" |
8961 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.6.tgz#69f920addf61aafc0b8b89002f5d66e28f2d8b20" | 8791 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" |
8962 | integrity sha512-AP1+fQIWSM/sMiET8fyayjx/J+JmTPt2Mr0FkrgqB4todtfa53sOsrSAcIrJRD5XS20bKUwaDIuMkWKCEiQLKA== | 8792 | integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== |
8963 | 8793 | ||
8964 | yargs-unparser@2.0.0: | 8794 | yargs-unparser@2.0.0: |
8965 | version "2.0.0" | 8795 | version "2.0.0" |