feat(win): faster popout windows

Summary:
This diff is designed to dramatically speed up new window load time for
all window types and reduce memory consumption of our hot windows.

Before this diff, windows loaded in ~3 seconds. They now boot in a couple
hundred milliseconds without requiring to keep hot windows around for
each and every type of popout window we want to load quickly.

One of the largest bottlenecks was the `require`ing and initializing of
everything in `NylasExports`.

I changed `NylasExports` to be entirely lazily-loaded. Drafts and tasks
now register their constructors with a `StoreRegistry` and the
`TaskRegistry`. This lets us explicitly choose a time to activate these
stores in the window initalization instead of whenever nylas-exports
happens to be required first.

Before, NylasExports was required first when components were first
rendering. This made initial render extremely slow and made the proposed
time picker popout slow.

By moving require into the very initial window boot, we can create a new
scheme of hot windows that are "half loaded". All of the expensive
require-ing and store initialization is done. All we need to do is
activate the packages for just the one window.

This means that the hot window scheme needs to fundamentally change from
have fully pre-loaded windows, to having half-loaded empty hot windows
that can get their window props overridden again.

This led to a major refactor of the WindowManager to support this new
window scheme.

Along the way the API of WindowManager was significantly simplifed.
Instead of a bunch of special-cased windows, there are now consistent
interfaces to get and `ensure` windows are created and displayed. This
DRYed up a lot of repeated logic around showing or creating core windows.

This also allowed the consolidation of the core window configurations into
one place for much easier reasoning about what's getting booted up.

When a hot window goes "live" and gets populated, we simply change the
`windowType`. This now re-triggers the loading of all of the packages for
the window. All of the loading time is now just for the packages that
window requires since core Nylas is there thanks to the hot window
mechanism.

Unfortunately loading all of the packages for the composer was still
unnaceptably slow. The major issue was that all of the composer plugins
were taking a long time to process and initialize. The solution was to
have the main composer load first, then trigger another window load
settings change to change the `windowType` that loads in all of the
plugins.

Another major bottleneck was the `RetinaImg` name lookup on disk. This
requires traversing the entire static folder synchronously on boot. This
is now done once when the main window loads and saved in a cache in the
browser process. Any secondary windows simply ask the backend for this
cache and save the filesystem access time.

The Paper Doc below is the current set of manual tests I'm doing to make
sure no window interactions (there are a lot of them!) regressed.

Test Plan: https://paper.dropbox.com/doc/Window-Refactor-UYsgvjgdXgVlTw8nXTr9h

Reviewers: juan, bengotow

Reviewed By: bengotow

Differential Revision: https://phab.nylas.com/D2916
This commit is contained in:
Evan Morikawa 2016-04-22 13:30:42 -07:00
parent d9d8ffaf45
commit 78112563a6
47 changed files with 793 additions and 1521 deletions

View file

@ -214,7 +214,7 @@
779612: package.json
779612: package.json
2197703: node_modules/season/lib/cson.js
2798802: src/window-secondary-bootstrap.js
2798802: src/secondary-window-bootstrap.js
2799486: node_modules/bluebird/js/main/bluebird.js
2799780: node_modules/bluebird/js/main/promise.js
2197703: node_modules/season/lib/cson.js
@ -596,7 +596,7 @@
2834742: src/window-bootstrap.js
3183165: src/package-manager.js
2951334: src/nylas-env.js
2798802: src/window-secondary-bootstrap.js
2798802: src/secondary-window-bootstrap.js
3268435: node_modules/service-hub/lib/service-hub.js
3270068: node_modules/service-hub/node_modules/event-kit/lib/event-kit.js
3270257: node_modules/service-hub/node_modules/event-kit/lib/emitter.js
@ -3092,7 +3092,7 @@
2765809: node_modules/semver/semver.js
779612: package.json
2197703: node_modules/season/lib/cson.js
2798802: src/window-secondary-bootstrap.js
2798802: src/secondary-window-bootstrap.js
2799486: node_modules/bluebird/js/main/bluebird.js
2799780: node_modules/bluebird/js/main/promise.js
2824131: node_modules/bluebird/js/main/util.js
@ -3282,7 +3282,7 @@
3205643: node_modules/q/q.js
3183165: src/package-manager.js
2951334: src/nylas-env.js
2798802: src/window-secondary-bootstrap.js
2798802: src/secondary-window-bootstrap.js
3268435: node_modules/service-hub/lib/service-hub.js
3270068: node_modules/service-hub/node_modules/event-kit/lib/event-kit.js
3270257: node_modules/service-hub/node_modules/event-kit/lib/emitter.js
@ -4584,7 +4584,7 @@
2765809: node_modules/semver/semver.js
779612: package.json
2197703: node_modules/season/lib/cson.js
2798802: src/window-secondary-bootstrap.js
2798802: src/secondary-window-bootstrap.js
2799486: node_modules/bluebird/js/main/bluebird.js
2799780: node_modules/bluebird/js/main/promise.js
2824131: node_modules/bluebird/js/main/util.js
@ -4774,7 +4774,7 @@
3205643: node_modules/q/q.js
3183165: src/package-manager.js
2951334: src/nylas-env.js
2798802: src/window-secondary-bootstrap.js
2798802: src/secondary-window-bootstrap.js
3268435: node_modules/service-hub/lib/service-hub.js
3270068: node_modules/service-hub/node_modules/event-kit/lib/event-kit.js
3270257: node_modules/service-hub/node_modules/event-kit/lib/emitter.js

View file

@ -28,6 +28,7 @@ export default class ProposedTimePicker extends React.Component {
pendingSave: ProposedTimeCalendarStore.pendingSave(),
});
})
NylasEnv.displayWindow()
}
shouldComponentUpdate(nextProps, nextState) {

View file

@ -9,6 +9,8 @@ import {
} from 'nylas-component-kit'
import {PLUGIN_ID} from '../scheduler-constants'
import NewEventHelper from './new-event-helper'
import ProposedTimeList from './proposed-time-list'
import {
@ -98,13 +100,7 @@ export default class NewEventCard extends React.Component {
}
_onProposeTimes = () => {
NylasEnv.newWindow({
title: "Calendar",
windowType: "calendar",
windowProps: {
draftClientId: this.props.draft.clientId,
},
});
NewEventHelper.launchCalendarWindow(this.props.draft.clientId);
}
_eventStart() {

View file

@ -14,6 +14,15 @@ export default class NewEventHelper {
return moment()
}
static launchCalendarWindow(draftClientId) {
NylasEnv.newWindow({
title: "Calendar",
hidden: true, // Displayed by ProposedTimePicker::componentDidMount
windowType: "calendar",
windowProps: {draftClientId},
});
}
static addEventToSession(session) {
if (!session) { return }
const draft = session.draft()

View file

@ -68,13 +68,7 @@ export default class SchedulerComposerButton extends React.Component {
NewEventHelper.addEventToSession(this.props.session)
if (item === PROPOSAL) {
NylasEnv.newWindow({
title: "Calendar",
windowType: "calendar",
windowProps: {
draftClientId: this.props.draft.clientId,
},
});
NewEventHelper.launchCalendarWindow(this.props.draft.clientId)
}
Actions.closePopover()
}

View file

@ -5,6 +5,10 @@ export default ComposedComponent => class extends React.Component {
static displayName = ComposedComponent.displayName;
static propTypes = {
draftClientId: React.PropTypes.string,
onDraftReady: React.PropTypes.func,
}
static defaultProps = {
onDraftReady: () => {},
}
static containerRequired = false;
@ -54,6 +58,7 @@ export default ComposedComponent => class extends React.Component {
session: session,
draft: session.draft(),
});
this.props.onDraftReady()
});
}

View file

@ -21,40 +21,34 @@ class ComposerWithWindowProps extends React.Component {
constructor(props) {
super(props);
this.state = NylasEnv.getWindowProps()
// We'll now always have windowProps by the time we construct this.
const windowProps = NylasEnv.getWindowProps()
const {draftJSON, draftClientId} = windowProps;
const draft = new Message().fromJSON(draftJSON);
DraftStore._createSession(draftClientId, draft);
this.state = windowProps
}
componentDidMount() {
if (this.state.draftClientId) {
this.ready();
}
this.unlisten = NylasEnv.onWindowPropsReceived((windowProps) => {
const {errorMessage, draftJSON, draftClientId} = windowProps;
if (draftJSON) {
const draft = new Message().fromJSON(draftJSON);
DraftStore._createSession(draftClientId, draft);
}
this.setState({draftClientId});
this.ready();
if (errorMessage) {
this._showInitialErrorDialog(errorMessage);
}
});
}
componentWillUnmount() {
if (this.unlisten) {
this.unlisten();
}
}
ready = () => {
onDraftReady = () => {
this.refs.composer.focus().then(() => {
NylasEnv.getCurrentWindow().show()
NylasEnv.getCurrentWindow().focus()
NylasEnv.displayWindow()
if (this.state.errorMessage) {
this._showInitialErrorDialog(this.state.errorMessage);
}
NylasEnv.getCurrentWindow().updateLoadSettings({
windowType: "composer",
})
// The call to updateLoadSettings will start loading the remaining
// packages. Once those packages load it'll cause a change in the
// root Sheet-level InjectedComponentSet, which will cause
// everything to re-render losing our focus. We have to manually
// refocus it but defer it so the event loop of the package
// activation happens first.
_.defer(() => {
this.refs.composer.focus()
})
});
}
@ -62,6 +56,7 @@ class ComposerWithWindowProps extends React.Component {
return (
<ComposerViewForDraftClientId
ref="composer"
onDraftReady={this.onDraftReady}
draftClientId={this.state.draftClientId}
className="composer-full-window"
/>
@ -91,28 +86,21 @@ export function activate() {
});
if (NylasEnv.isMainWindow()) {
NylasEnv.registerHotWindow({
windowType: 'composer',
replenishNum: 2,
});
ComponentRegistry.register(ComposeButton, {
location: WorkspaceStore.Location.RootSidebar.Toolbar,
});
} else {
NylasEnv.getCurrentWindow().setMinimumSize(480, 250);
WorkspaceStore.defineSheet('Main', {root: true}, {
popout: ['Center'],
});
ComponentRegistry.register(ComposerWithWindowProps, {
location: WorkspaceStore.Location.Center,
});
}
NylasEnv.getCurrentWindow().setMinimumSize(480, 250);
WorkspaceStore.defineSheet('Main', {root: true}, {
popout: ['Center'],
});
ComponentRegistry.register(ComposerWithWindowProps, {
location: WorkspaceStore.Location.Center,
});
}
export function deactivate() {
if (NylasEnv.isMainWindow()) {
NylasEnv.unregisterHotWindow('composer');
}
ComponentRegistry.unregister(ComposerViewForDraftClientId);
ComponentRegistry.unregister(ComposeButton);
ComponentRegistry.unregister(ComposerWithWindowProps);

View file

@ -14,6 +14,7 @@
},
"windowTypes": {
"default": true,
"composer": true
"composer": true,
"composer-preload": true
}
}

View file

@ -36,7 +36,7 @@ class PageRouter extends React.Component
{width, height} = ReactDOM.findDOMNode(@refs.activePage).getBoundingClientRect()
NylasEnv.setSize(width, height)
NylasEnv.center()
NylasEnv.show()
NylasEnv.displayWindow()
_updateWindowSize: =>
return if @_unmounted

View file

@ -4,8 +4,7 @@ _ = require 'underscore'
Actions,
AccountStore,
DatabaseStore,
MailRulesProcessor,
DatabaseObjectRegistry} = require 'nylas-exports'
MailRulesProcessor} = require 'nylas-exports'
NylasLongConnection = require './nylas-long-connection'
NylasSyncWorker = require './nylas-sync-worker'

View file

@ -73,7 +73,6 @@
{ label: 'Reload', command: 'window:reload' }
{ label: 'Toggle Developer Tools', command: 'window:toggle-dev-tools' }
{ label: 'Toggle Component Regions', command: 'window:toggle-component-regions' }
{ label: 'Toggle React Remote', command: 'window:toggle-react-remote' }
{ label: 'Toggle Screenshot Mode', command: 'window:toggle-screenshot-mode' }
{ type: 'separator' }
{ label: 'Open Activity Window', command: 'application:show-work-window' }

View file

@ -54,7 +54,6 @@
{ label: 'Reload', command: 'window:reload' }
{ label: 'Toggle Developer &Tools', command: 'window:toggle-dev-tools' }
{ label: 'Toggle Component Regions', command: 'window:toggle-component-regions' }
{ label: 'Toggle React Remote', command: 'window:toggle-react-remote' }
{ label: 'Toggle Screenshot Mode', command: 'window:toggle-screenshot-mode' }
{ type: 'separator' }
{ label: 'Open Activity Window', command: 'application:show-work-window' }

View file

@ -37,7 +37,6 @@
{ label: '&Reload', command: 'window:reload' }
{ label: 'Toggle Developer &Tools', command: 'window:toggle-dev-tools' }
{ label: 'Toggle Component Regions', command: 'window:toggle-component-regions' }
{ label: 'Toggle React Remote', command: 'window:toggle-react-remote' }
{ label: 'Toggle Screenshot Mode', command: 'window:toggle-screenshot-mode' }
{ type: 'separator' }
{ label: 'Open Activity Window', command: 'application:show-work-window' }

View file

