From 6a03c6a034ff86a4efd66d7483ea50f9e756ca53 Mon Sep 17 00:00:00 2001 From: Evan Morikawa Date: Thu, 12 Mar 2015 17:48:56 -0400 Subject: [PATCH] feat(composer): blocks multiple sending & lots of tests Summary: fix in composer sending Test Plan: edgehill --test Reviewers: bengotow Reviewed By: bengotow Differential Revision: https://review.inboxapp.com/D1286 --- .../lib/account-sidebar-store.coffee | 2 +- .../composer/lib/composer-view.cjsx | 14 +- .../spec/inbox-composer-view-spec.cjsx | 272 +++++++++++++++--- .../composer/stylesheets/composer.less | 10 + spec-inbox/stores/draft-store-spec.coffee | 71 +++++ spec-inbox/tasks/send-draft-spec.coffee | 53 +++- src/flux/actions.coffee | 2 + src/flux/stores/draft-store-proxy.coffee | 1 + src/flux/stores/draft-store.coffee | 20 +- src/flux/tasks/send-draft.coffee | 6 +- 10 files changed, 395 insertions(+), 56 deletions(-) diff --git a/internal_packages/account-sidebar/lib/account-sidebar-store.coffee b/internal_packages/account-sidebar/lib/account-sidebar-store.coffee index 1a66647e6..56f51ff1a 100644 --- a/internal_packages/account-sidebar/lib/account-sidebar-store.coffee +++ b/internal_packages/account-sidebar/lib/account-sidebar-store.coffee @@ -72,7 +72,7 @@ AccountSidebarStore = Reflux.createStore # Remove this when JOIN query speed is fixed! _populateUnreadCountsDebounced: _.debounce -> @_populateUnreadCounts() - , 750 + , 2000 _refetchFromAPI: -> namespace = NamespaceStore.current() diff --git a/internal_packages/composer/lib/composer-view.cjsx b/internal_packages/composer/lib/composer-view.cjsx index e55a3b5d2..2cb897b27 100644 --- a/internal_packages/composer/lib/composer-view.cjsx +++ b/internal_packages/composer/lib/composer-view.cjsx @@ -31,6 +31,7 @@ ComposerView = React.createClass bcc: [] body: "" subject: "" + isSending: DraftStore.sendingState(@props.localId) state getComponentRegistryState: -> @@ -42,6 +43,7 @@ ComposerView = React.createClass # @_checkForKnownFrames() componentDidMount: -> + @_draftStoreUnlisten = DraftStore.listen @_onSendingStateChanged @keymap_unsubscriber = atom.commands.add '.composer-outer-wrap', { 'composer:show-and-focus-bcc': @_showAndFocusBcc 'composer:show-and-focus-cc': @_showAndFocusCc @@ -59,6 +61,7 @@ ComposerView = React.createClass componentWillUnmount: -> @_teardownForDraft() + @_draftStoreUnlisten() if @_draftStoreUnlisten @keymap_unsubscriber.dispose() componentWillUpdate: -> @@ -114,6 +117,10 @@ ComposerView = React.createClass _renderComposer: ->
+
+
+
{@_footerComponents()}
@@ -277,6 +285,7 @@ ComposerView = React.createClass Actions.composePopoutDraft @props.localId _sendDraft: (options = {}) -> + return if @state.isSending draft = @_proxy.draft() remote = require('remote') dialog = remote.require('dialog') @@ -293,7 +302,7 @@ ComposerView = React.createClass warnings = [] if draft.subject.length is 0 warnings.push('without a subject line') - if draft.body.toLowerCase().indexOf('attachment') != -1 and draft.files?.length is 0 + if (draft.files ? []).length is 0 and draft.body.toLowerCase().indexOf('attach') >= 0 warnings.push('without an attachment') if warnings.length > 0 and not options.force @@ -334,6 +343,9 @@ ComposerView = React.createClass @_precalcComposerCss = minHeight: mheight - INLINE_COMPOSER_OTHER_HEIGHT + _onSendingStateChanged: -> + @setState isSending: DraftStore.sendingState(@props.localId) + diff --git a/internal_packages/composer/spec/inbox-composer-view-spec.cjsx b/internal_packages/composer/spec/inbox-composer-view-spec.cjsx index 8672c7973..9bf15a269 100644 --- a/internal_packages/composer/spec/inbox-composer-view-spec.cjsx +++ b/internal_packages/composer/spec/inbox-composer-view-spec.cjsx @@ -4,9 +4,11 @@ proxyquire = require "proxyquire" React = require "react/addons" ReactTestUtils = React.addons.TestUtils -{Contact, +{Actions, + Contact, Message, Namespace, + DraftStore, DatabaseStore, InboxTestUtils, NamespaceStore} = require "inbox-exports" @@ -28,9 +30,9 @@ textFieldStub = (className) -> render: ->
{@props.children}
focus: -> -draftStoreProxyStub = (localId) -> +draftStoreProxyStub = (localId, returnedDraft) -> listen: -> -> - draft: -> new Message() + draft: -> (returnedDraft ? new Message(draft: true)) changes: add: -> commit: -> @@ -41,8 +43,6 @@ searchContactStub = (email) -> ComposerView = proxyquire "../lib/composer-view.cjsx", "./file-uploads.cjsx": reactStub("file-uploads") - "./draft-store-proxy": draftStoreProxyStub - "./composer-participants.cjsx": reactStub("composer-participants") "./participants-text-field.cjsx": textFieldStub("") "inbox-exports": ContactStore: @@ -51,6 +51,7 @@ ComposerView = proxyquire "../lib/composer-view.cjsx", listen: -> -> findViewByName: (component) -> reactStub(component) findAllViewsByRole: (role) -> [reactStub('a'),reactStub('b')] + DraftStore: DraftStore beforeEach -> # The NamespaceStore isn't set yet in the new window, populate it first. @@ -78,71 +79,250 @@ describe "A blank composer view", -> expect(ReactTestUtils.isCompositeComponentWithType @composer, ComposerView).toBe true describe "testing keyboard inputs", -> - beforeEach -> - spyOn(@composer, "_sendDraft") - InboxTestUtils.loadKeymap "internal_packages/composer/keymaps/composer.cson" - - it "sends the draft on cmd-enter", -> - InboxTestUtils.keyPress("cmd-enter", @composer.getDOMNode()) - expect(@composer._sendDraft).toHaveBeenCalled() - - it "does not send the draft on enter if the button isn't in focus", -> - InboxTestUtils.keyPress("enter", @composer.getDOMNode()) - expect(@composer._sendDraft).not.toHaveBeenCalled() - - it "sends the draft on enter when the button is in focus", -> - sendBtn = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, "btn-send") - InboxTestUtils.keyPress("enter", sendBtn.getDOMNode()) - expect(@composer._sendDraft).toHaveBeenCalled() - it "shows and focuses on bcc field", -> it "shows and focuses on cc field", -> it "shows and focuses on bcc field when already open", -> - describe "should show subject", -> +describe "populated composer", -> + # This will setup the mocks necessary to make the composer element (once + # mounted) think it's attached to the given draft. This mocks out the + # proxy system used by the composer. + DRAFT_LOCAL_ID = "local-123" + useDraft = (draftAttributes={}) -> + @draft = new Message _.extend({draft: true}, draftAttributes) + spyOn(DraftStore, "sessionForLocalId").andCallFake (localId) => + return draftStoreProxyStub(localId, @draft) + + useFullDraft = -> + useDraft.call @, + from: [u1] + to: [u2] + cc: [u3, u4] + bcc: [u5] + subject: "Test Message 1" + body: "Hello World
This is a test" + + makeComposer = -> + @composer = ReactTestUtils.renderIntoDocument( + + ) + + describe "When displaying info from a draft", -> + beforeEach -> + useFullDraft.apply(@) + makeComposer.call(@) + + it "attaches the draft to the proxy", -> + expect(@draft).toBeDefined() + expect(@composer._proxy.draft()).toBe @draft + + it "set the state based on the draft", -> + expect(@composer.state.from).toBeUndefined() + expect(@composer.state.to).toEqual [u2] + expect(@composer.state.cc).toEqual [u3, u4] + expect(@composer.state.bcc).toEqual [u5] + expect(@composer.state.subject).toEqual "Test Message 1" + expect(@composer.state.body).toEqual "Hello World
This is a test" + + describe "when deciding whether or not to show the subject", -> it "shows the subject when the subject is empty", -> - msg = new Message - subject: "" - spyOn(@composer._proxy, "draft").andReturn msg + useDraft.call @, subject: "" + makeComposer.call @ expect(@composer._shouldShowSubject()).toBe true it "shows the subject when the subject looks like a fwd", -> - msg = new Message - subject: "Fwd: This is the message" - spyOn(@composer._proxy, "draft").andReturn msg + useDraft.call @, subject: "Fwd: This is the message" + makeComposer.call @ expect(@composer._shouldShowSubject()).toBe true it "shows the subject when the subject looks like a fwd", -> - msg = new Message - subject: "fwd foo" - spyOn(@composer._proxy, "draft").andReturn msg + useDraft.call @, subject: "fwd foo" + makeComposer.call @ expect(@composer._shouldShowSubject()).toBe true it "doesn't show subject when subject has fwd text in it", -> - msg = new Message - subject: "Trick fwd" - spyOn(@composer._proxy, "draft").andReturn msg + useDraft.call @, subject: "Trick fwd" + makeComposer.call @ expect(@composer._shouldShowSubject()).toBe false it "doesn't show the subject otherwise", -> - msg = new Message - subject: "Foo bar Baz" - spyOn(@composer._proxy, "draft").andReturn msg + useDraft.call @, subject: "Foo bar baz" + makeComposer.call @ expect(@composer._shouldShowSubject()).toBe false -describe "When composing a new message", -> - it "Can add someone in the to field", -> + describe "when deciding whether or not to show cc and bcc", -> + it "doesn't show cc when there's no one to cc", -> + useDraft.call @, cc: [] + makeComposer.call @ + expect(@composer.state.showcc).toBe false - it "Can add someone in the cc field", -> + it "shows cc when populated", -> + useDraft.call @, cc: [u1,u2] + makeComposer.call @ + expect(@composer.state.showcc).toBe true - it "Can add someone in the bcc field", -> + it "doesn't show bcc when there's no one to bcc", -> + useDraft.call @, bcc: [] + makeComposer.call @ + expect(@composer.state.showbcc).toBe false -describe "When replying to a message", -> + it "shows bcc when populated", -> + useDraft.call @, bcc: [u2,u3] + makeComposer.call @ + expect(@composer.state.showbcc).toBe true -describe "When replying all to a message", -> + describe "When sending a message", -> + beforeEach -> + remote = require('remote') + @dialog = remote.require('dialog') + spyOn(remote, "getCurrentWindow") + spyOn(@dialog, "showMessageBox") + spyOn(Actions, "sendDraft") + DraftStore._sendingState = {} -describe "When forwarding a message", -> + it "shows a warning if there are no recipients", -> + useDraft.call @, subject: "no recipients" + makeComposer.call(@) + @composer._sendDraft() + expect(Actions.sendDraft).not.toHaveBeenCalled() + expect(@dialog.showMessageBox).toHaveBeenCalled() + dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1] + expect(dialogArgs.buttons).toEqual ['Edit Message'] -describe "When changing the subject of a message", -> + it "shows a warning if there's no subject", -> + useDraft.call @, to: [u1], subject: "" + makeComposer.call(@) + @composer._sendDraft() + expect(Actions.sendDraft).not.toHaveBeenCalled() + expect(@dialog.showMessageBox).toHaveBeenCalled() + dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1] + expect(dialogArgs.buttons).toEqual ['Cancel', 'Send Anyway'] + + it "doesn't show a warning if requirements are satisfied", -> + useFullDraft.apply(@); makeComposer.call(@) + @composer._sendDraft() + expect(Actions.sendDraft).toHaveBeenCalled() + expect(@dialog.showMessageBox).not.toHaveBeenCalled() + + describe "Checking for attachments", -> + warn = (body) -> + useDraft.call @, subject: "Subject", to: [u1], body: body + makeComposer.call(@); @composer._sendDraft() + expect(Actions.sendDraft).not.toHaveBeenCalled() + expect(@dialog.showMessageBox).toHaveBeenCalled() + dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1] + expect(dialogArgs.buttons).toEqual ['Cancel', 'Send Anyway'] + + noWarn = (body) -> + useDraft.call @, subject: "Subject", to: [u1], body: "Sup yo" + makeComposer.call(@); @composer._sendDraft() + expect(Actions.sendDraft).toHaveBeenCalled() + expect(@dialog.showMessageBox).not.toHaveBeenCalled() + + it "warns", -> warn.call(@, "Check out the attached file") + it "warns", -> warn.call(@, "I've added an attachment") + it "warns", -> warn.call(@, "I'm going to attach the file") + + it "doesn't warn", -> noWarn.call(@, "sup yo") + it "doesn't warn", -> noWarn.call(@, "Look at the file") + + it "doesn't show a warning if you've attached a file", -> + useDraft.call @, + subject: "Subject" + to: [u1] + body: "Check out attached file" + files: [{filename:"abc"}] + makeComposer.call(@); @composer._sendDraft() + expect(Actions.sendDraft).toHaveBeenCalled() + expect(@dialog.showMessageBox).not.toHaveBeenCalled() + + it "bypasses the warning if force bit is set", -> + useDraft.call @, to: [u1], subject: "" + makeComposer.call(@) + @composer._sendDraft(force: true) + expect(Actions.sendDraft).toHaveBeenCalled() + expect(@dialog.showMessageBox).not.toHaveBeenCalled() + + it "sends when you click the send button", -> + useFullDraft.apply(@); makeComposer.call(@) + sendBtn = @composer.refs.sendButton.getDOMNode() + ReactTestUtils.Simulate.click sendBtn + expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_LOCAL_ID) + expect(Actions.sendDraft.calls.length).toBe 1 + + simulateDraftStore = -> + DraftStore._sendingState[DRAFT_LOCAL_ID] = true + DraftStore.trigger() + + it "doesn't send twice if you double click", -> + useFullDraft.apply(@); makeComposer.call(@) + sendBtn = @composer.refs.sendButton.getDOMNode() + ReactTestUtils.Simulate.click sendBtn + simulateDraftStore() + ReactTestUtils.Simulate.click sendBtn + expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_LOCAL_ID) + expect(Actions.sendDraft.calls.length).toBe 1 + + it "disables the composer once sending has started", -> + useFullDraft.apply(@); makeComposer.call(@) + sendBtn = @composer.refs.sendButton.getDOMNode() + cover = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, "composer-cover") + expect(cover.getDOMNode().style.display).toBe "none" + ReactTestUtils.Simulate.click sendBtn + simulateDraftStore() + expect(cover.getDOMNode().style.display).toBe "block" + expect(@composer.state.isSending).toBe true + + it "re-enables the composer if sending threw an error", -> + useFullDraft.apply(@); makeComposer.call(@) + sendBtn = @composer.refs.sendButton.getDOMNode() + ReactTestUtils.Simulate.click sendBtn + simulateDraftStore() + expect(@composer.state.isSending).toBe true + Actions.sendDraftError("oh no") + DraftStore._sendingState[DRAFT_LOCAL_ID] = false + DraftStore.trigger() + expect(@composer.state.isSending).toBe false + + describe "when sending a message with keyboard inputs", -> + beforeEach -> + useFullDraft.apply(@) + makeComposer.call(@) + spyOn(@composer, "_sendDraft") + InboxTestUtils.loadKeymap "internal_packages/composer/keymaps/composer.cson" + + it "sends the draft on cmd-enter", -> + InboxTestUtils.keyPress("cmd-enter", @composer.getDOMNode()) + expect(@composer._sendDraft).toHaveBeenCalled() + + it "does not send the draft on enter if the button isn't in focus", -> + InboxTestUtils.keyPress("enter", @composer.getDOMNode()) + expect(@composer._sendDraft).not.toHaveBeenCalled() + + it "sends the draft on enter when the button is in focus", -> + sendBtn = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, "btn-send") + InboxTestUtils.keyPress("enter", sendBtn.getDOMNode()) + expect(@composer._sendDraft).toHaveBeenCalled() + + it "doesn't let you send twice", -> + sendBtn = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, "btn-send") + InboxTestUtils.keyPress("enter", sendBtn.getDOMNode()) + expect(@composer._sendDraft).toHaveBeenCalled() + + + describe "When composing a new message", -> + it "Can add someone in the to field", -> + + it "Can add someone in the cc field", -> + + it "Can add someone in the bcc field", -> + + describe "When replying to a message", -> + + describe "When replying all to a message", -> + + describe "When forwarding a message", -> + + describe "When changing the subject of a message", -> diff --git a/internal_packages/composer/stylesheets/composer.less b/internal_packages/composer/stylesheets/composer.less index 393a677b2..8bf4214ba 100644 --- a/internal_packages/composer/stylesheets/composer.less +++ b/internal_packages/composer/stylesheets/composer.less @@ -14,7 +14,16 @@ display: flex; flex-direction: column; + .composer-cover { + position: absolute; + top: -1 * @spacing-double; right: 0; bottom: 0; left: 0; + z-index: 1000; + background: rgba(255,255,255,0.7); + } + .composer-action-bar-wrap { + position: relative; + z-index: 1; width: 100%; background: transparent; border-bottom: 0; @@ -36,6 +45,7 @@ .composer-content-wrap { position: relative; + z-index: 1; width: 100%; max-width: @compose-width; diff --git a/spec-inbox/stores/draft-store-spec.coffee b/spec-inbox/stores/draft-store-spec.coffee index 7f8cca77e..ef9d152ad 100644 --- a/spec-inbox/stores/draft-store-spec.coffee +++ b/spec-inbox/stores/draft-store-spec.coffee @@ -4,6 +4,8 @@ Contact = require '../../src/flux/models/contact' NamespaceStore = require '../../src/flux/stores/namespace-store.coffee' DatabaseStore = require '../../src/flux/stores/database-store.coffee' DraftStore = require '../../src/flux/stores/draft-store.coffee' +SendDraftTask = require '../../src/flux/tasks/send-draft' +Actions = require '../../src/flux/actions' _ = require 'underscore-plus' fakeThread = null @@ -264,3 +266,72 @@ describe "DraftStore", -> , (thread, message) -> expect(message).toEqual(fakeMessage1) {} + + describe "sending a draft", -> + draftLocalId = "local-123" + beforeEach -> + DraftStore._sendingState = {} + DraftStore._draftSessions = {} + DraftStore._draftSessions[draftLocalId] = + changes: + commit: -> Promise.resolve() + spyOn(DraftStore, "trigger") + + afterEach -> + atom.state.mode = "editor" # reset to default + + it "sets the sending state when sending", -> + DraftStore._onSendDraft(draftLocalId) + expect(DraftStore.sendingState(draftLocalId)).toBe true + expect(DraftStore.trigger).toHaveBeenCalled() + + it "returns false if the draft hasn't been seen", -> + expect(DraftStore.sendingState(draftLocalId)).toBe false + + it "resets the sending state on success", -> + DraftStore._onSendDraft(draftLocalId) + expect(DraftStore.sendingState(draftLocalId)).toBe true + DraftStore._onSendDraftSuccess(draftLocalId) + expect(DraftStore.sendingState(draftLocalId)).toBe false + expect(DraftStore.trigger).toHaveBeenCalled() + + it "resets the sending state on error", -> + DraftStore._onSendDraft(draftLocalId) + expect(DraftStore.sendingState(draftLocalId)).toBe true + DraftStore._onSendDraftError(draftLocalId) + expect(DraftStore.sendingState(draftLocalId)).toBe false + expect(DraftStore.trigger).toHaveBeenCalled() + + it "closes the window if it's a popout", -> + atom.state.mode = "composer" + spyOn(atom, "close") + waitsForPromise -> + DraftStore._onSendDraft(draftLocalId).then -> + expect(atom.close).toHaveBeenCalled() + + it "doesn't close the window if it's inline", -> + atom.state.mode = "other" + spyOn(atom, "close") + waitsForPromise -> + DraftStore._onSendDraft(draftLocalId).then -> + expect(atom.close).not.toHaveBeenCalled() + + it "queues a SendDraftTask", -> + spyOn(Actions, "queueTask") + waitsForPromise -> + DraftStore._onSendDraft(draftLocalId).then -> + expect(Actions.queueTask).toHaveBeenCalled() + task = Actions.queueTask.calls[0].args[0] + expect(task instanceof SendDraftTask).toBe true + expect(task.draftLocalId).toBe draftLocalId + expect(task.fromPopout).toBe false + + it "queues a SendDraftTask with popout info", -> + atom.state.mode = "composer" + spyOn(atom, "close") + spyOn(Actions, "queueTask") + waitsForPromise -> + DraftStore._onSendDraft(draftLocalId).then -> + expect(Actions.queueTask).toHaveBeenCalled() + task = Actions.queueTask.calls[0].args[0] + expect(task.fromPopout).toBe true diff --git a/spec-inbox/tasks/send-draft-spec.coffee b/spec-inbox/tasks/send-draft-spec.coffee index 589187b55..ae1ecbf4c 100644 --- a/spec-inbox/tasks/send-draft-spec.coffee +++ b/spec-inbox/tasks/send-draft-spec.coffee @@ -108,13 +108,33 @@ describe "SendDraftTask", -> to: name: 'Dummy' email: 'dummy@inboxapp.com' - @task = new SendDraftTask(@draft) + @draftLocalId = "local-123" + @task = new SendDraftTask(@draftLocalId) spyOn(atom.inbox, 'makeRequest').andCallFake (options) -> options.success() if options.success spyOn(DatabaseStore, 'findByLocalId').andCallFake (klass, localId) => Promise.resolve(@draft) - spyOn(DatabaseStore, 'unpersistModel').andCallFake (draft) => + spyOn(DatabaseStore, 'unpersistModel').andCallFake (draft) -> Promise.resolve() + spyOn(atom, "playSound") + spyOn(Actions, "postNotification") + spyOn(Actions, "sendDraftSuccess") + + it "should unpersist when successfully sent", -> + waitsForPromise => @task.performRemote().then => + expect(DatabaseStore.unpersistModel).toHaveBeenCalledWith(@draft) + + it "should notify the draft was sent", -> + waitsForPromise => @task.performRemote().then => + expect(Actions.sendDraftSuccess).toHaveBeenCalledWith(@draftLocalId) + + it "should play a sound", -> + waitsForPromise => @task.performRemote().then -> + expect(atom.playSound).toHaveBeenCalledWith("mail_sent.ogg") + + it "post a notification", -> + waitsForPromise => @task.performRemote().then -> + expect(Actions.postNotification).toHaveBeenCalled() it "should start an API request to /send", -> waitsForPromise => @@ -143,7 +163,7 @@ describe "SendDraftTask", -> to: name: 'Dummy' email: 'dummy@inboxapp.com' - @task = new SendDraftTask(@draft) + @task = new SendDraftTask(@draftLocalId) it "should send the draft JSON", -> waitsForPromise => @@ -170,7 +190,9 @@ describe "SendDraftTask", -> to: name: 'Dummy' email: 'dummy@inboxapp.com' - @task = new SendDraftTask(@draft) + @task = new SendDraftTask(@draft.id) + spyOn(Actions, "dequeueTask") + spyOn(Actions, "sendDraftError") it "throws an error if the draft can't be found", -> spyOn(DatabaseStore, 'findByLocalId').andCallFake (klass, localId) -> @@ -192,3 +214,26 @@ describe "SendDraftTask", -> waitsForPromise => @task.performRemote().catch (error) -> expect(error).toBe "DB error" + + checkError = -> + expect(Actions.sendDraftError).toHaveBeenCalled() + args = Actions.sendDraftError.calls[0].args + expect(args[0]).toBe @draft.id + expect(args[1].length).toBeGreaterThan 0 + + it "onAPIError notifies of the error", -> + @task.onAPIError(message: "oh no") + checkError.call(@) + + it "onOtherError notifies of the error", -> + @task.onOtherError() + checkError.call(@) + + it "onTimeoutError notifies of the error", -> + @task.onTimeoutError() + checkError.call(@) + + it "onOfflineError notifies of the error and dequeues", -> + @task.onOfflineError() + checkError.call(@) + expect(Actions.dequeueTask).toHaveBeenCalledWith(@task) diff --git a/src/flux/actions.coffee b/src/flux/actions.coffee index b552db4e7..024fc4e34 100644 --- a/src/flux/actions.coffee +++ b/src/flux/actions.coffee @@ -21,6 +21,8 @@ globalActions = [ "multiWindowNotification", # Draft actions + "sendDraftError", + "sendDraftSuccess", "destroyDraftSuccess", "destroyDraftError" ] diff --git a/src/flux/stores/draft-store-proxy.coffee b/src/flux/stores/draft-store-proxy.coffee index 835a0ac71..f1844b119 100644 --- a/src/flux/stores/draft-store-proxy.coffee +++ b/src/flux/stores/draft-store-proxy.coffee @@ -100,6 +100,7 @@ class DraftStoreProxy unlisten() for unlisten in @unlisteners _onDraftChanged: (change) -> + return if not change? # We don't accept changes unless our draft object is loaded return unless @_draft diff --git a/src/flux/stores/draft-store.coffee b/src/flux/stores/draft-store.coffee index 5a28fe7d0..cffed6474 100644 --- a/src/flux/stores/draft-store.coffee +++ b/src/flux/stores/draft-store.coffee @@ -41,9 +41,13 @@ DraftStore = Reflux.createStore @listenTo Actions.removeFile, @_onRemoveFile @listenTo Actions.attachFileComplete, @_onAttachFileComplete + @listenTo Actions.sendDraftError, @_onSendDraftSuccess + @listenTo Actions.sendDraftSuccess, @_onSendDraftError + @listenTo Actions.destroyDraftSuccess, @_closeWindow @_drafts = [] @_draftSessions = {} + @_sendingState = {} # TODO: Doesn't work if we do window.addEventListener, but this is # fragile. Pending an Atom fix perhaps? @@ -85,6 +89,8 @@ DraftStore = Reflux.createStore @_draftSessions[localId] ?= new DraftStoreProxy(localId) @_draftSessions[localId] + sendingState: (draftLocalId) -> @_sendingState[draftLocalId] ? false + ########### PRIVATE #################################################### _onDataChanged: (change) -> @@ -213,7 +219,9 @@ DraftStore = Reflux.createStore # Queue the task to destroy the draft Actions.queueTask(new DestroyDraftTask(draftLocalId)) - _onSendDraft: (draftLocalId) -> + _onSendDraft: (draftLocalId) -> new Promise (resolve, reject) => + @_sendingState[draftLocalId] = true + @trigger() # Immediately save any pending changes so we don't save after sending save = @_draftSessions[draftLocalId]?.changes.commit() save.then => @@ -223,6 +231,15 @@ DraftStore = Reflux.createStore # Queue the task to send the draft fromPopout = atom.state.mode is "composer" Actions.queueTask(new SendDraftTask(draftLocalId, fromPopout: fromPopout)) + resolve() + + _onSendDraftError: (draftLocalId) -> + @_sendingState[draftLocalId] = false + @trigger() + + _onSendDraftSuccess: (draftLocalId) -> + @_sendingState[draftLocalId] = false + @trigger() _onAttachFileComplete: ({file, messageLocalId}) -> @sessionForLocalId(messageLocalId).prepare().then (proxy) -> @@ -235,4 +252,3 @@ DraftStore = Reflux.createStore files = proxy.draft().files ? [] files = _.reject files, (f) -> f.id is file.id proxy.changes.add({files}, true) - diff --git a/src/flux/tasks/send-draft.coffee b/src/flux/tasks/send-draft.coffee index 4d48d6c96..c42b1c851 100644 --- a/src/flux/tasks/send-draft.coffee +++ b/src/flux/tasks/send-draft.coffee @@ -31,7 +31,7 @@ class SendDraftTask extends Task new Promise (resolve, reject) => # Fetch the latest draft data to make sure we make the request with the most # recent draft version - DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) -> + DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) => # The draft may have been deleted by another task. Nothing we can do. return reject(new Error("We couldn't find the saved draft.")) unless draft @@ -47,9 +47,10 @@ class SendDraftTask extends Task method: 'POST' body: body returnsModel: true - success: -> + success: => atom.playSound('mail_sent.ogg') Actions.postNotification({message: "Sent!", type: 'success'}) + Actions.sendDraftSuccess(@draftLocalId) DatabaseStore.unpersistModel(draft).then(resolve) error: reject .catch(reject) @@ -73,5 +74,6 @@ class SendDraftTask extends Task Actions.dequeueTask(@) _notifyError: (msg) -> + Actions.sendDraftError(@draftLocalId, msg) if @fromPopout then atom.displayComposer(@draftLocalId, error: msg) @notifyErrorMessage(msg)