mirror of
https://github.com/AleziaKurdis/overte.git
synced 2025-04-07 04:53:28 +02:00
Merge pull request #16046 from huffman/feat/mac-launcher-tags
DEV-316: Add build tags to Mac launcher
This commit is contained in:
commit
264abe7d0c
7 changed files with 150 additions and 74 deletions
|
@ -22,7 +22,10 @@
|
|||
|
||||
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
|
||||
CGFloat prog = (float)totalBytesWritten/totalBytesExpectedToWrite;
|
||||
NSLog(@"interface downloaded %d%%", (int)(100.0*prog));
|
||||
|
||||
if ((int)(100.0 * prog) != (int)self.progressPercentage) {
|
||||
NSLog(@"interface downloaded %d%%", (int)(100.0*prog));
|
||||
}
|
||||
|
||||
self.progressPercentage = (100.0 * prog);
|
||||
[[Launcher sharedLauncher] updateProgressIndicator];
|
||||
|
|
|
@ -1,31 +1,20 @@
|
|||
#import "LatestBuildRequest.h"
|
||||
#import "Launcher.h"
|
||||
#import "Settings.h"
|
||||
#import "Interface.h"
|
||||
|
||||
@implementation LatestBuildRequest
|
||||
|
||||
- (NSInteger) getCurrentVersion {
|
||||
NSInteger currentVersion;
|
||||
@try {
|
||||
NSString* interfaceAppPath = [[Launcher.sharedLauncher getAppPath] stringByAppendingString:@"interface.app"];
|
||||
NSError* error = nil;
|
||||
Interface* interface = [[Interface alloc] initWith:interfaceAppPath];
|
||||
currentVersion = [interface getVersion:&error];
|
||||
if (currentVersion == 0 && error != nil) {
|
||||
NSLog(@"can't get version from interface, falling back to settings: %@", error);
|
||||
currentVersion = [Settings.sharedSettings latestBuildVersion];
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
NSLog(@"an exception was thrown: %@", exception);
|
||||
currentVersion = [Settings.sharedSettings latestBuildVersion];
|
||||
}
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
- (void) requestLatestBuildInfo {
|
||||
NSString* buildsURL = [[[NSProcessInfo processInfo] environment] objectForKey:@"HQ_LAUNCHER_BUILDS_URL"];
|
||||
|
||||
if ([buildsURL length] == 0) {
|
||||
buildsURL = @"https://thunder.highfidelity.com/builds/api/tags/latest?format=json";
|
||||
}
|
||||
|
||||
NSLog(@"Making request for builds to: %@", buildsURL);
|
||||
|
||||
NSMutableURLRequest* request = [NSMutableURLRequest new];
|
||||
[request setURL:[NSURL URLWithString:@"https://thunder.highfidelity.com/builds/api/tags/latest?format=json"]];
|
||||
[request setURL:[NSURL URLWithString:buildsURL]];
|
||||
[request setHTTPMethod:@"GET"];
|
||||
[request setValue:@USER_AGENT_STRING forHTTPHeaderField:@"User-Agent"];
|
||||
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
||||
|
@ -39,7 +28,7 @@
|
|||
NSLog(@"Latest Build Request Response: %ld", [ne statusCode]);
|
||||
Launcher* sharedLauncher = [Launcher sharedLauncher];
|
||||
|
||||
if ([ne statusCode] == 500) {
|
||||
if (error || [ne statusCode] == 500) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[sharedLauncher displayErrorPage];
|
||||
});
|
||||
|
@ -60,36 +49,26 @@
|
|||
NSFileManager* fileManager = [NSFileManager defaultManager];
|
||||
NSArray* values = [json valueForKey:@"results"];
|
||||
NSDictionary* launcherValues = [json valueForKey:@"launcher"];
|
||||
NSDictionary* value = [values objectAtIndex:0];
|
||||
|
||||
NSString* defaultBuildTag = [json valueForKey:@"default_tag"];
|
||||
|
||||
NSString* launcherVersion = [launcherValues valueForKey:@"version"];
|
||||
NSString* launcherUrl = [[launcherValues valueForKey:@"mac"] valueForKey:@"url"];
|
||||
NSString* buildNumber = [value valueForKey:@"latest_version"];
|
||||
NSDictionary* installers = [value objectForKey:@"installers"];
|
||||
NSDictionary* macInstallerObject = [installers objectForKey:@"mac"];
|
||||
NSString* macInstallerUrl = [macInstallerObject valueForKey:@"zip_url"];
|
||||
|
||||
BOOL appDirectoryExist = [fileManager fileExistsAtPath:[[sharedLauncher getAppPath] stringByAppendingString:@"interface.app"]];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
NSInteger currentVersion = [self getCurrentVersion];
|
||||
NSInteger currentLauncherVersion = atoi(LAUNCHER_BUILD_VERSION);
|
||||
NSLog(@"Latest Build Request -> current launcher version %ld", currentLauncherVersion);
|
||||
NSLog(@"Latest Build Request -> latest launcher version %ld", launcherVersion.integerValue);
|
||||
NSLog(@"Latest Build Request -> launcher url %@", launcherUrl);
|
||||
NSLog(@"Latest Build Request -> does build directory exist: %@", appDirectoryExist ? @"TRUE" : @"FALSE");
|
||||
NSLog(@"Latest Build Request -> current version: %ld", currentVersion);
|
||||
NSLog(@"Latest Build Request -> latest version: %ld", buildNumber.integerValue);
|
||||
NSLog(@"Latest Build Request -> mac url: %@", macInstallerUrl);
|
||||
BOOL latestVersionAvailable = (currentVersion != buildNumber.integerValue);
|
||||
BOOL latestLauncherVersionAvailable = (currentLauncherVersion != launcherVersion.integerValue);
|
||||
[[Settings sharedSettings] buildVersion:buildNumber.integerValue];
|
||||
|
||||
BOOL shouldDownloadInterface = (latestVersionAvailable || !appDirectoryExist);
|
||||
NSLog(@"Latest Build Request -> SHOULD DOWNLOAD: %@", shouldDownloadInterface ? @"TRUE" : @"FALSE");
|
||||
[sharedLauncher shouldDownloadLatestBuild:shouldDownloadInterface :macInstallerUrl
|
||||
:latestLauncherVersionAvailable :launcherUrl];
|
||||
[sharedLauncher shouldDownloadLatestBuild:values
|
||||
:defaultBuildTag
|
||||
:latestLauncherVersionAvailable
|
||||
:launcherUrl];
|
||||
});
|
||||
}];
|
||||
|
||||
|
|
|
@ -55,6 +55,8 @@ struct LatestBuildInfo {
|
|||
@property (nonatomic) BOOL waitingForInterfaceToTerminate;
|
||||
@property (nonatomic) BOOL shouldDownloadInterface;
|
||||
@property (nonatomic) BOOL latestBuildRequestFinished;
|
||||
@property (nonatomic, assign) NSArray* latestBuilds;
|
||||
@property (nonatomic, assign) NSString* defaultBuildTag;
|
||||
@property (nonatomic, assign) NSTimer* updateProgressIndicatorTimer;
|
||||
@property (nonatomic, assign, readwrite) ProcessState processState;
|
||||
@property (nonatomic, assign, readwrite) LoginError loginError;
|
||||
|
@ -70,6 +72,7 @@ struct LatestBuildInfo {
|
|||
- (void) domainContentDownloadFinished;
|
||||
- (void) domainScriptsDownloadFinished;
|
||||
- (void) setDomainURLInfo:(NSString*) aDomainURL :(NSString*) aDomainContentUrl :(NSString*) aDomainScriptsUrl;
|
||||
- (void) setOrganizationBuildTag:(NSString*) organizationBuildTag;
|
||||
- (void) organizationRequestFinished:(BOOL) aOriginzationAccepted;
|
||||
- (BOOL) loginShouldSetErrorState;
|
||||
- (void) displayErrorPage;
|
||||
|
@ -81,7 +84,9 @@ struct LatestBuildInfo {
|
|||
- (void) setCurrentProcessState:(ProcessState) aProcessState;
|
||||
- (void) setLoginErrorState:(LoginError) aLoginError;
|
||||
- (LoginError) getLoginErrorState;
|
||||
- (void) shouldDownloadLatestBuild:(BOOL) shouldDownload :(NSString*) downloadUrl :(BOOL) newLauncherAvailable :(NSString*) launcherUrl;
|
||||
- (void) updateLatestBuildInfo;
|
||||
- (void) shouldDownloadLatestBuild:(NSArray*) latestBuilds :(NSString*) defaultBuildTag :(BOOL) newLauncherAvailable :(NSString*) launcherUrl;
|
||||
- (void) tryDownloadLatestBuild:(BOOL)progressScreenAlreadyDisplayed;
|
||||
- (void) interfaceFinishedDownloading;
|
||||
- (NSString*) getDownloadPathForContentAndScripts;
|
||||
- (void) launchInterface;
|
||||
|
@ -97,11 +102,9 @@ struct LatestBuildInfo {
|
|||
- (NSString*) getDownloadFilename;
|
||||
- (void) startUpdateProgressIndicatorTimer;
|
||||
- (void) endUpdateProgressIndicatorTimer;
|
||||
- (BOOL) isLoadedIn;
|
||||
- (BOOL) isLoggedIn;
|
||||
- (NSString*) getAppPath;
|
||||
- (void) updateProgressIndicator;
|
||||
- (void) setLatestBuildInfo:(struct LatestBuildInfo) latestBuildInfo;
|
||||
- (struct LatestBuildInfo) getLatestBuildInfo;
|
||||
|
||||
+ (id) sharedLauncher;
|
||||
@end
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#import "ErrorViewController.h"
|
||||
#import "Settings.h"
|
||||
#import "NSTask+NSTaskExecveAdditions.h"
|
||||
#import "Interface.h"
|
||||
|
||||
@interface Launcher ()
|
||||
|
||||
|
@ -241,7 +242,7 @@ static BOOL const DELETE_ZIP_FILES = TRUE;
|
|||
return self.waitingForInterfaceToTerminate;
|
||||
}
|
||||
|
||||
- (BOOL) isLoadedIn
|
||||
- (BOOL) isLoggedIn
|
||||
{
|
||||
return [[Settings sharedSettings] isLoggedIn];
|
||||
}
|
||||
|
@ -255,6 +256,11 @@ static BOOL const DELETE_ZIP_FILES = TRUE;
|
|||
[[Settings sharedSettings] setDomainUrl:aDomainURL];
|
||||
}
|
||||
|
||||
- (void) setOrganizationBuildTag:(NSString*) organizationBuildTag;
|
||||
{
|
||||
[[Settings sharedSettings] setOrganizationBuildTag:organizationBuildTag];
|
||||
}
|
||||
|
||||
- (NSString*) getAppPath
|
||||
{
|
||||
return [self getDownloadPathForContentAndScripts];
|
||||
|
@ -275,13 +281,26 @@ static BOOL const DELETE_ZIP_FILES = TRUE;
|
|||
self.displayName = aDiplayName;
|
||||
}
|
||||
|
||||
- (NSInteger) getCurrentVersion {
|
||||
NSInteger currentVersion;
|
||||
@try {
|
||||
NSString* interfaceAppPath = [[self getAppPath] stringByAppendingString:@"interface.app"];
|
||||
NSError* error = nil;
|
||||
Interface* interface = [[Interface alloc] initWith:interfaceAppPath];
|
||||
currentVersion = [interface getVersion:&error];
|
||||
if (currentVersion == 0 && error != nil) {
|
||||
NSLog(@"can't get version from interface: %@", error);
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
NSLog(@"an exception was thrown while getting current interface version: %@", exception);
|
||||
currentVersion = 0;
|
||||
}
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
- (void) domainContentDownloadFinished
|
||||
{
|
||||
if (self.shouldDownloadInterface) {
|
||||
[self.downloadInterface downloadInterface: self.interfaceDownloadUrl];
|
||||
return;
|
||||
}
|
||||
[self interfaceFinishedDownloading];
|
||||
[self tryDownloadLatestBuild:TRUE];
|
||||
}
|
||||
|
||||
- (void) domainScriptsDownloadFinished
|
||||
|
@ -337,6 +356,7 @@ static BOOL const DELETE_ZIP_FILES = TRUE;
|
|||
{
|
||||
self.credentialsAccepted = aOriginzationAccepted;
|
||||
if (aOriginzationAccepted) {
|
||||
[self updateLatestBuildInfo];
|
||||
[self.credentialsRequest confirmCredentials:self.username : self.password];
|
||||
} else {
|
||||
LoginScreen* loginScreen = [[LoginScreen alloc] initWithNibName:@"LoginScreen" bundle:nil];
|
||||
|
@ -349,42 +369,26 @@ static BOOL const DELETE_ZIP_FILES = TRUE;
|
|||
return YES;
|
||||
}
|
||||
|
||||
- (struct LatestBuildInfo) getLatestBuildInfo
|
||||
{
|
||||
return self.buildInfo;
|
||||
}
|
||||
|
||||
- (void) setLatestBuildInfo:(struct LatestBuildInfo) latestBuildInfo
|
||||
{
|
||||
self.buildInfo = latestBuildInfo;
|
||||
}
|
||||
|
||||
-(void) showLoginScreen
|
||||
{
|
||||
LoginScreen* loginScreen = [[LoginScreen alloc] initWithNibName:@"LoginScreen" bundle:nil];
|
||||
[[[[NSApplication sharedApplication] windows] objectAtIndex:0] setContentViewController: loginScreen];
|
||||
}
|
||||
|
||||
- (void) shouldDownloadLatestBuild:(BOOL) shouldDownload :(NSString*) downloadUrl :(BOOL) newLauncherAvailable :(NSString*) launcherUrl
|
||||
- (void) shouldDownloadLatestBuild:(NSArray*) latestBuilds :(NSString*) defaultBuildTag :(BOOL) newLauncherAvailable :(NSString*) launcherUrl
|
||||
{
|
||||
self.latestBuilds = [[NSArray alloc] initWithArray:latestBuilds copyItems:true];
|
||||
self.defaultBuildTag = defaultBuildTag;
|
||||
|
||||
[self updateLatestBuildInfo];
|
||||
|
||||
NSDictionary* launcherArguments = [LauncherCommandlineArgs arguments];
|
||||
if (newLauncherAvailable && ![launcherArguments valueForKey: @"--noUpdate"]) {
|
||||
[self.downloadLauncher downloadLauncher: launcherUrl];
|
||||
} else {
|
||||
self.shouldDownloadInterface = shouldDownload;
|
||||
self.interfaceDownloadUrl = downloadUrl;
|
||||
self.latestBuildRequestFinished = TRUE;
|
||||
if ([self isLoadedIn]) {
|
||||
Launcher* sharedLauncher = [Launcher sharedLauncher];
|
||||
[sharedLauncher setCurrentProcessState:CHECKING_UPDATE];
|
||||
if (shouldDownload) {
|
||||
ProcessScreen* processScreen = [[ProcessScreen alloc] initWithNibName:@"ProcessScreen" bundle:nil];
|
||||
[[[[NSApplication sharedApplication] windows] objectAtIndex:0] setContentViewController: processScreen];
|
||||
[self startUpdateProgressIndicatorTimer];
|
||||
[self.downloadInterface downloadInterface: downloadUrl];
|
||||
return;
|
||||
}
|
||||
[self interfaceFinishedDownloading];
|
||||
if ([self isLoggedIn]) {
|
||||
[self tryDownloadLatestBuild:FALSE];
|
||||
} else {
|
||||
[[NSApplication sharedApplication] activateIgnoringOtherApps:TRUE];
|
||||
[self showLoginScreen];
|
||||
|
@ -392,6 +396,67 @@ static BOOL const DELETE_ZIP_FILES = TRUE;
|
|||
}
|
||||
}
|
||||
|
||||
// The latest builds are always retrieved on application start because they contain not only
|
||||
// the latest interface builds, but also the latest launcher builds, which are required to know if
|
||||
// we need to self-update first. The interface builds are categorized by build tag, and we may
|
||||
// not know at application start which build tag we should be using. There are 2 scenarios where
|
||||
// we call this function to determine our build tag and the correct build:
|
||||
//
|
||||
// 1. If we are logged in, we will have our build tag and can immediately get the correct build
|
||||
// after receiving the builds.
|
||||
// 2. If we are not logged in, we need to wait until we have logged in and received the org
|
||||
// metadata for the user. The latest build info also needs to be updated _before_ downloading
|
||||
// the content set cache because the progress bar value depends on it.
|
||||
//
|
||||
- (void) updateLatestBuildInfo {
|
||||
NSLog(@"Updating latest build info");
|
||||
|
||||
NSInteger currentVersion = [self getCurrentVersion];
|
||||
NSInteger latestVersion = 0;
|
||||
Launcher* sharedLauncher = [Launcher sharedLauncher];
|
||||
[sharedLauncher setCurrentProcessState:CHECKING_UPDATE];
|
||||
BOOL newVersionAvailable = false;
|
||||
NSString* url = @"";
|
||||
NSString* buildTag = [[Settings sharedSettings] organizationBuildTag];
|
||||
if ([buildTag length] == 0) {
|
||||
buildTag = self.defaultBuildTag;
|
||||
}
|
||||
|
||||
for (NSDictionary* build in self.latestBuilds) {
|
||||
NSString* name = [build valueForKey:@"name"];
|
||||
NSLog(@"Checking %@", name);
|
||||
if ([name isEqual:buildTag]) {
|
||||
url = [[[build objectForKey:@"installers"] objectForKey:@"mac"] valueForKey:@"zip_url"];
|
||||
NSString* thisLatestVersion = [build valueForKey:@"latest_version"];
|
||||
latestVersion = thisLatestVersion.integerValue;
|
||||
newVersionAvailable = currentVersion != latestVersion;
|
||||
NSLog(@"Using %@, %ld", name, latestVersion);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.shouldDownloadInterface = newVersionAvailable;
|
||||
self.interfaceDownloadUrl = url;
|
||||
|
||||
NSLog(@"Updating latest build info, currentVersion=%ld, latestVersion=%ld, %@ %@",
|
||||
currentVersion, latestVersion, (self.shouldDownloadInterface ? @"Yes" : @"No"), self.interfaceDownloadUrl);
|
||||
}
|
||||
|
||||
- (void) tryDownloadLatestBuild:(BOOL)progressScreenAlreadyDisplayed
|
||||
{
|
||||
if (self.shouldDownloadInterface) {
|
||||
if (!progressScreenAlreadyDisplayed) {
|
||||
ProcessScreen* processScreen = [[ProcessScreen alloc] initWithNibName:@"ProcessScreen" bundle:nil];
|
||||
[[[[NSApplication sharedApplication] windows] objectAtIndex:0] setContentViewController: processScreen];
|
||||
[self startUpdateProgressIndicatorTimer];
|
||||
}
|
||||
[self.downloadInterface downloadInterface: self.interfaceDownloadUrl];
|
||||
return;
|
||||
}
|
||||
|
||||
[self interfaceFinishedDownloading];
|
||||
}
|
||||
|
||||
-(void)runAutoupdater
|
||||
{
|
||||
NSTask* task = [[NSTask alloc] init];
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#include <CommonCrypto/CommonDigest.h>
|
||||
#include <CommonCrypto/CommonHMAC.h>
|
||||
#import "Launcher.h"
|
||||
#import "Settings.h"
|
||||
|
||||
|
||||
static NSString* const organizationURL = @"https://orgs.highfidelity.com/organizations/";
|
||||
|
@ -76,11 +77,16 @@ static NSString* const organizationURL = @"https://orgs.highfidelity.com/organiz
|
|||
}
|
||||
NSString* domainURL = [json valueForKey:@"domain"];
|
||||
NSString* contentSetURL = [json valueForKey:@"content_set_url"];
|
||||
NSString* buildTag = [json valueForKey:@"build_tag"];
|
||||
if (buildTag == nil) {
|
||||
buildTag = @"";
|
||||
}
|
||||
|
||||
if (domainURL != nil && contentSetURL != nil) {
|
||||
NSLog(@"Organization: getting org file successful");
|
||||
[sharedLauncher setDomainURLInfo:[json valueForKey:@"domain"] :[json valueForKey:@"content_set_url"] :nil];
|
||||
[sharedLauncher setLoginErrorState: NONE];
|
||||
[[Settings sharedSettings] setOrganizationBuildTag:buildTag];
|
||||
[sharedLauncher organizationRequestFinished:TRUE];
|
||||
} else {
|
||||
NSLog(@"Organization: Either domainURL: %@ or contentSetURL: %@ json entries are invalid", domainURL, contentSetURL);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
@property (nonatomic, assign) BOOL loggedIn;
|
||||
@property (nonatomic, assign) NSString* domain;
|
||||
@property (nonatomic, assign) NSString* launcher;
|
||||
@property (nonatomic, assign) NSString* _organizationBuildTag;
|
||||
- (NSInteger) latestBuildVersion;
|
||||
- (BOOL) isLoggedIn;
|
||||
- (void) login:(BOOL)aLoggedIn;
|
||||
|
@ -15,6 +16,8 @@
|
|||
- (NSString*) getLaucnherPath;
|
||||
- (void) setDomainUrl:(NSString*) aDomainUrl;
|
||||
- (NSString*) getDomainUrl;
|
||||
- (void) setOrganizationBuildTag:(NSString*) aOrganizationBuildTag;
|
||||
- (NSString*) organizationBuildTag;
|
||||
- (void) save;
|
||||
+ (id) sharedSettings;
|
||||
@end
|
||||
|
|
|
@ -32,13 +32,18 @@
|
|||
NSError * err;
|
||||
NSData *data =[jsonString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSDictionary * json;
|
||||
if(data!=nil){
|
||||
if (data != nil) {
|
||||
json = (NSDictionary *)[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&err];
|
||||
|
||||
self.loggedIn = [[json valueForKey:@"loggedIn"] boolValue];
|
||||
self.build = [[json valueForKey:@"build_version"] integerValue];
|
||||
self.launcher = [json valueForKey:@"luancherPath"];
|
||||
self.domain = [json valueForKey:@"domain"];
|
||||
self.organizationBuildTag = [json valueForKey:@"organizationBuildTag"];
|
||||
if ([self.organizationBuildTag length] == 0) {
|
||||
self.organizationBuildTag = @"";
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +58,9 @@
|
|||
[NSString stringWithFormat:@"%ld", self.build], @"build_version",
|
||||
self.loggedIn ? @"TRUE" : @"FALSE", @"loggedIn",
|
||||
self.domain, @"domain",
|
||||
self.launcher, @"launcherPath", nil];
|
||||
self.launcher, @"launcherPath",
|
||||
self.organizationBuildTag, @"organizationBuildTag",
|
||||
nil];
|
||||
NSError * err;
|
||||
NSData * jsonData = [NSJSONSerialization dataWithJSONObject:json options:0 error:&err];
|
||||
NSString * jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||
|
@ -114,6 +121,16 @@
|
|||
return self.domain;
|
||||
}
|
||||
|
||||
- (void) setOrganizationBuildTag:(NSString*) aOrganizationBuildTag
|
||||
{
|
||||
self._organizationBuildTag = aOrganizationBuildTag;
|
||||
}
|
||||
|
||||
- (NSString*) organizationBuildTag
|
||||
{
|
||||
return self._organizationBuildTag;
|
||||
}
|
||||
|
||||
- (NSString*) getLaucnherPath
|
||||
{
|
||||
return self.launcher;
|
||||
|
|
Loading…
Reference in a new issue