@ -3,8 +3,6 @@ Model = require '../src/flux/models/model'
Attributes = require '../src/flux/attributes'
DatabaseObjectRegistry = require '../src/database-object-registry'
class BadTest
class GoodTest extends Model
@attributes: _.extend {}, Model.attributes,
"foo": Attributes.String
@ -15,25 +13,17 @@ describe 'DatabaseObjectRegistry', ->
beforeEach ->
DatabaseObjectRegistry.unregister("GoodTest")
it "throws an error if the constructor isn't a Model", ->
expect( -> DatabaseObjectRegistry.register()).toThrow()
expect( -> DatabaseObjectRegistry.register(BadTest)).toThrow()
it "can register constructors", ->
expect( -> DatabaseObjectRegistry.register(GoodTest)).not.toThrow()
expect(DatabaseObjectRegistry._constructors["GoodTest"]).toBe GoodTest
it "Retrurns a map of constructors", ->
DatabaseObjectRegistry.register(GoodTest)
map = DatabaseObjectRegistry.classMap()
expect(map.GoodTest).toBe GoodTest
testFn = -> GoodTest
expect( -> DatabaseObjectRegistry.register("GoodTest", testFn)).not.toThrow()
expect(DatabaseObjectRegistry.get("GoodTest")).toBe GoodTest
it "Tests if a constructor is in the registry", ->
DatabaseObjectRegistry.register(GoodTest)
DatabaseObjectRegistry.register("GoodTest", -> GoodTest)
expect(DatabaseObjectRegistry.isInRegistry("GoodTest")).toBe true
it "deserializes the objects for a constructor", ->
DatabaseObjectRegistry.register(GoodTest)
DatabaseObjectRegistry.register("GoodTest", -> GoodTest)
obj = DatabaseObjectRegistry.deserialize("GoodTest", foo: "bar")
expect(obj instanceof GoodTest).toBe true
expect(obj.foo).toBe "bar"

View file

@ -135,7 +135,9 @@ describe("DraftStore", () => {
runs(() => {
expect(NylasEnv.newWindow).toHaveBeenCalledWith({
title: 'Message',
windowType: "composer",
hidden: true,
windowKey: `composer-A`,
windowType: "composer-preload",
windowProps: { draftClientId: "A", draftJSON: this.newDraft.toJSON() },
});
});
@ -155,7 +157,9 @@ describe("DraftStore", () => {
runs(() => {
expect(NylasEnv.newWindow).toHaveBeenCalledWith({
title: 'Message',
windowType: "composer",
hidden: true,
windowKey: `composer-A`,
windowType: "composer-preload",
windowProps: { draftClientId: "A", draftJSON: this.newDraft.toJSON() },
});
});

View file

@ -1,6 +1,7 @@
SystemTrayManager = require './system-tray-manager'
NylasWindow = require './nylas-window'
WindowManager = require './window-manager'
FileListCache = require './file-list-cache'
ApplicationMenu = require './application-menu'
AutoUpdateManager = require './auto-update-manager'
NylasProtocolHandler = require './nylas-protocol-handler'
@ -64,6 +65,8 @@ class Application
constructor: (options) ->
{@resourcePath, @configDirPath, @version, @devMode, @specMode, @safeMode} = options
@fileListCache = new FileListCache(options)
# Normalize to make sure drive letter case is consistent on Windows
@resourcePath = path.normalize(@resourcePath) if @resourcePath
@ -129,7 +132,7 @@ class Application
@launchWithOptions(options)
getMainNylasWindow: ->
@windowManager.mainWindow()
@windowManager.get(WindowManager.MAIN_WINDOW)
getMainWindow: ->
@getMainNylasWindow().browserWindow
@ -202,10 +205,13 @@ class Application
openWindowsForTokenState: (loadingMessage) =>
hasAccount = @config.get('nylas.accounts')?.length > 0
if hasAccount
@windowManager.showMainWindow(loadingMessage)
@windowManager.ensureWorkWindow()
@windowManager.ensureWindow(WindowManager.MAIN_WINDOW, {loadingMessage})
@windowManager.ensureWindow(WindowManager.WORK_WINDOW)
else
@windowManager.ensureOnboardingWindow(welcome: true)
@windowManager.ensureWindow(WindowManager.ONBOARDING_WINDOW, {
title: "Welcome to N1"
windowProps: page: "welcome"
})
# The onboarding window automatically shows when it's ready
_resetConfigAndRelaunch: =>
@ -215,7 +221,10 @@ class Application
@config.set('nylas', null)
@config.set('edgehill', null)
@setDatabasePhase('setup')
@windowManager.ensureOnboardingWindow(welcome: true)
@windowManager.ensureWindow(WindowManager.ONBOARDING_WINDOW, {
title: "Welcome to N1"
windowProps: page: "welcome"
})
_deleteDatabase: (callback) ->
@deleteFileWithRetry path.join(@configDirPath,'edgehill.db'), callback
@ -232,9 +241,7 @@ class Application
return if phase is @_databasePhase
@_databasePhase = phase
@windowManager.windows().forEach (nylasWindow) ->
return unless nylasWindow.browserWindow.webContents
nylasWindow.browserWindow.webContents.send('database-phase-change', phase)
@windowManager.sendToAllWindows("database-phase-change", {}, phase)
rebuildDatabase: =>
return if @_databasePhase is 'close'
@ -279,18 +286,26 @@ class Application
nylasWindow?.browserWindow.inspectElement(x, y)
@on 'application:add-account', (provider) =>
@windowManager.ensureOnboardingWindow({provider})
@on 'application:new-message', => @windowManager.sendToMainWindow('new-message')
@windowManager.ensureWindow(WindowManager.ONBOARDING_WINDOW, {
title: "Add an Account"
windowProps:
page: "account-choose"
pageData: {provider}
})
@on 'application:new-message', => @windowManager.sendToWindow(WindowManager.MAIN_WINDOW, 'new-message')
@on 'application:view-help', =>
url = 'https://nylas.zendesk.com/hc/en-us/sections/203638587-N1'
require('electron').shell.openExternal(url)
@on 'application:open-preferences', => @windowManager.sendToMainWindow('open-preferences')
@on 'application:open-preferences', => @windowManager.sendToWindow(WindowManager.MAIN_WINDOW, 'open-preferences')
@on 'application:show-main-window', => @openWindowsForTokenState()
@on 'application:show-work-window', => @windowManager.showWorkWindow()
@on 'application:show-work-window', =>
win = @windowManager.get(WindowManager.WORK_WINDOW)
win.show()
win.focus()
@on 'application:check-for-update', => @autoUpdateManager.check()
@on 'application:install-update', =>
@quitting = true
@windowManager.unregisterAllHotWindows()
@windowManager.cleanupBeforeAppQuit()
@autoUpdateManager.install()
@on 'application:toggle-dev', =>
@ -326,7 +341,7 @@ class Application
@on 'application:zoom', -> @windowManager.focusedWindow()?.maximize()
app.on 'window-all-closed', =>
@windowManager.windowClosedOrHidden()
@windowManager.quitWinLinuxIfNoWindows()
# Called before the app tries to close any windows.
app.on 'before-quit', =>
@ -334,7 +349,7 @@ class Application
@quitting = true
# Destroy hot windows so that they can't block the app from quitting.
# (Electron will wait for them to finish loading before quitting.)
@windowManager.unregisterAllHotWindows()
@windowManager.cleanupBeforeAppQuit()
@systemTrayManager.destroyTray()
# Called after the app has closed all windows.
@ -362,19 +377,10 @@ class Application
app.dock?.setBadge?(value)
ipcMain.on 'new-window', (event, options) =>
@windowManager.newWindow(options)
ipcMain.on 'register-hot-window', (event, options) =>
@windowManager.registerHotWindow(options)
ipcMain.on 'unregister-hot-window', (event, windowType) =>
@windowManager.unregisterHotWindow(windowType)
ipcMain.on 'from-react-remote-window', (event, json) =>
@windowManager.sendToMainWindow('from-react-remote-window', json)
ipcMain.on 'from-react-remote-window-selection', (event, json) =>
@windowManager.sendToMainWindow('from-react-remote-window-selection', json)
if options.windowKey
@windowManager.ensureWindow(options.windowKey, options)
else
@windowManager.newWindow(options)
ipcMain.on 'inline-style-parse', (event, {html, key}) =>
juice = require 'juice'
@ -420,13 +426,10 @@ class Application
ipcMain.on 'action-bridge-rebroadcast-to-all', (event, args...) =>
win = BrowserWindow.fromWebContents(event.sender)
@windowManager.windows().forEach (nylasWindow) ->
return if nylasWindow.browserWindow == win
return unless nylasWindow.browserWindow.webContents
nylasWindow.browserWindow.webContents.send('action-bridge-message', args...)
@windowManager.sendToAllWindows('action-bridge-message', {except: win}, args...)
ipcMain.on 'action-bridge-rebroadcast-to-work', (event, args...) =>
workWindow = @windowManager.workWindow()
workWindow = @windowManager.get(WindowManager.WORK_WINDOW)
return if not workWindow or not workWindow.browserWindow.webContents
return if BrowserWindow.fromWebContents(event.sender) is workWindow
workWindow.browserWindow.webContents.send('action-bridge-message', args...)
@ -437,21 +440,21 @@ class Application
clipboard.writeText(selectedText, 'selection')
ipcMain.on 'account-setup-successful', (event) =>
@windowManager.showMainWindow()
@windowManager.ensureWorkWindow()
@windowManager.onboardingWindow()?.close()
@windowManager.ensureWindow(WindowManager.MAIN_WINDOW)
@windowManager.ensureWindow(WindowManager.WORK_WINDOW)
@windowManager.get(WindowManager.ONBOARDING_WINDOW)?.close()
ipcMain.on 'new-account-added', (event) =>
@windowManager.ensureWorkWindow()
@windowManager.ensureWindow(WindowManager.WORK_WINDOW)
ipcMain.on 'run-in-window', (event, params) =>
@_sourceWindows ?= {}
sourceWindow = BrowserWindow.fromWebContents(event.sender)
@_sourceWindows[params.taskId] = sourceWindow
if params.window is "work"
targetWindow = @windowManager.workWindow()
targetWindow = @windowManager.get(WindowManager.WORK_WINDOW)
else if params.window is "main"
targetWindow = @windowManager.mainWindow()
targetWindow = @windowManager.get(WindowManager.MAIN_WINDOW)
else throw new Error("We don't support running in that window")
return if not targetWindow or not targetWindow.browserWindow.webContents
targetWindow.browserWindow.webContents.send('run-in-window', params)
@ -478,7 +481,7 @@ class Application
else
unless @sendCommandToFirstResponder(command)
focusedBrowserWindow = BrowserWindow.getFocusedWindow()
mainWindow = @windowManager.mainWindow()
mainWindow = @windowManager.get(WindowManager.MAIN_WINDOW)
if focusedBrowserWindow
switch command
when 'window:reload' then focusedBrowserWindow.reload()
@ -519,12 +522,12 @@ class Application
openUrl: (urlToOpen) ->
{protocol} = url.parse(urlToOpen)
if protocol is 'mailto:'
@windowManager.sendToMainWindow('mailto', urlToOpen)
@windowManager.sendToWindow(WindowManager.MAIN_WINDOW, 'mailto', urlToOpen)
else
console.log "Ignoring unknown URL type: #{urlToOpen}"
openComposerWithFiles: (pathsToOpen) ->
@windowManager.sendToMainWindow('mailfiles', pathsToOpen)
@windowManager.sendToWindow(WindowManager.MAIN_WINDOW, 'mailfiles', pathsToOpen)
# Opens up a new {NylasWindow} to run specs within.
#
@ -538,7 +541,9 @@ class Application
# :specPath - The directory to load specs from.
# :safeMode - A Boolean that, if true, won't run specs from ~/.nylas/packages
# and ~/.nylas/dev/packages, defaults to false.
runSpecs: ({exitWhenDone, showSpecsInWindow, resourcePath, specDirectory, specFilePattern, logFile, safeMode}) ->
runSpecs: (specWindowOptions) ->
{resourcePath} = specWindowOptions
if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath)
resourcePath = @resourcePath
@ -547,12 +552,12 @@ class Application
catch error
bootstrapScript = require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'spec-bootstrap'))
isSpec = true
devMode = true
safeMode ?= false
# Important: Use .nylas-spec instead of .nylas to avoid overwriting the
# user's real email config!
configDirPath = path.join(app.getPath('home'), '.nylas-spec')
new NylasWindow({bootstrapScript, configDirPath, resourcePath, exitWhenDone, isSpec, devMode, specDirectory, specFilePattern, logFile, safeMode, showSpecsInWindow})
specWindowOptions.resourcePath = resourcePath
specWindowOptions.configDirPath = configDirPath
specWindowOptions.bootstrapScript = bootstrapScript
@windowManager.ensureWindow(WindowManager.SPEC_WINDOW, specWindowOptions)

View file

@ -69,7 +69,7 @@ class AutoUpdateManager
autoUpdater.on 'update-downloaded', (event, @releaseNotes, @releaseVersion) =>
@setState(UpdateAvailableState)
@emitUpdateAvailableEvent(@getWindows()...)
@emitUpdateAvailableEvent()
@check(hidePopups: true)
setInterval =>
@ -82,10 +82,10 @@ class AutoUpdateManager
if autoUpdater.supportsUpdates?
@setState(UnsupportedState) unless autoUpdater.supportsUpdates()
emitUpdateAvailableEvent: (windows...) ->
emitUpdateAvailableEvent: ->
return unless @releaseVersion
for nylasWindow in windows
nylasWindow.sendMessage('update-available', {@releaseVersion, @releaseNotes})
global.application.windowManager.sendToAllWindows("update-available",
{}, {@releaseVersion, @releaseNotes})
setState: (state) ->
return if @state is state
@ -141,6 +141,3 @@ class AutoUpdateManager
message: 'There was an error checking for updates.'
title: 'Update Error'
detail: message
getWindows: ->
global.application.windowManager.windows()

View file

@ -0,0 +1,12 @@
// File operations (like traversing directory trees) are extremely
// expensive. If any window traverses a tree once, we keep a cache of it
// on the backend process. That way any new windows don't need to spend
// their precious load time performing the same expensive operation.
export default class FileListCache {
constructor() {
this.imageData = "{}" // A JSON stringified hash
this.packagePaths = []
this.lessCacheImportPaths = []
this.lessCacheImportedFiles = []
}
}

View file

@ -6,6 +6,7 @@ _ = require 'underscore'
{EventEmitter} = require 'events'
WindowIconPath = null
idNum = 0
module.exports =
class NylasWindow
@ -27,13 +28,19 @@ class NylasWindow
showSpecsInWindow,
@isSpec,
@devMode,
@windowKey,
@safeMode,
@neverClose,
@mainWindow,
@windowType,
@resourcePath,
@exitWhenDone,
@configDirPath} = settings
if !@windowKey
@windowKey = "#{@windowType}-#{idNum}"
idNum += 1
# Normalize to make sure drive letter case is consistent on Windows
@resourcePath = path.normalize(@resourcePath) if @resourcePath
@ -81,7 +88,7 @@ class NylasWindow
options.icon = WindowIconPath
@browserWindow = new BrowserWindow(options)
global.application.windowManager.addWindow(this)
@browserWindow.updateLoadSettings = @updateLoadSettings
@handleEvents()
@ -104,7 +111,7 @@ class NylasWindow
if fs.statSyncNoException(pathToOpen).isFile?()
loadSettings.initialPath = path.dirname(pathToOpen)
@setLoadSettings(loadSettings)
@browserWindow.loadSettings = loadSettings
@browserWindow.once 'window:loaded', =>
@loaded = true
@ -117,14 +124,27 @@ class NylasWindow
@browserWindow.loadURL(@getURL(loadSettings))
@browserWindow.focusOnWebView() if @isSpec
# Let the applicationMenu know that there's a new window available.
# The applicationMenu automatically listens to the `closed` event of
# the browserWindow to unregister itself
global.application.applicationMenu?.addWindow(@browserWindow)
updateLoadSettings: (newSettings={}) =>
@loaded = true
@setLoadSettings(Object.assign({}, @browserWindow.loadSettings, newSettings))
loadSettings: ->
@browserWindow.loadSettings
# This gets called when we want to turn a WindowLauncher.EMPTY_WINDOW
# into a new kind of custom popout window.
#
# The windowType will change which will cause a new set of plugins to
# load.
setLoadSettings: (loadSettings) ->
@browserWindow.loadSettings = loadSettings
@browserWindow.loadSettingsChangedSinceGetURL = true
if @loaded
@browserWindow.webContents.send('load-settings-changed', loadSettings)
@browserWindow.webContents.send('load-settings-changed', loadSettings)
getURL: (loadSettingsObj) ->
# Ignore the windowState when passing loadSettings via URL, since it could
@ -148,8 +168,17 @@ class NylasWindow
new ContextMenu(menuTemplate, this)
handleEvents: ->
# Also see logic in `NylasEnv::onBeforeUnload` and
# `WindowEventHandler::AddUnloadCallback`. Classes like the DraftStore
# and ActionBridge intercept the closing of windows and perform
# action.
#
# This uses the DOM's `beforeunload` event.
@browserWindow.on 'close', (event) =>
if @neverClose and !global.application.quitting
# For neverClose windows (like the main window) simply hide and
# take out of full screen.
event.preventDefault()
if @browserWindow.isFullScreen()
@browserWindow.once 'leave-full-screen', =>
@ -157,10 +186,12 @@ class NylasWindow
@browserWindow.setFullScreen(false)
else
@browserWindow.hide()
@emit 'window:close-prevented'
@browserWindow.on 'closed', =>
global.application.windowManager.removeWindow(this)
# HOWEVER! If the neverClose window is the last window open, and
# it looks like there's no windows actually quit the application
# on Linux & Windows.
if not @isSpec
global.application.windowManager.quitWinLinuxIfNoWindows()
@browserWindow.on 'scroll-touch-begin', =>
@browserWindow.webContents.send('scroll-touch-begin')

