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