Draft creation, saving, deletion moved to c++

This commit is contained in:
Ben Gotow 2017-06-25 00:46:01 -07:00
parent 3c56e2fbfd
commit 4966ae3650
45 changed files with 277 additions and 344 deletions

View file

@ -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(
<TemplatePopover draftClientId={this.props.draftClientId} />,
<TemplatePopover headerMessageId={this.props.headerMessageId} />,
{originRect: buttonRect, direction: 'up'}
)
};

View file

@ -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(

View file

@ -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: '<p>Body</p>'});
} else {
d = new Message({subject: 'Subject', body: '<p>Body</p>'});
@ -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(() => {

View file

@ -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() {

View file

@ -326,7 +326,7 @@ export default class ComposerHeader extends React.Component {
return (
<div className="composer-header">
<ComposerHeaderActions
draftId={this.props.draft.id}
headerMessageId={this.props.draft.headerMessageId}
enabledFields={this.state.enabledFields}
participantsFocused={this.state.participantsFocused}
onShowAndFocusField={this.showAndFocusField}

View file

@ -205,7 +205,7 @@ export default class ComposerView extends React.Component {
_renderEditor() {
const exposedProps = {
body: this.props.draft.body,
draftClientId: this.props.draft.clientId,
headerMessageId: this.props.draft.headerMessageId,
parentActions: {
getComposerBoundingRect: this._getComposerBoundingRect,
scrollTo: this.props.scrollTo,
@ -292,7 +292,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,
}}
direction="column"
@ -311,11 +311,11 @@ export default class ComposerView extends React.Component {
}
_renderFileAttachments() {
const {files, clientId: messageClientId} = this.props.draft
const {files, headerMessageId} = this.props.draft
return (
<InjectedComponent
matching={{role: 'MessageAttachments'}}
exposedProps={{files, messageClientId, canRemoveAttachments: true}}
exposedProps={{files, headerMessageId, canRemoveAttachments: true}}
/>
)
}
@ -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() {

View file

@ -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}`,

View file

@ -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 {
<ComposerViewForDraftClientId
ref="composer"
onDraftReady={this._onDraftReady}
draftClientId={this.state.draftClientId}
headerMessageId={this.state.headerMessageId}
className="composer-full-window"
/>
);

View file

@ -10,7 +10,7 @@ describe "ComposerHeaderActions", ->
@onShowAndFocusField = jasmine.createSpy("onShowAndFocusField")
props.onShowAndFocusField = @onShowAndFocusField
props.enabledFields ?= []
props.draftClientId = 'a'
props.headerMessageId = 'a'
@component = ReactTestUtils.renderIntoDocument(
<ComposerHeaderActions {...props} />
)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -64,11 +64,11 @@ class MessageItemContainer extends React.Component
_renderComposer: =>
Composer = ComponentRegistry.findComponentsMatching(role: 'Composer')[0]
if (!Composer)
return <span></span>
return <span>No Composer Component Present</span>
<Composer
ref="message"
draftId={@props.message.id}
headerMessageId={@props.message.headerMessageId}
className={@_classNames()}
mode={"inline"}
threadId={@props.thread.id}
@ -82,8 +82,8 @@ class MessageItemContainer extends React.Component
"message-item-wrap": true
"before-reply-area": @props.isBeforeReplyArea
_onSendingStateChanged: (draftId) =>
if draftId is @props.message.id
_onSendingStateChanged: (headerMessageId) =>
if headerMessageId is @props.message.headerMessageId
@setState(@_getStateFromStores())
_getStateFromStores: (props = @props) ->

View file

@ -37,7 +37,7 @@ xdescribe 'MessageItemContainer', ->
ReactTestUtils.renderIntoDocument(
<MessageItemContainer thread={testThread}
message={message}
draftClientId={testClientId} />
headerMessageId={testClientId} />
)
it "shows composer if it's a draft", ->

View file

@ -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",

View file

@ -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})

View file

@ -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);

View file

@ -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')

View file

@ -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",

View file

@ -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)

View file

@ -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 <alias@nylas.com>"]
},{
@ -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

View file

@ -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', ->

View file

@ -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]])
});

View file

@ -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")
});
});
});

View file

@ -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();

View file

@ -14,11 +14,11 @@ components inside of your React render method. Rather than explicitly render
a component, such as a `<Composer>`, you can use InjectedComponent:
```coffee
<InjectedComponent matching={role:"Composer"} exposedProps={draftClientId:123} />
<InjectedComponent matching={role:"Composer"} exposedProps={headerMessageId:123} />
```
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.

View file

@ -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);
}
}

View file

@ -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');

View file

@ -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);
});

View file

@ -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;

View file

@ -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({

View file

@ -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: ->

View file

@ -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"

View file

@ -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) {

View file

@ -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

View file

@ -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

View file

@ -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});
}
}
}

View file

@ -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);

View file

@ -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) {

View file

@ -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) => {

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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)
}

View file

@ -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);

View file

@ -6,7 +6,7 @@ import MyComposerButton from '../lib/my-composer-button';
describe("MyComposerButton", () => {
beforeEach(() => {
this.component = ReactTestUtils.renderIntoDocument(
<MyComposerButton draftClientId="test" />
<MyComposerButton headerMessageId="test" />
);
});