View file

@ -0,0 +1,98 @@
import NylasWindow from './nylas-window'
const DEBUG_SHOW_HOT_WINDOW = false;
/**
* It takes a full second or more to bootup a Nylas window. Most of this
* is due to sheer amount of time it takes to parse all of the javascript
* and follow the require tree.
*
* Since popout windows need to be more responsive than that, we pre-load
* "hot" windows in the background that have most of the code loaded. Then
* all we need to do is load the handful of packages the window
* requires and show it.
*/
export default class WindowLauncher {
static EMPTY_WINDOW = "emptyWindow"
constructor(appOpts) {
this.defaultWindowOpts = {
hidden: false,
devMode: appOpts.devMode,
safeMode: appOpts.safeMode,
windowType: WindowLauncher.EMPTY_WINDOW,
resourcePath: appOpts.resourcePath,
configDirPath: appOpts.configDirPath,
}
this.hotWindow = new NylasWindow(this._hotWindowOpts());
if (DEBUG_SHOW_HOT_WINDOW) {
this.hotWindow.showWhenLoaded()
}
}
newWindow(options) {
const opts = Object.assign({}, this.defaultWindowOpts, options);
let win;
if (opts.bootstrapScript) {
win = new NylasWindow(opts)
} else {
opts.bootstrapScript = this._secondaryWindowBootstrap()
if (opts.coldStartOnly) {
// Useful for the Worker Window: A secondary window that shouldn't
// be hot-loaded
win = new NylasWindow(opts)
} else {
win = this.hotWindow;
// Regenerate the hot window.
this.hotWindow = new NylasWindow(this._hotWindowOpts());
if (DEBUG_SHOW_HOT_WINDOW) {
this.hotWindow.showWhenLoaded()
}
const newLoadSettings = Object.assign({}, win.loadSettings(), opts)
if (newLoadSettings.windowType === WindowLauncher.EMPTY_WINDOW) {
throw new Error("Must specify a windowType")
}
// Reset the loaded state and update the load settings.
// This will fire `NylasEnv::populateHotWindow` and reload the
// packages.
win.setLoadSettings(newLoadSettings);
}
}
if (!opts.hidden) {
// NOTE: In the case of a cold window, this will show it once
// loaded. If it's a hotWindow, since hotWindows have a
// `hidden:true` flag, nothing will show. When `setLoadSettings`
// starts populating the window in `populateHotWindow` we'll show or
// hide based on the windowOpts
win.showWhenLoaded()
}
return win
}
// Note: This method calls `browserWindow.destroy()` which closes
// windows without waiting for them to load or firing window lifecycle
// events. This is necessary for the app to quit promptly on Linux.
// https://phab.nylas.com/T1282
cleanupBeforeAppQuit() {
this.hotWindow.browserWindow.destroy()
}
_secondaryWindowBootstrap() {
if (!this._bootstrap) {
this._bootstrap = require.resolve("../secondary-window-bootstrap")
}
return this._bootstrap
}
_hotWindowOpts() {
const hotWindowOpts = Object.assign({}, this.defaultWindowOpts);
hotWindowOpts.packageLoadingDeferred = true;
hotWindowOpts.bootstrapScript = this._secondaryWindowBootstrap();
hotWindowOpts.hidden = DEBUG_SHOW_HOT_WINDOW;
return hotWindowOpts
}
}

View file

