Add alert and hide upload view when no upload is possible (#2966)

* Add alert and hide upload view when no upload is possible

* Add about instance link to alert

* Hide videos and imports links when no upload is possible

* Correct curly spacing lint

* Put logic canUpload to User model + add isHidden param to to-menu-dropdown

* Use canSeeVideoLinks from user model

* Rename and change logic canUpload to isUploadDisabled

* Use isDisplayed() method intead of isHidden value

* Refactor client and check videos count using quota

Co-authored-by: kimsible <kimsible@users.noreply.github.com>
Co-authored-by: Chocobozzz <me@florianbigard.com>
This commit is contained in:
Kim 2020-07-28 15:18:38 +02:00 committed by GitHub
parent 0579dee3b2
commit dfe3f7b72e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 186 additions and 57 deletions

View File

@ -74,12 +74,24 @@ export class AdminComponent implements OnInit {
})
}
if (this.hasUsersRight()) this.menuEntries.push({ label: this.i18n('Users'), routerLink: '/admin/users' })
if (this.hasUsersRight()) {
this.menuEntries.push({ label: this.i18n('Users'), routerLink: '/admin/users' })
}
if (this.hasServerFollowRight()) this.menuEntries.push(federationItems)
if (this.hasAbusesRight() || this.hasVideoBlocklistRight()) this.menuEntries.push(moderationItems)
if (this.hasConfigRight()) this.menuEntries.push({ label: this.i18n('Configuration'), routerLink: '/admin/config' })
if (this.hasPluginsRight()) this.menuEntries.push({ label: this.i18n('Plugins/Themes'), routerLink: '/admin/plugins' })
if (this.hasJobsRight() || this.hasLogsRight() || this.hasDebugRight()) this.menuEntries.push({ label: this.i18n('System'), routerLink: '/admin/system' })
if (this.hasConfigRight()) {
this.menuEntries.push({ label: this.i18n('Configuration'), routerLink: '/admin/config' })
}
if (this.hasPluginsRight()) {
this.menuEntries.push({ label: this.i18n('Plugins/Themes'), routerLink: '/admin/plugins' })
}
if (this.hasJobsRight() || this.hasLogsRight() || this.hasDebugRight()) {
this.menuEntries.push({ label: this.i18n('System'), routerLink: '/admin/system' })
}
}
hasUsersRight () {

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core'
import { ServerService } from '@app/core'
import { AuthService, ServerService, AuthUser } from '@app/core'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ServerConfig } from '@shared/models'
import { TopMenuDropdownParam } from '../shared/shared-main/misc/top-menu-dropdown.component'
@ -11,11 +11,13 @@ import { TopMenuDropdownParam } from '../shared/shared-main/misc/top-menu-dropdo
})
export class MyAccountComponent implements OnInit {
menuEntries: TopMenuDropdownParam[] = []
user: AuthUser
private serverConfig: ServerConfig
constructor (
private serverService: ServerService,
private authService: AuthService,
private i18n: I18n
) { }
@ -24,6 +26,20 @@ export class MyAccountComponent implements OnInit {
this.serverService.getConfig()
.subscribe(config => this.serverConfig = config)
this.user = this.authService.getUser()
this.authService.userInformationLoaded.subscribe(
() => this.buildMenu()
)
}
isVideoImportEnabled () {
const importConfig = this.serverConfig.import.videos
return importConfig.http.enabled || importConfig.torrent.enabled
}
private buildMenu () {
const libraryEntries: TopMenuDropdownParam = {
label: this.i18n('My library'),
children: [
@ -35,7 +51,8 @@ export class MyAccountComponent implements OnInit {
{
label: this.i18n('My videos'),
routerLink: '/my-account/videos',
iconName: 'videos'
iconName: 'videos',
isDisplayed: () => this.user.canSeeVideosLink
},
{
label: this.i18n('My playlists'),
@ -45,7 +62,7 @@ export class MyAccountComponent implements OnInit {
{
label: this.i18n('My subscriptions'),
routerLink: '/my-account/subscriptions',
iconName: 'subscriptions'
iconName: 'inbox-full'
},
{
label: this.i18n('My history'),
@ -59,7 +76,8 @@ export class MyAccountComponent implements OnInit {
libraryEntries.children.push({
label: 'My imports',
routerLink: '/my-account/video-imports',
iconName: 'cloud-download'
iconName: 'cloud-download',
isDisplayed: () => this.user.canSeeVideosLink
})
}
@ -97,11 +115,4 @@ export class MyAccountComponent implements OnInit {
miscEntries
]
}
isVideoImportEnabled () {
const importConfig = this.serverConfig.import.videos
return importConfig.http.enabled || importConfig.torrent.enabled
}
}

View File

@ -1,4 +1,12 @@
<div class="margin-content">
<div *ngIf="user.isUploadDisabled()" class="no-upload">
<div class="alert alert-warning">
<div i18n>Sorry, the upload feature is disabled for your account. If you want to add videos, an admin must unlock your quota.</div>
<a i18n routerLink="/about/instance" class="about-link">Read instance rules for help</a>
</div>
<img src="/client/assets/images/mascot/defeated.svg" alt="defeated mascot">
</div>
<div *ngIf="!user.isUploadDisabled()" class="margin-content">
<div class="alert alert-warning" *ngIf="isRootUser()" i18n>
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.
<br />
@ -45,4 +53,4 @@
</div>
<div [ngbNavOutlet]="nav"></div>
</div>
</div>

View File

@ -6,6 +6,34 @@ $border-type: solid;
$border-color: #EAEAEA;
$nav-link-height: 40px;
.no-upload {
height: 100%;
width: 100%;
text-align: center;
.about-link {
@include peertube-button-link;
@include orange-button;
height: fit-content;
margin-top: 10px;
}
img {
margin-top: 10px;
margin-bottom: 75px;
width: 220px;
height: auto;
}
@media screen and (max-height: 600px) {
img {
margin-top: 5px;
width: 160px;
}
}
}
.margin-content {
padding-top: 20px;
}

View File

@ -1,5 +1,5 @@
import { Component, HostListener, OnInit, ViewChild } from '@angular/core'
import { AuthService, CanComponentDeactivate, ServerService, User } from '@app/core'
import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core'
import { ServerConfig } from '@shared/models'
import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component'
import { VideoImportUrlComponent } from './video-add-components/video-import-url.component'
@ -15,7 +15,7 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate {
@ViewChild('videoImportUrl') videoImportUrl: VideoImportUrlComponent
@ViewChild('videoImportTorrent') videoImportTorrent: VideoImportTorrentComponent
user: User = null
user: AuthUser = null
secondStepType: 'upload' | 'import-url' | 'import-torrent'
videoName: string
@ -37,6 +37,8 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate {
this.serverService.getConfig()
.subscribe(config => this.serverConfig = config)
this.user = this.auth.getUser()
}
onFirstStepDone (type: 'upload' | 'import-url' | 'import-torrent', videoName: string) {
@ -80,6 +82,6 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate {
}
isRootUser () {
return this.auth.getUser().username === 'root'
return this.user.username === 'root'
}
}

View File

@ -1,3 +1,5 @@
import { Observable, of } from 'rxjs'
import { map } from 'rxjs/operators'
import { User } from '@app/core/users/user.model'
import { peertubeLocalStorage } from '@app/helpers/peertube-web-storage'
import {
@ -7,7 +9,8 @@ import {
NSFWPolicyType,
User as ServerUserModel,
UserRight,
UserRole
UserRole,
UserVideoQuota
} from '@shared/models'
export type TokenOptions = {
@ -74,6 +77,8 @@ export class AuthUser extends User implements ServerMyUserModel {
tokens: Tokens
specialPlaylists: MyUserSpecialPlaylist[]
canSeeVideosLink = true
static load () {
const usernameLocalStorage = peertubeLocalStorage.getItem(this.KEYS.USERNAME)
if (usernameLocalStorage) {
@ -150,4 +155,26 @@ export class AuthUser extends User implements ServerMyUserModel {
peertubeLocalStorage.setItem(AuthUser.KEYS.AUTO_PLAY_VIDEO, JSON.stringify(this.autoPlayVideo))
this.tokens.save()
}
computeCanSeeVideosLink (quotaObservable: Observable<UserVideoQuota>): Observable<boolean> {
if (!this.isUploadDisabled()) {
this.canSeeVideosLink = true
return of(this.canSeeVideosLink)
}
// Check if the user has videos
return quotaObservable.pipe(
map(({ videoQuotaUsed }) => {
if (videoQuotaUsed !== 0) {
// User already uploaded videos, so it can see the link
this.canSeeVideosLink = true
} else {
// No videos, no upload so the user don't need to see the videos link
this.canSeeVideosLink = false
}
return this.canSeeVideosLink
})
)
}
}

View File

@ -149,4 +149,8 @@ export class User implements UserServerModel {
updateAccountAvatar (newAccountAvatar: Avatar) {
this.account.updateAvatar(newAccountAvatar)
}
isUploadDisabled () {
return this.videoQuota === 0 || this.videoQuotaDaily === 0
}
}

View File

@ -81,7 +81,7 @@
<div *ngIf="isLoggedIn" class="panel-block">
<div i18n class="block-title">MY LIBRARY</div>
<a routerLink="/my-account/videos" routerLinkActive="active">
<a *ngIf="user.canSeeVideosLink" routerLink="/my-account/videos" routerLinkActive="active">
<my-global-icon iconName="videos" aria-hidden="true"></my-global-icon>
<ng-container i18n>Videos</ng-container>
</a>

View File

@ -1,21 +1,25 @@
import { HotkeysService } from 'angular2-hotkeys'
import * as debug from 'debug'
import { switchMap } from 'rxjs/operators'
import { Component, OnInit, ViewChild } from '@angular/core'
import { AuthService, AuthStatus, RedirectService, ScreenService, ServerService, User, UserService } from '@app/core'
import { AuthService, AuthStatus, AuthUser, RedirectService, ScreenService, ServerService, UserService } from '@app/core'
import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ServerConfig, UserRight, VideoConstant } from '@shared/models'
const logger = debug('peertube:menu:MenuComponent')
@Component({
selector: 'my-menu',
templateUrl: './menu.component.html',
styleUrls: [ './menu.component.scss' ]
styleUrls: ['./menu.component.scss']
})
export class MenuComponent implements OnInit {
@ViewChild('languageChooserModal', { static: true }) languageChooserModal: LanguageChooserComponent
@ViewChild('quickSettingsModal', { static: true }) quickSettingsModal: QuickSettingsModalComponent
user: User
user: AuthUser
isLoggedIn: boolean
userHasAdminAccess = false
@ -25,7 +29,7 @@ export class MenuComponent implements OnInit {
private languages: VideoConstant<string>[] = []
private serverConfig: ServerConfig
private routesPerRight: { [ role in UserRight ]?: string } = {
private routesPerRight: { [role in UserRight]?: string } = {
[UserRight.MANAGE_USERS]: '/admin/users',
[UserRight.MANAGE_SERVER_FOLLOW]: '/admin/friends',
[UserRight.MANAGE_ABUSES]: '/admin/moderation/abuses',
@ -62,21 +66,30 @@ export class MenuComponent implements OnInit {
.subscribe(config => this.serverConfig = config)
this.isLoggedIn = this.authService.isLoggedIn()
if (this.isLoggedIn === true) this.user = this.authService.getUser()
this.computeIsUserHasAdminAccess()
if (this.isLoggedIn === true) {
this.user = this.authService.getUser()
this.computeVideosLink()
}
this.computeAdminAccess()
this.authService.loginChangedSource.subscribe(
status => {
if (status === AuthStatus.LoggedIn) {
this.isLoggedIn = true
this.user = this.authService.getUser()
this.computeIsUserHasAdminAccess()
console.log('Logged in.')
this.computeAdminAccess()
this.computeVideosLink()
logger('Logged in.')
} else if (status === AuthStatus.LoggedOut) {
this.isLoggedIn = false
this.user = undefined
this.computeIsUserHasAdminAccess()
console.log('Logged out.')
this.computeAdminAccess()
logger('Logged out.')
} else {
console.error('Unknown auth status: ' + status)
}
@ -84,15 +97,15 @@ export class MenuComponent implements OnInit {
)
this.hotkeysService.cheatSheetToggle
.subscribe(isOpen => this.helpVisible = isOpen)
.subscribe(isOpen => this.helpVisible = isOpen)
this.serverService.getVideoLanguages()
.subscribe(languages => {
this.languages = languages
.subscribe(languages => {
this.languages = languages
this.authService.userInformationLoaded
.subscribe(() => this.buildUserLanguages())
})
this.authService.userInformationLoaded
.subscribe(() => this.buildUserLanguages())
})
}
get language () {
@ -116,7 +129,7 @@ export class MenuComponent implements OnInit {
isRegistrationAllowed () {
return this.serverConfig.signup.allowed &&
this.serverConfig.signup.allowedForCurrentIP
this.serverConfig.signup.allowedForCurrentIP
}
getFirstAdminRightAvailable () {
@ -172,7 +185,7 @@ export class MenuComponent implements OnInit {
this.user.webTorrentEnabled = !this.user.webTorrentEnabled
this.userService.updateMyProfile({ webTorrentEnabled: this.user.webTorrentEnabled })
.subscribe(() => this.authService.refreshUserInformation())
.subscribe(() => this.authService.refreshUserInformation())
}
langForLocale (localeId: string) {
@ -188,18 +201,28 @@ export class MenuComponent implements OnInit {
}
if (!this.user.videoLanguages) {
this.videoLanguages = [ this.i18n('any language') ]
this.videoLanguages = [this.i18n('any language')]
return
}
this.videoLanguages = this.user.videoLanguages
.map(locale => this.langForLocale(locale))
.map(value => value === undefined ? '?' : value)
.map(locale => this.langForLocale(locale))
.map(value => value === undefined ? '?' : value)
}
private computeIsUserHasAdminAccess () {
private computeAdminAccess () {
const right = this.getFirstAdminRightAvailable()
this.userHasAdminAccess = right !== undefined
}
private computeVideosLink () {
this.authService.userInformationLoaded
.pipe(
switchMap(() => this.user.computeCanSeeVideosLink(this.userService.getMyVideoQuotaUsed()))
).subscribe(res => {
if (res === true) logger('User can see videos link.')
else logger('User cannot see videos link.')
})
}
}

View File

@ -1,9 +1,9 @@
<div class="sub-menu" [ngClass]="{ 'no-scroll': isModalOpened }">
<ng-container *ngFor="let menuEntry of menuEntries; index as id">
<a *ngIf="menuEntry.routerLink" [routerLink]="menuEntry.routerLink" routerLinkActive="active" class="title-page title-page-settings">{{ menuEntry.label }}</a>
<a *ngIf="menuEntry.routerLink && isDisplayed(menuEntry)" [routerLink]="menuEntry.routerLink" routerLinkActive="active" class="title-page title-page-settings">{{ menuEntry.label }}</a>
<div *ngIf="!menuEntry.routerLink" ngbDropdown class="parent-entry"
<div *ngIf="!menuEntry.routerLink && isDisplayed(menuEntry)" ngbDropdown class="parent-entry"
#dropdown="ngbDropdown" autoClose="outside">
<span
*ngIf="isInSmallView"
@ -25,11 +25,15 @@
</span>
<div ngbDropdownMenu>
<a *ngFor="let menuChild of menuEntry.children" class="dropdown-item" [ngClass]="{ icon: hasIcons, active: suffixLabels[menuEntry.label] === menuChild.label }" [routerLink]="menuChild.routerLink">
<my-global-icon *ngIf="menuChild.iconName" [iconName]="menuChild.iconName" aria-hidden="true"></my-global-icon>
<ng-container *ngFor="let menuChild of menuEntry.children">
<a *ngIf="isDisplayed(menuChild)" class="dropdown-item"
[ngClass]="{ icon: hasIcons, active: suffixLabels[menuEntry.label] === menuChild.label }"
[routerLink]="menuChild.routerLink">
<my-global-icon *ngIf="menuChild.iconName" [iconName]="menuChild.iconName" aria-hidden="true"></my-global-icon>
{{ menuChild.label }}
</a>
{{ menuChild.label }}
</a>
</ng-container>
</div>
</div>
</ng-container>
@ -39,13 +43,15 @@
<div class="modal-body">
<ng-container *ngFor="let menuEntry of menuEntries; index as id">
<div [ngClass]="{ hidden: id !== currentMenuEntryIndex }">
<a *ngFor="let menuChild of menuEntry.children"
[ngClass]="{ icon: hasIcons }"
[routerLink]="menuChild.routerLink" routerLinkActive="active" (click)="dismissOtherModals()">
<my-global-icon *ngIf="menuChild.iconName" [iconName]="menuChild.iconName" aria-hidden="true"></my-global-icon>
<ng-container *ngFor="let menuChild of menuEntry.children">
<a *ngIf="isDisplayed(menuChild)"
[ngClass]="{ icon: hasIcons }"
[routerLink]="menuChild.routerLink" routerLinkActive="active" (click)="dismissOtherModals()">
<my-global-icon *ngIf="menuChild.iconName" [iconName]="menuChild.iconName" aria-hidden="true"></my-global-icon>
{{ menuChild.label }}
</a>
{{ menuChild.label }}
</a>
</ng-container>
</div>
</ng-container>
</div>

View File

@ -9,12 +9,14 @@ import { NgbDropdown, NgbModal } from '@ng-bootstrap/ng-bootstrap'
export type TopMenuDropdownParam = {
label: string
routerLink?: string
isDisplayed?: () => boolean // Default: () => true
children?: {
label: string
routerLink: string
iconName?: GlobalIconName
isDisplayed?: () => boolean // Default: () => true
}[]
}
@ -92,6 +94,12 @@ export class TopMenuDropdownComponent implements OnInit, OnDestroy {
this.modalService.dismissAll()
}
isDisplayed (obj: { isDisplayed?: () => boolean }) {
if (typeof obj.isDisplayed !== 'function') return true
return obj.isDisplayed()
}
private updateChildLabels (path: string) {
this.suffixLabels = {}