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.
This commit is contained in:
		@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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];
 | 
			
		||||
 
 | 
			
		||||
@@ -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<NSManagedObjectID *, NSString *> *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) {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user