diff options
author | Ms Kimsible <1877318+kimsible@users.noreply.github.com> | 2021-08-25 11:38:10 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-25 11:38:10 +0200 |
commit | 4e1592daa41f81667f914f37d36795e8c6c046c3 (patch) | |
tree | 8bed3af237b6d5d4da08af989c3824a168e6f3b6 | |
parent | 644800ef5588e08da2a8227f6d72751d3dca85db (diff) | |
download | PeerTube-4e1592daa41f81667f914f37d36795e8c6c046c3.tar.gz PeerTube-4e1592daa41f81667f914f37d36795e8c6c046c3.tar.zst PeerTube-4e1592daa41f81667f914f37d36795e8c6c046c3.zip |
Alert user for low quota and video auto-block on upload page (#4336)
* Replace wording of instance contact
* Add contact-us button to no-quota alert on upload page
* Add alert for accounts with auto-blocked videos on upload page
* Add alert for accounts without enough quota + refacto on upload page
* Using ng-container and ng-template
* Add alert for daily quota
* Add hook filter for upload page alert messages
* Add instance name as subtitle in contact modal
* Fix eslint max-len on string
* Fix missing word in quota left daily message - upload page
Co-authored-by: Kimsible <kimsible@users.noreply.github.com>
10 files changed, 121 insertions, 20 deletions
diff --git a/client/src/app/+about/about-instance/about-instance.component.html b/client/src/app/+about/about-instance/about-instance.component.html index 436d486ab..1026c4e0d 100644 --- a/client/src/app/+about/about-instance/about-instance.component.html +++ b/client/src/app/+about/about-instance/about-instance.component.html | |||
@@ -4,7 +4,7 @@ | |||
4 | <div class="about-instance-title"> | 4 | <div class="about-instance-title"> |
5 | <h1 i18n class="title">About {{ instanceName }}</h1> | 5 | <h1 i18n class="title">About {{ instanceName }}</h1> |
6 | 6 | ||
7 | <a routerLink="/about/contact" i18n *ngIf="isContactFormEnabled" class="contact-admin">Contact administrator</a> | 7 | <a routerLink="/about/contact" i18n *ngIf="isContactFormEnabled" class="contact-admin">Contact us</a> |
8 | </div> | 8 | </div> |
9 | 9 | ||
10 | <div class="instance-badges" *ngIf="categories.length !== 0 || languages.length !== 0"> | 10 | <div class="instance-badges" *ngIf="categories.length !== 0 || languages.length !== 0"> |
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.html b/client/src/app/+about/about-instance/contact-admin-modal.component.html index 8b6b707af..ed027af44 100644 --- a/client/src/app/+about/about-instance/contact-admin-modal.component.html +++ b/client/src/app/+about/about-instance/contact-admin-modal.component.html | |||
@@ -1,6 +1,6 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h1 i18n class="modal-title">Contact {{ instanceName }} administrator</h1> | 3 | <h1 i18n class="modal-title">Contact the administrator(s)<p class="modal-subtitle">{{ instanceName }}</p></h1> |
4 | <my-global-icon iconName="cross" aria-label="Close" tabindex="0" role="button" (click)="hide()" (keydown.enter)="hide()"></my-global-icon> | 4 | <my-global-icon iconName="cross" aria-label="Close" tabindex="0" role="button" (click)="hide()" (keydown.enter)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.scss b/client/src/app/+about/about-instance/contact-admin-modal.component.scss index c0b451b4e..e143a9dc6 100644 --- a/client/src/app/+about/about-instance/contact-admin-modal.component.scss +++ b/client/src/app/+about/about-instance/contact-admin-modal.component.scss | |||
@@ -1,6 +1,12 @@ | |||
1 | @use '_variables' as *; | 1 | @use '_variables' as *; |
2 | @use '_mixins' as *; | 2 | @use '_mixins' as *; |
3 | 3 | ||
4 | .modal-subtitle { | ||
5 | font-size: 16px; | ||
6 | line-height: 1rem; | ||
7 | margin-bottom: 0; | ||
8 | } | ||
9 | |||
4 | .modal-body { | 10 | .modal-body { |
5 | text-align: left; | 11 | text-align: left; |
6 | } | 12 | } |
diff --git a/client/src/app/+videos/+video-edit/video-add.component.html b/client/src/app/+videos/+video-edit/video-add.component.html index ac75d9ff8..b056c6e7a 100644 --- a/client/src/app/+videos/+video-edit/video-add.component.html +++ b/client/src/app/+videos/+video-edit/video-add.component.html | |||
@@ -1,18 +1,43 @@ | |||
1 | <div *ngIf="user.isUploadDisabled()" class="no-upload"> | 1 | <ng-template #AlertButtons> |
2 | <div class="alert alert-warning"> | 2 | <a i18n routerLink="/about/instance" *ngIf="!isContactFormEnabled" class="about-link">Read instance rules for help</a> |
3 | <div i18n>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</div> | 3 | <a i18n routerLink="/about/contact" *ngIf="isContactFormEnabled" class="contact-link">Contact us</a> |
4 | <a i18n routerLink="/about/instance" class="about-link">Read instance rules for help</a> | 4 | </ng-template> |
5 | |||
6 | <ng-container *ngIf="user.isUploadDisabled()"> | ||
7 | <div class="upload-message upload-disabled alert alert-warning"> | ||
8 | <div>{{ uploadMessages.noQuota }}</div> | ||
9 | <ng-template [ngTemplateOutlet]="AlertButtons"></ng-template> | ||
5 | </div> | 10 | </div> |
6 | <img src="/client/assets/images/mascot/defeated.svg" alt="defeated mascot"> | ||
7 | </div> | ||
8 | 11 | ||
9 | <div *ngIf="!user.isUploadDisabled()" class="margin-content"> | 12 | <div class="upload-image"> |
10 | <div class="alert alert-warning" *ngIf="isRootUser()" i18n> | 13 | <img src="/client/assets/images/mascot/defeated.svg" alt="defeated mascot"> |
14 | </div> | ||
15 | </ng-container> | ||
16 | |||
17 | <ng-container *ngIf="!user.isUploadDisabled()"> | ||
18 | <div *ngIf="user.isAutoBlocked()" class="upload-message auto-blocked alert alert-warning"> | ||
19 | <div>{{ uploadMessages.autoBlock }}</div> | ||
20 | <ng-template [ngTemplateOutlet]="AlertButtons" *ngIf="!user.hasNoQuotaLeft() && !user.hasNoQuotaLeftDaily()"></ng-template> | ||
21 | </div> | ||
22 | |||
23 | <div *ngIf="user.hasNoQuotaLeft()" class="upload-message quota-daily-left alert alert-warning"> | ||
24 | <div>{{ uploadMessages.quotaLeftDaily }}</div> | ||
25 | <ng-template [ngTemplateOutlet]="AlertButtons" *ngIf="!user.hasNoQuotaLeft()"></ng-template> | ||
26 | </div> | ||
27 | |||
28 | <div *ngIf="user.hasNoQuotaLeft()" class="upload-message quota-left alert alert-warning"> | ||
29 | <div>{{ uploadMessages.quotaLeft }}</div> | ||
30 | <ng-template [ngTemplateOutlet]="AlertButtons"></ng-template> | ||
31 | </div> | ||
32 | |||
33 | <div *ngIf="isRootUser()" class="upload-message root-user alert alert-warning" i18n> | ||
11 | We recommend you to not use the <strong>root</strong> user to publish your videos, since it's the super-admin account of your instance. | 34 | We recommend you to not use the <strong>root</strong> user to publish your videos, since it's the super-admin account of your instance. |
12 | <br /> | 35 | <br /> |
13 | Instead, <a routerLink="/admin/users">create a dedicated account</a> to upload your videos. | 36 | Instead, <a routerLink="/admin/users">create a dedicated account</a> to upload your videos. |
14 | </div> | 37 | </div> |
38 | </ng-container> | ||
15 | 39 | ||
40 | <div *ngIf="!user.isUploadDisabled()" class="margin-content"> | ||
16 | <my-user-quota *ngIf="!isInSecondStep() || secondStepType === 'go-live'" [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-quota> | 41 | <my-user-quota *ngIf="!isInSecondStep() || secondStepType === 'go-live'" [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-quota> |
17 | 42 | ||
18 | <div class="title-page title-page-single" *ngIf="isInSecondStep()"> | 43 | <div class="title-page title-page-single" *ngIf="isInSecondStep()"> |
diff --git a/client/src/app/+videos/+video-edit/video-add.component.scss b/client/src/app/+videos/+video-edit/video-add.component.scss index dea6fde36..26be86d29 100644 --- a/client/src/app/+videos/+video-edit/video-add.component.scss +++ b/client/src/app/+videos/+video-edit/video-add.component.scss | |||
@@ -6,18 +6,29 @@ $border-type: solid; | |||
6 | $border-color: #EAEAEA; | 6 | $border-color: #EAEAEA; |
7 | $nav-link-height: 40px; | 7 | $nav-link-height: 40px; |
8 | 8 | ||
9 | .no-upload { | 9 | .upload-message { |
10 | height: 100%; | ||
11 | width: 100%; | 10 | width: 100%; |
12 | text-align: center; | 11 | text-align: center; |
12 | font-size: 15px; | ||
13 | margin-bottom: 0; | ||
14 | |||
15 | &:last-child { | ||
16 | margin-bottom: 1rem; | ||
17 | } | ||
13 | 18 | ||
14 | .about-link { | 19 | .about-link, |
20 | .contact-link { | ||
15 | @include peertube-button-link; | 21 | @include peertube-button-link; |
16 | @include orange-button; | 22 | @include orange-button; |
17 | 23 | ||
18 | height: fit-content; | 24 | height: fit-content; |
19 | margin-top: 10px; | 25 | margin-top: 10px; |
20 | } | 26 | } |
27 | } | ||
28 | |||
29 | .upload-image { | ||
30 | width: 100%; | ||
31 | text-align: center; | ||
21 | 32 | ||
22 | img { | 33 | img { |
23 | margin-top: 10px; | 34 | margin-top: 10px; |
@@ -38,10 +49,6 @@ $nav-link-height: 40px; | |||
38 | padding-top: 20px; | 49 | padding-top: 20px; |
39 | } | 50 | } |
40 | 51 | ||
41 | .alert { | ||
42 | font-size: 15px; | ||
43 | } | ||
44 | |||
45 | ::ng-deep .video-add-nav { | 52 | ::ng-deep .video-add-nav { |
46 | border-bottom: $border-width $border-type $border-color; | 53 | border-bottom: $border-width $border-type $border-color; |
47 | margin: 20px 0 0 !important; | 54 | margin: 20px 0 0 !important; |
diff --git a/client/src/app/+videos/+video-edit/video-add.component.ts b/client/src/app/+videos/+video-edit/video-add.component.ts index 8606b8222..1443c64af 100644 --- a/client/src/app/+videos/+video-edit/video-add.component.ts +++ b/client/src/app/+videos/+video-edit/video-add.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, HostListener, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, HostListener, OnInit, ViewChild } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core' | 3 | import { AuthService, AuthUser, CanComponentDeactivate, HooksService, ServerService } from '@app/core' |
4 | import { HTMLServerConfig } from '@shared/models' | 4 | import { HTMLServerConfig } from '@shared/models' |
5 | import { VideoEditType } from './shared/video-edit.type' | 5 | import { VideoEditType } from './shared/video-edit.type' |
6 | import { VideoGoLiveComponent } from './video-add-components/video-go-live.component' | 6 | import { VideoGoLiveComponent } from './video-add-components/video-go-live.component' |
@@ -26,15 +26,27 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate { | |||
26 | 26 | ||
27 | activeNav: string | 27 | activeNav: string |
28 | 28 | ||
29 | uploadMessages: { | ||
30 | noQuota: string | ||
31 | autoBlock: string | ||
32 | quotaLeftDaily: string | ||
33 | quotaLeft: string | ||
34 | } | ||
35 | |||
29 | private serverConfig: HTMLServerConfig | 36 | private serverConfig: HTMLServerConfig |
30 | 37 | ||
31 | constructor ( | 38 | constructor ( |
32 | private auth: AuthService, | 39 | private auth: AuthService, |
40 | private hooks: HooksService, | ||
33 | private serverService: ServerService, | 41 | private serverService: ServerService, |
34 | private route: ActivatedRoute, | 42 | private route: ActivatedRoute, |
35 | private router: Router | 43 | private router: Router |
36 | ) {} | 44 | ) {} |
37 | 45 | ||
46 | get isContactFormEnabled () { | ||
47 | return this.serverConfig.email.enabled && this.serverConfig.contactForm.enabled | ||
48 | } | ||
49 | |||
38 | get userInformationLoaded () { | 50 | get userInformationLoaded () { |
39 | return this.auth.userInformationLoaded | 51 | return this.auth.userInformationLoaded |
40 | } | 52 | } |
@@ -49,6 +61,28 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate { | |||
49 | if (this.route.snapshot.fragment) { | 61 | if (this.route.snapshot.fragment) { |
50 | this.onNavChange(this.route.snapshot.fragment) | 62 | this.onNavChange(this.route.snapshot.fragment) |
51 | } | 63 | } |
64 | |||
65 | this.buildUploadMessages() | ||
66 | } | ||
67 | |||
68 | private async buildUploadMessages () { | ||
69 | // eslint-disable-next-line max-len | ||
70 | const noQuota = $localize`Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.` | ||
71 | // eslint-disable-next-line max-len | ||
72 | const autoBlock = $localize`Uploaded videos are reviewed before publishing for your account. If you want to add videos without moderation review, an admin must turn off your videos auto-block.` | ||
73 | // eslint-disable-next-line max-len | ||
74 | const quotaLeftDaily = $localize`Your daily video quota is insufficient. If you want to add more videos, you must wait for 24 hours or an admin must increase your daily quota.` | ||
75 | // eslint-disable-next-line max-len | ||
76 | const quotaLeft = $localize`Your video quota is insufficient. If you want to add more videos, an admin must increase your quota.` | ||
77 | |||
78 | const uploadMessages = { | ||
79 | noQuota, | ||
80 | autoBlock, | ||
81 | quotaLeftDaily, | ||
82 | quotaLeft | ||
83 | } | ||
84 | |||
85 | this.uploadMessages = await this.hooks.wrapObject(uploadMessages, 'common', 'filter:upload-page.alert-messages.edit.result') | ||
52 | } | 86 | } |
53 | 87 | ||
54 | onNavChange (newActiveNav: string) { | 88 | onNavChange (newActiveNav: string) { |
diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts index 7d03e1c40..5e1fb1c8d 100644 --- a/client/src/app/core/users/user.model.ts +++ b/client/src/app/core/users/user.model.ts | |||
@@ -133,4 +133,30 @@ export class User implements UserServerModel { | |||
133 | isUploadDisabled () { | 133 | isUploadDisabled () { |
134 | return this.videoQuota === 0 || this.videoQuotaDaily === 0 | 134 | return this.videoQuota === 0 || this.videoQuotaDaily === 0 |
135 | } | 135 | } |
136 | |||
137 | isAutoBlocked () { | ||
138 | return this.role === UserRole.USER && this.adminFlags !== UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST | ||
139 | } | ||
140 | |||
141 | hasNoQuotaLeft () { | ||
142 | // unlimited videoQuota | ||
143 | if (this.videoQuota === -1) return false | ||
144 | |||
145 | // no more videoQuota | ||
146 | if (!this.videoQuotaUsed) return true | ||
147 | |||
148 | // videoQuota left lower than 10% | ||
149 | return this.videoQuotaUsed > this.videoQuota * 0.9 | ||
150 | } | ||
151 | |||
152 | hasNoQuotaLeftDaily () { | ||
153 | // unlimited videoQuotaDaily | ||
154 | if (this.videoQuotaDaily === -1) return false | ||
155 | |||
156 | // no more videoQuotaDaily | ||
157 | if (!this.videoQuotaUsedDaily) return true | ||
158 | |||
159 | // videoQuotaDaily left lower than 10% | ||
160 | return this.videoQuotaUsedDaily > this.videoQuotaDaily * 0.9 | ||
161 | } | ||
136 | } | 162 | } |
diff --git a/server/models/user/user.ts b/server/models/user/user.ts index 20696b1f4..069d7266e 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts | |||
@@ -958,7 +958,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
958 | } | 958 | } |
959 | 959 | ||
960 | toMeFormattedJSON (this: MMyUserFormattable): MyUser { | 960 | toMeFormattedJSON (this: MMyUserFormattable): MyUser { |
961 | const formatted = this.toFormattedJSON() | 961 | const formatted = this.toFormattedJSON({ withAdminFlags: true }) |
962 | 962 | ||
963 | const specialPlaylists = this.Account.VideoPlaylists | 963 | const specialPlaylists = this.Account.VideoPlaylists |
964 | .map(p => ({ id: p.id, name: p.name, type: p.type })) | 964 | .map(p => ({ id: p.id, name: p.name, type: p.type })) |
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 1419ae820..318ff832a 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -290,7 +290,7 @@ describe('Test users', function () { | |||
290 | expect(user.account.description).to.be.null | 290 | expect(user.account.description).to.be.null |
291 | } | 291 | } |
292 | 292 | ||
293 | expect(userMe.adminFlags).to.be.undefined | 293 | expect(userMe.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST) |
294 | expect(userGet.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST) | 294 | expect(userGet.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST) |
295 | 295 | ||
296 | expect(userMe.specialPlaylists).to.have.lengthOf(1) | 296 | expect(userMe.specialPlaylists).to.have.lengthOf(1) |
diff --git a/shared/models/plugins/client/client-hook.model.ts b/shared/models/plugins/client/client-hook.model.ts index cedd1be61..aafc8c72b 100644 --- a/shared/models/plugins/client/client-hook.model.ts +++ b/shared/models/plugins/client/client-hook.model.ts | |||
@@ -58,6 +58,9 @@ export const clientFilterHookObject = { | |||
58 | // Filter left menu links | 58 | // Filter left menu links |
59 | 'filter:left-menu.links.create.result': true, | 59 | 'filter:left-menu.links.create.result': true, |
60 | 60 | ||
61 | // Filter upload page alert messages | ||
62 | 'filter:upload-page.alert-messages.edit.result': true, | ||
63 | |||
61 | // Filter videojs options built for PeerTube player | 64 | // Filter videojs options built for PeerTube player |
62 | 'filter:internal.player.videojs.options.result': true | 65 | 'filter:internal.player.videojs.options.result': true |
63 | } | 66 | } |