2
0

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:
Maarten Billemont 2017-04-29 17:50:48 -04:00
parent fcaa5d1d8c
commit fbbd08790d
4 changed files with 105 additions and 48 deletions

View File

@ -161,10 +161,9 @@
MPSiteEntity *site = [self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]]; MPSiteEntity *site = [self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]];
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if ([cell isKindOfClass:[MPGlobalAnswersCell class]]) { if ([cell isKindOfClass:[MPGlobalAnswersCell class]])
[PearlOverlay showTemporaryOverlayWithTitle:strl( @"Answer Copied" ) dismissAfter:2]; [self copyAnswer:((MPGlobalAnswersCell *)cell).answerField.text];
[UIPasteboard generalPasteboard].string = ((MPGlobalAnswersCell *)cell).answerField.text;
}
else if ([cell isKindOfClass:[MPMultipleAnswersCell class]]) { else if ([cell isKindOfClass:[MPMultipleAnswersCell class]]) {
if (!_multiple) if (!_multiple)
[self setMultiple:YES animated:YES]; [self setMultiple:YES animated:YES];
@ -192,6 +191,7 @@
} cancelTitle:@"Cancel" otherTitles:@"Remove Questions", nil]; } cancelTitle:@"Cancel" otherTitles:@"Remove Questions", nil];
} }
} }
else if ([cell isKindOfClass:[MPSendAnswersCell class]]) { else if ([cell isKindOfClass:[MPSendAnswersCell class]]) {
NSString *body; NSString *body;
if (!_multiple) { if (!_multiple) {
@ -215,14 +215,30 @@
[PearlEMail sendEMailTo:nil fromVC:self subject:strf( @"Master Password security answers for %@", site.name ) body:body]; [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]; else if ([cell isKindOfClass:[MPAnswersQuestionCell class]])
[UIPasteboard generalPasteboard].string = ((MPAnswersQuestionCell *)cell).answerField.text; [self copyAnswer:((MPAnswersQuestionCell *)cell).answerField.text];
}
[tableView deselectRowAtIndexPath:indexPath animated:YES]; [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 #pragma mark - Private
- (void)updateAnimated:(BOOL)animated { - (void)updateAnimated:(BOOL)animated {

View File

@ -84,18 +84,28 @@
- (IBAction)copyPassword:(id)sender { - (IBAction)copyPassword:(id)sender {
NSString *sitePassword = [self.passwordButton titleForState:UIControlStateNormal]; NSString *sitePassword = [self.passwordButton titleForState:UIControlStateNormal];
if ([sitePassword length]) { if (![sitePassword length])
[UIPasteboard generalPasteboard].string = sitePassword; return;
[UIView animateWithDuration:0.3f animations:^{
self.tipContainer.visible = YES; UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
} completion:^(BOOL finished) { if ([pasteboard respondsToSelector:@selector( setItems:options: )])
PearlMainQueueAfter( 3, ^{ [pasteboard setItems:@[ @{ UIPasteboardTypeAutomatic: sitePassword } ]
[UIView animateWithDuration:0.3f animations:^{ options:@{
self.tipContainer.visible = NO; 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 #pragma mark - Private

View File

@ -493,10 +493,12 @@
- (void)updateAnimated:(BOOL)animated { - (void)updateAnimated:(BOOL)animated {
Weakify( self );
if (![NSThread isMainThread]) { if (![NSThread isMainThread]) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{ PearlMainQueueOperation( ^{
Strongify( self );
[self updateAnimated:animated]; [self updateAnimated:animated];
}]; } );
return; return;
} }
@ -544,11 +546,17 @@
} ); } );
// Calculate Fields // Calculate Fields
[MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { if (![MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPSiteEntity *site = [self siteInContext:context]; MPSiteEntity *site = [self siteInContext:context];
MPKey *key = [MPiOSAppDelegate get].key; MPKey *key = [MPiOSAppDelegate get].key;
if (!key) if (!key) {
wrn( @"Could not load cell content: key unavailable." );
PearlMainQueueOperation( ^{
Strongify( self );
[self updateAnimated:YES];
} );
return; return;
}
BOOL loginGenerated = site.loginGenerated; BOOL loginGenerated = site.loginGenerated;
NSString *password = nil, *loginName = [site resolveLoginUsingKey:key]; NSString *password = nil, *loginName = [site resolveLoginUsingKey:key];
@ -600,7 +608,13 @@
else else
self.indicatorView.hidden = YES; self.indicatorView.hidden = YES;
} ); } );
}]; }]) {
wrn( @"Could not load cell content: store unavailable." );
PearlMainQueueOperation( ^{
Strongify( self );
[self updateAnimated:YES];
} );
}
[self.contentView layoutIfNeeded]; [self.contentView layoutIfNeeded];
}]; }];
@ -635,6 +649,8 @@
return NO; return NO;
PearlMainQueue( ^{ PearlMainQueue( ^{
[self.window endEditing:YES];
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
if ([pasteboard respondsToSelector:@selector(setItems:options:)]) { if ([pasteboard respondsToSelector:@selector(setItems:options:)]) {
[pasteboard setItems:@[ @{ UIPasteboardTypeAutomatic: password } ] [pasteboard setItems:@[ @{ UIPasteboardTypeAutomatic: password } ]
@ -663,8 +679,10 @@
return NO; return NO;
PearlMainQueue( ^{ PearlMainQueue( ^{
[PearlOverlay showTemporaryOverlayWithTitle:strl( @"Login Name Copied" ) dismissAfter:2]; [self.window endEditing:YES];
[UIPasteboard generalPasteboard].string = loginName; [UIPasteboard generalPasteboard].string = loginName;
[PearlOverlay showTemporaryOverlayWithTitle:strl( @"Login Name Copied" ) dismissAfter:2];
} ); } );
[site use]; [site use];

View File

@ -81,7 +81,6 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
[self registerObservers]; [self registerObservers];
[self updateConfigKey:nil]; [self updateConfigKey:nil];
[self reloadPasswords];
} }
- (void)viewDidAppear:(BOOL)animated { - (void)viewDidAppear:(BOOL)animated {
@ -301,17 +300,31 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
- (void)registerObservers { - (void)registerObservers {
static NSRegularExpression *bareHostRE = nil;
static dispatch_once_t once = 0;
dispatch_once( &once, ^{
bareHostRE = [NSRegularExpression regularExpressionWithPattern:@"([^\\.]+\\.[^\\.]+)$" options:0 error:nil];
} );
PearlRemoveNotificationObservers(); PearlRemoveNotificationObservers();
PearlAddNotificationObserver( UIApplicationDidEnterBackgroundNotification, nil, [NSOperationQueue mainQueue], PearlAddNotificationObserver( UIApplicationWillResignActiveNotification, nil, [NSOperationQueue mainQueue],
^(MPPasswordsViewController *self, NSNotification *note) { ^(MPPasswordsViewController *self, NSNotification *note) {
self.passwordSelectionContainer.visible = NO; self.passwordSelectionContainer.visible = NO;
} ); } );
PearlAddNotificationObserver( UIApplicationWillEnterForegroundNotification, nil, [NSOperationQueue mainQueue],
^(MPPasswordsViewController *self, NSNotification *note) {
[self reloadPasswords];
} );
PearlAddNotificationObserver( UIApplicationDidBecomeActiveNotification, nil, [NSOperationQueue mainQueue], PearlAddNotificationObserver( UIApplicationDidBecomeActiveNotification, nil, [NSOperationQueue mainQueue],
^(MPPasswordsViewController *self, NSNotification *note) { ^(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:^{ [UIView animateWithDuration:0.7f animations:^{
self.passwordSelectionContainer.visible = YES; self.passwordSelectionContainer.visible = YES;
}]; }];
@ -319,9 +332,8 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
PearlAddNotificationObserver( MPSignedOutNotification, nil, nil, PearlAddNotificationObserver( MPSignedOutNotification, nil, nil,
^(MPPasswordsViewController *self, NSNotification *note) { ^(MPPasswordsViewController *self, NSNotification *note) {
PearlMainQueue( ^{ PearlMainQueue( ^{
_fetchedResultsController = nil; self->_fetchedResultsController = nil;
self.passwordsSearchBar.text = nil; self.query = nil;
[self.passwordCollectionView reloadData];
} ); } );
} ); } );
PearlAddNotificationObserver( MPCheckConfigNotification, nil, nil, PearlAddNotificationObserver( MPCheckConfigNotification, nil, nil,
@ -333,9 +345,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
PearlAddNotificationObserver( NSPersistentStoreCoordinatorStoresWillChangeNotification, nil, nil, PearlAddNotificationObserver( NSPersistentStoreCoordinatorStoresWillChangeNotification, nil, nil,
^(MPPasswordsViewController *self, NSNotification *note) { ^(MPPasswordsViewController *self, NSNotification *note) {
self->_fetchedResultsController = nil; self->_fetchedResultsController = nil;
PearlMainQueue( ^{ [self reloadPasswords];
[self.passwordCollectionView reloadData];
} );
} ); } );
PearlAddNotificationObserver( NSPersistentStoreCoordinatorStoresDidChangeNotification, nil, nil, PearlAddNotificationObserver( NSPersistentStoreCoordinatorStoresDidChangeNotification, nil, nil,
^(MPPasswordsViewController *self, NSNotification *note) { ^(MPPasswordsViewController *self, NSNotification *note) {
@ -345,14 +355,13 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
} ); } );
} ); } );
NSManagedObjectContext *mainContext = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady]; [MPiOSAppDelegate managedObjectContextChanged:^(NSDictionary<NSManagedObjectID *, NSString *> *affectedObjects) {
if (mainContext) [MPiOSAppDelegate managedObjectContextForMainThreadPerformBlock:^(NSManagedObjectContext *mainContext) {
PearlAddNotificationObserver( NSManagedObjectContextDidSaveNotification, mainContext, nil, // TODO: either move this into the app delegate or remove the duplicate signOutAnimated: call from the app delegate.
^(MPPasswordsViewController *self, NSNotification *note) { if (![[MPiOSAppDelegate get] activeUserInContext:mainContext])
// TODO: either move this into the app delegate or remove the duplicate signOutAnimated: call from the app delegate. [[MPiOSAppDelegate get] signOutAnimated:YES];
if (![[MPiOSAppDelegate get] activeUserInContext:note.object]) }];
[[MPiOSAppDelegate get] signOutAnimated:YES]; }];
} );
} }
- (void)updateConfigKey:(NSString *)key { - (void)updateConfigKey:(NSString *)key {
@ -372,11 +381,9 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
fuzzyRE = [NSRegularExpression regularExpressionWithPattern:@"(.)" options:0 error:nil]; fuzzyRE = [NSRegularExpression regularExpressionWithPattern:@"(.)" options:0 error:nil];
} ); } );
prof_new( @"updateSites" );
NSString *queryString = self.query; NSString *queryString = self.query;
NSString *queryPattern = [[queryString stringByReplacingMatchesOfExpression:fuzzyRE withTemplate:@"*$1"] NSString *queryPattern = [[queryString stringByReplacingMatchesOfExpression:fuzzyRE withTemplate:@"*$1"]
stringByAppendingString:@"*"]; stringByAppendingString:@"*"];
prof_rewind( @"queryPattern" );
NSMutableArray *fuzzyGroups = [NSMutableArray new]; NSMutableArray *fuzzyGroups = [NSMutableArray new];
[fuzzyRE enumerateMatchesInString:queryString options:0 range:NSMakeRange( 0, queryString.length ) [fuzzyRE enumerateMatchesInString:queryString options:0 range:NSMakeRange( 0, queryString.length )
usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
@ -408,6 +415,12 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
return [self.passwordsSearchBar.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; return [self.passwordsSearchBar.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
} }
- (void)setQuery:(NSString *)query {
self.passwordsSearchBar.text = [query stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
[self reloadPasswords];
}
- (NSFetchedResultsController *)fetchedResultsController { - (NSFetchedResultsController *)fetchedResultsController {
if (!_fetchedResultsController) { if (!_fetchedResultsController) {