mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-21 15:56:10 +08:00
feat(win): faster popout windows
Summary: This diff is designed to dramatically speed up new window load time for all window types and reduce memory consumption of our hot windows. Before this diff, windows loaded in ~3 seconds. They now boot in a couple hundred milliseconds without requiring to keep hot windows around for each and every type of popout window we want to load quickly. One of the largest bottlenecks was the `require`ing and initializing of everything in `NylasExports`. I changed `NylasExports` to be entirely lazily-loaded. Drafts and tasks now register their constructors with a `StoreRegistry` and the `TaskRegistry`. This lets us explicitly choose a time to activate these stores in the window initalization instead of whenever nylas-exports happens to be required first. Before, NylasExports was required first when components were first rendering. This made initial render extremely slow and made the proposed time picker popout slow. By moving require into the very initial window boot, we can create a new scheme of hot windows that are "half loaded". All of the expensive require-ing and store initialization is done. All we need to do is activate the packages for just the one window. This means that the hot window scheme needs to fundamentally change from have fully pre-loaded windows, to having half-loaded empty hot windows that can get their window props overridden again. This led to a major refactor of the WindowManager to support this new window scheme. Along the way the API of WindowManager was significantly simplifed. Instead of a bunch of special-cased windows, there are now consistent interfaces to get and `ensure` windows are created and displayed. This DRYed up a lot of repeated logic around showing or creating core windows. This also allowed the consolidation of the core window configurations into one place for much easier reasoning about what's getting booted up. When a hot window goes "live" and gets populated, we simply change the `windowType`. This now re-triggers the loading of all of the packages for the window. All of the loading time is now just for the packages that window requires since core Nylas is there thanks to the hot window mechanism. Unfortunately loading all of the packages for the composer was still unnaceptably slow. The major issue was that all of the composer plugins were taking a long time to process and initialize. The solution was to have the main composer load first, then trigger another window load settings change to change the `windowType` that loads in all of the plugins. Another major bottleneck was the `RetinaImg` name lookup on disk. This requires traversing the entire static folder synchronously on boot. This is now done once when the main window loads and saved in a cache in the browser process. Any secondary windows simply ask the backend for this cache and save the filesystem access time. The Paper Doc below is the current set of manual tests I'm doing to make sure no window interactions (there are a lot of them!) regressed. Test Plan: https://paper.dropbox.com/doc/Window-Refactor-UYsgvjgdXgVlTw8nXTr9h Reviewers: juan, bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2916
This commit is contained in:
parent
d9d8ffaf45
commit
78112563a6
|
@ -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
|
||||
|
|
|
@ -28,6 +28,7 @@ export default class ProposedTimePicker extends React.Component {
|
|||
pendingSave: ProposedTimeCalendarStore.pendingSave(),
|
||||
});
|
||||
})
|
||||
NylasEnv.displayWindow()
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -21,40 +21,34 @@ class ComposerWithWindowProps extends React.Component {
|
|||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = NylasEnv.getWindowProps()
|
||||
|
||||
// We'll now always have windowProps by the time we construct this.
|
||||
const windowProps = NylasEnv.getWindowProps()
|
||||
const {draftJSON, draftClientId} = windowProps;
|
||||
const draft = new Message().fromJSON(draftJSON);
|
||||
DraftStore._createSession(draftClientId, draft);
|
||||
this.state = windowProps
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.state.draftClientId) {
|
||||
this.ready();
|
||||
}
|
||||
|
||||
this.unlisten = NylasEnv.onWindowPropsReceived((windowProps) => {
|
||||
const {errorMessage, draftJSON, draftClientId} = windowProps;
|
||||
|
||||
if (draftJSON) {
|
||||
const draft = new Message().fromJSON(draftJSON);
|
||||
DraftStore._createSession(draftClientId, draft);
|
||||
}
|
||||
|
||||
this.setState({draftClientId});
|
||||
this.ready();
|
||||
if (errorMessage) {
|
||||
this._showInitialErrorDialog(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
}
|
||||
|
||||
ready = () => {
|
||||
onDraftReady = () => {
|
||||
this.refs.composer.focus().then(() => {
|
||||
NylasEnv.getCurrentWindow().show()
|
||||
NylasEnv.getCurrentWindow().focus()
|
||||
NylasEnv.displayWindow()
|
||||
if (this.state.errorMessage) {
|
||||
this._showInitialErrorDialog(this.state.errorMessage);
|
||||
}
|
||||
NylasEnv.getCurrentWindow().updateLoadSettings({
|
||||
windowType: "composer",
|
||||
})
|
||||
|
||||
// The call to updateLoadSettings will start loading the remaining
|
||||
// packages. Once those packages load it'll cause a change in the
|
||||
// root Sheet-level InjectedComponentSet, which will cause
|
||||
// everything to re-render losing our focus. We have to manually
|
||||
// refocus it but defer it so the event loop of the package
|
||||
// activation happens first.
|
||||
_.defer(() => {
|
||||
this.refs.composer.focus()
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -62,6 +56,7 @@ class ComposerWithWindowProps extends React.Component {
|
|||
return (
|
||||
<ComposerViewForDraftClientId
|
||||
ref="composer"
|
||||
onDraftReady={this.onDraftReady}
|
||||
draftClientId={this.state.draftClientId}
|
||||
className="composer-full-window"
|
||||
/>
|
||||
|
@ -91,28 +86,21 @@ export function activate() {
|
|||
});
|
||||
|
||||
if (NylasEnv.isMainWindow()) {
|
||||
NylasEnv.registerHotWindow({
|
||||
windowType: 'composer',
|
||||
replenishNum: 2,
|
||||
});
|
||||
ComponentRegistry.register(ComposeButton, {
|
||||
location: WorkspaceStore.Location.RootSidebar.Toolbar,
|
||||
});
|
||||
} else {
|
||||
NylasEnv.getCurrentWindow().setMinimumSize(480, 250);
|
||||
WorkspaceStore.defineSheet('Main', {root: true}, {
|
||||
popout: ['Center'],
|
||||
});
|
||||
ComponentRegistry.register(ComposerWithWindowProps, {
|
||||
location: WorkspaceStore.Location.Center,
|
||||
});
|
||||
}
|
||||
|
||||
NylasEnv.getCurrentWindow().setMinimumSize(480, 250);
|
||||
WorkspaceStore.defineSheet('Main', {root: true}, {
|
||||
popout: ['Center'],
|
||||
});
|
||||
ComponentRegistry.register(ComposerWithWindowProps, {
|
||||
location: WorkspaceStore.Location.Center,
|
||||
});
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
if (NylasEnv.isMainWindow()) {
|
||||
NylasEnv.unregisterHotWindow('composer');
|
||||
}
|
||||
ComponentRegistry.unregister(ComposerViewForDraftClientId);
|
||||
ComponentRegistry.unregister(ComposeButton);
|
||||
ComponentRegistry.unregister(ComposerWithWindowProps);
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
"windowTypes": {
|
||||
"default": true,
|
||||
"composer": true
|
||||
"composer": true,
|
||||
"composer-preload": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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() },
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
12
src/browser/file-list-cache.es6
Normal file
12
src/browser/file-list-cache.es6
Normal file
|
@ -0,0 +1,12 @@
|
|||
// File operations (like traversing directory trees) are extremely
|
||||
// expensive. If any window traverses a tree once, we keep a cache of it
|
||||
// on the backend process. That way any new windows don't need to spend
|
||||
// their precious load time performing the same expensive operation.
|
||||
export default class FileListCache {
|
||||
constructor() {
|
||||
this.imageData = "{}" // A JSON stringified hash
|
||||
this.packagePaths = []
|
||||
this.lessCacheImportPaths = []
|
||||
this.lessCacheImportedFiles = []
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
|
|
98
src/browser/window-launcher.es6
Normal file
98
src/browser/window-launcher.es6
Normal file
|
@ -0,0 +1,98 @@
|
|||
import NylasWindow from './nylas-window'
|
||||
|
||||
const DEBUG_SHOW_HOT_WINDOW = false;
|
||||
|
||||
/**
|
||||
* It takes a full second or more to bootup a Nylas window. Most of this
|
||||
* is due to sheer amount of time it takes to parse all of the javascript
|
||||
* and follow the require tree.
|
||||
*
|
||||
* Since popout windows need to be more responsive than that, we pre-load
|
||||
* "hot" windows in the background that have most of the code loaded. Then
|
||||
* all we need to do is load the handful of packages the window
|
||||
* requires and show it.
|
||||
*/
|
||||
export default class WindowLauncher {
|
||||
static EMPTY_WINDOW = "emptyWindow"
|
||||
|
||||
constructor(appOpts) {
|
||||
this.defaultWindowOpts = {
|
||||
hidden: false,
|
||||
devMode: appOpts.devMode,
|
||||
safeMode: appOpts.safeMode,
|
||||
windowType: WindowLauncher.EMPTY_WINDOW,
|
||||
resourcePath: appOpts.resourcePath,
|
||||
configDirPath: appOpts.configDirPath,
|
||||
}
|
||||
this.hotWindow = new NylasWindow(this._hotWindowOpts());
|
||||
|
||||
if (DEBUG_SHOW_HOT_WINDOW) {
|
||||
this.hotWindow.showWhenLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
newWindow(options) {
|
||||
const opts = Object.assign({}, this.defaultWindowOpts, options);
|
||||
let win;
|
||||
if (opts.bootstrapScript) {
|
||||
win = new NylasWindow(opts)
|
||||
} else {
|
||||
opts.bootstrapScript = this._secondaryWindowBootstrap()
|
||||
if (opts.coldStartOnly) {
|
||||
// Useful for the Worker Window: A secondary window that shouldn't
|
||||
// be hot-loaded
|
||||
win = new NylasWindow(opts)
|
||||
} else {
|
||||
win = this.hotWindow;
|
||||
|
||||
// Regenerate the hot window.
|
||||
this.hotWindow = new NylasWindow(this._hotWindowOpts());
|
||||
if (DEBUG_SHOW_HOT_WINDOW) {
|
||||
this.hotWindow.showWhenLoaded()
|
||||
}
|
||||
|
||||
const newLoadSettings = Object.assign({}, win.loadSettings(), opts)
|
||||
if (newLoadSettings.windowType === WindowLauncher.EMPTY_WINDOW) {
|
||||
throw new Error("Must specify a windowType")
|
||||
}
|
||||
|
||||
// Reset the loaded state and update the load settings.
|
||||
// This will fire `NylasEnv::populateHotWindow` and reload the
|
||||
// packages.
|
||||
win.setLoadSettings(newLoadSettings);
|
||||
}
|
||||
}
|
||||
if (!opts.hidden) {
|
||||
// NOTE: In the case of a cold window, this will show it once
|
||||
// loaded. If it's a hotWindow, since hotWindows have a
|
||||
// `hidden:true` flag, nothing will show. When `setLoadSettings`
|
||||
// starts populating the window in `populateHotWindow` we'll show or
|
||||
// hide based on the windowOpts
|
||||
win.showWhenLoaded()
|
||||
}
|
||||
return win
|
||||
}
|
||||
|
||||
// Note: This method calls `browserWindow.destroy()` which closes
|
||||
// windows without waiting for them to load or firing window lifecycle
|
||||
// events. This is necessary for the app to quit promptly on Linux.
|
||||
// https://phab.nylas.com/T1282
|
||||
cleanupBeforeAppQuit() {
|
||||
this.hotWindow.browserWindow.destroy()
|
||||
}
|
||||
|
||||
_secondaryWindowBootstrap() {
|
||||
if (!this._bootstrap) {
|
||||
this._bootstrap = require.resolve("../secondary-window-bootstrap")
|
||||
}
|
||||
return this._bootstrap
|
||||
}
|
||||
|
||||
_hotWindowOpts() {
|
||||
const hotWindowOpts = Object.assign({}, this.defaultWindowOpts);
|
||||
hotWindowOpts.packageLoadingDeferred = true;
|
||||
hotWindowOpts.bootstrapScript = this._secondaryWindowBootstrap();
|
||||
hotWindowOpts.hidden = DEBUG_SHOW_HOT_WINDOW;
|
||||
return hotWindowOpts
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
React = require 'react'
|
||||
ReactDOM = require 'react-dom'
|
||||
_ = require 'underscore'
|
||||
{CompositeDisposable} = require 'event-kit'
|
||||
|
||||
|
|
|
@ -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()
|
6
src/database-object-registry.es6
Normal file
6
src/database-object-registry.es6
Normal file
|
@ -0,0 +1,6 @@
|
|||
import SerializableRegistry from './serializable-registry'
|
||||
|
||||
class DatabaseObjectRegistry extends SerializableRegistry { }
|
||||
|
||||
const registry = new DatabaseObjectRegistry()
|
||||
export default registry
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -10,7 +10,7 @@ export default class SyncbackMetadataTask extends SyncbackModelTask {
|
|||
}
|
||||
|
||||
getModelConstructor() {
|
||||
return DatabaseObjectRegistry.classMap()[this.modelClassName];
|
||||
return DatabaseObjectRegistry.get(this.modelClassName);
|
||||
}
|
||||
|
||||
getRequestData = (model) => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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 = []
|
||||
|
|
128
src/react-remote/react-remote-child.js
vendored
128
src/react-remote/react-remote-child.js
vendored
|
@ -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);
|
405
src/react-remote/react-remote-parent.js
vendored
405
src/react-remote/react-remote-parent.js
vendored
|
@ -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
|
||||
};
|
||||
*/
|
|
@ -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()
|
||||
|
|
@ -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
|
67
src/serializable-registry.es6
Normal file
67
src/serializable-registry.es6
Normal file
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* Public: This keeps track of constructors so we know how to inflate
|
||||
* serialized objects.
|
||||
*
|
||||
* We map constructor string names with factory functions that will return
|
||||
* the actual constructor itself.
|
||||
*
|
||||
* The reason we have an extra function call to return a constructor is so
|
||||
* we don't need to `require` all constructors at once on load. We are
|
||||
* wasting a very large amount of time on bootup requiring files that may
|
||||
* never be used or only used way down the line.
|
||||
*
|
||||
* If 3rd party packages want to register new inflatable models, they can
|
||||
* use `register` and pass the constructor generator along with the name.
|
||||
*
|
||||
* Note that there is one registry per window.
|
||||
*/
|
||||
export default class SerializableRegistry {
|
||||
constructor() {
|
||||
this._constructorFactories = {}
|
||||
}
|
||||
|
||||
get(name) {
|
||||
return this._constructorFactories[name].call(null)
|
||||
}
|
||||
|
||||
getAllConstructors() {
|
||||
const constructors = []
|
||||
for (const name in this._constructorFactories) {
|
||||
if (this._constructorFactories.hasOwnProperty(name)) {
|
||||
constructors.push(this.get(name))
|
||||
}
|
||||
}
|
||||
return constructors
|
||||
}
|
||||
|
||||
isInRegistry(name) {
|
||||
return !!this._constructorFactories[name]
|
||||
}
|
||||
|
||||
deserialize(name, dataJSON) {
|
||||
let data = dataJSON;
|
||||
if (typeof data === "string") {
|
||||
data = JSON.parse(dataJSON)
|
||||
}
|
||||
|
||||
const constructor = this.get(name)
|
||||
|
||||
if (typeof constructor !== "function") {
|
||||
throw new Error(`Unsure of how to inflate ${JSON.stringify(data)}. \
|
||||
Your constructor factory must return a class constructor.`);
|
||||
}
|
||||
|
||||
const object = new constructor()
|
||||
object.fromJSON(data)
|
||||
|
||||
return object
|
||||
}
|
||||
|
||||
register(name, constructorFactory) {
|
||||
this._constructorFactories[name] = constructorFactory
|
||||
}
|
||||
|
||||
unregister(name) {
|
||||
delete this._constructorFactories[name]
|
||||
}
|
||||
}
|
26
src/store-registry.es6
Normal file
26
src/store-registry.es6
Normal file
|
@ -0,0 +1,26 @@
|
|||
import SerializableRegistry from './serializable-registry'
|
||||
|
||||
class StoreRegistry extends SerializableRegistry {
|
||||
/**
|
||||
* Most of the core Flux stores construct themselves on require. That
|
||||
* construction initialize the stores, sets up listeners, and may access
|
||||
* the database.
|
||||
*
|
||||
* It also kicks off a fairly large tree of require statements that
|
||||
* takes considerable time to process.
|
||||
*/
|
||||
activateAllStores() {
|
||||
for (const name in this._constructorFactories) {
|
||||
if (this._constructorFactories.hasOwnProperty(name)) {
|
||||
// All we need to do is hit `require` on the store. This will
|
||||
// construct the object an initialize the require cache. The
|
||||
// stores are now available in nylas-exports or from the node
|
||||
// require cache.
|
||||
this.get(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const registry = new StoreRegistry()
|
||||
export default registry
|
|
@ -1,5 +0,0 @@
|
|||
SerializableRegistry = require './serializable-registry'
|
||||
|
||||
class TaskRegistry extends SerializableRegistry
|
||||
|
||||
module.exports = new TaskRegistry()
|
6
src/task-registry.es6
Normal file
6
src/task-registry.es6
Normal file
|
@ -0,0 +1,6 @@
|
|||
import SerializableRegistry from './serializable-registry'
|
||||
|
||||
class TaskRegistry extends SerializableRegistry { }
|
||||
|
||||
const registry = new TaskRegistry()
|
||||
export default registry
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
|
@ -1,19 +0,0 @@
|
|||
<body>
|
||||
<div class="sheet-container">
|
||||
<div class="sheet-toolbar">
|
||||
<div style="position:absolute; width:100%; height:100%; z-index: 1;" class="sheet-toolbar-container">
|
||||
<div name="ToolbarWindowControls" class="toolbar-window-controls">
|
||||
<button class="close" onClick="require('electron').remote.getCurrentWindow().close()"></button>
|
||||
<button class="minimize" onClick="require('electron').remote.getCurrentWindow().minimize()"></button>
|
||||
<button class="maximize" onClick="require('electron').remote.getCurrentWindow().maximize()"></button>
|
||||
</div>
|
||||
<div class="window-title"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="container" style="left:0; right:0; position:absolute;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
require('../src/react-remote/react-remote-child')
|
||||
</script>
|
||||
</body>
|
Loading…
Reference in a new issue