2
0

Compare commits

..

6 Commits

Author SHA1 Message Date
Maarten Billemont
d704f451a3 Fixed issue causing emergency generator password button to not respond. 2017-04-17 22:27:36 -04:00
Maarten Billemont
2c9ab5d153 Fixed issue when cancelling touchID login. 2017-04-17 22:13:01 -04:00
Maarten Billemont
d5d33da12f Fixed UI issues with passwords list and drop-down animation + support for phrase and name default types.
[FIXED]     Fixed issues with animating changes in the passwords list during certain & multiple events.
[FIXED]     Slightly broken UI prior to drop-down animation & improved animation a bit.
[ADDED]     Phrase & Name default password types.
2017-04-17 21:57:08 -04:00
Maarten Billemont
cbef1a611b Update Mac binary to 2.5-mac-2 2017-04-16 13:03:15 -04:00
Maarten Billemont
0a1f215a1a Style login name, add login generated gear, improve logic for when to show login name. 2017-04-15 10:57:52 -04:00
Maarten Billemont
907d2a8ca6 Fixed key disappearing from NSCache after suspension and not being reloaded from keychain. 2017-04-15 02:28:11 -04:00
18 changed files with 586 additions and 543 deletions

View File

@@ -430,58 +430,38 @@ NSOperationQueue *_mpwQueue = nil;
- (NSString *)resolveLoginForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey {
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter( group );
__block NSString *result = nil;
[self resolveLoginForSite:site usingKey:siteKey result:^(NSString *result_) {
result = result_;
dispatch_group_leave( group );
}];
dispatch_group_wait( group, DISPATCH_TIME_FOREVER );
return result;
return PearlAwait( ^(void (^setResult)(id)) {
[self resolveLoginForSite:site usingKey:siteKey result:^(NSString *result_) {
setResult( result_ );
}];
} );
}
- (NSString *)resolvePasswordForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey {
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter( group );
__block NSString *result = nil;
[self resolvePasswordForSite:site usingKey:siteKey result:^(NSString *result_) {
result = result_;
dispatch_group_leave( group );
}];
dispatch_group_wait( group, DISPATCH_TIME_FOREVER );
return result;
return PearlAwait( ^(void (^setResult)(id)) {
[self resolvePasswordForSite:site usingKey:siteKey result:^(NSString *result_) {
setResult( result_ );
}];
} );
}
- (NSString *)resolveAnswerForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey {
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter( group );
__block NSString *result = nil;
[self resolveAnswerForSite:site usingKey:siteKey result:^(NSString *result_) {
result = result_;
dispatch_group_leave( group );
}];
dispatch_group_wait( group, DISPATCH_TIME_FOREVER );
return result;
return PearlAwait( ^(void (^setResult)(id)) {
[self resolveAnswerForSite:site usingKey:siteKey result:^(NSString *result_) {
setResult( result_ );
}];
} );
}
- (NSString *)resolveAnswerForQuestion:(MPSiteQuestionEntity *)question usingKey:(MPKey *)siteKey {
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter( group );
__block NSString *result = nil;
[self resolveAnswerForQuestion:question usingKey:siteKey result:^(NSString *result_) {
result = result_;
dispatch_group_leave( group );
}];
dispatch_group_wait( group, DISPATCH_TIME_FOREVER );
return result;
return PearlAwait( ^(void (^setResult)(id)) {
[self resolveAnswerForQuestion:question usingKey:siteKey result:^(NSString *result_) {
setResult( result_ );
}];
} );
}
- (void)resolveLoginForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey result:(void ( ^ )(NSString *result))resultBlock {
@@ -498,10 +478,12 @@ NSOperationQueue *_mpwQueue = nil;
else
algorithm = site.algorithm;
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
resultBlock( loginName || !loginGenerated? loginName:
[algorithm generateLoginForSiteNamed:name usingKey:siteKey] );
} );
if (!loginGenerated || [loginName length])
resultBlock( loginName );
else
PearlNotMainQueue( ^{
resultBlock( [algorithm generateLoginForSiteNamed:name usingKey:siteKey] );
} );
}
- (void)resolvePasswordForSite:(MPSiteEntity *)site usingKey:(MPKey *)siteKey result:(void ( ^ )(NSString *result))resultBlock {
@@ -533,9 +515,8 @@ NSOperationQueue *_mpwQueue = nil;
else
algorithm = site.algorithm;
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
NSString *result = [algorithm generatePasswordForSiteNamed:name ofType:type withCounter:counter usingKey:siteKey];
resultBlock( result );
PearlNotMainQueue( ^{
resultBlock( [algorithm generatePasswordForSiteNamed:name ofType:type withCounter:counter usingKey:siteKey] );
} );
break;
}
@@ -549,9 +530,8 @@ NSOperationQueue *_mpwQueue = nil;
NSData *encryptedContent = ((MPStoredSiteEntity *)site).contentObject;
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
NSString *result = [self decryptContent:encryptedContent usingKey:siteKey];
resultBlock( result );
PearlNotMainQueue( ^{
resultBlock( [self decryptContent:encryptedContent usingKey:siteKey] );
} );
break;
}
@@ -563,9 +543,8 @@ NSOperationQueue *_mpwQueue = nil;
NSDictionary *siteQuery = [self queryForDevicePrivateSiteNamed:site.name];
NSData *encryptedContent = [PearlKeyChain dataOfItemForQuery:siteQuery];
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
NSString *result = [self decryptContent:encryptedContent usingKey:siteKey];
resultBlock( result );
PearlNotMainQueue( ^{
resultBlock( [self decryptContent:encryptedContent usingKey:siteKey] );
} );
break;
}
@@ -584,9 +563,8 @@ NSOperationQueue *_mpwQueue = nil;
else
algorithm = site.algorithm;
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
NSString *result = [algorithm generateAnswerForSiteNamed:name onQuestion:nil usingKey:siteKey];
resultBlock( result );
PearlNotMainQueue( ^{
resultBlock( [algorithm generateAnswerForSiteNamed:name onQuestion:nil usingKey:siteKey] );
} );
}
@@ -605,9 +583,8 @@ NSOperationQueue *_mpwQueue = nil;
else if ([[MPAppDelegate_Shared get] isFeatureUnlocked:MPProductGenerateAnswers])
algorithm = question.site.algorithm;
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
NSString *result = [algorithm generateAnswerForSiteNamed:name onQuestion:keyword usingKey:siteKey];
resultBlock( result );
PearlNotMainQueue( ^{
resultBlock( [algorithm generateAnswerForSiteNamed:name onQuestion:keyword usingKey:siteKey] );
} );
}

