1// 2// KBAppView.m 3// Keybase 4// 5// Created by Gabriel on 2/4/15. 6// Copyright (c) 2015 Gabriel Handford. All rights reserved. 7// 8 9#import "KBAppView.h" 10 11#import "KBApp.h" 12#import "KBAppToolbar.h" 13#import "KBSourceOutlineView.h" 14 15#import "KBComponent.h" 16#import "KBUsersAppView.h" 17#import "KBDevicesAppView.h" 18#import "KBFoldersAppView.h" 19#import "KBPGPAppView.h" 20#import "KBUserProfileView.h" 21#import "KBLoginView.h" 22#import "KBSignupView.h" 23#import "KBEnvironment.h" 24#import "KBDebugViews.h" 25#import "KBInstaller.h" 26#import "KBDebugViews.h" 27#import "KBAppProgressView.h" 28#import "KBSecretPromptView.h" 29#import "KBInstallStatusAppView.h" 30#import "KBAppDebug.h" 31#import "KBNotifications.h" 32#import "KBStatusView.h" 33#import "KBTask.h" 34#import "KBWorkspace.h" 35 36typedef NS_ENUM (NSInteger, KBAppViewMode) { 37 KBAppViewModeInProgress = 1, 38 KBAppViewModeStatus, 39 KBAppViewModeInstaller, 40 KBAppViewModeLogin, 41 KBAppViewModeSignup, 42 KBAppViewModeMain 43}; 44 45@interface KBAppView () <KBAppToolbarDelegate, KBComponent, KBSignupViewDelegate, KBLoginViewDelegate, KBRPClientDelegate, NSWindowDelegate> 46@property KBAppToolbar *toolbar; 47@property KBSourceOutlineView *sourceView; 48@property (readonly) YOView *contentView; 49 50@property KBAppProgressView *appProgressView; 51 52@property KBUsersAppView *usersAppView; 53@property KBDevicesAppView *devicesAppView; 54@property KBFoldersAppView *foldersAppView; 55@property KBPGPAppView *PGPAppView; 56 57@property KBUserProfileView *userProfileView; 58@property (nonatomic) KBLoginView *loginView; 59@property (nonatomic) KBSignupView *signupView; 60 61@property KBNavigationTitleView *titleView; 62 63@property NSString *title; 64@property KBAppViewMode mode; 65 66@property KBEnvironment *environment; 67@property KBRConfig *userConfig; 68@property KBRGetCurrentStatusRes *userStatus; 69@end 70 71#define TITLE_HEIGHT (32) 72 73@implementation KBAppView 74 75- (void)viewInit { 76 [super viewInit]; 77 78 _title = @"Keybase"; 79 80 _toolbar = [[KBAppToolbar alloc] init]; 81 _toolbar.hidden = YES; 82 _toolbar.delegate = self; 83 [self addSubview:_toolbar]; 84 85 YOSelf yself = self; 86 self.viewLayout = [YOLayout layoutWithLayoutBlock:^(id<YOLayout> layout, CGSize size) { 87 CGFloat x = 0; 88 CGFloat y = 0; 89 90 if (!yself.toolbar.hidden) { 91 y += [layout sizeToFitVerticalInFrame:CGRectMake(0, y, size.width, 0) view:yself.toolbar].size.height; 92 } 93 94 [layout setFrame:CGRectMake(x, y, size.width - x, size.height - y) view:yself.contentView]; 95 96 return size; 97 }]; 98 99 [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(userDidChange:) name:KBUserDidChangeNotification object:nil]; 100 101 [self showInProgress:@"Loading"]; 102} 103 104- (void)dealloc { 105 [NSNotificationCenter.defaultCenter removeObserver:self]; 106} 107 108- (void)openWithEnvironment:(KBEnvironment *)environment completion:(KBCompletion)completion { 109 _environment = environment; 110 111 NSDictionary *info = [[NSBundle mainBundle] infoDictionary]; 112 DDLogInfo(@"Keybase.app Version: %@", info[@"CFBundleShortVersionString"]); 113 114 [self install:completion]; 115} 116 117- (void)install:(KBCompletion)completion { 118 [self showInProgress:@"Loading"]; 119 KBInstaller *installer = [[KBInstaller alloc] init]; 120 [installer installWithEnvironment:_environment force:NO stopOnError:NO completion:^(NSError *error, NSArray *installables) { 121 [self showInstallStatusView:completion]; 122 }]; 123} 124 125- (void)connect:(KBCompletion)completion { 126 KBRPClient *client = _environment.service.client; 127 client.delegate = self; 128 [client open:completion]; 129} 130 131- (void)_checkStatus:(void (^)(NSError *error, KBRGetCurrentStatusRes *currentStatus, KBRConfig *config))completion { 132 GHWeakSelf gself = self; 133 KBRConfigRequest *statusRequest = [[KBRConfigRequest alloc] initWithClient:_environment.service.client]; 134 [statusRequest getCurrentStatus:^(NSError *error, KBRGetCurrentStatusRes *userStatus) { 135 if (error) { 136 completion(error, userStatus, nil); 137 return; 138 } 139 KBRConfigRequest *configRequest = [[KBRConfigRequest alloc] initWithClient:gself.environment.service.client]; 140 [configRequest getConfig:^(NSError *error, KBRConfig *userConfig) { 141 completion(error, userStatus, userConfig); 142 }]; 143 }]; 144} 145 146// If we errored while checking status 147- (void)setStatusError:(NSError *)error { 148 GHWeakSelf gself = self; 149 150 if (gself.mode == KBAppViewModeInProgress) { 151 NSMutableDictionary *errorInfo = [error.userInfo mutableCopy]; 152 errorInfo[NSLocalizedRecoveryOptionsErrorKey] = @[@"Retry", @"Quit"]; 153 error = [NSError errorWithDomain:error.domain code:error.code userInfo:errorInfo]; 154 155 [KBApp.app setError:error sender:self completion:^(NSModalResponse res) { 156 // Option to retry or quit if we are trying to get status for the first time 157 if (res == NSAlertFirstButtonReturn) { 158 [self checkStatus]; 159 } else { 160 [KBApp.app quitWithPrompt:YES sender:self]; 161 } 162 }]; 163 } else { 164 [KBApp.app setError:error sender:self completion:nil]; 165 } 166} 167 168- (void)setContentView:(YOView *)contentView mode:(KBAppViewMode)mode { 169 _mode = mode; 170 _toolbar.hidden = (mode != KBAppViewModeMain); 171 [_contentView removeFromSuperview]; 172 _contentView = contentView; 173 if (_contentView) [self addSubview:_contentView]; 174 if ([_contentView respondsToSelector:@selector(viewDidAppear:)]) [(id)_contentView viewDidAppear:NO]; 175 [self setNeedsLayout]; 176} 177 178- (KBLoginView *)loginView { 179 GHWeakSelf gself = self; 180 if (!_loginView) { 181 _loginView = [[KBLoginView alloc] init]; 182 _loginView.delegate = self; 183 _loginView.signupButton.targetBlock = ^{ 184 [gself showSignup]; 185 }; 186 } 187 188 // TODO reset progress? 189 //[_loginView.navigation setProgressEnabled:NO]; 190 _loginView.client = _environment.service.client; 191 return _loginView; 192} 193 194- (KBSignupView *)signupView { 195 GHWeakSelf gself = self; 196 if (!_signupView) { 197 _signupView = [[KBSignupView alloc] init]; 198 _signupView.delegate = self; 199 _signupView.loginButton.targetBlock = ^{ 200 [gself showLogin]; 201 }; 202 } 203 _signupView.client = _environment.service.client; 204 return _signupView; 205} 206 207- (void)showInProgress:(NSString *)title { 208 if (!_appProgressView || self.mode != KBAppViewModeInProgress) { 209 _appProgressView = [[KBAppProgressView alloc] init]; 210 KBNavigationView *navigation = [[KBNavigationView alloc] initWithView:_appProgressView title:_title]; 211 [self setContentView:navigation mode:KBAppViewModeInProgress]; 212 } 213 [_appProgressView setProgressTitle:title]; 214 _appProgressView.animating = YES; 215} 216 217- (void)showErrorView:(NSString *)title error:(NSError *)error { 218 KBStatusView *errorView = [[KBStatusView alloc] init]; 219 KBNavigationView *navigation = [[KBNavigationView alloc] initWithView:errorView title:_title]; 220 [self setContentView:navigation mode:KBAppViewModeStatus]; 221 GHWeakSelf gself = self; 222 [errorView setError:error title:title retry:^{ 223 [gself showConnect:^(NSError *error) {}]; 224 } close:^{ 225 [KBApp.app quitWithPrompt:NO sender:self]; 226 }]; 227} 228 229- (void)showBrewWarning:(dispatch_block_t)retry { 230 KBStatusView *view = [[KBStatusView alloc] init]; 231 view.insets = UIEdgeInsetsMake(100, 100, 100, 100); 232 [view setText:@"We've detected that you already have Keybase installed via Homebrew." description:@"We recommend that you uninstall the Homebrew installation since running both at the same time can causes issues." title:@"Homebrew Install Found" retry:retry close:^{ 233 [KBApp.app quitWithPrompt:NO sender:self]; 234 }]; 235 KBNavigationView *navigation = [[KBNavigationView alloc] initWithView:view title:_title]; 236 [self setContentView:navigation mode:KBAppViewModeStatus]; 237} 238 239- (void)showWelcome { 240 KBStatusView *view = [[KBStatusView alloc] init]; 241 view.insets = UIEdgeInsetsMake(100, 100, 100, 100); 242 [view setText:@"Thanks for installing Keybase." description:@"Cliche echo park synth, shoreditch crucifix church-key hoodie. Banh mi kitsch portland pitchfork iPhone mlkshk keffiyeh bitters stumptown polaroid listicle. Chambray ethical brunch, dreamcatcher lomo single-origin coffee yuccie irony beard. Microdosing knausgaard raw denim ethical fashion axe. Waistcoat cornhole brooklyn, truffaut bushwick meh keffiyeh. Blog schlitz next level banh mi, umami hella ugh tote bag paleo cliche lo-fi 8-bit ennui kinfolk. Shabby chic fap fixie keytar." title:@"Welcome to Keybase" retry:nil close:^{ 243 [self.window close]; 244 }]; 245 KBNavigationView *navigation = [[KBNavigationView alloc] initWithView:view title:_title]; 246 [self setContentView:navigation mode:KBAppViewModeStatus]; 247} 248 249- (void)showInstallStatusView:(KBCompletion)completion { 250 KBInstallStatusAppView *view = [[KBInstallStatusAppView alloc] init]; 251 [view setEnvironment:self.environment]; 252 view.completion = ^() { 253 [self showConnect:completion]; 254 }; 255 KBNavigationView *navigation = [[KBNavigationView alloc] initWithView:view title:_title]; 256 [self setContentView:navigation mode:KBAppViewModeInstaller]; 257} 258 259- (void)showConnect:(KBCompletion)completion { 260 [self showInProgress:@"Loading"]; 261 [self connect:completion]; 262} 263 264- (void)showLogin { 265 KBLoginView *view = [self loginView]; 266 [view removeFromSuperview]; 267 KBNavigationView *navigation = [[KBNavigationView alloc] initWithView:view title:_title]; 268 [self setContentView:navigation mode:KBAppViewModeLogin]; 269} 270 271- (void)showSignup { 272 KBSignupView *view = [self signupView]; 273 [view removeFromSuperview]; 274 KBNavigationView *navigation = [[KBNavigationView alloc] initWithView:view title:_title]; 275 [self setContentView:navigation mode:KBAppViewModeSignup]; 276} 277 278- (void)showUsers { 279 if (!_usersAppView) _usersAppView = [[KBUsersAppView alloc] init]; 280 _usersAppView.client = _environment.service.client; 281 [self setContentView:_usersAppView mode:KBAppViewModeMain]; 282} 283 284- (void)showProfile { 285 NSAssert(_userStatus.user, @"No user"); 286 if (!_userProfileView) _userProfileView = [[KBUserProfileView alloc] init]; 287 [_userProfileView setUsername:_userStatus.user.username client:_environment.service.client]; 288 [self setContentView:_userProfileView mode:KBAppViewModeMain]; 289 _toolbar.selectedItem = KBAppViewItemProfile; 290} 291 292- (void)showDevices { 293 if (!_devicesAppView) _devicesAppView = [[KBDevicesAppView alloc] init]; 294 _devicesAppView.client = _environment.service.client; 295 [_devicesAppView refresh]; 296 [self setContentView:_devicesAppView mode:KBAppViewModeMain]; 297} 298 299- (void)showFolders { 300 if (!_foldersAppView) _foldersAppView = [[KBFoldersAppView alloc] init]; 301 _foldersAppView.client = _environment.service.client; 302 [_foldersAppView reload]; 303 [self setContentView:_foldersAppView mode:KBAppViewModeMain]; 304} 305 306- (void)showPGP { 307 if (!_PGPAppView) _PGPAppView = [[KBPGPAppView alloc] init]; 308 _PGPAppView.client = _environment.service.client; 309 [self setContentView:_PGPAppView mode:KBAppViewModeMain]; 310} 311 312- (void)userDidChange:(NSNotification *)notification { 313 [_userProfileView refresh]; 314} 315 316- (void)logout:(BOOL)prompt { 317 GHWeakSelf gself = self; 318 dispatch_block_t logout = ^{ 319 [self showInProgress:@"Logging out"]; 320 KBRLoginRequest *request = [[KBRLoginRequest alloc] initWithClient:gself.environment.service.client]; 321 [request logout:^(NSError *error) { 322 if (error) { 323 [KBApp.app setError:error sender:self completion:nil]; 324 } 325 [self checkStatus]; 326 }]; 327 }; 328 329 if (prompt) { 330 [KBAlert yesNoWithTitle:@"Log Out" description:@"Are you sure you want to log out?" yes:@"Log Out" view:self completion:^(BOOL yes) { 331 if (yes) logout(); 332 }]; 333 } else { 334 logout(); 335 } 336} 337 338- (void)checkStatus { 339 [self _checkStatus:^(NSError *error, KBRGetCurrentStatusRes *userStatus, KBRConfig *userConfig) { 340 if (error) { 341 [self setStatusError:error]; 342 return; 343 } 344 [self setUserStatus:userStatus userConfig:userConfig]; 345 // TODO reload current view if coming back from disconnect? 346 [NSNotificationCenter.defaultCenter postNotificationName:KBStatusDidChangeNotification object:nil userInfo:@{@"userConfig": userConfig, @"userStatus": userStatus}]; 347 }]; 348} 349 350/* 351- (void)setConfig:(KBRConfig *)config { 352 _config = config; 353 NSString *host = _config.serverURI; 354 // TODO Directly accessing API client should eventually go away (everything goes to daemon) 355 if ([host isEqualTo:@"https://api.keybase.io:443"]) host = @"https://keybase.io"; 356 AppDelegate.sharedDelegate.APIClient = [[KBAPIClient alloc] initWithAPIHost:host]; 357} 358 */ 359 360- (NSString *)APIURLString:(NSString *)path { 361 NSAssert(_userConfig, @"No user config"); 362 NSString *host = _userConfig.serverURI; 363 if ([host isEqualTo:@"https://api.keybase.io:443"]) host = @"https://keybase.io"; 364 return [NSString stringWithFormat:@"%@/%@", host, path]; 365} 366 367- (void)setUserStatus:(KBRGetCurrentStatusRes *)userStatus userConfig:(KBRConfig *)userConfig { 368 _userStatus = userStatus; 369 _userConfig = userConfig; 370 371 [self.loginView setUsername:userStatus.user.username]; 372 [self.sourceView.statusView setStatus:userStatus]; 373 [self.toolbar setUser:userStatus.user]; 374 375 // Don't change if we are in the installer 376 if (_mode == KBAppViewModeInstaller) return; 377 378 [self showWelcome]; 379 return; 380 381 /* 382 if (userStatus.loggedIn && userStatus.user) { 383 // Show profile if logging in or we are already showing profile, refresh it 384 if (_mode != KBAppViewModeMain || _toolbar.selectedItem == KBAppViewItemProfile) { 385 [self showProfile]; 386 } 387 } else if (_mode != KBAppViewModeLogin || _mode != KBAppViewModeSignup) { 388 [self showLogin]; 389 } 390 */ 391} 392 393- (void)signupViewDidSignup:(KBSignupView *)signupView { 394 [self showInProgress:@"Loading"]; 395 [self checkStatus]; 396} 397 398- (void)loginViewDidLogin:(KBLoginView *)loginView { 399 [self showInProgress:@"Loading"]; 400 [self checkStatus]; 401} 402 403- (void)RPClientWillConnect:(KBRPClient *)RPClient { } 404 405- (void)RPClientDidConnect:(KBRPClient *)RPClient { 406 [self checkStatus]; 407} 408 409- (void)RPClientDidDisconnect:(KBRPClient *)RPClient { 410 DDLogInfo(@"Disconnected."); 411 [self showInProgress:nil]; 412 [NSNotificationCenter.defaultCenter postNotificationName:KBStatusDidChangeNotification object:nil userInfo:@{}]; 413} 414 415- (BOOL)RPClient:(KBRPClient *)RPClient didErrorOnConnect:(NSError *)error connectAttempt:(NSInteger)connectAttempt { 416 if (connectAttempt >= 3) { 417 [self showErrorView:@"Service Error" error:error]; 418 return NO; 419 } 420 return YES; 421} 422 423- (void)RPClient:(KBRPClient *)RPClient didLog:(NSString *)message { 424 DDLogInfo(@"%@", message); 425} 426 427- (void)RPClient:(KBRPClient *)RPClient didRequestSecretForPrompt:(NSString *)prompt info:(NSString *)info details:(NSString *)details previousError:(NSString *)previousError completion:(KBRPClientOnSecret)completion { 428 KBSecretPromptView *secretPrompt = [[KBSecretPromptView alloc] init]; 429 [secretPrompt setHeader:prompt info:info details:details previousError:previousError]; 430 secretPrompt.completion = completion; 431 [secretPrompt openInWindow:(KBWindow *)self.window]; 432} 433 434- (void)RPClient:(KBRPClient *)RPClient didRequestKeybasePassphraseForUsername:(NSString *)username completion:(KBRPClientOnPassphrase)completion { 435 [KBAlert promptForInputWithTitle:@"Passphrase" description:NSStringWithFormat(@"What's your passphrase (for user %@)?", username) secure:YES style:NSCriticalAlertStyle buttonTitles:@[@"OK", @"Cancel"] view:self completion:^(NSModalResponse response, NSString *password) { 436 password = response == NSAlertFirstButtonReturn ? password : nil; 437 completion(password); 438 }]; 439} 440 441- (void)appToolbar:(KBAppToolbar *)appToolbar didSelectItem:(KBAppViewItem)item { 442 switch (item) { 443 case KBAppViewItemNone: 444 NSAssert(NO, @"Can't select none"); 445 break; 446 case KBAppViewItemDevices: 447 [self showDevices]; 448 break; 449 case KBAppViewItemFolders: 450 [self showFolders]; 451 break; 452 case KBAppViewItemProfile: 453 [self showProfile]; 454 break; 455 case KBAppViewItemUsers: 456 [self showUsers]; 457 break; 458 case KBAppViewItemPGP: 459 [self showPGP]; 460 break; 461 } 462} 463 464- (NSRect)window:(NSWindow *)window willPositionSheet:(NSWindow *)sheet usingRect:(NSRect)rect { 465 CGFloat sheetPosition = 0; 466 if (_mode == KBAppViewModeMain) sheetPosition = 74; 467 else sheetPosition = 32; 468 rect.origin.y += -sheetPosition; 469 return rect; 470} 471 472/* 473- (BOOL)windowShouldClose:(id)sender { 474 [KBApp.app quitWithPrompt:YES sender:self]; 475 return NO; 476} 477 */ 478 479- (void)openWindow { 480 if (self.window) { 481 [NSApplication.sharedApplication activateIgnoringOtherApps:YES]; 482 [self.window orderFrontRegardless]; 483 } else { 484 NSWindow *window = [KBWorkspace createMainWindow:self]; 485 [window center]; 486 [window makeKeyAndOrderFront:nil]; 487 } 488} 489 490//- (void)encodeRestorableStateWithCoder:(NSCoder *)coder { } 491//- (void)restoreStateWithCoder:(NSCoder *)coder { } 492//invalidateRestorableState 493 494//+ (void)restoreWindowWithIdentifier:(NSString *)identifier state:(NSCoder *)state completionHandler:(void (^)(NSWindow *window, NSError *error))completionHandler { 495// KBAppView *appView = [[KBAppView alloc] init]; 496// NSWindow *window = [appView createWindow]; 497// completionHandler(window, nil); 498//} 499 500#pragma mark KBComponent 501 502- (NSString *)name { 503 return @"App"; 504} 505 506- (NSString *)info { 507 return @"The Keybase application"; 508} 509 510- (NSImage *)image { 511 return [KBIcons imageForIcon:KBIconGenericApp]; 512} 513 514- (NSView *)componentView { 515 return [[KBAppDebug alloc] init]; 516} 517 518- (void)refreshComponent:(KBRefreshComponentCompletion)completion { 519 completion(nil); 520} 521 522@end 523