@ -1,450 +1,73 @@
_ = require 'underscore'
fs = require 'fs-plus'
NylasWindow = require './nylas-window'
WindowLauncher = require './window-launcher'
{BrowserWindow, app} = require 'electron'
class WindowManager
constructor: ({@devMode, @safeMode, @resourcePath, @configDirPath, @config, @initializeInBackground}) ->
@_windows = []
@_mainWindow = null
@_workWindow = null
@_hotWindows = {}
@MAIN_WINDOW: "default"
@WORK_WINDOW: "work"
@SPEC_WINDOW: "spec"
@ONBOARDING_WINDOW: "onboarding"
constructor: (appOpts) ->
{@initializeInBackground} = appOpts
@_windows = {}
@windowLauncher = new WindowLauncher(appOpts)
# Be sure to register the very first hot window! If you don't, then
# the first window (only) won't get window events (like being notified
# the database is setup), which causes components loaded in that
# window to not work and may even prevent window closure (like in the
# case of the composer)
@_registerWindow(@windowLauncher.hotWindow)
get: (winId) -> @_windows[winId]
newWindow: (options={}) ->
win = @windowLauncher.newWindow(options)
@_registerWindow(win)
return win
_registerWindow: (win) =>
@_windows[win.windowKey] = win
win.browserWindow.on "closed", =>
delete @_windows[win.windowKey]
@quitWinLinuxIfNoWindows()
ensureWindow: (winId, extraOpts) ->
win = @_windows[winId]
if win
return if win.loadSettings().hidden
if win.isMinimized()
win.restore()
win.focus()
else if !win.isVisible()
win.showWhenLoaded()
else
win.focus()
else
@newWindow(@_coreWindowOpts(winId, extraOpts))
sendToWindow: (winId, args...) ->
if not @_windows[winId]
throw new Error("Can't find window: #{winId}")
@_windows[winId].sendMessage(args...)
sendToAllWindows: (msg, {except}, args...) ->
for winId, win of @_windows
continue if win.browserWindow == except
continue unless win.browserWindow.webContents
win.browserWindow.webContents.send(msg, args...)
closeAllWindows: ->
@closeMainWindow()
@closeWorkWindow()
@unregisterAllHotWindows()
for win in @_windows
win.close()
win.close() for winId, win of @_windows
windows: ->
@_windows
cleanupBeforeAppQuit: -> @windowLauncher.cleanupBeforeAppQuit()
windowWithPropsMatching: (props) ->
_.find @_windows, (nylasWindow) ->
{windowProps} = nylasWindow.loadSettings()
return false unless windowProps
_.every Object.keys(props), (key) -> _.isEqual(props[key],windowProps[key])
focusedWindow: ->
_.find @_windows, (nylasWindow) -> nylasWindow.isFocused()
visibleWindows: ->
_.filter @_windows, (nylasWindow) -> nylasWindow.isVisible()
###
Main Window
The main window is different from the others, because only one can exist at any
given time and it is hidden instead of closed so that mail processing still
happens.
###
mainWindow: ->
@_mainWindow
sendToMainWindow: ->
return unless @_mainWindow
@_mainWindow.sendMessage(arguments...)
closeMainWindow: ->
return unless @_mainWindow
@_mainWindow.neverClose = false
@_mainWindow.close()
@_mainWindow = null
showMainWindow: (loadingMessage) ->
if @_mainWindow
if @_mainWindow.isMinimized()
@_mainWindow.restore()
@_mainWindow.focus()
else if !@_mainWindow.isVisible()
@_mainWindow.showWhenLoaded()
else
@_mainWindow.focus()
else
if @devMode
try
bootstrapScript = require.resolve(path.join(@resourcePath, 'src', 'window-bootstrap'))
resourcePath = @resourcePath
bootstrapScript ?= require.resolve('../window-bootstrap')
resourcePath ?= @resourcePath
@_mainWindow = new NylasWindow _.extend {}, @defaultWindowOptions(),
loadingMessage: loadingMessage
bootstrapScript: bootstrapScript
neverClose: true
mainWindow: true
windowType: 'default'
initializeInBackground: @initializeInBackground
# The position and resizable bit gets reset when the window
# finishes loading. This represents the state of our "loading"
# window.
center: true
width: 640
height: 396
resizable: false
###
Work Window
###
workWindow: ->
@_workWindow
closeWorkWindow: ->
return unless @_workWindow
@_workWindow.neverClose = false
@_workWindow.close()
@_workWindow = null
ensureWorkWindow: ->
@_workWindow ?= @newWindow
windowType: 'work'
title: 'Activity'
toolbar: true
neverClose: true
width: 800
height: 400
hidden: true
showWorkWindow: ->
return unless @_workWindow
if @_workWindow.isMinimized()
@_workWindow.restore()
@_workWindow.focus()
else if !@_workWindow.isVisible()
@_workWindow.showWhenLoaded()
else
@_workWindow.focus()
###
Onboarding Window
The onboarding window is a normal secondary window, but the WindowManager knows
how to create it itself.
###
onboardingWindow: ->
@windowWithPropsMatching({uniqueId: 'onboarding'})
# Returns a new onboarding window
#
ensureOnboardingWindow: ({welcome, provider}={}) ->
existing = @onboardingWindow()
if existing
existing.focus()
else
options =
title: "Add an Account"
toolbar: false
resizable: false
hidden: true # The `PageRouter` will center and show on load
windowType: 'onboarding'
windowProps:
page: 'account-choose'
uniqueId: 'onboarding'
pageData: {provider}
if welcome
options.title = "Welcome to N1"
options.windowProps.page = 'welcome'
@newWindow(options)
# Makes a new window appear of a certain `windowType`.
#
# In almost all cases, instead of booting up a new window from scratch,
# we pass in new `windowProps` to a pre-loaded "hot window".
#
# Individual packages declare what windowTypes they support. We use this
# to determine what packages to load in a given `windowType`. Inside a
# package's `package.json` we expect to find an entry of the form:
#
# "windowTypes": {
# "myCustomWindowType": true
# "someOtherWindowType": true
# "composer": true
# }
#
# Individual packages must also call `registerHotWindow` upon activation
# to start the prepartion of `hotWindows` of various types.
#
# Once a hot window is registered, we'll have a hidden window with the
# declared packages of that `windowType` pre-loaded.
#
# This means that when `newWindow` is called, instead of going through
# the bootup process, it simply replaces key parameters and does a soft
# reload.
#
# To listen for window props being sent to your existing hot-loaded window,
# add a callback to `NylasEnv.onWindowPropsChanged`.
#
# Since the window is already loaded, there are only some options that
# can be soft-reloaded. If you attempt to pass options that a soft
# reload doesn't support, you'll be forced to load from a `coldStart`.
#
# Any options passed in here will be passed into the NylasWindow
# constructor, which will eventually show up in the window's main
# loadSettings, which is accessible via `NylasEnv.getLoadSettings()`
#
# REQUIRED options:
# - windowType: defaults "popout". This eventually ends up as
# NylasEnv.getWindowType()
#
# Valid options:
# - coldStart: true
# - windowProps: A good place to put any data components of the window
# need to initialize properly. NOTE: You can only put JSON
# serializable data. No functions!
# - title: The title of the page
#
# Other non required options:
# - All of the options of BrowserWindow
# https://github.com/atom/electron/blob/master/docs/api/browser-window.md#new-browserwindowoptions
#
# Returns a new NylasWindow
#
newWindow: (options={}) ->
if options.coldStart or not @_hotWindows[options.windowType]?
return @newColdWindow(options)
else
return @newHotWindow(options)
# This sets up some windows in the background with the requested
# packages already pre-loaded into it.
#
# REQUIRED options:
# - windowType: registers a new hot window of the given type. This is
# the key we use to find what packages to load and what kind of window
# to open
#
# Optional options:
# - replenishNum - (defaults 1) The number of hot windows to keep
# loaded at any given time. If your package is expected to use a large
# number of windows, it may be advisable to make this number more than
# 1. Beware that each load is very resource intensive.
#
# - windowPackages - A list of additional packages to load into a
# window in addition to those declared in various `package.json`s
#
registerHotWindow: ({windowType, replenishNum, windowPackages, windowOptions}={}) ->
if not windowType
throw new Error("registerHotWindow: please provide a windowType")
@_hotWindows ?= {}
@_hotWindows[windowType] ?= {}
@_hotWindows[windowType].replenishNum ?= (replenishNum ? 1)
@_hotWindows[windowType].loadedWindows ?= []
@_hotWindows[windowType].windowPackages ?= (windowPackages ? [])
@_hotWindows[windowType].windowOptions ?= (windowOptions ? {})
@_replenishHotWindows()
unregisterHotWindow: (windowType) ->
return unless @_hotWindows[windowType]
# Remove entries from the replentishQueue
@_replenishQueue = _.reject @_replenishQueue, (item) => item.windowType is windowType
# Destroy any hot windows already loaded
destroyedLoadingWindow = false
{loadedWindows} = @_hotWindows[windowType]
for win in loadedWindows
destroyedLoadingWindow = true if not win.isLoaded()
win.browserWindow.destroy()
# Delete the hot window configuration
delete @_hotWindows[windowType]
# If we destroyed a window that was currently loading,
# the queue will stop processing forever.
if destroyedLoadingWindow
@_processingQueue = false
@_processReplenishQueue()
# Immediately close all of the hot windows and reset the replentish queue
# to prevent more from being opened without additional calls to registerHotWindow.
#
# Note: This method calls `browserWindow.destroy()` which closes windows without
# waiting for them to load or firing window lifecycle events. This is necessary
# for the app to quit promptly on Linux. https://phab.nylas.com/T1282
#
unregisterAllHotWindows: ->
for type, {loadedWindows} of @_hotWindows
for win in loadedWindows
win.browserWindow.destroy()
@_replenishQueue = []
@_hotWindows = {}
defaultWindowOptions: ->
#TODO: Defaults are also applied in NylasWindow.constructor.
devMode: @devMode
safeMode: @safeMode
windowType: 'popout'
resourcePath: @resourcePath
configDirPath: @configDirPath
bootstrapScript: require.resolve("../window-secondary-bootstrap")
newColdWindow: (options={}) ->
options = _.extend(@defaultWindowOptions(), options)
win = new NylasWindow(options)
newLoadSettings = _.extend(win.loadSettings(), options)
win.setLoadSettings(newLoadSettings)
win.showWhenLoaded() unless options.hidden
return win
# Tries to create a new hot window. Since we're updating an existing
# window instead of creatinga new one, there are limitations in the
# options you can provide.
#
# Returns a new NylasWindow
#
newHotWindow: (options={}) ->
hotWindowParams = @_hotWindows[options.windowType]
win = null
if not hotWindowParams?
console.log "WindowManager: Warning! The requested windowType
'#{options.windowType}' has not been registered. Be sure to call
`registerWindowType` first in your packages setup."
return @newColdWindow(options)
supportedHotWindowKeys = [
"x"
"y"
"title"
"width"
"height"
"bounds"
"windowType"
"windowProps"
]
unsupported = _.difference(Object.keys(options), supportedHotWindowKeys)
if unsupported.length > 0
console.log "WindowManager: For the winodw of type
#{options.windowType}, you are passing options that can't be
applied to the preloaded window (#{JSON.stringify(unsupported)}).
Please change the options or pass the `coldStart:true` option to use
a new window instead of a hot window. If it's just data for the
window, please put them in the `windowProps` param."
if hotWindowParams.loadedWindows.length is 0
# No windows ready
console.log "No windows ready. Loading a new coldWindow"
options.windowPackages = hotWindowParams.windowPackages
win = @newColdWindow(options)
else
[win] = hotWindowParams.loadedWindows.splice(0,1)
newLoadSettings = _.extend(win.loadSettings(), options)
win.setLoadSettings(newLoadSettings)
win.browserWindow.setTitle options.title ? ""
if options.x and options.y
win.browserWindow.setPosition options.x, options.w
if options.width or options.height
[w,h] = win.browserWindow.getSize()
w = options.width ? w
h = options.height ? h
win.browserWindow.setSize(w,h)
if options.bounds
win.browserWindow.setBounds options.bounds
@_replenishHotWindows()
return win
# There may be many windowTypes, each that request many windows of that
# type (the `replenishNum`).
#
# Loading windows is very resource intensive, so we want to do them
# sequentially.
#
# We also want to round-robin load across the breadth of window types
# instead of loading all of the windows of a single type then moving on
# to the next.
#
# We first need to cycle through the registered `hotWindows` and create
# a breadth-first queue of window loads that we'll store in
# `@_replenishQueue`.
#
# Next we need to start processing the `@_replenishQueue`
__replenishHotWindows: =>
@_replenishQueue = []
queues = {}
maxWin = 0
for windowType, data of @_hotWindows
numOfType = data.replenishNum - data.loadedWindows.length
maxWin = Math.max(numOfType, maxWin)
if numOfType > 0
options = _.extend {}, @defaultWindowOptions(), data.windowOptions
options.windowType = windowType
options.windowPackages = data.windowPackages
queues[windowType] ?= []
queues[windowType].push(options) for [0...numOfType]
for [0...maxWin]
for windowType, optionsArray of queues
if optionsArray.length > 0
@_replenishQueue.push(optionsArray.shift())
@_processReplenishQueue()
_replenishHotWindows: _.debounce(WindowManager::__replenishHotWindows, 100)
_processReplenishQueue: ->
return if @_processingQueue
@_processingQueue = true
if @_replenishQueue.length > 0
options = @_replenishQueue.shift()
console.log "WindowManager: Preparing a new '#{options.windowType}' window"
newWindow = new NylasWindow(options)
@_hotWindows[options.windowType].loadedWindows.push(newWindow)
newWindow.once 'window:loaded', =>
@_processingQueue = false
@_processReplenishQueue()
else
@_processingQueue = false
###
Methods called from NylasWindow
###
# Public: Removes the {NylasWindow} from the global window list.
removeWindow: (window) ->
@_windows.splice @_windows.indexOf(window), 1
if window is @_mainWindow
@_mainWindow = null
if window is @_workWindow
@_workWindow = null
@applicationMenu?.enableWindowSpecificItems(false) if @_windows.length == 0
@windowClosedOrHidden()
# Public: Adds the {NylasWindow} to the global window list.
# IMPORTANT: NylasWindows add themselves - you don't need to manually add them
addWindow: (window) ->
@_windows.push window
global.application.applicationMenu?.addWindow(window.browserWindow)
window.once 'window:loaded', =>
global.application.autoUpdateManager.emitUpdateAvailableEvent(window)
unless window.isSpec
closePreventedHandler = => @windowClosedOrHidden()
window.on 'window:close-prevented', closePreventedHandler
window.browserWindow.once 'closed', =>
window.removeListener('window:close-prevented', closePreventedHandler)
windowClosedOrHidden: ->
quitWinLinuxIfNoWindows: ->
# Typically, N1 stays running in the background on all platforms, since it
# has a status icon you can use to quit it.
#
@ -457,12 +80,58 @@ class WindowManager
#
if process.platform in ['win32', 'linux']
@quitCheck ?= _.debounce =>
noVisibleWindows = @visibleWindows().length is 0
noMainWindowLoaded = not @mainWindow()?.isLoaded()
if noVisibleWindows and noMainWindowLoaded
visibleWindows = _.filter(@_windows, (win) -> win.isVisible())
noMainWindowLoaded = not @get(WindowManager.MAIN_WINDOW)?.isLoaded()
if visibleWindows.length is 0 and noMainWindowLoaded
app.quit()
, 10000
@quitCheck()
focusedWindow: -> _.find(@_windows, (win) -> win.isFocused())
_coreWindowOpts: (winId, extraOpts={}) ->
coreWinOpts = {}
coreWinOpts[WindowManager.MAIN_WINDOW] =
windowKey: WindowManager.MAIN_WINDOW
windowType: WindowManager.MAIN_WINDOW
title: "Nylas N1"
neverClose: true
bootstrapScript: require.resolve("../window-bootstrap")
mainWindow: true
width: 640
height: 396
center: true
resizable: false
initializeInBackground: @initializeInBackground
coreWinOpts[WindowManager.WORK_WINDOW] =
windowKey: WindowManager.WORK_WINDOW
windowType: WindowManager.WORK_WINDOW
coldStartOnly: true # It's a secondary window, but not a hot window
title: "Activity"
toolbar: true
hidden: true
neverClose: true
width: 800
height: 400
coreWinOpts[WindowManager.ONBOARDING_WINDOW] =
windowKey: WindowManager.ONBOARDING_WINDOW
windowType: WindowManager.ONBOARDING_WINDOW
resizable: false
toolbar: false
hidden: true # Displayed by PageRouter::_initializeWindowSize
# The SPEC_WINDOW gets passed its own bootstrapScript
coreWinOpts[WindowManager.SPEC_WINDOW] =
windowKey: WindowManager.SPEC_WINDOW
windowType: WindowManager.SPEC_WINDOW
hidden: true,
isSpec: true,
devMode: true,
defaultOptions = coreWinOpts[winId] ? {}
return Object.assign({}, defaultOptions, extraOpts)
module.exports = WindowManager

View file

@ -1,4 +1,5 @@
React = require 'react'
ReactDOM = require 'react-dom'
_ = require 'underscore'
{CompositeDisposable} = require 'event-kit'

View file

@ -1,16 +0,0 @@
_ = require 'underscore'
Model = null
SerializableRegistry = require './serializable-registry'
class DatabaseObjectRegistry extends SerializableRegistry
classMap: -> return @_constructors
register: (constructor) ->
Model ?= require './flux/models/model'
if constructor?.prototype instanceof Model
super
else
throw new Error("You must register a Database Object class with this registry", constructor)
module.exports = new DatabaseObjectRegistry()

View file

@ -0,0 +1,6 @@
import SerializableRegistry from './serializable-registry'
class DatabaseObjectRegistry extends SerializableRegistry { }
const registry = new DatabaseObjectRegistry()
export default registry

View file

@ -1,5 +1,3 @@
_ = require 'underscore'
class DeprecateUtils
# See
# http://www.codeovertones.com/2011/08/how-to-print-stack-trace-anywhere-in.html
@ -26,7 +24,7 @@ class DeprecateUtils
)
warn = false
return fn.apply(ctx, arguments)
return _.extend(newFn, fn)
return Object.assign(newFn, fn)
return fn
module.exports = DeprecateUtils

View file

@ -4,9 +4,11 @@ path = require('path')
moment = require('moment-timezone')
tz = Intl.DateTimeFormat().resolvedOptions().timeZone
TaskRegistry = null
DatabaseObjectRegistry = null
DefaultResourcePath = null
TaskRegistry = require '../../task-registry'
DatabaseObjectRegistry = require '../../database-object-registry'
imageData = null
module.exports =
Utils =
@ -36,9 +38,6 @@ Utils =
type = v?.__constructorName
return v unless type
TaskRegistry ?= require '../../task-registry'
DatabaseObjectRegistry ?= require '../../database-object-registry'
if DatabaseObjectRegistry.isInRegistry(type)
return DatabaseObjectRegistry.deserialize(type, v)
@ -48,9 +47,6 @@ Utils =
return v
registeredObjectReplacer: (k, v) ->
TaskRegistry ?= require '../../task-registry'
DatabaseObjectRegistry ?= require '../../database-object-registry'
if _.isObject(v)
type = this[k].constructor.name
if DatabaseObjectRegistry.isInRegistry(type) or TaskRegistry.isInRegistry(type)
@ -189,17 +185,23 @@ Utils =
DefaultResourcePath ?= NylasEnv.getLoadSettings().resourcePath
resourcePath ?= DefaultResourcePath
Utils.images ?= {}
if not Utils.images[resourcePath]?
if not imageData
imageData = NylasEnv.fileListCache().imageData ? "{}"
Utils.images = JSON.parse(imageData) ? {}
if not Utils?.images?[resourcePath]
Utils.images ?= {}
Utils.images[resourcePath] ?= {}
imagesPath = path.join(resourcePath, 'static', 'images')
files = fs.listTreeSync(imagesPath)
Utils.images[resourcePath] ?= {}
for file in files
# On Windows, we get paths like C:\images\compose.png, but Chromium doesn't
# accept the backward slashes. Convert to C:/images/compose.png
# On Windows, we get paths like C:\images\compose.png, but
# Chromium doesn't accept the backward slashes. Convert to
# C:/images/compose.png
file = file.replace(/\\/g, '/')
basename = path.basename(file)
Utils.images[resourcePath][path.basename(file)] = file
NylasEnv.fileListCache().imageData = JSON.stringify(Utils.images)
plat = process.platform ? ""
ratio = window.devicePixelRatio ? 1

View file

@ -12,13 +12,13 @@ class DatabaseSetupQueryBuilder
setupQueries: ->
queries = []
for key, klass of DatabaseObjectRegistry.classMap()
for klass in DatabaseObjectRegistry.getAllConstructors()
queries = queries.concat @setupQueriesForTable(klass)
return queries
analyzeQueries: ->
queries = []
for key, klass of DatabaseObjectRegistry.classMap()
for klass in DatabaseObjectRegistry.getAllConstructors()
attributes = _.values(klass.attributes)
collectionAttributes = _.filter attributes, (attr) ->
attr.queryable && attr instanceof AttributeCollection

