diff options
150 files changed, 2027 insertions, 1276 deletions
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 105bc12c3..379c0443e 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 | |||
@@ -9,8 +9,11 @@ | |||
9 | 9 | ||
10 | <div class="channel-avatar-row"> | 10 | <div class="channel-avatar-row"> |
11 | <my-actor-avatar | 11 | <my-actor-avatar |
12 | [channel]="videoChannel" [internalHref]="getVideoChannelLink(videoChannel)" | 12 | [channel]="videoChannel" |
13 | i18n-title title="See this video channel" | 13 | [internalHref]="getVideoChannelLink(videoChannel)" |
14 | i18n-title | ||
15 | title="See this video channel" | ||
16 | size="75" | ||
14 | ></my-actor-avatar> | 17 | ></my-actor-avatar> |
15 | 18 | ||
16 | <h2> | 19 | <h2> |
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 be9e94f69..30b8098be 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 | |||
@@ -29,7 +29,6 @@ | |||
29 | grid-template-rows: auto 1fr; | 29 | grid-template-rows: auto 1fr; |
30 | 30 | ||
31 | my-actor-avatar { | 31 | my-actor-avatar { |
32 | @include actor-avatar-size(75px); | ||
33 | @include margin-right(15px); | 32 | @include margin-right(15px); |
34 | 33 | ||
35 | grid-column: 1; | 34 | grid-column: 1; |
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index 8362e6b7e..1544ad034 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html | |||
@@ -2,7 +2,7 @@ | |||
2 | <div class="account-info"> | 2 | <div class="account-info"> |
3 | 3 | ||
4 | <div class="account-avatar-row"> | 4 | <div class="account-avatar-row"> |
5 | <my-actor-avatar class="main-avatar" [account]="account"></my-actor-avatar> | 5 | <my-actor-avatar class="main-avatar" [account]="account" size="120"></my-actor-avatar> |
6 | 6 | ||
7 | <div> | 7 | <div> |
8 | <div class="section-label" i18n>ACCOUNT</div> | 8 | <div class="section-label" i18n>ACCOUNT</div> |
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html deleted file mode 100644 index feade0c26..000000000 --- a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html +++ /dev/null | |||
@@ -1,56 +0,0 @@ | |||
1 | <p-table | ||
2 | [value]="blockedAccounts" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions" | ||
3 | [sortField]="sort.field" [sortOrder]="sort.order" | ||
4 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" | ||
5 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | ||
6 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} muted accounts" | ||
7 | > | ||
8 | <ng-template pTemplate="caption"> | ||
9 | <div class="caption"> | ||
10 | <div class="ml-auto"> | ||
11 | <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> | ||
12 | </div> | ||
13 | </div> | ||
14 | </ng-template> | ||
15 | |||
16 | <ng-template pTemplate="header"> | ||
17 | <tr> | ||
18 | <th style="width: 150px;">Action</th> <!-- column for action buttons --> | ||
19 | <th style="width: calc(100% - 300px);" i18n>Account</th> | ||
20 | <th style="width: 150px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th> | ||
21 | </tr> | ||
22 | </ng-template> | ||
23 | |||
24 | <ng-template pTemplate="body" let-accountBlock> | ||
25 | <tr> | ||
26 | <td class="action-cell"> | ||
27 | <button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button> | ||
28 | </td> | ||
29 | |||
30 | <td> | ||
31 | <a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> | ||
32 | <div class="chip two-lines"> | ||
33 | <my-actor-avatar [account]="accountBlock.blockedAccount"></my-actor-avatar> | ||
34 | <div> | ||
35 | {{ accountBlock.blockedAccount.displayName }} | ||
36 | <span class="text-muted">{{ accountBlock.blockedAccount.nameWithHost }}</span> | ||
37 | </div> | ||
38 | </div> | ||
39 | </a> | ||
40 | </td> | ||
41 | |||
42 | <td>{{ accountBlock.createdAt | date: 'short' }}</td> | ||
43 | </tr> | ||
44 | </ng-template> | ||
45 | |||
46 | <ng-template pTemplate="emptymessage"> | ||
47 | <tr> | ||
48 | <td colspan="6"> | ||
49 | <div class="no-results"> | ||
50 | <ng-container *ngIf="search" i18n>No account found matching current filters.</ng-container> | ||
51 | <ng-container *ngIf="!search" i18n>No account found.</ng-container> | ||
52 | </div> | ||
53 | </td> | ||
54 | </tr> | ||
55 | </ng-template> | ||
56 | </p-table> | ||
diff --git a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html index 9bf23c21a..0dbbbe1cc 100644 --- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html +++ b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html | |||
@@ -66,7 +66,7 @@ | |||
66 | <td> | 66 | <td> |
67 | <a [href]="videoComment.account.localUrl" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> | 67 | <a [href]="videoComment.account.localUrl" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> |
68 | <div class="chip two-lines"> | 68 | <div class="chip two-lines"> |
69 | <my-actor-avatar [account]="videoComment.account"></my-actor-avatar> | 69 | <my-actor-avatar [account]="videoComment.account" size="32"></my-actor-avatar> |
70 | <div> | 70 | <div> |
71 | {{ videoComment.account.displayName }} | 71 | {{ videoComment.account.displayName }} |
72 | <span>{{ videoComment.by }}</span> | 72 | <span>{{ videoComment.by }}</span> |
diff --git a/client/src/app/+manage/video-channel-edit/video-channel-update.component.ts b/client/src/app/+manage/video-channel-edit/video-channel-update.component.ts index 21b6167b2..51cd45605 100644 --- a/client/src/app/+manage/video-channel-edit/video-channel-update.component.ts +++ b/client/src/app/+manage/video-channel-edit/video-channel-update.component.ts | |||
@@ -111,7 +111,7 @@ export class VideoChannelUpdateComponent extends VideoChannelEdit implements OnI | |||
111 | next: data => { | 111 | next: data => { |
112 | this.notifier.success($localize`Avatar changed.`) | 112 | this.notifier.success($localize`Avatar changed.`) |
113 | 113 | ||
114 | this.videoChannel.updateAvatar(data.avatar) | 114 | this.videoChannel.updateAvatar(data.avatars) |
115 | }, | 115 | }, |
116 | 116 | ||
117 | error: (err: HttpErrorResponse) => genericUploadErrorHandler({ | 117 | error: (err: HttpErrorResponse) => genericUploadErrorHandler({ |
@@ -141,7 +141,7 @@ export class VideoChannelUpdateComponent extends VideoChannelEdit implements OnI | |||
141 | next: data => { | 141 | next: data => { |
142 | this.notifier.success($localize`Banner changed.`) | 142 | this.notifier.success($localize`Banner changed.`) |
143 | 143 | ||
144 | this.videoChannel.updateBanner(data.banner) | 144 | this.videoChannel.updateBanner(data.banners) |
145 | }, | 145 | }, |
146 | 146 | ||
147 | error: (err: HttpErrorResponse) => genericUploadErrorHandler({ | 147 | error: (err: HttpErrorResponse) => genericUploadErrorHandler({ |
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts index a5bcb6496..577f4a252 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts | |||
@@ -43,7 +43,7 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked { | |||
43 | next: data => { | 43 | next: data => { |
44 | this.notifier.success($localize`Avatar changed.`) | 44 | this.notifier.success($localize`Avatar changed.`) |
45 | 45 | ||
46 | this.user.updateAccountAvatar(data.avatar) | 46 | this.user.updateAccountAvatar(data.avatars) |
47 | }, | 47 | }, |
48 | 48 | ||
49 | error: (err: HttpErrorResponse) => genericUploadErrorHandler({ | 49 | error: (err: HttpErrorResponse) => genericUploadErrorHandler({ |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html index 77947315b..c1ded0f6d 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html | |||
@@ -19,7 +19,7 @@ | |||
19 | 19 | ||
20 | <div class="video-channels"> | 20 | <div class="video-channels"> |
21 | <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel"> | 21 | <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel"> |
22 | <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]"></my-actor-avatar> | 22 | <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]" size="80"></my-actor-avatar> |
23 | 23 | ||
24 | <div class="video-channel-info"> | 24 | <div class="video-channel-info"> |
25 | <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page"> | 25 | <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page"> |
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 998e46cb2..484355967 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 | |||
@@ -24,7 +24,6 @@ my-edit-button { | |||
24 | padding-bottom: 0; | 24 | padding-bottom: 0; |
25 | 25 | ||
26 | my-actor-avatar { | 26 | my-actor-avatar { |
27 | @include actor-avatar-size(80px); | ||
28 | @include margin-right(10px); | 27 | @include margin-right(10px); |
29 | } | 28 | } |
30 | } | 29 | } |
diff --git a/client/src/app/+my-library/my-follows/my-followers.component.html b/client/src/app/+my-library/my-follows/my-followers.component.html index eac750c86..4303695a3 100644 --- a/client/src/app/+my-library/my-follows/my-followers.component.html +++ b/client/src/app/+my-library/my-follows/my-followers.component.html | |||
@@ -14,7 +14,7 @@ | |||
14 | 14 | ||
15 | <div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> | 15 | <div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> |
16 | <div *ngFor="let follow of follows" class="actor"> | 16 | <div *ngFor="let follow of follows" class="actor"> |
17 | <my-actor-avatar [account]="follow.follower" [href]="follow.follower.url"></my-actor-avatar> | 17 | <my-actor-avatar [account]="follow.follower" [href]="follow.follower.url" size="40"></my-actor-avatar> |
18 | 18 | ||
19 | <div class="actor-info"> | 19 | <div class="actor-info"> |
20 | <a [href]="follow.follower.url" class="actor-names" rel="noopener noreferrer" target="_blank" i18n-title title="Follower page"> | 20 | <a [href]="follow.follower.url" class="actor-names" rel="noopener noreferrer" target="_blank" i18n-title title="Follower page"> |
diff --git a/client/src/app/+my-library/my-follows/my-followers.component.scss b/client/src/app/+my-library/my-follows/my-followers.component.scss index 15b51c419..fae4cd972 100644 --- a/client/src/app/+my-library/my-follows/my-followers.component.scss +++ b/client/src/app/+my-library/my-follows/my-followers.component.scss | |||
@@ -12,7 +12,7 @@ input[type=text] { | |||
12 | } | 12 | } |
13 | 13 | ||
14 | .actor { | 14 | .actor { |
15 | @include actor-row($avatar-size: 40px, $min-height: auto, $separator: true); | 15 | @include actor-row($min-height: auto, $separator: true); |
16 | 16 | ||
17 | .actor-display-name { | 17 | .actor-display-name { |
18 | font-size: 16px; | 18 | font-size: 16px; |
diff --git a/client/src/app/+my-library/my-follows/my-subscriptions.component.html b/client/src/app/+my-library/my-follows/my-subscriptions.component.html index 775f0e783..391c4d3be 100644 --- a/client/src/app/+my-library/my-follows/my-subscriptions.component.html +++ b/client/src/app/+my-library/my-follows/my-subscriptions.component.html | |||
@@ -14,7 +14,7 @@ | |||
14 | 14 | ||
15 | <div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> | 15 | <div class="actors" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> |
16 | <div *ngFor="let videoChannel of videoChannels" class="actor"> | 16 | <div *ngFor="let videoChannel of videoChannels" class="actor"> |
17 | <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]"></my-actor-avatar> | 17 | <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]" size="80"></my-actor-avatar> |
18 | 18 | ||
19 | <div class="actor-info"> | 19 | <div class="actor-info"> |
20 | <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="actor-names" i18n-title title="Channel page"> | 20 | <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="actor-names" i18n-title title="Channel page"> |
diff --git a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts index 764da2369..8ead237c7 100644 --- a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts +++ b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { SelectChannelItem } from 'src/types/select-options-item.model' | 1 | import { SelectChannelItem } from 'src/types/select-options-item.model' |
2 | import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 2 | import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
3 | import { AuthService, Notifier } from '@app/core' | 3 | import { AuthService, Notifier } from '@app/core' |
4 | import { listUserChannels } from '@app/helpers' | 4 | import { listUserChannelsForSelect } from '@app/helpers' |
5 | import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' | 5 | import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' |
6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' |
7 | import { VideoOwnershipService } from '@app/shared/shared-main' | 7 | import { VideoOwnershipService } from '@app/shared/shared-main' |
@@ -36,7 +36,7 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit { | |||
36 | ngOnInit () { | 36 | ngOnInit () { |
37 | this.videoChannels = [] | 37 | this.videoChannels = [] |
38 | 38 | ||
39 | listUserChannels(this.authService) | 39 | listUserChannelsForSelect(this.authService) |
40 | .subscribe(channels => this.videoChannels = channels) | 40 | .subscribe(channels => this.videoChannels = channels) |
41 | 41 | ||
42 | this.buildForm({ | 42 | this.buildForm({ |
diff --git a/client/src/app/+my-library/my-ownership/my-ownership.component.html b/client/src/app/+my-library/my-ownership/my-ownership.component.html index 4c02c78fc..cb032505e 100644 --- a/client/src/app/+my-library/my-ownership/my-ownership.component.html +++ b/client/src/app/+my-library/my-ownership/my-ownership.component.html | |||
@@ -37,7 +37,7 @@ | |||
37 | <td> | 37 | <td> |
38 | <a [href]="videoChangeOwnership.initiatorAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> | 38 | <a [href]="videoChangeOwnership.initiatorAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> |
39 | <div class="chip two-lines"> | 39 | <div class="chip two-lines"> |
40 | <my-actor-avatar [account]="videoChangeOwnership.initiatorAccount"></my-actor-avatar> | 40 | <my-actor-avatar [account]="videoChangeOwnership.initiatorAccount" size="32"></my-actor-avatar> |
41 | <div> | 41 | <div> |
42 | {{ videoChangeOwnership.initiatorAccount.displayName }} | 42 | {{ videoChangeOwnership.initiatorAccount.displayName }} |
43 | <span class="text-muted">{{ videoChangeOwnership.initiatorAccount.nameWithHost }}</span> | 43 | <span class="text-muted">{{ videoChangeOwnership.initiatorAccount.nameWithHost }}</span> |
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts index 8bc78b2db..9eb3e9888 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { AuthService, Notifier, ServerService } from '@app/core' | 3 | import { AuthService, Notifier, ServerService } from '@app/core' |
4 | import { listUserChannels } from '@app/helpers' | 4 | import { listUserChannelsForSelect } from '@app/helpers' |
5 | import { | 5 | import { |
6 | setPlaylistChannelValidator, | 6 | setPlaylistChannelValidator, |
7 | VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, | 7 | VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, |
@@ -46,7 +46,7 @@ export class MyVideoPlaylistCreateComponent extends MyVideoPlaylistEdit implemen | |||
46 | setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy) | 46 | setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy) |
47 | }) | 47 | }) |
48 | 48 | ||
49 | listUserChannels(this.authService) | 49 | listUserChannelsForSelect(this.authService) |
50 | .subscribe(channels => this.userVideoChannels = channels) | 50 | .subscribe(channels => this.userVideoChannels = channels) |
51 | 51 | ||
52 | this.serverService.getVideoPlaylistPrivacies() | 52 | this.serverService.getVideoPlaylistPrivacies() |
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts index 06ac3ad50..ef7ba0018 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts | |||
@@ -3,7 +3,7 @@ import { map, switchMap } from 'rxjs/operators' | |||
3 | import { Component, OnDestroy, OnInit } from '@angular/core' | 3 | import { Component, OnDestroy, OnInit } from '@angular/core' |
4 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { AuthService, Notifier, ServerService } from '@app/core' | 5 | import { AuthService, Notifier, ServerService } from '@app/core' |
6 | import { listUserChannels } from '@app/helpers' | 6 | import { listUserChannelsForSelect } from '@app/helpers' |
7 | import { | 7 | import { |
8 | setPlaylistChannelValidator, | 8 | setPlaylistChannelValidator, |
9 | VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, | 9 | VIDEO_PLAYLIST_CHANNEL_ID_VALIDATOR, |
@@ -51,7 +51,7 @@ export class MyVideoPlaylistUpdateComponent extends MyVideoPlaylistEdit implemen | |||
51 | setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy) | 51 | setPlaylistChannelValidator(this.form.get('videoChannelId'), privacy) |
52 | }) | 52 | }) |
53 | 53 | ||
54 | listUserChannels(this.authService) | 54 | listUserChannelsForSelect(this.authService) |
55 | .subscribe(channels => this.userVideoChannels = channels) | 55 | .subscribe(channels => this.userVideoChannels = channels) |
56 | 56 | ||
57 | this.paramsSub = this.route.params | 57 | this.paramsSub = this.route.params |
diff --git a/client/src/app/+search/search.component.html b/client/src/app/+search/search.component.html index 412b962d1..2c84dd930 100644 --- a/client/src/app/+search/search.component.html +++ b/client/src/app/+search/search.component.html | |||
@@ -36,7 +36,7 @@ | |||
36 | <ng-container *ngFor="let result of results"> | 36 | <ng-container *ngFor="let result of results"> |
37 | <div *ngIf="isVideoChannel(result)" class="entry video-channel"> | 37 | <div *ngIf="isVideoChannel(result)" class="entry video-channel"> |
38 | 38 | ||
39 | <my-actor-avatar [channel]="result" [internalHref]="getInternalChannelUrl(result)" [href]="getExternalChannelUrl(result)"></my-actor-avatar> | 39 | <my-actor-avatar [channel]="result" [internalHref]="getInternalChannelUrl(result)" [href]="getExternalChannelUrl(result)" size="120"></my-actor-avatar> |
40 | 40 | ||
41 | <div class="video-channel-info"> | 41 | <div class="video-channel-info"> |
42 | <a *ngIf="!isExternalChannelUrl()" [routerLink]="getInternalChannelUrl(result)" class="video-channel-names"> | 42 | <a *ngIf="!isExternalChannelUrl()" [routerLink]="getInternalChannelUrl(result)" class="video-channel-names"> |
diff --git a/client/src/app/+search/search.component.scss b/client/src/app/+search/search.component.scss index b521825e5..cab1d0e88 100644 --- a/client/src/app/+search/search.component.scss +++ b/client/src/app/+search/search.component.scss | |||
@@ -58,10 +58,6 @@ | |||
58 | max-width: 800px; | 58 | max-width: 800px; |
59 | } | 59 | } |
60 | 60 | ||
61 | .video-channel my-actor-avatar { | ||
62 | @include build-channel-img-size($video-thumbnail-width); | ||
63 | } | ||
64 | |||
65 | .video-channel-info { | 61 | .video-channel-info { |
66 | flex-grow: 1; | 62 | flex-grow: 1; |
67 | margin: 0 10px; | 63 | margin: 0 10px; |
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html index 212e2f867..09f81d2ce 100644 --- a/client/src/app/+video-channels/video-channels.component.html +++ b/client/src/app/+video-channels/video-channels.component.html | |||
@@ -23,7 +23,7 @@ | |||
23 | <div class="section-label" i18n>OWNER ACCOUNT</div> | 23 | <div class="section-label" i18n>OWNER ACCOUNT</div> |
24 | 24 | ||
25 | <div class="avatar-row"> | 25 | <div class="avatar-row"> |
26 | <my-actor-avatar class="account-avatar" [account]="ownerAccount" [internalHref]="getAccountUrl()"></my-actor-avatar> | 26 | <my-actor-avatar class="account-avatar" [account]="ownerAccount" [internalHref]="getAccountUrl()" size="48"></my-actor-avatar> |
27 | 27 | ||
28 | <div class="actor-info"> | 28 | <div class="actor-info"> |
29 | <h4> | 29 | <h4> |
@@ -51,7 +51,7 @@ | |||
51 | </ng-template> | 51 | </ng-template> |
52 | 52 | ||
53 | <div class="channel-avatar-row"> | 53 | <div class="channel-avatar-row"> |
54 | <my-actor-avatar class="main-avatar" [channel]="videoChannel"></my-actor-avatar> | 54 | <my-actor-avatar class="main-avatar" [channel]="videoChannel" size="120"></my-actor-avatar> |
55 | 55 | ||
56 | <div> | 56 | <div> |
57 | <div class="section-label" i18n>VIDEO CHANNEL</div> | 57 | <div class="section-label" i18n>VIDEO CHANNEL</div> |
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss index 72ee2d7bb..004ad7998 100644 --- a/client/src/app/+video-channels/video-channels.component.scss +++ b/client/src/app/+video-channels/video-channels.component.scss | |||
@@ -107,10 +107,6 @@ | |||
107 | display: flex; | 107 | display: flex; |
108 | margin-bottom: 15px; | 108 | margin-bottom: 15px; |
109 | 109 | ||
110 | .account-avatar { | ||
111 | @include actor-avatar-size(48px); | ||
112 | } | ||
113 | |||
114 | .actor-info { | 110 | .actor-info { |
115 | @include margin-left(15px); | 111 | @include margin-left(15px); |
116 | } | 112 | } |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-send.ts b/client/src/app/+videos/+video-edit/video-add-components/video-send.ts index 3d0e1bf2a..9de373cd3 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-send.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-send.ts | |||
@@ -2,7 +2,7 @@ import { catchError, switchMap, tap } from 'rxjs/operators' | |||
2 | import { SelectChannelItem } from 'src/types/select-options-item.model' | 2 | import { SelectChannelItem } from 'src/types/select-options-item.model' |
3 | import { Directive, EventEmitter, OnInit } from '@angular/core' | 3 | import { Directive, EventEmitter, OnInit } from '@angular/core' |
4 | import { AuthService, CanComponentDeactivateResult, Notifier, ServerService } from '@app/core' | 4 | import { AuthService, CanComponentDeactivateResult, Notifier, ServerService } from '@app/core' |
5 | import { listUserChannels } from '@app/helpers' | 5 | import { listUserChannelsForSelect } from '@app/helpers' |
6 | import { FormReactive } from '@app/shared/shared-forms' | 6 | import { FormReactive } from '@app/shared/shared-forms' |
7 | import { VideoCaptionEdit, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' | 7 | import { VideoCaptionEdit, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' |
8 | import { LoadingBarService } from '@ngx-loading-bar/core' | 8 | import { LoadingBarService } from '@ngx-loading-bar/core' |
@@ -38,7 +38,7 @@ export abstract class VideoSend extends FormReactive implements OnInit { | |||
38 | ngOnInit () { | 38 | ngOnInit () { |
39 | this.buildForm({}) | 39 | this.buildForm({}) |
40 | 40 | ||
41 | listUserChannels(this.authService) | 41 | listUserChannelsForSelect(this.authService) |
42 | .subscribe(channels => { | 42 | .subscribe(channels => { |
43 | this.userVideoChannels = channels | 43 | this.userVideoChannels = channels |
44 | this.firstStepChannelId = this.userVideoChannels[0].id | 44 | this.firstStepChannelId = this.userVideoChannels[0].id |
diff --git a/client/src/app/+videos/+video-edit/video-update.resolver.ts b/client/src/app/+videos/+video-edit/video-update.resolver.ts index 91e76b7fe..82dae5c1c 100644 --- a/client/src/app/+videos/+video-edit/video-update.resolver.ts +++ b/client/src/app/+videos/+video-edit/video-update.resolver.ts | |||
@@ -3,7 +3,7 @@ import { map, switchMap } from 'rxjs/operators' | |||
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router' | 4 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router' |
5 | import { AuthService } from '@app/core' | 5 | import { AuthService } from '@app/core' |
6 | import { listUserChannels } from '@app/helpers' | 6 | import { listUserChannelsForSelect } from '@app/helpers' |
7 | import { VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' | 7 | import { VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' |
8 | import { LiveVideoService } from '@app/shared/shared-video-live' | 8 | import { LiveVideoService } from '@app/shared/shared-video-live' |
9 | 9 | ||
@@ -33,7 +33,7 @@ export class VideoUpdateResolver implements Resolve<any> { | |||
33 | .loadCompleteDescription(video.descriptionPath) | 33 | .loadCompleteDescription(video.descriptionPath) |
34 | .pipe(map(description => Object.assign(video, { description }))), | 34 | .pipe(map(description => Object.assign(video, { description }))), |
35 | 35 | ||
36 | listUserChannels(this.authService), | 36 | listUserChannelsForSelect(this.authService), |
37 | 37 | ||
38 | this.videoCaptionService | 38 | this.videoCaptionService |
39 | .listCaptions(video.id) | 39 | .listCaptions(video.id) |
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html index d0e9bcd29..5014b9692 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.html | |||
@@ -1,6 +1,6 @@ | |||
1 | <div *ngIf="isCommentDisplayed()" class="root-comment" [ngClass]="{ 'is-child': isChild() }"> | 1 | <div *ngIf="isCommentDisplayed()" class="root-comment" [ngClass]="{ 'is-child': isChild() }"> |
2 | <div class="left"> | 2 | <div class="left"> |
3 | <my-actor-avatar *ngIf="!comment.isDeleted" [href]="comment.account.url" [account]="comment.account"></my-actor-avatar> | 3 | <my-actor-avatar *ngIf="!comment.isDeleted" [href]="comment.account.url" [account]="comment.account" [size]="isChild() ? '25' : '36'"></my-actor-avatar> |
4 | <div class="vertical-border"></div> | 4 | <div class="vertical-border"></div> |
5 | </div> | 5 | </div> |
6 | 6 | ||
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.scss b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.scss index 87e313d41..54f828014 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.scss +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.scss | |||
@@ -25,10 +25,6 @@ | |||
25 | } | 25 | } |
26 | } | 26 | } |
27 | 27 | ||
28 | my-actor-avatar { | ||
29 | @include actor-avatar-size(36px); | ||
30 | } | ||
31 | |||
32 | .comment { | 28 | .comment { |
33 | flex-grow: 1; | 29 | flex-grow: 1; |
34 | // Fix word-wrap with flex | 30 | // Fix word-wrap with flex |
@@ -160,11 +156,6 @@ my-video-comment-add { | |||
160 | } | 156 | } |
161 | 157 | ||
162 | .is-child { | 158 | .is-child { |
163 | // Reduce avatars size for replies | ||
164 | my-actor-avatar { | ||
165 | @include actor-avatar-size(25px); | ||
166 | } | ||
167 | |||
168 | .left { | 159 | .left { |
169 | @include margin-right(6px); | 160 | @include margin-right(6px); |
170 | } | 161 | } |
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html index d433c7aba..a23152b67 100644 --- a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html +++ b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.html | |||
@@ -2,19 +2,19 @@ | |||
2 | <my-actor-avatar | 2 | <my-actor-avatar |
3 | *ngIf="showChannel" | 3 | *ngIf="showChannel" |
4 | class="channel" | 4 | class="channel" |
5 | [class.main-avatar]="showChannel" | ||
6 | [channel]="video.channel" | 5 | [channel]="video.channel" |
7 | [internalHref]="[ '/c', video.byVideoChannel ]" | 6 | [internalHref]="[ '/c', video.byVideoChannel ]" |
8 | [title]="channelLinkTitle" | 7 | [title]="channelLinkTitle" |
8 | size="35" | ||
9 | ></my-actor-avatar> | 9 | ></my-actor-avatar> |
10 | 10 | ||
11 | <my-actor-avatar | 11 | <my-actor-avatar |
12 | *ngIf="showAccount" | 12 | *ngIf="showAccount" |
13 | class="account" | 13 | class="account" |
14 | [class.main-avatar]="!showChannel" | ||
15 | [class.second-avatar]="showChannel" | 14 | [class.second-avatar]="showChannel" |
16 | [account]="video.account" | 15 | [account]="video.account" |
17 | [internalHref]="[ '/a', video.byAccount ]" | 16 | [internalHref]="[ '/a', video.byAccount ]" |
18 | [title]="accountLinkTitle"> | 17 | [title]="accountLinkTitle" |
18 | size="35"> | ||
19 | </my-actor-avatar> | 19 | </my-actor-avatar> |
20 | </div> | 20 | </div> |
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss index 71c5e4b5a..80711ff32 100644 --- a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss +++ b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.scss | |||
@@ -1,9 +1,5 @@ | |||
1 | @use '_mixins' as *; | 1 | @use '_mixins' as *; |
2 | 2 | ||
3 | @mixin main { | ||
4 | @include actor-avatar-size(35px); | ||
5 | } | ||
6 | |||
7 | @mixin secondary { | 3 | @mixin secondary { |
8 | height: 60%; | 4 | height: 60%; |
9 | width: 60%; | 5 | width: 60%; |
@@ -14,16 +10,11 @@ | |||
14 | } | 10 | } |
15 | 11 | ||
16 | .wrapper { | 12 | .wrapper { |
17 | @include actor-avatar-size(35px); | ||
18 | @include margin-right(5px); | 13 | @include margin-right(5px); |
19 | 14 | ||
20 | position: relative; | 15 | position: relative; |
21 | margin-bottom: 5px; | 16 | margin-bottom: 5px; |
22 | 17 | ||
23 | .main-avatar { | ||
24 | @include main(); | ||
25 | } | ||
26 | |||
27 | .second-avatar { | 18 | .second-avatar { |
28 | @include secondary(); | 19 | @include secondary(); |
29 | } | 20 | } |
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.ts b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.ts index 146c440b3..4bfc7bd9d 100644 --- a/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.ts +++ b/client/src/app/+videos/+video-watch/shared/metadata/video-avatar-channel.component.ts | |||
@@ -20,8 +20,4 @@ export class VideoAvatarChannelComponent implements OnInit { | |||
20 | this.channelLinkTitle = $localize`${this.video.account.name} (channel page)` | 20 | this.channelLinkTitle = $localize`${this.video.account.name} (channel page)` |
21 | this.accountLinkTitle = $localize`${this.video.byAccount} (account page)` | 21 | this.accountLinkTitle = $localize`${this.video.byAccount} (account page)` |
22 | } | 22 | } |
23 | |||
24 | isChannelAvatarNull () { | ||
25 | return this.video.channel.avatar === null | ||
26 | } | ||
27 | } | 23 | } |
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 1a715560c..f250c2407 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 | |||
@@ -33,7 +33,7 @@ | |||
33 | <div class="section channel videos" *ngFor="let object of overview.channels"> | 33 | <div class="section channel videos" *ngFor="let object of overview.channels"> |
34 | <div class="section-title"> | 34 | <div class="section-title"> |
35 | <a [routerLink]="[ '/c', buildVideoChannelBy(object) ]"> | 35 | <a [routerLink]="[ '/c', buildVideoChannelBy(object) ]"> |
36 | <my-actor-avatar [channel]="buildVideoChannel(object)"></my-actor-avatar> | 36 | <my-actor-avatar [channel]="buildVideoChannel(object)" size="28"></my-actor-avatar> |
37 | 37 | ||
38 | <h2 class="section-title">{{ object.channel.displayName }}</h2> | 38 | <h2 class="section-title">{{ object.channel.displayName }}</h2> |
39 | </a> | 39 | </a> |
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 2239d1913..8b2aa88f2 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 | |||
@@ -52,7 +52,6 @@ | |||
52 | align-items: center; | 52 | align-items: center; |
53 | 53 | ||
54 | my-actor-avatar { | 54 | my-actor-avatar { |
55 | @include actor-avatar-size(28px); | ||
56 | @include margin-right(8px); | 55 | @include margin-right(8px); |
57 | 56 | ||
58 | font-size: initial; | 57 | font-size: initial; |
diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts index f211051ce..6ba30e4b8 100644 --- a/client/src/app/core/users/user.model.ts +++ b/client/src/app/core/users/user.model.ts | |||
@@ -132,8 +132,8 @@ export class User implements UserServerModel { | |||
132 | } | 132 | } |
133 | } | 133 | } |
134 | 134 | ||
135 | updateAccountAvatar (newAccountAvatar?: ActorImage) { | 135 | updateAccountAvatar (newAccountAvatars?: ActorImage[]) { |
136 | if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar) | 136 | if (newAccountAvatars) this.account.updateAvatar(newAccountAvatars) |
137 | else this.account.resetAvatar() | 137 | else this.account.resetAvatar() |
138 | } | 138 | } |
139 | 139 | ||
diff --git a/client/src/app/core/users/user.service.ts b/client/src/app/core/users/user.service.ts index b14bff193..b4024c02d 100644 --- a/client/src/app/core/users/user.service.ts +++ b/client/src/app/core/users/user.service.ts | |||
@@ -118,7 +118,7 @@ export class UserService { | |||
118 | changeAvatar (avatarForm: FormData) { | 118 | changeAvatar (avatarForm: FormData) { |
119 | const url = UserService.BASE_USERS_URL + 'me/avatar/pick' | 119 | const url = UserService.BASE_USERS_URL + 'me/avatar/pick' |
120 | 120 | ||
121 | return this.authHttp.post<{ avatar: ActorImage }>(url, avatarForm) | 121 | return this.authHttp.post<{ avatars: ActorImage[] }>(url, avatarForm) |
122 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 122 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
123 | } | 123 | } |
124 | 124 | ||
diff --git a/client/src/app/helpers/utils/channel.ts b/client/src/app/helpers/utils/channel.ts index 93863a8af..2cd56d6e6 100644 --- a/client/src/app/helpers/utils/channel.ts +++ b/client/src/app/helpers/utils/channel.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { minBy } from 'lodash-es' | ||
1 | import { first, map } from 'rxjs/operators' | 2 | import { first, map } from 'rxjs/operators' |
2 | import { SelectChannelItem } from 'src/types/select-options-item.model' | 3 | import { SelectChannelItem } from 'src/types/select-options-item.model' |
3 | import { AuthService } from '../../core/auth' | 4 | import { AuthService } from '../../core/auth' |
4 | 5 | ||
5 | function listUserChannels (authService: AuthService) { | 6 | function listUserChannelsForSelect (authService: AuthService) { |
6 | return authService.userInformationLoaded | 7 | return authService.userInformationLoaded |
7 | .pipe( | 8 | .pipe( |
8 | first(), | 9 | first(), |
@@ -23,12 +24,12 @@ function listUserChannels (authService: AuthService) { | |||
23 | id: c.id, | 24 | id: c.id, |
24 | label: c.displayName, | 25 | label: c.displayName, |
25 | support: c.support, | 26 | support: c.support, |
26 | avatarPath: c.avatar?.path | 27 | avatarPath: minBy(c.avatars, 'width')[0]?.path |
27 | }) as SelectChannelItem) | 28 | }) as SelectChannelItem) |
28 | }) | 29 | }) |
29 | ) | 30 | ) |
30 | } | 31 | } |
31 | 32 | ||
32 | export { | 33 | export { |
33 | listUserChannels | 34 | listUserChannelsForSelect |
34 | } | 35 | } |
diff --git a/client/src/app/modal/account-setup-warning-modal.component.ts b/client/src/app/modal/account-setup-warning-modal.component.ts index c78de447d..c4cbf92b6 100644 --- a/client/src/app/modal/account-setup-warning-modal.component.ts +++ b/client/src/app/modal/account-setup-warning-modal.component.ts | |||
@@ -32,7 +32,7 @@ export class AccountSetupWarningModalComponent { | |||
32 | } | 32 | } |
33 | 33 | ||
34 | hasAccountAvatar (user: User) { | 34 | hasAccountAvatar (user: User) { |
35 | return !!user.account.avatar | 35 | return user.account.avatars.length !== 0 |
36 | } | 36 | } |
37 | 37 | ||
38 | hasAccountDescription (user: User) { | 38 | hasAccountDescription (user: User) { |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html index 07cc73461..f0a27c6e2 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html | |||
@@ -43,7 +43,7 @@ | |||
43 | <td *ngIf="isAdminView()"> | 43 | <td *ngIf="isAdminView()"> |
44 | <a *ngIf="abuse.reporterAccount" [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> | 44 | <a *ngIf="abuse.reporterAccount" [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> |
45 | <div class="chip two-lines"> | 45 | <div class="chip two-lines"> |
46 | <my-actor-avatar [account]="abuse.reporterAccount"></my-actor-avatar> | 46 | <my-actor-avatar [account]="abuse.reporterAccount" size="32"></my-actor-avatar> |
47 | <div> | 47 | <div> |
48 | {{ abuse.reporterAccount.displayName }} | 48 | {{ abuse.reporterAccount.displayName }} |
49 | <span>{{ abuse.reporterAccount.nameWithHost }}</span> | 49 | <span>{{ abuse.reporterAccount.nameWithHost }}</span> |
diff --git a/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.ts b/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.ts index 8b7d64ed3..01bb401fb 100644 --- a/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.ts +++ b/client/src/app/shared/shared-actor-image-edit/actor-avatar-edit.component.ts | |||
@@ -72,7 +72,7 @@ export class ActorAvatarEditComponent implements OnInit { | |||
72 | } | 72 | } |
73 | 73 | ||
74 | hasAvatar () { | 74 | hasAvatar () { |
75 | return !!this.preview || !!this.actor.avatar | 75 | return !!this.preview || this.actor.avatars.length !== 0 |
76 | } | 76 | } |
77 | 77 | ||
78 | isChannel () { | 78 | isChannel () { |
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar.component.scss b/client/src/app/shared/shared-actor-image/actor-avatar.component.scss index a2424b593..68bf74553 100644 --- a/client/src/app/shared/shared-actor-image/actor-avatar.component.scss +++ b/client/src/app/shared/shared-actor-image/actor-avatar.component.scss | |||
@@ -20,38 +20,23 @@ | |||
20 | } | 20 | } |
21 | } | 21 | } |
22 | 22 | ||
23 | .avatar-18 { | 23 | $sizes: '18', '25', '28', '32', '34', '35', '36', '40', '48', '75', '80', '100', '120'; |
24 | --avatarSize: 18px; | ||
25 | --initialFontSize: 13px; | ||
26 | } | ||
27 | 24 | ||
28 | .avatar-25 { | 25 | @each $size in $sizes { |
29 | --avatarSize: 25px; | 26 | .avatar-#{$size} { |
27 | --avatarSize: #{$size}px; | ||
28 | } | ||
30 | } | 29 | } |
31 | 30 | ||
32 | .avatar-32 { | 31 | .avatar-18 { |
33 | --avatarSize: 32px; | 32 | --initialFontSize: 13px; |
34 | } | ||
35 | |||
36 | .avatar-34 { | ||
37 | --avatarSize: 34px; | ||
38 | } | ||
39 | |||
40 | .avatar-36 { | ||
41 | --avatarSize: 36px; | ||
42 | } | ||
43 | |||
44 | .avatar-40 { | ||
45 | --avatarSize: 40px; | ||
46 | } | 33 | } |
47 | 34 | ||
48 | .avatar-100 { | 35 | .avatar-100 { |
49 | --avatarSize: 100px; | ||
50 | --initialFontSize: 40px; | 36 | --initialFontSize: 40px; |
51 | } | 37 | } |
52 | 38 | ||
53 | .avatar-120 { | 39 | .avatar-120 { |
54 | --avatarSize: 120px; | ||
55 | --initialFontSize: 46px; | 40 | --initialFontSize: 46px; |
56 | } | 41 | } |
57 | 42 | ||
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar.component.ts b/client/src/app/shared/shared-actor-image/actor-avatar.component.ts index c323dc724..bc7e8a096 100644 --- a/client/src/app/shared/shared-actor-image/actor-avatar.component.ts +++ b/client/src/app/shared/shared-actor-image/actor-avatar.component.ts | |||
@@ -4,11 +4,11 @@ import { Account } from '../shared-main/account/account.model' | |||
4 | 4 | ||
5 | type ActorInput = { | 5 | type ActorInput = { |
6 | name: string | 6 | name: string |
7 | avatar?: { url?: string, path: string } | 7 | avatars: { width: number, url?: string, path: string }[] |
8 | url: string | 8 | url: string |
9 | } | 9 | } |
10 | 10 | ||
11 | export type ActorAvatarSize = '18' | '25' | '32' | '34' | '36' | '40' | '100' | '120' | 11 | export type ActorAvatarSize = '18' | '25' | '28' | '32' | '34' | '35' | '36' | '40' | '48' | '75' | '80' | '100' | '120' |
12 | 12 | ||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-actor-avatar', | 14 | selector: 'my-actor-avatar', |
@@ -23,7 +23,7 @@ export class ActorAvatarComponent { | |||
23 | 23 | ||
24 | @Input() previewImage: string | 24 | @Input() previewImage: string |
25 | 25 | ||
26 | @Input() size: ActorAvatarSize | 26 | @Input() size: ActorAvatarSize = '32' |
27 | 27 | ||
28 | // Use an external link | 28 | // Use an external link |
29 | @Input() href: string | 29 | @Input() href: string |
@@ -50,14 +50,13 @@ export class ActorAvatarComponent { | |||
50 | } | 50 | } |
51 | 51 | ||
52 | get defaultAvatarUrl () { | 52 | get defaultAvatarUrl () { |
53 | if (this.channel) return VideoChannel.GET_DEFAULT_AVATAR_URL() | 53 | if (this.account) return Account.GET_DEFAULT_AVATAR_URL(+this.size) |
54 | 54 | if (this.channel) return VideoChannel.GET_DEFAULT_AVATAR_URL(+this.size) | |
55 | return Account.GET_DEFAULT_AVATAR_URL() | ||
56 | } | 55 | } |
57 | 56 | ||
58 | get avatarUrl () { | 57 | get avatarUrl () { |
59 | if (this.account) return Account.GET_ACTOR_AVATAR_URL(this.account) | 58 | if (this.account) return Account.GET_ACTOR_AVATAR_URL(this.account, +this.size) |
60 | if (this.channel) return VideoChannel.GET_ACTOR_AVATAR_URL(this.channel) | 59 | if (this.channel) return VideoChannel.GET_ACTOR_AVATAR_URL(this.channel, +this.size) |
61 | 60 | ||
62 | return '' | 61 | return '' |
63 | } | 62 | } |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.html b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.html index de150aac9..52a402329 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.html +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <div *ngIf="channel" class="channel"> | 1 | <div *ngIf="channel" class="channel"> |
2 | 2 | ||
3 | <div class="channel-avatar-row"> | 3 | <div class="channel-avatar-row"> |
4 | <my-actor-avatar [channel]="channel" [internalHref]="getVideoChannelLink()" i18n-title title="See this video channel"></my-actor-avatar> | 4 | <my-actor-avatar [channel]="channel" [internalHref]="getVideoChannelLink()" i18n-title title="See this video channel" size="75"></my-actor-avatar> |
5 | 5 | ||
6 | <h6> | 6 | <h6> |
7 | <a [routerLink]="getVideoChannelLink()" i18n-title title="See this video channel"> | 7 | <a [routerLink]="getVideoChannelLink()" i18n-title title="See this video channel"> |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.scss b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.scss index 39e8d2091..e8ef478d9 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.scss +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.scss | |||
@@ -26,8 +26,6 @@ | |||
26 | } | 26 | } |
27 | 27 | ||
28 | my-actor-avatar { | 28 | my-actor-avatar { |
29 | @include actor-avatar-size(75px); | ||
30 | |||
31 | grid-column: 1; | 29 | grid-column: 1; |
32 | grid-row: 1 / 4; | 30 | grid-row: 1 / 4; |
33 | } | 31 | } |
diff --git a/client/src/app/shared/shared-forms/select/select-channel.component.ts b/client/src/app/shared/shared-forms/select/select-channel.component.ts index 40a7c53bb..5fcae0050 100644 --- a/client/src/app/shared/shared-forms/select/select-channel.component.ts +++ b/client/src/app/shared/shared-forms/select/select-channel.component.ts | |||
@@ -31,7 +31,7 @@ export class SelectChannelComponent implements ControlValueAccessor, OnChanges { | |||
31 | this.channels = this.items.map(c => { | 31 | this.channels = this.items.map(c => { |
32 | const avatarPath = c.avatarPath | 32 | const avatarPath = c.avatarPath |
33 | ? c.avatarPath | 33 | ? c.avatarPath |
34 | : VideoChannel.GET_DEFAULT_AVATAR_URL() | 34 | : VideoChannel.GET_DEFAULT_AVATAR_URL(20) |
35 | 35 | ||
36 | return Object.assign({}, c, { avatarPath }) | 36 | return Object.assign({}, c, { avatarPath }) |
37 | }) | 37 | }) |
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 8b78d01a6..a26a9c11c 100644 --- a/client/src/app/shared/shared-main/account/account.model.ts +++ b/client/src/app/shared/shared-main/account/account.model.ts | |||
@@ -17,11 +17,15 @@ export class Account extends Actor implements ServerAccount { | |||
17 | 17 | ||
18 | userId?: number | 18 | userId?: number |
19 | 19 | ||
20 | static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) { | 20 | static GET_ACTOR_AVATAR_URL (actor: { avatars: { width: number, url?: string, path: string }[] }, size: number) { |
21 | return Actor.GET_ACTOR_AVATAR_URL(actor) | 21 | return Actor.GET_ACTOR_AVATAR_URL(actor, size) |
22 | } | 22 | } |
23 | 23 | ||
24 | static GET_DEFAULT_AVATAR_URL () { | 24 | static GET_DEFAULT_AVATAR_URL (size: number) { |
25 | if (size <= 48) { | ||
26 | return `${window.location.origin}/client/assets/images/default-avatar-account-48x48.png` | ||
27 | } | ||
28 | |||
25 | return `${window.location.origin}/client/assets/images/default-avatar-account.png` | 29 | return `${window.location.origin}/client/assets/images/default-avatar-account.png` |
26 | } | 30 | } |
27 | 31 | ||
@@ -42,12 +46,12 @@ export class Account extends Actor implements ServerAccount { | |||
42 | this.mutedServerByInstance = false | 46 | this.mutedServerByInstance = false |
43 | } | 47 | } |
44 | 48 | ||
45 | updateAvatar (newAvatar: ActorImage) { | 49 | updateAvatar (newAvatars: ActorImage[]) { |
46 | this.avatar = newAvatar | 50 | this.avatars = newAvatars |
47 | } | 51 | } |
48 | 52 | ||
49 | resetAvatar () { | 53 | resetAvatar () { |
50 | this.avatar = null | 54 | this.avatars = [] |
51 | } | 55 | } |
52 | 56 | ||
53 | updateBlockStatus (blockStatus: BlockStatus) { | 57 | updateBlockStatus (blockStatus: BlockStatus) { |
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 082f44fb9..a54f51aa4 100644 --- a/client/src/app/shared/shared-main/account/actor.model.ts +++ b/client/src/app/shared/shared-main/account/actor.model.ts | |||
@@ -13,20 +13,22 @@ export abstract class Actor implements ServerActor { | |||
13 | 13 | ||
14 | createdAt: Date | string | 14 | createdAt: Date | string |
15 | 15 | ||
16 | avatar: ActorImage | 16 | // TODO: remove, deprecated in 4.2 |
17 | avatar: never | ||
18 | |||
19 | avatars: ActorImage[] | ||
17 | 20 | ||
18 | isLocal: boolean | 21 | isLocal: boolean |
19 | 22 | ||
20 | static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) { | 23 | static GET_ACTOR_AVATAR_URL (actor: { avatars: { width: number, url?: string, path: string }[] }, size: number) { |
21 | if (actor?.avatar?.url) return actor.avatar.url | 24 | const avatar = actor.avatars.sort((a, b) => a.width - b.width).find(a => a.width >= size) |
22 | 25 | ||
23 | if (actor?.avatar) { | 26 | if (!avatar) return '' |
24 | const absoluteAPIUrl = getAbsoluteAPIUrl() | 27 | if (avatar.url) return avatar.url |
25 | 28 | ||
26 | return absoluteAPIUrl + actor.avatar.path | 29 | const absoluteAPIUrl = getAbsoluteAPIUrl() |
27 | } | ||
28 | 30 | ||
29 | return '' | 31 | return absoluteAPIUrl + avatar.path |
30 | } | 32 | } |
31 | 33 | ||
32 | static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) { | 34 | static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) { |
@@ -55,7 +57,7 @@ export abstract class Actor implements ServerActor { | |||
55 | 57 | ||
56 | if (hash.createdAt) this.createdAt = new Date(hash.createdAt.toString()) | 58 | if (hash.createdAt) this.createdAt = new Date(hash.createdAt.toString()) |
57 | 59 | ||
58 | this.avatar = hash.avatar | 60 | this.avatars = hash.avatars |
59 | this.isLocal = Actor.IS_LOCAL(this.host) | 61 | this.isLocal = Actor.IS_LOCAL(this.host) |
60 | } | 62 | } |
61 | } | 63 | } |
diff --git a/client/src/app/shared/shared-main/misc/channels-setup-message.component.ts b/client/src/app/shared/shared-main/misc/channels-setup-message.component.ts index 702475029..4f9cbc525 100644 --- a/client/src/app/shared/shared-main/misc/channels-setup-message.component.ts +++ b/client/src/app/shared/shared-main/misc/channels-setup-message.component.ts | |||
@@ -19,7 +19,7 @@ export class ChannelsSetupMessageComponent implements OnInit { | |||
19 | hasChannelNotConfigured () { | 19 | hasChannelNotConfigured () { |
20 | if (!this.user.videoChannels) return false | 20 | if (!this.user.videoChannels) return false |
21 | 21 | ||
22 | return this.user.videoChannels.filter((channel: VideoChannel) => (!channel.avatar || !channel.description)).length > 0 | 22 | return this.user.videoChannels.filter((channel: VideoChannel) => (channel.avatars.length === 0 || !channel.description)).length > 0 |
23 | } | 23 | } |
24 | 24 | ||
25 | ngOnInit () { | 25 | ngOnInit () { |
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 439547102..1eb69d5a2 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 | |||
@@ -254,11 +254,11 @@ export class UserNotification implements UserNotificationServer { | |||
254 | return [ this.buildVideoUrl(comment.video), { threadId: comment.threadId } ] | 254 | return [ this.buildVideoUrl(comment.video), { threadId: comment.threadId } ] |
255 | } | 255 | } |
256 | 256 | ||
257 | private setAccountAvatarUrl (actor: { avatarUrl?: string, avatar?: { url?: string, path: string } }) { | 257 | private setAccountAvatarUrl (actor: { avatarUrl?: string, avatars: { width: number, url?: string, path: string }[] }) { |
258 | actor.avatarUrl = Account.GET_ACTOR_AVATAR_URL(actor) || Account.GET_DEFAULT_AVATAR_URL() | 258 | actor.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(actor, 48) || Account.GET_DEFAULT_AVATAR_URL(48) |
259 | } | 259 | } |
260 | 260 | ||
261 | private setVideoChannelAvatarUrl (actor: { avatarUrl?: string, avatar?: { url?: string, path: string } }) { | 261 | private setVideoChannelAvatarUrl (actor: { avatarUrl?: string, avatars: { width: number, url?: string, path: string }[] }) { |
262 | actor.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(actor) || VideoChannel.GET_DEFAULT_AVATAR_URL() | 262 | actor.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(actor, 48) || VideoChannel.GET_DEFAULT_AVATAR_URL(48) |
263 | } | 263 | } |
264 | } | 264 | } |
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 ac2679b42..e22b0cfd0 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 | |||
@@ -12,7 +12,11 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
12 | nameWithHost: string | 12 | nameWithHost: string |
13 | nameWithHostForced: string | 13 | nameWithHostForced: string |
14 | 14 | ||
15 | banner: ActorImage | 15 | // TODO: remove, deprecated in 4.2 |
16 | banner: never | ||
17 | |||
18 | banners: ActorImage[] | ||
19 | |||
16 | bannerUrl: string | 20 | bannerUrl: string |
17 | 21 | ||
18 | updatedAt: Date | string | 22 | updatedAt: Date | string |
@@ -24,23 +28,25 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
24 | 28 | ||
25 | viewsPerDay?: ViewsPerDate[] | 29 | viewsPerDay?: ViewsPerDate[] |
26 | 30 | ||
27 | static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) { | 31 | static GET_ACTOR_AVATAR_URL (actor: { avatars: { width: number, url?: string, path: string }[] }, size: number) { |
28 | return Actor.GET_ACTOR_AVATAR_URL(actor) | 32 | return Actor.GET_ACTOR_AVATAR_URL(actor, size) |
29 | } | 33 | } |
30 | 34 | ||
31 | static GET_ACTOR_BANNER_URL (channel: ServerVideoChannel) { | 35 | static GET_ACTOR_BANNER_URL (channel: ServerVideoChannel) { |
32 | if (channel?.banner?.url) return channel.banner.url | 36 | if (!channel) return '' |
33 | |||
34 | if (channel?.banner) { | ||
35 | const absoluteAPIUrl = getAbsoluteAPIUrl() | ||
36 | 37 | ||
37 | return absoluteAPIUrl + channel.banner.path | 38 | const banner = channel.banners[0] |
38 | } | 39 | if (!banner) return '' |
39 | 40 | ||
40 | return '' | 41 | if (banner.url) return banner.url |
42 | return getAbsoluteAPIUrl() + banner.path | ||
41 | } | 43 | } |
42 | 44 | ||
43 | static GET_DEFAULT_AVATAR_URL () { | 45 | static GET_DEFAULT_AVATAR_URL (size: number) { |
46 | if (size <= 48) { | ||
47 | return `${window.location.origin}/client/assets/images/default-avatar-video-channel-48x48.png` | ||
48 | } | ||
49 | |||
44 | return `${window.location.origin}/client/assets/images/default-avatar-video-channel.png` | 50 | return `${window.location.origin}/client/assets/images/default-avatar-video-channel.png` |
45 | } | 51 | } |
46 | 52 | ||
@@ -51,7 +57,7 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
51 | this.description = hash.description | 57 | this.description = hash.description |
52 | this.support = hash.support | 58 | this.support = hash.support |
53 | 59 | ||
54 | this.banner = hash.banner | 60 | this.banners = hash.banners |
55 | 61 | ||
56 | this.isLocal = hash.isLocal | 62 | this.isLocal = hash.isLocal |
57 | 63 | ||
@@ -74,24 +80,24 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
74 | this.updateComputedAttributes() | 80 | this.updateComputedAttributes() |
75 | } | 81 | } |
76 | 82 | ||
77 | updateAvatar (newAvatar: ActorImage) { | 83 | updateAvatar (newAvatars: ActorImage[]) { |
78 | this.avatar = newAvatar | 84 | this.avatars = newAvatars |
79 | 85 | ||
80 | this.updateComputedAttributes() | 86 | this.updateComputedAttributes() |
81 | } | 87 | } |
82 | 88 | ||
83 | resetAvatar () { | 89 | resetAvatar () { |
84 | this.updateAvatar(null) | 90 | this.updateAvatar([]) |
85 | } | 91 | } |
86 | 92 | ||
87 | updateBanner (newBanner: ActorImage) { | 93 | updateBanner (newBanners: ActorImage[]) { |
88 | this.banner = newBanner | 94 | this.banners = newBanners |
89 | 95 | ||
90 | this.updateComputedAttributes() | 96 | this.updateComputedAttributes() |
91 | } | 97 | } |
92 | 98 | ||
93 | resetBanner () { | 99 | resetBanner () { |
94 | this.updateBanner(null) | 100 | this.updateBanner([]) |
95 | } | 101 | } |
96 | 102 | ||
97 | updateComputedAttributes () { | 103 | updateComputedAttributes () { |
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 f37f13c51..480d250fb 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 | |||
@@ -80,7 +80,7 @@ export class VideoChannelService { | |||
80 | changeVideoChannelImage (videoChannelName: string, avatarForm: FormData, type: 'avatar' | 'banner') { | 80 | changeVideoChannelImage (videoChannelName: string, avatarForm: FormData, type: 'avatar' | 'banner') { |
81 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type + '/pick' | 81 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type + '/pick' |
82 | 82 | ||
83 | return this.authHttp.post<{ avatar?: ActorImage, banner?: ActorImage }>(url, avatarForm) | 83 | return this.authHttp.post<{ avatars?: ActorImage[], banners?: ActorImage[] }>(url, avatarForm) |
84 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 84 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
85 | } | 85 | } |
86 | 86 | ||
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 fe5643688..8e275181c 100644 --- a/client/src/app/shared/shared-main/video/video.model.ts +++ b/client/src/app/shared/shared-main/video/video.model.ts | |||
@@ -84,7 +84,11 @@ export class Video implements VideoServerModel { | |||
84 | displayName: string | 84 | displayName: string |
85 | url: string | 85 | url: string |
86 | host: string | 86 | host: string |
87 | avatar?: ActorImage | 87 | |
88 | // TODO: remove, deprecated in 4.2 | ||
89 | avatar: ActorImage | ||
90 | |||
91 | avatars: ActorImage[] | ||
88 | } | 92 | } |
89 | 93 | ||
90 | channel: { | 94 | channel: { |
@@ -93,7 +97,11 @@ export class Video implements VideoServerModel { | |||
93 | displayName: string | 97 | displayName: string |
94 | url: string | 98 | url: string |
95 | host: string | 99 | host: string |
96 | avatar?: ActorImage | 100 | |
101 | // TODO: remove, deprecated in 4.2 | ||
102 | avatar: ActorImage | ||
103 | |||
104 | avatars: ActorImage[] | ||
97 | } | 105 | } |
98 | 106 | ||
99 | userHistory?: { | 107 | userHistory?: { |
diff --git a/client/src/app/shared/shared-moderation/account-blocklist.component.html b/client/src/app/shared/shared-moderation/account-blocklist.component.html index 637abcb51..0143194e9 100644 --- a/client/src/app/shared/shared-moderation/account-blocklist.component.html +++ b/client/src/app/shared/shared-moderation/account-blocklist.component.html | |||
@@ -33,7 +33,7 @@ | |||
33 | <td> | 33 | <td> |
34 | <a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> | 34 | <a [href]="accountBlock.blockedAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> |
35 | <div class="chip two-lines"> | 35 | <div class="chip two-lines"> |
36 | <my-actor-avatar [account]="accountBlock.blockedAccount"></my-actor-avatar> | 36 | <my-actor-avatar [account]="accountBlock.blockedAccount" size="32"></my-actor-avatar> |
37 | <div> | 37 | <div> |
38 | {{ accountBlock.blockedAccount.displayName }} | 38 | {{ accountBlock.blockedAccount.displayName }} |
39 | <span class="text-muted">{{ accountBlock.blockedAccount.nameWithHost }}</span> | 39 | <span class="text-muted">{{ accountBlock.blockedAccount.nameWithHost }}</span> |
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 30483831a..3cf128de0 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 | |||
@@ -13,11 +13,13 @@ | |||
13 | <my-actor-avatar | 13 | <my-actor-avatar |
14 | *ngIf="displayOptions.avatar && displayOwnerVideoChannel() && !displayAsRow" [title]="channelLinkTitle" | 14 | *ngIf="displayOptions.avatar && displayOwnerVideoChannel() && !displayAsRow" [title]="channelLinkTitle" |
15 | [channel]="video.channel" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]" | 15 | [channel]="video.channel" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]" |
16 | size="32" | ||
16 | ></my-actor-avatar> | 17 | ></my-actor-avatar> |
17 | 18 | ||
18 | <my-actor-avatar | 19 | <my-actor-avatar |
19 | *ngIf="displayOptions.avatar && displayOwnerAccount() && !displayAsRow" [title]="channelLinkTitle" | 20 | *ngIf="displayOptions.avatar && displayOwnerAccount() && !displayAsRow" [title]="channelLinkTitle" |
20 | [account]="video.account" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]" | 21 | [account]="video.account" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]" |
22 | size="32" | ||
21 | ></my-actor-avatar> | 23 | ></my-actor-avatar> |
22 | 24 | ||
23 | <div class="w-100 d-flex flex-column"> | 25 | <div class="w-100 d-flex flex-column"> |
diff --git a/client/src/assets/images/default-avatar-account-48x48.png b/client/src/assets/images/default-avatar-account-48x48.png new file mode 100644 index 000000000..cd09cbe1d --- /dev/null +++ b/client/src/assets/images/default-avatar-account-48x48.png | |||
Binary files differ | |||
diff --git a/client/src/assets/images/default-avatar-video-channel-48x48.png b/client/src/assets/images/default-avatar-video-channel-48x48.png new file mode 100644 index 000000000..d0e7d11c9 --- /dev/null +++ b/client/src/assets/images/default-avatar-video-channel-48x48.png | |||
Binary files differ | |||
diff --git a/client/src/sass/include/_account-channel-page.scss b/client/src/sass/include/_account-channel-page.scss index b135bbb6d..06384b98d 100644 --- a/client/src/sass/include/_account-channel-page.scss +++ b/client/src/sass/include/_account-channel-page.scss | |||
@@ -26,10 +26,6 @@ | |||
26 | grid-column: 1; | 26 | grid-column: 1; |
27 | margin-bottom: 30px; | 27 | margin-bottom: 30px; |
28 | 28 | ||
29 | .main-avatar { | ||
30 | @include actor-avatar-size(120px); | ||
31 | } | ||
32 | |||
33 | > div { | 29 | > div { |
34 | @include margin-left($img-margin); | 30 | @include margin-left($img-margin); |
35 | 31 | ||
diff --git a/client/src/sass/include/_actor.scss b/client/src/sass/include/_actor.scss index f9e44b8ad..aa2331efe 100644 --- a/client/src/sass/include/_actor.scss +++ b/client/src/sass/include/_actor.scss | |||
@@ -1,12 +1,10 @@ | |||
1 | @use '_variables' as *; | 1 | @use '_variables' as *; |
2 | @use '_mixins' as *; | 2 | @use '_mixins' as *; |
3 | 3 | ||
4 | @mixin actor-row ($avatar-size: 80px, $avatar-margin-right: 10px, $min-height: 130px, $separator: true) { | 4 | @mixin actor-row ($avatar-margin-right: 10px, $min-height: 130px, $separator: true) { |
5 | @include row-blocks($min-height: $min-height, $separator: $separator); | 5 | @include row-blocks($min-height: $min-height, $separator: $separator); |
6 | 6 | ||
7 | > my-actor-avatar { | 7 | > my-actor-avatar { |
8 | @include actor-avatar-size($avatar-size); | ||
9 | |||
10 | @include margin-right($avatar-margin-right); | 8 | @include margin-right($avatar-margin-right); |
11 | } | 9 | } |
12 | 10 | ||
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index c8ec3b4d1..291bff6db 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -887,7 +887,7 @@ | |||
887 | height: $avatar-height; | 887 | height: $avatar-height; |
888 | 888 | ||
889 | my-actor-avatar { | 889 | my-actor-avatar { |
890 | @include actor-avatar-size($avatar-height); | 890 | display: inline-block; |
891 | } | 891 | } |
892 | 892 | ||
893 | div { | 893 | div { |
diff --git a/scripts/migrations/peertube-4.2.ts b/scripts/migrations/peertube-4.2.ts new file mode 100644 index 000000000..045c3e511 --- /dev/null +++ b/scripts/migrations/peertube-4.2.ts | |||
@@ -0,0 +1,106 @@ | |||
1 | import { minBy } from 'lodash' | ||
2 | import { join } from 'path' | ||
3 | import { processImage } from '@server/helpers/image-utils' | ||
4 | import { CONFIG } from '@server/initializers/config' | ||
5 | import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants' | ||
6 | import { updateActorImages } from '@server/lib/activitypub/actors' | ||
7 | import { sendUpdateActor } from '@server/lib/activitypub/send' | ||
8 | import { getBiggestActorImage } from '@server/lib/actor-image' | ||
9 | import { JobQueue } from '@server/lib/job-queue' | ||
10 | import { AccountModel } from '@server/models/account/account' | ||
11 | import { ActorModel } from '@server/models/actor/actor' | ||
12 | import { VideoChannelModel } from '@server/models/video/video-channel' | ||
13 | import { MAccountDefault, MActorDefault, MChannelDefault } from '@server/types/models' | ||
14 | import { getLowercaseExtension } from '@shared/core-utils' | ||
15 | import { buildUUID } from '@shared/extra-utils' | ||
16 | import { ActorImageType } from '@shared/models' | ||
17 | import { initDatabaseModels } from '../../server/initializers/database' | ||
18 | |||
19 | run() | ||
20 | .then(() => process.exit(0)) | ||
21 | .catch(err => { | ||
22 | console.error(err) | ||
23 | process.exit(-1) | ||
24 | }) | ||
25 | |||
26 | async function run () { | ||
27 | console.log('Generate avatar miniatures from existing avatars.') | ||
28 | |||
29 | await initDatabaseModels(true) | ||
30 | JobQueue.Instance.init(true) | ||
31 | |||
32 | const accounts: AccountModel[] = await AccountModel.findAll({ | ||
33 | include: [ | ||
34 | { | ||
35 | model: ActorModel, | ||
36 | required: true, | ||
37 | where: { | ||
38 | serverId: null | ||
39 | } | ||
40 | }, | ||
41 | { | ||
42 | model: VideoChannelModel, | ||
43 | include: [ | ||
44 | { | ||
45 | model: AccountModel | ||
46 | } | ||
47 | ] | ||
48 | } | ||
49 | ] | ||
50 | }) | ||
51 | |||
52 | for (const account of accounts) { | ||
53 | try { | ||
54 | await generateSmallerAvatarIfNeeded(account) | ||
55 | } catch (err) { | ||
56 | console.error(`Cannot process account avatar ${account.name}`, err) | ||
57 | } | ||
58 | |||
59 | for (const videoChannel of account.VideoChannels) { | ||
60 | try { | ||
61 | await generateSmallerAvatarIfNeeded(videoChannel) | ||
62 | } catch (err) { | ||
63 | console.error(`Cannot process channel avatar ${videoChannel.name}`, err) | ||
64 | } | ||
65 | } | ||
66 | } | ||
67 | |||
68 | console.log('Generation finished!') | ||
69 | } | ||
70 | |||
71 | async function generateSmallerAvatarIfNeeded (accountOrChannel: MAccountDefault | MChannelDefault) { | ||
72 | const avatars = accountOrChannel.Actor.Avatars | ||
73 | if (avatars.length !== 1) { | ||
74 | return | ||
75 | } | ||
76 | |||
77 | console.log(`Processing ${accountOrChannel.name}.`) | ||
78 | |||
79 | await generateSmallerAvatar(accountOrChannel.Actor) | ||
80 | accountOrChannel.Actor = Object.assign(accountOrChannel.Actor, { Server: null }) | ||
81 | |||
82 | return sendUpdateActor(accountOrChannel, undefined) | ||
83 | } | ||
84 | |||
85 | async function generateSmallerAvatar (actor: MActorDefault) { | ||
86 | const bigAvatar = getBiggestActorImage(actor.Avatars) | ||
87 | |||
88 | const imageSize = minBy(ACTOR_IMAGES_SIZE[ActorImageType.AVATAR], 'width') | ||
89 | const sourceFilename = bigAvatar.filename | ||
90 | |||
91 | const newImageName = buildUUID() + getLowercaseExtension(sourceFilename) | ||
92 | const source = join(CONFIG.STORAGE.ACTOR_IMAGES, sourceFilename) | ||
93 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, newImageName) | ||
94 | |||
95 | await processImage(source, destination, imageSize, true) | ||
96 | |||
97 | const actorImageInfo = { | ||
98 | name: newImageName, | ||
99 | fileUrl: null, | ||
100 | height: imageSize.height, | ||
101 | width: imageSize.width, | ||
102 | onDisk: true | ||
103 | } | ||
104 | |||
105 | await updateActorImages(actor, ActorImageType.AVATAR, [ actorImageInfo ], undefined) | ||
106 | } | ||
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 4e6bd5e25..c4d1be121 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts | |||
@@ -18,10 +18,10 @@ import { | |||
18 | } from '../../lib/activitypub/url' | 18 | } from '../../lib/activitypub/url' |
19 | import { | 19 | import { |
20 | asyncMiddleware, | 20 | asyncMiddleware, |
21 | ensureIsLocalChannel, | ||
21 | executeIfActivityPub, | 22 | executeIfActivityPub, |
22 | localAccountValidator, | 23 | localAccountValidator, |
23 | videoChannelsNameWithHostValidator, | 24 | videoChannelsNameWithHostValidator, |
24 | ensureIsLocalChannel, | ||
25 | videosCustomGetValidator, | 25 | videosCustomGetValidator, |
26 | videosShareValidator | 26 | videosShareValidator |
27 | } from '../../middlewares' | 27 | } from '../../middlewares' |
@@ -265,8 +265,8 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp | |||
265 | const handler = async (start: number, count: number) => { | 265 | const handler = async (start: number, count: number) => { |
266 | const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count) | 266 | const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count) |
267 | return { | 267 | return { |
268 | total: result.count, | 268 | total: result.total, |
269 | data: result.rows.map(r => r.url) | 269 | data: result.data.map(r => r.url) |
270 | } | 270 | } |
271 | } | 271 | } |
272 | const json = await activityPubCollectionPagination(getLocalVideoSharesActivityPubUrl(video), handler, req.query.page) | 272 | const json = await activityPubCollectionPagination(getLocalVideoSharesActivityPubUrl(video), handler, req.query.page) |
@@ -301,9 +301,10 @@ async function videoCommentsController (req: express.Request, res: express.Respo | |||
301 | 301 | ||
302 | const handler = async (start: number, count: number) => { | 302 | const handler = async (start: number, count: number) => { |
303 | const result = await VideoCommentModel.listAndCountByVideoForAP(video, start, count) | 303 | const result = await VideoCommentModel.listAndCountByVideoForAP(video, start, count) |
304 | |||
304 | return { | 305 | return { |
305 | total: result.count, | 306 | total: result.total, |
306 | data: result.rows.map(r => r.url) | 307 | data: result.data.map(r => r.url) |
307 | } | 308 | } |
308 | } | 309 | } |
309 | const json = await activityPubCollectionPagination(getLocalVideoCommentsActivityPubUrl(video), handler, req.query.page) | 310 | const json = await activityPubCollectionPagination(getLocalVideoCommentsActivityPubUrl(video), handler, req.query.page) |
@@ -425,8 +426,8 @@ function videoRates (req: express.Request, rateType: VideoRateType, video: MVide | |||
425 | const handler = async (start: number, count: number) => { | 426 | const handler = async (start: number, count: number) => { |
426 | const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count) | 427 | const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count) |
427 | return { | 428 | return { |
428 | total: result.count, | 429 | total: result.total, |
429 | data: result.rows.map(r => r.url) | 430 | data: result.data.map(r => r.url) |
430 | } | 431 | } |
431 | } | 432 | } |
432 | return activityPubCollectionPagination(url, handler, req.query.page) | 433 | return activityPubCollectionPagination(url, handler, req.query.page) |
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index 46d89bafa..8d9f92d93 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts | |||
@@ -213,7 +213,7 @@ async function listAccountRatings (req: express.Request, res: express.Response) | |||
213 | sort: req.query.sort, | 213 | sort: req.query.sort, |
214 | type: req.query.rating | 214 | type: req.query.rating |
215 | }) | 215 | }) |
216 | return res.json(getFormattedObjects(resultList.rows, resultList.count)) | 216 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
217 | } | 217 | } |
218 | 218 | ||
219 | async function listAccountFollowers (req: express.Request, res: express.Response) { | 219 | async function listAccountFollowers (req: express.Request, res: express.Response) { |
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index c2ad0b710..a1d621152 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts | |||
@@ -1,7 +1,9 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import express from 'express' | 2 | import 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 { getBiggestActorImage } from '@server/lib/actor-image' | ||
4 | import { Hooks } from '@server/lib/plugins/hooks' | 5 | import { Hooks } from '@server/lib/plugins/hooks' |
6 | import { pick } from '@shared/core-utils' | ||
5 | import { ActorImageType, HttpStatusCode, UserUpdateMe, UserVideoQuota, UserVideoRate as FormattedUserVideoRate } from '@shared/models' | 7 | import { ActorImageType, HttpStatusCode, UserUpdateMe, UserVideoQuota, UserVideoRate as FormattedUserVideoRate } from '@shared/models' |
6 | import { AttributesOnly } from '@shared/typescript-utils' | 8 | import { AttributesOnly } from '@shared/typescript-utils' |
7 | import { createReqFiles } from '../../../helpers/express-utils' | 9 | import { createReqFiles } from '../../../helpers/express-utils' |
@@ -10,7 +12,7 @@ import { CONFIG } from '../../../initializers/config' | |||
10 | import { MIMETYPES } from '../../../initializers/constants' | 12 | import { MIMETYPES } from '../../../initializers/constants' |
11 | import { sequelizeTypescript } from '../../../initializers/database' | 13 | import { sequelizeTypescript } from '../../../initializers/database' |
12 | import { sendUpdateActor } from '../../../lib/activitypub/send' | 14 | import { sendUpdateActor } from '../../../lib/activitypub/send' |
13 | import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/local-actor' | 15 | import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../../lib/local-actor' |
14 | import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' | 16 | import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' |
15 | import { | 17 | import { |
16 | asyncMiddleware, | 18 | asyncMiddleware, |
@@ -30,7 +32,6 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat | |||
30 | import { UserModel } from '../../../models/user/user' | 32 | import { UserModel } from '../../../models/user/user' |
31 | import { VideoModel } from '../../../models/video/video' | 33 | import { VideoModel } from '../../../models/video/video' |
32 | import { VideoImportModel } from '../../../models/video/video-import' | 34 | import { VideoImportModel } from '../../../models/video/video-import' |
33 | import { pick } from '@shared/core-utils' | ||
34 | 35 | ||
35 | const auditLogger = auditLoggerFactory('users') | 36 | const auditLogger = auditLoggerFactory('users') |
36 | 37 | ||
@@ -253,9 +254,17 @@ async function updateMyAvatar (req: express.Request, res: express.Response) { | |||
253 | 254 | ||
254 | const userAccount = await AccountModel.load(user.Account.id) | 255 | const userAccount = await AccountModel.load(user.Account.id) |
255 | 256 | ||
256 | const avatar = await updateLocalActorImageFile(userAccount, avatarPhysicalFile, ActorImageType.AVATAR) | 257 | const avatars = await updateLocalActorImageFiles( |
258 | userAccount, | ||
259 | avatarPhysicalFile, | ||
260 | ActorImageType.AVATAR | ||
261 | ) | ||
257 | 262 | ||
258 | return res.json({ avatar: avatar.toFormattedJSON() }) | 263 | return res.json({ |
264 | // TODO: remove, deprecated in 4.2 | ||
265 | avatar: getBiggestActorImage(avatars).toFormattedJSON(), | ||
266 | avatars: avatars.map(avatar => avatar.toFormattedJSON()) | ||
267 | }) | ||
259 | } | 268 | } |
260 | 269 | ||
261 | async function deleteMyAvatar (req: express.Request, res: express.Response) { | 270 | async function deleteMyAvatar (req: express.Request, res: express.Response) { |
@@ -264,5 +273,5 @@ async function deleteMyAvatar (req: express.Request, res: express.Response) { | |||
264 | const userAccount = await AccountModel.load(user.Account.id) | 273 | const userAccount = await AccountModel.load(user.Account.id) |
265 | await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR) | 274 | await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR) |
266 | 275 | ||
267 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | 276 | return res.json({ avatars: [] }) |
268 | } | 277 | } |
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts index d107a306e..58732158f 100644 --- a/server/controllers/api/users/my-notifications.ts +++ b/server/controllers/api/users/my-notifications.ts | |||
@@ -3,7 +3,6 @@ import express from 'express' | |||
3 | import { UserNotificationModel } from '@server/models/user/user-notification' | 3 | import { UserNotificationModel } from '@server/models/user/user-notification' |
4 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | 4 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' |
5 | import { UserNotificationSetting } from '../../../../shared/models/users' | 5 | import { UserNotificationSetting } from '../../../../shared/models/users' |
6 | import { getFormattedObjects } from '../../../helpers/utils' | ||
7 | import { | 6 | import { |
8 | asyncMiddleware, | 7 | asyncMiddleware, |
9 | asyncRetryTransactionMiddleware, | 8 | asyncRetryTransactionMiddleware, |
@@ -20,6 +19,7 @@ import { | |||
20 | } from '../../../middlewares/validators/user-notifications' | 19 | } from '../../../middlewares/validators/user-notifications' |
21 | import { UserNotificationSettingModel } from '../../../models/user/user-notification-setting' | 20 | import { UserNotificationSettingModel } from '../../../models/user/user-notification-setting' |
22 | import { meRouter } from './me' | 21 | import { meRouter } from './me' |
22 | import { getFormattedObjects } from '@server/helpers/utils' | ||
23 | 23 | ||
24 | const myNotificationsRouter = express.Router() | 24 | const myNotificationsRouter = express.Router() |
25 | 25 | ||
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index e65550a22..2f869d9b3 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { pickCommonVideoQuery } from '@server/helpers/query' | 2 | import { pickCommonVideoQuery } from '@server/helpers/query' |
3 | import { getBiggestActorImage } from '@server/lib/actor-image' | ||
3 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
4 | import { ActorFollowModel } from '@server/models/actor/actor-follow' | 5 | import { ActorFollowModel } from '@server/models/actor/actor-follow' |
5 | import { getServerActor } from '@server/models/application/application' | 6 | import { getServerActor } from '@server/models/application/application' |
@@ -16,7 +17,7 @@ import { MIMETYPES } from '../../initializers/constants' | |||
16 | import { sequelizeTypescript } from '../../initializers/database' | 17 | import { sequelizeTypescript } from '../../initializers/database' |
17 | import { sendUpdateActor } from '../../lib/activitypub/send' | 18 | import { sendUpdateActor } from '../../lib/activitypub/send' |
18 | import { JobQueue } from '../../lib/job-queue' | 19 | import { JobQueue } from '../../lib/job-queue' |
19 | import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/local-actor' | 20 | import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../lib/local-actor' |
20 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' | 21 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' |
21 | import { | 22 | import { |
22 | asyncMiddleware, | 23 | asyncMiddleware, |
@@ -186,11 +187,15 @@ async function updateVideoChannelBanner (req: express.Request, res: express.Resp | |||
186 | const videoChannel = res.locals.videoChannel | 187 | const videoChannel = res.locals.videoChannel |
187 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) | 188 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) |
188 | 189 | ||
189 | const banner = await updateLocalActorImageFile(videoChannel, bannerPhysicalFile, ActorImageType.BANNER) | 190 | const banners = await updateLocalActorImageFiles(videoChannel, bannerPhysicalFile, ActorImageType.BANNER) |
190 | 191 | ||
191 | auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) | 192 | auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) |
192 | 193 | ||
193 | return res.json({ banner: banner.toFormattedJSON() }) | 194 | return res.json({ |
195 | // TODO: remove, deprecated in 4.2 | ||
196 | banner: getBiggestActorImage(banners).toFormattedJSON(), | ||
197 | banners: banners.map(b => b.toFormattedJSON()) | ||
198 | }) | ||
194 | } | 199 | } |
195 | 200 | ||
196 | async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { | 201 | async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { |
@@ -198,11 +203,14 @@ async function updateVideoChannelAvatar (req: express.Request, res: express.Resp | |||
198 | const videoChannel = res.locals.videoChannel | 203 | const videoChannel = res.locals.videoChannel |
199 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) | 204 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) |
200 | 205 | ||
201 | const avatar = await updateLocalActorImageFile(videoChannel, avatarPhysicalFile, ActorImageType.AVATAR) | 206 | const avatars = await updateLocalActorImageFiles(videoChannel, avatarPhysicalFile, ActorImageType.AVATAR) |
202 | |||
203 | auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) | 207 | auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) |
204 | 208 | ||
205 | return res.json({ avatar: avatar.toFormattedJSON() }) | 209 | return res.json({ |
210 | // TODO: remove, deprecated in 4.2 | ||
211 | avatar: getBiggestActorImage(avatars).toFormattedJSON(), | ||
212 | avatars: avatars.map(a => a.toFormattedJSON()) | ||
213 | }) | ||
206 | } | 214 | } |
207 | 215 | ||
208 | async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) { | 216 | async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) { |
diff --git a/server/controllers/client.ts b/server/controllers/client.ts index 8a56f2f75..f9514d988 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts | |||
@@ -68,7 +68,9 @@ const staticClientOverrides = [ | |||
68 | 'assets/images/icons/icon-512x512.png', | 68 | 'assets/images/icons/icon-512x512.png', |
69 | 'assets/images/default-playlist.jpg', | 69 | 'assets/images/default-playlist.jpg', |
70 | 'assets/images/default-avatar-account.png', | 70 | 'assets/images/default-avatar-account.png', |
71 | 'assets/images/default-avatar-video-channel.png' | 71 | 'assets/images/default-avatar-account-48x48.png', |
72 | 'assets/images/default-avatar-video-channel.png', | ||
73 | 'assets/images/default-avatar-video-channel-48x48.png' | ||
72 | ] | 74 | ] |
73 | 75 | ||
74 | for (const staticClientOverride of staticClientOverrides) { | 76 | for (const staticClientOverride of staticClientOverrides) { |
diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts index a4076ee56..55bf02660 100644 --- a/server/controllers/lazy-static.ts +++ b/server/controllers/lazy-static.ts | |||
@@ -64,7 +64,15 @@ async function getActorImage (req: express.Request, res: express.Response, next: | |||
64 | logger.info('Lazy serve remote actor image %s.', image.fileUrl) | 64 | logger.info('Lazy serve remote actor image %s.', image.fileUrl) |
65 | 65 | ||
66 | try { | 66 | try { |
67 | await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type }) | 67 | await pushActorImageProcessInQueue({ |
68 | filename: image.filename, | ||
69 | fileUrl: image.fileUrl, | ||
70 | size: { | ||
71 | height: image.height, | ||
72 | width: image.width | ||
73 | }, | ||
74 | type: image.type | ||
75 | }) | ||
68 | } catch (err) { | 76 | } catch (err) { |
69 | logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err }) | 77 | logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err }) |
70 | return res.status(HttpStatusCode.NOT_FOUND_404).end() | 78 | return res.status(HttpStatusCode.NOT_FOUND_404).end() |
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index fe721cbac..cbba2f51c 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts | |||
@@ -38,6 +38,9 @@ function getContextData (type: ContextType) { | |||
38 | sensitive: 'as:sensitive', | 38 | sensitive: 'as:sensitive', |
39 | language: 'sc:inLanguage', | 39 | language: 'sc:inLanguage', |
40 | 40 | ||
41 | // TODO: remove in a few versions, introduced in 4.2 | ||
42 | icons: 'as:icon', | ||
43 | |||
41 | isLiveBroadcast: 'sc:isLiveBroadcast', | 44 | isLiveBroadcast: 'sc:isLiveBroadcast', |
42 | liveSaveReplay: { | 45 | liveSaveReplay: { |
43 | '@type': 'sc:Boolean', | 46 | '@type': 'sc:Boolean', |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 1c47d43f0..9b972b87e 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -14,7 +14,7 @@ import { | |||
14 | VideoTranscodingFPS | 14 | VideoTranscodingFPS |
15 | } from '../../shared/models' | 15 | } from '../../shared/models' |
16 | import { ActivityPubActorType } from '../../shared/models/activitypub' | 16 | import { ActivityPubActorType } from '../../shared/models/activitypub' |
17 | import { FollowState } from '../../shared/models/actors' | 17 | import { ActorImageType, FollowState } from '../../shared/models/actors' |
18 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' | 18 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' |
19 | import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model' | 19 | import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model' |
20 | import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model' | 20 | import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model' |
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
24 | 24 | ||
25 | // --------------------------------------------------------------------------- | 25 | // --------------------------------------------------------------------------- |
26 | 26 | ||
27 | const LAST_MIGRATION_VERSION = 680 | 27 | const LAST_MIGRATION_VERSION = 685 |
28 | 28 | ||
29 | // --------------------------------------------------------------------------- | 29 | // --------------------------------------------------------------------------- |
30 | 30 | ||
@@ -633,15 +633,23 @@ const PREVIEWS_SIZE = { | |||
633 | height: 480, | 633 | height: 480, |
634 | minWidth: 400 | 634 | minWidth: 400 |
635 | } | 635 | } |
636 | const ACTOR_IMAGES_SIZE = { | 636 | const ACTOR_IMAGES_SIZE: { [key in ActorImageType]: { width: number, height: number }[]} = { |
637 | AVATARS: { | 637 | [ActorImageType.AVATAR]: [ |
638 | width: 120, | 638 | { |
639 | height: 120 | 639 | width: 120, |
640 | }, | 640 | height: 120 |
641 | BANNERS: { | 641 | }, |
642 | width: 1920, | 642 | { |
643 | height: 317 // 6/1 ratio | 643 | width: 48, |
644 | } | 644 | height: 48 |
645 | } | ||
646 | ], | ||
647 | [ActorImageType.BANNER]: [ | ||
648 | { | ||
649 | width: 1920, | ||
650 | height: 317 // 6/1 ratio | ||
651 | } | ||
652 | ] | ||
645 | } | 653 | } |
646 | 654 | ||
647 | const EMBED_SIZE = { | 655 | const EMBED_SIZE = { |
diff --git a/server/initializers/migrations/0685-multiple-actor-images.ts b/server/initializers/migrations/0685-multiple-actor-images.ts new file mode 100644 index 000000000..c656f7e28 --- /dev/null +++ b/server/initializers/migrations/0685-multiple-actor-images.ts | |||
@@ -0,0 +1,62 @@ | |||
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 | await utils.queryInterface.addColumn('actorImage', 'actorId', { | ||
11 | type: Sequelize.INTEGER, | ||
12 | defaultValue: null, | ||
13 | allowNull: true, | ||
14 | references: { | ||
15 | model: 'actor', | ||
16 | key: 'id' | ||
17 | }, | ||
18 | onDelete: 'CASCADE' | ||
19 | }, { transaction: utils.transaction }) | ||
20 | |||
21 | // Avatars | ||
22 | { | ||
23 | const query = `UPDATE "actorImage" SET "actorId" = (SELECT "id" FROM "actor" WHERE "actor"."avatarId" = "actorImage"."id") ` + | ||
24 | `WHERE "type" = 1` | ||
25 | await utils.sequelize.query(query, { type: Sequelize.QueryTypes.UPDATE, transaction: utils.transaction }) | ||
26 | } | ||
27 | |||
28 | // Banners | ||
29 | { | ||
30 | const query = `UPDATE "actorImage" SET "actorId" = (SELECT "id" FROM "actor" WHERE "actor"."bannerId" = "actorImage"."id") ` + | ||
31 | `WHERE "type" = 2` | ||
32 | await utils.sequelize.query(query, { type: Sequelize.QueryTypes.UPDATE, transaction: utils.transaction }) | ||
33 | } | ||
34 | |||
35 | // Remove orphans | ||
36 | { | ||
37 | const query = `DELETE FROM "actorImage" WHERE id NOT IN (` + | ||
38 | `SELECT "bannerId" FROM actor WHERE "bannerId" IS NOT NULL ` + | ||
39 | `UNION select "avatarId" FROM actor WHERE "avatarId" IS NOT NULL` + | ||
40 | `);` | ||
41 | |||
42 | await utils.sequelize.query(query, { type: Sequelize.QueryTypes.DELETE, transaction: utils.transaction }) | ||
43 | } | ||
44 | |||
45 | await utils.queryInterface.changeColumn('actorImage', 'actorId', { | ||
46 | type: Sequelize.INTEGER, | ||
47 | allowNull: false | ||
48 | }, { transaction: utils.transaction }) | ||
49 | |||
50 | await utils.queryInterface.removeColumn('actor', 'avatarId', { transaction: utils.transaction }) | ||
51 | await utils.queryInterface.removeColumn('actor', 'bannerId', { transaction: utils.transaction }) | ||
52 | } | ||
53 | } | ||
54 | |||
55 | function down () { | ||
56 | throw new Error('Not implemented.') | ||
57 | } | ||
58 | |||
59 | export { | ||
60 | up, | ||
61 | down | ||
62 | } | ||
diff --git a/server/lib/activitypub/actors/image.ts b/server/lib/activitypub/actors/image.ts index 443ad0a63..d17c2ef1a 100644 --- a/server/lib/activitypub/actors/image.ts +++ b/server/lib/activitypub/actors/image.ts | |||
@@ -12,53 +12,52 @@ type ImageInfo = { | |||
12 | onDisk?: boolean | 12 | onDisk?: boolean |
13 | } | 13 | } |
14 | 14 | ||
15 | async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) { | 15 | async function updateActorImages (actor: MActorImages, type: ActorImageType, imagesInfo: ImageInfo[], t: Transaction) { |
16 | const oldImageModel = type === ActorImageType.AVATAR | 16 | const avatarsOrBanners = type === ActorImageType.AVATAR |
17 | ? actor.Avatar | 17 | ? actor.Avatars |
18 | : actor.Banner | 18 | : actor.Banners |
19 | 19 | ||
20 | if (oldImageModel) { | 20 | if (imagesInfo.length === 0) { |
21 | // Don't update the avatar if the file URL did not change | 21 | await deleteActorImages(actor, type, t) |
22 | if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) return actor | 22 | } |
23 | |||
24 | for (const imageInfo of imagesInfo) { | ||
25 | const oldImageModel = (avatarsOrBanners || []).find(i => i.width === imageInfo.width) | ||
23 | 26 | ||
24 | try { | 27 | if (oldImageModel) { |
25 | await oldImageModel.destroy({ transaction: t }) | 28 | // Don't update the avatar if the file URL did not change |
29 | if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) { | ||
30 | continue | ||
31 | } | ||
26 | 32 | ||
27 | setActorImage(actor, type, null) | 33 | await safeDeleteActorImage(actor, oldImageModel, type, t) |
28 | } catch (err) { | ||
29 | logger.error('Cannot remove old actor image of actor %s.', actor.url, { err }) | ||
30 | } | 34 | } |
31 | } | ||
32 | 35 | ||
33 | if (imageInfo) { | ||
34 | const imageModel = await ActorImageModel.create({ | 36 | const imageModel = await ActorImageModel.create({ |
35 | filename: imageInfo.name, | 37 | filename: imageInfo.name, |
36 | onDisk: imageInfo.onDisk ?? false, | 38 | onDisk: imageInfo.onDisk ?? false, |
37 | fileUrl: imageInfo.fileUrl, | 39 | fileUrl: imageInfo.fileUrl, |
38 | height: imageInfo.height, | 40 | height: imageInfo.height, |
39 | width: imageInfo.width, | 41 | width: imageInfo.width, |
40 | type | 42 | type, |
43 | actorId: actor.id | ||
41 | }, { transaction: t }) | 44 | }, { transaction: t }) |
42 | 45 | ||
43 | setActorImage(actor, type, imageModel) | 46 | addActorImage(actor, type, imageModel) |
44 | } | 47 | } |
45 | 48 | ||
46 | return actor | 49 | return actor |
47 | } | 50 | } |
48 | 51 | ||
49 | async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) { | 52 | async function deleteActorImages (actor: MActorImages, type: ActorImageType, t: Transaction) { |
50 | try { | 53 | try { |
51 | if (type === ActorImageType.AVATAR) { | 54 | const association = buildAssociationName(type) |
52 | await actor.Avatar.destroy({ transaction: t }) | ||
53 | |||
54 | actor.avatarId = null | ||
55 | actor.Avatar = null | ||
56 | } else { | ||
57 | await actor.Banner.destroy({ transaction: t }) | ||
58 | 55 | ||
59 | actor.bannerId = null | 56 | for (const image of actor[association]) { |
60 | actor.Banner = null | 57 | await image.destroy({ transaction: t }) |
61 | } | 58 | } |
59 | |||
60 | actor[association] = [] | ||
62 | } catch (err) { | 61 | } catch (err) { |
63 | logger.error('Cannot remove old image of actor %s.', actor.url, { err }) | 62 | logger.error('Cannot remove old image of actor %s.', actor.url, { err }) |
64 | } | 63 | } |
@@ -66,29 +65,37 @@ async function deleteActorImageInstance (actor: MActorImages, type: ActorImageTy | |||
66 | return actor | 65 | return actor |
67 | } | 66 | } |
68 | 67 | ||
68 | async function safeDeleteActorImage (actor: MActorImages, toDelete: MActorImage, type: ActorImageType, t: Transaction) { | ||
69 | try { | ||
70 | await toDelete.destroy({ transaction: t }) | ||
71 | |||
72 | const association = buildAssociationName(type) | ||
73 | actor[association] = actor[association].filter(image => image.id !== toDelete.id) | ||
74 | } catch (err) { | ||
75 | logger.error('Cannot remove old actor image of actor %s.', actor.url, { err }) | ||
76 | } | ||
77 | } | ||
78 | |||
69 | // --------------------------------------------------------------------------- | 79 | // --------------------------------------------------------------------------- |
70 | 80 | ||
71 | export { | 81 | export { |
72 | ImageInfo, | 82 | ImageInfo, |
73 | 83 | ||
74 | updateActorImageInstance, | 84 | updateActorImages, |
75 | deleteActorImageInstance | 85 | deleteActorImages |
76 | } | 86 | } |
77 | 87 | ||
78 | // --------------------------------------------------------------------------- | 88 | // --------------------------------------------------------------------------- |
79 | 89 | ||
80 | function setActorImage (actorModel: MActorImages, type: ActorImageType, imageModel: MActorImage) { | 90 | function addActorImage (actor: MActorImages, type: ActorImageType, imageModel: MActorImage) { |
81 | const id = imageModel | 91 | const association = buildAssociationName(type) |
82 | ? imageModel.id | 92 | if (!actor[association]) actor[association] = [] |
83 | : null | 93 | |
84 | 94 | actor[association].push(imageModel) | |
85 | if (type === ActorImageType.AVATAR) { | 95 | } |
86 | actorModel.avatarId = id | ||
87 | actorModel.Avatar = imageModel | ||
88 | } else { | ||
89 | actorModel.bannerId = id | ||
90 | actorModel.Banner = imageModel | ||
91 | } | ||
92 | 96 | ||
93 | return actorModel | 97 | function buildAssociationName (type: ActorImageType) { |
98 | return type === ActorImageType.AVATAR | ||
99 | ? 'Avatars' | ||
100 | : 'Banners' | ||
94 | } | 101 | } |
diff --git a/server/lib/activitypub/actors/shared/creator.ts b/server/lib/activitypub/actors/shared/creator.ts index 999aed97d..500bc9912 100644 --- a/server/lib/activitypub/actors/shared/creator.ts +++ b/server/lib/activitypub/actors/shared/creator.ts | |||
@@ -6,8 +6,8 @@ import { ServerModel } from '@server/models/server/server' | |||
6 | import { VideoChannelModel } from '@server/models/video/video-channel' | 6 | import { VideoChannelModel } from '@server/models/video/video-channel' |
7 | import { MAccount, MAccountDefault, MActor, MActorFullActor, MActorId, MActorImages, MChannel, MServer } from '@server/types/models' | 7 | import { MAccount, MAccountDefault, MActor, MActorFullActor, MActorId, MActorImages, MChannel, MServer } from '@server/types/models' |
8 | import { ActivityPubActor, ActorImageType } from '@shared/models' | 8 | import { ActivityPubActor, ActorImageType } from '@shared/models' |
9 | import { updateActorImageInstance } from '../image' | 9 | import { updateActorImages } from '../image' |
10 | import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImageInfoFromObject } from './object-to-model-attributes' | 10 | import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImagesInfoFromObject } from './object-to-model-attributes' |
11 | import { fetchActorFollowsCount } from './url-to-object' | 11 | import { fetchActorFollowsCount } from './url-to-object' |
12 | 12 | ||
13 | export class APActorCreator { | 13 | export class APActorCreator { |
@@ -27,11 +27,11 @@ export class APActorCreator { | |||
27 | return sequelizeTypescript.transaction(async t => { | 27 | return sequelizeTypescript.transaction(async t => { |
28 | const server = await this.setServer(actorInstance, t) | 28 | const server = await this.setServer(actorInstance, t) |
29 | 29 | ||
30 | await this.setImageIfNeeded(actorInstance, ActorImageType.AVATAR, t) | ||
31 | await this.setImageIfNeeded(actorInstance, ActorImageType.BANNER, t) | ||
32 | |||
33 | const { actorCreated, created } = await this.saveActor(actorInstance, t) | 30 | const { actorCreated, created } = await this.saveActor(actorInstance, t) |
34 | 31 | ||
32 | await this.setImageIfNeeded(actorCreated, ActorImageType.AVATAR, t) | ||
33 | await this.setImageIfNeeded(actorCreated, ActorImageType.BANNER, t) | ||
34 | |||
35 | await this.tryToFixActorUrlIfNeeded(actorCreated, actorInstance, created, t) | 35 | await this.tryToFixActorUrlIfNeeded(actorCreated, actorInstance, created, t) |
36 | 36 | ||
37 | if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { // Account or PeerTube instance | 37 | if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { // Account or PeerTube instance |
@@ -71,10 +71,10 @@ export class APActorCreator { | |||
71 | } | 71 | } |
72 | 72 | ||
73 | private async setImageIfNeeded (actor: MActor, type: ActorImageType, t: Transaction) { | 73 | private async setImageIfNeeded (actor: MActor, type: ActorImageType, t: Transaction) { |
74 | const imageInfo = getImageInfoFromObject(this.actorObject, type) | 74 | const imagesInfo = getImagesInfoFromObject(this.actorObject, type) |
75 | if (!imageInfo) return | 75 | if (imagesInfo.length === 0) return |
76 | 76 | ||
77 | return updateActorImageInstance(actor as MActorImages, type, imageInfo, t) | 77 | return updateActorImages(actor as MActorImages, type, imagesInfo, t) |
78 | } | 78 | } |
79 | 79 | ||
80 | private async saveActor (actor: MActor, t: Transaction) { | 80 | private async saveActor (actor: MActor, t: Transaction) { |
diff --git a/server/lib/activitypub/actors/shared/object-to-model-attributes.ts b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts index 23bc972e5..f6a78c457 100644 --- a/server/lib/activitypub/actors/shared/object-to-model-attributes.ts +++ b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts | |||
@@ -4,7 +4,7 @@ import { ActorModel } from '@server/models/actor/actor' | |||
4 | import { FilteredModelAttributes } from '@server/types' | 4 | import { FilteredModelAttributes } from '@server/types' |
5 | import { getLowercaseExtension } from '@shared/core-utils' | 5 | import { getLowercaseExtension } from '@shared/core-utils' |
6 | import { buildUUID } from '@shared/extra-utils' | 6 | import { buildUUID } from '@shared/extra-utils' |
7 | import { ActivityPubActor, ActorImageType } from '@shared/models' | 7 | import { ActivityIconObject, ActivityPubActor, ActorImageType } from '@shared/models' |
8 | 8 | ||
9 | function getActorAttributesFromObject ( | 9 | function getActorAttributesFromObject ( |
10 | actorObject: ActivityPubActor, | 10 | actorObject: ActivityPubActor, |
@@ -30,33 +30,36 @@ function getActorAttributesFromObject ( | |||
30 | } | 30 | } |
31 | } | 31 | } |
32 | 32 | ||
33 | function getImageInfoFromObject (actorObject: ActivityPubActor, type: ActorImageType) { | 33 | function getImagesInfoFromObject (actorObject: ActivityPubActor, type: ActorImageType) { |
34 | const mimetypes = MIMETYPES.IMAGE | 34 | const iconsOrImages = type === ActorImageType.AVATAR |
35 | const icon = type === ActorImageType.AVATAR | 35 | ? actorObject.icons || actorObject.icon |
36 | ? actorObject.icon | ||
37 | : actorObject.image | 36 | : actorObject.image |
38 | 37 | ||
39 | if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined | 38 | return normalizeIconOrImage(iconsOrImages).map(iconOrImage => { |
39 | const mimetypes = MIMETYPES.IMAGE | ||
40 | 40 | ||
41 | let extension: string | 41 | if (iconOrImage.type !== 'Image' || !isActivityPubUrlValid(iconOrImage.url)) return undefined |
42 | 42 | ||
43 | if (icon.mediaType) { | 43 | let extension: string |
44 | extension = mimetypes.MIMETYPE_EXT[icon.mediaType] | ||
45 | } else { | ||
46 | const tmp = getLowercaseExtension(icon.url) | ||
47 | 44 | ||
48 | if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp | 45 | if (iconOrImage.mediaType) { |
49 | } | 46 | extension = mimetypes.MIMETYPE_EXT[iconOrImage.mediaType] |
47 | } else { | ||
48 | const tmp = getLowercaseExtension(iconOrImage.url) | ||
50 | 49 | ||
51 | if (!extension) return undefined | 50 | if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp |
51 | } | ||
52 | 52 | ||
53 | return { | 53 | if (!extension) return undefined |
54 | name: buildUUID() + extension, | 54 | |
55 | fileUrl: icon.url, | 55 | return { |
56 | height: icon.height, | 56 | name: buildUUID() + extension, |
57 | width: icon.width, | 57 | fileUrl: iconOrImage.url, |
58 | type | 58 | height: iconOrImage.height, |
59 | } | 59 | width: iconOrImage.width, |
60 | type | ||
61 | } | ||
62 | }) | ||
60 | } | 63 | } |
61 | 64 | ||
62 | function getActorDisplayNameFromObject (actorObject: ActivityPubActor) { | 65 | function getActorDisplayNameFromObject (actorObject: ActivityPubActor) { |
@@ -65,6 +68,15 @@ function getActorDisplayNameFromObject (actorObject: ActivityPubActor) { | |||
65 | 68 | ||
66 | export { | 69 | export { |
67 | getActorAttributesFromObject, | 70 | getActorAttributesFromObject, |
68 | getImageInfoFromObject, | 71 | getImagesInfoFromObject, |
69 | getActorDisplayNameFromObject | 72 | getActorDisplayNameFromObject |
70 | } | 73 | } |
74 | |||
75 | // --------------------------------------------------------------------------- | ||
76 | |||
77 | function normalizeIconOrImage (icon: ActivityIconObject | ActivityIconObject[]): ActivityIconObject[] { | ||
78 | if (Array.isArray(icon)) return icon | ||
79 | if (icon) return [ icon ] | ||
80 | |||
81 | return [] | ||
82 | } | ||
diff --git a/server/lib/activitypub/actors/updater.ts b/server/lib/activitypub/actors/updater.ts index 042438d9c..fe94af9f1 100644 --- a/server/lib/activitypub/actors/updater.ts +++ b/server/lib/activitypub/actors/updater.ts | |||
@@ -5,9 +5,9 @@ import { VideoChannelModel } from '@server/models/video/video-channel' | |||
5 | import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models' | 5 | import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models' |
6 | import { ActivityPubActor, ActorImageType } from '@shared/models' | 6 | import { ActivityPubActor, ActorImageType } from '@shared/models' |
7 | import { getOrCreateAPOwner } from './get' | 7 | import { getOrCreateAPOwner } from './get' |
8 | import { updateActorImageInstance } from './image' | 8 | import { updateActorImages } from './image' |
9 | import { fetchActorFollowsCount } from './shared' | 9 | import { fetchActorFollowsCount } from './shared' |
10 | import { getImageInfoFromObject } from './shared/object-to-model-attributes' | 10 | import { getImagesInfoFromObject } from './shared/object-to-model-attributes' |
11 | 11 | ||
12 | export class APActorUpdater { | 12 | export class APActorUpdater { |
13 | 13 | ||
@@ -29,8 +29,8 @@ export class APActorUpdater { | |||
29 | } | 29 | } |
30 | 30 | ||
31 | async update () { | 31 | async update () { |
32 | const avatarInfo = getImageInfoFromObject(this.actorObject, ActorImageType.AVATAR) | 32 | const avatarsInfo = getImagesInfoFromObject(this.actorObject, ActorImageType.AVATAR) |
33 | const bannerInfo = getImageInfoFromObject(this.actorObject, ActorImageType.BANNER) | 33 | const bannersInfo = getImagesInfoFromObject(this.actorObject, ActorImageType.BANNER) |
34 | 34 | ||
35 | try { | 35 | try { |
36 | await this.updateActorInstance(this.actor, this.actorObject) | 36 | await this.updateActorInstance(this.actor, this.actorObject) |
@@ -47,8 +47,8 @@ export class APActorUpdater { | |||
47 | } | 47 | } |
48 | 48 | ||
49 | await runInReadCommittedTransaction(async t => { | 49 | await runInReadCommittedTransaction(async t => { |
50 | await updateActorImageInstance(this.actor, ActorImageType.AVATAR, avatarInfo, t) | 50 | await updateActorImages(this.actor, ActorImageType.BANNER, bannersInfo, t) |
51 | await updateActorImageInstance(this.actor, ActorImageType.BANNER, bannerInfo, t) | 51 | await updateActorImages(this.actor, ActorImageType.AVATAR, avatarsInfo, t) |
52 | }) | 52 | }) |
53 | 53 | ||
54 | await runInReadCommittedTransaction(async t => { | 54 | await runInReadCommittedTransaction(async t => { |
diff --git a/server/lib/actor-image.ts b/server/lib/actor-image.ts new file mode 100644 index 000000000..e9bd148f6 --- /dev/null +++ b/server/lib/actor-image.ts | |||
@@ -0,0 +1,14 @@ | |||
1 | import maxBy from 'lodash/maxBy' | ||
2 | |||
3 | function getBiggestActorImage <T extends { width: number }> (images: T[]) { | ||
4 | const image = maxBy(images, 'width') | ||
5 | |||
6 | // If width is null, maxBy won't return a value | ||
7 | if (!image) return images[0] | ||
8 | |||
9 | return image | ||
10 | } | ||
11 | |||
12 | export { | ||
13 | getBiggestActorImage | ||
14 | } | ||
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index 19354ab70..c010f3c44 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -3,6 +3,7 @@ import { readFile } from 'fs-extra' | |||
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import validator from 'validator' | 4 | import validator from 'validator' |
5 | import { toCompleteUUID } from '@server/helpers/custom-validators/misc' | 5 | import { toCompleteUUID } from '@server/helpers/custom-validators/misc' |
6 | import { ActorImageModel } from '@server/models/actor/actor-image' | ||
6 | import { root } from '@shared/core-utils' | 7 | import { root } from '@shared/core-utils' |
7 | import { escapeHTML } from '@shared/core-utils/renderer' | 8 | import { escapeHTML } from '@shared/core-utils/renderer' |
8 | import { sha256 } from '@shared/extra-utils' | 9 | import { sha256 } from '@shared/extra-utils' |
@@ -16,7 +17,6 @@ import { mdToOneLinePlainText } from '../helpers/markdown' | |||
16 | import { CONFIG } from '../initializers/config' | 17 | import { CONFIG } from '../initializers/config' |
17 | import { | 18 | import { |
18 | ACCEPT_HEADERS, | 19 | ACCEPT_HEADERS, |
19 | ACTOR_IMAGES_SIZE, | ||
20 | CUSTOM_HTML_TAG_COMMENTS, | 20 | CUSTOM_HTML_TAG_COMMENTS, |
21 | EMBED_SIZE, | 21 | EMBED_SIZE, |
22 | FILES_CONTENT_HASH, | 22 | FILES_CONTENT_HASH, |
@@ -29,6 +29,7 @@ import { VideoModel } from '../models/video/video' | |||
29 | import { VideoChannelModel } from '../models/video/video-channel' | 29 | import { VideoChannelModel } from '../models/video/video-channel' |
30 | import { VideoPlaylistModel } from '../models/video/video-playlist' | 30 | import { VideoPlaylistModel } from '../models/video/video-playlist' |
31 | import { MAccountActor, MChannelActor } from '../types/models' | 31 | import { MAccountActor, MChannelActor } from '../types/models' |
32 | import { getBiggestActorImage } from './actor-image' | ||
32 | import { ServerConfigManager } from './server-config-manager' | 33 | import { ServerConfigManager } from './server-config-manager' |
33 | 34 | ||
34 | type Tags = { | 35 | type Tags = { |
@@ -273,10 +274,11 @@ class ClientHtml { | |||
273 | const siteName = CONFIG.INSTANCE.NAME | 274 | const siteName = CONFIG.INSTANCE.NAME |
274 | const title = entity.getDisplayName() | 275 | const title = entity.getDisplayName() |
275 | 276 | ||
277 | const avatar = getBiggestActorImage(entity.Actor.Avatars) | ||
276 | const image = { | 278 | const image = { |
277 | url: entity.Actor.getAvatarUrl(), | 279 | url: ActorImageModel.getImageUrl(avatar), |
278 | width: ACTOR_IMAGES_SIZE.AVATARS.width, | 280 | width: avatar?.width, |
279 | height: ACTOR_IMAGES_SIZE.AVATARS.height | 281 | height: avatar?.height |
280 | } | 282 | } |
281 | 283 | ||
282 | const ogType = 'website' | 284 | const ogType = 'website' |
diff --git a/server/lib/local-actor.ts b/server/lib/local-actor.ts index c6826759b..01046d017 100644 --- a/server/lib/local-actor.ts +++ b/server/lib/local-actor.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import 'multer' | ||
2 | import { queue } from 'async' | 1 | import { queue } from 'async' |
2 | import { remove } from 'fs-extra' | ||
3 | import LRUCache from 'lru-cache' | 3 | import LRUCache from 'lru-cache' |
4 | import { join } from 'path' | 4 | import { join } from 'path' |
5 | import { ActorModel } from '@server/models/actor/actor' | 5 | import { ActorModel } from '@server/models/actor/actor' |
@@ -13,7 +13,7 @@ import { CONFIG } from '../initializers/config' | |||
13 | import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY, WEBSERVER } from '../initializers/constants' | 13 | import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY, WEBSERVER } from '../initializers/constants' |
14 | import { sequelizeTypescript } from '../initializers/database' | 14 | import { sequelizeTypescript } from '../initializers/database' |
15 | import { MAccountDefault, MActor, MChannelDefault } from '../types/models' | 15 | import { MAccountDefault, MActor, MChannelDefault } from '../types/models' |
16 | import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actors' | 16 | import { deleteActorImages, updateActorImages } from './activitypub/actors' |
17 | import { sendUpdateActor } from './activitypub/send' | 17 | import { sendUpdateActor } from './activitypub/send' |
18 | 18 | ||
19 | function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { | 19 | function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { |
@@ -33,64 +33,69 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU | |||
33 | }) as MActor | 33 | }) as MActor |
34 | } | 34 | } |
35 | 35 | ||
36 | async function updateLocalActorImageFile ( | 36 | async function updateLocalActorImageFiles ( |
37 | accountOrChannel: MAccountDefault | MChannelDefault, | 37 | accountOrChannel: MAccountDefault | MChannelDefault, |
38 | imagePhysicalFile: Express.Multer.File, | 38 | imagePhysicalFile: Express.Multer.File, |
39 | type: ActorImageType | 39 | type: ActorImageType |
40 | ) { | 40 | ) { |
41 | const imageSize = type === ActorImageType.AVATAR | 41 | const processImageSize = async (imageSize: { width: number, height: number }) => { |
42 | ? ACTOR_IMAGES_SIZE.AVATARS | 42 | const extension = getLowercaseExtension(imagePhysicalFile.filename) |
43 | : ACTOR_IMAGES_SIZE.BANNERS | 43 | |
44 | 44 | const imageName = buildUUID() + extension | |
45 | const extension = getLowercaseExtension(imagePhysicalFile.filename) | 45 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) |
46 | 46 | await processImage(imagePhysicalFile.path, destination, imageSize, true) | |
47 | const imageName = buildUUID() + extension | 47 | |
48 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) | 48 | return { |
49 | await processImage(imagePhysicalFile.path, destination, imageSize) | 49 | imageName, |
50 | 50 | imageSize | |
51 | return retryTransactionWrapper(() => { | 51 | } |
52 | return sequelizeTypescript.transaction(async t => { | 52 | } |
53 | const actorImageInfo = { | 53 | |
54 | name: imageName, | 54 | const processedImages = await Promise.all(ACTOR_IMAGES_SIZE[type].map(processImageSize)) |
55 | fileUrl: null, | 55 | await remove(imagePhysicalFile.path) |
56 | height: imageSize.height, | 56 | |
57 | width: imageSize.width, | 57 | return retryTransactionWrapper(() => sequelizeTypescript.transaction(async t => { |
58 | onDisk: true | 58 | const actorImagesInfo = processedImages.map(({ imageName, imageSize }) => ({ |
59 | } | 59 | name: imageName, |
60 | 60 | fileUrl: null, | |
61 | const updatedActor = await updateActorImageInstance(accountOrChannel.Actor, type, actorImageInfo, t) | 61 | height: imageSize.height, |
62 | await updatedActor.save({ transaction: t }) | 62 | width: imageSize.width, |
63 | 63 | onDisk: true | |
64 | await sendUpdateActor(accountOrChannel, t) | 64 | })) |
65 | 65 | ||
66 | return type === ActorImageType.AVATAR | 66 | const updatedActor = await updateActorImages(accountOrChannel.Actor, type, actorImagesInfo, t) |
67 | ? updatedActor.Avatar | 67 | await updatedActor.save({ transaction: t }) |
68 | : updatedActor.Banner | 68 | |
69 | }) | 69 | await sendUpdateActor(accountOrChannel, t) |
70 | }) | 70 | |
71 | return type === ActorImageType.AVATAR | ||
72 | ? updatedActor.Avatars | ||
73 | : updatedActor.Banners | ||
74 | })) | ||
71 | } | 75 | } |
72 | 76 | ||
73 | async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) { | 77 | async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) { |
74 | return retryTransactionWrapper(() => { | 78 | return retryTransactionWrapper(() => { |
75 | return sequelizeTypescript.transaction(async t => { | 79 | return sequelizeTypescript.transaction(async t => { |
76 | const updatedActor = await deleteActorImageInstance(accountOrChannel.Actor, type, t) | 80 | const updatedActor = await deleteActorImages(accountOrChannel.Actor, type, t) |
77 | await updatedActor.save({ transaction: t }) | 81 | await updatedActor.save({ transaction: t }) |
78 | 82 | ||
79 | await sendUpdateActor(accountOrChannel, t) | 83 | await sendUpdateActor(accountOrChannel, t) |
80 | 84 | ||
81 | return updatedActor.Avatar | 85 | return updatedActor.Avatars |
82 | }) | 86 | }) |
83 | }) | 87 | }) |
84 | } | 88 | } |
85 | 89 | ||
86 | type DownloadImageQueueTask = { fileUrl: string, filename: string, type: ActorImageType } | 90 | type DownloadImageQueueTask = { |
91 | fileUrl: string | ||
92 | filename: string | ||
93 | type: ActorImageType | ||
94 | size: typeof ACTOR_IMAGES_SIZE[ActorImageType][0] | ||
95 | } | ||
87 | 96 | ||
88 | const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { | 97 | const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { |
89 | const size = task.type === ActorImageType.AVATAR | 98 | downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, task.size) |
90 | ? ACTOR_IMAGES_SIZE.AVATARS | ||
91 | : ACTOR_IMAGES_SIZE.BANNERS | ||
92 | |||
93 | downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, size) | ||
94 | .then(() => cb()) | 99 | .then(() => cb()) |
95 | .catch(err => cb(err)) | 100 | .catch(err => cb(err)) |
96 | }, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE) | 101 | }, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE) |
@@ -110,7 +115,7 @@ const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE. | |||
110 | 115 | ||
111 | export { | 116 | export { |
112 | actorImagePathUnsafeCache, | 117 | actorImagePathUnsafeCache, |
113 | updateLocalActorImageFile, | 118 | updateLocalActorImageFiles, |
114 | deleteLocalActorImageFile, | 119 | deleteLocalActorImageFile, |
115 | pushActorImageProcessInQueue, | 120 | pushActorImageProcessInQueue, |
116 | buildActorInstance | 121 | buildActorInstance |
diff --git a/server/lib/notifier/shared/comment/comment-mention.ts b/server/lib/notifier/shared/comment/comment-mention.ts index 765cbaad9..ecd1687b4 100644 --- a/server/lib/notifier/shared/comment/comment-mention.ts +++ b/server/lib/notifier/shared/comment/comment-mention.ts | |||
@@ -77,7 +77,7 @@ export class CommentMention extends AbstractNotification <MCommentOwnerVideo, MU | |||
77 | userId: user.id, | 77 | userId: user.id, |
78 | commentId: this.payload.id | 78 | commentId: this.payload.id |
79 | }) | 79 | }) |
80 | notification.Comment = this.payload | 80 | notification.VideoComment = this.payload |
81 | 81 | ||
82 | return notification | 82 | return notification |
83 | } | 83 | } |
diff --git a/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts b/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts index b76fc15bf..757502703 100644 --- a/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts +++ b/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts | |||
@@ -44,7 +44,7 @@ export class NewCommentForVideoOwner extends AbstractNotification <MCommentOwner | |||
44 | userId: user.id, | 44 | userId: user.id, |
45 | commentId: this.payload.id | 45 | commentId: this.payload.id |
46 | }) | 46 | }) |
47 | notification.Comment = this.payload | 47 | notification.VideoComment = this.payload |
48 | 48 | ||
49 | return notification | 49 | return notification |
50 | } | 50 | } |
diff --git a/server/models/abuse/abuse-message.ts b/server/models/abuse/abuse-message.ts index 6a441a210..d9eb25f0f 100644 --- a/server/models/abuse/abuse-message.ts +++ b/server/models/abuse/abuse-message.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses' | 2 | import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses' |
3 | import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models' | 3 | import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/typescript-utils' | ||
5 | import { AbuseMessage } from '@shared/models' | 4 | import { AbuseMessage } from '@shared/models' |
5 | import { AttributesOnly } from '@shared/typescript-utils' | ||
6 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' | 6 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' |
7 | import { getSort, throwIfNotValid } from '../utils' | 7 | import { getSort, throwIfNotValid } from '../utils' |
8 | import { AbuseModel } from './abuse' | 8 | import { AbuseModel } from './abuse' |
9 | import { FindOptions } from 'sequelize/dist' | ||
9 | 10 | ||
10 | @Table({ | 11 | @Table({ |
11 | tableName: 'abuseMessage', | 12 | tableName: 'abuseMessage', |
@@ -62,21 +63,28 @@ export class AbuseMessageModel extends Model<Partial<AttributesOnly<AbuseMessage | |||
62 | Abuse: AbuseModel | 63 | Abuse: AbuseModel |
63 | 64 | ||
64 | static listForApi (abuseId: number) { | 65 | static listForApi (abuseId: number) { |
65 | const options = { | 66 | const getQuery = (forCount: boolean) => { |
66 | where: { abuseId }, | 67 | const query: FindOptions = { |
68 | where: { abuseId }, | ||
69 | order: getSort('createdAt') | ||
70 | } | ||
67 | 71 | ||
68 | order: getSort('createdAt'), | 72 | if (forCount !== true) { |
73 | query.include = [ | ||
74 | { | ||
75 | model: AccountModel.scope(AccountScopeNames.SUMMARY), | ||
76 | required: false | ||
77 | } | ||
78 | ] | ||
79 | } | ||
69 | 80 | ||
70 | include: [ | 81 | return query |
71 | { | ||
72 | model: AccountModel.scope(AccountScopeNames.SUMMARY), | ||
73 | required: false | ||
74 | } | ||
75 | ] | ||
76 | } | 82 | } |
77 | 83 | ||
78 | return AbuseMessageModel.findAndCountAll(options) | 84 | return Promise.all([ |
79 | .then(({ rows, count }) => ({ data: rows, total: count })) | 85 | AbuseMessageModel.count(getQuery(true)), |
86 | AbuseMessageModel.findAll(getQuery(false)) | ||
87 | ]).then(([ total, data ]) => ({ total, data })) | ||
80 | } | 88 | } |
81 | 89 | ||
82 | static loadByIdAndAbuseId (messageId: number, abuseId: number): Promise<MAbuseMessage> { | 90 | static loadByIdAndAbuseId (messageId: number, abuseId: number): Promise<MAbuseMessage> { |
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index 1162962bf..a7b8db076 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Op, QueryTypes } from 'sequelize' | 1 | import { FindOptions, Op, QueryTypes } from 'sequelize' |
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { handlesToNameAndHost } from '@server/helpers/actors' | 3 | import { handlesToNameAndHost } from '@server/helpers/actors' |
4 | import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' | 4 | import { MAccountBlocklist, MAccountBlocklistFormattable } from '@server/types/models' |
5 | import { AttributesOnly } from '@shared/typescript-utils' | 5 | import { AttributesOnly } from '@shared/typescript-utils' |
6 | import { AccountBlock } from '../../../shared/models' | 6 | import { AccountBlock } from '../../../shared/models' |
7 | import { ActorModel } from '../actor/actor' | 7 | import { ActorModel } from '../actor/actor' |
@@ -9,27 +9,6 @@ import { ServerModel } from '../server/server' | |||
9 | import { createSafeIn, getSort, searchAttribute } from '../utils' | 9 | import { createSafeIn, getSort, searchAttribute } from '../utils' |
10 | import { AccountModel } from './account' | 10 | import { AccountModel } from './account' |
11 | 11 | ||
12 | enum ScopeNames { | ||
13 | WITH_ACCOUNTS = 'WITH_ACCOUNTS' | ||
14 | } | ||
15 | |||
16 | @Scopes(() => ({ | ||
17 | [ScopeNames.WITH_ACCOUNTS]: { | ||
18 | include: [ | ||
19 | { | ||
20 | model: AccountModel, | ||
21 | required: true, | ||
22 | as: 'ByAccount' | ||
23 | }, | ||
24 | { | ||
25 | model: AccountModel, | ||
26 | required: true, | ||
27 | as: 'BlockedAccount' | ||
28 | } | ||
29 | ] | ||
30 | } | ||
31 | })) | ||
32 | |||
33 | @Table({ | 12 | @Table({ |
34 | tableName: 'accountBlocklist', | 13 | tableName: 'accountBlocklist', |
35 | indexes: [ | 14 | indexes: [ |
@@ -123,33 +102,45 @@ export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountB | |||
123 | }) { | 102 | }) { |
124 | const { start, count, sort, search, accountId } = parameters | 103 | const { start, count, sort, search, accountId } = parameters |
125 | 104 | ||
126 | const query = { | 105 | const getQuery = (forCount: boolean) => { |
127 | offset: start, | 106 | const query: FindOptions = { |
128 | limit: count, | 107 | offset: start, |
129 | order: getSort(sort) | 108 | limit: count, |
130 | } | 109 | order: getSort(sort), |
110 | where: { accountId } | ||
111 | } | ||
131 | 112 | ||
132 | const where = { | 113 | if (search) { |
133 | accountId | 114 | Object.assign(query.where, { |
134 | } | 115 | [Op.or]: [ |
116 | searchAttribute(search, '$BlockedAccount.name$'), | ||
117 | searchAttribute(search, '$BlockedAccount.Actor.url$') | ||
118 | ] | ||
119 | }) | ||
120 | } | ||
135 | 121 | ||
136 | if (search) { | 122 | if (forCount !== true) { |
137 | Object.assign(where, { | 123 | query.include = [ |
138 | [Op.or]: [ | 124 | { |
139 | searchAttribute(search, '$BlockedAccount.name$'), | 125 | model: AccountModel, |
140 | searchAttribute(search, '$BlockedAccount.Actor.url$') | 126 | required: true, |
127 | as: 'ByAccount' | ||
128 | }, | ||
129 | { | ||
130 | model: AccountModel, | ||
131 | required: true, | ||
132 | as: 'BlockedAccount' | ||
133 | } | ||
141 | ] | 134 | ] |
142 | }) | 135 | } |
143 | } | ||
144 | 136 | ||
145 | Object.assign(query, { where }) | 137 | return query |
138 | } | ||
146 | 139 | ||
147 | return AccountBlocklistModel | 140 | return Promise.all([ |
148 | .scope([ ScopeNames.WITH_ACCOUNTS ]) | 141 | AccountBlocklistModel.count(getQuery(true)), |
149 | .findAndCountAll<MAccountBlocklistAccounts>(query) | 142 | AccountBlocklistModel.findAll(getQuery(false)) |
150 | .then(({ rows, count }) => { | 143 | ]).then(([ total, data ]) => ({ total, data })) |
151 | return { total: count, data: rows } | ||
152 | }) | ||
153 | } | 144 | } |
154 | 145 | ||
155 | static listHandlesBlockedBy (accountIds: number[]): Promise<string[]> { | 146 | static listHandlesBlockedBy (accountIds: number[]): Promise<string[]> { |
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts index e89d31adf..7303651eb 100644 --- a/server/models/account/account-video-rate.ts +++ b/server/models/account/account-video-rate.ts | |||
@@ -121,29 +121,40 @@ export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountV | |||
121 | type?: string | 121 | type?: string |
122 | accountId: number | 122 | accountId: number |
123 | }) { | 123 | }) { |
124 | const query: FindOptions = { | 124 | const getQuery = (forCount: boolean) => { |
125 | offset: options.start, | 125 | const query: FindOptions = { |
126 | limit: options.count, | 126 | offset: options.start, |
127 | order: getSort(options.sort), | 127 | limit: options.count, |
128 | where: { | 128 | order: getSort(options.sort), |
129 | accountId: options.accountId | 129 | where: { |
130 | }, | 130 | accountId: options.accountId |
131 | include: [ | ||
132 | { | ||
133 | model: VideoModel, | ||
134 | required: true, | ||
135 | include: [ | ||
136 | { | ||
137 | model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), | ||
138 | required: true | ||
139 | } | ||
140 | ] | ||
141 | } | 131 | } |
142 | ] | 132 | } |
133 | |||
134 | if (options.type) query.where['type'] = options.type | ||
135 | |||
136 | if (forCount !== true) { | ||
137 | query.include = [ | ||
138 | { | ||
139 | model: VideoModel, | ||
140 | required: true, | ||
141 | include: [ | ||
142 | { | ||
143 | model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), | ||
144 | required: true | ||
145 | } | ||
146 | ] | ||
147 | } | ||
148 | ] | ||
149 | } | ||
150 | |||
151 | return query | ||
143 | } | 152 | } |
144 | if (options.type) query.where['type'] = options.type | ||
145 | 153 | ||
146 | return AccountVideoRateModel.findAndCountAll(query) | 154 | return Promise.all([ |
155 | AccountVideoRateModel.count(getQuery(true)), | ||
156 | AccountVideoRateModel.findAll(getQuery(false)) | ||
157 | ]).then(([ total, data ]) => ({ total, data })) | ||
147 | } | 158 | } |
148 | 159 | ||
149 | static listRemoteRateUrlsOfLocalVideos () { | 160 | static listRemoteRateUrlsOfLocalVideos () { |
@@ -232,7 +243,10 @@ export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountV | |||
232 | ] | 243 | ] |
233 | } | 244 | } |
234 | 245 | ||
235 | return AccountVideoRateModel.findAndCountAll<MAccountVideoRateAccountUrl>(query) | 246 | return Promise.all([ |
247 | AccountVideoRateModel.count(query), | ||
248 | AccountVideoRateModel.findAll<MAccountVideoRateAccountUrl>(query) | ||
249 | ]).then(([ total, data ]) => ({ total, data })) | ||
236 | } | 250 | } |
237 | 251 | ||
238 | static cleanOldRatesOf (videoId: number, type: VideoRateType, beforeUpdatedAt: Date) { | 252 | static cleanOldRatesOf (videoId: number, type: VideoRateType, beforeUpdatedAt: Date) { |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 619a598dd..8a7dfba94 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -54,6 +54,7 @@ export type SummaryOptions = { | |||
54 | whereActor?: WhereOptions | 54 | whereActor?: WhereOptions |
55 | whereServer?: WhereOptions | 55 | whereServer?: WhereOptions |
56 | withAccountBlockerIds?: number[] | 56 | withAccountBlockerIds?: number[] |
57 | forCount?: boolean | ||
57 | } | 58 | } |
58 | 59 | ||
59 | @DefaultScope(() => ({ | 60 | @DefaultScope(() => ({ |
@@ -73,22 +74,24 @@ export type SummaryOptions = { | |||
73 | where: options.whereServer | 74 | where: options.whereServer |
74 | } | 75 | } |
75 | 76 | ||
76 | const queryInclude: Includeable[] = [ | 77 | const actorInclude: Includeable = { |
77 | { | 78 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId' ], |
78 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], | 79 | model: ActorModel.unscoped(), |
79 | model: ActorModel.unscoped(), | 80 | required: options.actorRequired ?? true, |
80 | required: options.actorRequired ?? true, | 81 | where: options.whereActor, |
81 | where: options.whereActor, | 82 | include: [ serverInclude ] |
82 | include: [ | 83 | } |
83 | serverInclude, | ||
84 | 84 | ||
85 | { | 85 | if (options.forCount !== true) { |
86 | model: ActorImageModel.unscoped(), | 86 | actorInclude.include.push({ |
87 | as: 'Avatar', | 87 | model: ActorImageModel, |
88 | required: false | 88 | as: 'Avatars', |
89 | } | 89 | required: false |
90 | ] | 90 | }) |
91 | } | 91 | } |
92 | |||
93 | const queryInclude: Includeable[] = [ | ||
94 | actorInclude | ||
92 | ] | 95 | ] |
93 | 96 | ||
94 | const query: FindOptions = { | 97 | const query: FindOptions = { |
@@ -349,13 +352,10 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { | |||
349 | order: getSort(sort) | 352 | order: getSort(sort) |
350 | } | 353 | } |
351 | 354 | ||
352 | return AccountModel.findAndCountAll(query) | 355 | return Promise.all([ |
353 | .then(({ rows, count }) => { | 356 | AccountModel.count(), |
354 | return { | 357 | AccountModel.findAll(query) |
355 | data: rows, | 358 | ]).then(([ total, data ]) => ({ total, data })) |
356 | total: count | ||
357 | } | ||
358 | }) | ||
359 | } | 359 | } |
360 | 360 | ||
361 | static loadAccountIdFromVideo (videoId: number): Promise<MAccount> { | 361 | static loadAccountIdFromVideo (videoId: number): Promise<MAccount> { |
@@ -407,16 +407,15 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { | |||
407 | } | 407 | } |
408 | 408 | ||
409 | toFormattedJSON (this: MAccountFormattable): Account { | 409 | toFormattedJSON (this: MAccountFormattable): Account { |
410 | const actor = this.Actor.toFormattedJSON() | 410 | return { |
411 | const account = { | 411 | ...this.Actor.toFormattedJSON(), |
412 | |||
412 | id: this.id, | 413 | id: this.id, |
413 | displayName: this.getDisplayName(), | 414 | displayName: this.getDisplayName(), |
414 | description: this.description, | 415 | description: this.description, |
415 | updatedAt: this.updatedAt, | 416 | updatedAt: this.updatedAt, |
416 | userId: this.userId ? this.userId : undefined | 417 | userId: this.userId ?? undefined |
417 | } | 418 | } |
418 | |||
419 | return Object.assign(actor, account) | ||
420 | } | 419 | } |
421 | 420 | ||
422 | toFormattedSummaryJSON (this: MAccountSummaryFormattable): AccountSummary { | 421 | toFormattedSummaryJSON (this: MAccountSummaryFormattable): AccountSummary { |
@@ -424,10 +423,14 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { | |||
424 | 423 | ||
425 | return { | 424 | return { |
426 | id: this.id, | 425 | id: this.id, |
427 | name: actor.name, | ||
428 | displayName: this.getDisplayName(), | 426 | displayName: this.getDisplayName(), |
427 | |||
428 | name: actor.name, | ||
429 | url: actor.url, | 429 | url: actor.url, |
430 | host: actor.host, | 430 | host: actor.host, |
431 | avatars: actor.avatars, | ||
432 | |||
433 | // TODO: remove, deprecated in 4.2 | ||
431 | avatar: actor.avatar | 434 | avatar: actor.avatar |
432 | } | 435 | } |
433 | } | 436 | } |
diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts index 006282530..0f4d3c0a6 100644 --- a/server/models/actor/actor-follow.ts +++ b/server/models/actor/actor-follow.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { difference, values } from 'lodash' | 1 | import { difference, values } from 'lodash' |
2 | import { IncludeOptions, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize' | 2 | import { Includeable, IncludeOptions, literal, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize' |
3 | import { | 3 | import { |
4 | AfterCreate, | 4 | AfterCreate, |
5 | AfterDestroy, | 5 | AfterDestroy, |
@@ -30,12 +30,12 @@ import { | |||
30 | MActorFollowFormattable, | 30 | MActorFollowFormattable, |
31 | MActorFollowSubscriptions | 31 | MActorFollowSubscriptions |
32 | } from '@server/types/models' | 32 | } from '@server/types/models' |
33 | import { AttributesOnly } from '@shared/typescript-utils' | ||
34 | import { ActivityPubActorType } from '@shared/models' | 33 | import { ActivityPubActorType } from '@shared/models' |
34 | import { AttributesOnly } from '@shared/typescript-utils' | ||
35 | import { FollowState } from '../../../shared/models/actors' | 35 | import { FollowState } from '../../../shared/models/actors' |
36 | import { ActorFollow } from '../../../shared/models/actors/follow.model' | 36 | import { ActorFollow } from '../../../shared/models/actors/follow.model' |
37 | import { logger } from '../../helpers/logger' | 37 | import { logger } from '../../helpers/logger' |
38 | import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' | 38 | import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME, SORTABLE_COLUMNS } from '../../initializers/constants' |
39 | import { AccountModel } from '../account/account' | 39 | import { AccountModel } from '../account/account' |
40 | import { ServerModel } from '../server/server' | 40 | import { ServerModel } from '../server/server' |
41 | import { doesExist } from '../shared/query' | 41 | import { doesExist } from '../shared/query' |
@@ -375,43 +375,46 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
375 | Object.assign(followingWhere, { type: actorType }) | 375 | Object.assign(followingWhere, { type: actorType }) |
376 | } | 376 | } |
377 | 377 | ||
378 | const query = { | 378 | const getQuery = (forCount: boolean) => { |
379 | distinct: true, | 379 | const actorModel = forCount |
380 | offset: start, | 380 | ? ActorModel.unscoped() |
381 | limit: count, | 381 | : ActorModel |
382 | order: getFollowsSort(sort), | 382 | |
383 | where: followWhere, | 383 | return { |
384 | include: [ | 384 | distinct: true, |
385 | { | 385 | offset: start, |
386 | model: ActorModel, | 386 | limit: count, |
387 | required: true, | 387 | order: getFollowsSort(sort), |
388 | as: 'ActorFollower', | 388 | where: followWhere, |
389 | where: { | 389 | include: [ |
390 | id | 390 | { |
391 | } | 391 | model: actorModel, |
392 | }, | 392 | required: true, |
393 | { | 393 | as: 'ActorFollower', |
394 | model: ActorModel, | 394 | where: { |
395 | as: 'ActorFollowing', | 395 | id |
396 | required: true, | ||
397 | where: followingWhere, | ||
398 | include: [ | ||
399 | { | ||
400 | model: ServerModel, | ||
401 | required: true | ||
402 | } | 396 | } |
403 | ] | 397 | }, |
404 | } | 398 | { |
405 | ] | 399 | model: actorModel, |
400 | as: 'ActorFollowing', | ||
401 | required: true, | ||
402 | where: followingWhere, | ||
403 | include: [ | ||
404 | { | ||
405 | model: ServerModel, | ||
406 | required: true | ||
407 | } | ||
408 | ] | ||
409 | } | ||
410 | ] | ||
411 | } | ||
406 | } | 412 | } |
407 | 413 | ||
408 | return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query) | 414 | return Promise.all([ |
409 | .then(({ rows, count }) => { | 415 | ActorFollowModel.count(getQuery(true)), |
410 | return { | 416 | ActorFollowModel.findAll<MActorFollowActorsDefault>(getQuery(false)) |
411 | data: rows, | 417 | ]).then(([ total, data ]) => ({ total, data })) |
412 | total: count | ||
413 | } | ||
414 | }) | ||
415 | } | 418 | } |
416 | 419 | ||
417 | static listFollowersForApi (options: { | 420 | static listFollowersForApi (options: { |
@@ -429,11 +432,17 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
429 | const followerWhere: WhereOptions = {} | 432 | const followerWhere: WhereOptions = {} |
430 | 433 | ||
431 | if (search) { | 434 | if (search) { |
432 | Object.assign(followWhere, { | 435 | const escapedSearch = ActorFollowModel.sequelize.escape('%' + search + '%') |
433 | [Op.or]: [ | 436 | |
434 | searchAttribute(search, '$ActorFollower.preferredUsername$'), | 437 | Object.assign(followerWhere, { |
435 | searchAttribute(search, '$ActorFollower.Server.host$') | 438 | id: { |
436 | ] | 439 | [Op.in]: literal( |
440 | `(` + | ||
441 | `SELECT "actor".id FROM actor LEFT JOIN server on server.id = actor."serverId" ` + | ||
442 | `WHERE "preferredUsername" ILIKE ${escapedSearch} OR "host" ILIKE ${escapedSearch}` + | ||
443 | `)` | ||
444 | ) | ||
445 | } | ||
437 | }) | 446 | }) |
438 | } | 447 | } |
439 | 448 | ||
@@ -441,39 +450,43 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
441 | Object.assign(followerWhere, { type: actorType }) | 450 | Object.assign(followerWhere, { type: actorType }) |
442 | } | 451 | } |
443 | 452 | ||
444 | const query = { | 453 | const getQuery = (forCount: boolean) => { |
445 | distinct: true, | 454 | const actorModel = forCount |
446 | offset: start, | 455 | ? ActorModel.unscoped() |
447 | limit: count, | 456 | : ActorModel |
448 | order: getFollowsSort(sort), | 457 | |
449 | where: followWhere, | 458 | return { |
450 | include: [ | 459 | distinct: true, |
451 | { | 460 | |
452 | model: ActorModel, | 461 | offset: start, |
453 | required: true, | 462 | limit: count, |
454 | as: 'ActorFollower', | 463 | order: getFollowsSort(sort), |
455 | where: followerWhere | 464 | where: followWhere, |
456 | }, | 465 | include: [ |
457 | { | 466 | { |
458 | model: ActorModel, | 467 | model: actorModel, |
459 | as: 'ActorFollowing', | 468 | required: true, |
460 | required: true, | 469 | as: 'ActorFollower', |
461 | where: { | 470 | where: followerWhere |
462 | id: { | 471 | }, |
463 | [Op.in]: actorIds | 472 | { |
473 | model: actorModel, | ||
474 | as: 'ActorFollowing', | ||
475 | required: true, | ||
476 | where: { | ||
477 | id: { | ||
478 | [Op.in]: actorIds | ||
479 | } | ||
464 | } | 480 | } |
465 | } | 481 | } |
466 | } | 482 | ] |
467 | ] | 483 | } |
468 | } | 484 | } |
469 | 485 | ||
470 | return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query) | 486 | return Promise.all([ |
471 | .then(({ rows, count }) => { | 487 | ActorFollowModel.count(getQuery(true)), |
472 | return { | 488 | ActorFollowModel.findAll<MActorFollowActorsDefault>(getQuery(false)) |
473 | data: rows, | 489 | ]).then(([ total, data ]) => ({ total, data })) |
474 | total: count | ||
475 | } | ||
476 | }) | ||
477 | } | 490 | } |
478 | 491 | ||
479 | static listSubscriptionsForApi (options: { | 492 | static listSubscriptionsForApi (options: { |
@@ -497,58 +510,68 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
497 | }) | 510 | }) |
498 | } | 511 | } |
499 | 512 | ||
500 | const query = { | 513 | const getQuery = (forCount: boolean) => { |
501 | attributes: [], | 514 | let channelInclude: Includeable[] = [] |
502 | distinct: true, | 515 | |
503 | offset: start, | 516 | if (forCount !== true) { |
504 | limit: count, | 517 | channelInclude = [ |
505 | order: getSort(sort), | 518 | { |
506 | where, | 519 | attributes: { |
507 | include: [ | 520 | exclude: unusedActorAttributesForAPI |
508 | { | 521 | }, |
509 | attributes: [ 'id' ], | 522 | model: ActorModel, |
510 | model: ActorModel.unscoped(), | 523 | required: true |
511 | as: 'ActorFollowing', | 524 | }, |
512 | required: true, | 525 | { |
513 | include: [ | 526 | model: AccountModel.unscoped(), |
514 | { | 527 | required: true, |
515 | model: VideoChannelModel.unscoped(), | 528 | include: [ |
516 | required: true, | 529 | { |
517 | include: [ | 530 | attributes: { |
518 | { | 531 | exclude: unusedActorAttributesForAPI |
519 | attributes: { | ||
520 | exclude: unusedActorAttributesForAPI | ||
521 | }, | ||
522 | model: ActorModel, | ||
523 | required: true | ||
524 | }, | 532 | }, |
525 | { | 533 | model: ActorModel, |
526 | model: AccountModel.unscoped(), | 534 | required: true |
527 | required: true, | 535 | } |
528 | include: [ | 536 | ] |
529 | { | 537 | } |
530 | attributes: { | 538 | ] |
531 | exclude: unusedActorAttributesForAPI | 539 | } |
532 | }, | 540 | |
533 | model: ActorModel, | 541 | return { |
534 | required: true | 542 | attributes: forCount === true |
535 | } | 543 | ? [] |
536 | ] | 544 | : SORTABLE_COLUMNS.USER_SUBSCRIPTIONS, |
537 | } | 545 | distinct: true, |
538 | ] | 546 | offset: start, |
539 | } | 547 | limit: count, |
540 | ] | 548 | order: getSort(sort), |
541 | } | 549 | where, |
542 | ] | 550 | include: [ |
551 | { | ||
552 | attributes: [ 'id' ], | ||
553 | model: ActorModel.unscoped(), | ||
554 | as: 'ActorFollowing', | ||
555 | required: true, | ||
556 | include: [ | ||
557 | { | ||
558 | model: VideoChannelModel.unscoped(), | ||
559 | required: true, | ||
560 | include: channelInclude | ||
561 | } | ||
562 | ] | ||
563 | } | ||
564 | ] | ||
565 | } | ||
543 | } | 566 | } |
544 | 567 | ||
545 | return ActorFollowModel.findAndCountAll<MActorFollowSubscriptions>(query) | 568 | return Promise.all([ |
546 | .then(({ rows, count }) => { | 569 | ActorFollowModel.count(getQuery(true)), |
547 | return { | 570 | ActorFollowModel.findAll<MActorFollowSubscriptions>(getQuery(false)) |
548 | data: rows.map(r => r.ActorFollowing.VideoChannel), | 571 | ]).then(([ total, rows ]) => ({ |
549 | total: count | 572 | total, |
550 | } | 573 | data: rows.map(r => r.ActorFollowing.VideoChannel) |
551 | }) | 574 | })) |
552 | } | 575 | } |
553 | 576 | ||
554 | static async keepUnfollowedInstance (hosts: string[]) { | 577 | static async keepUnfollowedInstance (hosts: string[]) { |
diff --git a/server/models/actor/actor-image.ts b/server/models/actor/actor-image.ts index 8edff5ab4..f74ab735e 100644 --- a/server/models/actor/actor-image.ts +++ b/server/models/actor/actor-image.ts | |||
@@ -1,15 +1,29 @@ | |||
1 | import { remove } from 'fs-extra' | 1 | import { remove } from 'fs-extra' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 3 | import { |
4 | import { MActorImageFormattable } from '@server/types/models' | 4 | AfterDestroy, |
5 | AllowNull, | ||
6 | BelongsTo, | ||
7 | Column, | ||
8 | CreatedAt, | ||
9 | Default, | ||
10 | ForeignKey, | ||
11 | Is, | ||
12 | Model, | ||
13 | Table, | ||
14 | UpdatedAt | ||
15 | } from 'sequelize-typescript' | ||
16 | import { MActorImage, MActorImageFormattable } from '@server/types/models' | ||
17 | import { getLowercaseExtension } from '@shared/core-utils' | ||
18 | import { ActivityIconObject, ActorImageType } from '@shared/models' | ||
5 | import { AttributesOnly } from '@shared/typescript-utils' | 19 | import { AttributesOnly } from '@shared/typescript-utils' |
6 | import { ActorImageType } from '@shared/models' | ||
7 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' | 20 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' |
8 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 21 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
9 | import { logger } from '../../helpers/logger' | 22 | import { logger } from '../../helpers/logger' |
10 | import { CONFIG } from '../../initializers/config' | 23 | import { CONFIG } from '../../initializers/config' |
11 | import { LAZY_STATIC_PATHS } from '../../initializers/constants' | 24 | import { LAZY_STATIC_PATHS, MIMETYPES, WEBSERVER } from '../../initializers/constants' |
12 | import { throwIfNotValid } from '../utils' | 25 | import { throwIfNotValid } from '../utils' |
26 | import { ActorModel } from './actor' | ||
13 | 27 | ||
14 | @Table({ | 28 | @Table({ |
15 | tableName: 'actorImage', | 29 | tableName: 'actorImage', |
@@ -17,6 +31,10 @@ import { throwIfNotValid } from '../utils' | |||
17 | { | 31 | { |
18 | fields: [ 'filename' ], | 32 | fields: [ 'filename' ], |
19 | unique: true | 33 | unique: true |
34 | }, | ||
35 | { | ||
36 | fields: [ 'actorId', 'type', 'width' ], | ||
37 | unique: true | ||
20 | } | 38 | } |
21 | ] | 39 | ] |
22 | }) | 40 | }) |
@@ -55,6 +73,18 @@ export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageMode | |||
55 | @UpdatedAt | 73 | @UpdatedAt |
56 | updatedAt: Date | 74 | updatedAt: Date |
57 | 75 | ||
76 | @ForeignKey(() => ActorModel) | ||
77 | @Column | ||
78 | actorId: number | ||
79 | |||
80 | @BelongsTo(() => ActorModel, { | ||
81 | foreignKey: { | ||
82 | allowNull: false | ||
83 | }, | ||
84 | onDelete: 'CASCADE' | ||
85 | }) | ||
86 | Actor: ActorModel | ||
87 | |||
58 | @AfterDestroy | 88 | @AfterDestroy |
59 | static removeFilesAndSendDelete (instance: ActorImageModel) { | 89 | static removeFilesAndSendDelete (instance: ActorImageModel) { |
60 | logger.info('Removing actor image file %s.', instance.filename) | 90 | logger.info('Removing actor image file %s.', instance.filename) |
@@ -74,20 +104,41 @@ export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageMode | |||
74 | return ActorImageModel.findOne(query) | 104 | return ActorImageModel.findOne(query) |
75 | } | 105 | } |
76 | 106 | ||
107 | static getImageUrl (image: MActorImage) { | ||
108 | if (!image) return undefined | ||
109 | |||
110 | return WEBSERVER.URL + image.getStaticPath() | ||
111 | } | ||
112 | |||
77 | toFormattedJSON (this: MActorImageFormattable): ActorImage { | 113 | toFormattedJSON (this: MActorImageFormattable): ActorImage { |
78 | return { | 114 | return { |
115 | width: this.width, | ||
79 | path: this.getStaticPath(), | 116 | path: this.getStaticPath(), |
80 | createdAt: this.createdAt, | 117 | createdAt: this.createdAt, |
81 | updatedAt: this.updatedAt | 118 | updatedAt: this.updatedAt |
82 | } | 119 | } |
83 | } | 120 | } |
84 | 121 | ||
85 | getStaticPath () { | 122 | toActivityPubObject (): ActivityIconObject { |
86 | if (this.type === ActorImageType.AVATAR) { | 123 | const extension = getLowercaseExtension(this.filename) |
87 | return join(LAZY_STATIC_PATHS.AVATARS, this.filename) | 124 | |
125 | return { | ||
126 | type: 'Image', | ||
127 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], | ||
128 | height: this.height, | ||
129 | width: this.width, | ||
130 | url: ActorImageModel.getImageUrl(this) | ||
88 | } | 131 | } |
132 | } | ||
89 | 133 | ||
90 | return join(LAZY_STATIC_PATHS.BANNERS, this.filename) | 134 | getStaticPath () { |
135 | switch (this.type) { | ||
136 | case ActorImageType.AVATAR: | ||
137 | return join(LAZY_STATIC_PATHS.AVATARS, this.filename) | ||
138 | |||
139 | case ActorImageType.BANNER: | ||
140 | return join(LAZY_STATIC_PATHS.BANNERS, this.filename) | ||
141 | } | ||
91 | } | 142 | } |
92 | 143 | ||
93 | getPath () { | 144 | getPath () { |
diff --git a/server/models/actor/actor.ts b/server/models/actor/actor.ts index c12dcf634..08cb2fd24 100644 --- a/server/models/actor/actor.ts +++ b/server/models/actor/actor.ts | |||
@@ -16,11 +16,11 @@ import { | |||
16 | Table, | 16 | Table, |
17 | UpdatedAt | 17 | UpdatedAt |
18 | } from 'sequelize-typescript' | 18 | } from 'sequelize-typescript' |
19 | import { getBiggestActorImage } from '@server/lib/actor-image' | ||
19 | import { ModelCache } from '@server/models/model-cache' | 20 | import { ModelCache } from '@server/models/model-cache' |
20 | import { getLowercaseExtension } from '@shared/core-utils' | 21 | import { getLowercaseExtension } from '@shared/core-utils' |
22 | import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models' | ||
21 | import { AttributesOnly } from '@shared/typescript-utils' | 23 | import { AttributesOnly } from '@shared/typescript-utils' |
22 | import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' | ||
23 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' | ||
24 | import { activityPubContextify } from '../../helpers/activitypub' | 24 | import { activityPubContextify } from '../../helpers/activitypub' |
25 | import { | 25 | import { |
26 | isActorFollowersCountValid, | 26 | isActorFollowersCountValid, |
@@ -81,7 +81,7 @@ export const unusedActorAttributesForAPI = [ | |||
81 | }, | 81 | }, |
82 | { | 82 | { |
83 | model: ActorImageModel, | 83 | model: ActorImageModel, |
84 | as: 'Avatar', | 84 | as: 'Avatars', |
85 | required: false | 85 | required: false |
86 | } | 86 | } |
87 | ] | 87 | ] |
@@ -109,12 +109,12 @@ export const unusedActorAttributesForAPI = [ | |||
109 | }, | 109 | }, |
110 | { | 110 | { |
111 | model: ActorImageModel, | 111 | model: ActorImageModel, |
112 | as: 'Avatar', | 112 | as: 'Avatars', |
113 | required: false | 113 | required: false |
114 | }, | 114 | }, |
115 | { | 115 | { |
116 | model: ActorImageModel, | 116 | model: ActorImageModel, |
117 | as: 'Banner', | 117 | as: 'Banners', |
118 | required: false | 118 | required: false |
119 | } | 119 | } |
120 | ] | 120 | ] |
@@ -153,9 +153,6 @@ export const unusedActorAttributesForAPI = [ | |||
153 | fields: [ 'serverId' ] | 153 | fields: [ 'serverId' ] |
154 | }, | 154 | }, |
155 | { | 155 | { |
156 | fields: [ 'avatarId' ] | ||
157 | }, | ||
158 | { | ||
159 | fields: [ 'followersUrl' ] | 156 | fields: [ 'followersUrl' ] |
160 | } | 157 | } |
161 | ] | 158 | ] |
@@ -231,35 +228,31 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
231 | @UpdatedAt | 228 | @UpdatedAt |
232 | updatedAt: Date | 229 | updatedAt: Date |
233 | 230 | ||
234 | @ForeignKey(() => ActorImageModel) | 231 | @HasMany(() => ActorImageModel, { |
235 | @Column | 232 | as: 'Avatars', |
236 | avatarId: number | 233 | onDelete: 'cascade', |
237 | 234 | hooks: true, | |
238 | @ForeignKey(() => ActorImageModel) | ||
239 | @Column | ||
240 | bannerId: number | ||
241 | |||
242 | @BelongsTo(() => ActorImageModel, { | ||
243 | foreignKey: { | 235 | foreignKey: { |
244 | name: 'avatarId', | 236 | allowNull: false |
245 | allowNull: true | ||
246 | }, | 237 | }, |
247 | as: 'Avatar', | 238 | scope: { |
248 | onDelete: 'set null', | 239 | type: ActorImageType.AVATAR |
249 | hooks: true | 240 | } |
250 | }) | 241 | }) |
251 | Avatar: ActorImageModel | 242 | Avatars: ActorImageModel[] |
252 | 243 | ||
253 | @BelongsTo(() => ActorImageModel, { | 244 | @HasMany(() => ActorImageModel, { |
245 | as: 'Banners', | ||
246 | onDelete: 'cascade', | ||
247 | hooks: true, | ||
254 | foreignKey: { | 248 | foreignKey: { |
255 | name: 'bannerId', | 249 | allowNull: false |
256 | allowNull: true | ||
257 | }, | 250 | }, |
258 | as: 'Banner', | 251 | scope: { |
259 | onDelete: 'set null', | 252 | type: ActorImageType.BANNER |
260 | hooks: true | 253 | } |
261 | }) | 254 | }) |
262 | Banner: ActorImageModel | 255 | Banners: ActorImageModel[] |
263 | 256 | ||
264 | @HasMany(() => ActorFollowModel, { | 257 | @HasMany(() => ActorFollowModel, { |
265 | foreignKey: { | 258 | foreignKey: { |
@@ -386,8 +379,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
386 | transaction | 379 | transaction |
387 | } | 380 | } |
388 | 381 | ||
389 | return ActorModel.scope(ScopeNames.FULL) | 382 | return ActorModel.scope(ScopeNames.FULL).findOne(query) |
390 | .findOne(query) | ||
391 | } | 383 | } |
392 | 384 | ||
393 | return ModelCache.Instance.doCache({ | 385 | return ModelCache.Instance.doCache({ |
@@ -410,8 +402,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
410 | transaction | 402 | transaction |
411 | } | 403 | } |
412 | 404 | ||
413 | return ActorModel.unscoped() | 405 | return ActorModel.unscoped().findOne(query) |
414 | .findOne(query) | ||
415 | } | 406 | } |
416 | 407 | ||
417 | return ModelCache.Instance.doCache({ | 408 | return ModelCache.Instance.doCache({ |
@@ -532,55 +523,50 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
532 | } | 523 | } |
533 | 524 | ||
534 | toFormattedSummaryJSON (this: MActorSummaryFormattable) { | 525 | toFormattedSummaryJSON (this: MActorSummaryFormattable) { |
535 | let avatar: ActorImage = null | ||
536 | if (this.Avatar) { | ||
537 | avatar = this.Avatar.toFormattedJSON() | ||
538 | } | ||
539 | |||
540 | return { | 526 | return { |
541 | url: this.url, | 527 | url: this.url, |
542 | name: this.preferredUsername, | 528 | name: this.preferredUsername, |
543 | host: this.getHost(), | 529 | host: this.getHost(), |
544 | avatar | 530 | avatars: (this.Avatars || []).map(a => a.toFormattedJSON()), |
531 | |||
532 | // TODO: remove, deprecated in 4.2 | ||
533 | avatar: this.hasImage(ActorImageType.AVATAR) | ||
534 | ? this.Avatars[0].toFormattedJSON() | ||
535 | : undefined | ||
545 | } | 536 | } |
546 | } | 537 | } |
547 | 538 | ||
548 | toFormattedJSON (this: MActorFormattable) { | 539 | toFormattedJSON (this: MActorFormattable) { |
549 | const base = this.toFormattedSummaryJSON() | 540 | return { |
550 | 541 | ...this.toFormattedSummaryJSON(), | |
551 | let banner: ActorImage = null | ||
552 | if (this.Banner) { | ||
553 | banner = this.Banner.toFormattedJSON() | ||
554 | } | ||
555 | 542 | ||
556 | return Object.assign(base, { | ||
557 | id: this.id, | 543 | id: this.id, |
558 | hostRedundancyAllowed: this.getRedundancyAllowed(), | 544 | hostRedundancyAllowed: this.getRedundancyAllowed(), |
559 | followingCount: this.followingCount, | 545 | followingCount: this.followingCount, |
560 | followersCount: this.followersCount, | 546 | followersCount: this.followersCount, |
561 | banner, | 547 | createdAt: this.getCreatedAt(), |
562 | createdAt: this.getCreatedAt() | 548 | |
563 | }) | 549 | banners: (this.Banners || []).map(b => b.toFormattedJSON()), |
550 | |||
551 | // TODO: remove, deprecated in 4.2 | ||
552 | banner: this.hasImage(ActorImageType.BANNER) | ||
553 | ? this.Banners[0].toFormattedJSON() | ||
554 | : undefined | ||
555 | } | ||
564 | } | 556 | } |
565 | 557 | ||
566 | toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) { | 558 | toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) { |
567 | let icon: ActivityIconObject | 559 | let icon: ActivityIconObject |
560 | let icons: ActivityIconObject[] | ||
568 | let image: ActivityIconObject | 561 | let image: ActivityIconObject |
569 | 562 | ||
570 | if (this.avatarId) { | 563 | if (this.hasImage(ActorImageType.AVATAR)) { |
571 | const extension = getLowercaseExtension(this.Avatar.filename) | 564 | icon = getBiggestActorImage(this.Avatars).toActivityPubObject() |
572 | 565 | icons = this.Avatars.map(a => a.toActivityPubObject()) | |
573 | icon = { | ||
574 | type: 'Image', | ||
575 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], | ||
576 | height: this.Avatar.height, | ||
577 | width: this.Avatar.width, | ||
578 | url: this.getAvatarUrl() | ||
579 | } | ||
580 | } | 566 | } |
581 | 567 | ||
582 | if (this.bannerId) { | 568 | if (this.hasImage(ActorImageType.BANNER)) { |
583 | const banner = (this as MActorAPChannel).Banner | 569 | const banner = getBiggestActorImage((this as MActorAPChannel).Banners) |
584 | const extension = getLowercaseExtension(banner.filename) | 570 | const extension = getLowercaseExtension(banner.filename) |
585 | 571 | ||
586 | image = { | 572 | image = { |
@@ -588,7 +574,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
588 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], | 574 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], |
589 | height: banner.height, | 575 | height: banner.height, |
590 | width: banner.width, | 576 | width: banner.width, |
591 | url: this.getBannerUrl() | 577 | url: ActorImageModel.getImageUrl(banner) |
592 | } | 578 | } |
593 | } | 579 | } |
594 | 580 | ||
@@ -612,7 +598,10 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
612 | publicKeyPem: this.publicKey | 598 | publicKeyPem: this.publicKey |
613 | }, | 599 | }, |
614 | published: this.getCreatedAt().toISOString(), | 600 | published: this.getCreatedAt().toISOString(), |
601 | |||
615 | icon, | 602 | icon, |
603 | icons, | ||
604 | |||
616 | image | 605 | image |
617 | } | 606 | } |
618 | 607 | ||
@@ -677,16 +666,12 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
677 | return this.Server ? this.Server.redundancyAllowed : false | 666 | return this.Server ? this.Server.redundancyAllowed : false |
678 | } | 667 | } |
679 | 668 | ||
680 | getAvatarUrl () { | 669 | hasImage (type: ActorImageType) { |
681 | if (!this.avatarId) return undefined | 670 | const images = type === ActorImageType.AVATAR |
682 | 671 | ? this.Avatars | |
683 | return WEBSERVER.URL + this.Avatar.getStaticPath() | 672 | : this.Banners |
684 | } | ||
685 | |||
686 | getBannerUrl () { | ||
687 | if (!this.bannerId) return undefined | ||
688 | 673 | ||
689 | return WEBSERVER.URL + this.Banner.getStaticPath() | 674 | return Array.isArray(images) && images.length !== 0 |
690 | } | 675 | } |
691 | 676 | ||
692 | isOutdated () { | 677 | isOutdated () { |
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts index 05083e3f7..fa5b4cc4b 100644 --- a/server/models/server/plugin.ts +++ b/server/models/server/plugin.ts | |||
@@ -239,11 +239,10 @@ export class PluginModel extends Model<Partial<AttributesOnly<PluginModel>>> { | |||
239 | 239 | ||
240 | if (options.pluginType) query.where['type'] = options.pluginType | 240 | if (options.pluginType) query.where['type'] = options.pluginType |
241 | 241 | ||
242 | return PluginModel | 242 | return Promise.all([ |
243 | .findAndCountAll<MPlugin>(query) | 243 | PluginModel.count(query), |
244 | .then(({ rows, count }) => { | 244 | PluginModel.findAll<MPlugin>(query) |
245 | return { total: count, data: rows } | 245 | ]).then(([ total, data ]) => ({ total, data })) |
246 | }) | ||
247 | } | 246 | } |
248 | 247 | ||
249 | static listInstalled (): Promise<MPlugin[]> { | 248 | static listInstalled (): Promise<MPlugin[]> { |
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts index 9f64eeb7f..9752dfbc3 100644 --- a/server/models/server/server-blocklist.ts +++ b/server/models/server/server-blocklist.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Op, QueryTypes } from 'sequelize' | 1 | import { Op, QueryTypes } from 'sequelize' |
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' | 3 | import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/typescript-utils' | ||
5 | import { ServerBlock } from '@shared/models' | 4 | import { ServerBlock } from '@shared/models' |
5 | import { AttributesOnly } from '@shared/typescript-utils' | ||
6 | import { AccountModel } from '../account/account' | 6 | import { AccountModel } from '../account/account' |
7 | import { createSafeIn, getSort, searchAttribute } from '../utils' | 7 | import { createSafeIn, getSort, searchAttribute } from '../utils' |
8 | import { ServerModel } from './server' | 8 | import { ServerModel } from './server' |
@@ -169,16 +169,15 @@ export class ServerBlocklistModel extends Model<Partial<AttributesOnly<ServerBlo | |||
169 | order: getSort(sort), | 169 | order: getSort(sort), |
170 | where: { | 170 | where: { |
171 | accountId, | 171 | accountId, |
172 | |||
172 | ...searchAttribute(search, '$BlockedServer.host$') | 173 | ...searchAttribute(search, '$BlockedServer.host$') |
173 | } | 174 | } |
174 | } | 175 | } |
175 | 176 | ||
176 | return ServerBlocklistModel | 177 | return Promise.all([ |
177 | .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_SERVER ]) | 178 | ServerBlocklistModel.scope(ScopeNames.WITH_SERVER).count(query), |
178 | .findAndCountAll<MServerBlocklistAccountServer>(query) | 179 | ServerBlocklistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_SERVER ]).findAll<MServerBlocklistAccountServer>(query) |
179 | .then(({ rows, count }) => { | 180 | ]).then(([ total, data ]) => ({ total, data })) |
180 | return { total: count, data: rows } | ||
181 | }) | ||
182 | } | 181 | } |
183 | 182 | ||
184 | toFormattedJSON (this: MServerBlocklistFormattable): ServerBlock { | 183 | toFormattedJSON (this: MServerBlocklistFormattable): ServerBlock { |
diff --git a/server/models/shared/index.ts b/server/models/shared/index.ts index 5b97510e0..802404555 100644 --- a/server/models/shared/index.ts +++ b/server/models/shared/index.ts | |||
@@ -1,2 +1,3 @@ | |||
1 | export * from './model-builder' | ||
1 | export * from './query' | 2 | export * from './query' |
2 | export * from './update' | 3 | export * from './update' |
diff --git a/server/models/shared/model-builder.ts b/server/models/shared/model-builder.ts new file mode 100644 index 000000000..c015ca4f5 --- /dev/null +++ b/server/models/shared/model-builder.ts | |||
@@ -0,0 +1,101 @@ | |||
1 | import { isPlainObject } from 'lodash' | ||
2 | import { Model as SequelizeModel, Sequelize } from 'sequelize' | ||
3 | import { logger } from '@server/helpers/logger' | ||
4 | |||
5 | export class ModelBuilder <T extends SequelizeModel> { | ||
6 | private readonly modelRegistry = new Map<string, T>() | ||
7 | |||
8 | constructor (private readonly sequelize: Sequelize) { | ||
9 | |||
10 | } | ||
11 | |||
12 | createModels (jsonArray: any[], baseModelName: string): T[] { | ||
13 | const result: T[] = [] | ||
14 | |||
15 | for (const json of jsonArray) { | ||
16 | const { created, model } = this.createModel(json, baseModelName, json.id + '.' + baseModelName) | ||
17 | |||
18 | if (created) result.push(model) | ||
19 | } | ||
20 | |||
21 | return result | ||
22 | } | ||
23 | |||
24 | private createModel (json: any, modelName: string, keyPath: string) { | ||
25 | if (!json.id) return { created: false, model: null } | ||
26 | |||
27 | const { created, model } = this.createOrFindModel(json, modelName, keyPath) | ||
28 | |||
29 | for (const key of Object.keys(json)) { | ||
30 | const value = json[key] | ||
31 | if (!value) continue | ||
32 | |||
33 | // Child model | ||
34 | if (isPlainObject(value)) { | ||
35 | const { created, model: subModel } = this.createModel(value, key, keyPath + '.' + json.id + '.' + key) | ||
36 | if (!created || !subModel) continue | ||
37 | |||
38 | const Model = this.findModelBuilder(modelName) | ||
39 | const association = Model.associations[key] | ||
40 | |||
41 | if (!association) { | ||
42 | logger.error('Cannot find association %s of model %s', key, modelName, { associations: Object.keys(Model.associations) }) | ||
43 | continue | ||
44 | } | ||
45 | |||
46 | if (association.isMultiAssociation) { | ||
47 | if (!Array.isArray(model[key])) model[key] = [] | ||
48 | |||
49 | model[key].push(subModel) | ||
50 | } else { | ||
51 | model[key] = subModel | ||
52 | } | ||
53 | } | ||
54 | } | ||
55 | |||
56 | return { created, model } | ||
57 | } | ||
58 | |||
59 | private createOrFindModel (json: any, modelName: string, keyPath: string) { | ||
60 | const registryKey = this.getModelRegistryKey(json, keyPath) | ||
61 | if (this.modelRegistry.has(registryKey)) { | ||
62 | return { | ||
63 | created: false, | ||
64 | model: this.modelRegistry.get(registryKey) | ||
65 | } | ||
66 | } | ||
67 | |||
68 | const Model = this.findModelBuilder(modelName) | ||
69 | |||
70 | if (!Model) { | ||
71 | logger.error( | ||
72 | 'Cannot build model %s that does not exist', this.buildSequelizeModelName(modelName), | ||
73 | { existing: this.sequelize.modelManager.all.map(m => m.name) } | ||
74 | ) | ||
75 | return undefined | ||
76 | } | ||
77 | |||
78 | // FIXME: typings | ||
79 | const model = new (Model as any)(json) | ||
80 | this.modelRegistry.set(registryKey, model) | ||
81 | |||
82 | return { created: true, model } | ||
83 | } | ||
84 | |||
85 | private findModelBuilder (modelName: string) { | ||
86 | return this.sequelize.modelManager.getModel(this.buildSequelizeModelName(modelName)) | ||
87 | } | ||
88 | |||
89 | private buildSequelizeModelName (modelName: string) { | ||
90 | if (modelName === 'Avatars') return 'ActorImageModel' | ||
91 | if (modelName === 'ActorFollowing') return 'ActorModel' | ||
92 | if (modelName === 'ActorFollower') return 'ActorModel' | ||
93 | if (modelName === 'FlaggedAccount') return 'AccountModel' | ||
94 | |||
95 | return modelName + 'Model' | ||
96 | } | ||
97 | |||
98 | private getModelRegistryKey (json: any, keyPath: string) { | ||
99 | return keyPath + json.id | ||
100 | } | ||
101 | } | ||
diff --git a/server/models/user/sql/user-notitication-list-query-builder.ts b/server/models/user/sql/user-notitication-list-query-builder.ts new file mode 100644 index 000000000..9eae4fc22 --- /dev/null +++ b/server/models/user/sql/user-notitication-list-query-builder.ts | |||
@@ -0,0 +1,269 @@ | |||
1 | import { QueryTypes, Sequelize } from 'sequelize' | ||
2 | import { ModelBuilder } from '@server/models/shared' | ||
3 | import { getSort } from '@server/models/utils' | ||
4 | import { UserNotificationModelForApi } from '@server/types/models' | ||
5 | import { ActorImageType } from '@shared/models' | ||
6 | |||
7 | export interface ListNotificationsOptions { | ||
8 | userId: number | ||
9 | unread?: boolean | ||
10 | sort: string | ||
11 | offset: number | ||
12 | limit: number | ||
13 | sequelize: Sequelize | ||
14 | } | ||
15 | |||
16 | export class UserNotificationListQueryBuilder { | ||
17 | private innerQuery: string | ||
18 | private replacements: any = {} | ||
19 | private query: string | ||
20 | |||
21 | constructor (private readonly options: ListNotificationsOptions) { | ||
22 | |||
23 | } | ||
24 | |||
25 | async listNotifications () { | ||
26 | this.buildQuery() | ||
27 | |||
28 | const results = await this.options.sequelize.query(this.query, { | ||
29 | replacements: this.replacements, | ||
30 | type: QueryTypes.SELECT, | ||
31 | nest: true | ||
32 | }) | ||
33 | |||
34 | const modelBuilder = new ModelBuilder<UserNotificationModelForApi>(this.options.sequelize) | ||
35 | |||
36 | return modelBuilder.createModels(results, 'UserNotification') | ||
37 | } | ||
38 | |||
39 | private buildInnerQuery () { | ||
40 | this.innerQuery = `SELECT * FROM "userNotification" AS "UserNotificationModel" ` + | ||
41 | `${this.getWhere()} ` + | ||
42 | `${this.getOrder()} ` + | ||
43 | `LIMIT :limit OFFSET :offset ` | ||
44 | |||
45 | this.replacements.limit = this.options.limit | ||
46 | this.replacements.offset = this.options.offset | ||
47 | } | ||
48 | |||
49 | private buildQuery () { | ||
50 | this.buildInnerQuery() | ||
51 | |||
52 | this.query = ` | ||
53 | ${this.getSelect()} | ||
54 | FROM (${this.innerQuery}) "UserNotificationModel" | ||
55 | ${this.getJoins()} | ||
56 | ${this.getOrder()}` | ||
57 | } | ||
58 | |||
59 | private getWhere () { | ||
60 | let base = '"UserNotificationModel"."userId" = :userId ' | ||
61 | this.replacements.userId = this.options.userId | ||
62 | |||
63 | if (this.options.unread === true) { | ||
64 | base += 'AND "UserNotificationModel"."read" IS FALSE ' | ||
65 | } else if (this.options.unread === false) { | ||
66 | base += 'AND "UserNotificationModel"."read" IS TRUE ' | ||
67 | } | ||
68 | |||
69 | return `WHERE ${base}` | ||
70 | } | ||
71 | |||
72 | private getOrder () { | ||
73 | const orders = getSort(this.options.sort) | ||
74 | |||
75 | return 'ORDER BY ' + orders.map(o => `"UserNotificationModel"."${o[0]}" ${o[1]}`).join(', ') | ||
76 | } | ||
77 | |||
78 | private getSelect () { | ||
79 | return `SELECT | ||
80 | "UserNotificationModel"."id", | ||
81 | "UserNotificationModel"."type", | ||
82 | "UserNotificationModel"."read", | ||
83 | "UserNotificationModel"."createdAt", | ||
84 | "UserNotificationModel"."updatedAt", | ||
85 | "Video"."id" AS "Video.id", | ||
86 | "Video"."uuid" AS "Video.uuid", | ||
87 | "Video"."name" AS "Video.name", | ||
88 | "Video->VideoChannel"."id" AS "Video.VideoChannel.id", | ||
89 | "Video->VideoChannel"."name" AS "Video.VideoChannel.name", | ||
90 | "Video->VideoChannel->Actor"."id" AS "Video.VideoChannel.Actor.id", | ||
91 | "Video->VideoChannel->Actor"."preferredUsername" AS "Video.VideoChannel.Actor.preferredUsername", | ||
92 | "Video->VideoChannel->Actor->Avatars"."id" AS "Video.VideoChannel.Actor.Avatars.id", | ||
93 | "Video->VideoChannel->Actor->Avatars"."width" AS "Video.VideoChannel.Actor.Avatars.width", | ||
94 | "Video->VideoChannel->Actor->Avatars"."filename" AS "Video.VideoChannel.Actor.Avatars.filename", | ||
95 | "Video->VideoChannel->Actor->Server"."id" AS "Video.VideoChannel.Actor.Server.id", | ||
96 | "Video->VideoChannel->Actor->Server"."host" AS "Video.VideoChannel.Actor.Server.host", | ||
97 | "VideoComment"."id" AS "VideoComment.id", | ||
98 | "VideoComment"."originCommentId" AS "VideoComment.originCommentId", | ||
99 | "VideoComment->Account"."id" AS "VideoComment.Account.id", | ||
100 | "VideoComment->Account"."name" AS "VideoComment.Account.name", | ||
101 | "VideoComment->Account->Actor"."id" AS "VideoComment.Account.Actor.id", | ||
102 | "VideoComment->Account->Actor"."preferredUsername" AS "VideoComment.Account.Actor.preferredUsername", | ||
103 | "VideoComment->Account->Actor->Avatars"."id" AS "VideoComment.Account.Actor.Avatars.id", | ||
104 | "VideoComment->Account->Actor->Avatars"."width" AS "VideoComment.Account.Actor.Avatars.width", | ||
105 | "VideoComment->Account->Actor->Avatars"."filename" AS "VideoComment.Account.Actor.Avatars.filename", | ||
106 | "VideoComment->Account->Actor->Server"."id" AS "VideoComment.Account.Actor.Server.id", | ||
107 | "VideoComment->Account->Actor->Server"."host" AS "VideoComment.Account.Actor.Server.host", | ||
108 | "VideoComment->Video"."id" AS "VideoComment.Video.id", | ||
109 | "VideoComment->Video"."uuid" AS "VideoComment.Video.uuid", | ||
110 | "VideoComment->Video"."name" AS "VideoComment.Video.name", | ||
111 | "Abuse"."id" AS "Abuse.id", | ||
112 | "Abuse"."state" AS "Abuse.state", | ||
113 | "Abuse->VideoAbuse"."id" AS "Abuse.VideoAbuse.id", | ||
114 | "Abuse->VideoAbuse->Video"."id" AS "Abuse.VideoAbuse.Video.id", | ||
115 | "Abuse->VideoAbuse->Video"."uuid" AS "Abuse.VideoAbuse.Video.uuid", | ||
116 | "Abuse->VideoAbuse->Video"."name" AS "Abuse.VideoAbuse.Video.name", | ||
117 | "Abuse->VideoCommentAbuse"."id" AS "Abuse.VideoCommentAbuse.id", | ||
118 | "Abuse->VideoCommentAbuse->VideoComment"."id" AS "Abuse.VideoCommentAbuse.VideoComment.id", | ||
119 | "Abuse->VideoCommentAbuse->VideoComment"."originCommentId" AS "Abuse.VideoCommentAbuse.VideoComment.originCommentId", | ||
120 | "Abuse->VideoCommentAbuse->VideoComment->Video"."id" AS "Abuse.VideoCommentAbuse.VideoComment.Video.id", | ||
121 | "Abuse->VideoCommentAbuse->VideoComment->Video"."name" AS "Abuse.VideoCommentAbuse.VideoComment.Video.name", | ||
122 | "Abuse->VideoCommentAbuse->VideoComment->Video"."uuid" AS "Abuse.VideoCommentAbuse.VideoComment.Video.uuid", | ||
123 | "Abuse->FlaggedAccount"."id" AS "Abuse.FlaggedAccount.id", | ||
124 | "Abuse->FlaggedAccount"."name" AS "Abuse.FlaggedAccount.name", | ||
125 | "Abuse->FlaggedAccount"."description" AS "Abuse.FlaggedAccount.description", | ||
126 | "Abuse->FlaggedAccount"."actorId" AS "Abuse.FlaggedAccount.actorId", | ||
127 | "Abuse->FlaggedAccount"."userId" AS "Abuse.FlaggedAccount.userId", | ||
128 | "Abuse->FlaggedAccount"."applicationId" AS "Abuse.FlaggedAccount.applicationId", | ||
129 | "Abuse->FlaggedAccount"."createdAt" AS "Abuse.FlaggedAccount.createdAt", | ||
130 | "Abuse->FlaggedAccount"."updatedAt" AS "Abuse.FlaggedAccount.updatedAt", | ||
131 | "Abuse->FlaggedAccount->Actor"."id" AS "Abuse.FlaggedAccount.Actor.id", | ||
132 | "Abuse->FlaggedAccount->Actor"."preferredUsername" AS "Abuse.FlaggedAccount.Actor.preferredUsername", | ||
133 | "Abuse->FlaggedAccount->Actor->Avatars"."id" AS "Abuse.FlaggedAccount.Actor.Avatars.id", | ||
134 | "Abuse->FlaggedAccount->Actor->Avatars"."width" AS "Abuse.FlaggedAccount.Actor.Avatars.width", | ||
135 | "Abuse->FlaggedAccount->Actor->Avatars"."filename" AS "Abuse.FlaggedAccount.Actor.Avatars.filename", | ||
136 | "Abuse->FlaggedAccount->Actor->Server"."id" AS "Abuse.FlaggedAccount.Actor.Server.id", | ||
137 | "Abuse->FlaggedAccount->Actor->Server"."host" AS "Abuse.FlaggedAccount.Actor.Server.host", | ||
138 | "VideoBlacklist"."id" AS "VideoBlacklist.id", | ||
139 | "VideoBlacklist->Video"."id" AS "VideoBlacklist.Video.id", | ||
140 | "VideoBlacklist->Video"."uuid" AS "VideoBlacklist.Video.uuid", | ||
141 | "VideoBlacklist->Video"."name" AS "VideoBlacklist.Video.name", | ||
142 | "VideoImport"."id" AS "VideoImport.id", | ||
143 | "VideoImport"."magnetUri" AS "VideoImport.magnetUri", | ||
144 | "VideoImport"."targetUrl" AS "VideoImport.targetUrl", | ||
145 | "VideoImport"."torrentName" AS "VideoImport.torrentName", | ||
146 | "VideoImport->Video"."id" AS "VideoImport.Video.id", | ||
147 | "VideoImport->Video"."uuid" AS "VideoImport.Video.uuid", | ||
148 | "VideoImport->Video"."name" AS "VideoImport.Video.name", | ||
149 | "Plugin"."id" AS "Plugin.id", | ||
150 | "Plugin"."name" AS "Plugin.name", | ||
151 | "Plugin"."type" AS "Plugin.type", | ||
152 | "Plugin"."latestVersion" AS "Plugin.latestVersion", | ||
153 | "Application"."id" AS "Application.id", | ||
154 | "Application"."latestPeerTubeVersion" AS "Application.latestPeerTubeVersion", | ||
155 | "ActorFollow"."id" AS "ActorFollow.id", | ||
156 | "ActorFollow"."state" AS "ActorFollow.state", | ||
157 | "ActorFollow->ActorFollower"."id" AS "ActorFollow.ActorFollower.id", | ||
158 | "ActorFollow->ActorFollower"."preferredUsername" AS "ActorFollow.ActorFollower.preferredUsername", | ||
159 | "ActorFollow->ActorFollower->Account"."id" AS "ActorFollow.ActorFollower.Account.id", | ||
160 | "ActorFollow->ActorFollower->Account"."name" AS "ActorFollow.ActorFollower.Account.name", | ||
161 | "ActorFollow->ActorFollower->Avatars"."id" AS "ActorFollow.ActorFollower.Avatars.id", | ||
162 | "ActorFollow->ActorFollower->Avatars"."width" AS "ActorFollow.ActorFollower.Avatars.width", | ||
163 | "ActorFollow->ActorFollower->Avatars"."filename" AS "ActorFollow.ActorFollower.Avatars.filename", | ||
164 | "ActorFollow->ActorFollower->Server"."id" AS "ActorFollow.ActorFollower.Server.id", | ||
165 | "ActorFollow->ActorFollower->Server"."host" AS "ActorFollow.ActorFollower.Server.host", | ||
166 | "ActorFollow->ActorFollowing"."id" AS "ActorFollow.ActorFollowing.id", | ||
167 | "ActorFollow->ActorFollowing"."preferredUsername" AS "ActorFollow.ActorFollowing.preferredUsername", | ||
168 | "ActorFollow->ActorFollowing"."type" AS "ActorFollow.ActorFollowing.type", | ||
169 | "ActorFollow->ActorFollowing->VideoChannel"."id" AS "ActorFollow.ActorFollowing.VideoChannel.id", | ||
170 | "ActorFollow->ActorFollowing->VideoChannel"."name" AS "ActorFollow.ActorFollowing.VideoChannel.name", | ||
171 | "ActorFollow->ActorFollowing->Account"."id" AS "ActorFollow.ActorFollowing.Account.id", | ||
172 | "ActorFollow->ActorFollowing->Account"."name" AS "ActorFollow.ActorFollowing.Account.name", | ||
173 | "ActorFollow->ActorFollowing->Server"."id" AS "ActorFollow.ActorFollowing.Server.id", | ||
174 | "ActorFollow->ActorFollowing->Server"."host" AS "ActorFollow.ActorFollowing.Server.host", | ||
175 | "Account"."id" AS "Account.id", | ||
176 | "Account"."name" AS "Account.name", | ||
177 | "Account->Actor"."id" AS "Account.Actor.id", | ||
178 | "Account->Actor"."preferredUsername" AS "Account.Actor.preferredUsername", | ||
179 | "Account->Actor->Avatars"."id" AS "Account.Actor.Avatars.id", | ||
180 | "Account->Actor->Avatars"."width" AS "Account.Actor.Avatars.width", | ||
181 | "Account->Actor->Avatars"."filename" AS "Account.Actor.Avatars.filename", | ||
182 | "Account->Actor->Server"."id" AS "Account.Actor.Server.id", | ||
183 | "Account->Actor->Server"."host" AS "Account.Actor.Server.host"` | ||
184 | } | ||
185 | |||
186 | private getJoins () { | ||
187 | return ` | ||
188 | LEFT JOIN ( | ||
189 | "video" AS "Video" | ||
190 | INNER JOIN "videoChannel" AS "Video->VideoChannel" ON "Video"."channelId" = "Video->VideoChannel"."id" | ||
191 | INNER JOIN "actor" AS "Video->VideoChannel->Actor" ON "Video->VideoChannel"."actorId" = "Video->VideoChannel->Actor"."id" | ||
192 | LEFT JOIN "actorImage" AS "Video->VideoChannel->Actor->Avatars" | ||
193 | ON "Video->VideoChannel->Actor"."id" = "Video->VideoChannel->Actor->Avatars"."actorId" | ||
194 | AND "Video->VideoChannel->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
195 | LEFT JOIN "server" AS "Video->VideoChannel->Actor->Server" | ||
196 | ON "Video->VideoChannel->Actor"."serverId" = "Video->VideoChannel->Actor->Server"."id" | ||
197 | ) ON "UserNotificationModel"."videoId" = "Video"."id" | ||
198 | |||
199 | LEFT JOIN ( | ||
200 | "videoComment" AS "VideoComment" | ||
201 | INNER JOIN "account" AS "VideoComment->Account" ON "VideoComment"."accountId" = "VideoComment->Account"."id" | ||
202 | INNER JOIN "actor" AS "VideoComment->Account->Actor" ON "VideoComment->Account"."actorId" = "VideoComment->Account->Actor"."id" | ||
203 | LEFT JOIN "actorImage" AS "VideoComment->Account->Actor->Avatars" | ||
204 | ON "VideoComment->Account->Actor"."id" = "VideoComment->Account->Actor->Avatars"."actorId" | ||
205 | AND "VideoComment->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
206 | LEFT JOIN "server" AS "VideoComment->Account->Actor->Server" | ||
207 | ON "VideoComment->Account->Actor"."serverId" = "VideoComment->Account->Actor->Server"."id" | ||
208 | INNER JOIN "video" AS "VideoComment->Video" ON "VideoComment"."videoId" = "VideoComment->Video"."id" | ||
209 | ) ON "UserNotificationModel"."commentId" = "VideoComment"."id" | ||
210 | |||
211 | LEFT JOIN "abuse" AS "Abuse" ON "UserNotificationModel"."abuseId" = "Abuse"."id" | ||
212 | LEFT JOIN "videoAbuse" AS "Abuse->VideoAbuse" ON "Abuse"."id" = "Abuse->VideoAbuse"."abuseId" | ||
213 | LEFT JOIN "video" AS "Abuse->VideoAbuse->Video" ON "Abuse->VideoAbuse"."videoId" = "Abuse->VideoAbuse->Video"."id" | ||
214 | LEFT JOIN "commentAbuse" AS "Abuse->VideoCommentAbuse" ON "Abuse"."id" = "Abuse->VideoCommentAbuse"."abuseId" | ||
215 | LEFT JOIN "videoComment" AS "Abuse->VideoCommentAbuse->VideoComment" | ||
216 | ON "Abuse->VideoCommentAbuse"."videoCommentId" = "Abuse->VideoCommentAbuse->VideoComment"."id" | ||
217 | LEFT JOIN "video" AS "Abuse->VideoCommentAbuse->VideoComment->Video" | ||
218 | ON "Abuse->VideoCommentAbuse->VideoComment"."videoId" = "Abuse->VideoCommentAbuse->VideoComment->Video"."id" | ||
219 | LEFT JOIN ( | ||
220 | "account" AS "Abuse->FlaggedAccount" | ||
221 | INNER JOIN "actor" AS "Abuse->FlaggedAccount->Actor" ON "Abuse->FlaggedAccount"."actorId" = "Abuse->FlaggedAccount->Actor"."id" | ||
222 | LEFT JOIN "actorImage" AS "Abuse->FlaggedAccount->Actor->Avatars" | ||
223 | ON "Abuse->FlaggedAccount->Actor"."id" = "Abuse->FlaggedAccount->Actor->Avatars"."actorId" | ||
224 | AND "Abuse->FlaggedAccount->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
225 | LEFT JOIN "server" AS "Abuse->FlaggedAccount->Actor->Server" | ||
226 | ON "Abuse->FlaggedAccount->Actor"."serverId" = "Abuse->FlaggedAccount->Actor->Server"."id" | ||
227 | ) ON "Abuse"."flaggedAccountId" = "Abuse->FlaggedAccount"."id" | ||
228 | |||
229 | LEFT JOIN ( | ||
230 | "videoBlacklist" AS "VideoBlacklist" | ||
231 | INNER JOIN "video" AS "VideoBlacklist->Video" ON "VideoBlacklist"."videoId" = "VideoBlacklist->Video"."id" | ||
232 | ) ON "UserNotificationModel"."videoBlacklistId" = "VideoBlacklist"."id" | ||
233 | |||
234 | LEFT JOIN "videoImport" AS "VideoImport" ON "UserNotificationModel"."videoImportId" = "VideoImport"."id" | ||
235 | LEFT JOIN "video" AS "VideoImport->Video" ON "VideoImport"."videoId" = "VideoImport->Video"."id" | ||
236 | |||
237 | LEFT JOIN "plugin" AS "Plugin" ON "UserNotificationModel"."pluginId" = "Plugin"."id" | ||
238 | |||
239 | LEFT JOIN "application" AS "Application" ON "UserNotificationModel"."applicationId" = "Application"."id" | ||
240 | |||
241 | LEFT JOIN ( | ||
242 | "actorFollow" AS "ActorFollow" | ||
243 | INNER JOIN "actor" AS "ActorFollow->ActorFollower" ON "ActorFollow"."actorId" = "ActorFollow->ActorFollower"."id" | ||
244 | INNER JOIN "account" AS "ActorFollow->ActorFollower->Account" | ||
245 | ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Account"."actorId" | ||
246 | LEFT JOIN "actorImage" AS "ActorFollow->ActorFollower->Avatars" | ||
247 | ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Avatars"."actorId" | ||
248 | AND "ActorFollow->ActorFollower->Avatars"."type" = ${ActorImageType.AVATAR} | ||
249 | LEFT JOIN "server" AS "ActorFollow->ActorFollower->Server" | ||
250 | ON "ActorFollow->ActorFollower"."serverId" = "ActorFollow->ActorFollower->Server"."id" | ||
251 | INNER JOIN "actor" AS "ActorFollow->ActorFollowing" ON "ActorFollow"."targetActorId" = "ActorFollow->ActorFollowing"."id" | ||
252 | LEFT JOIN "videoChannel" AS "ActorFollow->ActorFollowing->VideoChannel" | ||
253 | ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->VideoChannel"."actorId" | ||
254 | LEFT JOIN "account" AS "ActorFollow->ActorFollowing->Account" | ||
255 | ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->Account"."actorId" | ||
256 | LEFT JOIN "server" AS "ActorFollow->ActorFollowing->Server" | ||
257 | ON "ActorFollow->ActorFollowing"."serverId" = "ActorFollow->ActorFollowing->Server"."id" | ||
258 | ) ON "UserNotificationModel"."actorFollowId" = "ActorFollow"."id" | ||
259 | |||
260 | LEFT JOIN ( | ||
261 | "account" AS "Account" | ||
262 | INNER JOIN "actor" AS "Account->Actor" ON "Account"."actorId" = "Account->Actor"."id" | ||
263 | LEFT JOIN "actorImage" AS "Account->Actor->Avatars" | ||
264 | ON "Account->Actor"."id" = "Account->Actor->Avatars"."actorId" | ||
265 | AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR} | ||
266 | LEFT JOIN "server" AS "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id" | ||
267 | ) ON "UserNotificationModel"."accountId" = "Account"."id"` | ||
268 | } | ||
269 | } | ||
diff --git a/server/models/user/user-notification.ts b/server/models/user/user-notification.ts index edad10a55..eca127e7e 100644 --- a/server/models/user/user-notification.ts +++ b/server/models/user/user-notification.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' | 1 | import { ModelIndexesOptions, Op, WhereOptions } from 'sequelize' |
2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { getBiggestActorImage } from '@server/lib/actor-image' | ||
3 | import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' | 4 | import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' |
4 | import { uuidToShort } from '@shared/extra-utils' | 5 | import { uuidToShort } from '@shared/extra-utils' |
5 | import { UserNotification, UserNotificationType } from '@shared/models' | 6 | import { UserNotification, UserNotificationType } from '@shared/models' |
@@ -7,207 +8,18 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
7 | import { isBooleanValid } from '../../helpers/custom-validators/misc' | 8 | import { isBooleanValid } from '../../helpers/custom-validators/misc' |
8 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' | 9 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' |
9 | import { AbuseModel } from '../abuse/abuse' | 10 | import { AbuseModel } from '../abuse/abuse' |
10 | import { VideoAbuseModel } from '../abuse/video-abuse' | ||
11 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | ||
12 | import { AccountModel } from '../account/account' | 11 | import { AccountModel } from '../account/account' |
13 | import { ActorModel } from '../actor/actor' | ||
14 | import { ActorFollowModel } from '../actor/actor-follow' | 12 | import { ActorFollowModel } from '../actor/actor-follow' |
15 | import { ActorImageModel } from '../actor/actor-image' | ||
16 | import { ApplicationModel } from '../application/application' | 13 | import { ApplicationModel } from '../application/application' |
17 | import { PluginModel } from '../server/plugin' | 14 | import { PluginModel } from '../server/plugin' |
18 | import { ServerModel } from '../server/server' | 15 | import { throwIfNotValid } from '../utils' |
19 | import { getSort, throwIfNotValid } from '../utils' | ||
20 | import { VideoModel } from '../video/video' | 16 | import { VideoModel } from '../video/video' |
21 | import { VideoBlacklistModel } from '../video/video-blacklist' | 17 | import { VideoBlacklistModel } from '../video/video-blacklist' |
22 | import { VideoChannelModel } from '../video/video-channel' | ||
23 | import { VideoCommentModel } from '../video/video-comment' | 18 | import { VideoCommentModel } from '../video/video-comment' |
24 | import { VideoImportModel } from '../video/video-import' | 19 | import { VideoImportModel } from '../video/video-import' |
20 | import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder' | ||
25 | import { UserModel } from './user' | 21 | import { UserModel } from './user' |
26 | 22 | ||
27 | enum ScopeNames { | ||
28 | WITH_ALL = 'WITH_ALL' | ||
29 | } | ||
30 | |||
31 | function buildActorWithAvatarInclude () { | ||
32 | return { | ||
33 | attributes: [ 'preferredUsername' ], | ||
34 | model: ActorModel.unscoped(), | ||
35 | required: true, | ||
36 | include: [ | ||
37 | { | ||
38 | attributes: [ 'filename' ], | ||
39 | as: 'Avatar', | ||
40 | model: ActorImageModel.unscoped(), | ||
41 | required: false | ||
42 | }, | ||
43 | { | ||
44 | attributes: [ 'host' ], | ||
45 | model: ServerModel.unscoped(), | ||
46 | required: false | ||
47 | } | ||
48 | ] | ||
49 | } | ||
50 | } | ||
51 | |||
52 | function buildVideoInclude (required: boolean) { | ||
53 | return { | ||
54 | attributes: [ 'id', 'uuid', 'name' ], | ||
55 | model: VideoModel.unscoped(), | ||
56 | required | ||
57 | } | ||
58 | } | ||
59 | |||
60 | function buildChannelInclude (required: boolean, withActor = false) { | ||
61 | return { | ||
62 | required, | ||
63 | attributes: [ 'id', 'name' ], | ||
64 | model: VideoChannelModel.unscoped(), | ||
65 | include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] | ||
66 | } | ||
67 | } | ||
68 | |||
69 | function buildAccountInclude (required: boolean, withActor = false) { | ||
70 | return { | ||
71 | required, | ||
72 | attributes: [ 'id', 'name' ], | ||
73 | model: AccountModel.unscoped(), | ||
74 | include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] | ||
75 | } | ||
76 | } | ||
77 | |||
78 | @Scopes(() => ({ | ||
79 | [ScopeNames.WITH_ALL]: { | ||
80 | include: [ | ||
81 | Object.assign(buildVideoInclude(false), { | ||
82 | include: [ buildChannelInclude(true, true) ] | ||
83 | }), | ||
84 | |||
85 | { | ||
86 | attributes: [ 'id', 'originCommentId' ], | ||
87 | model: VideoCommentModel.unscoped(), | ||
88 | required: false, | ||
89 | include: [ | ||
90 | buildAccountInclude(true, true), | ||
91 | buildVideoInclude(true) | ||
92 | ] | ||
93 | }, | ||
94 | |||
95 | { | ||
96 | attributes: [ 'id', 'state' ], | ||
97 | model: AbuseModel.unscoped(), | ||
98 | required: false, | ||
99 | include: [ | ||
100 | { | ||
101 | attributes: [ 'id' ], | ||
102 | model: VideoAbuseModel.unscoped(), | ||
103 | required: false, | ||
104 | include: [ buildVideoInclude(false) ] | ||
105 | }, | ||
106 | { | ||
107 | attributes: [ 'id' ], | ||
108 | model: VideoCommentAbuseModel.unscoped(), | ||
109 | required: false, | ||
110 | include: [ | ||
111 | { | ||
112 | attributes: [ 'id', 'originCommentId' ], | ||
113 | model: VideoCommentModel.unscoped(), | ||
114 | required: false, | ||
115 | include: [ | ||
116 | { | ||
117 | attributes: [ 'id', 'name', 'uuid' ], | ||
118 | model: VideoModel.unscoped(), | ||
119 | required: false | ||
120 | } | ||
121 | ] | ||
122 | } | ||
123 | ] | ||
124 | }, | ||
125 | { | ||
126 | model: AccountModel, | ||
127 | as: 'FlaggedAccount', | ||
128 | required: false, | ||
129 | include: [ buildActorWithAvatarInclude() ] | ||
130 | } | ||
131 | ] | ||
132 | }, | ||
133 | |||
134 | { | ||
135 | attributes: [ 'id' ], | ||
136 | model: VideoBlacklistModel.unscoped(), | ||
137 | required: false, | ||
138 | include: [ buildVideoInclude(true) ] | ||
139 | }, | ||
140 | |||
141 | { | ||
142 | attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], | ||
143 | model: VideoImportModel.unscoped(), | ||
144 | required: false, | ||
145 | include: [ buildVideoInclude(false) ] | ||
146 | }, | ||
147 | |||
148 | { | ||
149 | attributes: [ 'id', 'name', 'type', 'latestVersion' ], | ||
150 | model: PluginModel.unscoped(), | ||
151 | required: false | ||
152 | }, | ||
153 | |||
154 | { | ||
155 | attributes: [ 'id', 'latestPeerTubeVersion' ], | ||
156 | model: ApplicationModel.unscoped(), | ||
157 | required: false | ||
158 | }, | ||
159 | |||
160 | { | ||
161 | attributes: [ 'id', 'state' ], | ||
162 | model: ActorFollowModel.unscoped(), | ||
163 | required: false, | ||
164 | include: [ | ||
165 | { | ||
166 | attributes: [ 'preferredUsername' ], | ||
167 | model: ActorModel.unscoped(), | ||
168 | required: true, | ||
169 | as: 'ActorFollower', | ||
170 | include: [ | ||
171 | { | ||
172 | attributes: [ 'id', 'name' ], | ||
173 | model: AccountModel.unscoped(), | ||
174 | required: true | ||
175 | }, | ||
176 | { | ||
177 | attributes: [ 'filename' ], | ||
178 | as: 'Avatar', | ||
179 | model: ActorImageModel.unscoped(), | ||
180 | required: false | ||
181 | }, | ||
182 | { | ||
183 | attributes: [ 'host' ], | ||
184 | model: ServerModel.unscoped(), | ||
185 | required: false | ||
186 | } | ||
187 | ] | ||
188 | }, | ||
189 | { | ||
190 | attributes: [ 'preferredUsername', 'type' ], | ||
191 | model: ActorModel.unscoped(), | ||
192 | required: true, | ||
193 | as: 'ActorFollowing', | ||
194 | include: [ | ||
195 | buildChannelInclude(false), | ||
196 | buildAccountInclude(false), | ||
197 | { | ||
198 | attributes: [ 'host' ], | ||
199 | model: ServerModel.unscoped(), | ||
200 | required: false | ||
201 | } | ||
202 | ] | ||
203 | } | ||
204 | ] | ||
205 | }, | ||
206 | |||
207 | buildAccountInclude(false, true) | ||
208 | ] | ||
209 | } | ||
210 | })) | ||
211 | @Table({ | 23 | @Table({ |
212 | tableName: 'userNotification', | 24 | tableName: 'userNotification', |
213 | indexes: [ | 25 | indexes: [ |
@@ -342,7 +154,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
342 | }, | 154 | }, |
343 | onDelete: 'cascade' | 155 | onDelete: 'cascade' |
344 | }) | 156 | }) |
345 | Comment: VideoCommentModel | 157 | VideoComment: VideoCommentModel |
346 | 158 | ||
347 | @ForeignKey(() => AbuseModel) | 159 | @ForeignKey(() => AbuseModel) |
348 | @Column | 160 | @Column |
@@ -431,11 +243,14 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
431 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { | 243 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { |
432 | const where = { userId } | 244 | const where = { userId } |
433 | 245 | ||
434 | const query: FindOptions = { | 246 | const query = { |
247 | userId, | ||
248 | unread, | ||
435 | offset: start, | 249 | offset: start, |
436 | limit: count, | 250 | limit: count, |
437 | order: getSort(sort), | 251 | sort, |
438 | where | 252 | where, |
253 | sequelize: this.sequelize | ||
439 | } | 254 | } |
440 | 255 | ||
441 | if (unread !== undefined) query.where['read'] = !unread | 256 | if (unread !== undefined) query.where['read'] = !unread |
@@ -445,8 +260,8 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
445 | .then(count => count || 0), | 260 | .then(count => count || 0), |
446 | 261 | ||
447 | count === 0 | 262 | count === 0 |
448 | ? [] | 263 | ? [] as UserNotificationModelForApi[] |
449 | : UserNotificationModel.scope(ScopeNames.WITH_ALL).findAll(query) | 264 | : new UserNotificationListQueryBuilder(query).listNotifications() |
450 | ]).then(([ total, data ]) => ({ total, data })) | 265 | ]).then(([ total, data ]) => ({ total, data })) |
451 | } | 266 | } |
452 | 267 | ||
@@ -524,25 +339,31 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
524 | 339 | ||
525 | toFormattedJSON (this: UserNotificationModelForApi): UserNotification { | 340 | toFormattedJSON (this: UserNotificationModelForApi): UserNotification { |
526 | const video = this.Video | 341 | const video = this.Video |
527 | ? Object.assign(this.formatVideo(this.Video), { channel: this.formatActor(this.Video.VideoChannel) }) | 342 | ? { |
343 | ...this.formatVideo(this.Video), | ||
344 | |||
345 | channel: this.formatActor(this.Video.VideoChannel) | ||
346 | } | ||
528 | : undefined | 347 | : undefined |
529 | 348 | ||
530 | const videoImport = this.VideoImport | 349 | const videoImport = this.VideoImport |
531 | ? { | 350 | ? { |
532 | id: this.VideoImport.id, | 351 | id: this.VideoImport.id, |
533 | video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined, | 352 | video: this.VideoImport.Video |
353 | ? this.formatVideo(this.VideoImport.Video) | ||
354 | : undefined, | ||
534 | torrentName: this.VideoImport.torrentName, | 355 | torrentName: this.VideoImport.torrentName, |
535 | magnetUri: this.VideoImport.magnetUri, | 356 | magnetUri: this.VideoImport.magnetUri, |
536 | targetUrl: this.VideoImport.targetUrl | 357 | targetUrl: this.VideoImport.targetUrl |
537 | } | 358 | } |
538 | : undefined | 359 | : undefined |
539 | 360 | ||
540 | const comment = this.Comment | 361 | const comment = this.VideoComment |
541 | ? { | 362 | ? { |
542 | id: this.Comment.id, | 363 | id: this.VideoComment.id, |
543 | threadId: this.Comment.getThreadId(), | 364 | threadId: this.VideoComment.getThreadId(), |
544 | account: this.formatActor(this.Comment.Account), | 365 | account: this.formatActor(this.VideoComment.Account), |
545 | video: this.formatVideo(this.Comment.Video) | 366 | video: this.formatVideo(this.VideoComment.Video) |
546 | } | 367 | } |
547 | : undefined | 368 | : undefined |
548 | 369 | ||
@@ -570,8 +391,9 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
570 | id: this.ActorFollow.ActorFollower.Account.id, | 391 | id: this.ActorFollow.ActorFollower.Account.id, |
571 | displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), | 392 | displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), |
572 | name: this.ActorFollow.ActorFollower.preferredUsername, | 393 | name: this.ActorFollow.ActorFollower.preferredUsername, |
573 | avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getStaticPath() } : undefined, | 394 | host: this.ActorFollow.ActorFollower.getHost(), |
574 | host: this.ActorFollow.ActorFollower.getHost() | 395 | |
396 | ...this.formatAvatars(this.ActorFollow.ActorFollower.Avatars) | ||
575 | }, | 397 | }, |
576 | following: { | 398 | following: { |
577 | type: actorFollowingType[this.ActorFollow.ActorFollowing.type], | 399 | type: actorFollowingType[this.ActorFollow.ActorFollowing.type], |
@@ -612,7 +434,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
612 | } | 434 | } |
613 | } | 435 | } |
614 | 436 | ||
615 | formatVideo (this: UserNotificationModelForApi, video: UserNotificationIncludes.VideoInclude) { | 437 | formatVideo (video: UserNotificationIncludes.VideoInclude) { |
616 | return { | 438 | return { |
617 | id: video.id, | 439 | id: video.id, |
618 | uuid: video.uuid, | 440 | uuid: video.uuid, |
@@ -621,7 +443,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
621 | } | 443 | } |
622 | } | 444 | } |
623 | 445 | ||
624 | formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) { | 446 | formatAbuse (abuse: UserNotificationIncludes.AbuseInclude) { |
625 | const commentAbuse = abuse.VideoCommentAbuse?.VideoComment | 447 | const commentAbuse = abuse.VideoCommentAbuse?.VideoComment |
626 | ? { | 448 | ? { |
627 | threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), | 449 | threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), |
@@ -637,9 +459,13 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
637 | } | 459 | } |
638 | : undefined | 460 | : undefined |
639 | 461 | ||
640 | const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined | 462 | const videoAbuse = abuse.VideoAbuse?.Video |
463 | ? this.formatVideo(abuse.VideoAbuse.Video) | ||
464 | : undefined | ||
641 | 465 | ||
642 | const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined | 466 | const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) |
467 | ? this.formatActor(abuse.FlaggedAccount) | ||
468 | : undefined | ||
643 | 469 | ||
644 | return { | 470 | return { |
645 | id: abuse.id, | 471 | id: abuse.id, |
@@ -651,19 +477,32 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti | |||
651 | } | 477 | } |
652 | 478 | ||
653 | formatActor ( | 479 | formatActor ( |
654 | this: UserNotificationModelForApi, | ||
655 | accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor | 480 | accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor |
656 | ) { | 481 | ) { |
657 | const avatar = accountOrChannel.Actor.Avatar | ||
658 | ? { path: accountOrChannel.Actor.Avatar.getStaticPath() } | ||
659 | : undefined | ||
660 | |||
661 | return { | 482 | return { |
662 | id: accountOrChannel.id, | 483 | id: accountOrChannel.id, |
663 | displayName: accountOrChannel.getDisplayName(), | 484 | displayName: accountOrChannel.getDisplayName(), |
664 | name: accountOrChannel.Actor.preferredUsername, | 485 | name: accountOrChannel.Actor.preferredUsername, |
665 | host: accountOrChannel.Actor.getHost(), | 486 | host: accountOrChannel.Actor.getHost(), |
666 | avatar | 487 | |
488 | ...this.formatAvatars(accountOrChannel.Actor.Avatars) | ||
489 | } | ||
490 | } | ||
491 | |||
492 | formatAvatars (avatars: UserNotificationIncludes.ActorImageInclude[]) { | ||
493 | if (!avatars || avatars.length === 0) return { avatar: undefined, avatars: [] } | ||
494 | |||
495 | return { | ||
496 | avatar: this.formatAvatar(getBiggestActorImage(avatars)), | ||
497 | |||
498 | avatars: avatars.map(a => this.formatAvatar(a)) | ||
499 | } | ||
500 | } | ||
501 | |||
502 | formatAvatar (a: UserNotificationIncludes.ActorImageInclude) { | ||
503 | return { | ||
504 | path: a.getStaticPath(), | ||
505 | width: a.width | ||
667 | } | 506 | } |
668 | } | 507 | } |
669 | } | 508 | } |
diff --git a/server/models/user/user.ts b/server/models/user/user.ts index ad8ce08cb..bcf56dfa1 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts | |||
@@ -106,7 +106,7 @@ enum ScopeNames { | |||
106 | include: [ | 106 | include: [ |
107 | { | 107 | { |
108 | model: ActorImageModel, | 108 | model: ActorImageModel, |
109 | as: 'Banner', | 109 | as: 'Banners', |
110 | required: false | 110 | required: false |
111 | } | 111 | } |
112 | ] | 112 | ] |
@@ -495,13 +495,10 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
495 | where | 495 | where |
496 | } | 496 | } |
497 | 497 | ||
498 | return UserModel.findAndCountAll(query) | 498 | return Promise.all([ |
499 | .then(({ rows, count }) => { | 499 | UserModel.unscoped().count(query), |
500 | return { | 500 | UserModel.findAll(query) |
501 | data: rows, | 501 | ]).then(([ total, data ]) => ({ total, data })) |
502 | total: count | ||
503 | } | ||
504 | }) | ||
505 | } | 502 | } |
506 | 503 | ||
507 | static listWithRight (right: UserRight): Promise<MUserDefault[]> { | 504 | static listWithRight (right: UserRight): Promise<MUserDefault[]> { |
diff --git a/server/models/utils.ts b/server/models/utils.ts index 66b653e3d..70bfbdb8b 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -181,7 +181,7 @@ function buildServerIdsFollowedBy (actorId: any) { | |||
181 | 'SELECT "actor"."serverId" FROM "actorFollow" ' + | 181 | 'SELECT "actor"."serverId" FROM "actorFollow" ' + |
182 | 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + | 182 | 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + |
183 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + | 183 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + |
184 | ')' | 184 | ')' |
185 | } | 185 | } |
186 | 186 | ||
187 | function buildWhereIdOrUUID (id: number | string) { | 187 | function buildWhereIdOrUUID (id: number | string) { |
diff --git a/server/models/video/sql/video/index.ts b/server/models/video/sql/video/index.ts new file mode 100644 index 000000000..e9132d5e1 --- /dev/null +++ b/server/models/video/sql/video/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './video-model-get-query-builder' | ||
2 | export * from './videos-id-list-query-builder' | ||
3 | export * from './videos-model-list-query-builder' | ||
diff --git a/server/models/video/sql/shared/abstract-run-query.ts b/server/models/video/sql/video/shared/abstract-run-query.ts index 8e7a7642d..8e7a7642d 100644 --- a/server/models/video/sql/shared/abstract-run-query.ts +++ b/server/models/video/sql/video/shared/abstract-run-query.ts | |||
diff --git a/server/models/video/sql/shared/abstract-video-query-builder.ts b/server/models/video/sql/video/shared/abstract-video-query-builder.ts index a6afb04e4..490e5e6e0 100644 --- a/server/models/video/sql/shared/abstract-video-query-builder.ts +++ b/server/models/video/sql/video/shared/abstract-video-query-builder.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { createSafeIn } from '@server/models/utils' | 1 | import { createSafeIn } from '@server/models/utils' |
2 | import { MUserAccountId } from '@server/types/models' | 2 | import { MUserAccountId } from '@server/types/models' |
3 | import { ActorImageType } from '@shared/models' | ||
3 | import validator from 'validator' | 4 | import validator from 'validator' |
4 | import { AbstractRunQuery } from './abstract-run-query' | 5 | import { AbstractRunQuery } from './abstract-run-query' |
5 | import { VideoTableAttributes } from './video-table-attributes' | 6 | import { VideoTableAttributes } from './video-table-attributes' |
@@ -42,8 +43,9 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
42 | ) | 43 | ) |
43 | 44 | ||
44 | this.addJoin( | 45 | this.addJoin( |
45 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' + | 46 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatars" ' + |
46 | 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"' | 47 | 'ON "VideoChannel->Actor"."id" = "VideoChannel->Actor->Avatars"."actorId" ' + |
48 | `AND "VideoChannel->Actor->Avatars"."type" = ${ActorImageType.AVATAR}` | ||
47 | ) | 49 | ) |
48 | 50 | ||
49 | this.attributes = { | 51 | this.attributes = { |
@@ -51,7 +53,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
51 | 53 | ||
52 | ...this.buildAttributesObject('VideoChannel', this.tables.getChannelAttributes()), | 54 | ...this.buildAttributesObject('VideoChannel', this.tables.getChannelAttributes()), |
53 | ...this.buildActorInclude('VideoChannel->Actor'), | 55 | ...this.buildActorInclude('VideoChannel->Actor'), |
54 | ...this.buildAvatarInclude('VideoChannel->Actor->Avatar'), | 56 | ...this.buildAvatarInclude('VideoChannel->Actor->Avatars'), |
55 | ...this.buildServerInclude('VideoChannel->Actor->Server') | 57 | ...this.buildServerInclude('VideoChannel->Actor->Server') |
56 | } | 58 | } |
57 | } | 59 | } |
@@ -68,8 +70,9 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
68 | ) | 70 | ) |
69 | 71 | ||
70 | this.addJoin( | 72 | this.addJoin( |
71 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' + | 73 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatars" ' + |
72 | 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"' | 74 | 'ON "VideoChannel->Account"."actorId"= "VideoChannel->Account->Actor->Avatars"."actorId" ' + |
75 | `AND "VideoChannel->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}` | ||
73 | ) | 76 | ) |
74 | 77 | ||
75 | this.attributes = { | 78 | this.attributes = { |
@@ -77,7 +80,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
77 | 80 | ||
78 | ...this.buildAttributesObject('VideoChannel->Account', this.tables.getAccountAttributes()), | 81 | ...this.buildAttributesObject('VideoChannel->Account', this.tables.getAccountAttributes()), |
79 | ...this.buildActorInclude('VideoChannel->Account->Actor'), | 82 | ...this.buildActorInclude('VideoChannel->Account->Actor'), |
80 | ...this.buildAvatarInclude('VideoChannel->Account->Actor->Avatar'), | 83 | ...this.buildAvatarInclude('VideoChannel->Account->Actor->Avatars'), |
81 | ...this.buildServerInclude('VideoChannel->Account->Actor->Server') | 84 | ...this.buildServerInclude('VideoChannel->Account->Actor->Server') |
82 | } | 85 | } |
83 | } | 86 | } |
diff --git a/server/models/video/sql/shared/video-file-query-builder.ts b/server/models/video/sql/video/shared/video-file-query-builder.ts index 3eb3dc07d..3eb3dc07d 100644 --- a/server/models/video/sql/shared/video-file-query-builder.ts +++ b/server/models/video/sql/video/shared/video-file-query-builder.ts | |||
diff --git a/server/models/video/sql/shared/video-model-builder.ts b/server/models/video/sql/video/shared/video-model-builder.ts index 7751d8e68..b1b47b721 100644 --- a/server/models/video/sql/shared/video-model-builder.ts +++ b/server/models/video/sql/video/shared/video-model-builder.ts | |||
@@ -9,15 +9,15 @@ import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | |||
9 | import { TrackerModel } from '@server/models/server/tracker' | 9 | import { TrackerModel } from '@server/models/server/tracker' |
10 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' | 10 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' |
11 | import { VideoInclude } from '@shared/models' | 11 | import { VideoInclude } from '@shared/models' |
12 | import { ScheduleVideoUpdateModel } from '../../schedule-video-update' | 12 | import { ScheduleVideoUpdateModel } from '../../../schedule-video-update' |
13 | import { TagModel } from '../../tag' | 13 | import { TagModel } from '../../../tag' |
14 | import { ThumbnailModel } from '../../thumbnail' | 14 | import { ThumbnailModel } from '../../../thumbnail' |
15 | import { VideoModel } from '../../video' | 15 | import { VideoModel } from '../../../video' |
16 | import { VideoBlacklistModel } from '../../video-blacklist' | 16 | import { VideoBlacklistModel } from '../../../video-blacklist' |
17 | import { VideoChannelModel } from '../../video-channel' | 17 | import { VideoChannelModel } from '../../../video-channel' |
18 | import { VideoFileModel } from '../../video-file' | 18 | import { VideoFileModel } from '../../../video-file' |
19 | import { VideoLiveModel } from '../../video-live' | 19 | import { VideoLiveModel } from '../../../video-live' |
20 | import { VideoStreamingPlaylistModel } from '../../video-streaming-playlist' | 20 | import { VideoStreamingPlaylistModel } from '../../../video-streaming-playlist' |
21 | import { VideoTableAttributes } from './video-table-attributes' | 21 | import { VideoTableAttributes } from './video-table-attributes' |
22 | 22 | ||
23 | type SQLRow = { [id: string]: string | number } | 23 | type SQLRow = { [id: string]: string | number } |
@@ -34,6 +34,7 @@ export class VideoModelBuilder { | |||
34 | private videoFileMemo: { [ id: number ]: VideoFileModel } | 34 | private videoFileMemo: { [ id: number ]: VideoFileModel } |
35 | 35 | ||
36 | private thumbnailsDone: Set<any> | 36 | private thumbnailsDone: Set<any> |
37 | private actorImagesDone: Set<any> | ||
37 | private historyDone: Set<any> | 38 | private historyDone: Set<any> |
38 | private blacklistDone: Set<any> | 39 | private blacklistDone: Set<any> |
39 | private accountBlocklistDone: Set<any> | 40 | private accountBlocklistDone: Set<any> |
@@ -69,11 +70,21 @@ export class VideoModelBuilder { | |||
69 | for (const row of rows) { | 70 | for (const row of rows) { |
70 | this.buildVideoAndAccount(row) | 71 | this.buildVideoAndAccount(row) |
71 | 72 | ||
72 | const videoModel = this.videosMemo[row.id] | 73 | const videoModel = this.videosMemo[row.id as number] |
73 | 74 | ||
74 | this.setUserHistory(row, videoModel) | 75 | this.setUserHistory(row, videoModel) |
75 | this.addThumbnail(row, videoModel) | 76 | this.addThumbnail(row, videoModel) |
76 | 77 | ||
78 | const channelActor = videoModel.VideoChannel?.Actor | ||
79 | if (channelActor) { | ||
80 | this.addActorAvatar(row, 'VideoChannel.Actor', channelActor) | ||
81 | } | ||
82 | |||
83 | const accountActor = videoModel.VideoChannel?.Account?.Actor | ||
84 | if (accountActor) { | ||
85 | this.addActorAvatar(row, 'VideoChannel.Account.Actor', accountActor) | ||
86 | } | ||
87 | |||
77 | if (!rowsWebTorrentFiles) { | 88 | if (!rowsWebTorrentFiles) { |
78 | this.addWebTorrentFile(row, videoModel) | 89 | this.addWebTorrentFile(row, videoModel) |
79 | } | 90 | } |
@@ -113,6 +124,7 @@ export class VideoModelBuilder { | |||
113 | this.videoFileMemo = {} | 124 | this.videoFileMemo = {} |
114 | 125 | ||
115 | this.thumbnailsDone = new Set() | 126 | this.thumbnailsDone = new Set() |
127 | this.actorImagesDone = new Set() | ||
116 | this.historyDone = new Set() | 128 | this.historyDone = new Set() |
117 | this.blacklistDone = new Set() | 129 | this.blacklistDone = new Set() |
118 | this.liveDone = new Set() | 130 | this.liveDone = new Set() |
@@ -195,13 +207,8 @@ export class VideoModelBuilder { | |||
195 | 207 | ||
196 | private buildActor (row: SQLRow, prefix: string) { | 208 | private buildActor (row: SQLRow, prefix: string) { |
197 | const actorPrefix = `${prefix}.Actor` | 209 | const actorPrefix = `${prefix}.Actor` |
198 | const avatarPrefix = `${actorPrefix}.Avatar` | ||
199 | const serverPrefix = `${actorPrefix}.Server` | 210 | const serverPrefix = `${actorPrefix}.Server` |
200 | 211 | ||
201 | const avatarModel = row[`${avatarPrefix}.id`] !== null | ||
202 | ? new ActorImageModel(this.grab(row, this.tables.getAvatarAttributes(), avatarPrefix), this.buildOpts) | ||
203 | : null | ||
204 | |||
205 | const serverModel = row[`${serverPrefix}.id`] !== null | 212 | const serverModel = row[`${serverPrefix}.id`] !== null |
206 | ? new ServerModel(this.grab(row, this.tables.getServerAttributes(), serverPrefix), this.buildOpts) | 213 | ? new ServerModel(this.grab(row, this.tables.getServerAttributes(), serverPrefix), this.buildOpts) |
207 | : null | 214 | : null |
@@ -209,8 +216,8 @@ export class VideoModelBuilder { | |||
209 | if (serverModel) serverModel.BlockedBy = [] | 216 | if (serverModel) serverModel.BlockedBy = [] |
210 | 217 | ||
211 | const actorModel = new ActorModel(this.grab(row, this.tables.getActorAttributes(), actorPrefix), this.buildOpts) | 218 | const actorModel = new ActorModel(this.grab(row, this.tables.getActorAttributes(), actorPrefix), this.buildOpts) |
212 | actorModel.Avatar = avatarModel | ||
213 | actorModel.Server = serverModel | 219 | actorModel.Server = serverModel |
220 | actorModel.Avatars = [] | ||
214 | 221 | ||
215 | return actorModel | 222 | return actorModel |
216 | } | 223 | } |
@@ -226,6 +233,18 @@ export class VideoModelBuilder { | |||
226 | this.historyDone.add(id) | 233 | this.historyDone.add(id) |
227 | } | 234 | } |
228 | 235 | ||
236 | private addActorAvatar (row: SQLRow, actorPrefix: string, actor: ActorModel) { | ||
237 | const avatarPrefix = `${actorPrefix}.Avatar` | ||
238 | const id = row[`${avatarPrefix}.id`] | ||
239 | if (!id || this.actorImagesDone.has(id)) return | ||
240 | |||
241 | const attributes = this.grab(row, this.tables.getAvatarAttributes(), avatarPrefix) | ||
242 | const avatarModel = new ActorImageModel(attributes, this.buildOpts) | ||
243 | actor.Avatars.push(avatarModel) | ||
244 | |||
245 | this.actorImagesDone.add(id) | ||
246 | } | ||
247 | |||
229 | private addThumbnail (row: SQLRow, videoModel: VideoModel) { | 248 | private addThumbnail (row: SQLRow, videoModel: VideoModel) { |
230 | const id = row['Thumbnails.id'] | 249 | const id = row['Thumbnails.id'] |
231 | if (!id || this.thumbnailsDone.has(id)) return | 250 | if (!id || this.thumbnailsDone.has(id)) return |
diff --git a/server/models/video/sql/shared/video-table-attributes.ts b/server/models/video/sql/video/shared/video-table-attributes.ts index 8a8d2073a..df2ed3fb0 100644 --- a/server/models/video/sql/shared/video-table-attributes.ts +++ b/server/models/video/sql/video/shared/video-table-attributes.ts | |||
@@ -186,8 +186,7 @@ export class VideoTableAttributes { | |||
186 | 'id', | 186 | 'id', |
187 | 'preferredUsername', | 187 | 'preferredUsername', |
188 | 'url', | 188 | 'url', |
189 | 'serverId', | 189 | 'serverId' |
190 | 'avatarId' | ||
191 | ] | 190 | ] |
192 | 191 | ||
193 | if (this.mode === 'get') { | 192 | if (this.mode === 'get') { |
@@ -212,6 +211,7 @@ export class VideoTableAttributes { | |||
212 | getAvatarAttributes () { | 211 | getAvatarAttributes () { |
213 | let attributeKeys = [ | 212 | let attributeKeys = [ |
214 | 'id', | 213 | 'id', |
214 | 'width', | ||
215 | 'filename', | 215 | 'filename', |
216 | 'type', | 216 | 'type', |
217 | 'fileUrl', | 217 | 'fileUrl', |
diff --git a/server/models/video/sql/video-model-get-query-builder.ts b/server/models/video/sql/video/video-model-get-query-builder.ts index a65c96097..a65c96097 100644 --- a/server/models/video/sql/video-model-get-query-builder.ts +++ b/server/models/video/sql/video/video-model-get-query-builder.ts | |||
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/video/videos-id-list-query-builder.ts index 098e15359..098e15359 100644 --- a/server/models/video/sql/videos-id-list-query-builder.ts +++ b/server/models/video/sql/video/videos-id-list-query-builder.ts | |||
diff --git a/server/models/video/sql/videos-model-list-query-builder.ts b/server/models/video/sql/video/videos-model-list-query-builder.ts index b15b29ec3..b15b29ec3 100644 --- a/server/models/video/sql/videos-model-list-query-builder.ts +++ b/server/models/video/sql/video/videos-model-list-query-builder.ts | |||
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 2c6669bcb..410fd6d3f 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -31,6 +31,7 @@ import { | |||
31 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | 31 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
32 | import { sendDeleteActor } from '../../lib/activitypub/send' | 32 | import { sendDeleteActor } from '../../lib/activitypub/send' |
33 | import { | 33 | import { |
34 | MChannel, | ||
34 | MChannelActor, | 35 | MChannelActor, |
35 | MChannelAP, | 36 | MChannelAP, |
36 | MChannelBannerAccountDefault, | 37 | MChannelBannerAccountDefault, |
@@ -62,6 +63,7 @@ type AvailableForListOptions = { | |||
62 | search?: string | 63 | search?: string |
63 | host?: string | 64 | host?: string |
64 | handles?: string[] | 65 | handles?: string[] |
66 | forCount?: boolean | ||
65 | } | 67 | } |
66 | 68 | ||
67 | type AvailableWithStatsOptions = { | 69 | type AvailableWithStatsOptions = { |
@@ -116,70 +118,91 @@ export type SummaryOptions = { | |||
116 | }) | 118 | }) |
117 | } | 119 | } |
118 | 120 | ||
119 | let rootWhere: WhereOptions | 121 | if (Array.isArray(options.handles) && options.handles.length !== 0) { |
120 | if (options.handles) { | 122 | const or: string[] = [] |
121 | const or: WhereOptions[] = [] | ||
122 | 123 | ||
123 | for (const handle of options.handles || []) { | 124 | for (const handle of options.handles || []) { |
124 | const [ preferredUsername, host ] = handle.split('@') | 125 | const [ preferredUsername, host ] = handle.split('@') |
125 | 126 | ||
126 | if (!host || host === WEBSERVER.HOST) { | 127 | if (!host || host === WEBSERVER.HOST) { |
127 | or.push({ | 128 | or.push(`("preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} AND "serverId" IS NULL)`) |
128 | '$Actor.preferredUsername$': preferredUsername, | ||
129 | '$Actor.serverId$': null | ||
130 | }) | ||
131 | } else { | 129 | } else { |
132 | or.push({ | 130 | or.push( |
133 | '$Actor.preferredUsername$': preferredUsername, | 131 | `(` + |
134 | '$Actor.Server.host$': host | 132 | `"preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} ` + |
135 | }) | 133 | `AND "host" = ${VideoChannelModel.sequelize.escape(host)}` + |
134 | `)` | ||
135 | ) | ||
136 | } | 136 | } |
137 | } | 137 | } |
138 | 138 | ||
139 | rootWhere = { | 139 | whereActorAnd.push({ |
140 | [Op.or]: or | 140 | id: { |
141 | } | 141 | [Op.in]: literal(`(SELECT "actor".id FROM actor LEFT JOIN server on server.id = actor."serverId" WHERE ${or.join(' OR ')})`) |
142 | } | ||
143 | }) | ||
144 | } | ||
145 | |||
146 | const channelInclude: Includeable[] = [] | ||
147 | const accountInclude: Includeable[] = [] | ||
148 | |||
149 | if (options.forCount !== true) { | ||
150 | accountInclude.push({ | ||
151 | model: ServerModel, | ||
152 | required: false | ||
153 | }) | ||
154 | |||
155 | accountInclude.push({ | ||
156 | model: ActorImageModel, | ||
157 | as: 'Avatars', | ||
158 | required: false | ||
159 | }) | ||
160 | |||
161 | channelInclude.push({ | ||
162 | model: ActorImageModel, | ||
163 | as: 'Avatars', | ||
164 | required: false | ||
165 | }) | ||
166 | |||
167 | channelInclude.push({ | ||
168 | model: ActorImageModel, | ||
169 | as: 'Banners', | ||
170 | required: false | ||
171 | }) | ||
172 | } | ||
173 | |||
174 | if (options.forCount !== true || serverRequired) { | ||
175 | channelInclude.push({ | ||
176 | model: ServerModel, | ||
177 | duplicating: false, | ||
178 | required: serverRequired, | ||
179 | where: whereServer | ||
180 | }) | ||
142 | } | 181 | } |
143 | 182 | ||
144 | return { | 183 | return { |
145 | where: rootWhere, | ||
146 | include: [ | 184 | include: [ |
147 | { | 185 | { |
148 | attributes: { | 186 | attributes: { |
149 | exclude: unusedActorAttributesForAPI | 187 | exclude: unusedActorAttributesForAPI |
150 | }, | 188 | }, |
151 | model: ActorModel, | 189 | model: ActorModel.unscoped(), |
152 | where: { | 190 | where: { |
153 | [Op.and]: whereActorAnd | 191 | [Op.and]: whereActorAnd |
154 | }, | 192 | }, |
155 | include: [ | 193 | include: channelInclude |
156 | { | ||
157 | model: ServerModel, | ||
158 | required: serverRequired, | ||
159 | where: whereServer | ||
160 | }, | ||
161 | { | ||
162 | model: ActorImageModel, | ||
163 | as: 'Avatar', | ||
164 | required: false | ||
165 | }, | ||
166 | { | ||
167 | model: ActorImageModel, | ||
168 | as: 'Banner', | ||
169 | required: false | ||
170 | } | ||
171 | ] | ||
172 | }, | 194 | }, |
173 | { | 195 | { |
174 | model: AccountModel, | 196 | model: AccountModel.unscoped(), |
175 | required: true, | 197 | required: true, |
176 | include: [ | 198 | include: [ |
177 | { | 199 | { |
178 | attributes: { | 200 | attributes: { |
179 | exclude: unusedActorAttributesForAPI | 201 | exclude: unusedActorAttributesForAPI |
180 | }, | 202 | }, |
181 | model: ActorModel, // Default scope includes avatar and server | 203 | model: ActorModel.unscoped(), |
182 | required: true | 204 | required: true, |
205 | include: accountInclude | ||
183 | } | 206 | } |
184 | ] | 207 | ] |
185 | } | 208 | } |
@@ -189,7 +212,7 @@ export type SummaryOptions = { | |||
189 | [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { | 212 | [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { |
190 | const include: Includeable[] = [ | 213 | const include: Includeable[] = [ |
191 | { | 214 | { |
192 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], | 215 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId' ], |
193 | model: ActorModel.unscoped(), | 216 | model: ActorModel.unscoped(), |
194 | required: options.actorRequired ?? true, | 217 | required: options.actorRequired ?? true, |
195 | include: [ | 218 | include: [ |
@@ -199,8 +222,8 @@ export type SummaryOptions = { | |||
199 | required: false | 222 | required: false |
200 | }, | 223 | }, |
201 | { | 224 | { |
202 | model: ActorImageModel.unscoped(), | 225 | model: ActorImageModel, |
203 | as: 'Avatar', | 226 | as: 'Avatars', |
204 | required: false | 227 | required: false |
205 | } | 228 | } |
206 | ] | 229 | ] |
@@ -245,7 +268,7 @@ export type SummaryOptions = { | |||
245 | { | 268 | { |
246 | model: ActorImageModel, | 269 | model: ActorImageModel, |
247 | required: false, | 270 | required: false, |
248 | as: 'Banner' | 271 | as: 'Banners' |
249 | } | 272 | } |
250 | ] | 273 | ] |
251 | } | 274 | } |
@@ -474,14 +497,14 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
474 | order: getSort(parameters.sort) | 497 | order: getSort(parameters.sort) |
475 | } | 498 | } |
476 | 499 | ||
477 | return VideoChannelModel | 500 | const getScope = (forCount: boolean) => { |
478 | .scope({ | 501 | return { method: [ ScopeNames.FOR_API, { actorId, forCount } as AvailableForListOptions ] } |
479 | method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] | 502 | } |
480 | }) | 503 | |
481 | .findAndCountAll(query) | 504 | return Promise.all([ |
482 | .then(({ rows, count }) => { | 505 | VideoChannelModel.scope(getScope(true)).count(), |
483 | return { total: count, data: rows } | 506 | VideoChannelModel.scope(getScope(false)).findAll(query) |
484 | }) | 507 | ]).then(([ total, data ]) => ({ total, data })) |
485 | } | 508 | } |
486 | 509 | ||
487 | static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & { | 510 | static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & { |
@@ -519,14 +542,22 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
519 | where | 542 | where |
520 | } | 543 | } |
521 | 544 | ||
522 | return VideoChannelModel | 545 | const getScope = (forCount: boolean) => { |
523 | .scope({ | 546 | return { |
524 | method: [ ScopeNames.FOR_API, pick(options, [ 'actorId', 'host', 'handles' ]) as AvailableForListOptions ] | 547 | method: [ |
525 | }) | 548 | ScopeNames.FOR_API, { |
526 | .findAndCountAll(query) | 549 | ...pick(options, [ 'actorId', 'host', 'handles' ]), |
527 | .then(({ rows, count }) => { | 550 | |
528 | return { total: count, data: rows } | 551 | forCount |
529 | }) | 552 | } as AvailableForListOptions |
553 | ] | ||
554 | } | ||
555 | } | ||
556 | |||
557 | return Promise.all([ | ||
558 | VideoChannelModel.scope(getScope(true)).count(query), | ||
559 | VideoChannelModel.scope(getScope(false)).findAll(query) | ||
560 | ]).then(([ total, data ]) => ({ total, data })) | ||
530 | } | 561 | } |
531 | 562 | ||
532 | static listByAccountForAPI (options: { | 563 | static listByAccountForAPI (options: { |
@@ -552,20 +583,26 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
552 | } | 583 | } |
553 | : null | 584 | : null |
554 | 585 | ||
555 | const query = { | 586 | const getQuery = (forCount: boolean) => { |
556 | offset: options.start, | 587 | const accountModel = forCount |
557 | limit: options.count, | 588 | ? AccountModel.unscoped() |
558 | order: getSort(options.sort), | 589 | : AccountModel |
559 | include: [ | 590 | |
560 | { | 591 | return { |
561 | model: AccountModel, | 592 | offset: options.start, |
562 | where: { | 593 | limit: options.count, |
563 | id: options.accountId | 594 | order: getSort(options.sort), |
564 | }, | 595 | include: [ |
565 | required: true | 596 | { |
566 | } | 597 | model: accountModel, |
567 | ], | 598 | where: { |
568 | where | 599 | id: options.accountId |
600 | }, | ||
601 | required: true | ||
602 | } | ||
603 | ], | ||
604 | where | ||
605 | } | ||
569 | } | 606 | } |
570 | 607 | ||
571 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ] | 608 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ] |
@@ -576,21 +613,19 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
576 | }) | 613 | }) |
577 | } | 614 | } |
578 | 615 | ||
579 | return VideoChannelModel | 616 | return Promise.all([ |
580 | .scope(scopes) | 617 | VideoChannelModel.scope(scopes).count(getQuery(true)), |
581 | .findAndCountAll(query) | 618 | VideoChannelModel.scope(scopes).findAll(getQuery(false)) |
582 | .then(({ rows, count }) => { | 619 | ]).then(([ total, data ]) => ({ total, data })) |
583 | return { total: count, data: rows } | ||
584 | }) | ||
585 | } | 620 | } |
586 | 621 | ||
587 | static listAllByAccount (accountId: number) { | 622 | static listAllByAccount (accountId: number): Promise<MChannel[]> { |
588 | const query = { | 623 | const query = { |
589 | limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER, | 624 | limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER, |
590 | include: [ | 625 | include: [ |
591 | { | 626 | { |
592 | attributes: [], | 627 | attributes: [], |
593 | model: AccountModel, | 628 | model: AccountModel.unscoped(), |
594 | where: { | 629 | where: { |
595 | id: accountId | 630 | id: accountId |
596 | }, | 631 | }, |
@@ -621,7 +656,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
621 | { | 656 | { |
622 | model: ActorImageModel, | 657 | model: ActorImageModel, |
623 | required: false, | 658 | required: false, |
624 | as: 'Banner' | 659 | as: 'Banners' |
625 | } | 660 | } |
626 | ] | 661 | ] |
627 | } | 662 | } |
@@ -655,7 +690,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
655 | { | 690 | { |
656 | model: ActorImageModel, | 691 | model: ActorImageModel, |
657 | required: false, | 692 | required: false, |
658 | as: 'Banner' | 693 | as: 'Banners' |
659 | } | 694 | } |
660 | ] | 695 | ] |
661 | } | 696 | } |
@@ -685,7 +720,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
685 | { | 720 | { |
686 | model: ActorImageModel, | 721 | model: ActorImageModel, |
687 | required: false, | 722 | required: false, |
688 | as: 'Banner' | 723 | as: 'Banners' |
689 | } | 724 | } |
690 | ] | 725 | ] |
691 | } | 726 | } |
@@ -706,6 +741,9 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
706 | displayName: this.getDisplayName(), | 741 | displayName: this.getDisplayName(), |
707 | url: actor.url, | 742 | url: actor.url, |
708 | host: actor.host, | 743 | host: actor.host, |
744 | avatars: actor.avatars, | ||
745 | |||
746 | // TODO: remove, deprecated in 4.2 | ||
709 | avatar: actor.avatar | 747 | avatar: actor.avatar |
710 | } | 748 | } |
711 | } | 749 | } |
@@ -736,9 +774,16 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
736 | support: this.support, | 774 | support: this.support, |
737 | isLocal: this.Actor.isOwned(), | 775 | isLocal: this.Actor.isOwned(), |
738 | updatedAt: this.updatedAt, | 776 | updatedAt: this.updatedAt, |
777 | |||
739 | ownerAccount: undefined, | 778 | ownerAccount: undefined, |
779 | |||
740 | videosCount, | 780 | videosCount, |
741 | viewsPerDay | 781 | viewsPerDay, |
782 | |||
783 | avatars: actor.avatars, | ||
784 | |||
785 | // TODO: remove, deprecated in 4.2 | ||
786 | avatar: actor.avatar | ||
742 | } | 787 | } |
743 | 788 | ||
744 | if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() | 789 | if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index fa77455bc..2d60c6a30 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { uniq } from 'lodash' | 1 | import { uniq } from 'lodash' |
2 | import { FindAndCountOptions, FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' | 2 | import { FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
3 | import { | 3 | import { |
4 | AllowNull, | 4 | AllowNull, |
5 | BelongsTo, | 5 | BelongsTo, |
@@ -16,8 +16,8 @@ import { | |||
16 | } from 'sequelize-typescript' | 16 | } from 'sequelize-typescript' |
17 | import { getServerActor } from '@server/models/application/application' | 17 | import { getServerActor } from '@server/models/application/application' |
18 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' | 18 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' |
19 | import { AttributesOnly } from '@shared/typescript-utils' | ||
20 | import { VideoPrivacy } from '@shared/models' | 19 | import { VideoPrivacy } from '@shared/models' |
20 | import { AttributesOnly } from '@shared/typescript-utils' | ||
21 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' | 21 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' |
22 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' | 22 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' |
23 | import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model' | 23 | import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model' |
@@ -363,40 +363,43 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
363 | Object.assign(whereVideo, searchAttribute(searchVideo, 'name')) | 363 | Object.assign(whereVideo, searchAttribute(searchVideo, 'name')) |
364 | } | 364 | } |
365 | 365 | ||
366 | const query: FindAndCountOptions = { | 366 | const getQuery = (forCount: boolean) => { |
367 | offset: start, | 367 | return { |
368 | limit: count, | 368 | offset: start, |
369 | order: getCommentSort(sort), | 369 | limit: count, |
370 | where, | 370 | order: getCommentSort(sort), |
371 | include: [ | 371 | where, |
372 | { | 372 | include: [ |
373 | model: AccountModel.unscoped(), | 373 | { |
374 | required: true, | 374 | model: AccountModel.unscoped(), |
375 | where: whereAccount, | 375 | required: true, |
376 | include: [ | 376 | where: whereAccount, |
377 | { | 377 | include: [ |
378 | attributes: { | 378 | { |
379 | exclude: unusedActorAttributesForAPI | 379 | attributes: { |
380 | }, | 380 | exclude: unusedActorAttributesForAPI |
381 | model: ActorModel, // Default scope includes avatar and server | 381 | }, |
382 | required: true, | 382 | model: forCount === true |
383 | where: whereActor | 383 | ? ActorModel.unscoped() // Default scope includes avatar and server |
384 | } | 384 | : ActorModel, |
385 | ] | 385 | required: true, |
386 | }, | 386 | where: whereActor |
387 | { | 387 | } |
388 | model: VideoModel.unscoped(), | 388 | ] |
389 | required: true, | 389 | }, |
390 | where: whereVideo | 390 | { |
391 | } | 391 | model: VideoModel.unscoped(), |
392 | ] | 392 | required: true, |
393 | where: whereVideo | ||
394 | } | ||
395 | ] | ||
396 | } | ||
393 | } | 397 | } |
394 | 398 | ||
395 | return VideoCommentModel | 399 | return Promise.all([ |
396 | .findAndCountAll(query) | 400 | VideoCommentModel.count(getQuery(true)), |
397 | .then(({ rows, count }) => { | 401 | VideoCommentModel.findAll(getQuery(false)) |
398 | return { total: count, data: rows } | 402 | ]).then(([ total, data ]) => ({ total, data })) |
399 | }) | ||
400 | } | 403 | } |
401 | 404 | ||
402 | static async listThreadsForApi (parameters: { | 405 | static async listThreadsForApi (parameters: { |
@@ -443,14 +446,20 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
443 | } | 446 | } |
444 | } | 447 | } |
445 | 448 | ||
446 | const scopesList: (string | ScopeOptions)[] = [ | 449 | const findScopesList: (string | ScopeOptions)[] = [ |
447 | ScopeNames.WITH_ACCOUNT_FOR_API, | 450 | ScopeNames.WITH_ACCOUNT_FOR_API, |
448 | { | 451 | { |
449 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] | 452 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] |
450 | } | 453 | } |
451 | ] | 454 | ] |
452 | 455 | ||
453 | const queryCount = { | 456 | const countScopesList: ScopeOptions[] = [ |
457 | { | ||
458 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] | ||
459 | } | ||
460 | ] | ||
461 | |||
462 | const notDeletedQueryCount = { | ||
454 | where: { | 463 | where: { |
455 | videoId, | 464 | videoId, |
456 | deletedAt: null, | 465 | deletedAt: null, |
@@ -459,9 +468,10 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
459 | } | 468 | } |
460 | 469 | ||
461 | return Promise.all([ | 470 | return Promise.all([ |
462 | VideoCommentModel.scope(scopesList).findAndCountAll(queryList), | 471 | VideoCommentModel.scope(findScopesList).findAll(queryList), |
463 | VideoCommentModel.count(queryCount) | 472 | VideoCommentModel.scope(countScopesList).count(queryList), |
464 | ]).then(([ { rows, count }, totalNotDeletedComments ]) => { | 473 | VideoCommentModel.count(notDeletedQueryCount) |
474 | ]).then(([ rows, count, totalNotDeletedComments ]) => { | ||
465 | return { total: count, data: rows, totalNotDeletedComments } | 475 | return { total: count, data: rows, totalNotDeletedComments } |
466 | }) | 476 | }) |
467 | } | 477 | } |
@@ -512,11 +522,10 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
512 | } | 522 | } |
513 | ] | 523 | ] |
514 | 524 | ||
515 | return VideoCommentModel.scope(scopes) | 525 | return Promise.all([ |
516 | .findAndCountAll(query) | 526 | VideoCommentModel.count(query), |
517 | .then(({ rows, count }) => { | 527 | VideoCommentModel.scope(scopes).findAll(query) |
518 | return { total: count, data: rows } | 528 | ]).then(([ total, data ]) => ({ total, data })) |
519 | }) | ||
520 | } | 529 | } |
521 | 530 | ||
522 | static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> { | 531 | static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> { |
@@ -565,7 +574,10 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
565 | transaction: t | 574 | transaction: t |
566 | } | 575 | } |
567 | 576 | ||
568 | return VideoCommentModel.findAndCountAll<MComment>(query) | 577 | return Promise.all([ |
578 | VideoCommentModel.count(query), | ||
579 | VideoCommentModel.findAll<MComment>(query) | ||
580 | ]).then(([ total, data ]) => ({ total, data })) | ||
569 | } | 581 | } |
570 | 582 | ||
571 | static async listForFeed (parameters: { | 583 | static async listForFeed (parameters: { |
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index 5d2b230e8..1d8296060 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts | |||
@@ -155,13 +155,10 @@ export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportMo | |||
155 | where | 155 | where |
156 | } | 156 | } |
157 | 157 | ||
158 | return VideoImportModel.findAndCountAll<MVideoImportDefault>(query) | 158 | return Promise.all([ |
159 | .then(({ rows, count }) => { | 159 | VideoImportModel.unscoped().count(query), |
160 | return { | 160 | VideoImportModel.findAll<MVideoImportDefault>(query) |
161 | data: rows, | 161 | ]).then(([ total, data ]) => ({ total, data })) |
162 | total: count | ||
163 | } | ||
164 | }) | ||
165 | } | 162 | } |
166 | 163 | ||
167 | getTargetIdentifier () { | 164 | getTargetIdentifier () { |
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index e20e32f8b..4e4160818 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts | |||
@@ -23,6 +23,7 @@ import { | |||
23 | MVideoPlaylistElementVideoUrlPlaylistPrivacy, | 23 | MVideoPlaylistElementVideoUrlPlaylistPrivacy, |
24 | MVideoPlaylistVideoThumbnail | 24 | MVideoPlaylistVideoThumbnail |
25 | } from '@server/types/models/video/video-playlist-element' | 25 | } from '@server/types/models/video/video-playlist-element' |
26 | import { AttributesOnly } from '@shared/typescript-utils' | ||
26 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | 27 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' |
27 | import { VideoPrivacy } from '../../../shared/models/videos' | 28 | import { VideoPrivacy } from '../../../shared/models/videos' |
28 | import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model' | 29 | import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model' |
@@ -32,7 +33,6 @@ import { AccountModel } from '../account/account' | |||
32 | import { getSort, throwIfNotValid } from '../utils' | 33 | import { getSort, throwIfNotValid } from '../utils' |
33 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' | 34 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' |
34 | import { VideoPlaylistModel } from './video-playlist' | 35 | import { VideoPlaylistModel } from './video-playlist' |
35 | import { AttributesOnly } from '@shared/typescript-utils' | ||
36 | 36 | ||
37 | @Table({ | 37 | @Table({ |
38 | tableName: 'videoPlaylistElement', | 38 | tableName: 'videoPlaylistElement', |
@@ -208,22 +208,28 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide | |||
208 | } | 208 | } |
209 | 209 | ||
210 | static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Transaction) { | 210 | static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Transaction) { |
211 | const query = { | 211 | const getQuery = (forCount: boolean) => { |
212 | attributes: [ 'url' ], | 212 | return { |
213 | offset: start, | 213 | attributes: forCount |
214 | limit: count, | 214 | ? [] |
215 | order: getSort('position'), | 215 | : [ 'url' ], |
216 | where: { | 216 | offset: start, |
217 | videoPlaylistId | 217 | limit: count, |
218 | }, | 218 | order: getSort('position'), |
219 | transaction: t | 219 | where: { |
220 | videoPlaylistId | ||
221 | }, | ||
222 | transaction: t | ||
223 | } | ||
220 | } | 224 | } |
221 | 225 | ||
222 | return VideoPlaylistElementModel | 226 | return Promise.all([ |
223 | .findAndCountAll(query) | 227 | VideoPlaylistElementModel.count(getQuery(true)), |
224 | .then(({ rows, count }) => { | 228 | VideoPlaylistElementModel.findAll(getQuery(false)) |
225 | return { total: count, data: rows.map(e => e.url) } | 229 | ]).then(([ total, rows ]) => ({ |
226 | }) | 230 | total, |
231 | data: rows.map(e => e.url) | ||
232 | })) | ||
227 | } | 233 | } |
228 | 234 | ||
229 | static loadFirstElementWithVideoThumbnail (videoPlaylistId: number): Promise<MVideoPlaylistVideoThumbnail> { | 235 | static loadFirstElementWithVideoThumbnail (videoPlaylistId: number): Promise<MVideoPlaylistVideoThumbnail> { |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index c125db3ff..ae5e237ec 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { FindOptions, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' | 2 | import { FindOptions, Includeable, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
3 | import { | 3 | import { |
4 | AllowNull, | 4 | AllowNull, |
5 | BelongsTo, | 5 | BelongsTo, |
@@ -86,6 +86,7 @@ type AvailableForListOptions = { | |||
86 | host?: string | 86 | host?: string |
87 | uuids?: string[] | 87 | uuids?: string[] |
88 | withVideos?: boolean | 88 | withVideos?: boolean |
89 | forCount?: boolean | ||
89 | } | 90 | } |
90 | 91 | ||
91 | function getVideoLengthSelect () { | 92 | function getVideoLengthSelect () { |
@@ -239,23 +240,28 @@ function getVideoLengthSelect () { | |||
239 | [Op.and]: whereAnd | 240 | [Op.and]: whereAnd |
240 | } | 241 | } |
241 | 242 | ||
243 | const include: Includeable[] = [ | ||
244 | { | ||
245 | model: AccountModel.scope({ | ||
246 | method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer, forCount: options.forCount } as SummaryOptions ] | ||
247 | }), | ||
248 | required: true | ||
249 | } | ||
250 | ] | ||
251 | |||
252 | if (options.forCount !== true) { | ||
253 | include.push({ | ||
254 | model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY), | ||
255 | required: false | ||
256 | }) | ||
257 | } | ||
258 | |||
242 | return { | 259 | return { |
243 | attributes: { | 260 | attributes: { |
244 | include: attributesInclude | 261 | include: attributesInclude |
245 | }, | 262 | }, |
246 | where, | 263 | where, |
247 | include: [ | 264 | include |
248 | { | ||
249 | model: AccountModel.scope({ | ||
250 | method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer } as SummaryOptions ] | ||
251 | }), | ||
252 | required: true | ||
253 | }, | ||
254 | { | ||
255 | model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY), | ||
256 | required: false | ||
257 | } | ||
258 | ] | ||
259 | } as FindOptions | 265 | } as FindOptions |
260 | } | 266 | } |
261 | })) | 267 | })) |
@@ -369,12 +375,23 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
369 | order: getPlaylistSort(options.sort) | 375 | order: getPlaylistSort(options.sort) |
370 | } | 376 | } |
371 | 377 | ||
372 | const scopes: (string | ScopeOptions)[] = [ | 378 | const commonAvailableForListOptions = pick(options, [ |
379 | 'type', | ||
380 | 'followerActorId', | ||
381 | 'accountId', | ||
382 | 'videoChannelId', | ||
383 | 'listMyPlaylists', | ||
384 | 'search', | ||
385 | 'host', | ||
386 | 'uuids' | ||
387 | ]) | ||
388 | |||
389 | const scopesFind: (string | ScopeOptions)[] = [ | ||
373 | { | 390 | { |
374 | method: [ | 391 | method: [ |
375 | ScopeNames.AVAILABLE_FOR_LIST, | 392 | ScopeNames.AVAILABLE_FOR_LIST, |
376 | { | 393 | { |
377 | ...pick(options, [ 'type', 'followerActorId', 'accountId', 'videoChannelId', 'listMyPlaylists', 'search', 'host', 'uuids' ]), | 394 | ...commonAvailableForListOptions, |
378 | 395 | ||
379 | withVideos: options.withVideos || false | 396 | withVideos: options.withVideos || false |
380 | } as AvailableForListOptions | 397 | } as AvailableForListOptions |
@@ -384,12 +401,26 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
384 | ScopeNames.WITH_THUMBNAIL | 401 | ScopeNames.WITH_THUMBNAIL |
385 | ] | 402 | ] |
386 | 403 | ||
387 | return VideoPlaylistModel | 404 | const scopesCount: (string | ScopeOptions)[] = [ |
388 | .scope(scopes) | 405 | { |
389 | .findAndCountAll(query) | 406 | method: [ |
390 | .then(({ rows, count }) => { | 407 | ScopeNames.AVAILABLE_FOR_LIST, |
391 | return { total: count, data: rows } | 408 | |
392 | }) | 409 | { |
410 | ...commonAvailableForListOptions, | ||
411 | |||
412 | withVideos: options.withVideos || false, | ||
413 | forCount: true | ||
414 | } as AvailableForListOptions | ||
415 | ] | ||
416 | }, | ||
417 | ScopeNames.WITH_VIDEOS_LENGTH | ||
418 | ] | ||
419 | |||
420 | return Promise.all([ | ||
421 | VideoPlaylistModel.scope(scopesCount).count(), | ||
422 | VideoPlaylistModel.scope(scopesFind).findAll(query) | ||
423 | ]).then(([ count, rows ]) => ({ total: count, data: rows })) | ||
393 | } | 424 | } |
394 | 425 | ||
395 | static searchForApi (options: Pick<AvailableForListOptions, 'followerActorId' | 'search'| 'host'| 'uuids'> & { | 426 | static searchForApi (options: Pick<AvailableForListOptions, 'followerActorId' | 'search'| 'host'| 'uuids'> & { |
@@ -419,17 +450,24 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
419 | Object.assign(where, { videoChannelId: options.channel.id }) | 450 | Object.assign(where, { videoChannelId: options.channel.id }) |
420 | } | 451 | } |
421 | 452 | ||
422 | const query = { | 453 | const getQuery = (forCount: boolean) => { |
423 | attributes: [ 'url' ], | 454 | return { |
424 | offset: start, | 455 | attributes: forCount === true |
425 | limit: count, | 456 | ? [] |
426 | where | 457 | : [ 'url' ], |
458 | offset: start, | ||
459 | limit: count, | ||
460 | where | ||
461 | } | ||
427 | } | 462 | } |
428 | 463 | ||
429 | return VideoPlaylistModel.findAndCountAll(query) | 464 | return Promise.all([ |
430 | .then(({ rows, count }) => { | 465 | VideoPlaylistModel.count(getQuery(true)), |
431 | return { total: count, data: rows.map(p => p.url) } | 466 | VideoPlaylistModel.findAll(getQuery(false)) |
432 | }) | 467 | ]).then(([ total, rows ]) => ({ |
468 | total, | ||
469 | data: rows.map(p => p.url) | ||
470 | })) | ||
433 | } | 471 | } |
434 | 472 | ||
435 | static listPlaylistIdsOf (accountId: number, videoIds: number[]): Promise<MVideoPlaylistIdWithElements[]> { | 473 | static listPlaylistIdsOf (accountId: number, videoIds: number[]): Promise<MVideoPlaylistIdWithElements[]> { |
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index f6659b992..ad95dec6e 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts | |||
@@ -183,7 +183,10 @@ export class VideoShareModel extends Model<Partial<AttributesOnly<VideoShareMode | |||
183 | transaction: t | 183 | transaction: t |
184 | } | 184 | } |
185 | 185 | ||
186 | return VideoShareModel.findAndCountAll(query) | 186 | return Promise.all([ |
187 | VideoShareModel.count(query), | ||
188 | VideoShareModel.findAll(query) | ||
189 | ]).then(([ total, data ]) => ({ total, data })) | ||
187 | } | 190 | } |
188 | 191 | ||
189 | static listRemoteShareUrlsOfLocalVideos () { | 192 | static listRemoteShareUrlsOfLocalVideos () { |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 9111c71b0..5536334eb 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -114,9 +114,13 @@ import { | |||
114 | videoModelToFormattedJSON | 114 | videoModelToFormattedJSON |
115 | } from './formatter/video-format-utils' | 115 | } from './formatter/video-format-utils' |
116 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | 116 | import { ScheduleVideoUpdateModel } from './schedule-video-update' |
117 | import { VideoModelGetQueryBuilder } from './sql/video-model-get-query-builder' | 117 | import { |
118 | import { BuildVideosListQueryOptions, DisplayOnlyForFollowerOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder' | 118 | BuildVideosListQueryOptions, |
119 | import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder' | 119 | DisplayOnlyForFollowerOptions, |
120 | VideoModelGetQueryBuilder, | ||
121 | VideosIdListQueryBuilder, | ||
122 | VideosModelListQueryBuilder | ||
123 | } from './sql/video' | ||
120 | import { TagModel } from './tag' | 124 | import { TagModel } from './tag' |
121 | import { ThumbnailModel } from './thumbnail' | 125 | import { ThumbnailModel } from './thumbnail' |
122 | import { VideoBlacklistModel } from './video-blacklist' | 126 | import { VideoBlacklistModel } from './video-blacklist' |
@@ -229,8 +233,8 @@ export type ForAPIOptions = { | |||
229 | required: false | 233 | required: false |
230 | }, | 234 | }, |
231 | { | 235 | { |
232 | model: ActorImageModel.unscoped(), | 236 | model: ActorImageModel, |
233 | as: 'Avatar', | 237 | as: 'Avatars', |
234 | required: false | 238 | required: false |
235 | } | 239 | } |
236 | ] | 240 | ] |
@@ -252,8 +256,8 @@ export type ForAPIOptions = { | |||
252 | required: false | 256 | required: false |
253 | }, | 257 | }, |
254 | { | 258 | { |
255 | model: ActorImageModel.unscoped(), | 259 | model: ActorImageModel, |
256 | as: 'Avatar', | 260 | as: 'Avatars', |
257 | required: false | 261 | required: false |
258 | } | 262 | } |
259 | ] | 263 | ] |
diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts index 1e9732fe9..5c2650fac 100644 --- a/server/tests/api/check-params/video-channels.ts +++ b/server/tests/api/check-params/video-channels.ts | |||
@@ -228,7 +228,7 @@ describe('Test video channels API validator', function () { | |||
228 | }) | 228 | }) |
229 | }) | 229 | }) |
230 | 230 | ||
231 | describe('When updating video channel avatar/banner', function () { | 231 | describe('When updating video channel avatars/banners', function () { |
232 | const types = [ 'avatar', 'banner' ] | 232 | const types = [ 'avatar', 'banner' ] |
233 | let path: string | 233 | let path: string |
234 | 234 | ||
diff --git a/server/tests/api/moderation/abuses.ts b/server/tests/api/moderation/abuses.ts index 0c3bed3e7..7bf49c7ec 100644 --- a/server/tests/api/moderation/abuses.ts +++ b/server/tests/api/moderation/abuses.ts | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { AbuseMessage, AbusePredefinedReasonsString, AbuseState, AdminAbuse, UserAbuse } from '@shared/models' | ||
5 | import { | 6 | import { |
6 | AbusesCommand, | 7 | AbusesCommand, |
7 | cleanupTests, | 8 | cleanupTests, |
@@ -9,9 +10,10 @@ import { | |||
9 | doubleFollow, | 10 | doubleFollow, |
10 | PeerTubeServer, | 11 | PeerTubeServer, |
11 | setAccessTokensToServers, | 12 | setAccessTokensToServers, |
13 | setDefaultAccountAvatar, | ||
14 | setDefaultChannelAvatar, | ||
12 | waitJobs | 15 | waitJobs |
13 | } from '@shared/server-commands' | 16 | } from '@shared/server-commands' |
14 | import { AbuseMessage, AbusePredefinedReasonsString, AbuseState, AdminAbuse, UserAbuse } from '@shared/models' | ||
15 | 17 | ||
16 | const expect = chai.expect | 18 | const expect = chai.expect |
17 | 19 | ||
@@ -27,8 +29,9 @@ describe('Test abuses', function () { | |||
27 | // Run servers | 29 | // Run servers |
28 | servers = await createMultipleServers(2) | 30 | servers = await createMultipleServers(2) |
29 | 31 | ||
30 | // Get the access tokens | ||
31 | await setAccessTokensToServers(servers) | 32 | await setAccessTokensToServers(servers) |
33 | await setDefaultChannelAvatar(servers) | ||
34 | await setDefaultAccountAvatar(servers) | ||
32 | 35 | ||
33 | // Server 1 and server 2 follow each other | 36 | // Server 1 and server 2 follow each other |
34 | await doubleFollow(servers[0], servers[1]) | 37 | await doubleFollow(servers[0], servers[1]) |
diff --git a/server/tests/api/moderation/blocklist.ts b/server/tests/api/moderation/blocklist.ts index b45460bb4..e1344a245 100644 --- a/server/tests/api/moderation/blocklist.ts +++ b/server/tests/api/moderation/blocklist.ts | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { UserNotificationType } from '@shared/models' | ||
5 | import { | 6 | import { |
6 | BlocklistCommand, | 7 | BlocklistCommand, |
7 | cleanupTests, | 8 | cleanupTests, |
@@ -10,9 +11,9 @@ import { | |||
10 | doubleFollow, | 11 | doubleFollow, |
11 | PeerTubeServer, | 12 | PeerTubeServer, |
12 | setAccessTokensToServers, | 13 | setAccessTokensToServers, |
14 | setDefaultAccountAvatar, | ||
13 | waitJobs | 15 | waitJobs |
14 | } from '@shared/server-commands' | 16 | } from '@shared/server-commands' |
15 | import { UserNotificationType } from '@shared/models' | ||
16 | 17 | ||
17 | const expect = chai.expect | 18 | const expect = chai.expect |
18 | 19 | ||
@@ -79,6 +80,7 @@ describe('Test blocklist', function () { | |||
79 | 80 | ||
80 | servers = await createMultipleServers(3) | 81 | servers = await createMultipleServers(3) |
81 | await setAccessTokensToServers(servers) | 82 | await setAccessTokensToServers(servers) |
83 | await setDefaultAccountAvatar(servers) | ||
82 | 84 | ||
83 | command = servers[0].blocklist | 85 | command = servers[0].blocklist |
84 | commentsCommand = servers.map(s => s.comments) | 86 | commentsCommand = servers.map(s => s.comments) |
diff --git a/server/tests/api/moderation/video-blacklist.ts b/server/tests/api/moderation/video-blacklist.ts index 3e7f2ba33..1790210ff 100644 --- a/server/tests/api/moderation/video-blacklist.ts +++ b/server/tests/api/moderation/video-blacklist.ts | |||
@@ -13,6 +13,7 @@ import { | |||
13 | killallServers, | 13 | killallServers, |
14 | PeerTubeServer, | 14 | PeerTubeServer, |
15 | setAccessTokensToServers, | 15 | setAccessTokensToServers, |
16 | setDefaultChannelAvatar, | ||
16 | waitJobs | 17 | waitJobs |
17 | } from '@shared/server-commands' | 18 | } from '@shared/server-commands' |
18 | 19 | ||
@@ -42,6 +43,7 @@ describe('Test video blacklist', function () { | |||
42 | 43 | ||
43 | // Server 1 and server 2 follow each other | 44 | // Server 1 and server 2 follow each other |
44 | await doubleFollow(servers[0], servers[1]) | 45 | await doubleFollow(servers[0], servers[1]) |
46 | await setDefaultChannelAvatar(servers[0]) | ||
45 | 47 | ||
46 | // Upload 2 videos on server 2 | 48 | // Upload 2 videos on server 2 |
47 | await servers[1].videos.upload({ attributes: { name: 'My 1st video', description: 'A video on server 2' } }) | 49 | await servers[1].videos.upload({ attributes: { name: 'My 1st video', description: 'A video on server 2' } }) |
diff --git a/server/tests/api/notifications/notifications-api.ts b/server/tests/api/notifications/notifications-api.ts index ac08449f8..78864c8a0 100644 --- a/server/tests/api/notifications/notifications-api.ts +++ b/server/tests/api/notifications/notifications-api.ts | |||
@@ -38,6 +38,16 @@ describe('Test notifications API', function () { | |||
38 | await waitJobs([ server ]) | 38 | await waitJobs([ server ]) |
39 | }) | 39 | }) |
40 | 40 | ||
41 | describe('Notification list & count', function () { | ||
42 | |||
43 | it('Should correctly list notifications', async function () { | ||
44 | const { data, total } = await server.notifications.list({ token: userToken, start: 0, count: 2 }) | ||
45 | |||
46 | expect(data).to.have.lengthOf(2) | ||
47 | expect(total).to.equal(10) | ||
48 | }) | ||
49 | }) | ||
50 | |||
41 | describe('Mark as read', function () { | 51 | describe('Mark as read', function () { |
42 | 52 | ||
43 | it('Should mark as read some notifications', async function () { | 53 | it('Should mark as read some notifications', async function () { |
diff --git a/server/tests/api/search/search-activitypub-video-channels.ts b/server/tests/api/search/search-activitypub-video-channels.ts index 2e0abc6ba..5f5322d03 100644 --- a/server/tests/api/search/search-activitypub-video-channels.ts +++ b/server/tests/api/search/search-activitypub-video-channels.ts | |||
@@ -10,6 +10,8 @@ import { | |||
10 | PeerTubeServer, | 10 | PeerTubeServer, |
11 | SearchCommand, | 11 | SearchCommand, |
12 | setAccessTokensToServers, | 12 | setAccessTokensToServers, |
13 | setDefaultAccountAvatar, | ||
14 | setDefaultVideoChannel, | ||
13 | waitJobs | 15 | waitJobs |
14 | } from '@shared/server-commands' | 16 | } from '@shared/server-commands' |
15 | 17 | ||
@@ -28,6 +30,8 @@ describe('Test ActivityPub video channels search', function () { | |||
28 | servers = await createMultipleServers(2) | 30 | servers = await createMultipleServers(2) |
29 | 31 | ||
30 | await setAccessTokensToServers(servers) | 32 | await setAccessTokensToServers(servers) |
33 | await setDefaultVideoChannel(servers) | ||
34 | await setDefaultAccountAvatar(servers) | ||
31 | 35 | ||
32 | { | 36 | { |
33 | await servers[0].users.create({ username: 'user1_server1', password: 'password' }) | 37 | await servers[0].users.create({ username: 'user1_server1', password: 'password' }) |
diff --git a/server/tests/api/search/search-activitypub-video-playlists.ts b/server/tests/api/search/search-activitypub-video-playlists.ts index d9243ac53..b9a424292 100644 --- a/server/tests/api/search/search-activitypub-video-playlists.ts +++ b/server/tests/api/search/search-activitypub-video-playlists.ts | |||
@@ -10,6 +10,7 @@ import { | |||
10 | PeerTubeServer, | 10 | PeerTubeServer, |
11 | SearchCommand, | 11 | SearchCommand, |
12 | setAccessTokensToServers, | 12 | setAccessTokensToServers, |
13 | setDefaultAccountAvatar, | ||
13 | setDefaultVideoChannel, | 14 | setDefaultVideoChannel, |
14 | waitJobs | 15 | waitJobs |
15 | } from '@shared/server-commands' | 16 | } from '@shared/server-commands' |
@@ -31,6 +32,7 @@ describe('Test ActivityPub playlists search', function () { | |||
31 | 32 | ||
32 | await setAccessTokensToServers(servers) | 33 | await setAccessTokensToServers(servers) |
33 | await setDefaultVideoChannel(servers) | 34 | await setDefaultVideoChannel(servers) |
35 | await setDefaultAccountAvatar(servers) | ||
34 | 36 | ||
35 | { | 37 | { |
36 | const video1 = (await servers[0].videos.quickUpload({ name: 'video 1' })).uuid | 38 | const video1 = (await servers[0].videos.quickUpload({ name: 'video 1' })).uuid |
diff --git a/server/tests/api/search/search-activitypub-videos.ts b/server/tests/api/search/search-activitypub-videos.ts index 60b95ae4c..20249b1f1 100644 --- a/server/tests/api/search/search-activitypub-videos.ts +++ b/server/tests/api/search/search-activitypub-videos.ts | |||
@@ -10,6 +10,8 @@ import { | |||
10 | PeerTubeServer, | 10 | PeerTubeServer, |
11 | SearchCommand, | 11 | SearchCommand, |
12 | setAccessTokensToServers, | 12 | setAccessTokensToServers, |
13 | setDefaultAccountAvatar, | ||
14 | setDefaultVideoChannel, | ||
13 | waitJobs | 15 | waitJobs |
14 | } from '@shared/server-commands' | 16 | } from '@shared/server-commands' |
15 | 17 | ||
@@ -28,6 +30,8 @@ describe('Test ActivityPub videos search', function () { | |||
28 | servers = await createMultipleServers(2) | 30 | servers = await createMultipleServers(2) |
29 | 31 | ||
30 | await setAccessTokensToServers(servers) | 32 | await setAccessTokensToServers(servers) |
33 | await setDefaultVideoChannel(servers) | ||
34 | await setDefaultAccountAvatar(servers) | ||
31 | 35 | ||
32 | { | 36 | { |
33 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 1 on server 1' } }) | 37 | const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 1 on server 1' } }) |
diff --git a/server/tests/api/search/search-channels.ts b/server/tests/api/search/search-channels.ts index 8a92def61..0073c71e1 100644 --- a/server/tests/api/search/search-channels.ts +++ b/server/tests/api/search/search-channels.ts | |||
@@ -2,15 +2,17 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { VideoChannel } from '@shared/models' | ||
5 | import { | 6 | import { |
6 | cleanupTests, | 7 | cleanupTests, |
7 | createSingleServer, | 8 | createSingleServer, |
8 | doubleFollow, | 9 | doubleFollow, |
9 | PeerTubeServer, | 10 | PeerTubeServer, |
10 | SearchCommand, | 11 | SearchCommand, |
11 | setAccessTokensToServers | 12 | setAccessTokensToServers, |
13 | setDefaultAccountAvatar, | ||
14 | setDefaultChannelAvatar | ||
12 | } from '@shared/server-commands' | 15 | } from '@shared/server-commands' |
13 | import { VideoChannel } from '@shared/models' | ||
14 | 16 | ||
15 | const expect = chai.expect | 17 | const expect = chai.expect |
16 | 18 | ||
@@ -30,6 +32,8 @@ describe('Test channels search', function () { | |||
30 | remoteServer = servers[1] | 32 | remoteServer = servers[1] |
31 | 33 | ||
32 | await setAccessTokensToServers([ server, remoteServer ]) | 34 | await setAccessTokensToServers([ server, remoteServer ]) |
35 | await setDefaultChannelAvatar(server) | ||
36 | await setDefaultAccountAvatar(server) | ||
33 | 37 | ||
34 | { | 38 | { |
35 | await server.users.create({ username: 'user1' }) | 39 | await server.users.create({ username: 'user1' }) |
diff --git a/server/tests/api/search/search-index.ts b/server/tests/api/search/search-index.ts index f84d03345..ae933449f 100644 --- a/server/tests/api/search/search-index.ts +++ b/server/tests/api/search/search-index.ts | |||
@@ -14,7 +14,7 @@ import { | |||
14 | 14 | ||
15 | const expect = chai.expect | 15 | const expect = chai.expect |
16 | 16 | ||
17 | describe('Test videos search', function () { | 17 | describe('Test index search', function () { |
18 | const localVideoName = 'local video' + new Date().toISOString() | 18 | const localVideoName = 'local video' + new Date().toISOString() |
19 | 19 | ||
20 | let server: PeerTubeServer = null | 20 | let server: PeerTubeServer = null |
@@ -134,12 +134,16 @@ describe('Test videos search', function () { | |||
134 | expect(video.account.host).to.equal('framatube.org') | 134 | expect(video.account.host).to.equal('framatube.org') |
135 | expect(video.account.name).to.equal('framasoft') | 135 | expect(video.account.name).to.equal('framasoft') |
136 | expect(video.account.url).to.equal('https://framatube.org/accounts/framasoft') | 136 | expect(video.account.url).to.equal('https://framatube.org/accounts/framasoft') |
137 | // TODO: remove, deprecated in 4.2 | ||
137 | expect(video.account.avatar).to.exist | 138 | expect(video.account.avatar).to.exist |
139 | expect(video.account.avatars.length).to.equal(1, 'Account should have one avatar image') | ||
138 | 140 | ||
139 | expect(video.channel.host).to.equal('framatube.org') | 141 | expect(video.channel.host).to.equal('framatube.org') |
140 | expect(video.channel.name).to.equal('joinpeertube') | 142 | expect(video.channel.name).to.equal('joinpeertube') |
141 | expect(video.channel.url).to.equal('https://framatube.org/video-channels/joinpeertube') | 143 | expect(video.channel.url).to.equal('https://framatube.org/video-channels/joinpeertube') |
144 | // TODO: remove, deprecated in 4.2 | ||
142 | expect(video.channel.avatar).to.exist | 145 | expect(video.channel.avatar).to.exist |
146 | expect(video.channel.avatars.length).to.equal(1, 'Channel should have one avatar image') | ||
143 | } | 147 | } |
144 | 148 | ||
145 | const baseSearch: VideosSearchQuery = { | 149 | const baseSearch: VideosSearchQuery = { |
@@ -316,13 +320,17 @@ describe('Test videos search', function () { | |||
316 | const videoChannel = body.data[0] | 320 | const videoChannel = body.data[0] |
317 | expect(videoChannel.url).to.equal('https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8') | 321 | expect(videoChannel.url).to.equal('https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8') |
318 | expect(videoChannel.host).to.equal('framatube.org') | 322 | expect(videoChannel.host).to.equal('framatube.org') |
323 | // TODO: remove, deprecated in 4.2 | ||
319 | expect(videoChannel.avatar).to.exist | 324 | expect(videoChannel.avatar).to.exist |
325 | expect(videoChannel.avatars.length).to.equal(1, 'Channel should have two avatar images') | ||
320 | expect(videoChannel.displayName).to.exist | 326 | expect(videoChannel.displayName).to.exist |
321 | 327 | ||
322 | expect(videoChannel.ownerAccount.url).to.equal('https://framatube.org/accounts/framasoft') | 328 | expect(videoChannel.ownerAccount.url).to.equal('https://framatube.org/accounts/framasoft') |
323 | expect(videoChannel.ownerAccount.name).to.equal('framasoft') | 329 | expect(videoChannel.ownerAccount.name).to.equal('framasoft') |
324 | expect(videoChannel.ownerAccount.host).to.equal('framatube.org') | 330 | expect(videoChannel.ownerAccount.host).to.equal('framatube.org') |
331 | // TODO: remove, deprecated in 4.2 | ||
325 | expect(videoChannel.ownerAccount.avatar).to.exist | 332 | expect(videoChannel.ownerAccount.avatar).to.exist |
333 | expect(videoChannel.ownerAccount.avatars.length).to.equal(1, 'Account should have two avatar images') | ||
326 | } | 334 | } |
327 | 335 | ||
328 | it('Should make a simple search and not have results', async function () { | 336 | it('Should make a simple search and not have results', async function () { |
@@ -388,12 +396,16 @@ describe('Test videos search', function () { | |||
388 | expect(videoPlaylist.ownerAccount.url).to.equal('https://peertube2.cpy.re/accounts/chocobozzz') | 396 | expect(videoPlaylist.ownerAccount.url).to.equal('https://peertube2.cpy.re/accounts/chocobozzz') |
389 | expect(videoPlaylist.ownerAccount.name).to.equal('chocobozzz') | 397 | expect(videoPlaylist.ownerAccount.name).to.equal('chocobozzz') |
390 | expect(videoPlaylist.ownerAccount.host).to.equal('peertube2.cpy.re') | 398 | expect(videoPlaylist.ownerAccount.host).to.equal('peertube2.cpy.re') |
399 | // TODO: remove, deprecated in 4.2 | ||
391 | expect(videoPlaylist.ownerAccount.avatar).to.exist | 400 | expect(videoPlaylist.ownerAccount.avatar).to.exist |
401 | expect(videoPlaylist.ownerAccount.avatars.length).to.equal(1, 'Account should have two avatar images') | ||
392 | 402 | ||
393 | expect(videoPlaylist.videoChannel.url).to.equal('https://peertube2.cpy.re/video-channels/chocobozzz_channel') | 403 | expect(videoPlaylist.videoChannel.url).to.equal('https://peertube2.cpy.re/video-channels/chocobozzz_channel') |
394 | expect(videoPlaylist.videoChannel.name).to.equal('chocobozzz_channel') | 404 | expect(videoPlaylist.videoChannel.name).to.equal('chocobozzz_channel') |
395 | expect(videoPlaylist.videoChannel.host).to.equal('peertube2.cpy.re') | 405 | expect(videoPlaylist.videoChannel.host).to.equal('peertube2.cpy.re') |
406 | // TODO: remove, deprecated in 4.2 | ||
396 | expect(videoPlaylist.videoChannel.avatar).to.exist | 407 | expect(videoPlaylist.videoChannel.avatar).to.exist |
408 | expect(videoPlaylist.videoChannel.avatars.length).to.equal(1, 'Channel should have two avatar images') | ||
397 | } | 409 | } |
398 | 410 | ||
399 | it('Should make a simple search and not have results', async function () { | 411 | it('Should make a simple search and not have results', async function () { |
diff --git a/server/tests/api/search/search-playlists.ts b/server/tests/api/search/search-playlists.ts index 1e9c8d4bb..fcf2f2ee2 100644 --- a/server/tests/api/search/search-playlists.ts +++ b/server/tests/api/search/search-playlists.ts | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { VideoPlaylistPrivacy } from '@shared/models' | ||
5 | import { | 6 | import { |
6 | cleanupTests, | 7 | cleanupTests, |
7 | createSingleServer, | 8 | createSingleServer, |
@@ -9,9 +10,10 @@ import { | |||
9 | PeerTubeServer, | 10 | PeerTubeServer, |
10 | SearchCommand, | 11 | SearchCommand, |
11 | setAccessTokensToServers, | 12 | setAccessTokensToServers, |
13 | setDefaultAccountAvatar, | ||
14 | setDefaultChannelAvatar, | ||
12 | setDefaultVideoChannel | 15 | setDefaultVideoChannel |
13 | } from '@shared/server-commands' | 16 | } from '@shared/server-commands' |
14 | import { VideoPlaylistPrivacy } from '@shared/models' | ||
15 | 17 | ||
16 | const expect = chai.expect | 18 | const expect = chai.expect |
17 | 19 | ||
@@ -34,6 +36,8 @@ describe('Test playlists search', function () { | |||
34 | 36 | ||
35 | await setAccessTokensToServers([ remoteServer, server ]) | 37 | await setAccessTokensToServers([ remoteServer, server ]) |
36 | await setDefaultVideoChannel([ remoteServer, server ]) | 38 | await setDefaultVideoChannel([ remoteServer, server ]) |
39 | await setDefaultChannelAvatar([ remoteServer, server ]) | ||
40 | await setDefaultAccountAvatar([ remoteServer, server ]) | ||
37 | 41 | ||
38 | { | 42 | { |
39 | const videoId = (await server.videos.upload()).uuid | 43 | const videoId = (await server.videos.upload()).uuid |
diff --git a/server/tests/api/search/search-videos.ts b/server/tests/api/search/search-videos.ts index c544705d3..ff4c3c161 100644 --- a/server/tests/api/search/search-videos.ts +++ b/server/tests/api/search/search-videos.ts | |||
@@ -2,6 +2,8 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { wait } from '@shared/core-utils' | ||
6 | import { VideoPrivacy } from '@shared/models' | ||
5 | import { | 7 | import { |
6 | cleanupTests, | 8 | cleanupTests, |
7 | createSingleServer, | 9 | createSingleServer, |
@@ -9,11 +11,11 @@ import { | |||
9 | PeerTubeServer, | 11 | PeerTubeServer, |
10 | SearchCommand, | 12 | SearchCommand, |
11 | setAccessTokensToServers, | 13 | setAccessTokensToServers, |
14 | setDefaultAccountAvatar, | ||
15 | setDefaultChannelAvatar, | ||
12 | setDefaultVideoChannel, | 16 | setDefaultVideoChannel, |
13 | stopFfmpeg | 17 | stopFfmpeg |
14 | } from '@shared/server-commands' | 18 | } from '@shared/server-commands' |
15 | import { VideoPrivacy } from '@shared/models' | ||
16 | import { wait } from '@shared/core-utils' | ||
17 | 19 | ||
18 | const expect = chai.expect | 20 | const expect = chai.expect |
19 | 21 | ||
@@ -38,6 +40,8 @@ describe('Test videos search', function () { | |||
38 | 40 | ||
39 | await setAccessTokensToServers([ server, remoteServer ]) | 41 | await setAccessTokensToServers([ server, remoteServer ]) |
40 | await setDefaultVideoChannel([ server, remoteServer ]) | 42 | await setDefaultVideoChannel([ server, remoteServer ]) |
43 | await setDefaultChannelAvatar(server) | ||
44 | await setDefaultAccountAvatar(servers) | ||
41 | 45 | ||
42 | { | 46 | { |
43 | const attributes1 = { | 47 | const attributes1 = { |
diff --git a/server/tests/api/server/homepage.ts b/server/tests/api/server/homepage.ts index 552ee98cf..e7de6bfee 100644 --- a/server/tests/api/server/homepage.ts +++ b/server/tests/api/server/homepage.ts | |||
@@ -9,7 +9,9 @@ import { | |||
9 | CustomPagesCommand, | 9 | CustomPagesCommand, |
10 | killallServers, | 10 | killallServers, |
11 | PeerTubeServer, | 11 | PeerTubeServer, |
12 | setAccessTokensToServers | 12 | setAccessTokensToServers, |
13 | setDefaultAccountAvatar, | ||
14 | setDefaultChannelAvatar | ||
13 | } from '../../../../shared/server-commands/index' | 15 | } from '../../../../shared/server-commands/index' |
14 | 16 | ||
15 | const expect = chai.expect | 17 | const expect = chai.expect |
@@ -29,6 +31,8 @@ describe('Test instance homepage actions', function () { | |||
29 | 31 | ||
30 | server = await createSingleServer(1) | 32 | server = await createSingleServer(1) |
31 | await setAccessTokensToServers([ server ]) | 33 | await setAccessTokensToServers([ server ]) |
34 | await setDefaultChannelAvatar(server) | ||
35 | await setDefaultAccountAvatar(server) | ||
32 | 36 | ||
33 | command = server.customPage | 37 | command = server.customPage |
34 | }) | 38 | }) |
diff --git a/server/tests/api/users/user-subscriptions.ts b/server/tests/api/users/user-subscriptions.ts index 57cca6ad4..9553a69bb 100644 --- a/server/tests/api/users/user-subscriptions.ts +++ b/server/tests/api/users/user-subscriptions.ts | |||
@@ -9,6 +9,8 @@ import { | |||
9 | doubleFollow, | 9 | doubleFollow, |
10 | PeerTubeServer, | 10 | PeerTubeServer, |
11 | setAccessTokensToServers, | 11 | setAccessTokensToServers, |
12 | setDefaultAccountAvatar, | ||
13 | setDefaultChannelAvatar, | ||
12 | SubscriptionsCommand, | 14 | SubscriptionsCommand, |
13 | waitJobs | 15 | waitJobs |
14 | } from '@shared/server-commands' | 16 | } from '@shared/server-commands' |
@@ -29,6 +31,8 @@ describe('Test users subscriptions', function () { | |||
29 | 31 | ||
30 | // Get the access tokens | 32 | // Get the access tokens |
31 | await setAccessTokensToServers(servers) | 33 | await setAccessTokensToServers(servers) |
34 | await setDefaultChannelAvatar(servers) | ||
35 | await setDefaultAccountAvatar(servers) | ||
32 | 36 | ||
33 | // Server 1 and server 2 follow each other | 37 | // Server 1 and server 2 follow each other |
34 | await doubleFollow(servers[0], servers[1]) | 38 | await doubleFollow(servers[0], servers[1]) |
diff --git a/server/tests/api/users/users-multiple-servers.ts b/server/tests/api/users/users-multiple-servers.ts index 5b2bbc520..3e8b932c0 100644 --- a/server/tests/api/users/users-multiple-servers.ts +++ b/server/tests/api/users/users-multiple-servers.ts | |||
@@ -16,6 +16,7 @@ import { | |||
16 | doubleFollow, | 16 | doubleFollow, |
17 | PeerTubeServer, | 17 | PeerTubeServer, |
18 | setAccessTokensToServers, | 18 | setAccessTokensToServers, |
19 | setDefaultChannelAvatar, | ||
19 | waitJobs | 20 | waitJobs |
20 | } from '@shared/server-commands' | 21 | } from '@shared/server-commands' |
21 | 22 | ||
@@ -29,7 +30,7 @@ describe('Test users with multiple servers', function () { | |||
29 | 30 | ||
30 | let videoUUID: string | 31 | let videoUUID: string |
31 | let userAccessToken: string | 32 | let userAccessToken: string |
32 | let userAvatarFilename: string | 33 | let userAvatarFilenames: string[] |
33 | 34 | ||
34 | before(async function () { | 35 | before(async function () { |
35 | this.timeout(120_000) | 36 | this.timeout(120_000) |
@@ -38,6 +39,7 @@ describe('Test users with multiple servers', function () { | |||
38 | 39 | ||
39 | // Get the access tokens | 40 | // Get the access tokens |
40 | await setAccessTokensToServers(servers) | 41 | await setAccessTokensToServers(servers) |
42 | await setDefaultChannelAvatar(servers) | ||
41 | 43 | ||
42 | // Server 1 and server 2 follow each other | 44 | // Server 1 and server 2 follow each other |
43 | await doubleFollow(servers[0], servers[1]) | 45 | await doubleFollow(servers[0], servers[1]) |
@@ -97,9 +99,11 @@ describe('Test users with multiple servers', function () { | |||
97 | await servers[0].users.updateMyAvatar({ fixture }) | 99 | await servers[0].users.updateMyAvatar({ fixture }) |
98 | 100 | ||
99 | user = await servers[0].users.getMyInfo() | 101 | user = await servers[0].users.getMyInfo() |
100 | userAvatarFilename = user.account.avatar.path | 102 | userAvatarFilenames = user.account.avatars.map(({ path }) => path) |
101 | 103 | ||
102 | await testImage(servers[0].url, 'avatar2-resized', userAvatarFilename, '.png') | 104 | for (const avatar of user.account.avatars) { |
105 | await testImage(servers[0].url, `avatar2-resized-${avatar.width}x${avatar.width}`, avatar.path, '.png') | ||
106 | } | ||
103 | 107 | ||
104 | await waitJobs(servers) | 108 | await waitJobs(servers) |
105 | }) | 109 | }) |
@@ -129,7 +133,9 @@ describe('Test users with multiple servers', function () { | |||
129 | expect(account.userId).to.be.undefined | 133 | expect(account.userId).to.be.undefined |
130 | } | 134 | } |
131 | 135 | ||
132 | await testImage(server.url, 'avatar2-resized', account.avatar.path, '.png') | 136 | for (const avatar of account.avatars) { |
137 | await testImage(server.url, `avatar2-resized-${avatar.width}x${avatar.width}`, avatar.path, '.png') | ||
138 | } | ||
133 | } | 139 | } |
134 | }) | 140 | }) |
135 | 141 | ||
@@ -193,7 +199,9 @@ describe('Test users with multiple servers', function () { | |||
193 | 199 | ||
194 | it('Should not have actor files', async () => { | 200 | it('Should not have actor files', async () => { |
195 | for (const server of servers) { | 201 | for (const server of servers) { |
196 | await checkActorFilesWereRemoved(userAvatarFilename, server.internalServerNumber) | 202 | for (const userAvatarFilename of userAvatarFilenames) { |
203 | await checkActorFilesWereRemoved(userAvatarFilename, server.internalServerNumber) | ||
204 | } | ||
197 | } | 205 | } |
198 | }) | 206 | }) |
199 | 207 | ||
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 7023b3f08..a47713bf0 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -604,7 +604,9 @@ describe('Test users', function () { | |||
604 | await server.users.updateMyAvatar({ token: userToken, fixture }) | 604 | await server.users.updateMyAvatar({ token: userToken, fixture }) |
605 | 605 | ||
606 | const user = await server.users.getMyInfo({ token: userToken }) | 606 | const user = await server.users.getMyInfo({ token: userToken }) |
607 | await testImage(server.url, 'avatar-resized', user.account.avatar.path, '.gif') | 607 | for (const avatar of user.account.avatars) { |
608 | await testImage(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, '.gif') | ||
609 | } | ||
608 | }) | 610 | }) |
609 | 611 | ||
610 | it('Should be able to update my avatar with a gif, and then a png', async function () { | 612 | it('Should be able to update my avatar with a gif, and then a png', async function () { |
@@ -614,7 +616,9 @@ describe('Test users', function () { | |||
614 | await server.users.updateMyAvatar({ token: userToken, fixture }) | 616 | await server.users.updateMyAvatar({ token: userToken, fixture }) |
615 | 617 | ||
616 | const user = await server.users.getMyInfo({ token: userToken }) | 618 | const user = await server.users.getMyInfo({ token: userToken }) |
617 | await testImage(server.url, 'avatar-resized', user.account.avatar.path, extension) | 619 | for (const avatar of user.account.avatars) { |
620 | await testImage(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, extension) | ||
621 | } | ||
618 | } | 622 | } |
619 | }) | 623 | }) |
620 | 624 | ||
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index ecdd36613..5bbc60559 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts | |||
@@ -19,6 +19,8 @@ import { | |||
19 | doubleFollow, | 19 | doubleFollow, |
20 | PeerTubeServer, | 20 | PeerTubeServer, |
21 | setAccessTokensToServers, | 21 | setAccessTokensToServers, |
22 | setDefaultAccountAvatar, | ||
23 | setDefaultChannelAvatar, | ||
22 | waitJobs, | 24 | waitJobs, |
23 | webtorrentAdd | 25 | webtorrentAdd |
24 | } from '@shared/server-commands' | 26 | } from '@shared/server-commands' |
@@ -46,6 +48,9 @@ describe('Test multiple servers', function () { | |||
46 | description: 'super channel' | 48 | description: 'super channel' |
47 | } | 49 | } |
48 | await servers[0].channels.create({ attributes: videoChannel }) | 50 | await servers[0].channels.create({ attributes: videoChannel }) |
51 | await setDefaultChannelAvatar(servers[0], videoChannel.name) | ||
52 | await setDefaultAccountAvatar(servers) | ||
53 | |||
49 | const { data } = await servers[0].channels.list({ start: 0, count: 1 }) | 54 | const { data } = await servers[0].channels.list({ start: 0, count: 1 }) |
50 | videoChannelId = data[0].id | 55 | videoChannelId = data[0].id |
51 | } | 56 | } |
@@ -207,7 +212,7 @@ describe('Test multiple servers', function () { | |||
207 | }, | 212 | }, |
208 | { | 213 | { |
209 | resolution: 720, | 214 | resolution: 720, |
210 | size: 788000 | 215 | size: 750000 |
211 | } | 216 | } |
212 | ], | 217 | ], |
213 | thumbnailfile: 'thumbnail', | 218 | thumbnailfile: 'thumbnail', |
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts index 28bf018c5..d37043aef 100644 --- a/server/tests/api/videos/single-server.ts +++ b/server/tests/api/videos/single-server.ts | |||
@@ -5,7 +5,14 @@ import * as chai from 'chai' | |||
5 | import { checkVideoFilesWereRemoved, completeVideoCheck, testImage } from '@server/tests/shared' | 5 | import { checkVideoFilesWereRemoved, completeVideoCheck, testImage } from '@server/tests/shared' |
6 | import { wait } from '@shared/core-utils' | 6 | import { wait } from '@shared/core-utils' |
7 | import { Video, VideoPrivacy } from '@shared/models' | 7 | import { Video, VideoPrivacy } from '@shared/models' |
8 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | 8 | import { |
9 | cleanupTests, | ||
10 | createSingleServer, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultAccountAvatar, | ||
14 | setDefaultChannelAvatar | ||
15 | } from '@shared/server-commands' | ||
9 | 16 | ||
10 | const expect = chai.expect | 17 | const expect = chai.expect |
11 | 18 | ||
@@ -90,6 +97,8 @@ describe('Test a single server', function () { | |||
90 | server = await createSingleServer(1) | 97 | server = await createSingleServer(1) |
91 | 98 | ||
92 | await setAccessTokensToServers([ server ]) | 99 | await setAccessTokensToServers([ server ]) |
100 | await setDefaultChannelAvatar(server) | ||
101 | await setDefaultAccountAvatar(server) | ||
93 | }) | 102 | }) |
94 | 103 | ||
95 | it('Should list video categories', async function () { | 104 | it('Should list video categories', async function () { |
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts index d435f3682..0f8227fd3 100644 --- a/server/tests/api/videos/video-channels.ts +++ b/server/tests/api/videos/video-channels.ts | |||
@@ -6,13 +6,14 @@ import { basename } from 'path' | |||
6 | import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants' | 6 | import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants' |
7 | import { testFileExistsOrNot, testImage } from '@server/tests/shared' | 7 | import { testFileExistsOrNot, testImage } from '@server/tests/shared' |
8 | import { wait } from '@shared/core-utils' | 8 | import { wait } from '@shared/core-utils' |
9 | import { User, VideoChannel } from '@shared/models' | 9 | import { ActorImageType, User, VideoChannel } from '@shared/models' |
10 | import { | 10 | import { |
11 | cleanupTests, | 11 | cleanupTests, |
12 | createMultipleServers, | 12 | createMultipleServers, |
13 | doubleFollow, | 13 | doubleFollow, |
14 | PeerTubeServer, | 14 | PeerTubeServer, |
15 | setAccessTokensToServers, | 15 | setAccessTokensToServers, |
16 | setDefaultAccountAvatar, | ||
16 | setDefaultVideoChannel, | 17 | setDefaultVideoChannel, |
17 | waitJobs | 18 | waitJobs |
18 | } from '@shared/server-commands' | 19 | } from '@shared/server-commands' |
@@ -44,6 +45,7 @@ describe('Test video channels', function () { | |||
44 | 45 | ||
45 | await setAccessTokensToServers(servers) | 46 | await setAccessTokensToServers(servers) |
46 | await setDefaultVideoChannel(servers) | 47 | await setDefaultVideoChannel(servers) |
48 | await setDefaultAccountAvatar(servers) | ||
47 | 49 | ||
48 | await doubleFollow(servers[0], servers[1]) | 50 | await doubleFollow(servers[0], servers[1]) |
49 | }) | 51 | }) |
@@ -281,14 +283,19 @@ describe('Test video channels', function () { | |||
281 | 283 | ||
282 | for (const server of servers) { | 284 | for (const server of servers) { |
283 | const videoChannel = await findChannel(server, secondVideoChannelId) | 285 | const videoChannel = await findChannel(server, secondVideoChannelId) |
286 | const expectedSizes = ACTOR_IMAGES_SIZE[ActorImageType.AVATAR] | ||
284 | 287 | ||
285 | avatarPaths[server.port] = videoChannel.avatar.path | 288 | expect(videoChannel.avatars.length).to.equal(expectedSizes.length, 'Expected avatars to be generated in all sizes') |
286 | await testImage(server.url, 'avatar-resized', avatarPaths[server.port], '.png') | ||
287 | await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), true) | ||
288 | 289 | ||
289 | const row = await server.sql.getActorImage(basename(avatarPaths[server.port])) | 290 | for (const avatar of videoChannel.avatars) { |
290 | expect(row.height).to.equal(ACTOR_IMAGES_SIZE.AVATARS.height) | 291 | avatarPaths[server.port] = avatar.path |
291 | expect(row.width).to.equal(ACTOR_IMAGES_SIZE.AVATARS.width) | 292 | await testImage(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatarPaths[server.port], '.png') |
293 | await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), true) | ||
294 | |||
295 | const row = await server.sql.getActorImage(basename(avatarPaths[server.port])) | ||
296 | |||
297 | expect(expectedSizes.some(({ height, width }) => row.height === height && row.width === width)).to.equal(true) | ||
298 | } | ||
292 | } | 299 | } |
293 | }) | 300 | }) |
294 | 301 | ||
@@ -308,19 +315,18 @@ describe('Test video channels', function () { | |||
308 | for (const server of servers) { | 315 | for (const server of servers) { |
309 | const videoChannel = await server.channels.get({ channelName: 'second_video_channel@' + servers[0].host }) | 316 | const videoChannel = await server.channels.get({ channelName: 'second_video_channel@' + servers[0].host }) |
310 | 317 | ||
311 | bannerPaths[server.port] = videoChannel.banner.path | 318 | bannerPaths[server.port] = videoChannel.banners[0].path |
312 | await testImage(server.url, 'banner-resized', bannerPaths[server.port]) | 319 | await testImage(server.url, 'banner-resized', bannerPaths[server.port]) |
313 | await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), true) | 320 | await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), true) |
314 | 321 | ||
315 | const row = await server.sql.getActorImage(basename(bannerPaths[server.port])) | 322 | const row = await server.sql.getActorImage(basename(bannerPaths[server.port])) |
316 | expect(row.height).to.equal(ACTOR_IMAGES_SIZE.BANNERS.height) | 323 | expect(row.height).to.equal(ACTOR_IMAGES_SIZE[ActorImageType.BANNER][0].height) |
317 | expect(row.width).to.equal(ACTOR_IMAGES_SIZE.BANNERS.width) | 324 | expect(row.width).to.equal(ACTOR_IMAGES_SIZE[ActorImageType.BANNER][0].width) |
318 | } | 325 | } |
319 | }) | 326 | }) |
320 | 327 | ||
321 | it('Should delete the video channel avatar', async function () { | 328 | it('Should delete the video channel avatar', async function () { |
322 | this.timeout(15000) | 329 | this.timeout(15000) |
323 | |||
324 | await servers[0].channels.deleteImage({ channelName: 'second_video_channel', type: 'avatar' }) | 330 | await servers[0].channels.deleteImage({ channelName: 'second_video_channel', type: 'avatar' }) |
325 | 331 | ||
326 | await waitJobs(servers) | 332 | await waitJobs(servers) |
@@ -329,7 +335,7 @@ describe('Test video channels', function () { | |||
329 | const videoChannel = await findChannel(server, secondVideoChannelId) | 335 | const videoChannel = await findChannel(server, secondVideoChannelId) |
330 | await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), false) | 336 | await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), false) |
331 | 337 | ||
332 | expect(videoChannel.avatar).to.be.null | 338 | expect(videoChannel.avatars).to.be.empty |
333 | } | 339 | } |
334 | }) | 340 | }) |
335 | 341 | ||
@@ -344,7 +350,7 @@ describe('Test video channels', function () { | |||
344 | const videoChannel = await findChannel(server, secondVideoChannelId) | 350 | const videoChannel = await findChannel(server, secondVideoChannelId) |
345 | await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), false) | 351 | await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), false) |
346 | 352 | ||
347 | expect(videoChannel.banner).to.be.null | 353 | expect(videoChannel.banners).to.be.empty |
348 | } | 354 | } |
349 | }) | 355 | }) |
350 | 356 | ||
diff --git a/server/tests/api/videos/video-comments.ts b/server/tests/api/videos/video-comments.ts index 2ae523970..1488ce2b5 100644 --- a/server/tests/api/videos/video-comments.ts +++ b/server/tests/api/videos/video-comments.ts | |||
@@ -3,7 +3,15 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { dateIsValid, testImage } from '@server/tests/shared' | 5 | import { dateIsValid, testImage } from '@server/tests/shared' |
6 | import { cleanupTests, CommentsCommand, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | 6 | import { |
7 | cleanupTests, | ||
8 | CommentsCommand, | ||
9 | createSingleServer, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultChannelAvatar | ||
14 | } from '@shared/server-commands' | ||
7 | 15 | ||
8 | const expect = chai.expect | 16 | const expect = chai.expect |
9 | 17 | ||
@@ -29,7 +37,8 @@ describe('Test video comments', function () { | |||
29 | videoUUID = uuid | 37 | videoUUID = uuid |
30 | videoId = id | 38 | videoId = id |
31 | 39 | ||
32 | await server.users.updateMyAvatar({ fixture: 'avatar.png' }) | 40 | await setDefaultChannelAvatar(server) |
41 | await setDefaultAccountAvatar(server) | ||
33 | 42 | ||
34 | userAccessTokenServer1 = await server.users.generateUserAndToken('user1') | 43 | userAccessTokenServer1 = await server.users.generateUserAndToken('user1') |
35 | 44 | ||
@@ -81,7 +90,9 @@ describe('Test video comments', function () { | |||
81 | expect(comment.account.name).to.equal('root') | 90 | expect(comment.account.name).to.equal('root') |
82 | expect(comment.account.host).to.equal('localhost:' + server.port) | 91 | expect(comment.account.host).to.equal('localhost:' + server.port) |
83 | 92 | ||
84 | await testImage(server.url, 'avatar-resized', comment.account.avatar.path, '.png') | 93 | for (const avatar of comment.account.avatars) { |
94 | await testImage(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, '.png') | ||
95 | } | ||
85 | 96 | ||
86 | expect(comment.totalReplies).to.equal(0) | 97 | expect(comment.totalReplies).to.equal(0) |
87 | expect(comment.totalRepliesFromVideoAuthor).to.equal(0) | 98 | expect(comment.totalRepliesFromVideoAuthor).to.equal(0) |
diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts index 34327334f..1e8dbef02 100644 --- a/server/tests/api/videos/video-playlists.ts +++ b/server/tests/api/videos/video-playlists.ts | |||
@@ -20,6 +20,7 @@ import { | |||
20 | PeerTubeServer, | 20 | PeerTubeServer, |
21 | PlaylistsCommand, | 21 | PlaylistsCommand, |
22 | setAccessTokensToServers, | 22 | setAccessTokensToServers, |
23 | setDefaultAccountAvatar, | ||
23 | setDefaultVideoChannel, | 24 | setDefaultVideoChannel, |
24 | waitJobs | 25 | waitJobs |
25 | } from '@shared/server-commands' | 26 | } from '@shared/server-commands' |
@@ -79,6 +80,7 @@ describe('Test video playlists', function () { | |||
79 | // Get the access tokens | 80 | // Get the access tokens |
80 | await setAccessTokensToServers(servers) | 81 | await setAccessTokensToServers(servers) |
81 | await setDefaultVideoChannel(servers) | 82 | await setDefaultVideoChannel(servers) |
83 | await setDefaultAccountAvatar(servers) | ||
82 | 84 | ||
83 | // Server 1 and server 2 follow each other | 85 | // Server 1 and server 2 follow each other |
84 | await doubleFollow(servers[0], servers[1]) | 86 | await doubleFollow(servers[0], servers[1]) |
diff --git a/server/tests/api/videos/videos-common-filters.ts b/server/tests/api/videos/videos-common-filters.ts index 0254662c5..317de90a9 100644 --- a/server/tests/api/videos/videos-common-filters.ts +++ b/server/tests/api/videos/videos-common-filters.ts | |||
@@ -3,6 +3,7 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { expect } from 'chai' | 4 | import { expect } from 'chai' |
5 | import { pick } from '@shared/core-utils' | 5 | import { pick } from '@shared/core-utils' |
6 | import { HttpStatusCode, UserRole, Video, VideoDetails, VideoInclude, VideoPrivacy } from '@shared/models' | ||
6 | import { | 7 | import { |
7 | cleanupTests, | 8 | cleanupTests, |
8 | createMultipleServers, | 9 | createMultipleServers, |
@@ -10,10 +11,10 @@ import { | |||
10 | makeGetRequest, | 11 | makeGetRequest, |
11 | PeerTubeServer, | 12 | PeerTubeServer, |
12 | setAccessTokensToServers, | 13 | setAccessTokensToServers, |
14 | setDefaultAccountAvatar, | ||
13 | setDefaultVideoChannel, | 15 | setDefaultVideoChannel, |
14 | waitJobs | 16 | waitJobs |
15 | } from '@shared/server-commands' | 17 | } from '@shared/server-commands' |
16 | import { HttpStatusCode, UserRole, Video, VideoDetails, VideoInclude, VideoPrivacy } from '@shared/models' | ||
17 | 18 | ||
18 | describe('Test videos filter', function () { | 19 | describe('Test videos filter', function () { |
19 | let servers: PeerTubeServer[] | 20 | let servers: PeerTubeServer[] |
@@ -29,6 +30,7 @@ describe('Test videos filter', function () { | |||
29 | 30 | ||
30 | await setAccessTokensToServers(servers) | 31 | await setAccessTokensToServers(servers) |
31 | await setDefaultVideoChannel(servers) | 32 | await setDefaultVideoChannel(servers) |
33 | await setDefaultAccountAvatar(servers) | ||
32 | 34 | ||
33 | for (const server of servers) { | 35 | for (const server of servers) { |
34 | const moderator = { username: 'moderator', password: 'my super password' } | 36 | const moderator = { username: 'moderator', password: 'my super password' } |
diff --git a/server/tests/cli/prune-storage.ts b/server/tests/cli/prune-storage.ts index a723ed8b4..3ca7c19ea 100644 --- a/server/tests/cli/prune-storage.ts +++ b/server/tests/cli/prune-storage.ts | |||
@@ -51,7 +51,7 @@ async function assertCountAreOkay (servers: PeerTubeServer[]) { | |||
51 | expect(thumbnailsCount).to.equal(6) | 51 | expect(thumbnailsCount).to.equal(6) |
52 | 52 | ||
53 | const avatarsCount = await countFiles(server, 'avatars') | 53 | const avatarsCount = await countFiles(server, 'avatars') |
54 | expect(avatarsCount).to.equal(2) | 54 | expect(avatarsCount).to.equal(4) |
55 | 55 | ||
56 | const hlsRootCount = await countFiles(server, 'streaming-playlists/hls') | 56 | const hlsRootCount = await countFiles(server, 'streaming-playlists/hls') |
57 | expect(hlsRootCount).to.equal(2) | 57 | expect(hlsRootCount).to.equal(2) |
@@ -87,23 +87,28 @@ describe('Test prune storage scripts', function () { | |||
87 | 87 | ||
88 | await doubleFollow(servers[0], servers[1]) | 88 | await doubleFollow(servers[0], servers[1]) |
89 | 89 | ||
90 | // Lazy load the remote avatar | 90 | // Lazy load the remote avatars |
91 | { | 91 | { |
92 | const account = await servers[0].accounts.get({ accountName: 'root@localhost:' + servers[1].port }) | 92 | const account = await servers[0].accounts.get({ accountName: 'root@localhost:' + servers[1].port }) |
93 | await makeGetRequest({ | 93 | |
94 | url: servers[0].url, | 94 | for (const avatar of account.avatars) { |
95 | path: account.avatar.path, | 95 | await makeGetRequest({ |
96 | expectedStatus: HttpStatusCode.OK_200 | 96 | url: servers[0].url, |
97 | }) | 97 | path: avatar.path, |
98 | expectedStatus: HttpStatusCode.OK_200 | ||
99 | }) | ||
100 | } | ||
98 | } | 101 | } |
99 | 102 | ||
100 | { | 103 | { |
101 | const account = await servers[1].accounts.get({ accountName: 'root@localhost:' + servers[0].port }) | 104 | const account = await servers[1].accounts.get({ accountName: 'root@localhost:' + servers[0].port }) |
102 | await makeGetRequest({ | 105 | for (const avatar of account.avatars) { |
103 | url: servers[1].url, | 106 | await makeGetRequest({ |
104 | path: account.avatar.path, | 107 | url: servers[1].url, |
105 | expectedStatus: HttpStatusCode.OK_200 | 108 | path: avatar.path, |
106 | }) | 109 | expectedStatus: HttpStatusCode.OK_200 |
110 | }) | ||
111 | } | ||
107 | } | 112 | } |
108 | 113 | ||
109 | await wait(1000) | 114 | await wait(1000) |
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts index 4dcd77cca..320dc3333 100644 --- a/server/tests/feeds/feeds.ts +++ b/server/tests/feeds/feeds.ts | |||
@@ -3,6 +3,7 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { XMLParser, XMLValidator } from 'fast-xml-parser' | 5 | import { XMLParser, XMLValidator } from 'fast-xml-parser' |
6 | import { HttpStatusCode, VideoPrivacy } from '@shared/models' | ||
6 | import { | 7 | import { |
7 | cleanupTests, | 8 | cleanupTests, |
8 | createMultipleServers, | 9 | createMultipleServers, |
@@ -11,9 +12,9 @@ import { | |||
11 | makeGetRequest, | 12 | makeGetRequest, |
12 | PeerTubeServer, | 13 | PeerTubeServer, |
13 | setAccessTokensToServers, | 14 | setAccessTokensToServers, |
15 | setDefaultChannelAvatar, | ||
14 | waitJobs | 16 | waitJobs |
15 | } from '@shared/server-commands' | 17 | } from '@shared/server-commands' |
16 | import { HttpStatusCode, VideoPrivacy } from '@shared/models' | ||
17 | 18 | ||
18 | chai.use(require('chai-xml')) | 19 | chai.use(require('chai-xml')) |
19 | chai.use(require('chai-json-schema')) | 20 | chai.use(require('chai-json-schema')) |
@@ -44,6 +45,7 @@ describe('Test syndication feeds', () => { | |||
44 | }) | 45 | }) |
45 | 46 | ||
46 | await setAccessTokensToServers([ ...servers, serverHLSOnly ]) | 47 | await setAccessTokensToServers([ ...servers, serverHLSOnly ]) |
48 | await setDefaultChannelAvatar(servers[0]) | ||
47 | await doubleFollow(servers[0], servers[1]) | 49 | await doubleFollow(servers[0], servers[1]) |
48 | 50 | ||
49 | { | 51 | { |
diff --git a/server/tests/fixtures/avatar-resized.gif b/server/tests/fixtures/avatar-resized-120x120.gif index 81a82189e..81a82189e 100644 --- a/server/tests/fixtures/avatar-resized.gif +++ b/server/tests/fixtures/avatar-resized-120x120.gif | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/avatar-resized.png b/server/tests/fixtures/avatar-resized-120x120.png index 9d84151f8..9d84151f8 100644 --- a/server/tests/fixtures/avatar-resized.png +++ b/server/tests/fixtures/avatar-resized-120x120.png | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/avatar-resized-48x48.gif b/server/tests/fixtures/avatar-resized-48x48.gif new file mode 100644 index 000000000..5900ff12e --- /dev/null +++ b/server/tests/fixtures/avatar-resized-48x48.gif | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/avatar-resized-48x48.png b/server/tests/fixtures/avatar-resized-48x48.png new file mode 100644 index 000000000..9e5f3b490 --- /dev/null +++ b/server/tests/fixtures/avatar-resized-48x48.png | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/avatar2-resized.png b/server/tests/fixtures/avatar2-resized-120x120.png index 44149facb..44149facb 100644 --- a/server/tests/fixtures/avatar2-resized.png +++ b/server/tests/fixtures/avatar2-resized-120x120.png | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/avatar2-resized-48x48.png b/server/tests/fixtures/avatar2-resized-48x48.png new file mode 100644 index 000000000..bb3939b1a --- /dev/null +++ b/server/tests/fixtures/avatar2-resized-48x48.png | |||
Binary files differ | |||
diff --git a/server/tests/shared/notifications.ts b/server/tests/shared/notifications.ts index cdc21fdc8..78d3787f0 100644 --- a/server/tests/shared/notifications.ts +++ b/server/tests/shared/notifications.ts | |||
@@ -10,7 +10,14 @@ import { | |||
10 | UserNotificationSettingValue, | 10 | UserNotificationSettingValue, |
11 | UserNotificationType | 11 | UserNotificationType |
12 | } from '@shared/models' | 12 | } from '@shared/models' |
13 | import { createMultipleServers, doubleFollow, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | 13 | import { |
14 | createMultipleServers, | ||
15 | doubleFollow, | ||
16 | PeerTubeServer, | ||
17 | setAccessTokensToServers, | ||
18 | setDefaultAccountAvatar, | ||
19 | setDefaultChannelAvatar | ||
20 | } from '@shared/server-commands' | ||
14 | import { MockSmtpServer } from './mock-servers' | 21 | import { MockSmtpServer } from './mock-servers' |
15 | 22 | ||
16 | type CheckerBaseParams = { | 23 | type CheckerBaseParams = { |
@@ -646,6 +653,8 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an | |||
646 | const servers = await createMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg)) | 653 | const servers = await createMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg)) |
647 | 654 | ||
648 | await setAccessTokensToServers(servers) | 655 | await setAccessTokensToServers(servers) |
656 | await setDefaultChannelAvatar(servers) | ||
657 | await setDefaultAccountAvatar(servers) | ||
649 | 658 | ||
650 | if (serversCount > 1) { | 659 | if (serversCount > 1) { |
651 | await doubleFollow(servers[0], servers[1]) | 660 | await doubleFollow(servers[0], servers[1]) |
diff --git a/server/types/models/actor/actor-image.ts b/server/types/models/actor/actor-image.ts index 521b4cc59..e8f32b71e 100644 --- a/server/types/models/actor/actor-image.ts +++ b/server/types/models/actor/actor-image.ts | |||
@@ -9,4 +9,4 @@ export type MActorImage = ActorImageModel | |||
9 | 9 | ||
10 | export type MActorImageFormattable = | 10 | export type MActorImageFormattable = |
11 | FunctionProperties<MActorImage> & | 11 | FunctionProperties<MActorImage> & |
12 | Pick<MActorImage, 'filename' | 'createdAt' | 'updatedAt'> | 12 | Pick<MActorImage, 'width' | 'filename' | 'createdAt' | 'updatedAt'> |
diff --git a/server/types/models/actor/actor.ts b/server/types/models/actor/actor.ts index 9ce97094f..280256bab 100644 --- a/server/types/models/actor/actor.ts +++ b/server/types/models/actor/actor.ts | |||
@@ -10,7 +10,7 @@ type UseOpt<K extends keyof ActorModel, M> = PickWithOpt<ActorModel, K, M> | |||
10 | 10 | ||
11 | // ############################################################################ | 11 | // ############################################################################ |
12 | 12 | ||
13 | export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server' | 'Banner'> | 13 | export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'ActorFollowers' | 'Server' | 'Banners'> |
14 | 14 | ||
15 | // ############################################################################ | 15 | // ############################################################################ |
16 | 16 | ||
@@ -35,7 +35,7 @@ export type MActorRedundancyAllowedOpt = PickWithOpt<ActorModel, 'Server', MServ | |||
35 | export type MActorDefaultLight = | 35 | export type MActorDefaultLight = |
36 | MActorLight & | 36 | MActorLight & |
37 | Use<'Server', MServerHost> & | 37 | Use<'Server', MServerHost> & |
38 | Use<'Avatar', MActorImage> | 38 | Use<'Avatars', MActorImage[]> |
39 | 39 | ||
40 | export type MActorAccountId = | 40 | export type MActorAccountId = |
41 | MActor & | 41 | MActor & |
@@ -78,13 +78,13 @@ export type MActorServer = | |||
78 | 78 | ||
79 | export type MActorImages = | 79 | export type MActorImages = |
80 | MActor & | 80 | MActor & |
81 | Use<'Avatar', MActorImage> & | 81 | Use<'Avatars', MActorImage[]> & |
82 | UseOpt<'Banner', MActorImage> | 82 | UseOpt<'Banners', MActorImage[]> |
83 | 83 | ||
84 | export type MActorDefault = | 84 | export type MActorDefault = |
85 | MActor & | 85 | MActor & |
86 | Use<'Server', MServer> & | 86 | Use<'Server', MServer> & |
87 | Use<'Avatar', MActorImage> | 87 | Use<'Avatars', MActorImage[]> |
88 | 88 | ||
89 | export type MActorDefaultChannelId = | 89 | export type MActorDefaultChannelId = |
90 | MActorDefault & | 90 | MActorDefault & |
@@ -93,8 +93,8 @@ export type MActorDefaultChannelId = | |||
93 | export type MActorDefaultBanner = | 93 | export type MActorDefaultBanner = |
94 | MActor & | 94 | MActor & |
95 | Use<'Server', MServer> & | 95 | Use<'Server', MServer> & |
96 | Use<'Avatar', MActorImage> & | 96 | Use<'Avatars', MActorImage[]> & |
97 | Use<'Banner', MActorImage> | 97 | Use<'Banners', MActorImage[]> |
98 | 98 | ||
99 | // Actor with channel that is associated to an account and its actor | 99 | // Actor with channel that is associated to an account and its actor |
100 | // Actor -> VideoChannel -> Account -> Actor | 100 | // Actor -> VideoChannel -> Account -> Actor |
@@ -105,8 +105,8 @@ export type MActorChannelAccountActor = | |||
105 | export type MActorFull = | 105 | export type MActorFull = |
106 | MActor & | 106 | MActor & |
107 | Use<'Server', MServer> & | 107 | Use<'Server', MServer> & |
108 | Use<'Avatar', MActorImage> & | 108 | Use<'Avatars', MActorImage[]> & |
109 | Use<'Banner', MActorImage> & | 109 | Use<'Banners', MActorImage[]> & |
110 | Use<'Account', MAccount> & | 110 | Use<'Account', MAccount> & |
111 | Use<'VideoChannel', MChannelAccountActor> | 111 | Use<'VideoChannel', MChannelAccountActor> |
112 | 112 | ||
@@ -114,8 +114,8 @@ export type MActorFull = | |||
114 | export type MActorFullActor = | 114 | export type MActorFullActor = |
115 | MActor & | 115 | MActor & |
116 | Use<'Server', MServer> & | 116 | Use<'Server', MServer> & |
117 | Use<'Avatar', MActorImage> & | 117 | Use<'Avatars', MActorImage[]> & |
118 | Use<'Banner', MActorImage> & | 118 | Use<'Banners', MActorImage[]> & |
119 | Use<'Account', MAccountDefault> & | 119 | Use<'Account', MAccountDefault> & |
120 | Use<'VideoChannel', MChannelAccountDefault> | 120 | Use<'VideoChannel', MChannelAccountDefault> |
121 | 121 | ||
@@ -125,9 +125,9 @@ export type MActorFullActor = | |||
125 | 125 | ||
126 | export type MActorSummary = | 126 | export type MActorSummary = |
127 | FunctionProperties<MActor> & | 127 | FunctionProperties<MActor> & |
128 | Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId' | 'avatarId'> & | 128 | Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId'> & |
129 | Use<'Server', MServerHost> & | 129 | Use<'Server', MServerHost> & |
130 | Use<'Avatar', MActorImage> | 130 | Use<'Avatars', MActorImage[]> |
131 | 131 | ||
132 | export type MActorSummaryBlocks = | 132 | export type MActorSummaryBlocks = |
133 | MActorSummary & | 133 | MActorSummary & |
@@ -145,21 +145,22 @@ export type MActorSummaryFormattable = | |||
145 | FunctionProperties<MActor> & | 145 | FunctionProperties<MActor> & |
146 | Pick<MActor, 'url' | 'preferredUsername'> & | 146 | Pick<MActor, 'url' | 'preferredUsername'> & |
147 | Use<'Server', MServerHost> & | 147 | Use<'Server', MServerHost> & |
148 | Use<'Avatar', MActorImageFormattable> | 148 | Use<'Avatars', MActorImageFormattable[]> |
149 | 149 | ||
150 | export type MActorFormattable = | 150 | export type MActorFormattable = |
151 | MActorSummaryFormattable & | 151 | MActorSummaryFormattable & |
152 | Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt' | 'remoteCreatedAt' | 'bannerId' | 'avatarId'> & | 152 | Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt' | 'remoteCreatedAt'> & |
153 | Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>> & | 153 | Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>> & |
154 | UseOpt<'Banner', MActorImageFormattable> | 154 | UseOpt<'Banners', MActorImageFormattable[]> & |
155 | UseOpt<'Avatars', MActorImageFormattable[]> | ||
155 | 156 | ||
156 | type MActorAPBase = | 157 | type MActorAPBase = |
157 | MActor & | 158 | MActor & |
158 | Use<'Avatar', MActorImage> | 159 | Use<'Avatars', MActorImage[]> |
159 | 160 | ||
160 | export type MActorAPAccount = | 161 | export type MActorAPAccount = |
161 | MActorAPBase | 162 | MActorAPBase |
162 | 163 | ||
163 | export type MActorAPChannel = | 164 | export type MActorAPChannel = |
164 | MActorAPBase & | 165 | MActorAPBase & |
165 | Use<'Banner', MActorImage> | 166 | Use<'Banners', MActorImage[]> |
diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts index db9ec0400..d4715a0b6 100644 --- a/server/types/models/user/user-notification.ts +++ b/server/types/models/user/user-notification.ts | |||
@@ -21,6 +21,7 @@ type Use<K extends keyof UserNotificationModel, M> = PickWith<UserNotificationMo | |||
21 | // ############################################################################ | 21 | // ############################################################################ |
22 | 22 | ||
23 | export module UserNotificationIncludes { | 23 | export module UserNotificationIncludes { |
24 | export type ActorImageInclude = Pick<ActorImageModel, 'createdAt' | 'filename' | 'getStaticPath' | 'width' | 'updatedAt'> | ||
24 | 25 | ||
25 | export type VideoInclude = Pick<VideoModel, 'id' | 'uuid' | 'name'> | 26 | export type VideoInclude = Pick<VideoModel, 'id' | 'uuid' | 'name'> |
26 | export type VideoIncludeChannel = | 27 | export type VideoIncludeChannel = |
@@ -29,7 +30,7 @@ export module UserNotificationIncludes { | |||
29 | 30 | ||
30 | export type ActorInclude = | 31 | export type ActorInclude = |
31 | Pick<ActorModel, 'preferredUsername' | 'getHost'> & | 32 | Pick<ActorModel, 'preferredUsername' | 'getHost'> & |
32 | PickWith<ActorModel, 'Avatar', Pick<ActorImageModel, 'filename' | 'getStaticPath'>> & | 33 | PickWith<ActorModel, 'Avatars', ActorImageInclude[]> & |
33 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> | 34 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> |
34 | 35 | ||
35 | export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'> | 36 | export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'> |
@@ -75,7 +76,7 @@ export module UserNotificationIncludes { | |||
75 | Pick<ActorModel, 'preferredUsername' | 'getHost'> & | 76 | Pick<ActorModel, 'preferredUsername' | 'getHost'> & |
76 | PickWith<ActorModel, 'Account', AccountInclude> & | 77 | PickWith<ActorModel, 'Account', AccountInclude> & |
77 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> & | 78 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> & |
78 | PickWithOpt<ActorModel, 'Avatar', Pick<ActorImageModel, 'filename' | 'getStaticPath'>> | 79 | PickWithOpt<ActorModel, 'Avatars', ActorImageInclude[]> |
79 | 80 | ||
80 | export type ActorFollowing = | 81 | export type ActorFollowing = |
81 | Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> & | 82 | Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> & |
@@ -98,7 +99,7 @@ export module UserNotificationIncludes { | |||
98 | // ############################################################################ | 99 | // ############################################################################ |
99 | 100 | ||
100 | export type MUserNotification = | 101 | export type MUserNotification = |
101 | Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' | | 102 | Omit<UserNotificationModel, 'User' | 'Video' | 'VideoComment' | 'Abuse' | 'VideoBlacklist' | |
102 | 'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application'> | 103 | 'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application'> |
103 | 104 | ||
104 | // ############################################################################ | 105 | // ############################################################################ |
@@ -106,7 +107,7 @@ export type MUserNotification = | |||
106 | export type UserNotificationModelForApi = | 107 | export type UserNotificationModelForApi = |
107 | MUserNotification & | 108 | MUserNotification & |
108 | Use<'Video', UserNotificationIncludes.VideoIncludeChannel> & | 109 | Use<'Video', UserNotificationIncludes.VideoIncludeChannel> & |
109 | Use<'Comment', UserNotificationIncludes.VideoCommentInclude> & | 110 | Use<'VideoComment', UserNotificationIncludes.VideoCommentInclude> & |
110 | Use<'Abuse', UserNotificationIncludes.AbuseInclude> & | 111 | Use<'Abuse', UserNotificationIncludes.AbuseInclude> & |
111 | Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> & | 112 | Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> & |
112 | Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> & | 113 | Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> & |
diff --git a/shared/models/activitypub/activitypub-actor.ts b/shared/models/activitypub/activitypub-actor.ts index 09d4f7402..efb6edec4 100644 --- a/shared/models/activitypub/activitypub-actor.ts +++ b/shared/models/activitypub/activitypub-actor.ts | |||
@@ -27,8 +27,11 @@ export interface ActivityPubActor { | |||
27 | publicKeyPem: string | 27 | publicKeyPem: string |
28 | } | 28 | } |
29 | 29 | ||
30 | icon?: ActivityIconObject | 30 | image?: ActivityIconObject | ActivityIconObject[] |
31 | image?: ActivityIconObject | 31 | |
32 | icon?: ActivityIconObject | ActivityIconObject[] | ||
33 | // TODO: migrate to `icon`, introduced in 4.2 | ||
34 | icons?: ActivityIconObject[] | ||
32 | 35 | ||
33 | published?: string | 36 | published?: string |
34 | } | 37 | } |
diff --git a/shared/models/actors/account.model.ts b/shared/models/actors/account.model.ts index f2138077e..60f4236d5 100644 --- a/shared/models/actors/account.model.ts +++ b/shared/models/actors/account.model.ts | |||
@@ -4,6 +4,7 @@ import { Actor } from './actor.model' | |||
4 | export interface Account extends Actor { | 4 | export interface Account extends Actor { |
5 | displayName: string | 5 | displayName: string |
6 | description: string | 6 | description: string |
7 | avatars: ActorImage[] | ||
7 | 8 | ||
8 | updatedAt: Date | string | 9 | updatedAt: Date | string |
9 | 10 | ||
@@ -16,5 +17,9 @@ export interface AccountSummary { | |||
16 | displayName: string | 17 | displayName: string |
17 | url: string | 18 | url: string |
18 | host: string | 19 | host: string |
19 | avatar?: ActorImage | 20 | |
21 | avatars: ActorImage[] | ||
22 | |||
23 | // TODO: remove, deprecated in 4.2 | ||
24 | avatar: ActorImage | ||
20 | } | 25 | } |
diff --git a/shared/models/actors/actor-image.model.ts b/shared/models/actors/actor-image.model.ts index ad5eab627..cfe44ac15 100644 --- a/shared/models/actors/actor-image.model.ts +++ b/shared/models/actors/actor-image.model.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export interface ActorImage { | 1 | export interface ActorImage { |
2 | width: number | ||
2 | path: string | 3 | path: string |
3 | 4 | ||
4 | url?: string | 5 | url?: string |
diff --git a/shared/models/actors/actor.model.ts b/shared/models/actors/actor.model.ts index fd0662331..bf86a917f 100644 --- a/shared/models/actors/actor.model.ts +++ b/shared/models/actors/actor.model.ts | |||
@@ -8,5 +8,9 @@ export interface Actor { | |||
8 | followingCount: number | 8 | followingCount: number |
9 | followersCount: number | 9 | followersCount: number |
10 | createdAt: Date | string | 10 | createdAt: Date | string |
11 | avatar?: ActorImage | 11 | |
12 | avatars: ActorImage[] | ||
13 | |||
14 | // TODO: remove, deprecated in 4.2 | ||
15 | avatar: ActorImage | ||
12 | } | 16 | } |
diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts index 5820589fe..a2621fb5b 100644 --- a/shared/models/users/user-notification.model.ts +++ b/shared/models/users/user-notification.model.ts | |||
@@ -40,14 +40,19 @@ export interface VideoInfo { | |||
40 | name: string | 40 | name: string |
41 | } | 41 | } |
42 | 42 | ||
43 | export interface AvatarInfo { | ||
44 | width: number | ||
45 | path: string | ||
46 | } | ||
47 | |||
43 | export interface ActorInfo { | 48 | export interface ActorInfo { |
44 | id: number | 49 | id: number |
45 | displayName: string | 50 | displayName: string |
46 | name: string | 51 | name: string |
47 | host: string | 52 | host: string |
48 | avatar?: { | 53 | |
49 | path: string | 54 | avatars: AvatarInfo[] |
50 | } | 55 | avatar: AvatarInfo |
51 | } | 56 | } |
52 | 57 | ||
53 | export interface UserNotification { | 58 | export interface UserNotification { |
diff --git a/shared/models/videos/channel/video-channel.model.ts b/shared/models/videos/channel/video-channel.model.ts index 5393f924d..58b60c177 100644 --- a/shared/models/videos/channel/video-channel.model.ts +++ b/shared/models/videos/channel/video-channel.model.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Actor } from '../../actors/actor.model' | ||
2 | import { Account, ActorImage } from '../../actors' | 1 | import { Account, ActorImage } from '../../actors' |
2 | import { Actor } from '../../actors/actor.model' | ||
3 | 3 | ||
4 | export type ViewsPerDate = { | 4 | export type ViewsPerDate = { |
5 | date: Date | 5 | date: Date |
@@ -19,7 +19,10 @@ export interface VideoChannel extends Actor { | |||
19 | videosCount?: number | 19 | videosCount?: number |
20 | viewsPerDay?: ViewsPerDate[] // chronologically ordered | 20 | viewsPerDay?: ViewsPerDate[] // chronologically ordered |
21 | 21 | ||
22 | banner?: ActorImage | 22 | banners: ActorImage[] |
23 | |||
24 | // TODO: remove, deprecated in 4.2 | ||
25 | banner: ActorImage | ||
23 | } | 26 | } |
24 | 27 | ||
25 | export interface VideoChannelSummary { | 28 | export interface VideoChannelSummary { |
@@ -28,5 +31,9 @@ export interface VideoChannelSummary { | |||
28 | displayName: string | 31 | displayName: string |
29 | url: string | 32 | url: string |
30 | host: string | 33 | host: string |
31 | avatar?: ActorImage | 34 | |
35 | avatars: ActorImage[] | ||
36 | |||
37 | // TODO: remove, deprecated in 4.2 | ||
38 | avatar: ActorImage | ||
32 | } | 39 | } |
diff --git a/shared/server-commands/users/accounts.ts b/shared/server-commands/users/accounts.ts new file mode 100644 index 000000000..6387891f4 --- /dev/null +++ b/shared/server-commands/users/accounts.ts | |||
@@ -0,0 +1,15 @@ | |||
1 | import { PeerTubeServer } from '../server/server' | ||
2 | |||
3 | async function setDefaultAccountAvatar (serversArg: PeerTubeServer | PeerTubeServer[], token?: string) { | ||
4 | const servers = Array.isArray(serversArg) | ||
5 | ? serversArg | ||
6 | : [ serversArg ] | ||
7 | |||
8 | for (const server of servers) { | ||
9 | await server.users.updateMyAvatar({ fixture: 'avatar.png', token }) | ||
10 | } | ||
11 | } | ||
12 | |||
13 | export { | ||
14 | setDefaultAccountAvatar | ||
15 | } | ||
diff --git a/shared/server-commands/users/index.ts b/shared/server-commands/users/index.ts index c2bc5c44f..f6f93b4d2 100644 --- a/shared/server-commands/users/index.ts +++ b/shared/server-commands/users/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export * from './accounts-command' | 1 | export * from './accounts-command' |
2 | export * from './accounts' | ||
2 | export * from './blocklist-command' | 3 | export * from './blocklist-command' |
3 | export * from './login' | 4 | export * from './login' |
4 | export * from './login-command' | 5 | export * from './login-command' |
diff --git a/shared/server-commands/videos/channels.ts b/shared/server-commands/videos/channels.ts index 756c47453..3c0d4b723 100644 --- a/shared/server-commands/videos/channels.ts +++ b/shared/server-commands/videos/channels.ts | |||
@@ -13,6 +13,17 @@ function setDefaultVideoChannel (servers: PeerTubeServer[]) { | |||
13 | return Promise.all(tasks) | 13 | return Promise.all(tasks) |
14 | } | 14 | } |
15 | 15 | ||
16 | async function setDefaultChannelAvatar (serversArg: PeerTubeServer | PeerTubeServer[], channelName: string = 'root_channel') { | ||
17 | const servers = Array.isArray(serversArg) | ||
18 | ? serversArg | ||
19 | : [ serversArg ] | ||
20 | |||
21 | for (const server of servers) { | ||
22 | await server.channels.updateImage({ channelName, fixture: 'avatar.png', type: 'avatar' }) | ||
23 | } | ||
24 | } | ||
25 | |||
16 | export { | 26 | export { |
17 | setDefaultVideoChannel | 27 | setDefaultVideoChannel, |
28 | setDefaultChannelAvatar | ||
18 | } | 29 | } |
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 396074225..70f2d97f5 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml | |||
@@ -1556,8 +1556,10 @@ paths: | |||
1556 | schema: | 1556 | schema: |
1557 | type: object | 1557 | type: object |
1558 | properties: | 1558 | properties: |
1559 | avatar: | 1559 | avatars: |
1560 | $ref: '#/components/schemas/ActorImage' | 1560 | type: array |
1561 | items: | ||
1562 | $ref: '#/components/schemas/ActorImage' | ||
1561 | '413': | 1563 | '413': |
1562 | description: image file too large | 1564 | description: image file too large |
1563 | headers: | 1565 | headers: |
@@ -2878,7 +2880,7 @@ paths: | |||
2878 | type: object | 2880 | type: object |
2879 | properties: | 2881 | properties: |
2880 | id: | 2882 | id: |
2881 | $ref: '#/components/schemas/VideoChannel/properties/id' | 2883 | $ref: '#/components/schemas/id' |
2882 | requestBody: | 2884 | requestBody: |
2883 | content: | 2885 | content: |
2884 | application/json: | 2886 | application/json: |
@@ -3010,8 +3012,10 @@ paths: | |||
3010 | schema: | 3012 | schema: |
3011 | type: object | 3013 | type: object |
3012 | properties: | 3014 | properties: |
3013 | avatar: | 3015 | avatars: |
3014 | $ref: '#/components/schemas/ActorImage' | 3016 | type: array |
3017 | items: | ||
3018 | $ref: '#/components/schemas/ActorImage' | ||
3015 | '413': | 3019 | '413': |
3016 | description: image file too large | 3020 | description: image file too large |
3017 | headers: | 3021 | headers: |
@@ -3064,8 +3068,10 @@ paths: | |||
3064 | schema: | 3068 | schema: |
3065 | type: object | 3069 | type: object |
3066 | properties: | 3070 | properties: |
3067 | banner: | 3071 | banners: |
3068 | $ref: '#/components/schemas/ActorImage' | 3072 | type: array |
3073 | items: | ||
3074 | $ref: '#/components/schemas/ActorImage' | ||
3069 | '413': | 3075 | '413': |
3070 | description: image file too large | 3076 | description: image file too large |
3071 | headers: | 3077 | headers: |
@@ -5364,10 +5370,10 @@ components: | |||
5364 | host: | 5370 | host: |
5365 | type: string | 5371 | type: string |
5366 | format: hostname | 5372 | format: hostname |
5367 | avatar: | 5373 | avatars: |
5368 | nullable: true | 5374 | type: array |
5369 | allOf: | 5375 | items: |
5370 | - $ref: '#/components/schemas/ActorImage' | 5376 | $ref: '#/components/schemas/ActorImage' |
5371 | VideoChannelSummary: | 5377 | VideoChannelSummary: |
5372 | properties: | 5378 | properties: |
5373 | id: | 5379 | id: |
@@ -5382,10 +5388,10 @@ components: | |||
5382 | host: | 5388 | host: |
5383 | type: string | 5389 | type: string |
5384 | format: hostname | 5390 | format: hostname |
5385 | avatar: | 5391 | avatars: |
5386 | nullable: true | 5392 | type: array |
5387 | allOf: | 5393 | items: |
5388 | - $ref: '#/components/schemas/ActorImage' | 5394 | $ref: '#/components/schemas/ActorImage' |
5389 | PlaylistElement: | 5395 | PlaylistElement: |
5390 | properties: | 5396 | properties: |
5391 | position: | 5397 | position: |
@@ -5969,6 +5975,8 @@ components: | |||
5969 | properties: | 5975 | properties: |
5970 | path: | 5976 | path: |
5971 | type: string | 5977 | type: string |
5978 | width: | ||
5979 | type: integer | ||
5972 | createdAt: | 5980 | createdAt: |
5973 | type: string | 5981 | type: string |
5974 | format: date-time | 5982 | format: date-time |
@@ -5986,12 +5994,10 @@ components: | |||
5986 | host: | 5994 | host: |
5987 | type: string | 5995 | type: string |
5988 | format: hostname | 5996 | format: hostname |
5989 | avatar: | 5997 | avatars: |
5990 | nullable: true | 5998 | type: array |
5991 | type: object | 5999 | items: |
5992 | properties: | 6000 | $ref: '#/components/schemas/ActorImage' |
5993 | path: | ||
5994 | type: string | ||
5995 | Actor: | 6001 | Actor: |
5996 | properties: | 6002 | properties: |
5997 | id: | 6003 | id: |
@@ -6024,8 +6030,6 @@ components: | |||
6024 | updatedAt: | 6030 | updatedAt: |
6025 | type: string | 6031 | type: string |
6026 | format: date-time | 6032 | format: date-time |
6027 | avatar: | ||
6028 | $ref: '#/components/schemas/ActorImage' | ||
6029 | Account: | 6033 | Account: |
6030 | allOf: | 6034 | allOf: |
6031 | - $ref: '#/components/schemas/Actor' | 6035 | - $ref: '#/components/schemas/Actor' |
@@ -6934,7 +6938,7 @@ components: | |||
6934 | name: | 6938 | name: |
6935 | $ref: '#/components/schemas/usernameChannel' | 6939 | $ref: '#/components/schemas/usernameChannel' |
6936 | displayName: | 6940 | displayName: |
6937 | $ref: '#/components/schemas/VideoChannel/properties/displayName' | 6941 | type: string |
6938 | required: | 6942 | required: |
6939 | - username | 6943 | - username |
6940 | - password | 6944 | - password |
@@ -6996,46 +7000,47 @@ components: | |||
6996 | - refresh_token | 7000 | - refresh_token |
6997 | 7001 | ||
6998 | VideoChannel: | 7002 | VideoChannel: |
6999 | properties: | 7003 | allOf: |
7000 | # GET/POST/PUT properties | 7004 | - $ref: '#/components/schemas/Actor' |
7001 | displayName: | 7005 | - type: object |
7002 | type: string | ||
7003 | description: editable name of the channel, displayed in its representations | ||
7004 | example: Videos of Framasoft | ||
7005 | minLength: 1 | ||
7006 | maxLength: 120 | ||
7007 | description: | ||
7008 | type: string | ||
7009 | example: Videos made with <3 by Framasoft | ||
7010 | minLength: 3 | ||
7011 | maxLength: 1000 | ||
7012 | support: | ||
7013 | type: string | ||
7014 | description: text shown by default on all videos of this channel, to tell the audience how to support it | ||
7015 | example: Please support our work on https://soutenir.framasoft.org/en/ <3 | ||
7016 | minLength: 3 | ||
7017 | maxLength: 1000 | ||
7018 | # GET-only properties | ||
7019 | id: | ||
7020 | readOnly: true | ||
7021 | allOf: | ||
7022 | - $ref: '#/components/schemas/id' | ||
7023 | isLocal: | ||
7024 | readOnly: true | ||
7025 | type: boolean | ||
7026 | updatedAt: | ||
7027 | readOnly: true | ||
7028 | type: string | ||
7029 | format: date-time | ||
7030 | ownerAccount: | ||
7031 | readOnly: true | ||
7032 | nullable: true | ||
7033 | type: object | ||
7034 | properties: | 7006 | properties: |
7035 | id: | 7007 | displayName: |
7036 | type: integer | 7008 | type: string |
7037 | uuid: | 7009 | description: editable name of the channel, displayed in its representations |
7038 | $ref: '#/components/schemas/UUIDv4' | 7010 | example: Videos of Framasoft |
7011 | minLength: 1 | ||
7012 | maxLength: 120 | ||
7013 | description: | ||
7014 | type: string | ||
7015 | example: Videos made with <3 by Framasoft | ||
7016 | minLength: 3 | ||
7017 | maxLength: 1000 | ||
7018 | support: | ||
7019 | type: string | ||
7020 | description: text shown by default on all videos of this channel, to tell the audience how to support it | ||
7021 | example: Please support our work on https://soutenir.framasoft.org/en/ <3 | ||
7022 | minLength: 3 | ||
7023 | maxLength: 1000 | ||
7024 | isLocal: | ||
7025 | readOnly: true | ||
7026 | type: boolean | ||
7027 | updatedAt: | ||
7028 | readOnly: true | ||
7029 | type: string | ||
7030 | format: date-time | ||
7031 | banners: | ||
7032 | type: array | ||
7033 | items: | ||
7034 | $ref: '#/components/schemas/ActorImage' | ||
7035 | ownerAccount: | ||
7036 | readOnly: true | ||
7037 | nullable: true | ||
7038 | type: object | ||
7039 | properties: | ||
7040 | id: | ||
7041 | type: integer | ||
7042 | uuid: | ||
7043 | $ref: '#/components/schemas/UUIDv4' | ||
7039 | VideoChannelCreate: | 7044 | VideoChannelCreate: |
7040 | allOf: | 7045 | allOf: |
7041 | - $ref: '#/components/schemas/VideoChannel' | 7046 | - $ref: '#/components/schemas/VideoChannel' |
diff --git a/support/nginx/peertube b/support/nginx/peertube index 2b1600d97..5d7b4f0f0 100644 --- a/support/nginx/peertube +++ b/support/nginx/peertube | |||
@@ -172,7 +172,7 @@ server { | |||
172 | 172 | ||
173 | # Bypass PeerTube for performance reasons. Optional. | 173 | # Bypass PeerTube for performance reasons. Optional. |
174 | # Should be consistent with client-overrides assets list in /server/controllers/client.ts | 174 | # Should be consistent with client-overrides assets list in /server/controllers/client.ts |
175 | location ~ ^/client/(assets/images/(icons/icon-36x36\.png|icons/icon-48x48\.png|icons/icon-72x72\.png|icons/icon-96x96\.png|icons/icon-144x144\.png|icons/icon-192x192\.png|icons/icon-512x512\.png|logo\.svg|favicon\.png|default-playlist\.jpg|default-avatar-account\.png|default-avatar-video-channel\.png))$ { | 175 | location ~ ^/client/(assets/images/(icons/icon-36x36\.png|icons/icon-48x48\.png|icons/icon-72x72\.png|icons/icon-96x96\.png|icons/icon-144x144\.png|icons/icon-192x192\.png|icons/icon-512x512\.png|logo\.svg|favicon\.png|default-playlist\.jpg|default-avatar-account\.png|default-avatar-account-48x48\.png|default-avatar-video-channel\.png|default-avatar-video-channel-48x48\.png))$ { |
176 | add_header Cache-Control "public, max-age=31536000, immutable"; # Cache 1 year | 176 | add_header Cache-Control "public, max-age=31536000, immutable"; # Cache 1 year |
177 | 177 | ||
178 | root /var/www/peertube; | 178 | root /var/www/peertube; |