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:
parent
0579dee3b2
commit
dfe3f7b72e
|
@ -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 () {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -149,4 +149,8 @@ export class User implements UserServerModel {
|
|||
updateAccountAvatar (newAccountAvatar: Avatar) {
|
||||
this.account.updateAvatar(newAccountAvatar)
|
||||
}
|
||||
|
||||
isUploadDisabled () {
|
||||
return this.videoQuota === 0 || this.videoQuotaDaily === 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 = {}
|
||||
|
||||
|
|
Loading…
Reference in New Issue