View file

@ -279,16 +279,15 @@ class DraftStore
title = if options.newDraft then "New Message" else "Message"
save.then =>
app = require('electron').remote.getGlobal('application')
existing = app.windowManager.windowWithPropsMatching({draftClientId})
if existing
existing.restore() if existing.isMinimized()
existing.focus()
else
NylasEnv.newWindow
title: title
windowType: "composer"
windowProps: _.extend(options, {draftClientId, draftJSON})
# Since we pass a windowKey, if the popout composer draft already
# exists we'll simply show that one instead of spawning a whole new
# window.
NylasEnv.newWindow
title: title
hidden: true # We manually show in ComposerWithWindowProps::onDraftReady
windowKey: "composer-#{draftClientId}"
windowType: "composer-preload"
windowProps: _.extend(options, {draftClientId, draftJSON})
_onHandleMailtoLink: (event, urlString) =>
DraftFactory.createDraftForMailto(urlString).then (draft) =>

View file

@ -10,7 +10,7 @@ export default class SyncbackMetadataTask extends SyncbackModelTask {
}
getModelConstructor() {
return DatabaseObjectRegistry.classMap()[this.modelClassName];
return DatabaseObjectRegistry.get(this.modelClassName);
}
getRequestData = (model) => {

View file

@ -1,211 +1,187 @@
Task = null
Model = null
TaskRegistry = null
DatabaseObjectRegistry = null
TaskRegistry = require '../task-registry'
StoreRegistry = require '../store-registry'
DatabaseObjectRegistry = require '../database-object-registry'
class NylasExports
@registerSerializable = (exported) ->
if exported.prototype
Task ?= require '../flux/tasks/task'
Model ?= require '../flux/models/model'
if exported.prototype instanceof Model
DatabaseObjectRegistry ?= require '../database-object-registry'
DatabaseObjectRegistry.register(exported)
else if exported.prototype instanceof Task
TaskRegistry ?= require '../task-registry'
TaskRegistry.register(exported)
# Will lazy load when requested
@lazyLoad = (prop, path) ->
Object.defineProperty @, prop,
get: -> require("../#{path}")
enumerable: true
@get = (prop, get) ->
@lazyLoadCustomGetter = (prop, get) ->
Object.defineProperty @, prop, {get, enumerable: true}
# Will lazy load when requested
@load = (prop, path) ->
Object.defineProperty @, prop,
get: ->
exported = require "../#{path}"
NylasExports.registerSerializable(exported)
return exported
enumerable: true
@lazyLoadAndRegisterStore = (klassName, path) ->
constructorFactory = -> require("../flux/stores/#{path}")
StoreRegistry.register(klassName, constructorFactory)
@lazyLoad(klassName, "flux/stores/#{path}")
# Will require immediately
@require = (prop, path) ->
exported = require "../#{path}"
NylasExports.registerSerializable(exported)
@[prop] = exported
@lazyLoadAndRegisterModel = (klassName, path) ->
constructorFactory = -> require("../flux/models/#{path}")
DatabaseObjectRegistry.register(klassName, constructorFactory)
@lazyLoad(klassName, "flux/models/#{path}")
@requireDeprecated = (prop, path, {instead} = {}) ->
@lazyLoadAndRegisterTask = (klassName, path) ->
constructorFactory = -> require("../flux/tasks/#{path}")
TaskRegistry.register(klassName, constructorFactory)
@lazyLoad(klassName, "flux/tasks/#{path}")
@lazyLoadDeprecated = (prop, path, {instead} = {}) ->
{deprecate} = require '../deprecate-utils'
Object.defineProperty @, prop,
get: deprecate prop, instead, @, ->
exported = require "../#{path}"
NylasExports.registerSerializable(exported)
return exported
get: deprecate prop, instead, @, -> require("../#{path}")
enumerable: true
# Make sure our custom observable helpers are defined immediately
# (fromStore, fromQuery, etc...)
require 'nylas-observables'
# Actions
@load "Actions", 'flux/actions'
@lazyLoad "Actions", 'flux/actions'
# API Endpoints
@load "NylasAPI", 'flux/nylas-api'
@load "NylasSyncStatusStore", 'flux/stores/nylas-sync-status-store'
@load "EdgehillAPI", 'flux/edgehill-api'
@lazyLoad "NylasAPI", 'flux/nylas-api'
@lazyLoad "EdgehillAPI", 'flux/edgehill-api'
@lazyLoad "NylasSyncStatusStore", 'flux/stores/nylas-sync-status-store'
# The Database
@load "Matcher", 'flux/attributes/matcher'
@load "DatabaseStore", 'flux/stores/database-store'
@load "DatabaseTransaction", 'flux/stores/database-transaction'
@load "QueryResultSet", 'flux/models/query-result-set'
@load "MutableQueryResultSet", 'flux/models/mutable-query-result-set'
@load "ObservableListDataSource", 'flux/stores/observable-list-data-source'
@load "CalendarDataSource", 'components/nylas-calendar/calendar-data-source'
@load "QuerySubscription", 'flux/models/query-subscription'
@load "MutableQuerySubscription", 'flux/models/mutable-query-subscription'
@load "QuerySubscriptionPool", 'flux/models/query-subscription-pool'
@lazyLoad "Matcher", 'flux/attributes/matcher'
@lazyLoad "DatabaseStore", 'flux/stores/database-store'
@lazyLoad "QueryResultSet", 'flux/models/query-result-set'
@lazyLoad "QuerySubscription", 'flux/models/query-subscription'
@lazyLoad "CalendarDataSource", 'components/nylas-calendar/calendar-data-source'
@lazyLoad "DatabaseTransaction", 'flux/stores/database-transaction'
@lazyLoad "MutableQueryResultSet", 'flux/models/mutable-query-result-set'
@lazyLoad "QuerySubscriptionPool", 'flux/models/query-subscription-pool'
@lazyLoad "ObservableListDataSource", 'flux/stores/observable-list-data-source'
@lazyLoad "MutableQuerySubscription", 'flux/models/mutable-query-subscription'
# Database Objects
# These need to be required immeidatley to populated the
# DatabaseObjectRegistry so we know what Database Tables to construct
@require "File", 'flux/models/file'
@require "Event", 'flux/models/event'
@require "Label", 'flux/models/label'
@require "Folder", 'flux/models/folder'
@require "Thread", 'flux/models/thread'
@require "Account", 'flux/models/account'
@require "Message", 'flux/models/message'
@require "Contact", 'flux/models/contact'
@require "Category", 'flux/models/category'
@require "Calendar", 'flux/models/calendar'
@require "JSONBlob", 'flux/models/json-blob'
@require "DatabaseObjectRegistry", "database-object-registry"
@require "MailboxPerspective", 'mailbox-perspective'
# Exported so 3rd party packages can subclass Model
@load "Model", 'flux/models/model'
@load "Attributes", 'flux/attributes'
# The Task Queue
@require "Task", 'flux/tasks/task'
@require "TaskRegistry", "task-registry"
@require "TaskQueue", 'flux/stores/task-queue'
@require "TaskFactory", 'flux/tasks/task-factory'
@load "TaskQueueStatusStore", 'flux/stores/task-queue-status-store'
@require "UndoRedoStore", 'flux/stores/undo-redo-store'
@DatabaseObjectRegistry = DatabaseObjectRegistry
@lazyLoad "Model", 'flux/models/model'
@lazyLoad "Attributes", 'flux/attributes'
@lazyLoadAndRegisterModel "File", 'file'
@lazyLoadAndRegisterModel "Event", 'event'
@lazyLoadAndRegisterModel "Label", 'label'
@lazyLoadAndRegisterModel "Folder", 'folder'
@lazyLoadAndRegisterModel "Thread", 'thread'
@lazyLoadAndRegisterModel "Account", 'account'
@lazyLoadAndRegisterModel "Message", 'message'
@lazyLoadAndRegisterModel "Contact", 'contact'
@lazyLoadAndRegisterModel "Category", 'category'
@lazyLoadAndRegisterModel "Calendar", 'calendar'
@lazyLoadAndRegisterModel "JSONBlob", 'json-blob'
# Tasks
# These need to be required immediately to populate the TaskRegistry so
# we know how to deserialized saved or IPC-sent tasks.
@require "EventRSVPTask", 'flux/tasks/event-rsvp-task'
@require "SendDraftTask", 'flux/tasks/send-draft-task'
@require "DestroyDraftTask", 'flux/tasks/destroy-draft-task'
@require "ChangeMailTask", 'flux/tasks/change-mail-task'
@require "ChangeLabelsTask", 'flux/tasks/change-labels-task'
@require "ChangeFolderTask", 'flux/tasks/change-folder-task'
@require "SyncbackCategoryTask", 'flux/tasks/syncback-category-task'
@require "DestroyCategoryTask", 'flux/tasks/destroy-category-task'
@require "ChangeUnreadTask", 'flux/tasks/change-unread-task'
@require "SyncbackDraftFilesTask", 'flux/tasks/syncback-draft-files-task'
@require "SyncbackDraftTask", 'flux/tasks/syncback-draft-task'
@require "ChangeStarredTask", 'flux/tasks/change-starred-task'
@require "DestroyModelTask", 'flux/tasks/destroy-model-task'
@require "SyncbackModelTask", 'flux/tasks/syncback-model-task'
@require "SyncbackMetadataTask", 'flux/tasks/syncback-metadata-task'
@require "ReprocessMailRulesTask", 'flux/tasks/reprocess-mail-rules-task'
@TaskRegistry = TaskRegistry
@lazyLoad "Task", 'flux/tasks/task'
@lazyLoad "TaskFactory", 'flux/tasks/task-factory'
@lazyLoadAndRegisterTask "EventRSVPTask", 'event-rsvp-task'
@lazyLoadAndRegisterTask "SendDraftTask", 'send-draft-task'
@lazyLoadAndRegisterTask "ChangeMailTask", 'change-mail-task'
@lazyLoadAndRegisterTask "DestroyDraftTask", 'destroy-draft-task'
@lazyLoadAndRegisterTask "ChangeLabelsTask", 'change-labels-task'
@lazyLoadAndRegisterTask "ChangeFolderTask", 'change-folder-task'
@lazyLoadAndRegisterTask "ChangeUnreadTask", 'change-unread-task'
@lazyLoadAndRegisterTask "DestroyModelTask", 'destroy-model-task'
@lazyLoadAndRegisterTask "SyncbackDraftTask", 'syncback-draft-task'
@lazyLoadAndRegisterTask "ChangeStarredTask", 'change-starred-task'
@lazyLoadAndRegisterTask "SyncbackModelTask", 'syncback-model-task'
@lazyLoadAndRegisterTask "DestroyCategoryTask", 'destroy-category-task'
@lazyLoadAndRegisterTask "SyncbackCategoryTask", 'syncback-category-task'
@lazyLoadAndRegisterTask "SyncbackMetadataTask", 'syncback-metadata-task'
@lazyLoadAndRegisterTask "SyncbackDraftFilesTask", 'syncback-draft-files-task'
@lazyLoadAndRegisterTask "ReprocessMailRulesTask", 'reprocess-mail-rules-task'
# Stores
# These need to be required immediately since some Stores are
# listen-only and not explicitly required from anywhere. Stores
# currently set themselves up on require.
@require "DraftStore", 'flux/stores/draft-store'
@require "OutboxStore", 'flux/stores/outbox-store'
@require "AccountStore", 'flux/stores/account-store'
@require "MessageStore", 'flux/stores/message-store'
@require "MetadataStore", 'flux/stores/metadata-store'
@require "ContactStore", 'flux/stores/contact-store'
@require "CategoryStore", 'flux/stores/category-store'
@require "WorkspaceStore", 'flux/stores/workspace-store'
@require "FileUploadStore", 'flux/stores/file-upload-store'
@require "MailRulesStore", 'flux/stores/mail-rules-store'
@require "ThreadCountsStore", 'flux/stores/thread-counts-store'
@require "BadgeStore", 'flux/stores/badge-store'
@require "FileDownloadStore", 'flux/stores/file-download-store'
@require "FocusedContentStore", 'flux/stores/focused-content-store'
@require "FocusedPerspectiveStore", 'flux/stores/focused-perspective-store'
@require "FocusedContactsStore", 'flux/stores/focused-contacts-store'
@require "PreferencesUIStore", 'flux/stores/preferences-ui-store'
@require "PopoverStore", 'flux/stores/popover-store'
@require "ModalStore", 'flux/stores/modal-store'
@require "SearchableComponentStore", 'flux/stores/searchable-component-store'
@require "MessageBodyProcessor", 'flux/stores/message-body-processor'
@require "MailRulesTemplates", 'mail-rules-templates'
@require "MailRulesProcessor", 'mail-rules-processor'
# Deprecated
@requireDeprecated "DraftStoreExtension", 'flux/stores/draft-store-extension',
instead: 'ComposerExtension'
@requireDeprecated "MessageStoreExtension", 'flux/stores/message-store-extension',
instead: 'MessageViewExtension'
@lazyLoadAndRegisterStore "TaskQueue", 'task-queue'
@lazyLoadAndRegisterStore "BadgeStore", 'badge-store'
@lazyLoadAndRegisterStore "DraftStore", 'draft-store'
@lazyLoadAndRegisterStore "ModalStore", 'modal-store'
@lazyLoadAndRegisterStore "OutboxStore", 'outbox-store'
@lazyLoadAndRegisterStore "PopoverStore", 'popover-store'
@lazyLoadAndRegisterStore "AccountStore", 'account-store'
@lazyLoadAndRegisterStore "MessageStore", 'message-store'
@lazyLoadAndRegisterStore "ContactStore", 'contact-store'
@lazyLoadAndRegisterStore "MetadataStore", 'metadata-store'
@lazyLoadAndRegisterStore "CategoryStore", 'category-store'
@lazyLoadAndRegisterStore "UndoRedoStore", 'undo-redo-store'
@lazyLoadAndRegisterStore "WorkspaceStore", 'workspace-store'
@lazyLoadAndRegisterStore "MailRulesStore", 'mail-rules-store'
@lazyLoadAndRegisterStore "FileUploadStore", 'file-upload-store'
@lazyLoadAndRegisterStore "ThreadCountsStore", 'thread-counts-store'
@lazyLoadAndRegisterStore "FileDownloadStore", 'file-download-store'
@lazyLoadAndRegisterStore "PreferencesUIStore", 'preferences-ui-store'
@lazyLoadAndRegisterStore "FocusedContentStore", 'focused-content-store'
@lazyLoadAndRegisterStore "MessageBodyProcessor", 'message-body-processor'
@lazyLoadAndRegisterStore "FocusedContactsStore", 'focused-contacts-store'
@lazyLoadAndRegisterStore "TaskQueueStatusStore", 'task-queue-status-store'
@lazyLoadAndRegisterStore "FocusedPerspectiveStore", 'focused-perspective-store'
@lazyLoadAndRegisterStore "SearchableComponentStore", 'searchable-component-store'
# Extensions
@require "ExtensionRegistry", 'extension-registry'
@require "ContenteditableExtension", 'extensions/contenteditable-extension'
@require "ComposerExtension", 'extensions/composer-extension'
@require "MessageViewExtension", 'extensions/message-view-extension'
@lazyLoad "ExtensionRegistry", 'extension-registry'
@lazyLoad "ComposerExtension", 'extensions/composer-extension'
@lazyLoad "MessageViewExtension", 'extensions/message-view-extension'
@lazyLoad "ContenteditableExtension", 'extensions/contenteditable-extension'
# Libraries
@get "React", -> require 'react' # Our version of React for 3rd party use
@get "ReactDOM", -> require 'react-dom'
@get "ReactTestUtils", -> require 'react-addons-test-utils'
@get "Reflux", -> require 'reflux'
@get "Rx", -> require 'rx-lite'
@get "Keytar", -> require 'keytar' # atom-keytar access through native module
# 3rd party libraries
@lazyLoadCustomGetter "Rx", -> require 'rx-lite'
@lazyLoadCustomGetter "React", -> require 'react'
@lazyLoadCustomGetter "Reflux", -> require 'reflux'
@lazyLoadCustomGetter "ReactDOM", -> require 'react-dom'
@lazyLoadCustomGetter "ReactTestUtils", -> require 'react-addons-test-utils'
@lazyLoadCustomGetter "Keytar", -> require 'keytar' # atom-keytar access through native module
# React Components
@load "ReactRemote", 'react-remote/react-remote-parent'
@load "ComponentRegistry", 'component-registry'
@load "PriorityUICoordinator", 'priority-ui-coordinator'
@lazyLoad "ComponentRegistry", 'component-registry'
@lazyLoad "PriorityUICoordinator", 'priority-ui-coordinator'
# Utils
@load "DeprecateUtils", 'deprecate-utils'
@load "Utils", 'flux/models/utils'
@load "DOMUtils", 'dom-utils'
@load "VirtualDOMUtils", 'virtual-dom-utils'
@load "CanvasUtils", 'canvas-utils'
@load "RegExpUtils", 'regexp-utils'
@load "DateUtils", 'date-utils'
@load "MenuHelpers", 'menu-helpers'
@load "MessageUtils", 'flux/models/message-utils'
@load "NylasSpellchecker", 'nylas-spellchecker'
@lazyLoad "Utils", 'flux/models/utils'
@lazyLoad "DOMUtils", 'dom-utils'
@lazyLoad "DateUtils", 'date-utils'
@lazyLoad "CanvasUtils", 'canvas-utils'
@lazyLoad "RegExpUtils", 'regexp-utils'
@lazyLoad "MenuHelpers", 'menu-helpers'
@lazyLoad "MessageUtils", 'flux/models/message-utils'
@lazyLoad "DeprecateUtils", 'deprecate-utils'
@lazyLoad "VirtualDOMUtils", 'virtual-dom-utils'
@lazyLoad "NylasSpellchecker", 'nylas-spellchecker'
# Services
@load "UndoManager", 'undo-manager'
@load "SoundRegistry", 'sound-registry'
@load "NativeNotifications", 'native-notifications'
@load "SearchableComponentMaker", 'searchable-components/searchable-component-maker'
@load "QuotedHTMLTransformer", 'services/quoted-html-transformer'
@load "QuotedPlainTextTransformer", 'services/quoted-plain-text-transformer'
@load "SanitizeTransformer", 'services/sanitize-transformer'
@load "InlineStyleTransformer", 'services/inline-style-transformer'
@requireDeprecated "QuotedHTMLParser", 'services/quoted-html-transformer',
instead: 'QuotedHTMLTransformer'
@lazyLoad "UndoManager", 'undo-manager'
@lazyLoad "SoundRegistry", 'sound-registry'
@lazyLoad "MailRulesTemplates", 'mail-rules-templates'
@lazyLoad "MailRulesProcessor", 'mail-rules-processor'
@lazyLoad "MailboxPerspective", 'mailbox-perspective'
@lazyLoad "NativeNotifications", 'native-notifications'
@lazyLoad "SanitizeTransformer", 'services/sanitize-transformer'
@lazyLoad "QuotedHTMLTransformer", 'services/quoted-html-transformer'
@lazyLoad "InlineStyleTransformer", 'services/inline-style-transformer'
@lazyLoad "SearchableComponentMaker", 'searchable-components/searchable-component-maker'
@lazyLoad "QuotedPlainTextTransformer", 'services/quoted-plain-text-transformer'
# Errors
@get "APIError", -> require('../flux/errors').APIError
@get "TimeoutError", -> require('../flux/errors').TimeoutError
@lazyLoadCustomGetter "APIError", -> require('../flux/errors').APIError
@lazyLoadCustomGetter "TimeoutError", -> require('../flux/errors').TimeoutError
# Process Internals
@load "LaunchServices", 'launch-services'
@load "SystemStartService", 'system-start-service'
@load "BufferedProcess", 'buffered-process'
@get "APMWrapper", -> require('../apm-wrapper')
@lazyLoad "LaunchServices", 'launch-services'
@lazyLoad "BufferedProcess", 'buffered-process'
@lazyLoad "SystemStartService", 'system-start-service'
@lazyLoadCustomGetter "APMWrapper", -> require('../apm-wrapper')
# Testing
@get "NylasTestUtils", -> require '../../spec/nylas-test-utils'
@lazyLoadCustomGetter "NylasTestUtils", -> require '../../spec/nylas-test-utils'
# Deprecated
@lazyLoadDeprecated "QuotedHTMLParser", 'services/quoted-html-transformer',
instead: 'QuotedHTMLTransformer'
@lazyLoadDeprecated "DraftStoreExtension", 'flux/stores/draft-store-extension',
instead: 'ComposerExtension'
@lazyLoadDeprecated "MessageStoreExtension", 'flux/stores/message-store-extension',
instead: 'MessageViewExtension'
window.$n = NylasExports
module.exports = NylasExports

View file

@ -1,6 +1,9 @@
_ = require 'underscore'
path = require 'path'
LessCache = require 'less-cache'
fileCacheImportPaths = null
# {LessCache} wrapper used by {ThemeManager} to read stylesheets.
module.exports =
class LessCompileCache
@ -21,8 +24,18 @@ class LessCompileCache
resourcePath: resourcePath
fallbackDir: path.join(resourcePath, 'less-compile-cache')
# Setting the import paths is a VERY expensive operation (200ms +)
# because it walks the entire file tree and does a file state for each
# and every importPath. If we already have the imports, then load it
# from our backend FileListCache.
setImportPaths: (importPaths=[]) ->
@cache.setImportPaths(importPaths.concat(@lessSearchPaths))
fileCache = NylasEnv.fileListCache()
fileCacheImportPaths ?= fileCache.lessCacheImportPaths ? []
fullImportPaths = importPaths.concat(@lessSearchPaths)
pathDiff = _.difference(fullImportPaths, fileCacheImportPaths)
if pathDiff.length isnt 0
@cache.setImportPaths(fullImportPaths)
fileCache.lessCacheImportPaths = fullImportPaths
read: (stylesheetPath) ->
@cache.readFileSync(stylesheetPath)

View file

@ -9,13 +9,13 @@ AccountStore = require './flux/stores/account-store'
DatabaseStore = require './flux/stores/database-store'
TaskQueueStatusStore = require './flux/stores/task-queue-status-store'
MailRulesStore = require './flux/stores/mail-rules-store'
{ConditionMode, ConditionTemplates} = require './mail-rules-templates'
ChangeUnreadTask = require './flux/tasks/change-unread-task'
ChangeFolderTask = require './flux/tasks/change-folder-task'
ChangeStarredTask = require './flux/tasks/change-starred-task'
ChangeLabelsTask = require './flux/tasks/change-labels-task'
MailRulesStore = null
###
Note: At first glance, it seems like these task factory methods should use the
@ -78,6 +78,7 @@ class MailRulesProcessor
constructor: ->
processMessages: (messages) =>
MailRulesStore ?= require './flux/stores/mail-rules-store'
return Promise.resolve() unless messages.length > 0
enabledRules = MailRulesStore.rules().filter (r) -> not r.disabled

View file

@ -14,6 +14,7 @@ fs = require 'fs-plus'
WindowEventHandler = require './window-event-handler'
StylesElement = require './styles-element'
StoreRegistry = require './store-registry'
Utils = require './flux/models/utils'
{APIError} = require './flux/errors'
@ -211,6 +212,15 @@ class NylasEnvConstructor extends Model
unless @inSpecMode()
@actionBridge = new ActionBridge(ipcRenderer)
# Nylas exports is designed to provide a lazy-loaded set of globally
# accessible objects to all packages. Upon require, nylas-exports will
# fill the TaskRegistry, StoreRegistry, and DatabaseObjectRegistries
# with various constructors.
#
# We initialize all of the stores loaded into the StoreRegistry once
# the window starts loading.
require('nylas-exports')
# This ties window.onerror and Promise.onPossiblyUnhandledRejection to
# the publically callable `reportError` method. This will take care of
# reporting errors if necessary and hooking into error handling
@ -535,19 +545,6 @@ class NylasEnvConstructor extends Model
reload: ->
ipcRenderer.send('call-webcontents-method', 'reload')
# Updates the window load settings - called when the app is ready to display
# a hot-loaded window. Causes listeners registered with `onWindowPropsReceived`
# to receive new window props.
loadSettingsChanged: (event, loadSettings) =>
@loadSettings = loadSettings
@constructor.loadSettings = loadSettings
{width, height, windowProps} = loadSettings
@emitter.emit('window-props-received', windowProps ? {})
if width and height
@setWindowDimensions({width, height})
# Public: The windowProps passed when creating the window via `newWindow`.
#
getWindowProps: ->
@ -663,9 +660,23 @@ class NylasEnvConstructor extends Model
@savedState.columnWidths ?= {}
@savedState.columnWidths[id]
startWindow: ->
{packageLoadingDeferred, windowType} = @getLoadSettings()
@extendRxObservables()
StoreRegistry.activateAllStores()
@loadConfig()
@keymaps.loadBundledKeymaps()
@themes.loadBaseStylesheets()
@packages.loadPackages(windowType) unless packageLoadingDeferred
@deserializePackageStates() unless packageLoadingDeferred
@initializeReactRoot()
@packages.activate() unless packageLoadingDeferred
@keymaps.loadUserKeymap()
@menu.update()
# Call this method when establishing a real application window.
startRootWindow: ->
{safeMode, windowType, initializeInBackground} = @getLoadSettings()
{safeMode, initializeInBackground} = @getLoadSettings()
# Temporary. It takes five paint cycles for all the CSS in index.html to
# be applied. Remove if https://github.com/atom/brightray/issues/196 fixed!
@ -675,73 +686,66 @@ class NylasEnvConstructor extends Model
window.requestAnimationFrame =>
window.requestAnimationFrame =>
@displayWindow() unless initializeInBackground
@loadConfig()
@keymaps.loadBundledKeymaps()
@themes.loadBaseStylesheets()
@packages.loadPackages(windowType)
@deserializePackageStates()
@deserializeSheetContainer()
@packages.activate()
@keymaps.loadUserKeymap()
@startWindow()
@requireUserInitScript() unless safeMode
@menu.update()
@showRootWindow()
@showMainWindow()
ipcRenderer.send('window-command', 'window:loaded')
showRootWindow: ->
# Initializes a secondary window.
# NOTE: If the `packageLoadingDeferred` option is set (which is true for
# hot windows), the packages won't be loaded until `populateHotWindow`
# gets fired.
startSecondaryWindow: ->
document.getElementById("application-loading-cover")?.remove()
@startWindow()
ipcRenderer.on("load-settings-changed", @populateHotWindow)
ipcRenderer.send('window-command', 'window:loaded')
showMainWindow: ->
document.getElementById("application-loading-cover").remove()
document.body.classList.add("window-loaded")
@restoreWindowDimensions()
@getCurrentWindow().setMinimumSize(875, 250)
# Call this method when establishing a secondary application window
# displaying a specific set of packages.
# Updates the window load settings - called when the app is ready to
# display a hot-loaded window. Causes listeners registered with
# `onWindowPropsReceived` to receive new window props.
#
startSecondaryWindow: ->
{width,
height,
windowType,
windowPackages} = @getLoadSettings()
# This also means that the windowType has changed and a different set of
# plugins needs to be loaded.
populateHotWindow: (event, loadSettings) =>
@loadSettings = loadSettings
@constructor.loadSettings = loadSettings
cover = document.getElementById("application-loading-cover")
cover.remove() if cover
@loadConfig()
@keymaps.loadBundledKeymaps()
@themes.loadBaseStylesheets()
{width, height, windowProps, windowType, hidden} = loadSettings
@packages.loadPackages(windowType)
@packages.loadPackage(pack) for pack in (windowPackages ? [])
@deserializeSheetContainer()
@deserializePackageStates()
@packages.activate()
@keymaps.loadUserKeymap()
ipcRenderer.on("load-settings-changed", @loadSettingsChanged)
@emitter.emit('window-props-received', windowProps ? {})
@setWindowDimensions({width, height}) if width and height
if width and height
@setWindowDimensions({width, height})
@menu.update()
@displayWindow() unless hidden
ipcRenderer.send('window-command', 'window:loaded')
# We extend nylas observables with our own methods. This happens on
# require of nylas-observables
extendRxObservables: ->
require('nylas-observables')
# Requests that the backend browser bootup a new window with the given
# options.
# See the valid option types in Application::newWindow in
# src/browser/application.coffee
# Launches a new window via the browser/WindowLauncher.
#
# If you pass a `windowKey` in the options, and that windowKey already
# exists, it'll show that window instead of spawing a new one. This is
# useful for places like popout composer windows where you want to
# simply display the draft instead of spawning a whole new window for
# the same draft.
#
# `options` are documented in browser/WindowLauncher
newWindow: (options={}) -> ipcRenderer.send('new-window', options)
# Registers a hot window for certain packages
# See the valid option types in Application::registerHotWindow in
# src/browser/application.coffee
registerHotWindow: (options={}) -> ipcRenderer.send('register-hot-window', options)
# Unregisters a hot window with the given windowType
unregisterHotWindow: (windowType) -> ipcRenderer.send('unregister-hot-window', windowType)
saveStateAndUnloadWindow: ->
@packages.deactivatePackages()
@savedState.packageStates = @packages.packageStates
@ -753,6 +757,7 @@ class NylasEnvConstructor extends Model
###
displayWindow: ({maximize} = {}) ->
return if @inSpecMode()
@show()
@focus()
@maximize() if maximize
@ -821,7 +826,7 @@ class NylasEnvConstructor extends Model
Section: Private
###
deserializeSheetContainer: ->
initializeReactRoot: ->
startTime = Date.now()
# Put state back into sheet-container? Restore app state here
@item = document.createElement("nylas-workspace")
@ -886,6 +891,10 @@ class NylasEnvConstructor extends Model
detail: message
}
# Delegate to the browser's process fileListCache
fileListCache: ->
return remote.getGlobal('application').fileListCache
saveSync: ->
stateString = JSON.stringify(@savedState)
if statePath = @constructor.getStatePath()
@ -944,6 +953,8 @@ class NylasEnvConstructor extends Model
# work and then call finishUnload. We do not support cancelling quit!
# https://phab.nylas.com/D1932#inline-11722
#
# Also see logic in browser/NylasWindow::handleEvents where we listen
# to the browserWindow.on 'close' event to catch "unclosable" windows.
onBeforeUnload: (callback) ->
@windowEventHandler.addUnloadCallback(callback)

