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
This commit is contained in:
Evan Morikawa 2015-03-12 17:48:56 -04:00
parent 818bca892c
commit 6a03c6a034
10 changed files with 395 additions and 56 deletions

View file

@ -72,7 +72,7 @@ AccountSidebarStore = Reflux.createStore
# Remove this when JOIN query speed is fixed!
_populateUnreadCountsDebounced: _.debounce ->
@_populateUnreadCounts()
, 750
, 2000
_refetchFromAPI: ->
namespace = NamespaceStore.current()

View file

@ -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: ->
<div className="composer-inner-wrap" onDragOver={@_onDragNoop} onDragLeave={@_onDragNoop} onDragEnd={@_onDragNoop} onDrop={@_onDrop}>
<div className="composer-cover"
style={display: (if @state.isSending then "block" else "none")}>
</div>
<div className="composer-action-bar-wrap">
<div className="composer-action-bar-content">
<button className="btn btn-toolbar pull-right btn-trash"
@ -127,6 +134,7 @@ ComposerView = React.createClass
onClick={@_popoutComposer}><RetinaImg name="toolbar-popout.png"/></button>
<button className="btn btn-toolbar btn-send"
ref="sendButton"
onClick={@_sendDraft}><RetinaImg name="toolbar-send.png" /></button>
{@_footerComponents()}
</div>
@ -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)

View file

@ -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: -> <div className={className}>{@props.children}</div>
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 <b>World</b><br/> This is a test"
makeComposer = ->
@composer = ReactTestUtils.renderIntoDocument(
<ComposerView localId={DRAFT_LOCAL_ID} />
)
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 <b>World</b><br/> 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", ->

View file

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

View file

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

View file

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

View file

@ -21,6 +21,8 @@ globalActions = [
"multiWindowNotification",
# Draft actions
"sendDraftError",
"sendDraftSuccess",
"destroyDraftSuccess",
"destroyDraftError"
]

View file

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

View file

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

View file

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