From fbbd08790dd269d1a0cf5383ad6686496e5c9f7f Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sat, 29 Apr 2017 17:50:48 -0400 Subject: [PATCH] Pasteboard improvements, UI fixes and site name from pasteboard URL. [UPDATED] Timeout after 3 min for other pasteboard copies too. [FIXED] Sometimes cell content loading can fail, schedule a retry. [UPDATED] Dismiss keyboard when copying content. [IMPROVED] Handling of deactivation and reactivation observation. [ADDED] When a URL is in the pasteboard, search for the hostname. --- .../Source/iOS/MPAnswersViewController.m | 32 ++++++++--- .../Source/iOS/MPEmergencyViewController.m | 34 +++++++---- platform-darwin/Source/iOS/MPPasswordCell.m | 30 ++++++++-- .../Source/iOS/MPPasswordsViewController.m | 57 ++++++++++++------- 4 files changed, 105 insertions(+), 48 deletions(-) diff --git a/platform-darwin/Source/iOS/MPAnswersViewController.m b/platform-darwin/Source/iOS/MPAnswersViewController.m index c2242c79..14f2f0a0 100644 --- a/platform-darwin/Source/iOS/MPAnswersViewController.m +++ b/platform-darwin/Source/iOS/MPAnswersViewController.m @@ -161,10 +161,9 @@ MPSiteEntity *site = [self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]]; UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; - if ([cell isKindOfClass:[MPGlobalAnswersCell class]]) { - [PearlOverlay showTemporaryOverlayWithTitle:strl( @"Answer Copied" ) dismissAfter:2]; - [UIPasteboard generalPasteboard].string = ((MPGlobalAnswersCell *)cell).answerField.text; - } + if ([cell isKindOfClass:[MPGlobalAnswersCell class]]) + [self copyAnswer:((MPGlobalAnswersCell *)cell).answerField.text]; + else if ([cell isKindOfClass:[MPMultipleAnswersCell class]]) { if (!_multiple) [self setMultiple:YES animated:YES]; @@ -192,6 +191,7 @@ } cancelTitle:@"Cancel" otherTitles:@"Remove Questions", nil]; } } + else if ([cell isKindOfClass:[MPSendAnswersCell class]]) { NSString *body; if (!_multiple) { @@ -215,14 +215,30 @@ [PearlEMail sendEMailTo:nil fromVC:self subject:strf( @"Master Password security answers for %@", site.name ) body:body]; } - else if ([cell isKindOfClass:[MPAnswersQuestionCell class]]) { - [PearlOverlay showTemporaryOverlayWithTitle:strl( @"Answer Copied" ) dismissAfter:2]; - [UIPasteboard generalPasteboard].string = ((MPAnswersQuestionCell *)cell).answerField.text; - } + + else if ([cell isKindOfClass:[MPAnswersQuestionCell class]]) + [self copyAnswer:((MPAnswersQuestionCell *)cell).answerField.text]; [tableView deselectRowAtIndexPath:indexPath animated:YES]; } +- (void)copyAnswer:(NSString *)answer { + + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + if ([pasteboard respondsToSelector:@selector( setItems:options: )]) { + [pasteboard setItems:@[ @{ UIPasteboardTypeAutomatic: answer } ] + options:@{ + UIPasteboardOptionLocalOnly : @NO, + UIPasteboardOptionExpirationDate: [NSDate dateWithTimeIntervalSinceNow:3 * 60] + }]; + [PearlOverlay showTemporaryOverlayWithTitle:strl( @"Answer Copied (3 min)" ) dismissAfter:2]; + } + else { + pasteboard.string = answer; + [PearlOverlay showTemporaryOverlayWithTitle:strl( @"Answer Copied" ) dismissAfter:2]; + } +} + #pragma mark - Private - (void)updateAnimated:(BOOL)animated { diff --git a/platform-darwin/Source/iOS/MPEmergencyViewController.m b/platform-darwin/Source/iOS/MPEmergencyViewController.m index 1ee98c01..613778d5 100644 --- a/platform-darwin/Source/iOS/MPEmergencyViewController.m +++ b/platform-darwin/Source/iOS/MPEmergencyViewController.m @@ -84,18 +84,28 @@ - (IBAction)copyPassword:(id)sender { NSString *sitePassword = [self.passwordButton titleForState:UIControlStateNormal]; - if ([sitePassword length]) { - [UIPasteboard generalPasteboard].string = sitePassword; - [UIView animateWithDuration:0.3f animations:^{ - self.tipContainer.visible = YES; - } completion:^(BOOL finished) { - PearlMainQueueAfter( 3, ^{ - [UIView animateWithDuration:0.3f animations:^{ - self.tipContainer.visible = NO; - }]; - } ); - }]; - } + if (![sitePassword length]) + return; + + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + if ([pasteboard respondsToSelector:@selector( setItems:options: )]) + [pasteboard setItems:@[ @{ UIPasteboardTypeAutomatic: sitePassword } ] + options:@{ + UIPasteboardOptionLocalOnly : @NO, + UIPasteboardOptionExpirationDate: [NSDate dateWithTimeIntervalSinceNow:3 * 60] + }]; + else + pasteboard.string = sitePassword; + + [UIView animateWithDuration:0.3f animations:^{ + self.tipContainer.visible = YES; + } completion:^(BOOL finished) { + PearlMainQueueAfter( 3, ^{ + [UIView animateWithDuration:0.3f animations:^{ + self.tipContainer.visible = NO; + }]; + } ); + }]; } #pragma mark - Private diff --git a/platform-darwin/Source/iOS/MPPasswordCell.m b/platform-darwin/Source/iOS/MPPasswordCell.m index 70187398..cef71a3e 100644 --- a/platform-darwin/Source/iOS/MPPasswordCell.m +++ b/platform-darwin/Source/iOS/MPPasswordCell.m @@ -493,10 +493,12 @@ - (void)updateAnimated:(BOOL)animated { + Weakify( self ); if (![NSThread isMainThread]) { - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + PearlMainQueueOperation( ^{ + Strongify( self ); [self updateAnimated:animated]; - }]; + } ); return; } @@ -544,11 +546,17 @@ } ); // Calculate Fields - [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + if (![MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { MPSiteEntity *site = [self siteInContext:context]; MPKey *key = [MPiOSAppDelegate get].key; - if (!key) + if (!key) { + wrn( @"Could not load cell content: key unavailable." ); + PearlMainQueueOperation( ^{ + Strongify( self ); + [self updateAnimated:YES]; + } ); return; + } BOOL loginGenerated = site.loginGenerated; NSString *password = nil, *loginName = [site resolveLoginUsingKey:key]; @@ -600,7 +608,13 @@ else self.indicatorView.hidden = YES; } ); - }]; + }]) { + wrn( @"Could not load cell content: store unavailable." ); + PearlMainQueueOperation( ^{ + Strongify( self ); + [self updateAnimated:YES]; + } ); + } [self.contentView layoutIfNeeded]; }]; @@ -635,6 +649,8 @@ return NO; PearlMainQueue( ^{ + [self.window endEditing:YES]; + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; if ([pasteboard respondsToSelector:@selector(setItems:options:)]) { [pasteboard setItems:@[ @{ UIPasteboardTypeAutomatic: password } ] @@ -663,8 +679,10 @@ return NO; PearlMainQueue( ^{ - [PearlOverlay showTemporaryOverlayWithTitle:strl( @"Login Name Copied" ) dismissAfter:2]; + [self.window endEditing:YES]; + [UIPasteboard generalPasteboard].string = loginName; + [PearlOverlay showTemporaryOverlayWithTitle:strl( @"Login Name Copied" ) dismissAfter:2]; } ); [site use]; diff --git a/platform-darwin/Source/iOS/MPPasswordsViewController.m b/platform-darwin/Source/iOS/MPPasswordsViewController.m index 35fde63e..82e42d6d 100644 --- a/platform-darwin/Source/iOS/MPPasswordsViewController.m +++ b/platform-darwin/Source/iOS/MPPasswordsViewController.m @@ -81,7 +81,6 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { [self registerObservers]; [self updateConfigKey:nil]; - [self reloadPasswords]; } - (void)viewDidAppear:(BOOL)animated { @@ -301,17 +300,31 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { - (void)registerObservers { + static NSRegularExpression *bareHostRE = nil; + static dispatch_once_t once = 0; + dispatch_once( &once, ^{ + bareHostRE = [NSRegularExpression regularExpressionWithPattern:@"([^\\.]+\\.[^\\.]+)$" options:0 error:nil]; + } ); + PearlRemoveNotificationObservers(); - PearlAddNotificationObserver( UIApplicationDidEnterBackgroundNotification, nil, [NSOperationQueue mainQueue], + PearlAddNotificationObserver( UIApplicationWillResignActiveNotification, nil, [NSOperationQueue mainQueue], ^(MPPasswordsViewController *self, NSNotification *note) { self.passwordSelectionContainer.visible = NO; } ); - PearlAddNotificationObserver( UIApplicationWillEnterForegroundNotification, nil, [NSOperationQueue mainQueue], - ^(MPPasswordsViewController *self, NSNotification *note) { - [self reloadPasswords]; - } ); PearlAddNotificationObserver( UIApplicationDidBecomeActiveNotification, nil, [NSOperationQueue mainQueue], ^(MPPasswordsViewController *self, NSNotification *note) { + NSURL *pasteboardURL = nil; + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + if ([pasteboard respondsToSelector:@selector( hasURLs )]) + pasteboardURL = pasteboard.hasURLs? pasteboard.URL: nil; + else + pasteboardURL = [NSURL URLWithString:pasteboard.string]; + + if (pasteboardURL.host) + self.query = NSNullToNil( [pasteboardURL.host firstMatchGroupsOfExpression:bareHostRE][0] ); + else + [self reloadPasswords]; + [UIView animateWithDuration:0.7f animations:^{ self.passwordSelectionContainer.visible = YES; }]; @@ -319,9 +332,8 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { PearlAddNotificationObserver( MPSignedOutNotification, nil, nil, ^(MPPasswordsViewController *self, NSNotification *note) { PearlMainQueue( ^{ - _fetchedResultsController = nil; - self.passwordsSearchBar.text = nil; - [self.passwordCollectionView reloadData]; + self->_fetchedResultsController = nil; + self.query = nil; } ); } ); PearlAddNotificationObserver( MPCheckConfigNotification, nil, nil, @@ -333,9 +345,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { PearlAddNotificationObserver( NSPersistentStoreCoordinatorStoresWillChangeNotification, nil, nil, ^(MPPasswordsViewController *self, NSNotification *note) { self->_fetchedResultsController = nil; - PearlMainQueue( ^{ - [self.passwordCollectionView reloadData]; - } ); + [self reloadPasswords]; } ); PearlAddNotificationObserver( NSPersistentStoreCoordinatorStoresDidChangeNotification, nil, nil, ^(MPPasswordsViewController *self, NSNotification *note) { @@ -345,14 +355,13 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { } ); } ); - NSManagedObjectContext *mainContext = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady]; - 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]; - } ); + [MPiOSAppDelegate managedObjectContextChanged:^(NSDictionary *affectedObjects) { + [MPiOSAppDelegate managedObjectContextForMainThreadPerformBlock:^(NSManagedObjectContext *mainContext) { + // TODO: either move this into the app delegate or remove the duplicate signOutAnimated: call from the app delegate. + if (![[MPiOSAppDelegate get] activeUserInContext:mainContext]) + [[MPiOSAppDelegate get] signOutAnimated:YES]; + }]; + }]; } - (void)updateConfigKey:(NSString *)key { @@ -372,11 +381,9 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { fuzzyRE = [NSRegularExpression regularExpressionWithPattern:@"(.)" options:0 error:nil]; } ); - prof_new( @"updateSites" ); NSString *queryString = self.query; NSString *queryPattern = [[queryString stringByReplacingMatchesOfExpression:fuzzyRE withTemplate:@"*$1"] stringByAppendingString:@"*"]; - prof_rewind( @"queryPattern" ); NSMutableArray *fuzzyGroups = [NSMutableArray new]; [fuzzyRE enumerateMatchesInString:queryString options:0 range:NSMakeRange( 0, queryString.length ) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { @@ -408,6 +415,12 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { return [self.passwordsSearchBar.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; } +- (void)setQuery:(NSString *)query { + + self.passwordsSearchBar.text = [query stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + [self reloadPasswords]; +} + - (NSFetchedResultsController *)fetchedResultsController { if (!_fetchedResultsController) {