View file

@ -15,6 +15,8 @@ ThemePackage = require './theme-package'
DatabaseStore = require './flux/stores/database-store'
APMWrapper = require './apm-wrapper'
basePackagePaths = null
# Extended: Package manager for coordinating the lifecycle of N1 packages.
#
# An instance of this class is always available as the `NylasEnv.packages` global.
@ -296,13 +298,20 @@ class PackageManager
loadPackagesWhenNoTypesSpecified = windowType is 'default'
for packageDirPath in @packageDirPaths
for packagePath in fs.listSync(packageDirPath)
# Ignore files in package directory
continue unless fs.isDirectorySync(packagePath)
# Ignore .git in package directory
continue if path.basename(packagePath)[0] is '.'
packagePaths.push(packagePath)
basePackagePaths ?= NylasEnv.fileListCache().basePackagePaths ? []
if basePackagePaths.length is 0
for packageDirPath in @packageDirPaths
for packagePath in fs.listSync(packageDirPath)
# Ignore files in package directory
continue unless fs.isDirectorySync(packagePath)
# Ignore .git in package directory
continue if path.basename(packagePath)[0] is '.'
packagePaths.push(packagePath)
basePackagePaths = packagePaths
cache = NylasEnv.fileListCache()
cache.basePackagePaths = basePackagePaths
else
packagePaths = basePackagePaths
if windowType
packagePaths = _.filter packagePaths, (packagePath) ->