View File

@@ -72,14 +72,23 @@ static NSDictionary *createKeyQuery(MPUserEntity *user, BOOL newItem, MPKeyOrigi
MPKeyOrigin keyOrigin;
NSDictionary *keyQuery = createKeyQuery( user, NO, &keyOrigin );
NSData *keyData = [PearlKeyChain dataOfItemForQuery:keyQuery];
if (!keyData) {
id<MPAlgorithm> keyAlgorithm = user.algorithm;
MPKey *key = [[MPKey alloc] initForFullName:user.name withKeyResolver:^NSData *(id<MPAlgorithm> algorithm) {
return ![algorithm isEqual:keyAlgorithm]? nil:
PearlMainQueueAwait( (id)^{
return [PearlKeyChain dataOfItemForQuery:keyQuery];
} );
} keyOrigin:keyOrigin];
if ([key keyIDForAlgorithm:user.algorithm])
inf( @"Found key in keychain for user: %@", user.userID );
else {
inf( @"No key found in keychain for user: %@", user.userID );
return nil;
key = nil;
}
inf( @"Found key in keychain for user: %@", user.userID );
return [[MPKey alloc] initForFullName:user.name withKeyData:keyData forAlgorithm:user.algorithm keyOrigin:keyOrigin];
return key;
}
- (void)storeSavedKeyFor:(MPUserEntity *)user {
@@ -230,28 +239,22 @@ static NSDictionary *createKeyQuery(MPUserEntity *user, BOOL newItem, MPKeyOrigi
NSString *content;
while (!(content = [site.algorithm storedPasswordForSite:(MPStoredSiteEntity *)site usingKey:recoverKey])) {
// Failed to decrypt site with the current recoveryKey. Ask user for a new one to use.
__block NSString *masterPassword = nil;
NSString *masterPassword = nil;
#ifdef PEARL_UIKIT
dispatch_group_t recoverPasswordGroup = dispatch_group_create();
dispatch_group_enter( recoverPasswordGroup );
[PearlAlert showAlertWithTitle:@"Enter Old Master Password"
message:PearlString( @"Your old master password is required to migrate the stored password for %@",
site.name )
viewStyle:UIAlertViewStyleSecureTextInput
initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
@try {
masterPassword = PearlAwait( ^(void (^setResult)(id)) {
[PearlAlert showAlertWithTitle:@"Enter Old Master Password"
message:PearlString(
@"Your old master password is required to migrate the stored password for %@",
site.name )
viewStyle:UIAlertViewStyleSecureTextInput
initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
if (buttonIndex_ == [alert_ cancelButtonIndex])
// Don't Migrate
return;
masterPassword = [alert_ textFieldAtIndex:0].text;
}
@finally {
dispatch_group_leave( recoverPasswordGroup );
}
} cancelTitle:@"Don't Migrate" otherTitles:@"Migrate", nil];
dispatch_group_wait( recoverPasswordGroup, DISPATCH_TIME_FOREVER );
setResult( nil );
else
setResult( [alert_ textFieldAtIndex:0].text );
} cancelTitle:@"Don't Migrate" otherTitles:@"Migrate", nil];
} );
#endif
if (!masterPassword)
// Don't Migrate

View File

@@ -28,12 +28,12 @@ typedef NS_ENUM( NSUInteger, MPKeyOrigin ) {
@interface MPKey : NSObject
@property(nonatomic, readonly) NSString *fullName;
@property(nonatomic, readonly) MPKeyOrigin origin;
@property(nonatomic, readonly, copy) NSString *fullName;
- (instancetype)initForFullName:(NSString *)fullName withMasterPassword:(NSString *)masterPassword;
- (instancetype)initForFullName:(NSString *)fullName withKeyData:(NSData *)keyData
forAlgorithm:(id<MPAlgorithm>)algorithm keyOrigin:(MPKeyOrigin)origin;
- (instancetype)initForFullName:(NSString *)fullName withKeyResolver:(NSData *( ^ )(id<MPAlgorithm>))keyResolver
keyOrigin:(MPKeyOrigin)origin;
- (NSData *)keyIDForAlgorithm:(id<MPAlgorithm>)algorithm;
- (NSData *)keyDataForAlgorithm:(id<MPAlgorithm>)algorithm;

View File

@@ -19,9 +19,9 @@
@interface MPKey()
@property(nonatomic) NSString *fullName;
@property(nonatomic) MPKeyOrigin origin;
@property(nonatomic) NSString *masterPassword;
@property(nonatomic, copy) NSString *fullName;
@property(nonatomic, copy) NSData *( ^keyResolver )(id<MPAlgorithm>);
@end
@@ -31,25 +31,22 @@
- (instancetype)initForFullName:(NSString *)fullName withMasterPassword:(NSString *)masterPassword {
return [self initForFullName:fullName withKeyResolver:^NSData *(id<MPAlgorithm> algorithm) {
return [algorithm keyDataForFullName:self.fullName withMasterPassword:masterPassword];
} keyOrigin:MPKeyOriginMasterPassword];
}
- (instancetype)initForFullName:(NSString *)fullName withKeyResolver:(NSData *( ^ )(id<MPAlgorithm>))keyResolver
keyOrigin:(MPKeyOrigin)origin {
if (!(self = [super init]))
return nil;
_keyCache = [NSCache new];
self.fullName = fullName;
self.origin = MPKeyOriginMasterPassword;
self.masterPassword = masterPassword;
return self;
}
- (instancetype)initForFullName:(NSString *)fullName withKeyData:(NSData *)keyData
forAlgorithm:(id<MPAlgorithm>)algorithm keyOrigin:(MPKeyOrigin)origin {
if (!(self = [self initForFullName:fullName withMasterPassword:nil]))
return nil;
self.origin = origin;
[_keyCache setObject:keyData forKey:algorithm];
self.fullName = fullName;
self.keyResolver = keyResolver;
return self;
}
@@ -61,15 +58,17 @@
- (NSData *)keyDataForAlgorithm:(id<MPAlgorithm>)algorithm {
NSData *keyData = [_keyCache objectForKey:algorithm];
if (keyData)
@synchronized (self) {
NSData *keyData = [_keyCache objectForKey:algorithm];
if (keyData)
return keyData;
keyData = self.keyResolver( algorithm );
if (keyData)
[_keyCache setObject:keyData forKey:algorithm];
return keyData;
keyData = [algorithm keyDataForFullName:self.fullName withMasterPassword:self.masterPassword];
if (keyData)
[_keyCache setObject:keyData forKey:algorithm];
return keyData;
}
}
- (NSData *)keyDataForAlgorithm:(id<MPAlgorithm>)algorithm trimmedLength:(NSUInteger)subKeyLength {
@@ -80,7 +79,7 @@
- (BOOL)isEqualToKey:(MPKey *)key {
return [self.fullName isEqualToString:key.fullName] && [self.masterPassword isEqualToString:self.masterPassword];
return [[self keyIDForAlgorithm:MPAlgorithmDefault] isEqualToData:[key keyIDForAlgorithm:MPAlgorithmDefault]];
}
- (BOOL)isEqual:(id)object {

View File

@@ -28,10 +28,10 @@
[super windowDidLoad];
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillCloseNotification object:self.window
queue:nil usingBlock:^(NSNotification *note) {
[MPMacAppDelegate get].initialWindowController = nil;
}];
PearlAddNotificationObserver( NSWindowWillCloseNotification, self.window, nil, ^(id host, NSNotification *note) {
PearlRemoveNotificationObserversFrom( host );
[MPMacAppDelegate get].initialWindowController = nil;
} );
}
#pragma mark - Actions

View File

@@ -41,40 +41,38 @@
[self replaceFonts:self.window.contentView];
prof_rewind( @"replaceFonts" );
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowDidBecomeKeyNotification object:self.window
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
PearlAddNotificationObserver( NSWindowDidBecomeKeyNotification, self.window, [NSOperationQueue mainQueue],
^(id host, NSNotification *note) {
prof_new( @"didBecomeKey" );
[self.window makeKeyAndOrderFront:nil];
prof_rewind( @"fadeIn" );
[self updateUser];
prof_finish( @"updateUser" );
}];
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillCloseNotification object:self.window
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
} );
PearlAddNotificationObserver( NSWindowWillCloseNotification, self.window, [NSOperationQueue mainQueue],
^(id host, NSNotification *note) {
PearlRemoveNotificationObservers();
NSWindow *sheet = [self.window attachedSheet];
if (sheet)
[self.window endSheet:sheet];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillResignActiveNotification object:nil
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
} );
PearlAddNotificationObserver( NSApplicationWillResignActiveNotification, nil, [NSOperationQueue mainQueue],
^(id host, NSNotification *note) {
[self.window close];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:MPSignedInNotification object:nil
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
[self updateUser];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:MPSignedOutNotification object:nil
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
[self updateUser];
}];
} );
PearlAddNotificationObserver( MPSignedInNotification, nil, [NSOperationQueue mainQueue], ^(id host, NSNotification *note) {
[self updateUser];
} );
PearlAddNotificationObserver( MPSignedOutNotification, nil, [NSOperationQueue mainQueue], ^(id host, NSNotification *note) {
[self updateUser];
} );
[self observeKeyPath:@"sitesController.selection" withBlock:^(id from, id to, NSKeyValueChange cause, id _self) {
prof_new( @"sitesController.selection" );
[_self updateSelection];
prof_finish( @"updateSelection" );
}];
prof_rewind( @"observers" );
NSSearchFieldCell *siteFieldCell = (NSSearchFieldCell *)self.siteField.cell;
NSSearchFieldCell *siteFieldCell = self.siteField.cell;
siteFieldCell.searchButtonCell = nil;
siteFieldCell.cancelButtonCell = nil;

