diff --git a/packages/client-app/internal_packages/composer-templates/lib/template-picker.jsx b/packages/client-app/internal_packages/composer-templates/lib/template-picker.jsx index 2ca49a3cc..0d4e12769 100644 --- a/packages/client-app/internal_packages/composer-templates/lib/template-picker.jsx +++ b/packages/client-app/internal_packages/composer-templates/lib/template-picker.jsx @@ -7,7 +7,7 @@ class TemplatePopover extends React.Component { static displayName = 'TemplatePopover'; static propTypes = { - draftClientId: React.PropTypes.string, + headerMessageId: React.PropTypes.string, }; constructor() { @@ -45,7 +45,7 @@ class TemplatePopover extends React.Component { }; _onChooseTemplate = (template) => { - Actions.insertTemplateId({templateId: template.id, draftClientId: this.props.draftClientId}); + Actions.insertTemplateId({templateId: template.id, headerMessageId: this.props.headerMessageId}); Actions.closePopover(); } @@ -54,7 +54,7 @@ class TemplatePopover extends React.Component { }; _onNewTemplate = () => { - Actions.createTemplate({draftClientId: this.props.draftClientId}); + Actions.createTemplate({headerMessageId: this.props.headerMessageId}); }; _onClickButton = () => { @@ -103,13 +103,13 @@ class TemplatePicker extends React.Component { static displayName = 'TemplatePicker'; static propTypes = { - draftClientId: React.PropTypes.string, + headerMessageId: React.PropTypes.string, }; _onClickButton = () => { const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect() Actions.openPopover( - , + , {originRect: buttonRect, direction: 'up'} ) }; diff --git a/packages/client-app/internal_packages/composer-templates/lib/template-store.es6 b/packages/client-app/internal_packages/composer-templates/lib/template-store.es6 index 8baa4c5db..8642f685c 100644 --- a/packages/client-app/internal_packages/composer-templates/lib/template-store.es6 +++ b/packages/client-app/internal_packages/composer-templates/lib/template-store.es6 @@ -112,9 +112,9 @@ class TemplateStore extends NylasStore { }); } - _onCreateTemplate({draftClientId, name, contents} = {}) { - if (draftClientId) { - DraftStore.sessionForClientId(draftClientId).then((session) => { + _onCreateTemplate({headerMessageId, name, contents} = {}) { + if (headerMessageId) { + DraftStore.sessionForClientId(headerMessageId).then((session) => { const draft = session.draft(); const draftName = name || draft.subject.replace(TemplateStore.INVALID_TEMPLATE_NAME_REGEX, ''); let draftContents = contents || QuotedHTMLTransformer.removeQuotedHTML(draft.body); @@ -255,9 +255,9 @@ class TemplateStore extends NylasStore { }); } - _onInsertTemplateId({templateId, draftClientId} = {}) { + _onInsertTemplateId({templateId, headerMessageId} = {}) { this.getTemplateContents(templateId, (templateBody) => { - DraftStore.sessionForClientId(draftClientId).then((session) => { + DraftStore.sessionForClientId(headerMessageId).then((session) => { let proceed = true; if (!session.draft().pristine && !session.draft().hasEmptyBody()) { proceed = this._displayDialog( diff --git a/packages/client-app/internal_packages/composer-templates/spec/template-store-spec.es6 b/packages/client-app/internal_packages/composer-templates/spec/template-store-spec.es6 index 65a7ffba3..c7e411d35 100644 --- a/packages/client-app/internal_packages/composer-templates/spec/template-store-spec.es6 +++ b/packages/client-app/internal_packages/composer-templates/spec/template-store-spec.es6 @@ -83,7 +83,7 @@ xdescribe('TemplateStore', function templateStore() { runs(() => { TemplateStore._onInsertTemplateId({ templateId: 'template1.html', - draftClientId: 'localid-draft', + headerMessageId: 'localid-draft', }); }); waitsFor(() => add.calls.length > 0); @@ -98,8 +98,8 @@ xdescribe('TemplateStore', function templateStore() { describe('onCreateTemplate', () => { beforeEach(() => { let d; - spyOn(DraftStore, 'sessionForClientId').andCallFake((draftClientId) => { - if (draftClientId === 'localid-nosubject') { + spyOn(DraftStore, 'sessionForClientId').andCallFake((headerMessageId) => { + if (headerMessageId === 'localid-nosubject') { d = new Message({subject: '', body: '

Body

'}); } else { d = new Message({subject: 'Subject', body: '

Body

'}); @@ -149,7 +149,7 @@ xdescribe('TemplateStore', function templateStore() { spyOn(TemplateStore, 'trigger'); spyOn(TemplateStore, '_populate'); runs(() => { - TemplateStore._onCreateTemplate({draftClientId: 'localid-b'}); + TemplateStore._onCreateTemplate({headerMessageId: 'localid-b'}); }); waitsFor(() => TemplateStore.trigger.callCount > 0); runs(() => { @@ -161,7 +161,7 @@ xdescribe('TemplateStore', function templateStore() { spyOn(TemplateStore, '_displayError'); spyOn(fs, 'watch'); runs(() => { - TemplateStore._onCreateTemplate({draftClientId: 'localid-nosubject'}); + TemplateStore._onCreateTemplate({headerMessageId: 'localid-nosubject'}); }); waitsFor(() => TemplateStore._displayError.callCount > 0); runs(() => { diff --git a/packages/client-app/internal_packages/composer/lib/composer-header-actions.jsx b/packages/client-app/internal_packages/composer/lib/composer-header-actions.jsx index 28a7982ea..f7baea48e 100644 --- a/packages/client-app/internal_packages/composer/lib/composer-header-actions.jsx +++ b/packages/client-app/internal_packages/composer/lib/composer-header-actions.jsx @@ -7,14 +7,14 @@ export default class ComposerHeaderActions extends React.Component { static displayName = 'ComposerHeaderActions'; static propTypes = { - draftClientId: React.PropTypes.string.isRequired, + headerMessageId: React.PropTypes.string.isRequired, enabledFields: React.PropTypes.array.isRequired, participantsFocused: React.PropTypes.bool, onShowAndFocusField: React.PropTypes.func.isRequired, } _onPopoutComposer = () => { - Actions.composePopoutDraft(this.props.draftClientId); + Actions.composePopoutDraft(this.props.headerMessageId); } render() { diff --git a/packages/client-app/internal_packages/composer/lib/composer-header.jsx b/packages/client-app/internal_packages/composer/lib/composer-header.jsx index a9c373cc9..06d40458a 100644 --- a/packages/client-app/internal_packages/composer/lib/composer-header.jsx +++ b/packages/client-app/internal_packages/composer/lib/composer-header.jsx @@ -326,7 +326,7 @@ export default class ComposerHeader extends React.Component { return (
) } @@ -365,7 +365,7 @@ export default class ComposerView extends React.Component { exposedProps={{ draft: this.props.draft, threadId: this.props.draft.threadId, - draftClientId: this.props.draft.clientId, + headerMessageId: this.props.draft.headerMessageId, session: this.props.session, }} /> @@ -415,7 +415,7 @@ export default class ComposerView extends React.Component { ]} exposedProps={{ draft: this.props.draft, - draftClientId: this.props.draft.clientId, + headerMessageId: this.props.draft.headerMessageId, session: this.props.session, isValidDraft: this._isValidDraft, }} @@ -510,7 +510,7 @@ export default class ComposerView extends React.Component { // called from onDrop and onFilePaste - assume images should be inline Actions.addAttachment({ filePath: filePath, - messageClientId: this.props.draft.clientId, + headerMessageId: this.props.draft.headerMessageId, onUploadCreated: (upload) => { if (Utils.shouldDisplayAsImage(upload)) { const {draft, session} = this.props; @@ -522,7 +522,7 @@ export default class ComposerView extends React.Component { session.changes.add({uploads}) Actions.insertAttachmentIntoDraft({ - draftClientId: draft.clientId, + headerMessageId: draft.headerMessageId, uploadId: matchingUpload.id, }); } @@ -541,7 +541,7 @@ export default class ComposerView extends React.Component { // immediately and synchronously updated as soon as this function // fires. Since `setState` is asynchronous, if we used that as our only // check, then we might get a false reading. - if (DraftStore.isSendingDraft(this.props.draft.clientId)) { + if (DraftStore.isSendingDraft(this.props.draft.headerMessageId)) { return false; } @@ -579,11 +579,11 @@ export default class ComposerView extends React.Component { } _onDestroyDraft = () => { - Actions.destroyDraft(this.props.draft.clientId); + Actions.destroyDraft(this.props.draft.headerMessageId); } _onSelectAttachment = () => { - Actions.selectAttachment({messageClientId: this.props.draft.clientId}); + Actions.selectAttachment({headerMessageId: this.props.draft.headerMessageId}); } render() { diff --git a/packages/client-app/internal_packages/composer/lib/image-upload-composer-extension.es6 b/packages/client-app/internal_packages/composer/lib/image-upload-composer-extension.es6 index a3179ae07..99de16728 100644 --- a/packages/client-app/internal_packages/composer/lib/image-upload-composer-extension.es6 +++ b/packages/client-app/internal_packages/composer/lib/image-upload-composer-extension.es6 @@ -24,7 +24,7 @@ export default class ImageUploadComposerExtension extends ComposerExtension { } static _onInsertAttachmentIntoDraft({editor, actionArg}) { - if (editor.draftClientId === actionArg.draftClientId) { return } + if (editor.headerMessageId === actionArg.headerMessageId) { return } editor.insertCustomComponent("InlineImageUploadContainer", { className: `inline-container-${actionArg.uploadId}`, diff --git a/packages/client-app/internal_packages/composer/lib/main.jsx b/packages/client-app/internal_packages/composer/lib/main.jsx index 5865fd7c6..db4b18ec2 100644 --- a/packages/client-app/internal_packages/composer/lib/main.jsx +++ b/packages/client-app/internal_packages/composer/lib/main.jsx @@ -29,12 +29,12 @@ class ComposerWithWindowProps extends React.Component { // We'll now always have windowProps by the time we construct this. const windowProps = NylasEnv.getWindowProps(); - const {draftJSON, draftClientId} = windowProps; + const {draftJSON, headerMessageId} = windowProps; if (!draftJSON) { throw new Error("Initialize popout composer windows with valid draftJSON") } const draft = new Message().fromJSON(draftJSON); - DraftStore._createSession(draftClientId, draft); + DraftStore._createSession(headerMessageId, draft); this.state = windowProps } @@ -74,7 +74,7 @@ class ComposerWithWindowProps extends React.Component { ); diff --git a/packages/client-app/internal_packages/composer/spec/composer-header-actions-spec.cjsx b/packages/client-app/internal_packages/composer/spec/composer-header-actions-spec.cjsx index 1cc97ef13..41a6961c7 100644 --- a/packages/client-app/internal_packages/composer/spec/composer-header-actions-spec.cjsx +++ b/packages/client-app/internal_packages/composer/spec/composer-header-actions-spec.cjsx @@ -10,7 +10,7 @@ describe "ComposerHeaderActions", -> @onShowAndFocusField = jasmine.createSpy("onShowAndFocusField") props.onShowAndFocusField = @onShowAndFocusField props.enabledFields ?= [] - props.draftClientId = 'a' + props.headerMessageId = 'a' @component = ReactTestUtils.renderIntoDocument( ) diff --git a/packages/client-app/internal_packages/draft-list/lib/draft-list-store.coffee b/packages/client-app/internal_packages/draft-list/lib/draft-list-store.coffee index bd26108d9..883e84330 100644 --- a/packages/client-app/internal_packages/draft-list/lib/draft-list-store.coffee +++ b/packages/client-app/internal_packages/draft-list/lib/draft-list-store.coffee @@ -58,7 +58,7 @@ class DraftListStore extends NylasStore mailboxPerspective.accountIds.forEach (aid) => OutboxStore.itemsForAccount(aid).forEach (task) => - draft = resultSet.modelWithId(task.draftClientId) + draft = resultSet.modelWithId(task.headerMessageId) if draft draft = draft.clone() draft.uploadTaskId = task.id diff --git a/packages/client-app/internal_packages/draft-list/lib/draft-list.cjsx b/packages/client-app/internal_packages/draft-list/lib/draft-list.cjsx index ad02f0f1c..ddf37fd52 100644 --- a/packages/client-app/internal_packages/draft-list/lib/draft-list.cjsx +++ b/packages/client-app/internal_packages/draft-list/lib/draft-list.cjsx @@ -41,12 +41,12 @@ class DraftList extends React.Component _onDoubleClick: (draft) => unless draft.uploadTaskId - Actions.composePopoutDraft(draft.id) + Actions.composePopoutDraft(draft.headerMessageId) # Additional Commands _onRemoveFromView: => drafts = DraftListStore.dataSource().selection.items() - Actions.destroyDraft(draft.id) for draft in drafts + Actions.destroyDraft(draft.headerMessageId) for draft in drafts module.exports = DraftList diff --git a/packages/client-app/internal_packages/draft-list/lib/draft-toolbar-buttons.cjsx b/packages/client-app/internal_packages/draft-list/lib/draft-toolbar-buttons.cjsx index be98010ae..c6fc6ceef 100644 --- a/packages/client-app/internal_packages/draft-list/lib/draft-toolbar-buttons.cjsx +++ b/packages/client-app/internal_packages/draft-list/lib/draft-toolbar-buttons.cjsx @@ -19,7 +19,7 @@ class DraftDeleteButton extends React.Component _destroySelected: => for item in @props.selection.items() - Actions.destroyDraft(item.id) + Actions.destroyDraft(item.headerMessageId) @props.selection.clear() return diff --git a/packages/client-app/internal_packages/message-list/lib/message-item-container.cjsx b/packages/client-app/internal_packages/message-list/lib/message-item-container.cjsx index 15ef0a7f6..099f00265 100644 --- a/packages/client-app/internal_packages/message-list/lib/message-item-container.cjsx +++ b/packages/client-app/internal_packages/message-list/lib/message-item-container.cjsx @@ -64,11 +64,11 @@ class MessageItemContainer extends React.Component _renderComposer: => Composer = ComponentRegistry.findComponentsMatching(role: 'Composer')[0] if (!Composer) - return + return No Composer Component Present - if draftId is @props.message.id + _onSendingStateChanged: (headerMessageId) => + if headerMessageId is @props.message.headerMessageId @setState(@_getStateFromStores()) _getStateFromStores: (props = @props) -> diff --git a/packages/client-app/internal_packages/message-list/spec/message-item-container-spec.cjsx b/packages/client-app/internal_packages/message-list/spec/message-item-container-spec.cjsx index 8badf1da5..68908ffc9 100644 --- a/packages/client-app/internal_packages/message-list/spec/message-item-container-spec.cjsx +++ b/packages/client-app/internal_packages/message-list/spec/message-item-container-spec.cjsx @@ -37,7 +37,7 @@ xdescribe 'MessageItemContainer', -> ReactTestUtils.renderIntoDocument( + headerMessageId={testClientId} /> ) it "shows composer if it's a draft", -> diff --git a/packages/client-app/internal_packages/message-list/spec/message-list-spec.cjsx b/packages/client-app/internal_packages/message-list/spec/message-list-spec.cjsx index bd97b6f64..c6834b225 100644 --- a/packages/client-app/internal_packages/message-list/spec/message-list-spec.cjsx +++ b/packages/client-app/internal_packages/message-list/spec/message-list-spec.cjsx @@ -50,7 +50,7 @@ m1 = (new Message).fromJSON({ "draft" : false "files" : [], "unread" : false, - "object" : "message", + "__constructorName": "Message", "snippet" : "snippet one...", "subject" : "Subject One", "thread_id" : "thread_12345", @@ -67,7 +67,7 @@ m2 = (new Message).fromJSON({ "draft" : false "files" : [], "unread" : false, - "object" : "message", + "__constructorName": "Message", "snippet" : "snippet Two...", "subject" : "Subject Two", "thread_id" : "thread_12345", @@ -84,7 +84,7 @@ m3 = (new Message).fromJSON({ "draft" : false "files" : [], "unread" : false, - "object" : "message", + "__constructorName": "Message", "snippet" : "snippet Three...", "subject" : "Subject Three", "thread_id" : "thread_12345", @@ -101,7 +101,7 @@ m4 = (new Message).fromJSON({ "draft" : false "files" : [], "unread" : false, - "object" : "message", + "__constructorName": "Message", "snippet" : "snippet Four...", "subject" : "Subject Four", "thread_id" : "thread_12345", @@ -118,7 +118,7 @@ m5 = (new Message).fromJSON({ "draft" : false "files" : [], "unread" : false, - "object" : "message", + "__constructorName": "Message", "snippet" : "snippet Five...", "subject" : "Subject Five", "thread_id" : "thread_12345", @@ -137,7 +137,7 @@ draftMessages = [ "draft" : true "files" : [], "unread" : false, - "object" : "draft", + "__constructorName": "Message", "snippet" : "draft snippet one...", "subject" : "Draft One", "thread_id" : "thread_12345", diff --git a/packages/client-app/internal_packages/thread-snooze/lib/snooze-store.jsx b/packages/client-app/internal_packages/thread-snooze/lib/snooze-store.jsx index 05c479d11..d90b670b2 100644 --- a/packages/client-app/internal_packages/thread-snooze/lib/snooze-store.jsx +++ b/packages/client-app/internal_packages/thread-snooze/lib/snooze-store.jsx @@ -90,7 +90,7 @@ class SnoozeStore { // Get messages for those threads and metadata for those. DatabaseStore.findAll(Message, {threadId: update.threads.map(t => t.id)}).then((messages) => { for (const message of messages) { - const header = message.messageIdHeader; + const header = message.headerMessageId; const stableId = message.id; Actions.setMetadata(message, this.pluginId, {expiration: snoozeDate, header, stableId, snoozeCategoryId, returnCategoryId}) diff --git a/packages/client-app/script/grunt b/packages/client-app/script/grunt index ddf19bd51..ab78dcb67 100755 --- a/packages/client-app/script/grunt +++ b/packages/client-app/script/grunt @@ -4,7 +4,7 @@ var fs = require('fs'); var path = require('path'); // node build/node_modules/.bin/grunt "$@" -var gruntPath = path.resolve(__dirname, '..', 'build', 'node_modules', '.bin', 'grunt') + (process.platform === 'win32' ? '.cmd' : ''); +var gruntPath = path.resolve(__dirname, '..', 'node_modules', '.bin', 'grunt') + (process.platform === 'win32' ? '.cmd' : ''); if (!fs.existsSync(gruntPath)) { console.error('Grunt command does not exist at: ' + gruntPath); diff --git a/packages/client-app/spec/action-bridge-spec.coffee b/packages/client-app/spec/action-bridge-spec.coffee index c962efa8f..20e2df9ff 100644 --- a/packages/client-app/spec/action-bridge-spec.coffee +++ b/packages/client-app/spec/action-bridge-spec.coffee @@ -12,14 +12,14 @@ ipc = describe "ActionBridge", -> - describe "in the work window", -> + describe "in the womainrk window", -> beforeEach -> spyOn(NylasEnv, "getWindowType").andReturn "default" spyOn(NylasEnv, "isWorkWindow").andReturn true @bridge = new ActionBridge(ipc) - it "should have the role Role.WORK", -> - expect(@bridge.role).toBe(ActionBridge.Role.WORK) + it "should have the role Role.MAIN", -> + expect(@bridge.role).toBe(ActionBridge.Role.MAIN) it "should rebroadcast global actions", -> spyOn(@bridge, 'onRebroadcast') @@ -87,12 +87,12 @@ describe "ActionBridge", -> @bridge.onRebroadcast(ActionBridge.TargetWindows.ALL, 'onNewMailDeltas', [{oldModel: '1', newModel: 2}]) expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-all', 'popout', 'onNewMailDeltas', '[{"oldModel":"1","newModel":2}]') - describe "when called with TargetWindows.WORK", -> + describe "when called with TargetWindows.MAIN", -> it "should broadcast the action over IPC to the main window only", -> spyOn(ipc, 'send') Actions.onNewMailDeltas.firing = false - @bridge.onRebroadcast(ActionBridge.TargetWindows.WORK, 'onNewMailDeltas', [{oldModel: '1', newModel: 2}]) - expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-work', 'popout', 'onNewMailDeltas', '[{"oldModel":"1","newModel":2}]') + @bridge.onRebroadcast(ActionBridge.TargetWindows.MAIN, 'onNewMailDeltas', [{oldModel: '1', newModel: 2}]) + expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-default', 'popout', 'onNewMailDeltas', '[{"oldModel":"1","newModel":2}]') it "should not do anything if the current invocation of the Action was triggered by itself", -> spyOn(ipc, 'send') diff --git a/packages/client-app/spec/models/event-spec.coffee b/packages/client-app/spec/models/event-spec.coffee index 40656d3ba..b6f9d991d 100644 --- a/packages/client-app/spec/models/event-spec.coffee +++ b/packages/client-app/spec/models/event-spec.coffee @@ -3,7 +3,7 @@ AccountStore = require("../../src/flux/stores/account-store").default json_event = { - "object": "event", + "__constructorName": "Event", "id": "4ee4xbnx7pxdb9g7c2f8ncyto", "calendar_id": "ci0k1wfyv533ccgox4t7uri4h", "account_id": "14e5bn96uizyuhidhcw5rfrb0", diff --git a/packages/client-app/spec/models/thread-spec.coffee b/packages/client-app/spec/models/thread-spec.coffee index 50f1d7590..b1641e746 100644 --- a/packages/client-app/spec/models/thread-spec.coffee +++ b/packages/client-app/spec/models/thread-spec.coffee @@ -9,7 +9,7 @@ describe 'Thread', -> describe 'serialization performance', -> xit '1,000,000 iterations', -> iterations = 0 - json = '[{"client_id":"local-76c370af-65de","server_id":"f0vkowp7zxt7djue7ifylb940","object":"thread","account_id":"1r6w6qiq3sb0o9fiwin6v87dd","snippet":"http://itunestandc.tumblr.com/tagged/itunes-terms-and-conditions/chrono _______________________________________________ http://www.macgroup.com/mailman/listinfo/smartfriends-chat","subject":"iTunes Terms And Conditions as you\'ve never seen them before","unread":true,"starred":false,"version":1,"folders":[],"labels":[{"server_id":"8cf4fn20k9pjjhjawrv3xrxo0","name":"all","display_name":"All Mail","id":"8cf4fn20k9pjjhjawrv3xrxo0"},{"server_id":"f1lq8faw8vv06m67y8f3xdf84","name":"inbox","display_name":"Inbox","id":"f1lq8faw8vv06m67y8f3xdf84"}],"participants":[{"name":"Andrew Stadler","email":"stadler@gmail.com","thirdPartyData":{}},{"name":"Smart Friends™ Chat","email":"smartfriends-chat@macgroup.com","thirdPartyData":{}}],"has_attachments":false,"last_message_received_timestamp":1446600615,"id":"f0vkowp7zxt7djue7ifylb940"}]' + json = '[{"client_id":"local-76c370af-65de","server_id":"f0vkowp7zxt7djue7ifylb940","__constructorName":"Thread","account_id":"1r6w6qiq3sb0o9fiwin6v87dd","snippet":"http://itunestandc.tumblr.com/tagged/itunes-terms-and-conditions/chrono _______________________________________________ http://www.macgroup.com/mailman/listinfo/smartfriends-chat","subject":"iTunes Terms And Conditions as you\'ve never seen them before","unread":true,"starred":false,"version":1,"folders":[],"labels":[{"server_id":"8cf4fn20k9pjjhjawrv3xrxo0","name":"all","display_name":"All Mail","id":"8cf4fn20k9pjjhjawrv3xrxo0"},{"server_id":"f1lq8faw8vv06m67y8f3xdf84","name":"inbox","display_name":"Inbox","id":"f1lq8faw8vv06m67y8f3xdf84"}],"participants":[{"name":"Andrew Stadler","email":"stadler@gmail.com","thirdPartyData":{}},{"name":"Smart Friends™ Chat","email":"smartfriends-chat@macgroup.com","thirdPartyData":{}}],"has_attachments":false,"last_message_received_timestamp":1446600615,"id":"f0vkowp7zxt7djue7ifylb940"}]' start = Date.now() while iterations < 1000000 if _.isString(json) diff --git a/packages/client-app/spec/stores/account-store-spec.coffee b/packages/client-app/spec/stores/account-store-spec.coffee index e6ec4358a..24eb93c65 100644 --- a/packages/client-app/spec/stores/account-store-spec.coffee +++ b/packages/client-app/spec/stores/account-store-spec.coffee @@ -26,7 +26,7 @@ describe "AccountStore", -> "client_id" : 'local-4f9d476a-c173', "server_id" : 'A', "email_address":"bengotow@gmail.com", - "object":"account" + "__constructorName":"Account" "organization_unit": "label" "aliases": ["Alias "] },{ @@ -34,7 +34,7 @@ describe "AccountStore", -> "client_id" : 'local-4f9d476a-c175', "server_id" : 'B', "email_address":"ben@nylas.com", - "object":"account" + "__constructorName":"Account" "organization_unit": "label" }] @@ -95,7 +95,7 @@ describe "AccountStore", -> "server_id" : 'B', "email_address":"ben@nylas.com", "provider":"gmail", - "object":"account", + "__constructorName":"Account", "organization_unit": "label", @instance = new @constructor spyOn(NylasEnv.config, "set") @@ -127,7 +127,7 @@ describe "AccountStore", -> "server_id" : 'B', "email_address":"ben@nylas.com", "provider":"gmail", - "object":"account" + "__constructorName":"Account" "organization_unit": "label" @spyOnConfig() @instance = new @constructor @@ -144,7 +144,7 @@ describe "AccountStore", -> "server_id" : 'NEVER SEEN BEFORE', "email_address":"ben@nylas.com", "provider":"gmail", - "object":"account" + "__constructorName":"Account" "organization_unit": "label" @spyOnConfig() @instance = new @constructor diff --git a/packages/client-app/spec/stores/file-upload-store-spec.coffee b/packages/client-app/spec/stores/file-upload-store-spec.coffee index 4bbb233c3..4ae8af9c9 100644 --- a/packages/client-app/spec/stores/file-upload-store-spec.coffee +++ b/packages/client-app/spec/stores/file-upload-store-spec.coffee @@ -132,9 +132,9 @@ xdescribe 'FileUploadStore', -> describe "when a draft is sent", -> it "should delete its uploads directory", -> - spyOn(FileUploadStore, '_deleteUploadsForClientId') + spyOn(FileUploadStore, '_deleteUploadsForId') Actions.ensureMessageInSentSuccess({messageClientId: '123'}) - expect(FileUploadStore._deleteUploadsForClientId).toHaveBeenCalledWith('123') + expect(FileUploadStore._deleteUploadsForId).toHaveBeenCalledWith('123') describe '_getFileStats', -> it 'returns the correct stats', -> diff --git a/packages/client-app/spec/stores/undo-redo-store-spec.es6 b/packages/client-app/spec/stores/undo-redo-store-spec.es6 index 032b47fd7..b2d381689 100644 --- a/packages/client-app/spec/stores/undo-redo-store-spec.es6 +++ b/packages/client-app/spec/stores/undo-redo-store-spec.es6 @@ -23,7 +23,6 @@ describe("UndoRedoStore", function undoRedoStoreSpec() { UndoRedoStore._undo = [] UndoRedoStore._redo = [] spyOn(UndoRedoStore, "trigger") - spyOn(Actions, "undoTaskId") spyOn(Actions, "queueTask").andCallFake((...args) => { UndoRedoStore._onQueue(...args) }); @@ -83,9 +82,9 @@ describe("UndoRedoStore", function undoRedoStoreSpec() { it("runs undoTask on each group of undo tasks", () => { UndoRedoStore._undo = [[this.t3], [this.t1, this.t2]] UndoRedoStore.undo() - expect(Actions.undoTaskId.calls.length).toBe(2) - expect(Actions.undoTaskId.calls[0].args[0]).toBe("t1") - expect(Actions.undoTaskId.calls[1].args[0]).toBe("t2") + expect(Actions.queueTask.calls.length).toBe(2) + expect(Actions.queueTask.calls[0].args[0]).toBe("t1") + expect(Actions.queueTask.calls[1].args[0]).toBe("t2") expect(UndoRedoStore._undo).toEqual([[this.t3]]) }); diff --git a/packages/client-app/spec/tasks/base-draft-task-spec.es6 b/packages/client-app/spec/tasks/base-draft-task-spec.es6 index 7c3b6b2ff..3ad7780e0 100644 --- a/packages/client-app/spec/tasks/base-draft-task-spec.es6 +++ b/packages/client-app/spec/tasks/base-draft-task-spec.es6 @@ -68,7 +68,7 @@ xdescribe('BaseDraftTask', function baseDraftTask() { badTask.performLocal().then(() => { throw new Error("Shouldn't succeed") }).catch((err) => { - expect(err.message).toBe("Attempt to call BaseDraftTask.performLocal without a draftClientId") + expect(err.message).toBe("Attempt to call BaseDraftTask.performLocal without a headerMessageId") }); }); }); diff --git a/packages/client-app/src/browser/application.es6 b/packages/client-app/src/browser/application.es6 index 116d7b299..8ba9a4375 100644 --- a/packages/client-app/src/browser/application.es6 +++ b/packages/client-app/src/browser/application.es6 @@ -456,7 +456,7 @@ export default class Application extends EventEmitter { ipcMain.on('ensure-worker-window', () => { // TODO BG - // this.windowManager.ensureWindow(WindowManager.WORK_WINDOW) + // this.windowManager.ensureWindow(WindowManager.MAIN_WINDOW) }) ipcMain.on('inline-style-parse', (event, {html, key}) => { @@ -523,16 +523,15 @@ export default class Application extends EventEmitter { this.windowManager.sendToAllWindows('action-bridge-message', {except: win}, ...args) }); - ipcMain.on('action-bridge-rebroadcast-to-work', (event, ...args) => { - // TODO BG - // const workWindow = this.windowManager.get(WindowManager.WORK_WINDOW) - // if (!workWindow || !workWindow.browserWindow.webContents) { - // return; - // } - // if (BrowserWindow.fromWebContents(event.sender) === workWindow) { - // return; - // } - // workWindow.browserWindow.webContents.send('action-bridge-message', ...args); + ipcMain.on('action-bridge-rebroadcast-to-default', (event, ...args) => { + const mainWindow = this.windowManager.get(WindowManager.MAIN_WINDOW) + if (!mainWindow || !mainWindow.browserWindow.webContents) { + return; + } + if (BrowserWindow.fromWebContents(event.sender) === mainWindow) { + return; + } + mainWindow.browserWindow.webContents.send('action-bridge-message', ...args); }); ipcMain.on('write-text-to-selection-clipboard', (event, selectedText) => { @@ -542,7 +541,6 @@ export default class Application extends EventEmitter { ipcMain.on('account-setup-successful', () => { this.windowManager.ensureWindow(WindowManager.MAIN_WINDOW); - this.windowManager.ensureWindow(WindowManager.WORK_WINDOW); const onboarding = this.windowManager.get(WindowManager.ONBOARDING_WINDOW); if (onboarding) { onboarding.close(); diff --git a/packages/client-app/src/components/injected-component.cjsx b/packages/client-app/src/components/injected-component.cjsx index 176177ec3..f6d3af1c2 100644 --- a/packages/client-app/src/components/injected-component.cjsx +++ b/packages/client-app/src/components/injected-component.cjsx @@ -14,11 +14,11 @@ components inside of your React render method. Rather than explicitly render a component, such as a ``, you can use InjectedComponent: ```coffee - + ``` InjectedComponent will look up the component registered with that role in the -{ComponentRegistry} and render it, passing the exposedProps (`draftClientId={123}`) along. +{ComponentRegistry} and render it, passing the exposedProps (`headerMessageId={123}`) along. InjectedComponent monitors the ComponentRegistry for changes. If a new component is registered that matches the descriptor you provide, InjectedComponent will refresh. diff --git a/packages/client-app/src/decorators/inflates-draft-client-id.jsx b/packages/client-app/src/decorators/inflates-draft-client-id.jsx index a2b9e923f..5e02f3715 100644 --- a/packages/client-app/src/decorators/inflates-draft-client-id.jsx +++ b/packages/client-app/src/decorators/inflates-draft-client-id.jsx @@ -8,7 +8,7 @@ function InflatesDraftClientId(ComposedComponent) { static displayName = ComposedComponent.displayName; static propTypes = { - draftClientId: React.PropTypes.string, + headerMessageId: React.PropTypes.string, onDraftReady: React.PropTypes.func, } @@ -28,7 +28,7 @@ function InflatesDraftClientId(ComposedComponent) { componentDidMount() { this._mounted = true; - this._prepareForDraft(this.props.draftClientId); + this._prepareForDraft(this.props.headerMessageId); } componentWillUnmount() { @@ -38,19 +38,19 @@ function InflatesDraftClientId(ComposedComponent) { } componentWillReceiveProps(newProps) { - if (newProps.draftClientId !== this.props.draftClientId) { + if (newProps.headerMessageId !== this.props.headerMessageId) { this._teardownForDraft(); - this._prepareForDraft(newProps.draftClientId); + this._prepareForDraft(newProps.headerMessageId); } } - _prepareForDraft(draftClientId) { - if (!draftClientId) { + _prepareForDraft(headerMessageId) { + if (!headerMessageId) { return; } - DraftStore.sessionForClientId(draftClientId).then((session) => { + DraftStore.sessionForClientId(headerMessageId).then((session) => { const shouldSetState = () => - this._mounted && session.draftClientId === this.props.draftClientId + this._mounted && session.headerMessageId === this.props.headerMessageId if (!shouldSetState()) { return; } this._sessionUnlisten = session.listen(() => { @@ -80,7 +80,7 @@ function InflatesDraftClientId(ComposedComponent) { return; } if (this.state.draft.pristine) { - Actions.destroyDraft(this.props.draftClientId); + Actions.destroyDraft(this.props.headerMessageId); } } diff --git a/packages/client-app/src/flux/action-bridge-cpp.es6 b/packages/client-app/src/flux/action-bridge-cpp.es6 index 60be9563e..45ffa9206 100644 --- a/packages/client-app/src/flux/action-bridge-cpp.es6 +++ b/packages/client-app/src/flux/action-bridge-cpp.es6 @@ -2,7 +2,8 @@ import net from 'net'; import fs from 'fs'; import DatabaseStore from './stores/database-store'; import DatabaseChangeRecord from './stores/database-change-record'; - +import DatabaseObjectRegistry from '../registries/database-object-registry'; +import Actions from './actions'; import Utils from './models/utils'; class ActionBridgeCPP { @@ -13,6 +14,13 @@ class ActionBridgeCPP { return; } + Actions.queueTask.listen(this.onQueueTask, this); + Actions.queueTasks.listen((tasks) => { + if (!tasks || !tasks.length) { return; } + for (const task of tasks) { this.onQueueTask(task); } + }); + Actions.dequeueTask.listen(this.onDequeueTask, this); + try { fs.unlinkSync('/tmp/cmail.sock'); } catch (err) { @@ -41,7 +49,7 @@ class ActionBridgeCPP { }); }); - unixServer.listen('/tmp/cmail.sock', () => { + unixServer.listen('/tmp/cmail.sock', () => { console.log('server bound'); }); @@ -54,6 +62,29 @@ class ActionBridgeCPP { process.on('SIGINT', shutdown); } + onQueueTask(task) { + // if (!(task instanceof Task)) { + // console.log(task); + // throw new Error("You must queue a `Task` instance. Be sure you have the task registered with the DatabaseObjectRegistry. If this is a task for a custom plugin, you must export a `taskConstructors` array with your `Task` constructors in it. You must all subclass the base Nylas `Task`."); + // } + if (!DatabaseObjectRegistry.isInRegistry(task.constructor.name)) { + console.log(task); + throw new Error("You must queue a `Task` instance which is registred with the DatabaseObjectRegistry") + } + if (!task.id) { + console.log(task); + throw new Error("Tasks must have an ID prior to being queued. Check that your Task constructor is calling `super`"); + } + task.sequentialId = ++this._currentSequentialId; + task.status = 'local'; + + this.onTellClients({type: 'task-queued', task: task}); + } + + onDequeueTask() { // task + throw new Error("Unimplemented"); + } + onIncomingMessage(message) { this._readBuffer += message; const msgs = this._readBuffer.split('\n'); diff --git a/packages/client-app/src/flux/action-bridge.es6 b/packages/client-app/src/flux/action-bridge.es6 index 5d7047a46..690bd4c78 100644 --- a/packages/client-app/src/flux/action-bridge.es6 +++ b/packages/client-app/src/flux/action-bridge.es6 @@ -6,13 +6,13 @@ import DatabaseChangeRecord from './stores/database-change-record'; import Utils from './models/utils'; const Role = { - WORK: 'work', + MAIN: 'default', SECONDARY: 'secondary', }; const TargetWindows = { ALL: 'all', - WORK: 'work', + MAIN: 'default', }; const Message = { @@ -49,7 +49,7 @@ class ActionBridge { this.ipc = ipc; this.ipcLastSendTime = null; this.initiatorId = NylasEnv.getWindowType(); - this.role = NylasEnv.isWorkWindow() ? Role.WORK : Role.SECONDARY; + this.role = NylasEnv.isMainWindow() ? Role.MAIN : Role.SECONDARY; NylasEnv.onBeforeUnload(this.onBeforeUnload); @@ -76,11 +76,11 @@ class ActionBridge { }; DatabaseStore.listen(databaseCallback, this); - if (this.role !== Role.WORK) { + if (this.role !== Role.MAIN) { // Observe all mainWindow actions fired in this window and re-broadcast // them to other windows so the central application stores can take action - Actions.workWindowActions.forEach(name => { - const callback = (...args) => this.onRebroadcast(TargetWindows.WORK, name, args); + Actions.mainWindowActions.forEach(name => { + const callback = (...args) => this.onRebroadcast(TargetWindows.MAIN, name, args); return Actions[name].listen(callback, this); }); } @@ -157,7 +157,7 @@ class ActionBridge { const params = []; args.forEach((arg) => { if (arg instanceof Function) { - throw new Error("ActionBridge cannot forward action argument of type `function` to work window."); + throw new Error("ActionBridge cannot forward action argument of type `function` to another window."); } return params.push(arg); }); diff --git a/packages/client-app/src/flux/actions.es6 b/packages/client-app/src/flux/actions.es6 index 164866bd9..cee9d8bf1 100644 --- a/packages/client-app/src/flux/actions.es6 +++ b/packages/client-app/src/flux/actions.es6 @@ -2,7 +2,7 @@ import Reflux from 'reflux'; const ActionScopeWindow = 'window'; const ActionScopeGlobal = 'global'; -const ActionScopeWorkWindow = 'work'; +const ActionScopeMainWindow = 'main'; /* Public: In the Flux {Architecture.md}, almost every user action @@ -81,36 +81,33 @@ class Actions { /* Public: Queue a {Task} object to the {TaskQueue}. - *Scope: Work Window* + *Scope: Main Window* */ - static queueTask = ActionScopeWorkWindow; + static queueTask = ActionScopeMainWindow; /* Public: Queue multiple {Task} objects to the {TaskQueue}, which should be undone as a single user action. - *Scope: Work Window* + *Scope: Main Window* */ - static queueTasks = ActionScopeWorkWindow; - - static undoTaskId = ActionScopeWorkWindow; - + static queueTasks = ActionScopeMainWindow; /* Public: Dequeue all {Task}s from the {TaskQueue}. Use with care. - *Scope: Work Window* + *Scope: Main Window* */ - static dequeueTask = ActionScopeWorkWindow; + static dequeueTask = ActionScopeMainWindow; /* Public: Dequeue a {Task} matching the description provided. - *Scope: Work Window* + *Scope: Main Window* */ - static longPollReceivedRawDeltas = ActionScopeWorkWindow; - static longPollProcessedDeltas = ActionScopeWorkWindow; - static willMakeAPIRequest = ActionScopeWorkWindow; - static didMakeAPIRequest = ActionScopeWorkWindow; + static longPollReceivedRawDeltas = ActionScopeMainWindow; + static longPollProcessedDeltas = ActionScopeMainWindow; + static willMakeAPIRequest = ActionScopeMainWindow; + static didMakeAPIRequest = ActionScopeMainWindow; static checkOnlineStatus = ActionScopeWindow; @@ -456,7 +453,7 @@ class Actions { ``` Actions.removeFile file: fileObject - messageClientId: draftClientId + headerMessageId: headerMessageId ``` */ static removeFile = ActionScopeWindow; @@ -510,7 +507,7 @@ class Actions { /* Public: Publish a user event to any analytics services linked to N1. */ - static recordUserEvent = ActionScopeWorkWindow; + static recordUserEvent = ActionScopeMainWindow; static addMailRule = ActionScopeWindow; static reorderMailRule = ActionScopeWindow; @@ -570,14 +567,14 @@ const create = (obj, name, scope) => { const scopes = { window: [], global: [], - work: [], + main: [], }; for (const name of Object.getOwnPropertyNames(Actions)) { if (name === 'length' || name === 'name' || name === 'arguments' || name === 'caller' || name === 'prototype') { continue; } - if (Actions[name] !== 'window' && Actions[name] !== 'global' && Actions[name] !== 'work') { + if (Actions[name] !== 'window' && Actions[name] !== 'global' && Actions[name] !== 'main') { continue; } const scope = Actions[name]; @@ -586,7 +583,7 @@ for (const name of Object.getOwnPropertyNames(Actions)) { } Actions.windowActions = scopes.window; -Actions.workWindowActions = scopes.work; +Actions.mainWindowActions = scopes.main; Actions.globalActions = scopes.global; export default Actions; diff --git a/packages/client-app/src/flux/models/message.es6 b/packages/client-app/src/flux/models/message.es6 index b1503eca1..d5cdf4c7f 100644 --- a/packages/client-app/src/flux/models/message.es6 +++ b/packages/client-app/src/flux/models/message.es6 @@ -139,8 +139,9 @@ export default class Message extends ModelWithMetadata { modelKey: 'threadId', }), - messageIdHeader: Attributes.String({ - modelKey: 'messageIdHeader', + headerMessageId: Attributes.String({ + queryable: true, + modelKey: 'headerMessageId', }), subject: Attributes.String({ diff --git a/packages/client-app/src/flux/models/model.coffee b/packages/client-app/src/flux/models/model.coffee index bb2bf2f0a..16c63fa0c 100644 --- a/packages/client-app/src/flux/models/model.coffee +++ b/packages/client-app/src/flux/models/model.coffee @@ -27,9 +27,6 @@ class Model queryable: true modelKey: 'id' - 'object': Attributes.String - modelKey: 'object' - 'accountId': Attributes.String queryable: true modelKey: 'accountId' @@ -96,6 +93,7 @@ class Model continue if attr instanceof Attributes.AttributeJoinedData and options.joined is false json[attr.jsonKey] = attr.toJSON(attrValue) json["id"] = @id + json["__constructorName"] = this.constructor.name json toString: -> diff --git a/packages/client-app/src/flux/nylas-api.es6 b/packages/client-app/src/flux/nylas-api.es6 index e8cb3d816..41707f520 100644 --- a/packages/client-app/src/flux/nylas-api.es6 +++ b/packages/client-app/src/flux/nylas-api.es6 @@ -1,4 +1,4 @@ -import {AccountStore} from 'nylas-exports' +import AccountStore from './stores/account-store' import NylasLongConnection from './nylas-long-connection' // A 0 code is when an error returns without a status code, like "ESOCKETTIMEDOUT" diff --git a/packages/client-app/src/flux/stores/database-store.es6 b/packages/client-app/src/flux/stores/database-store.es6 index 31869700e..7d1e2f63c 100644 --- a/packages/client-app/src/flux/stores/database-store.es6 +++ b/packages/client-app/src/flux/stores/database-store.es6 @@ -3,7 +3,6 @@ import path from 'path'; import createDebug from 'debug'; import childProcess from 'child_process'; import PromiseQueue from 'promise-queue'; -import {remote} from 'electron'; import LRU from "lru-cache"; import {ExponentialBackoffScheduler} from '../../backoff-schedulers'; @@ -111,21 +110,6 @@ class DatabaseStore extends NylasStore { this._emitter.emit('ready') } - // When 3rd party components register new models, we need to refresh the - // database schema to prepare those tables. This method may be called - // extremely frequently as new models are added when packages load. - refreshDatabaseSchema() { - if (!NylasEnv.isMainWindow()) { - return Promise.resolve(); - } - const app = remote.getGlobal('application'); - const phase = app.databasePhase(); - if (phase !== DatabasePhase.Setup) { - app.setDatabasePhase(DatabasePhase.Setup); - } - return this._asyncWaitForReady() - } - _prettyConsoleLog(qa) { let q = qa.replace(/%/g, '%%'); q = `color:black |||%c ${q}`; @@ -200,7 +184,7 @@ class DatabaseStore extends NylasStore { const msec = Date.now() - start; if (debugVerbose.enabled) { const q = `🔶 (${msec}ms) Background: ${query}`; - debugVerbose(StringUtils.trimTo(q)) + debugVerbose(trimTo(q)) } if (msec > 100) { @@ -253,7 +237,7 @@ class DatabaseStore extends NylasStore { const msec = Date.now() - start; if (debugVerbose.enabled) { const q = `(${msec}ms) ${query}`; - debugVerbose(StringUtils.trimTo(q)) + debugVerbose(trimTo(q)) } if (msec > 100) { diff --git a/packages/client-app/src/flux/stores/draft-editing-session.coffee b/packages/client-app/src/flux/stores/draft-editing-session.coffee index d6d309f30..f7c64c94e 100644 --- a/packages/client-app/src/flux/stores/draft-editing-session.coffee +++ b/packages/client-app/src/flux/stores/draft-editing-session.coffee @@ -102,7 +102,7 @@ class DraftEditingSession @include Publisher @include Listener - constructor: (@draftId, draft = null) -> + constructor: (@headerMessageId, draft = null) -> DraftStore ?= require('./draft-store').default @listenTo DraftStore, @_onDraftChanged @@ -137,9 +137,9 @@ class DraftEditingSession @_draftPristineBody prepare: -> - @_draftPromise ?= DatabaseStore.findBy(Message, id: @draftId).include(Message.attributes.body).then (draft) => + @_draftPromise ?= DatabaseStore.findBy(Message, headerMessageId: @headerMessageId).include(Message.attributes.body).then (draft) => return Promise.reject(new Error("Draft has been destroyed.")) if @_destroyed - return Promise.reject(new Error("Assertion Failure: Draft #{@draftId} not found.")) if not draft + return Promise.reject(new Error("Assertion Failure: Draft #{@headerMessageId} not found.")) if not draft return @_setDraft(draft) teardown: -> @@ -261,31 +261,18 @@ class DraftEditingSession # underneath us inMemoryDraft = @_draft - DatabaseStore.inTransaction (t) => - t.findBy(Message, id: inMemoryDraft.id).include(Message.attributes.body).then (draft) => - # This can happen if we get a "delete" delta, or something else - # strange happens. In this case, we'll use the @_draft we have in - # memory to apply the changes to. On the `persistModel` in the - # next line it will save the correct changes. The - # `SyncbackDraftTask` may then fail due to differing Ids not - # existing, but if this happens it'll 404 and recover gracefully - # by creating a new draft - draft ?= inMemoryDraft - updatedDraft = @changes.applyToModel(draft) - return t.persistModel(updatedDraft) - - .then => - return if noSyncback - # We have temporarily disabled the syncback of most drafts to user's mail - # providers, due to a number of issues in the sync-engine that we're still - # firefighting. - # - # For now, drafts are only synced when you choose "Send Later", and then - # once they have a serverId we sync them periodically here. - # - return unless @_draft.serverId - Actions.ensureDraftSynced(@draftId) - + DatabaseStore.findBy(Message, id: inMemoryDraft.id).include(Message.attributes.body).then (draft) => + # This can happen if we get a "delete" delta, or something else + # strange happens. In this case, we'll use the @_draft we have in + # memory to apply the changes to. On the `persistModel` in the + # next line it will save the correct changes. The + # `SyncbackDraftTask` may then fail due to differing Ids not + # existing, but if this happens it'll 404 and recover gracefully + # by creating a new draft + draft ?= inMemoryDraft + updatedDraft = @changes.applyToModel(draft) + console.log("Queueing SyncbackDraftTask") + Actions.queueTask(new SyncbackDraftTask(updatedDraft)) # Undo / Redo diff --git a/packages/client-app/src/flux/stores/draft-factory.coffee b/packages/client-app/src/flux/stores/draft-factory.coffee index 521e75308..ffc3de4b7 100644 --- a/packages/client-app/src/flux/stores/draft-factory.coffee +++ b/packages/client-app/src/flux/stores/draft-factory.coffee @@ -27,7 +27,11 @@ class DraftFactory Promise.resolve(new Message(_.extend({ body: '' subject: '' - id: Utils.generateTempId() + version: 0 + unread: false + starred: false + folderImapUID: 0 + headerMessageId: Utils.generateTempId() from: [account.defaultMe()] date: (new Date) draft: true diff --git a/packages/client-app/src/flux/stores/draft-store.es6 b/packages/client-app/src/flux/stores/draft-store.es6 index 0deb0d458..10c471f57 100644 --- a/packages/client-app/src/flux/stores/draft-store.es6 +++ b/packages/client-app/src/flux/stores/draft-store.es6 @@ -91,26 +91,26 @@ class DraftStore extends NylasStore { @param {String} clientId - The clientId of the draft. @returns {Promise} - Resolves to an {DraftEditingSession} for the draft once it has been prepared */ - sessionForClientId(clientId) { - if (!clientId) { + sessionForClientId(headerMessageId) { + if (!headerMessageId) { throw new Error("DraftStore::sessionForClientId requires a clientId"); } - if (this._draftSessions[clientId] == null) { - this._draftSessions[clientId] = this._createSession(clientId); + if (this._draftSessions[headerMessageId] == null) { + this._draftSessions[headerMessageId] = this._createSession(headerMessageId); } - return this._draftSessions[clientId].prepare(); + return this._draftSessions[headerMessageId].prepare(); } - // Public: Look up the sending state of the given draftClientId. + // Public: Look up the sending state of the given draft headerMessageId. // In popout windows the existance of the window is the sending state. - isSendingDraft(draftClientId) { - return this._draftsSending[draftClientId] || false; + isSendingDraft(headerMessageId) { + return this._draftsSending[headerMessageId] || false; } _doneWithSession(session) { session.teardown(); - delete this._draftSessions[session.draftClientId]; + delete this._draftSessions[session.headerMessageId]; } _cleanupAllSessions() { @@ -129,7 +129,7 @@ class DraftStore extends NylasStore { _.each(this._draftSessions, (session) => { const draft = session.draft() if (draft && draft.pristine) { - Actions.queueTask(new DestroyDraftTask(session.draftClientId)); + Actions.queueTask(new DestroyDraftTask(session.headerMessageId)); } else { promises.push(session.changes.commit()); } @@ -172,10 +172,10 @@ class DraftStore extends NylasStore { .then((draft) => { draft.body = `${body}\n\n${draft.body}` draft.pristine = false; - return DatabaseStore.inTransaction((t) => { - return t.persistModel(draft); - }) - .then(() => { + + const t = new SyncbackDraftTask(draft) + Actions.queueTask(t) + TaskQueue.waitForPerformLocal(t).then(() => { Actions.sendDraft(draft.clientId); }); }); @@ -249,17 +249,17 @@ class DraftStore extends NylasStore { // doesn't need to do a query for it a second from now when the composer wants it. this._createSession(draft.clientId, draft); - return DatabaseStore.inTransaction((t) => { - return t.persistModel(draft); - }) - .then(() => { + const task = new SyncbackDraftTask(draft); + Actions.queueTask(task) + + return TaskQueue.waitForPerformLocal(task).then(() => { if (popout) { - this._onPopoutDraftClientId(draft.clientId); + this._onPopoutDraftClientId(draft.headerMessageId); } else { - Actions.focusDraft({draftClientId: draft.clientId}); + Actions.focusDraft({headerMessageId: draft.headerMessageId}); } + return {headerMessageId: draft.headerMessageId, draft}; }) - .thenReturn({draftClientId: draft.clientId, draft}); } _createSession(clientId, draft) { @@ -276,18 +276,18 @@ class DraftStore extends NylasStore { _onPopoutBlankDraft = () => { Actions.recordUserEvent("Draft Created", {type: "new"}); return DraftFactory.createDraft().then((draft) => { - return this._finalizeAndPersistNewMessage(draft).then(({draftClientId}) => { - return this._onPopoutDraftClientId(draftClientId, {newDraft: true}); + return this._finalizeAndPersistNewMessage(draft).then(({headerMessageId}) => { + return this._onPopoutDraftClientId(headerMessageId, {newDraft: true}); }); }); } - _onPopoutDraftClientId = (draftClientId, options = {}) => { - if (draftClientId == null) { - throw new Error("DraftStore::onPopoutDraftId - You must provide a draftClientId"); + _onPopoutDraftClientId = (headerMessageId, options = {}) => { + if (headerMessageId == null) { + throw new Error("DraftStore::onPopoutDraftId - You must provide a headerMessageId"); } const title = options.newDraft ? "New Message" : "Message"; - return this.sessionForClientId(draftClientId).then((session) => { + return this.sessionForClientId(headerMessageId).then((session) => { return session.changes.commit().then(() => { const draftJSON = session.draft().toJSON(); // Since we pass a windowKey, if the popout composer draft already @@ -296,9 +296,9 @@ class DraftStore extends NylasStore { NylasEnv.newWindow({ title, hidden: true, // We manually show in ComposerWithWindowProps::onDraftReady - windowKey: `composer-${draftClientId}`, + windowKey: `composer-${headerMessageId}`, windowType: 'composer-preload', - windowProps: _.extend(options, {draftClientId, draftJSON}), + windowProps: _.extend(options, {headerMessageId, draftJSON}), }); }); }); @@ -318,27 +318,27 @@ class DraftStore extends NylasStore { return DraftFactory.createDraft().then((draft) => { return this._finalizeAndPersistNewMessage(draft); }) - .then(({draftClientId}) => { + .then(({headerMessageId}) => { let remaining = paths.length; const callback = () => { remaining -= 1; if (remaining === 0) { - this._onPopoutDraftClientId(draftClientId); + this._onPopoutDraftClientId(headerMessageId); } }; paths.forEach((path) => { Actions.addAttachment({ filePath: path, - messageClientId: draftClientId, + messageClientId: headerMessageId, onUploadCreated: callback, }); }) }); } - _onDestroyDraft = (draftClientId) => { - const session = this._draftSessions[draftClientId]; + _onDestroyDraft = (headerMessageId) => { + const session = this._draftSessions[headerMessageId]; // Immediately reset any pending changes so no saves occur if (session) { @@ -347,34 +347,34 @@ class DraftStore extends NylasStore { // Stop any pending tasks related ot the draft TaskQueue.queue().forEach((task) => { - if (task instanceof BaseDraftTask && task.draftClientId === draftClientId) { + if (task instanceof BaseDraftTask && task.headerMessageId === headerMessageId) { Actions.dequeueTask(task.id); } }) // Queue the task to destroy the draft - Actions.queueTask(new DestroyDraftTask(draftClientId)); + Actions.queueTask(new DestroyDraftTask(headerMessageId)); if (NylasEnv.isComposerWindow()) { NylasEnv.close(); } } - _onEnsureDraftSynced = (draftClientId) => { - return this.sessionForClientId(draftClientId).then((session) => { + _onEnsureDraftSynced = (headerMessageId) => { + return this.sessionForClientId(headerMessageId).then((session) => { return DraftHelpers.prepareDraftForSyncback(session) .then(() => { - Actions.queueTask(new SyncbackDraftTask(draftClientId)); + Actions.queueTask(new SyncbackDraftTask(headerMessageId)); }); }); } - _onSendDraft = (draftClientId, sendActionKey = DefaultSendActionKey) => { - this._draftsSending[draftClientId] = true; - return this.sessionForClientId(draftClientId).then((session) => { + _onSendDraft = (headerMessageId, sendActionKey = DefaultSendActionKey) => { + this._draftsSending[headerMessageId] = true; + return this.sessionForClientId(headerMessageId).then((session) => { return DraftHelpers.prepareDraftForSyncback(session) .then(() => { - Actions.queueTask(new PerformSendActionTask(draftClientId, sendActionKey)); + Actions.queueTask(new PerformSendActionTask(headerMessageId, sendActionKey)); this._doneWithSession(session); if (NylasEnv.config.get("core.sending.sounds")) { SoundRegistry.playSound('hit-send'); @@ -387,7 +387,7 @@ class DraftStore extends NylasStore { } __testExtensionTransforms() { - const clientId = NylasEnv.getWindowProps().draftClientId; + const clientId = NylasEnv.getWindowProps().headerMessageId; return this.sessionForClientId(clientId).then((session) => { return this._prepareForSyncback(session).then(() => { window.__draft = session.draft(); @@ -405,19 +405,19 @@ class DraftStore extends NylasStore { }); } - _onDidCancelSendAction = ({draftClientId}) => { - delete this._draftsSending[draftClientId]; - this.trigger(draftClientId); + _onDidCancelSendAction = ({headerMessageId}) => { + delete this._draftsSending[headerMessageId]; + this.trigger(headerMessageId); } - _onSendDraftSuccess = ({draftClientId}) => { - delete this._draftsSending[draftClientId]; - this.trigger(draftClientId); + _onSendDraftSuccess = ({headerMessageId}) => { + delete this._draftsSending[headerMessageId]; + this.trigger(headerMessageId); } - _onSendDraftFailed = ({draftClientId, threadId, errorMessage, errorDetail}) => { - this._draftsSending[draftClientId] = false; - this.trigger(draftClientId); + _onSendDraftFailed = ({headerMessageId, threadId, errorMessage, errorDetail}) => { + this._draftsSending[headerMessageId] = false; + this.trigger(headerMessageId); if (NylasEnv.isMainWindow()) { // We delay so the view has time to update the restored draft. If we // don't delay the modal may come up in a state where the draft looks @@ -426,17 +426,17 @@ class DraftStore extends NylasStore { // We also need to delay because the old draft window needs to fully // close. It takes windows currently (June 2016) 100ms to close by setTimeout(() => { - this._notifyUserOfError({draftClientId, threadId, errorMessage, errorDetail}); + this._notifyUserOfError({headerMessageId, threadId, errorMessage, errorDetail}); }, 300); } } - _notifyUserOfError({draftClientId, threadId, errorMessage, errorDetail}) { + _notifyUserOfError({headerMessageId, threadId, errorMessage, errorDetail}) { const focusedThread = FocusedContentStore.focused('thread'); if (threadId && focusedThread && focusedThread.id === threadId) { NylasEnv.showErrorDialog(errorMessage, {detail: errorDetail}); } else { - Actions.composePopoutDraft(draftClientId, {errorMessage, errorDetail}); + Actions.composePopoutDraft(headerMessageId, {errorMessage, errorDetail}); } } } diff --git a/packages/client-app/src/flux/stores/file-upload-store.es6 b/packages/client-app/src/flux/stores/file-upload-store.es6 index 4ec50e36f..a9484bc0d 100644 --- a/packages/client-app/src/flux/stores/file-upload-store.es6 +++ b/packages/client-app/src/flux/stores/file-upload-store.es6 @@ -50,7 +50,7 @@ class FileUploadStore extends NylasStore { mkdirp.sync(UPLOAD_DIR); if (NylasEnv.isMainWindow() || NylasEnv.inSpecMode()) { this.listenTo(Actions.ensureMessageInSentSuccess, ({messageClientId}) => { - this._deleteUploadsForClientId(messageClientId); + this._deleteUploadsForId(messageClientId); }); } } @@ -107,7 +107,7 @@ class FileUploadStore extends NylasStore { .catch((err) => Promise.reject(new Error(`Error deleting file upload ${upload.filename}:\n\n${err.message}`))); } - _deleteUploadsForClientId(messageClientId) { + _deleteUploadsForId(messageClientId) { rimraf(path.join(UPLOAD_DIR, messageClientId), {disableGlob: true}, (err) => { if (err) { console.warn(err); diff --git a/packages/client-app/src/flux/stores/task-queue.es6 b/packages/client-app/src/flux/stores/task-queue.es6 index fbbcaebef..9fefd76be 100644 --- a/packages/client-app/src/flux/stores/task-queue.es6 +++ b/packages/client-app/src/flux/stores/task-queue.es6 @@ -2,8 +2,6 @@ import _ from 'underscore'; import NylasStore from 'nylas-store'; import {Rx} from 'nylas-exports'; import Task from "../tasks/task"; -import DatabaseObjectRegistry from '../../registries/database-object-registry'; -import Actions from '../actions'; import DatabaseStore from './database-store'; /** @@ -52,19 +50,28 @@ class TaskQueue extends NylasStore { this._waitingForRemote = []; Rx.Observable.fromQuery(DatabaseStore.findAll(Task)).subscribe((tasks => { - this._queue = tasks.filter(t => t.complete === false); - this._completed = tasks.filter(t => t.complete === true); - this.trigger(); - // TODO : this._waitingForLocal! - })) + this._queue = tasks.filter(t => t.status !== 'complete'); + this._completed = tasks.filter(t => t.status === 'complete'); + const all = [].concat(this._queue, this._completed); - this.listenTo(Actions.queueTask, this.enqueue) - this.listenTo(Actions.queueTasks, (tasks) => { - if (!tasks || !tasks.length) { return; } - for (const task of tasks) { this.enqueue(task); } - }); - this.listenTo(Actions.undoTaskId, this.enqueueUndoOfTaskId); - this.listenTo(Actions.dequeueTask, this.dequeue); + this._waitingForLocal.filter(({task, resolve}) => { + if (all.find(t => task.id === t.id)) { + resolve(); + return false; + } + return true; + }); + + this._waitingForRemote.filter(({task, resolve}) => { + if (this._completed.find(t => task.id === t.id)) { + resolve(); + return false; + } + return true; + }); + + this.trigger(); + })); } queue() { @@ -88,7 +95,7 @@ class TaskQueue extends NylasStore { {SaveDraftTask} or 'SaveDraftTask') - `matching`: Optional An {Object} with criteria to pass to _.isMatch. For a - SaveDraftTask, this could be {draftClientId: "123123"} + SaveDraftTask, this could be {headerMessageId: "123123"} Returns a matching {Task}, or null. */ @@ -111,32 +118,6 @@ class TaskQueue extends NylasStore { return matches; } - enqueue = (task) => { - if (!(task instanceof Task)) { - console.log(task); - throw new Error("You must queue a `Task` instance. Be sure you have the task registered with the DatabaseObjectRegistry. If this is a task for a custom plugin, you must export a `taskConstructors` array with your `Task` constructors in it. You must all subclass the base Nylas `Task`."); - } - if (!DatabaseObjectRegistry.isInRegistry(task.constructor.name)) { - console.log(task); - throw new Error("You must queue a `Task` instance which is registred with the DatabaseObjectRegistry") - } - if (!task.id) { - console.log(task); - throw new Error("Tasks must have an ID prior to being queued. Check that your Task constructor is calling `super`"); - } - task.sequentialId = ++this._currentSequentialId; - task.status = 'local'; - - NylasEnv.actionBridgeCpp.onTellClients({type: 'task-queued', task: task}); - } - - enqueueUndoOfTaskId = (taskId) => { - const task = this._queue.find(t => t.id === taskId) || this._completed.find(t => t.id === taskId); - if (task) { - this.enqueue(task.createUndoTask()); - } - } - dequeue = (taskOrId) => { const task = this._resolveTaskArgument(taskOrId); if (!task) { diff --git a/packages/client-app/src/flux/stores/undo-redo-store.es6 b/packages/client-app/src/flux/stores/undo-redo-store.es6 index 501350b00..6be6a35d5 100644 --- a/packages/client-app/src/flux/stores/undo-redo-store.es6 +++ b/packages/client-app/src/flux/stores/undo-redo-store.es6 @@ -43,7 +43,7 @@ class UndoRedoStore extends NylasStore { this.trigger(); for (const task of topTasks) { - Actions.undoTaskId(task.id); + Actions.queueTask(task.createUndoTask()); } const redoTasks = topTasks.map((t) => { diff --git a/packages/client-app/src/flux/tasks/base-draft-task.es6 b/packages/client-app/src/flux/tasks/base-draft-task.es6 index d4c857bdc..607b80fda 100644 --- a/packages/client-app/src/flux/tasks/base-draft-task.es6 +++ b/packages/client-app/src/flux/tasks/base-draft-task.es6 @@ -3,47 +3,8 @@ import DraftHelpers from '../stores/draft-helpers'; export default class BaseDraftTask extends Task { - static DraftNotFoundError = DraftHelpers.DraftNotFoundError; - - constructor(draftClientId) { + constructor(draft) { super(); - this.draftClientId = draftClientId; - this.draft = null; - } - - shouldDequeueOtherTask(other) { - const isSameDraft = (other.draftClientId === this.draftClientId); - const isOlderTask = (other.sequentialId < this.sequentialId); - const isExactClass = (other.constructor.name === this.constructor.name); - return (isSameDraft && isOlderTask && isExactClass); - } - - isDependentOnTask(other) { - // Set this task to be dependent on any SyncbackDraftTasks and - // SendDraftTasks for the same draft that were created first. - // This, in conjunction with this method on SendDraftTask, ensures - // that a send and a syncback never run at the same time for a draft. - - // Require here rather than on top to avoid a circular dependency - const isSameDraft = (other.draftClientId === this.draftClientId); - const isOlderTask = (other.sequentialId < this.sequentialId); - const isSaveOrSend = (other instanceof BaseDraftTask); - return (isSameDraft && isOlderTask && isSaveOrSend); - } - - performLocal() { - // SyncbackDraftTask does not do anything locally. You should persist your changes - // to the local database directly or using a DraftEditingSession, and then queue a - // SyncbackDraftTask to send those changes to the server. - if (!this.draftClientId) { - const errMsg = `Attempt to call ${this.constructor.name}.performLocal without a draftClientId`; - return Promise.reject(new Error(errMsg)); - } - return Promise.resolve(); - } - - async refreshDraftReference() { - this.draft = await DraftHelpers.refreshDraftReference(this.draftClientId) - return this.draft; + this.draft = draft; } } diff --git a/packages/client-app/src/flux/tasks/destroy-draft-task.es6 b/packages/client-app/src/flux/tasks/destroy-draft-task.es6 index b8bc8d81f..f5761ecda 100644 --- a/packages/client-app/src/flux/tasks/destroy-draft-task.es6 +++ b/packages/client-app/src/flux/tasks/destroy-draft-task.es6 @@ -1,21 +1,8 @@ -import Task from './task'; -import {APIError} from '../errors'; -import Message from '../models/message'; -import DatabaseStore from '../stores/database-store'; -import NylasAPI from '../nylas-api'; -import NylasAPIRequest from '../nylas-api-request'; import BaseDraftTask from './base-draft-task'; export default class DestroyDraftTask extends BaseDraftTask { - - shouldDequeueOtherTask(other) { - return (other instanceof BaseDraftTask && other.draftClientId === this.draftClientId); - } - - performLocal() { - super.performLocal(); - return this.refreshDraftReference() - .then(() => DatabaseStore.inTransaction((t) => t.unpersistModel(this.draft))) - .catch(BaseDraftTask.DraftNotFoundError, () => Promise.resolve()); + constructor(headerMessageId) { + super(); + this.headerMessageId = headerMessageId; } } diff --git a/packages/client-app/src/flux/tasks/perform-send-action-task.es6 b/packages/client-app/src/flux/tasks/perform-send-action-task.es6 index 2d2e242b6..5395c5d6a 100644 --- a/packages/client-app/src/flux/tasks/perform-send-action-task.es6 +++ b/packages/client-app/src/flux/tasks/perform-send-action-task.es6 @@ -7,8 +7,8 @@ import SendActionsStore from '../stores/send-actions-store'; class PerformSendActionTask extends BaseDraftTask { - constructor(draftClientId, sendActionKey) { - super(draftClientId) + constructor(headerMessageId, sendActionKey) { + super(headerMessageId) this._sendActionKey = sendActionKey this._sendTimer = null this._taskResolve = () => {} @@ -21,22 +21,22 @@ class PerformSendActionTask extends BaseDraftTask { shouldDequeueOtherTask(otherTask) { return ( otherTask instanceof PerformSendActionTask && - this.draftClientId === otherTask.draftClientId + this.headerMessageId === otherTask.headerMessageId ) } performLocal() { - if (!this.draftClientId) { - const errMsg = `Attempt to call ${this.constructor.name}.performLocal without a draftClientId`; + if (!this.headerMessageId) { + const errMsg = `Attempt to call ${this.constructor.name}.performLocal without a headerMessageId`; return Promise.reject(new Error(errMsg)); } return Promise.resolve() } cancel() { - const {id: taskId, draftClientId} = this + const {id: taskId, headerMessageId} = this clearTimeout(this._sendTimer) - Actions.didCancelSendAction({taskId, draftClientId}) + Actions.didCancelSendAction({taskId, headerMessageId}) this._taskResolve(Task.Status.Continue) } diff --git a/packages/client-app/src/nylas-env.es6 b/packages/client-app/src/nylas-env.es6 index e877ee720..8c0e814d7 100644 --- a/packages/client-app/src/nylas-env.es6 +++ b/packages/client-app/src/nylas-env.es6 @@ -171,9 +171,9 @@ export default class NylasEnvConstructor { const PackageManager = require('./package-manager'); const ThemeManager = require('./theme-manager'); const StyleManager = require('./style-manager'); + const MenuManager = require('./menu-manager').default; const ActionBridge = require('./flux/action-bridge').default; const ActionBridgeCPP = require('./flux/action-bridge-cpp').default; - const MenuManager = require('./menu-manager').default; const {devMode, benchmarkMode, safeMode, resourcePath, configDirPath, windowType} = this.getLoadSettings(); @@ -371,8 +371,13 @@ export default class NylasEnvConstructor { if (event.defaultPrevented) { return; } this.lastUncaughtError = error; - - extra.pluginIds = this._findPluginsFromError(error); + + try { + extra.pluginIds = this._findPluginsFromError(error); + } catch (err) { + // can happen when an error is thrown very early + extra.pluginIds = []; + } if (this.inSpecMode()) { jasmine.getEnv().currentSpec.fail(error); diff --git a/packages/client-app/static/package-template/spec/my-composer-button-spec.jsx b/packages/client-app/static/package-template/spec/my-composer-button-spec.jsx index 1aca3968d..5ee2e991c 100644 --- a/packages/client-app/static/package-template/spec/my-composer-button-spec.jsx +++ b/packages/client-app/static/package-template/spec/my-composer-button-spec.jsx @@ -6,7 +6,7 @@ import MyComposerButton from '../lib/my-composer-button'; describe("MyComposerButton", () => { beforeEach(() => { this.component = ReactTestUtils.renderIntoDocument( - + ); });