View file

@ -157,12 +157,15 @@ class Package
registerModelConstructors: (constructors=[]) ->
if constructors.length > 0
@declaresNewDatabaseObjects = true
for constructor in constructors
DatabaseObjectRegistry.register(constructor)
_.each constructors, (constructor) ->
constructorFactory = -> constructor
DatabaseObjectRegistry.register(constructor.name, constructorFactory)
registerTaskConstructors: (constructors=[]) ->
for constructor in constructors
TaskRegistry.register(constructor)
_.each constructors, (constructor) ->
constructorFactory = -> constructor
TaskRegistry.register(constructor.name, constructorFactory)
reset: ->
@stylesheets = []

View file

@ -1,128 +0,0 @@
var _ = require('underscore')
var container = document.getElementById("container");
var ipc = require('electron').ipcRenderer;
var lastSelectionData = {}
document.body.classList.add("platform-"+process.platform);
document.body.classList.add("window-type-react-remote");
exp = require('./selection-listeners.js');
restoreSelection = exp.restoreSelection;
getSelectionData = exp.getSelectionData;
var receiveEvent = function (json) {
var remote = require('electron').remote;
if (json.selectionData) {
document.removeEventListener("selectionchange", selectionChange);
restoreSelection(json.selectionData)
document.addEventListener("selectionchange", selectionChange);
}
if (json.html) {
var browserWindow = remote.getCurrentWindow();
browserWindow.on('focus', function() {
document.body.classList.remove('is-blurred')
});
browserWindow.on('blur', function() {
document.body.classList.add('is-blurred')
});
container.innerHTML = json.html;
var style = document.createElement('style');
style.onload = function() {
for (var ii = 0; ii < json.waiting.length; ii ++) {
receiveEvent(json.waiting[ii]);
}
window.requestAnimationFrame(function() {
browserWindow.show();
});
};
style.textContent = json.style;
document.body.appendChild(style);
}
if (json.sel) {
var React = require('react');
var ReactMount = require('react/lib/ReactMount');
ReactMount.getNode = function(id) {
return document.querySelector("[data-reactid='"+id+"']");
};
var sources = {
CSSPropertyOperations: require('react/lib/CSSPropertyOperations'),
DOMPropertyOperations: require('react/lib/DOMPropertyOperations'),
DOMChildrenOperations: require('react/lib/DOMChildrenOperations'),
ReactDOMIDOperations: require('react/lib/ReactDOMIDOperations'),
Custom: {
setSelectCurrentValue: function(reactid, value) {
var children = ReactMount.getNode(reactid).childNodes;
for (var ii = 0; ii < children.length; ii ++) {
children[ii].selected = (children[ii].value == value);
}
}
}
};
if (json.firstArgType == 'node') {
json.arguments[0] = ReactMount.getNode(json.arguments[0]);
} else if (json.firstArgType == 'array') {
for (var ii = 0; ii < json.arguments[0].length; ii ++) {
json.arguments[0][ii].parentNode = ReactMount.getNode(json.arguments[0][ii].parentNode)
}
}
sources[json.parent][json.sel].apply(sources[json.parent], json.arguments);
}
};
ipc.on("to-react-remote-window", receiveEvent);
var events = ['keypress', 'keydown', 'keyup', 'change', 'submit', 'click', 'focus', 'blur', 'input', 'select'];
events.forEach(function(type) {
container.addEventListener(type, function(event) {
var representation = {
eventType: event.type,
eventClass: event.constructor.name,
pageX: event.pageX,
pageY: event.pageY,
bubbles: event.bubbles,
cancelable: event.cancelable,
clientX: event.clientX,
clientY: event.clientY,
charCode: event.charCode,
keyCode: event.keyCode,
detail: event.detail,
eventPhase: event.eventPhase
}
if (event.target) {
representation.targetReactId = event.target.dataset.reactid;
}
if (event.target.contentEditable=="true") {
representation.targetValue = event.target.innerHTML;
}
else if (event.target.value !== undefined) {
representation.targetValue = event.target.value;
}
if (event.target.checked !== undefined) {
representation.targetChecked = event.target.checked;
}
var remote = require('electron').remote;
ipc.send("from-react-remote-window", {windowId: remote.getCurrentWindow().id, event: representation});
if ((event.type != 'keydown') && (event.type != 'keypress') && (event.type != 'keyup')) {
event.preventDefault();
}
}, true);
});
selectionChange = function() {
selectionData = getSelectionData()
if (_.isEqual(selectionData, lastSelectionData)) { return; }
lastSelectionData = _.clone(selectionData)
var remote = require('electron').remote;
remote.getCurrentWindow().id
ipc.send("from-react-remote-window-selection", selectionData);
}
// document.addEventListener("selectionchange", selectionChange);

View file

