Code Study: GitHub Desktop / ui / app.tsx

The file defines the App react component.

When using TypeScript for React we define interfaces for the props and state objects. IAppProps and IAppState are the interfaces that define the types these objects should contain for the App component:

interface IAppProps {
  readonly dispatcher: Dispatcher
  readonly repositoryStateManager: RepositoryStateCache
  readonly appStore: AppStore
  readonly issuesStore: IssuesStore
  readonly gitHubUserStore: GitHubUserStore
  readonly startTime: number
}

export interface IAppState {
  ...
}

export class App extends React.Component<IAppProps, IAppState> {
  public constructor(props: IAppProps) {
    ...
  }
  public render() {
    ...
  }
  ...
}

constructor

public constructor(props: IAppProps) {
  super(props)
  ...
}

The constructor does the following in order:

  1. Register a global handler for dispatching contextual menu actions. This should be called only once, around app load time.

     ipcRenderer.on(
       'contextual-menu-action',
       ...
     }
    
  2. Load the initial state of the app (async)

Loading the initial state of the app includes getting and refreshing accounts, caching users, getting all local repositories, initialize some UI component width, update some menu item labels, etc. See loadInitialState in app-store.ts

```typescript
props.dispatcher.loadInitialState().then(() => {
  ...
}
```

After the initial state is loaded, trigger the re-rendering of the component based on the initial state

```typescript
this.forceUpdate()
```

Later, when the browser idles (everything finishes loading), record the app launch time and perform deferred launch actions, which include loading emoji, reporting stats, installing the global Git LFS filters, and checking for app updates.

```typescript
requestIdleCallback(
  () => {
    const now = performance.now()
    sendReady(now - props.startTime)

    requestIdleCallback(() => {
      this.performDeferredLaunchActions()
    })
  },
  { timeout: ReadyDelay }
)
```

render

See app-theme.tsx for the AppTheme component.

<div id="desktop-app-chrome" className={className}>
  <AppTheme theme={currentTheme} />
  {this.renderTitlebar()}
  {this.state.showWelcomeFlow
    ? this.renderWelcomeFlow()
    : this.renderApp()}
  {this.renderZoomInfo()}
  {this.renderFullScreenInfo()}
</div>

renderTitlebar

See window/title-bar.tsx for the TitleBar component.

<TitleBar
  showAppIcon={showAppIcon}
  titleBarStyle={this.state.titleBarStyle}
  windowState={this.state.windowState}
  windowZoomFactor={this.state.windowZoomFactor}
>
  {this.renderAppMenuBar()}
</TitleBar>

renderAppMenuBar

See app-menu/app-menu-bar.tsx for the AppMenuBar component.

<AppMenuBar
  appMenu={this.state.appMenuState}
  dispatcher={this.props.dispatcher}
  highlightAppMenuAccessKeys={this.state.highlightAccessKeys}
  foldoutState={foldoutState}
  onLostFocus={this.onMenuBarLostFocus}
/>

renderWelcomeFlow

See welcome/welcome.tsx for the Welcome component.

<Welcome
  dispatcher={this.props.dispatcher}
  appStore={this.props.appStore}
  signInState={this.state.signInState}
/>

renderApp

<div id="desktop-app-contents">
  {this.renderToolbar()}
  {this.renderUpdateBanner()}
  {this.renderRepository()}
  {this.renderPopup()}
  {this.renderAppError()}
</div>

renderToolbar

<Toolbar id="desktop-app-toolbar">
  <div
    className="sidebar-section"
  >
    {this.renderRepositoryToolbarButton()}
  </div>
  {this.renderBranchToolbarButton()}
  {this.renderPushPullToolbarButton()}
</Toolbar>

renderRepositoryToolbarButton

<ToolbarDropdown
  icon={icon}
  title={title}
  description={__DARWIN__ ? 'Current Repository' : 'Current repository'}
  tooltip={tooltip}
  foldoutStyle={foldoutStyle}
  onDropdownStateChanged={this.onRepositoryDropdownStateChanged}
  dropdownContentRenderer={this.renderRepositoryList}
  dropdownState={currentState}
/>

renderBranchToolbarButton

<BranchDropdown
  dispatcher={this.props.dispatcher}
  isOpen={isOpen}
  onDropDownStateChanged={this.onBranchDropdownStateChanged}
  repository={repository}
  repositoryState={selection.state}
  selectedTab={this.state.selectedBranchesTab}
  pullRequests={branchesState.openPullRequests}
  currentPullRequest={branchesState.currentPullRequest}
  isLoadingPullRequests={branchesState.isLoadingPullRequests}
/>

renderPushPullToolbarButton

<PushPullButton
  dispatcher={this.props.dispatcher}
  repository={selection.repository}
  aheadBehind={state.aheadBehind}
  remoteName={remoteName}
  lastFetched={state.lastFetched}
  networkActionInProgress={state.isPushPullFetchInProgress}
  progress={progress}
  tipState={tipState}
/>

renderRepository

If no repository, show the BlankSlateView component.

<BlankSlateView
  onCreate={this.showCreateRepository}
  onClone={this.showCloneRepo}
  onAdd={this.showAddLocalRepo}
/>

If there are repositories, but none of them is selected,

<NoRepositorySelected />

If a repository is selected,

See repository.tsx for the RepositoryView component.

<RepositoryView
  repository={selectedState.repository}
  state={selectedState.state}
  dispatcher={this.props.dispatcher}
  emoji={state.emoji}
  sidebarWidth={state.sidebarWidth}
  commitSummaryWidth={state.commitSummaryWidth}
  issuesStore={this.props.issuesStore}
  gitHubUserStore={this.props.gitHubUserStore}
  onViewCommitOnGitHub={this.onViewCommitOnGitHub}
  imageDiffType={state.imageDiffType}
  askForConfirmationOnDiscardChanges={
    state.askForConfirmationOnDiscardChanges
  }
  accounts={state.accounts}
  externalEditorLabel={externalEditorLabel}
  onOpenInExternalEditor={this.openFileInExternalEditor}
/>

If the app is cloning a repository,

<CloningRepositoryView
  repository={selectedState.repository}
  progress={selectedState.progress}
/>
tags: GitHub Desktop - Electron - React