2
0
MasterPassword/platform-darwin/Source/iOS/MPUsersViewController.m
2020-03-25 12:16:19 -04:00

906 lines
38 KiB
Objective-C

//==============================================================================
// This file is part of Master Password.
// Copyright (c) 2011-2017, Maarten Billemont.
//
// Master Password is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Master Password is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You can find a copy of the GNU General Public License in the
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
//==============================================================================
#import <Countly/Countly.h>
#import "MPUsersViewController.h"
#import "MPEntities.h"
#import "MPAvatarCell.h"
#import "MPiOSAppDelegate.h"
#import "MPAppDelegate_Store.h"
#import "MPAppDelegate_Key.h"
#import "MPWebViewController.h"
typedef NS_OPTIONS( NSUInteger, MPUsersTips ) {
MPUsersThanksTip = 1 << 0,
MPUsersAvatarTip = 1 << 1,
MPUsersMasterPasswordTip = 1 << 2,
MPUsersPreferencesTip = 1 << 3,
};
typedef NS_ENUM( NSUInteger, MPActiveUserState ) {
/** The users are all inactive */
MPActiveUserStateNone,
/** The selected user is activated and being logged in with */
MPActiveUserStateLogin,
/** The selected user is activated and its user name is being asked for */
MPActiveUserStateUserName,
/** The selected user is activated and its new master password is being asked for */
MPActiveUserStateMasterPasswordChoice,
/** The selected user is activated and the confirmation of the previously entered master password is being asked for */
MPActiveUserStateMasterPasswordConfirmation,
/** The selected user is activated displayed at the top with the rest of the UI inactive */
MPActiveUserStateMinimized,
};
@interface MPUsersViewController()
@property(nonatomic) MPActiveUserState activeUserState;
@property(nonatomic, strong) NSArray *userIDs;
@property(nonatomic, strong) NSTimer *marqueeTipTimer;
@property(nonatomic, strong) NSArray *marqueeTipTexts;
@property(nonatomic) NSUInteger marqueeTipTextIndex;
@property(nonatomic, copy) NSString *masterPasswordChoice;
@property(nonatomic, strong) NSOperationQueue *afterUpdates;
@property(nonatomic, weak) id contextChangedObserver;
@end
@implementation MPUsersViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.afterUpdates = [NSOperationQueue new];
self.marqueeTipTexts = @[
strl( @"Thanks, lhunath ➚" ),
strl( @"Press and hold to delete or reset user." ),
strl( @"Shake for emergency generator." ),
];
self.view.backgroundColor = [UIColor clearColor];
self.avatarCollectionView.allowsMultipleSelection = YES;
[self.entryField addTarget:self action:@selector( textFieldEditingChanged: ) forControlEvents:UIControlEventEditingChanged];
self.preferencesTipContainer.visible = NO;
[self setActive:YES animated:NO];
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"tipped.thanks"])
[self showTips:MPUsersThanksTip];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.userSelectionContainer.visible = NO;
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self registerObservers];
[self reloadUsers];
[self.marqueeTipTimer invalidate];
self.marqueeTipTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector( firedMarqueeTimer: )
userInfo:nil repeats:YES];
[self firedMarqueeTimer:nil];
}
- (void)viewWillLayoutSubviews {
[self.avatarCollectionView.collectionViewLayout invalidateLayout];
[super viewWillLayoutSubviews];
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[self.avatarCollectionView.collectionViewLayout invalidateLayout];
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:@"thanks"])
((MPWebViewController *)segue.destinationViewController).initialURL =
[NSURL URLWithString:@"https://thanks.lhunath.com"];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self removeObservers];
[self.marqueeTipTimer invalidate];
}
#pragma mark - UITextFieldDelegate
- (void)textFieldDidEndEditing:(UITextField *)textField {
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
if (textField == self.entryField) {
switch (self.activeUserState) {
case MPActiveUserStateNone: {
[textField resignFirstResponder];
break;
}
case MPActiveUserStateLogin: {
self.entryField.enabled = NO;
MPAvatarCell *selectedAvatar = [self selectedAvatar];
selectedAvatar.spinnerActive = YES;
NSIndexPath *selectedPath = selectedAvatar? [self.avatarCollectionView indexPathForCell:selectedAvatar]: nil;
NSString *masterPassword = self.entryField.text;
if (![MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
BOOL signedIn = NO, isNew = NO;
MPUserEntity *user = selectedPath? [self userForIndexPath:selectedPath inContext:context isNew:&isNew]: nil;
if (!isNew && user)
signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context
usingMasterPassword:masterPassword];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.entryField.text = @"";
self.entryField.enabled = YES;
[self selectedAvatar].spinnerActive = NO;
if (!signedIn) {
// Sign in failed.
[self showEntryTip:strl( @"Looks like a typo!\nTry again; that password was incorrect." )];
return;
}
}];
}]) {
self.entryField.enabled = YES;
[self selectedAvatar].spinnerActive = NO;
}
break;
}
case MPActiveUserStateUserName: {
NSString *userName = self.entryField.text;
if (![userName length]) {
// No name entered.
[self showEntryTip:strl( @"First, enter your name." )];
return NO;
}
[self selectedAvatar].name = userName;
self.activeUserState = MPActiveUserStateMasterPasswordChoice;
break;
}
case MPActiveUserStateMasterPasswordChoice: {
NSString *masterPassword = self.entryField.text;
if (![masterPassword length]) {
// No password entered.
[self showEntryTip:strl( @"Pick a master password." )];
return NO;
}
self.activeUserState = MPActiveUserStateMasterPasswordConfirmation;
break;
}
case MPActiveUserStateMasterPasswordConfirmation: {
NSString *masterPassword = self.entryField.text;
if (![masterPassword length]) {
// No password entered.
[self showEntryTip:strl( @"Confirm your master password." )];
return NO;
}
if (![masterPassword isEqualToString:self.masterPasswordChoice]) {
// Master password confirmation failed.
[self showEntryTip:strl( @"Looks like a typo!\nTry again; enter your master password twice." )];
self.activeUserState = MPActiveUserStateMasterPasswordChoice;
return NO;
}
self.entryField.enabled = NO;
MPAvatarCell *avatarCell = [self selectedAvatar];
avatarCell.spinnerActive = YES;
NSIndexPath *avatarPath = [self.avatarCollectionView indexPathForCell:avatarCell];
NSUInteger newUserAvatar = avatarCell.avatar;
NSString *newUserName = avatarCell.name;
if (![MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
BOOL isNew = NO;
MPUserEntity *user = [self userForIndexPath:avatarPath inContext:context isNew:&isNew];
if (isNew) {
user = [MPUserEntity insertNewObjectInContext:context];
user.algorithm = MPAlgorithmDefault;
user.defaultType = user.algorithm.defaultType;
user.avatar = newUserAvatar;
user.name = newUserName;
if ([[MPConfig get].sendInfo boolValue]) {
[Countly.sharedInstance recordEvent:@"new-user" segmentation:@{
@"algorithm": @(user.algorithm.version).description,
@"avatar" : @(user.avatar).description,
}];
}
}
BOOL signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context
usingMasterPassword:masterPassword];
PearlMainQueue( ^{
self.entryField.text = @"";
self.entryField.enabled = YES;
avatarCell.spinnerActive = NO;
if (!signedIn) {
// Sign in failed, shouldn't happen for a new user.
[self showEntryTip:strl( @"Couldn't create new user." )];
self.activeUserState = MPActiveUserStateNone;
return;
}
} );
}]) {
self.entryField.enabled = YES;
avatarCell.spinnerActive = NO;
}
break;
}
case MPActiveUserStateMinimized: {
[textField resignFirstResponder];
break;
}
}
}
return NO;
}
// This isn't really in UITextFieldDelegate. We fake it from UITextFieldTextDidChangeNotification.
- (void)textFieldEditingChanged:(UITextField *)textField {
if ([[textField.text lowercaseString] isEqualToString:@"hangtest"])
[NSThread sleepForTimeInterval:10];
if (textField == self.entryField) {
switch (self.activeUserState) {
case MPActiveUserStateNone:
break;
case MPActiveUserStateLogin:
break;
case MPActiveUserStateUserName: {
NSString *userName = self.entryField.text;
[self selectedAvatar].name = [userName length]? userName: strl( @"New User" );
break;
}
case MPActiveUserStateMasterPasswordChoice:
break;
case MPActiveUserStateMasterPasswordConfirmation:
break;
case MPActiveUserStateMinimized:
break;
}
}
}
#pragma mark - UICollectionViewDelegateFlowLayout
- (CGSize) collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout
referenceSizeForHeaderInSection:(NSInteger)section {
CGSize parentSize = self.avatarCollectionView.bounds.size;
return CGSizeMake( parentSize.width / 4, parentSize.height );
}
- (CGSize) collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout
referenceSizeForFooterInSection:(NSInteger)section {
CGSize parentSize = self.avatarCollectionView.bounds.size;
return CGSizeMake( parentSize.width / 4, parentSize.height );
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
CGSize parentSize = self.avatarCollectionView.bounds.size;
return CGSizeMake( parentSize.width / 2, parentSize.height );
}
#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
if (collectionView == self.avatarCollectionView)
return [self.userIDs count] + 1;
Throw( @"unexpected collection view: %@", collectionView );
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
if (collectionView == self.avatarCollectionView) {
MPAvatarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[MPAvatarCell reuseIdentifier] forIndexPath:indexPath];
cell.contentView.frame = cell.bounds;
[cell addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector( didLongPress: )]];
[self updateModeForAvatar:cell atIndexPath:indexPath animated:NO];
[self updateVisibilityForAvatar:cell atIndexPath:indexPath animated:NO];
BOOL isNew = NO;
MPUserEntity *user = [self userForIndexPath:indexPath inContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]
isNew:&isNew];
if (isNew)
// New User
cell.avatar = MPAvatarAdd;
else {
// Existing User
cell.avatar = user.avatar;
cell.name = user.name;
}
return cell;
}
Throw( @"unexpected collection view: %@", collectionView );
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
if (collectionView == self.avatarCollectionView) {
[self.avatarCollectionView scrollToItemAtIndexPath:indexPath
atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
// Deselect all other cells.
for (NSUInteger otherItem = 0; otherItem < [collectionView numberOfItemsInSection:indexPath.section]; ++otherItem)
if (otherItem != indexPath.item) {
NSIndexPath *otherIndexPath = [NSIndexPath indexPathForItem:otherItem inSection:indexPath.section];
[collectionView deselectItemAtIndexPath:otherIndexPath animated:YES];
}
BOOL isNew = NO;
NSManagedObjectContext *mainContext = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady];
MPUserEntity *mainUser = [self userForIndexPath:indexPath inContext:mainContext isNew:&isNew];
if (isNew)
self.activeUserState = MPActiveUserStateUserName;
else if (!mainUser.keyID)
self.activeUserState = MPActiveUserStateMasterPasswordChoice;
else {
self.activeUserState = MPActiveUserStateLogin;
self.entryField.enabled = NO;
MPAvatarCell *userAvatar = [self selectedAvatar];
userAvatar.spinnerActive = YES;
if (!isNew && mainUser && [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPUserEntity *user = [MPUserEntity existingObjectWithID:mainUser.permanentObjectID inContext:context];
BOOL signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context usingMasterPassword:nil];
PearlMainQueue( ^{
self.entryField.text = @"";
self.entryField.enabled = YES;
userAvatar.spinnerActive = NO;
if (!signedIn)
[self.entryField becomeFirstResponder];
} );
}])
return;
self.entryField.text = @"";
self.entryField.enabled = YES;
userAvatar.spinnerActive = NO;
[self.entryField becomeFirstResponder];
}
}
}
- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath {
if (collectionView == self.avatarCollectionView)
self.activeUserState = MPActiveUserStateNone;
}
#pragma mark - UILongPressGestureRecognizer
- (void)didLongPress:(UILongPressGestureRecognizer *)recognizer {
if ([recognizer.view isKindOfClass:[MPAvatarCell class]]) {
if (recognizer.state != UIGestureRecognizerStateBegan)
// Don't show the action menu unless the state is Began.
return;
MPAvatarCell *avatarCell = (MPAvatarCell *)recognizer.view;
NSManagedObjectContext *mainContext = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady];
BOOL isNew = NO;
MPUserEntity *user = [self userForIndexPath:[self.avatarCollectionView indexPathForCell:avatarCell]
inContext:mainContext isNew:&isNew];
if (isNew || !user)
return;
NSManagedObjectID *userID = user.permanentObjectID;
UIAlertController *controller = [UIAlertController alertControllerWithTitle:user.name message:nil preferredStyle:UIAlertControllerStyleActionSheet];
[controller addAction:[UIAlertAction actionWithTitle:@"Delete User" style:UIAlertActionStyleDestructive handler:^(UIAlertAction * _Nonnull action) {
UIAlertController *controller_ = [UIAlertController alertControllerWithTitle:@"Deleting User" message:
@"The user and its sites will be deleted." preferredStyle:UIAlertControllerStyleAlert];
[controller_ addAction:[UIAlertAction actionWithTitle:@"Delete User" style:UIAlertActionStyleDestructive handler:^(UIAlertAction * _Nonnull action) {
[self deleteUser:userID];
}]];
[controller_ addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:controller_ animated:YES completion:nil];
}]];
[controller addAction:[UIAlertAction actionWithTitle:@"Reset Password" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
UIAlertController *controller_ = [UIAlertController alertControllerWithTitle:@"Resetting User" message:
@"The user's master password will be reset." preferredStyle:UIAlertControllerStyleAlert];
[controller_ addAction:[UIAlertAction actionWithTitle:@"Reset User" style:UIAlertActionStyleDestructive handler:^(UIAlertAction * _Nonnull action) {
[self resetUser:userID avatar:avatarCell];
}]];
[controller_ addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:controller_ animated:YES completion:nil];
}]];
[controller addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:controller animated:YES completion:nil];
}
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset {
if (scrollView == self.avatarCollectionView) {
CGPoint offsetToCenter = self.avatarCollectionView.center;
NSIndexPath *avatarIndexPath = [self.avatarCollectionView indexPathForItemAtPoint:
CGPointPlusCGPoint( *targetContentOffset, offsetToCenter )];
CGPoint targetCenter = [self.avatarCollectionView layoutAttributesForItemAtIndexPath:avatarIndexPath].center;
*targetContentOffset = CGPointMinusCGPoint( targetCenter, offsetToCenter );
NSAssert( [self.avatarCollectionView indexPathForItemAtPoint:targetCenter].item == avatarIndexPath.item, @"should be same item" );
}
}
#pragma mark - Private
- (void)deleteUser:(NSManagedObjectID *)userID {
[MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPUserEntity *user = [MPUserEntity existingObjectWithID:userID inContext:context];
if (!user)
return;
[context deleteObject:user];
[context saveToStore];
}];
}
- (void)resetUser:(NSManagedObjectID *)userID avatar:(MPAvatarCell *)avatarCell {
[MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
MPUserEntity *user = [MPUserEntity existingObjectWithID:userID inContext:context];
if (!user)
return;
[[MPiOSAppDelegate get] changeMasterPasswordFor:user saveInContext:context didResetBlock:^{
PearlMainQueue( ^{
NSIndexPath *avatarIndexPath = [self.avatarCollectionView indexPathForCell:avatarCell];
[self.avatarCollectionView selectItemAtIndexPath:avatarIndexPath animated:NO
scrollPosition:UICollectionViewScrollPositionNone];
[self collectionView:self.avatarCollectionView didSelectItemAtIndexPath:avatarIndexPath];
} );
}];
}];
}
- (void)showTips:(MPUsersTips)showTips {
[UIView animateWithDuration:0.3f animations:^{
if (showTips & MPUsersThanksTip)
self.thanksTipContainer.visible = YES;
if (showTips & MPUsersAvatarTip)
self.avatarTipContainer.visible = YES;
if (showTips & MPUsersMasterPasswordTip)
self.entryTipContainer.visible = YES;
if (showTips & MPUsersPreferencesTip)
self.preferencesTipContainer.visible = YES;
} completion:^(BOOL finished) {
if (finished)
PearlMainQueueAfter( 5, ^{
[UIView animateWithDuration:0.3f animations:^{
if (showTips & MPUsersThanksTip)
self.thanksTipContainer.visible = NO;
if (showTips & MPUsersAvatarTip)
self.avatarTipContainer.visible = NO;
if (showTips & MPUsersMasterPasswordTip)
self.entryTipContainer.visible = NO;
if (showTips & MPUsersPreferencesTip)
self.preferencesTipContainer.visible = NO;
}];
} );
}];
}
- (void)showEntryTip:(NSString *)message {
NSUInteger newlineIndex = [message rangeOfString:@"\n"].location;
NSString *messageTitle = newlineIndex == NSNotFound? message: [message substringToIndex:newlineIndex];
NSString *messageSubtitle = newlineIndex == NSNotFound? nil: [message substringFromIndex:newlineIndex];
self.entryTipTitleLabel.text = messageTitle;
self.entryTipSubtitleLabel.text = messageSubtitle;
[self showTips:MPUsersMasterPasswordTip];
}
- (void)firedMarqueeTimer:(NSTimer *)timer {
NSString *nextMarqueeString = self.marqueeTipTexts[self.marqueeTipTextIndex++ % [self.marqueeTipTexts count]];
if ([nextMarqueeString isEqualToString:[self.marqueeButton titleForState:UIControlStateNormal]])
return;
[UIView animateWithDuration:timer? 0.5f: 0 animations:^{
self.marqueeButton.visible = NO;
} completion:^(BOOL finished) {
if (!finished)
return;
[self.marqueeButton setTitle:nextMarqueeString forState:UIControlStateNormal];
[UIView animateWithDuration:timer? 0.5f: 0 animations:^{
self.marqueeButton.visible = YES;
}];
}];
}
- (MPAvatarCell *)selectedAvatar {
NSArray *selectedIndexPaths = self.avatarCollectionView.indexPathsForSelectedItems;
if (![selectedIndexPaths count]) {
// No selected user.
return nil;
}
return (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:selectedIndexPaths.firstObject];
}
- (MPUserEntity *)userForIndexPath:(NSIndexPath *)indexPath inContext:(NSManagedObjectContext *)context isNew:(BOOL *)isNew {
*isNew = NO;
if (!indexPath || (*isNew = indexPath.item >= [self.userIDs count]))
return nil;
return [MPUserEntity existingObjectWithID:self.userIDs[indexPath.item] inContext:context];
}
- (void)updateAvatarVisibility {
for (NSIndexPath *indexPath in self.avatarCollectionView.indexPathsForVisibleItems) {
MPAvatarCell *cell = (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:indexPath];
[self updateVisibilityForAvatar:cell atIndexPath:indexPath animated:NO];
}
}
- (void)updateModeForAvatar:(MPAvatarCell *)avatarCell atIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated {
switch (self.activeUserState) {
case MPActiveUserStateNone: {
[self.avatarCollectionView deselectItemAtIndexPath:indexPath animated:YES];
[avatarCell setMode:MPAvatarModeLowered animated:animated];
break;
}
case MPActiveUserStateLogin:
case MPActiveUserStateUserName:
case MPActiveUserStateMasterPasswordChoice:
case MPActiveUserStateMasterPasswordConfirmation: {
if ([self.avatarCollectionView.indexPathsForSelectedItems containsObject:indexPath])
[avatarCell setMode:MPAvatarModeRaisedAndActive animated:animated];
else
[avatarCell setMode:MPAvatarModeRaisedButInactive animated:animated];
break;
}
case MPActiveUserStateMinimized: {
if ([self.avatarCollectionView.indexPathsForSelectedItems containsObject:indexPath])
[avatarCell setMode:MPAvatarModeRaisedAndMinimized animated:animated];
else
[avatarCell setMode:MPAvatarModeRaisedAndHidden animated:animated];
break;
}
}
}
- (void)updateVisibilityForAvatar:(MPAvatarCell *)cell atIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated {
CGFloat current = [self.avatarCollectionView layoutAttributesForItemAtIndexPath:indexPath].center.x -
self.avatarCollectionView.contentOffset.x;
CGFloat max = self.avatarCollectionView.bounds.size.width;
CGFloat visibility = MAX( 0, MIN( 1, 1 - ABS( current / (max / 2) - 1 ) ) );
[cell setVisibility:visibility animated:animated];
[UIView animateWithDuration:animated? 0.3f: 0 animations:^{
self.nextAvatarButton.visible = self.previousAvatarButton.visible = cell.newUser && cell.mode == MPAvatarModeRaisedAndActive;
self.nextAvatarButton.alpha = self.previousAvatarButton.alpha = visibility * 0.7f;
}];
}
- (void)afterUpdatesMainQueue:(void ( ^ )(void))block {
[self.afterUpdates addOperationWithBlock:^{
PearlMainQueue( block );
}];
}
- (void)removeObservers {
[self removeKeyPathObservers];
PearlRemoveNotificationObservers();
[[NSNotificationCenter defaultCenter] removeObserver:self.contextChangedObserver];
}
- (void)registerObservers {
[self removeObservers];
[self observeKeyPath:@"avatarCollectionView.contentOffset" withBlock:
^(id from, id to, NSKeyValueChange cause, MPUsersViewController *self) {
PearlMainQueue( ^{ [self updateAvatarVisibility]; } );
}];
PearlAddNotificationObserver( UIApplicationDidEnterBackgroundNotification, nil, [NSOperationQueue mainQueue],
^(MPUsersViewController *self, NSNotification *note) {
self.userSelectionContainer.visible = NO;
} );
PearlAddNotificationObserver( UIApplicationWillEnterForegroundNotification, nil, [NSOperationQueue mainQueue],
^(MPUsersViewController *self, NSNotification *note) {
[self reloadUsers];
} );
PearlAddNotificationObserver( UIApplicationDidBecomeActiveNotification, nil, [NSOperationQueue mainQueue],
^(MPUsersViewController *self, NSNotification *note) {
[UIView animateWithDuration:0.5f animations:^{
self.userSelectionContainer.visible = YES;
}];
} );
PearlAddNotificationObserver( UIKeyboardWillShowNotification, nil, [NSOperationQueue mainQueue],
^(MPUsersViewController *self, NSNotification *note) {
CGRect keyboardRect = [note.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGFloat keyboardHeight = CGRectGetHeight( self.view.window.screen.bounds ) - CGRectGetMinY( keyboardRect );
[self.keyboardHeightConstraint updateConstant:keyboardHeight];
} );
if ((self.contextChangedObserver
= [[MPiOSAppDelegate get] managedObjectContextChanged:^(NSDictionary<NSManagedObjectID *, NSString *> *affectedObjects) {
if ([[[affectedObjects allKeys] filteredArrayUsingPredicate:
[NSPredicate predicateWithBlock:^BOOL(NSManagedObjectID *objectID, NSDictionary *bindings) {
return [objectID.entity.name isEqualToString:NSStringFromClass( [MPUserEntity class] )];
}]] count])
[self reloadUsers];
}]))
[UIView animateWithDuration:0.3f animations:^{
self.avatarCollectionView.visible = YES;
[self.storeLoadingActivity stopAnimating];
}];
else
[UIView animateWithDuration:0.3f animations:^{
self.avatarCollectionView.visible = NO;
[self.storeLoadingActivity startAnimating];
}];
PearlAddNotificationObserver( NSPersistentStoreCoordinatorStoresWillChangeNotification, [MPiOSAppDelegate get].storeCoordinator, nil,
^(MPUsersViewController *self, NSNotification *note) {
self.userIDs = nil;
} );
PearlAddNotificationObserver( NSPersistentStoreCoordinatorStoresDidChangeNotification, [MPiOSAppDelegate get].storeCoordinator, nil,
^(MPUsersViewController *self, NSNotification *note) {
[self registerObservers];
[self reloadUsers];
} );
}
- (void)reloadUsers {
[self afterUpdatesMainQueue:^{
if (![MPiOSAppDelegate managedObjectContextForMainThreadPerformBlockAndWait:^(NSManagedObjectContext *mainContext) {
NSError *error = nil;
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPUserEntity class] )];
fetchRequest.sortDescriptors = @[
[NSSortDescriptor sortDescriptorWithKey:NSStringFromSelector( @selector( lastUsed ) ) ascending:NO]
];
NSArray *users = [mainContext executeFetchRequest:fetchRequest error:&error];
if (!users) {
MPError( error, @"Failed to load users." );
self.userIDs = nil;
}
NSMutableArray *userIDs = [NSMutableArray arrayWithCapacity:[users count]];
for (MPUserEntity *user in users)
[userIDs addObject:user.permanentObjectID];
self.userIDs = userIDs;
}])
self.userIDs = nil;
}];
}
#pragma mark - Properties
- (void)setActive:(BOOL)active animated:(BOOL)animated {
_active = active;
if (active)
[self setActiveUserState:MPActiveUserStateNone animated:animated];
else
[self setActiveUserState:MPActiveUserStateMinimized animated:animated];
}
- (void)setUserIDs:(NSArray *)userIDs {
_userIDs = userIDs;
PearlMainQueue( ^{
BOOL isNew = NO;
NSManagedObjectID *selectUserID = [MPiOSAppDelegate get].activeUserOID;
if (!selectUserID) {
selectUserID = [self userForIndexPath:self.avatarCollectionView.indexPathsForSelectedItems.firstObject
inContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]
isNew:&isNew].permanentObjectID;
}
[self.avatarCollectionView reloadData];
NSUInteger selectedAvatarItem = isNew? [self.userIDs count]: selectUserID? [self.userIDs indexOfObject:selectUserID]: NSNotFound;
if (selectedAvatarItem != NSNotFound)
[self.avatarCollectionView selectItemAtIndexPath:[NSIndexPath indexPathForItem:selectedAvatarItem inSection:0] animated:NO
scrollPosition:UICollectionViewScrollPositionCenteredHorizontally];
[UIView animateWithDuration:0.3f animations:^{
self.userSelectionContainer.visible = YES;
}];
} );
}
- (void)setActiveUserState:(MPActiveUserState)activeUserState {
[self setActiveUserState:activeUserState animated:YES];
}
- (void)setActiveUserState:(MPActiveUserState)activeUserState animated:(BOOL)animated {
_activeUserState = activeUserState;
self.masterPasswordChoice = nil;
if (activeUserState != MPActiveUserStateMinimized && (!self.active || [MPiOSAppDelegate get].activeUserOID)) {
[[MPiOSAppDelegate get] signOutAnimated:YES];
return;
}
// Set the entry container's contents.
[self.afterUpdates setSuspended:YES];
__block BOOL requestFirstResponder = NO;
[self.view layoutIfNeeded];
[UIView animateWithDuration:animated? 0.4f: 0 animations:^{
switch (activeUserState) {
case MPActiveUserStateNone:
break;
case MPActiveUserStateLogin: {
self.entryLabel.text = strl( @"Enter your master password:" );
self.entryField.secureTextEntry = YES;
self.entryField.autocapitalizationType = UITextAutocapitalizationTypeNone;
self.entryField.text = nil;
break;
}
case MPActiveUserStateUserName: {
self.entryLabel.text = strl( @"Enter your full name:" );
self.entryField.secureTextEntry = NO;
self.entryField.autocapitalizationType = UITextAutocapitalizationTypeWords;
self.entryField.text = nil;
break;
}
case MPActiveUserStateMasterPasswordChoice: {
self.entryLabel.text = strl( @"Choose your master password:" );
self.entryField.secureTextEntry = YES;
self.entryField.autocapitalizationType = UITextAutocapitalizationTypeNone;
self.entryField.text = nil;
break;
}
case MPActiveUserStateMasterPasswordConfirmation: {
self.masterPasswordChoice = self.entryField.text;
self.entryLabel.text = strl( @"Confirm your master password:" );
self.entryField.secureTextEntry = YES;
self.entryField.autocapitalizationType = UITextAutocapitalizationTypeNone;
self.entryField.text = nil;
break;
}
case MPActiveUserStateMinimized:
break;
}
// Manage the entry container depending on whether a user is activate or not.
switch (activeUserState) {
case MPActiveUserStateNone: {
self.avatarCollectionView.scrollEnabled = YES;
self.entryContainer.visible = NO;
self.footerContainer.visible = YES;
break;
}
case MPActiveUserStateLogin:
case MPActiveUserStateUserName:
case MPActiveUserStateMasterPasswordChoice:
case MPActiveUserStateMasterPasswordConfirmation: {
self.avatarCollectionView.scrollEnabled = NO;
self.entryContainer.visible = YES;
self.footerContainer.visible = YES;
requestFirstResponder = YES;
break;
}
case MPActiveUserStateMinimized: {
self.avatarCollectionView.scrollEnabled = NO;
self.entryContainer.visible = NO;
self.footerContainer.visible = NO;
break;
}
}
// Manage tip visibility.
switch (activeUserState) {
case MPActiveUserStateNone:
case MPActiveUserStateMasterPasswordConfirmation:
case MPActiveUserStateLogin: {
break;
}
case MPActiveUserStateUserName: {
[self showTips:MPUsersAvatarTip];
break;
}
case MPActiveUserStateMasterPasswordChoice: {
[self showEntryTip:strl( @"A short phrase makes a strong, memorable password." )];
break;
}
case MPActiveUserStateMinimized: {
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"tipped.passwordsPreferences"])
[self showTips:MPUsersPreferencesTip];
break;
}
}
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
[self.afterUpdates setSuspended:NO];
}];
[self.entryField resignFirstResponder];
if (requestFirstResponder)
[self.entryField becomeFirstResponder];
// Set avatar modes.
MPAvatarCell *selectedAvatar = [self selectedAvatar];
for (NSIndexPath *indexPath in [self.avatarCollectionView indexPathsForVisibleItems]) {
MPAvatarCell *avatarCell = (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:indexPath];
[self updateModeForAvatar:avatarCell atIndexPath:indexPath animated:animated];
[self updateVisibilityForAvatar:avatarCell atIndexPath:indexPath animated:animated];
if (selectedAvatar && avatarCell == selectedAvatar)
[self.avatarCollectionView scrollToItemAtIndexPath:indexPath
atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
}
}
#pragma mark - Actions
- (IBAction)changeAvatar:(UIButton *)sender {
if (sender == self.previousAvatarButton)
--[self selectedAvatar].avatar;
if (sender == self.nextAvatarButton)
++[self selectedAvatar].avatar;
}
@end