View File

@@ -28,7 +28,7 @@
@property(weak, nonatomic) IBOutlet UISegmentedControl *typeControl;
@property(weak, nonatomic) IBOutlet UILabel *counterLabel;
@property(weak, nonatomic) IBOutlet UIActivityIndicatorView *activity;
@property(weak, nonatomic) IBOutlet UILabel *passwordLabel;
@property(weak, nonatomic) IBOutlet UIButton *passwordButton;
@property(weak, nonatomic) IBOutlet UIView *tipContainer;
- (IBAction)controlChanged:(UIControl *)control;

View File

@@ -81,21 +81,19 @@
[self updatePassword];
}
- (IBAction)copyPassword:(UITapGestureRecognizer *)recognizer {
- (IBAction)copyPassword:(id)sender {
if (recognizer.state == UIGestureRecognizerStateEnded) {
NSString *sitePassword = self.passwordLabel.text;
if ([sitePassword length]) {
[UIPasteboard generalPasteboard].string = sitePassword;
[UIView animateWithDuration:0.3f animations:^{
self.tipContainer.alpha = 1;
} completion:^(BOOL finished) {
if (finished)
PearlMainQueueAfter( 3, ^{
self.tipContainer.alpha = 0;
} );
}];
}
NSString *sitePassword = [self.passwordButton titleForState:UIControlStateNormal];
if ([sitePassword length]) {
[UIPasteboard generalPasteboard].string = sitePassword;
[UIView animateWithDuration:0.3f animations:^{
self.tipContainer.alpha = 1;
} completion:^(BOOL finished) {
if (finished)
PearlMainQueueAfter( 3, ^{
self.tipContainer.alpha = 0;
} );
}];
}
}
@@ -106,7 +104,7 @@
NSString *fullName = self.fullNameField.text;
NSString *masterPassword = self.masterPasswordField.text;
self.passwordLabel.text = nil;
[self.passwordButton setTitle:nil forState:UIControlStateNormal];
[self.activity startAnimating];
[_emergencyKeyQueue cancelAllOperations];
[_emergencyKeyQueue addOperationWithBlock:^{
@@ -128,7 +126,7 @@
NSUInteger siteCounter = (NSUInteger)self.counterStepper.value;
self.counterLabel.text = strf( @"%lu", (unsigned long)siteCounter );
self.passwordLabel.text = nil;
[self.passwordButton setTitle:nil forState:UIControlStateNormal];
[self.activity startAnimating];
[_emergencyPasswordQueue cancelAllOperations];
[_emergencyPasswordQueue addOperationWithBlock:^{
@@ -138,7 +136,7 @@
PearlMainQueue( ^{
[self.activity stopAnimating];
self.passwordLabel.text = sitePassword;
[self.passwordButton setTitle:sitePassword forState:UIControlStateNormal];
} );
}];
}

View File

@@ -28,6 +28,7 @@
@property(nonatomic, strong) IBOutlet UITextField *passwordField;
@property(nonatomic, strong) IBOutlet UIView *loginNameContainer;
@property(nonatomic, strong) IBOutlet UITextField *loginNameField;
@property(nonatomic, strong) IBOutlet UILabel *loginNameGenerated;
@property(nonatomic, strong) IBOutlet UILabel *strengthLabel;
@property(nonatomic, strong) IBOutlet UILabel *counterLabel;
@property(nonatomic, strong) IBOutlet UIButton *counterButton;
@@ -199,7 +200,7 @@
atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES];
if (textField == self.loginNameField)
self.loginNameButton.titleLabel.alpha = [self.loginNameField.text length] || self.loginNameField.enabled? 0: 1;
self.loginNameButton.hidden = [self.loginNameField.attributedText length] || self.loginNameField.enabled;
}
- (IBAction)textFieldDidChange:(UITextField *)textField {
@@ -224,7 +225,7 @@
if (textField == self.passwordField || textField == self.loginNameField) {
textField.enabled = NO;
NSString *text = textField.text;
NSString *text = [textField.attributedText string]?: textField.text;
[MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPSiteEntity *site = [self siteInContext:context];
@@ -235,10 +236,8 @@
if ([site.algorithm savePassword:text toSite:site usingKey:[MPiOSAppDelegate get].key])
[PearlOverlay showTemporaryOverlayWithTitle:@"Password Updated" dismissAfter:2];
}
else if (textField == self.loginNameField &&
((site.loginGenerated && ![text length]) ||
(!site.loginGenerated && ![text isEqualToString:site.loginName]))) {
if (site.loginGenerated || !([site.loginName isEqualToString:text] || (!text && !site.loginName))) {
else if (textField == self.loginNameField) {
if (![text isEqualToString:[site.algorithm resolveLoginForSite:site usingKey:[MPiOSAppDelegate get].key]]) {
site.loginGenerated = NO;
site.loginName = text;
@@ -508,7 +507,6 @@
self.answersButton.gone = ![[MPiOSAppDelegate get] isFeatureUnlocked:MPProductGenerateAnswers];
BOOL settingsMode = self.mode == MPPasswordCellModeSettings;
self.loginNameContainer.alpha = settingsMode || mainSite.loginGenerated || [mainSite.loginName length]? 0.7f: 0;
self.loginNameField.textColor = [UIColor colorWithHexString:mainSite.loginGenerated? @"5E636D": @"6D5E63"];
self.modeButton.alpha = self.transientSite? 0: settingsMode? 0.5f: 0.1f;
self.counterLabel.alpha = self.counterButton.alpha = mainSite.type & MPSiteTypeClassGenerated? 0.5f: 0;
self.modeButton.selected = settingsMode;
@@ -520,13 +518,21 @@
[self.passwordField resignFirstResponder];
}
if ([[MPiOSAppDelegate get] isFeatureUnlocked:MPProductGenerateLogins])
[self.loginNameButton setTitle:@"Tap to generate username or use pencil to save one" forState:UIControlStateNormal];
[self.loginNameButton setTitle:@"Tap here to ⚙ generate username or the pencil to type one" forState:UIControlStateNormal];
else
[self.loginNameButton setTitle:@"Tap the pencil to save a username" forState:UIControlStateNormal];
[self.loginNameButton setTitle:@"Tap the pencil to type a username" forState:UIControlStateNormal];
// Site Name
[self updateSiteName:mainSite];
// Site Counter
if ([mainSite isKindOfClass:[MPGeneratedSiteEntity class]])
self.counterLabel.text = strf( @"%lu", (unsigned long)((MPGeneratedSiteEntity *)mainSite).counter );
// Site Login Name
self.loginNameField.enabled = self.passwordField.enabled = //
[self.loginNameField isFirstResponder] || [self.passwordField isFirstResponder];
// Site Password
self.passwordField.secureTextEntry = [[MPiOSConfig get].hidePasswords boolValue];
self.passwordField.attributedPlaceholder = stra(
@@ -534,12 +540,15 @@
mainSite.type & MPSiteTypeClassGenerated? strl( @"..." ): @"", @{
NSForegroundColorAttributeName: [UIColor whiteColor]
} );
// Calculate Fields
[MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPSiteEntity *site = [self siteInContext:context];
MPKey *key = [MPiOSAppDelegate get].key;
if (!key)
return;
BOOL loginGenerated = site.loginGenerated;
NSString *password = nil, *loginName = [site resolveLoginUsingKey:key];
MPSiteType transientType = [[MPiOSAppDelegate get] activeUserInContext:context].defaultType?: MPSiteTypeGeneratedLong;
if (self.transientSite && transientType & MPSiteTypeClassGenerated)
@@ -559,13 +568,15 @@
BOOL requiresExplicitMigration = site.requiresExplicitMigration;
PearlMainQueue( ^{
self.loginNameField.text = loginName;
self.passwordField.text = password;
self.strengthLabel.text = timeToCrackString;
self.loginNameButton.titleLabel.alpha = [loginName length] || self.loginNameField.enabled? 0: 1;
self.loginNameGenerated.hidden = !loginGenerated;
self.loginNameField.attributedText =
strarm( stra( loginName?: @"", self.siteNameLabel.textAttributes ), NSParagraphStyleAttributeName, nil );
self.loginNameButton.hidden = [loginName length] || self.loginNameField.enabled;
if (![password length]) {
self.indicatorView.alpha = 1;
self.indicatorView.hidden = NO;
[self.indicatorView removeFromSuperview];
[self.modeScrollView addSubview:self.indicatorView];
[self.contentView addConstraintsWithVisualFormat:@"V:[indicator][target]" options:NSLayoutFormatAlignAllCenterX
@@ -575,7 +586,7 @@
}];
}
else if (requiresExplicitMigration) {
self.indicatorView.alpha = 1;
self.indicatorView.hidden = NO;
[self.indicatorView removeFromSuperview];
[self.modeScrollView addSubview:self.indicatorView];
[self.contentView addConstraintsWithVisualFormat:@"V:[indicator][target]" options:NSLayoutFormatAlignAllCenterX
@@ -585,18 +596,10 @@
}];
}
else
self.indicatorView.alpha = 0;
self.indicatorView.hidden = YES;
} );
}];
// Site Counter
if ([mainSite isKindOfClass:[MPGeneratedSiteEntity class]])
self.counterLabel.text = strf( @"%lu", (unsigned long)((MPGeneratedSiteEntity *)mainSite).counter );
// Site Login Name
self.loginNameField.enabled = self.passwordField.enabled = //
[self.loginNameField isFirstResponder] || [self.passwordField isFirstResponder];
[self.contentView layoutIfNeeded];
}];
}
@@ -616,8 +619,9 @@
range:NSMakeRange( s, [self.fuzzyGroups[f] length] )];
}
[attributedSiteName appendAttributedString:stra(
strf( @" - %@", self.transientSite? @"Tap to create": [site.algorithm shortNameOfType:site.type] ), @{} )];
if (self.transientSite)
[attributedSiteName appendAttributedString:stra( @" Tap to create", @{} )];
self.siteNameLabel.attributedText = attributedSiteName;
}