@ -1,405 +0,0 @@
/*
This code stopped working when we moved from React `0.13.2` to `0.14.7`.
It would most likely still work, but some of the internal modules have moved.
var ipcRenderer = require("electron").ipcRenderer;
var React = require('react');
var ReactDOM = require('react-dom');
var _ = require('underscore');
var LinkedValueUtils = require('react/lib/LinkedValueUtils');
var ReactDOMComponent = require('react/lib/ReactDOMComponent');
var methods = Object.keys(ReactDOMComponent.BackendIDOperations);
var invocationTargets = [];
var lastSelectionData = {}
var sources = {
CSSPropertyOperations: require('react/lib/CSSPropertyOperations'),
DOMPropertyOperations: require('react/lib/DOMPropertyOperations'),
DOMChildrenOperations: require('react/lib/DOMChildrenOperations'),
ReactDOMIDOperations: require('react/lib/ReactDOMIDOperations'),
ReactDOMSelect: require('react/lib/ReactDOMSelect')
}
var Custom = {
sendSelectCurrentValue: function() {
var reactid = this.getDOMNode().dataset.reactid;
var target = invocationTargetForReactId(reactid);
if (target) {
var value = LinkedValueUtils.getValue(this);
target.send({
parent: 'Custom',
sel: 'setSelectCurrentValue',
arguments: [reactid, LinkedValueUtils.getValue(this)]
});
}
}
};
var invocationTargetForReactId = function(id) {
for (var ii = 0; ii < invocationTargets.length; ii++) {
var target = invocationTargets[ii];
if (id.substr(0, target.reactid.length) == target.reactid) {
return target;
}
if (target.reactid == 'not-yet-rendered') {
var node = document.querySelector("[data-reactid='"+id+"']");
while (node = node.parentNode) {
if (node == target.container) {
return target;
}
}
}
}
return null;
};
var observeMethod = function(parent, sel, callback) {
var owner = sources[parent];
if (!owner[sel]) {
owner = owner.prototype;
}
var oldImpl = owner[sel];
owner[sel] = function() {
oldImpl.apply(this, arguments);
if (invocationTargets.length == 0)
return;
callback.apply(this, arguments);
}
};
var observeMethodAndBroadcast = function(parent, sel) {
observeMethod(parent, sel, function() {
var id = null;
var target = null;
var firstArgType = null;
var args = [];
for (var ii = 0; ii < arguments.length; ii ++) {
args.push(arguments[ii]);
}
if (arguments[0] instanceof Node) {
args[0] = args[0].dataset.reactid;
target = invocationTargetForReactId(args[0]);
firstArgType = "node";
} else if (typeof(args[0]) === 'string') {
target = invocationTargetForReactId(args[0]);
firstArgType = "id";
} else if (args[0] instanceof Array) {
for (var ii = 0; ii < args[0].length; ii ++) {
args[0][ii].parentNode = args[0][ii].parentNode.dataset.reactid;
}
target = invocationTargetForReactId(args[0][0].parentNode);
firstArgType = "array";
}
if (target) {
target.send({
parent: parent,
sel: sel,
arguments: args,
firstArgType: firstArgType
});
target.sendSizingInformation();
}
});
};
setTimeout(function(){
observeMethodAndBroadcast('CSSPropertyOperations', 'setValueForStyles');
observeMethodAndBroadcast('DOMChildrenOperations', 'updateTextContent');
observeMethodAndBroadcast('DOMChildrenOperations', 'dangerouslyReplaceNodeWithMarkup');
observeMethodAndBroadcast('DOMPropertyOperations', 'deleteValueForProperty');
observeMethodAndBroadcast('DOMPropertyOperations', 'setValueForProperty');
observeMethodAndBroadcast('ReactDOMIDOperations', 'updateInnerHTMLByID');
observeMethodAndBroadcast('DOMChildrenOperations', 'processUpdates');
observeMethod('ReactDOMSelect', 'componentDidUpdate', Custom.sendSelectCurrentValue);
observeMethod('ReactDOMSelect', 'componentDidMount', Custom.sendSelectCurrentValue);
}, 10);
ipcRenderer.on('from-react-remote-window', function(event, json) {
var container = null;
for (var ii = 0; ii < invocationTargets.length; ii ++) {
if (invocationTargets[ii].windowId == json.windowId) {
container = invocationTargets[ii].container;
}
}
if (!container) {
console.error("Received message from child window "+json.windowId+" which is not recognized.");
return;
}
if (json.event) {
var rep = json.event;
if (rep.targetReactId) {
rep.target = document.querySelector(["[data-reactid='"+rep.targetReactId+"']"]);
}
if (rep.target && rep.target.contentEditable=="true") {
rep.target.innerHTML = rep.targetValue;
}
else if (rep.target && (rep.targetValue !== undefined)) {
rep.target.value = rep.targetValue;
}
if (rep.target && (rep.targetChecked !== undefined)) {
rep.target.checked = rep.targetChecked;
}
var EventClass = {
"MouseEvent": MouseEvent,
"KeyboardEvent": KeyboardEvent,
"FocusEvent": FocusEvent
}[rep.eventClass] || Event;
var e = new EventClass(rep.eventType, rep);
if (rep.target) {
rep.target.dispatchEvent(e);
} else {
container.dispatchEvent(e);
}
}
});
exp = require('./selection-listeners.js');
restoreSelection = exp.restoreSelection;
getSelectionData = exp.getSelectionData;
selectionChange = function() {
selectionData = getSelectionData();
if (_.isEqual(selectionData, lastSelectionData)) { return; }
lastSelectionData = _.clone(selectionData)
for (var i = 0; i < invocationTargets.length; i++) {
var target = invocationTargets[i];
target.send({selectionData: selectionData})
}
}
// document.addEventListener("selectionchange", selectionChange);
ipcRenderer.on('from-react-remote-window-selection', function(event, selectionData){
document.removeEventListener("selectionchange", selectionChange)
restoreSelection(selectionData)
document.addEventListener("selectionchange", selectionChange);
});
var parentListenersAttached = false;
var reactRemoteContainer = document.createElement('div');
reactRemoteContainer.style.left = '-10000px';
reactRemoteContainer.style.top = '40px';
reactRemoteContainer.style.backgroundColor = 'white';
reactRemoteContainer.style.position = 'absolute';
reactRemoteContainer.style.zIndex = 10000;
reactRemoteContainer.style.border = '5px solid orange';
document.body.appendChild(reactRemoteContainer);
var reactRemoteContainerTitle = document.createElement('div');
reactRemoteContainerTitle.style.color = 'white';
reactRemoteContainerTitle.style.backgroundColor = 'orange';
reactRemoteContainerTitle.innerText = 'React Remote Container';
reactRemoteContainer.appendChild(reactRemoteContainerTitle);
var toggleContainerVisible = function() {
if (reactRemoteContainer.style.left === '-10000px') {
reactRemoteContainer.style.left = 0;
} else {
reactRemoteContainer.style.left = '-10000px';
}
};
var openWindowForComponent = function(Component, options) {
// If a tag is specified, see if we can find an existing window to bring to foreground
if (options.tag) {
for (var ii = 0; ii < invocationTargets.length; ii++) {
if (invocationTargets[ii].tag === options.tag) {
invocationTargets[ii].window.focus();
return;
}
}
}
var remote = require('electron').remote;
var url = require('url');
var BrowserWindow = remote.require('browser-window');
// Read rendered styles out of the page
var styles = document.querySelectorAll("style");
var thinStyles = "";
for (var ii = 0; ii < styles.length; ii++) {
var styleNode = styles[ii];
if (!styleNode.sourcePath) {
continue;
}
if ((styleNode.sourcePath.indexOf('index') > 0) || (options.stylesheetRegex && options.stylesheetRegex.test(styleNode.sourcePath))) {
thinStyles = thinStyles + styleNode.innerText;
}
}
// Create a browser window
var thinWindowUrl = url.format({
protocol: 'file',
pathname: NylasEnv.getLoadSettings().resourcePath+"/static/react-remote-child.html",
slashes: true
});
var thinWindow = new BrowserWindow({
title: options.title || "",
frame: process.platform !== 'darwin',
width: options.width || 800,
height: options.height || 600,
resizable: options.resizable,
show: false
});
thinWindow.loadURL(thinWindowUrl);
if (process.platform !== 'darwin') {
thinWindow.setMenu(null);
}
// Add a container to our local document to hold the root component of the window
var container = document.createElement('div');
container.id = 'react-remote-window-container-'+thinWindow.id;
if (options.width) {
container.style.width = options.width+'px';
} else {
container.style.height = 'auto';
}
if (options.height) {
container.style.height = options.height+'px';
} else {
container.style.height = 'auto';
}
reactRemoteContainer.appendChild(container);
var cleanup = function() {
if (container == null) {
return;
}
for (var ii = 0; ii < invocationTargets.length; ii++) {
if (invocationTargets[ii].container === container) {
invocationTargets[ii].windowReady = false
invocationTargets[ii].window = null
invocationTargets.splice(ii, 1);
break;
}
}
ReactDOM.render(React.createElement('div'), container, function() {
reactRemoteContainer.removeChild(container);
});
container = null;
thinWindow = null;
};
var sendWaiting = [];
var sendSizingInformation = function() {
if (!options.autosize) {
return;
}
if (!thinWindow) {
return;
}
// Weirdly, this returns an array of [width, height] and not a hash
var size = thinWindow.getContentSize();
var changed = false;
var containerSize = container.getBoundingClientRect();
var containerWidth = Math.ceil(containerSize.width);
var containerHeight = Math.ceil(containerSize.height);
if (containerHeight == 0) {
containerHeight = 100;
debugger;
}
if (containerWidth == 0) {
containerWidth = 400;
debugger;
}
if ((!options.height) && (size[1] != containerHeight)) {
size[1] = containerHeight;
changed = true;
}
if ((!options.width) && (size[0] != containerWidth)) {
size[0] = containerWidth;
changed = true;
}
if (changed) {
thinWindow.setContentSize(size[0], size[1]);
}
};
// Create a "Target" object that we'll use to store information about the
// remote window, it's reactId, etc.
var target = {
container: container,
containerReady: false,
window: thinWindow,
windowReady: false,
windowId: thinWindow.id,
tag: options.tag,
reactid: 'not-yet-rendered',
send: function(args) {
if (target.containerReady && target.windowReady) {
thinWindow.webContents.send('to-react-remote-window', args);
} else {
sendWaiting.push(args);
}
},
sendSizingInformation: _.debounce(function() {
if (target.containerReady && target.windowReady) {
sendSizingInformation();
}
}, 20),
sendHTMLIfReady: function() {
if (target.containerReady && target.windowReady) {
sendSizingInformation();
thinWindow.webContents.send('to-react-remote-window', {
html: container.innerHTML,
style: thinStyles,
waiting: sendWaiting
});
}
}
};
invocationTargets.push(target);
// Finally, render the react component into our local container and open
// the browser window. When both of these things finish, we send the html
// css, and any observed method invocations that occurred during the first
// React cycle (componentDidMount).
ReactDOM.render(React.createElement(Component, options.props), container, function() {
target.reactid = container.firstChild.dataset.reactid,
target.containerReady = true;
target.sendHTMLIfReady();
});
thinWindow.on('closed', cleanup);
thinWindow.webContents.on('crashed', cleanup);
thinWindow.webContents.on('did-finish-load', function () {
target.windowReady = true;
target.sendHTMLIfReady();
});
// The first time a remote window is opened, add event listeners to our
// own window so that we close dependent windows when we're closed.
if (parentListenersAttached == false) {
remote.getCurrentWindow().on('close', function() {
for (var ii = 0; ii < invocationTargets.length; ii++) {
invocationTargets[ii].window.close();
}
invocationTargets = [];
})
parentListenersAttached = true;
}
};
module.exports = {
openWindowForComponent: openWindowForComponent,
toggleContainerVisible: toggleContainerVisible
};
*/

View file

@ -1,18 +1,26 @@
# Effectively all secondary windows are empty hot windows. We spawn the
# window and pre-load all of the basic javascript libraries (which takes a
# full second or so).
#
# Eventually when `WindowManager::newWindow` gets called, instead of
# actually spawning a new window, we'll call
# `NylasWindow::setLoadSettings` on the window instead. This will replace
# the window options, adjust params as necessary, and then re-load the
# plugins. Once `NylasWindow::setLoadSettings` fires, the main NylasEnv in
# the window will be notified via the `load-settings-changed` config
# Swap out Node's native Promise for Bluebird, which allows us to
# do fancy things like handle exceptions inside promise blocks
global.Promise = require 'bluebird'
Promise.setScheduler(global.setImmediate)
# Like sands through the hourglass, so are the days of our lives.
require './window'
# Skip "?loadSettings=".
# loadSettings = JSON.parse(decodeURIComponent(location.search.substr(14)))
# {windowType} = loadSettings
NylasEnvConstructor = require './nylas-env'
window.NylasEnv = window.atom = NylasEnvConstructor.loadOrCreate()
global.Promise.longStackTraces() if NylasEnv.inDevMode()
NylasEnv.initialize()
NylasEnv.startSecondaryWindow()

View file

@ -1,38 +0,0 @@
_ = require 'underscore'
# Public: This keeps track of constructors so we know how to inflate
# serialized objects.
#
# If 3rd party packages want to register new inflatable models, they can
# use `register` and pass the constructor along with the name.
#
# Note that there is one registry per window.
class SerializableRegistry
constructor: ->
# A mapping of the string name and the constructor class.
@_constructors = {}
get: (name) -> @_constructors[name]
isInRegistry: (name) -> @_constructors[name]?
deserialize: (name, data) ->
if _.isString(data)
data = JSON.parse(data)
constructor = @get(name)
if not _.isFunction(constructor)
throw new Error "Unsure of how to inflate #{JSON.stringify(data)}"
object = new constructor()
object.fromJSON(data)
return object
register: (constructor) ->
@_constructors[constructor.name] = constructor
unregister: (name) -> delete @_constructors[name]
module.exports = SerializableRegistry

View file

@ -0,0 +1,67 @@
/**
* Public: This keeps track of constructors so we know how to inflate
* serialized objects.
*
* We map constructor string names with factory functions that will return
* the actual constructor itself.
*
* The reason we have an extra function call to return a constructor is so
* we don't need to `require` all constructors at once on load. We are
* wasting a very large amount of time on bootup requiring files that may
* never be used or only used way down the line.
*
* If 3rd party packages want to register new inflatable models, they can
* use `register` and pass the constructor generator along with the name.
*
* Note that there is one registry per window.
*/
export default class SerializableRegistry {
constructor() {
this._constructorFactories = {}
}
get(name) {
return this._constructorFactories[name].call(null)
}
getAllConstructors() {
const constructors = []
for (const name in this._constructorFactories) {
if (this._constructorFactories.hasOwnProperty(name)) {
constructors.push(this.get(name))
}
}
return constructors
}
isInRegistry(name) {
return !!this._constructorFactories[name]
}
deserialize(name, dataJSON) {
let data = dataJSON;
if (typeof data === "string") {
data = JSON.parse(dataJSON)
}
const constructor = this.get(name)
if (typeof constructor !== "function") {
throw new Error(`Unsure of how to inflate ${JSON.stringify(data)}. \
Your constructor factory must return a class constructor.`);
}
const object = new constructor()
object.fromJSON(data)
return object
}
register(name, constructorFactory) {
this._constructorFactories[name] = constructorFactory
}
unregister(name) {
delete this._constructorFactories[name]
}
}

26
src/store-registry.es6 Normal file
View file

@ -0,0 +1,26 @@
import SerializableRegistry from './serializable-registry'
class StoreRegistry extends SerializableRegistry {
/**
* Most of the core Flux stores construct themselves on require. That
* construction initialize the stores, sets up listeners, and may access
* the database.
*
* It also kicks off a fairly large tree of require statements that
* takes considerable time to process.
*/
activateAllStores() {
for (const name in this._constructorFactories) {
if (this._constructorFactories.hasOwnProperty(name)) {
// All we need to do is hit `require` on the store. This will
// construct the object an initialize the require cache. The
// stores are now available in nylas-exports or from the node
// require cache.
this.get(name)
}
}
}
}
const registry = new StoreRegistry()
export default registry

View file

@ -1,5 +0,0 @@
SerializableRegistry = require './serializable-registry'
class TaskRegistry extends SerializableRegistry
module.exports = new TaskRegistry()

6
src/task-registry.es6 Normal file
View file

@ -0,0 +1,6 @@
import SerializableRegistry from './serializable-registry'
class TaskRegistry extends SerializableRegistry { }
const registry = new TaskRegistry()
export default registry

View file

@ -368,7 +368,9 @@ class ThemeManager
NylasEnv.config.observe 'core.themes', =>
@deactivateThemes()
@refreshLessCache() # Update cache for packages in core.themes config
# Refreshing the less cache is very expensive (hundreds of ms). It
# will be refreshed once the promise resolves after packages are
# activated.
promises = []
for themeName in @getEnabledThemeNames()

View file

@ -82,10 +82,6 @@ class WindowEventHandler
ComponentRegistry = require './component-registry'
ComponentRegistry.toggleComponentRegions()
@subscribeToCommand $(window), 'window:toggle-react-remote', ->
ReactRemote = require './react-remote/react-remote-parent'
ReactRemote.toggleContainerVisible()
document.addEventListener 'keydown', @onKeydown
# "Pinch to zoom" on the Mac gets translated by the system into a

View file

@ -1,37 +0,0 @@
path = require('path')
fs = require('fs-plus')
ipc = require('electron').ipcRenderer
require('module').globalPaths.push(path.resolve('exports'))
# Swap out Node's native Promise for Bluebird, which allows us to
# do fancy things like handle exceptions inside promise blocks
global.Promise = require 'bluebird'
global.NylasEnv =
commands:
add: ->
remove: ->
config:
get: -> null
set: ->
onDidChange: ->
onBeforeUnload: ->
getWindowLoadTime: -> 0
getConfigDirPath: ->
@configDirPath ?= JSON.parse(decodeURIComponent(location.search.substr(14))).configDirPath
getLoadSettings: ->
@loadSettings ?= JSON.parse(decodeURIComponent(location.search.substr(14)))
inSpecMode: ->
false
isMainWindow: ->
false
# Like sands through the hourglass, so are the days of our lives.
require './window'
prefs = require '../internal_packages/preferences/lib/main'
prefs.activate()
ipc.on 'command', (command, args) ->
if command is 'window:toggle-dev-tools'
ipc.send('call-webcontents-method', 'toggleDevTools')

View file

@ -1,19 +0,0 @@
<body>
<div class="sheet-container">
<div class="sheet-toolbar">
<div style="position:absolute; width:100%; height:100%; z-index: 1;" class="sheet-toolbar-container">
<div name="ToolbarWindowControls" class="toolbar-window-controls">
<button class="close" onClick="require('electron').remote.getCurrentWindow().close()"></button>
<button class="minimize" onClick="require('electron').remote.getCurrentWindow().minimize()"></button>
<button class="maximize" onClick="require('electron').remote.getCurrentWindow().maximize()"></button>
</div>
<div class="window-title"></div>
</div>
</div>
<div id="container" style="left:0; right:0; position:absolute;"></div>
</div>
<script>
require('../src/react-remote/react-remote-child')
</script>
</body>