diff --git a/build/resources/asar-ordering-hint.txt b/build/resources/asar-ordering-hint.txt index 26eb1c60b..f75d39086 100644 --- a/build/resources/asar-ordering-hint.txt +++ b/build/resources/asar-ordering-hint.txt @@ -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 diff --git a/internal_packages/composer-scheduler/lib/calendar/proposed-time-picker.jsx b/internal_packages/composer-scheduler/lib/calendar/proposed-time-picker.jsx index 5416c9eb7..1a6a9356e 100644 --- a/internal_packages/composer-scheduler/lib/calendar/proposed-time-picker.jsx +++ b/internal_packages/composer-scheduler/lib/calendar/proposed-time-picker.jsx @@ -28,6 +28,7 @@ export default class ProposedTimePicker extends React.Component { pendingSave: ProposedTimeCalendarStore.pendingSave(), }); }) + NylasEnv.displayWindow() } shouldComponentUpdate(nextProps, nextState) { diff --git a/internal_packages/composer-scheduler/lib/composer/new-event-card.jsx b/internal_packages/composer-scheduler/lib/composer/new-event-card.jsx index 5f0d9d320..72751a2f1 100644 --- a/internal_packages/composer-scheduler/lib/composer/new-event-card.jsx +++ b/internal_packages/composer-scheduler/lib/composer/new-event-card.jsx @@ -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() { diff --git a/internal_packages/composer-scheduler/lib/composer/new-event-helper.es6 b/internal_packages/composer-scheduler/lib/composer/new-event-helper.es6 index daaf017d9..bffe8cd25 100644 --- a/internal_packages/composer-scheduler/lib/composer/new-event-helper.es6 +++ b/internal_packages/composer-scheduler/lib/composer/new-event-helper.es6 @@ -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() diff --git a/internal_packages/composer-scheduler/lib/composer/scheduler-composer-button.jsx b/internal_packages/composer-scheduler/lib/composer/scheduler-composer-button.jsx index 79ebc143e..4f4a25bc8 100644 --- a/internal_packages/composer-scheduler/lib/composer/scheduler-composer-button.jsx +++ b/internal_packages/composer-scheduler/lib/composer/scheduler-composer-button.jsx @@ -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() } diff --git a/internal_packages/composer/lib/decorators/inflate-draft-client-id.es6 b/internal_packages/composer/lib/decorators/inflate-draft-client-id.es6 index 56cd66f37..22d23b8b3 100644 --- a/internal_packages/composer/lib/decorators/inflate-draft-client-id.es6 +++ b/internal_packages/composer/lib/decorators/inflate-draft-client-id.es6 @@ -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() }); } diff --git a/internal_packages/composer/lib/main.es6 b/internal_packages/composer/lib/main.es6 index 091ddd37c..a13266fe2 100644 --- a/internal_packages/composer/lib/main.es6 +++ b/internal_packages/composer/lib/main.es6 @@ -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 ( @@ -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); diff --git a/internal_packages/composer/package.json b/internal_packages/composer/package.json index 9e4b8f34d..9dfb64905 100644 --- a/internal_packages/composer/package.json +++ b/internal_packages/composer/package.json @@ -14,6 +14,7 @@ }, "windowTypes": { "default": true, - "composer": true + "composer": true, + "composer-preload": true } } diff --git a/internal_packages/onboarding/lib/page-router.cjsx b/internal_packages/onboarding/lib/page-router.cjsx index 8f5271338..d600694af 100644 --- a/internal_packages/onboarding/lib/page-router.cjsx +++ b/internal_packages/onboarding/lib/page-router.cjsx @@ -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 diff --git a/internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee b/internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee index c138e3b54..65c1582bb 100644 --- a/internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee +++ b/internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee @@ -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' diff --git a/menus/darwin.cson b/menus/darwin.cson index 43d2fd9bc..3dc2893bd 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -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' } diff --git a/menus/linux.cson b/menus/linux.cson index f233e24d4..1e273be7c 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -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' } diff --git a/menus/win32.cson b/menus/win32.cson index c52a92045..29c13bd18 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -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' } diff --git a/spec/database-object-registry-spec.coffee b/spec/database-object-registry-spec.coffee index 4075b3d51..f4fa286e1 100644 --- a/spec/database-object-registry-spec.coffee +++ b/spec/database-object-registry-spec.coffee @@ -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" diff --git a/spec/stores/draft-store-spec.es6 b/spec/stores/draft-store-spec.es6 index 76251a463..850c011d8 100644 --- a/spec/stores/draft-store-spec.es6 +++ b/spec/stores/draft-store-spec.es6 @@ -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() }, }); }); diff --git a/src/browser/application.coffee b/src/browser/application.coffee index ad9f1d62d..04830b367 100644 --- a/src/browser/application.coffee +++ b/src/browser/application.coffee @@ -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) diff --git a/src/browser/auto-update-manager.coffee b/src/browser/auto-update-manager.coffee index 8d6776a10..c41b76c9b 100644 --- a/src/browser/auto-update-manager.coffee +++ b/src/browser/auto-update-manager.coffee @@ -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() diff --git a/src/browser/file-list-cache.es6 b/src/browser/file-list-cache.es6 new file mode 100644 index 000000000..f5f686087 --- /dev/null +++ b/src/browser/file-list-cache.es6 @@ -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 = [] + } +} diff --git a/src/browser/nylas-window.coffee b/src/browser/nylas-window.coffee index 8fc302640..4c6d45ead 100644 --- a/src/browser/nylas-window.coffee +++ b/src/browser/nylas-window.coffee @@ -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') diff --git a/src/browser/window-launcher.es6 b/src/browser/window-launcher.es6 new file mode 100644 index 000000000..77315c0d9 --- /dev/null +++ b/src/browser/window-launcher.es6 @@ -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 + } +} diff --git a/src/browser/window-manager.coffee b/src/browser/window-manager.coffee index 5fae1cfdd..29f21b06f 100644 --- a/src/browser/window-manager.coffee +++ b/src/browser/window-manager.coffee @@ -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 diff --git a/src/components/popover.cjsx b/src/components/popover.cjsx index 6410f351c..7fa3865d6 100644 --- a/src/components/popover.cjsx +++ b/src/components/popover.cjsx @@ -1,4 +1,5 @@ React = require 'react' +ReactDOM = require 'react-dom' _ = require 'underscore' {CompositeDisposable} = require 'event-kit' diff --git a/src/database-object-registry.coffee b/src/database-object-registry.coffee deleted file mode 100644 index 0647483ea..000000000 --- a/src/database-object-registry.coffee +++ /dev/null @@ -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() diff --git a/src/database-object-registry.es6 b/src/database-object-registry.es6 new file mode 100644 index 000000000..766de3fdd --- /dev/null +++ b/src/database-object-registry.es6 @@ -0,0 +1,6 @@ +import SerializableRegistry from './serializable-registry' + +class DatabaseObjectRegistry extends SerializableRegistry { } + +const registry = new DatabaseObjectRegistry() +export default registry diff --git a/src/deprecate-utils.coffee b/src/deprecate-utils.coffee index 165209a0e..4c642c40d 100644 --- a/src/deprecate-utils.coffee +++ b/src/deprecate-utils.coffee @@ -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 diff --git a/src/flux/models/utils.coffee b/src/flux/models/utils.coffee index 238d3bf4b..47bca868d 100644 --- a/src/flux/models/utils.coffee +++ b/src/flux/models/utils.coffee @@ -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 diff --git a/src/flux/stores/database-setup-query-builder.coffee b/src/flux/stores/database-setup-query-builder.coffee index 9b8a837be..1b2484716 100644 --- a/src/flux/stores/database-setup-query-builder.coffee +++ b/src/flux/stores/database-setup-query-builder.coffee @@ -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 diff --git a/src/flux/stores/draft-store.coffee b/src/flux/stores/draft-store.coffee index 9884c388a..8df2ba7df 100644 --- a/src/flux/stores/draft-store.coffee +++ b/src/flux/stores/draft-store.coffee @@ -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) => diff --git a/src/flux/tasks/syncback-metadata-task.es6 b/src/flux/tasks/syncback-metadata-task.es6 index 733b6d902..8309ac18f 100644 --- a/src/flux/tasks/syncback-metadata-task.es6 +++ b/src/flux/tasks/syncback-metadata-task.es6 @@ -10,7 +10,7 @@ export default class SyncbackMetadataTask extends SyncbackModelTask { } getModelConstructor() { - return DatabaseObjectRegistry.classMap()[this.modelClassName]; + return DatabaseObjectRegistry.get(this.modelClassName); } getRequestData = (model) => { diff --git a/src/global/nylas-exports.coffee b/src/global/nylas-exports.coffee index 3b35e161c..0bc64e76f 100644 --- a/src/global/nylas-exports.coffee +++ b/src/global/nylas-exports.coffee @@ -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 diff --git a/src/less-compile-cache.coffee b/src/less-compile-cache.coffee index bf7d7d913..16b3d5894 100644 --- a/src/less-compile-cache.coffee +++ b/src/less-compile-cache.coffee @@ -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) diff --git a/src/mail-rules-processor.coffee b/src/mail-rules-processor.coffee index 6231f959a..447f5a975 100644 --- a/src/mail-rules-processor.coffee +++ b/src/mail-rules-processor.coffee @@ -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 diff --git a/src/nylas-env.coffee b/src/nylas-env.coffee index 9a6fb94f9..adb79417e 100644 --- a/src/nylas-env.coffee +++ b/src/nylas-env.coffee @@ -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) diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 194d88521..0a5bbfe92 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -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) -> diff --git a/src/package.coffee b/src/package.coffee index 19acaa200..1f51e7286 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -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 = [] diff --git a/src/react-remote/react-remote-child.js b/src/react-remote/react-remote-child.js deleted file mode 100644 index 4082ee9af..000000000 --- a/src/react-remote/react-remote-child.js +++ /dev/null @@ -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); diff --git a/src/react-remote/react-remote-parent.js b/src/react-remote/react-remote-parent.js deleted file mode 100644 index 5a27009f6..000000000 --- a/src/react-remote/react-remote-parent.js +++ /dev/null @@ -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 -}; -*/ diff --git a/src/window-secondary-bootstrap.coffee b/src/secondary-window-bootstrap.coffee similarity index 54% rename from src/window-secondary-bootstrap.coffee rename to src/secondary-window-bootstrap.coffee index 7df9801f0..768e7b30b 100644 --- a/src/window-secondary-bootstrap.coffee +++ b/src/secondary-window-bootstrap.coffee @@ -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() diff --git a/src/serializable-registry.coffee b/src/serializable-registry.coffee deleted file mode 100644 index 85307a91e..000000000 --- a/src/serializable-registry.coffee +++ /dev/null @@ -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 diff --git a/src/serializable-registry.es6 b/src/serializable-registry.es6 new file mode 100644 index 000000000..ccb304f13 --- /dev/null +++ b/src/serializable-registry.es6 @@ -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] + } +} diff --git a/src/store-registry.es6 b/src/store-registry.es6 new file mode 100644 index 000000000..35f002fed --- /dev/null +++ b/src/store-registry.es6 @@ -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 diff --git a/src/task-registry.coffee b/src/task-registry.coffee deleted file mode 100644 index c37098733..000000000 --- a/src/task-registry.coffee +++ /dev/null @@ -1,5 +0,0 @@ -SerializableRegistry = require './serializable-registry' - -class TaskRegistry extends SerializableRegistry - -module.exports = new TaskRegistry() diff --git a/src/task-registry.es6 b/src/task-registry.es6 new file mode 100644 index 000000000..a20131529 --- /dev/null +++ b/src/task-registry.es6 @@ -0,0 +1,6 @@ +import SerializableRegistry from './serializable-registry' + +class TaskRegistry extends SerializableRegistry { } + +const registry = new TaskRegistry() +export default registry diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index 364123c4d..870782493 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -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() diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 604013580..8bedf4419 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -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 diff --git a/src/window-thin-bootstrap.coffee b/src/window-thin-bootstrap.coffee deleted file mode 100644 index 736b9689d..000000000 --- a/src/window-thin-bootstrap.coffee +++ /dev/null @@ -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') diff --git a/static/react-remote-child.html b/static/react-remote-child.html deleted file mode 100644 index 6196b2545..000000000 --- a/static/react-remote-child.html +++ /dev/null @@ -1,19 +0,0 @@ - -
-
-
-
- - - -
-
-
-
-
-
- - -