View File

@@ -46,6 +46,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
NSUInteger _transientItem;
NSCharacterSet *_siteNameAcceptableCharactersSet;
NSArray *_fuzzyGroups;
NSMutableArray *_passwordCollectionViewUpdatesBatch;
}
#pragma mark - Life
@@ -62,6 +63,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
_backgroundColor = self.passwordCollectionView.backgroundColor;
_darkenedBackgroundColor = [_backgroundColor colorWithAlphaComponent:0.6f];
_transientItem = NSNotFound;
_passwordCollectionViewUpdatesBatch = [NSMutableArray arrayWithCapacity:4];
self.view.backgroundColor = [UIColor clearColor];
[self.passwordCollectionView automaticallyAdjustInsetsForKeyboard];
@@ -181,10 +183,13 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
Weakify( self );
if (controller == _fetchedResultsController) {
@try {
[self.passwordCollectionView performBatchUpdates:^{
[self fetchedItemsDidUpdate];
@synchronized (_passwordCollectionViewUpdatesBatch) {
[_passwordCollectionViewUpdatesBatch addObject:[^{
Strongify( self );
switch (type) {
case NSFetchedResultsChangeInsert:
[self.passwordCollectionView insertItemsAtIndexPaths:@[ newIndexPath ]];
@@ -193,18 +198,35 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
[self.passwordCollectionView deleteItemsAtIndexPaths:@[ indexPath ]];
break;
case NSFetchedResultsChangeMove:
[self.passwordCollectionView moveItemAtIndexPath:indexPath toIndexPath:newIndexPath];
if (![indexPath isEqual:newIndexPath])
[self.passwordCollectionView moveItemAtIndexPath:indexPath toIndexPath:newIndexPath];
break;
case NSFetchedResultsChangeUpdate:
[self.passwordCollectionView reloadItemsAtIndexPaths:@[ indexPath ]];
break;
}
} completion:nil];
}
@catch (NSException *exception) {
wrn( @"While updating password cells: %@", [exception fullDescription] );
[self.passwordCollectionView reloadData];
} copy]];
}
[controller.managedObjectContext performBlock:^{
PearlMainQueueOperation( ^{
@try {
[self.passwordCollectionView performBatchUpdates:^{
[self updateTransientItem];
@synchronized (_passwordCollectionViewUpdatesBatch) {
for (VoidBlock block in _passwordCollectionViewUpdatesBatch)
block();
[_passwordCollectionViewUpdatesBatch removeAllObjects];
}
} completion:nil];
}
@catch (NSException *exception) {
wrn( @"While updating password cells: %@", [exception fullDescription] );
[self.passwordCollectionView reloadData];
}
} );
}];
}
}
@@ -290,25 +312,24 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
}];
}
- (void)fetchedItemsDidUpdate {
- (void)updateTransientItem {
NSString *query = self.query;
_showTransientItem = [query length] > 0;
NSUInteger objects = ((id<NSFetchedResultsSectionInfo>)self.fetchedResultsController.sections[0]).numberOfObjects;
if (_showTransientItem && objects == 1 &&
[[[self.fetchedResultsController.fetchedObjects firstObject] name] isEqualToString:query])
_showTransientItem = NO;
if ([self.passwordCollectionView numberOfSections] > 0) {
if (!_showTransientItem && _transientItem != NSNotFound)
[self.passwordCollectionView deleteItemsAtIndexPaths:
@[ [NSIndexPath indexPathForItem:_transientItem inSection:0] ]];
else if (_showTransientItem && _transientItem == NSNotFound)
[self.passwordCollectionView insertItemsAtIndexPaths:
@[ [NSIndexPath indexPathForItem:objects inSection:0] ]];
else if (_transientItem != NSNotFound)
[self.passwordCollectionView reloadItemsAtIndexPaths:
@[ [NSIndexPath indexPathForItem:_transientItem inSection:0] ]];
_showTransientItem = [query length] > 0 && ![[[self.fetchedResultsController.sections[0] objects] filteredArrayUsingPredicate:
[NSPredicate predicateWithBlock:^BOOL(MPSiteEntity *site, NSDictionary<NSString *, id> *bindings) {
return [site.name isEqualToString:query];
}]] count];
if (!_showTransientItem && _transientItem != NSNotFound)
[self.passwordCollectionView deleteItemsAtIndexPaths:
@[ [NSIndexPath indexPathForItem:_transientItem inSection:0] ]];
else if (_showTransientItem && _transientItem == NSNotFound) {
NSUInteger objects = [self.fetchedResultsController.sections[0] numberOfObjects];
[self.passwordCollectionView insertItemsAtIndexPaths:
@[ [NSIndexPath indexPathForItem:objects inSection:0] ]];
}
else if (_transientItem != NSNotFound)
[self.passwordCollectionView reloadItemsAtIndexPaths:
@[ [NSIndexPath indexPathForItem:_transientItem inSection:0] ]];
}
- (void)registerObservers {
@@ -361,6 +382,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
if (mainContext)
PearlAddNotificationObserver( NSManagedObjectContextDidSaveNotification, mainContext, nil,
^(MPPasswordsViewController *self, NSNotification *note) {
// TODO: either move this into the app delegate or remove the duplicate signOutAnimated: call from the app delegate.
if (![[MPiOSAppDelegate get] activeUserInContext:note.object])
[[MPiOSAppDelegate get] signOutAnimated:YES];
} );
@@ -421,7 +443,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
PearlMainQueue( ^{
@try {
[self.passwordCollectionView performBatchUpdates:^{
[self fetchedItemsDidUpdate];
[self updateTransientItem];
NSInteger fromSections = self.passwordCollectionView.numberOfSections;
NSInteger toSections = [self numberOfSectionsInCollectionView:self.passwordCollectionView];

View File

@@ -22,8 +22,6 @@
@implementation MPPopdownSegue {
}
static char UnwindingObserverKey;
- (void)perform {
MPPasswordsViewController *passwordsVC;
@@ -39,19 +37,20 @@ static char UnwindingObserverKey;
[passwordsVC.popdownContainer addConstraintsWithVisualFormats:@[ @"H:|[popdownView]|", @"V:|[popdownView]|" ] options:0
metrics:nil views:NSDictionaryOfVariableBindings( popdownView )];
[UIView animateWithDuration:0.3f animations:^{
[[passwordsVC.popdownToTopConstraint updatePriority:1] layoutIfNeeded];
} completion:^(BOOL finished) {
[popdownVC didMoveToParentViewController:passwordsVC];
[passwordsVC.popdownToTopConstraint layoutIfNeeded];
id<NSObject> observer = [[NSNotificationCenter defaultCenter] addObserverForName:MPSignedOutNotification object:nil
queue:[NSOperationQueue mainQueue] usingBlock:
^(NSNotification *note) {
[[[MPPopdownSegue alloc] initWithIdentifier:@"unwind-popdown" source:popdownVC
destination:passwordsVC] perform];
}];
objc_setAssociatedObject( popdownVC, &UnwindingObserverKey, observer, OBJC_ASSOCIATION_RETAIN );
}];
[UIView animateWithDuration:0.6f delay:0 usingSpringWithDamping:0.75f initialSpringVelocity:1
options:UIViewAnimationOptionCurveEaseOut animations:^{
[[passwordsVC.popdownToTopConstraint updatePriority:1] layoutIfNeeded];
} completion:^(BOOL finished) {
[popdownVC didMoveToParentViewController:passwordsVC];
PearlAddNotificationObserverTo( popdownVC, MPSignedOutNotification, nil, [NSOperationQueue mainQueue],
^(id host, NSNotification *note) {
[[[MPPopdownSegue alloc] initWithIdentifier:@"unwind-popdown" source:popdownVC destination:passwordsVC]
perform];
} );
}];
}
else {
popdownVC = self.sourceViewController;
@@ -59,16 +58,16 @@ static char UnwindingObserverKey;
passwordsVC = (id)passwordsVC.parentViewController);
NSAssert( passwordsVC, @"Couldn't find passwords VC to pop back to." );
[[NSNotificationCenter defaultCenter] removeObserver:objc_getAssociatedObject( popdownVC, &UnwindingObserverKey )];
objc_setAssociatedObject( popdownVC, &UnwindingObserverKey, nil, OBJC_ASSOCIATION_RETAIN );
PearlRemoveNotificationObserversFrom( popdownVC );
[popdownVC willMoveToParentViewController:nil];
[UIView animateWithDuration:0.3f delay:0 options:UIViewAnimationOptionOverrideInheritedDuration animations:^{
[[passwordsVC.popdownToTopConstraint updatePriority:UILayoutPriorityDefaultHigh] layoutIfNeeded];
} completion:^(BOOL finished) {
[popdownVC.view removeFromSuperview];
[popdownVC removeFromParentViewController];
}];
[UIView animateWithDuration:0.4f delay:0 options:UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionOverrideInheritedDuration
animations:^{
[[passwordsVC.popdownToTopConstraint updatePriority:UILayoutPriorityDefaultHigh] layoutIfNeeded];
} completion:^(BOOL finished) {
[popdownVC.view removeFromSuperview];
[popdownVC removeFromParentViewController];
}];
}
}

View File

@@ -29,8 +29,10 @@
@property(weak, nonatomic) IBOutlet UITableViewCell *exportCell;
@property(weak, nonatomic) IBOutlet UITableViewCell *checkInconsistencies;
@property(weak, nonatomic) IBOutlet UIImageView *avatarImage;
@property(weak, nonatomic) IBOutlet UISegmentedControl *generatedTypeControl;
@property(weak, nonatomic) IBOutlet UISegmentedControl *generated1TypeControl;
@property(weak, nonatomic) IBOutlet UISegmentedControl *generated2TypeControl;
@property(weak, nonatomic) IBOutlet UISegmentedControl *storedTypeControl;
@property(weak, nonatomic) IBOutlet UILabel *passwordTypeExample;
- (IBAction)previousAvatar:(id)sender;
- (IBAction)nextAvatar:(id)sender;

View File

@@ -55,12 +55,24 @@
- (void)reload {
MPUserEntity *activeUser = [[MPiOSAppDelegate get] activeUserForMainThread];
self.generatedTypeControl.selectedSegmentIndex = [self generatedSegmentIndexForType:activeUser.defaultType];
self.storedTypeControl.selectedSegmentIndex = [self storedSegmentIndexForType:activeUser.defaultType];
self.avatarImage.image = [UIImage imageNamed:strf( @"avatar-%lu", (unsigned long)activeUser.avatar )];
self.savePasswordSwitch.on = activeUser.saveKey;
self.touchIDSwitch.on = activeUser.touchID;
self.touchIDSwitch.enabled = self.savePasswordSwitch.on && [[MPiOSAppDelegate get] isFeatureUnlocked:MPProductTouchID];
MPSiteType defaultType = activeUser.defaultType;
self.generated1TypeControl.selectedSegmentIndex = [self generated1SegmentIndexForType:defaultType];
self.generated2TypeControl.selectedSegmentIndex = [self generated2SegmentIndexForType:defaultType];
self.storedTypeControl.selectedSegmentIndex = [self storedSegmentIndexForType:defaultType];
PearlNotMainQueue( ^{
NSString *examplePassword = nil;
if (defaultType & MPSiteTypeClassGenerated)
examplePassword = [MPAlgorithmDefault generatePasswordForSiteNamed:@"test" ofType:defaultType
withCounter:1 usingKey:[MPiOSAppDelegate get].key];
PearlMainQueue( ^{
self.passwordTypeExample.text = [examplePassword length]? [NSString stringWithFormat:@"eg. %@", examplePassword]: nil;
} );
} );
}
#pragma mark - UITableViewDelegate
@@ -88,14 +100,18 @@
[self dismissPopup];
[[MPiOSAppDelegate get] signOutAnimated:YES];
}
if (cell == self.feedbackCell)
[[MPiOSAppDelegate get] showFeedbackWithLogs:YES forVC:self];
if (cell == self.exportCell)
[[MPiOSAppDelegate get] showExportForVC:self];
if (cell == self.showHelpCell) {
MPPasswordsViewController *passwordsVC = [self dismissPopup];
[passwordsVC performSegueWithIdentifier:@"guide" sender:self];
}
if (cell == self.checkInconsistencies)
[MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
if ([[MPiOSAppDelegate get] findAndFixInconsistenciesSaveInContext:context] == MPFixableResultNoProblems)
@@ -140,11 +156,13 @@
} );
}];
if (sender == self.generatedTypeControl || sender == self.storedTypeControl) {
if (sender == self.generatedTypeControl)
if (sender == self.generated1TypeControl || sender == self.generated2TypeControl || sender == self.storedTypeControl) {
if (sender != self.generated1TypeControl)
self.generated1TypeControl.selectedSegmentIndex = -1;
if (sender != self.generated2TypeControl)
self.generated2TypeControl.selectedSegmentIndex = -1;
if (sender != self.storedTypeControl)
self.storedTypeControl.selectedSegmentIndex = -1;
else if (sender == self.storedTypeControl)
self.generatedTypeControl.selectedSegmentIndex = -1;
MPSiteType defaultType = [self typeForSelectedSegment];
[MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
@@ -226,10 +244,17 @@
- (MPSiteType)typeForSelectedSegment {
NSInteger selectedGeneratedIndex = self.generatedTypeControl.selectedSegmentIndex;
NSInteger selectedGenerated1Index = self.generated1TypeControl.selectedSegmentIndex;
NSInteger selectedGenerated2Index = self.generated2TypeControl.selectedSegmentIndex;
NSInteger selectedStoredIndex = self.storedTypeControl.selectedSegmentIndex;
switch (selectedGeneratedIndex) {
switch (selectedGenerated1Index) {
case 0:
return MPSiteTypeGeneratedPhrase;
case 1:
return MPSiteTypeGeneratedName;
default:
switch (selectedGenerated2Index) {
case 0:
return MPSiteTypeGeneratedMaximum;
case 1:
@@ -250,13 +275,26 @@
case 1:
return MPSiteTypeStoredDevicePrivate;
default:
Throw( @"unsupported selected type index: generated=%ld, stored=%ld", (long)selectedGeneratedIndex,
(long)selectedStoredIndex );
Throw( @"unsupported selected type index: generated1=%ld, generated2=%ld, stored=%ld",
(long)selectedGenerated1Index, (long)selectedGenerated2Index, (long)selectedStoredIndex );
}
}
}
}
- (NSInteger)generatedSegmentIndexForType:(MPSiteType)type {
- (NSInteger)generated1SegmentIndexForType:(MPSiteType)type {
switch (type) {
case MPSiteTypeGeneratedPhrase:
return 0;
case MPSiteTypeGeneratedName:
return 1;
default:
return -1;
}
}
- (NSInteger)generated2SegmentIndexForType:(MPSiteType)type {
switch (type) {
case MPSiteTypeGeneratedMaximum:

View File

@@ -89,11 +89,11 @@
if ([selectedSite isKindOfClass:[MPGeneratedSiteEntity class]])
counter = ((MPGeneratedSiteEntity *)selectedSite).counter;
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0 ), ^{
PearlNotMainQueue( ^{
NSString *typeContent = [MPAlgorithmDefault generatePasswordForSiteNamed:name ofType:cellType
withCounter:counter usingKey:[MPiOSAppDelegate get].key];
dispatch_async( dispatch_get_main_queue(), ^{
PearlMainQueue( ^{
[(UITextField *)[[tableView cellForRowAtIndexPath:indexPath] viewWithTag:2] setText:typeContent];
} );
} );

View File

@@ -19,7 +19,6 @@
#import "MPiOSAppDelegate.h"
#import "MPAppDelegate_Key.h"
#import "MPAppDelegate_Store.h"
#import "IASKSettingsReader.h"
#import "MPStoreViewController.h"
@interface MPiOSAppDelegate()<UIDocumentInteractionControllerDelegate>
@@ -164,26 +163,24 @@
return NO;
// Arbitrary URL to mpsites data.
dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
NSError *error;
NSURLResponse *response;
NSData *importedSitesData = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url]
returningResponse:&response error:&error];
if (error)
err( @"While reading imported sites from %@: %@", url, [error fullDescription] );
if (!importedSitesData) {
[PearlAlert showError:strf( @"Master Password couldn't read the import sites.\n\n%@", [error localizedDescription]?: error )];
return;
}
[[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:
^(NSData *importedSitesData, NSURLResponse *response, NSError *error) {
if (error)
err( @"While reading imported sites from %@: %@", url, [error fullDescription] );
if (!importedSitesData) {
[PearlAlert showError:strf( @"Master Password couldn't read the import sites.\n\n%@",
[error localizedDescription]?: error )];
return;
}
NSString *importedSitesString = [[NSString alloc] initWithData:importedSitesData encoding:NSUTF8StringEncoding];
if (!importedSitesString) {
[PearlAlert showError:@"Master Password couldn't understand the import file."];
return;
}
NSString *importedSitesString = [[NSString alloc] initWithData:importedSitesData encoding:NSUTF8StringEncoding];
if (!importedSitesString) {
[PearlAlert showError:@"Master Password couldn't understand the import file."];
return;
}
[self importSites:importedSitesString];
} );
[self importSites:importedSitesString];
}] resume];
return YES;
}
@@ -199,57 +196,33 @@
PearlOverlay *activityOverlay = [PearlOverlay showProgressOverlayWithTitle:@"Importing"];
MPImportResult result = [self importSites:importedSitesString askImportPassword:^NSString *(NSString *userName) {
__block NSString *masterPassword = nil;
dispatch_group_t importPasswordGroup = dispatch_group_create();
dispatch_group_enter( importPasswordGroup );
dispatch_async( dispatch_get_main_queue(), ^{
return PearlAwait( ^(void (^setResult)(id)) {
[PearlAlert showAlertWithTitle:@"Import File's Master Password"
message:strf( @"%@'s export was done using a different master password.\n"
@"Enter that master password to unlock the exported data.", userName )
viewStyle:UIAlertViewStyleSecureTextInput
initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
@try {
if (buttonIndex_ == [alert_ cancelButtonIndex])
return;
masterPassword = [alert_ textFieldAtIndex:0].text;
}
@finally {
dispatch_group_leave( importPasswordGroup );
}
if (buttonIndex_ == [alert_ cancelButtonIndex])
setResult( nil );
else
setResult( [alert_ textFieldAtIndex:0].text );
}
cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Unlock Import", nil];
} );
dispatch_group_wait( importPasswordGroup, DISPATCH_TIME_FOREVER );
return masterPassword;
} askUserPassword:^NSString *(NSString *userName, NSUInteger importCount, NSUInteger deleteCount) {
__block NSString *masterPassword = nil;
dispatch_group_t userPasswordGroup = dispatch_group_create();
dispatch_group_enter( userPasswordGroup );
dispatch_async( dispatch_get_main_queue(), ^{
return PearlAwait( (id)^(void (^setResult)(id)) {
[PearlAlert showAlertWithTitle:strf( @"Master Password for\n%@", userName )
message:strf( @"Imports %lu sites, overwriting %lu.",
(unsigned long)importCount, (unsigned long)deleteCount )
viewStyle:UIAlertViewStyleSecureTextInput
initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
@try {
if (buttonIndex_ == [alert_ cancelButtonIndex])
return;
masterPassword = [alert_ textFieldAtIndex:0].text;
}
@finally {
dispatch_group_leave( userPasswordGroup );
}
if (buttonIndex_ == [alert_ cancelButtonIndex])
setResult( nil );
else
setResult( [alert_ textFieldAtIndex:0].text );
}
cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Import", nil];
} );
dispatch_group_wait( userPasswordGroup, DISPATCH_TIME_FOREVER );
return masterPassword;
}];
switch (result) {

File diff suppressed because it is too large Load Diff