mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-28 07:24:36 +08:00
feat(outbox): Sending status now appears beside drafts
Summary: This diff adds an "OutboxStore" which reflects the TaskQueue and adds a progress bar / cancel button to drafts which are currently sending. - Sending state is different from things like Send later because drafts which are sending shouldn't be editable. You should have to stop them from sending before editing. I think we can implement "Send Later" indicators, etc. with a simple InjectedComponentSet on the draft list rows, but the OutboxStore is woven into the DraftList query subscription so every draft has a `uploadTaskId`. - The TaskQueue now saves periodically (every one second) when there are "Processing" tasks. This is not really necessary, but makes it super easy for tasks to expose "progress", because they're essentially serialized and propagated to all windows every one second with the current progress value. Kind of questionable, but super convenient. - I also cleaned up ListTabular and MultiselectList a bit because they applied the className prop to an inner element and not the top one. - If a DestroyDraft task is created for a draft without a server id, it ends with Task.Status.Continue and not Failed. - The SendDraftTask doesn't delete uploads until the send actually goes through, in case the app crashes and it forgets the file IDs it created. Test Plan: Tests coming soon Reviewers: juan, evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D2524
This commit is contained in:
parent
40cb19d122
commit
8b3f7f0578
32 changed files with 391 additions and 149 deletions
|
@ -15,7 +15,7 @@ idForCategories = (categories) ->
|
|||
countForItem = (perspective) ->
|
||||
unreadCountEnabled = NylasEnv.config.get('core.workspace.showUnreadForAllCategories')
|
||||
if perspective.isInbox() or unreadCountEnabled
|
||||
return perspective.threadUnreadCount()
|
||||
return perspective.unreadCount()
|
||||
return 0
|
||||
|
||||
isItemSelected = (perspective) ->
|
||||
|
|
|
@ -4,6 +4,7 @@ _ = require 'underscore'
|
|||
AccountStore,
|
||||
ThreadCountsStore,
|
||||
WorkspaceStore,
|
||||
OutboxStore,
|
||||
FocusedPerspectiveStore,
|
||||
CategoryStore} = require 'nylas-exports'
|
||||
|
||||
|
@ -42,6 +43,7 @@ class SidebarStore extends NylasStore
|
|||
@listenTo AccountStore, @_onAccountsChanged
|
||||
@listenTo FocusedPerspectiveStore, @_onFocusedPerspectiveChanged
|
||||
@listenTo WorkspaceStore, @_updateSections
|
||||
@listenTo OutboxStore, @_updateSections
|
||||
@listenTo ThreadCountsStore, @_updateSections
|
||||
@listenTo CategoryStore, @_updateSections
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ class DraftDeleteButton extends React.Component
|
|||
|
||||
_destroySelected: =>
|
||||
for item in @props.selection.items()
|
||||
Actions.queueTask(new DestroyDraftTask(draftClientId: item.clientId))
|
||||
Actions.destroyDraft(item.clientId)
|
||||
@props.selection.clear()
|
||||
return
|
||||
|
||||
|
|
|
@ -2,10 +2,17 @@ _ = require 'underscore'
|
|||
React = require 'react'
|
||||
classNames = require 'classnames'
|
||||
|
||||
{ListTabular, InjectedComponent} = require 'nylas-component-kit'
|
||||
{ListTabular,
|
||||
InjectedComponent,
|
||||
Flexbox} = require 'nylas-component-kit'
|
||||
|
||||
{timestamp,
|
||||
subject} = require './formatting-utils'
|
||||
|
||||
{Actions} = require 'nylas-exports'
|
||||
SendingProgressBar = require './sending-progress-bar'
|
||||
SendingCancelButton = require './sending-cancel-button'
|
||||
|
||||
snippet = (html) =>
|
||||
return "" unless html and typeof(html) is 'string'
|
||||
try
|
||||
|
@ -16,17 +23,23 @@ snippet = (html) =>
|
|||
catch
|
||||
return ""
|
||||
|
||||
c1 = new ListTabular.Column
|
||||
name: "Name"
|
||||
ParticipantsColumn = new ListTabular.Column
|
||||
name: "Participants"
|
||||
width: 200
|
||||
resolver: (draft) =>
|
||||
<div className="participants">
|
||||
<InjectedComponent matching={role:"Participants"}
|
||||
exposedProps={participants: [].concat(draft.to, draft.cc, draft.bcc), clickable: false}/>
|
||||
</div>
|
||||
list = [].concat(draft.to, draft.cc, draft.bcc)
|
||||
|
||||
c2 = new ListTabular.Column
|
||||
name: "Message"
|
||||
if list.length > 0
|
||||
<div className="participants">
|
||||
{list.map (p) => <span key={p.email}>{p.displayName()}</span>}
|
||||
</div>
|
||||
else
|
||||
<div className="participants no-recipients">
|
||||
(No Recipients)
|
||||
</div>
|
||||
|
||||
ContentsColumn = new ListTabular.Column
|
||||
name: "Contents"
|
||||
flex: 4
|
||||
resolver: (draft) =>
|
||||
attachments = []
|
||||
|
@ -38,11 +51,16 @@ c2 = new ListTabular.Column
|
|||
{attachments}
|
||||
</span>
|
||||
|
||||
c3 = new ListTabular.Column
|
||||
name: "Date"
|
||||
flex: 1
|
||||
SendStateColumn = new ListTabular.Column
|
||||
name: "State"
|
||||
resolver: (draft) =>
|
||||
<span className="timestamp">{timestamp(draft.date)}</span>
|
||||
if draft.uploadTaskId
|
||||
<Flexbox style={width:150, whiteSpace: 'no-wrap'}>
|
||||
<SendingProgressBar style={flex: 1, marginRight: 10} progress={draft.uploadProgress * 100} />
|
||||
<SendingCancelButton taskId={draft.uploadTaskId} />
|
||||
</Flexbox>
|
||||
else
|
||||
<span className="timestamp">{timestamp(draft.date)}</span>
|
||||
|
||||
module.exports =
|
||||
Wide: [c1, c2, c3]
|
||||
Wide: [ParticipantsColumn, ContentsColumn, SendStateColumn]
|
||||
|
|
|
@ -2,6 +2,8 @@ NylasStore = require 'nylas-store'
|
|||
Rx = require 'rx-lite'
|
||||
_ = require 'underscore'
|
||||
{Message,
|
||||
OutboxStore,
|
||||
MutableQueryResultSet,
|
||||
MutableQuerySubscription,
|
||||
ObservableListDataSource,
|
||||
FocusedPerspectiveStore,
|
||||
|
@ -34,7 +36,28 @@ class DraftListStore extends NylasStore
|
|||
.page(0, 1)
|
||||
|
||||
subscription = new MutableQuerySubscription(query, {asResultSet: true})
|
||||
$resultSet = Rx.Observable.fromPrivateQuerySubscription('draft-list', subscription)
|
||||
$resultSet = Rx.Observable.fromNamedQuerySubscription('draft-list', subscription)
|
||||
$resultSet = Rx.Observable.combineLatest [
|
||||
$resultSet,
|
||||
Rx.Observable.fromStore(OutboxStore)
|
||||
], (resultSet, outbox) =>
|
||||
|
||||
# Generate a new result set that includes additional information on
|
||||
# the draft objects. This is similar to what we do in the thread-list,
|
||||
# where we set thread.metadata to the message array.
|
||||
resultSetWithTasks = new MutableQueryResultSet(resultSet)
|
||||
|
||||
mailboxPerspective.accountIds.forEach (aid) =>
|
||||
OutboxStore.itemsForAccount(aid).forEach (task) =>
|
||||
draft = resultSet.modelWithId(task.draft.clientId)
|
||||
if draft
|
||||
draft = draft.clone()
|
||||
draft.uploadTaskId = task.id
|
||||
draft.uploadProgress = task.progress
|
||||
resultSetWithTasks.replaceModel(draft)
|
||||
|
||||
return resultSetWithTasks.immutableClone()
|
||||
|
||||
@_dataSource = new ObservableListDataSource($resultSet, subscription.replaceRange)
|
||||
else
|
||||
@_dataSource = new ListTabular.DataSource.Empty()
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
_ = require 'underscore'
|
||||
React = require 'react'
|
||||
{Actions,
|
||||
FocusedContentStore} = require 'nylas-exports'
|
||||
{ListTabular,
|
||||
FluxContainer,
|
||||
{Actions} = require 'nylas-exports'
|
||||
{FluxContainer,
|
||||
MultiselectList} = require 'nylas-component-kit'
|
||||
DraftListStore = require './draft-list-store'
|
||||
DraftListColumns = require './draft-list-columns'
|
||||
|
@ -12,7 +10,6 @@ EmptyState = require './empty-state'
|
|||
|
||||
class DraftList extends React.Component
|
||||
@displayName: 'DraftList'
|
||||
|
||||
@containerRequired: false
|
||||
|
||||
render: =>
|
||||
|
@ -25,24 +22,28 @@ class DraftList extends React.Component
|
|||
onDoubleClick={@_onDoubleClick}
|
||||
emptyComponent={EmptyState}
|
||||
keymapHandlers={@_keymapHandlers()}
|
||||
itemPropsProvider={ -> {} }
|
||||
itemPropsProvider={@_itemPropsProvider}
|
||||
itemHeight={39}
|
||||
className="draft-list" />
|
||||
</FocusContainer>
|
||||
</FluxContainer>
|
||||
|
||||
_itemPropsProvider: (draft) ->
|
||||
props = {}
|
||||
props.className = 'sending' if draft.uploadTaskId
|
||||
props
|
||||
|
||||
_keymapHandlers: =>
|
||||
'core:remove-from-view': @_onRemoveFromView
|
||||
|
||||
_onDoubleClick: (item) =>
|
||||
Actions.composePopoutDraft(item.clientId)
|
||||
_onDoubleClick: (draft) =>
|
||||
unless draft.uploadTaskId
|
||||
Actions.composePopoutDraft(draft.clientId)
|
||||
|
||||
# Additional Commands
|
||||
|
||||
_onRemoveFromView: =>
|
||||
items = DraftListStore.dataSource().selection.items()
|
||||
for item in items
|
||||
Actions.destroyDraft(item.clientId)
|
||||
|
||||
drafts = DraftListStore.dataSource().selection.items()
|
||||
Actions.destroyDraft(draft.clientId) for draft in drafts
|
||||
|
||||
module.exports = DraftList
|
||||
|
|
32
internal_packages/thread-list/lib/sending-cancel-button.cjsx
Normal file
32
internal_packages/thread-list/lib/sending-cancel-button.cjsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
React = require 'react'
|
||||
{Actions} = require 'nylas-exports'
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
|
||||
class SendingCancelButton extends React.Component
|
||||
@displayName: 'SendingCancelButton'
|
||||
|
||||
@propTypes:
|
||||
taskId: React.PropTypes.string.isRequired
|
||||
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
cancelling: false
|
||||
|
||||
render: =>
|
||||
if @state.cancelling
|
||||
<RetinaImg
|
||||
style={width: 20, height: 20, marginTop: 2}
|
||||
name="inline-loading-spinner.gif"
|
||||
mode={RetinaImg.Mode.ContentPreserve} />
|
||||
else
|
||||
<div onClick={@_onClick} style={marginTop: 1}>
|
||||
<RetinaImg
|
||||
name="image-cancel-button.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve} />
|
||||
</div>
|
||||
|
||||
_onClick: =>
|
||||
Actions.dequeueTask(@props.taskId)
|
||||
@setState(cancelling: true)
|
||||
|
||||
module.exports = SendingCancelButton
|
21
internal_packages/thread-list/lib/sending-progress-bar.cjsx
Normal file
21
internal_packages/thread-list/lib/sending-progress-bar.cjsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
React = require 'react'
|
||||
_ = require 'underscore'
|
||||
|
||||
class SendingProgressBar extends React.Component
|
||||
@propTypes:
|
||||
progress: React.PropTypes.number.isRequired
|
||||
|
||||
render: ->
|
||||
otherProps = _.omit(@props, _.keys(@constructor.propTypes))
|
||||
if 0 < @props.progress < 99
|
||||
<div className="sending-progress" {...otherProps}>
|
||||
<div className="filled"
|
||||
style={width:"#{Math.min(100, @props.progress)}%"}>
|
||||
</div>
|
||||
</div>
|
||||
else
|
||||
<div className="sending-progress" {...otherProps}>
|
||||
<div className="indeterminate"></div>
|
||||
</div>
|
||||
|
||||
module.exports = SendingProgressBar
|
|
@ -102,7 +102,7 @@ c5 = new ListTabular.Column
|
|||
children=
|
||||
{[
|
||||
<ThreadTrashQuickAction key="thread-trash-quick-action" thread={thread} />
|
||||
<ThreadArchiveQuickAction key="thread-arhive-quick-action" thread={thread} />
|
||||
<ThreadArchiveQuickAction key="thread-archive-quick-action" thread={thread} />
|
||||
]}
|
||||
matching={role: "ThreadListQuickAction"}
|
||||
className="thread-injected-quick-actions"
|
||||
|
|
|
@ -63,12 +63,12 @@ _observableForThreadMessages = (id, initialModels) ->
|
|||
asResultSet: true,
|
||||
initialModels: initialModels
|
||||
})
|
||||
Rx.Observable.fromPrivateQuerySubscription('message-'+id, subscription)
|
||||
Rx.Observable.fromNamedQuerySubscription('message-'+id, subscription)
|
||||
|
||||
|
||||
class ThreadListDataSource extends ObservableListDataSource
|
||||
constructor: (subscription) ->
|
||||
$resultSetObservable = Rx.Observable.fromPrivateQuerySubscription('thread-list', subscription)
|
||||
$resultSetObservable = Rx.Observable.fromNamedQuerySubscription('thread-list', subscription)
|
||||
$resultSetObservable = _flatMapJoiningMessages($resultSetObservable)
|
||||
super($resultSetObservable, subscription.replaceRange)
|
||||
|
||||
|
|
|
@ -42,12 +42,11 @@
|
|||
}
|
||||
|
||||
.thread-list, .draft-list {
|
||||
order: 3;
|
||||
flex: 1;
|
||||
position:absolute;
|
||||
width:100%;
|
||||
height:100%;
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
.list-container, .scroll-region {
|
||||
width:100%;
|
||||
height:100%;
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
background-color: darken(@background-primary, 2%);
|
||||
|
@ -88,6 +87,10 @@
|
|||
overflow: hidden;
|
||||
position: relative;
|
||||
top:2px;
|
||||
|
||||
&.no-recipients {
|
||||
color: @text-color-very-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
|
@ -393,3 +396,61 @@ body.is-blurred {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sending-progress-move {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 50px 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.draft-list {
|
||||
.sending {
|
||||
background-color: @background-primary;
|
||||
&:hover {
|
||||
background-color: @background-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.sending-progress {
|
||||
display: block;
|
||||
height:7px;
|
||||
margin-top:10px;
|
||||
background-color: @background-primary;
|
||||
border-bottom:1px solid @border-color-divider;
|
||||
position: relative;
|
||||
|
||||
.filled {
|
||||
display: block;
|
||||
background: @component-active-color;
|
||||
height:6px;
|
||||
width: 0px; //overridden by style
|
||||
transition: width 1000ms linear;
|
||||
}
|
||||
.indeterminate {
|
||||
display: block;
|
||||
background: @component-active-color;
|
||||
height:6px;
|
||||
width: 100%;
|
||||
}
|
||||
.indeterminate:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0; left: 0; bottom: 0; right: 0;
|
||||
background-image: linear-gradient(
|
||||
-45deg,
|
||||
rgba(255, 255, 255, .2) 25%,
|
||||
transparent 25%,
|
||||
transparent 50%,
|
||||
rgba(255, 255, 255, .2) 50%,
|
||||
rgba(255, 255, 255, .2) 75%,
|
||||
transparent 75%,
|
||||
transparent
|
||||
);
|
||||
background-size: 50px 50px;
|
||||
animation: sending-progress-move 2s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ describe "ListSelection", ->
|
|||
@trigger = jasmine.createSpy('trigger')
|
||||
|
||||
@items = []
|
||||
@items.push(new Thread(id: "#{ii}")) for ii in [0..99]
|
||||
@items.push(new Thread(id: "#{ii}", clientId: "#{ii}")) for ii in [0..99]
|
||||
|
||||
@view = new ListDataSource()
|
||||
@view.indexOfId = jasmine.createSpy('indexOfId').andCallFake (id) =>
|
||||
|
@ -84,13 +84,13 @@ describe "ListSelection", ->
|
|||
@selection.set([@items[2], @items[4], @items[7]])
|
||||
expect(@selection.items()[0]).toBe(@items[2])
|
||||
expect(@selection.items()[0].subject).toBe(undefined)
|
||||
newItem2 = new Thread(id: '2', subject:'Hello world!')
|
||||
newItem2 = new Thread(id: '2', clientId: '2', subject:'Hello world!')
|
||||
@selection._applyChangeRecord({objectClass: 'Thread', objects: [newItem2], type: 'persist'})
|
||||
expect(@selection.items()[0].subject).toBe('Hello world!')
|
||||
|
||||
it "should rremove items in the selection if type is unpersist", ->
|
||||
@selection.set([@items[2], @items[4], @items[7]])
|
||||
newItem2 = new Thread(id: '2', subject:'Hello world!')
|
||||
newItem2 = new Thread(id: '2', clientId: '2', subject:'Hello world!')
|
||||
@selection._applyChangeRecord({objectClass: 'Thread', objects: [newItem2], type: 'unpersist'})
|
||||
expect(@selection.ids()).toEqual(['4', '7'])
|
||||
|
||||
|
|
|
@ -634,12 +634,12 @@ describe "DraftStore", ->
|
|||
}}
|
||||
|
||||
it "should return false and call window.close itself", ->
|
||||
spyOn(NylasEnv, 'finishUnload')
|
||||
expect(DraftStore._onBeforeUnload()).toBe(false)
|
||||
expect(NylasEnv.finishUnload).not.toHaveBeenCalled()
|
||||
callback = jasmine.createSpy('callback')
|
||||
expect(DraftStore._onBeforeUnload(callback)).toBe(false)
|
||||
expect(callback).not.toHaveBeenCalled()
|
||||
@resolve()
|
||||
advanceClock(1000)
|
||||
expect(NylasEnv.finishUnload).toHaveBeenCalled()
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
describe "when drafts return immediately fulfilled commit promises", ->
|
||||
beforeEach ->
|
||||
|
@ -651,11 +651,11 @@ describe "DraftStore", ->
|
|||
}}
|
||||
|
||||
it "should still wait one tick before firing NylasEnv.close again", ->
|
||||
spyOn(NylasEnv, 'finishUnload')
|
||||
expect(DraftStore._onBeforeUnload()).toBe(false)
|
||||
expect(NylasEnv.finishUnload).not.toHaveBeenCalled()
|
||||
callback = jasmine.createSpy('callback')
|
||||
expect(DraftStore._onBeforeUnload(callback)).toBe(false)
|
||||
expect(callback).not.toHaveBeenCalled()
|
||||
advanceClock()
|
||||
expect(NylasEnv.finishUnload).toHaveBeenCalled()
|
||||
expect(callback).toHaveBeenCalled()
|
||||
|
||||
describe "when there are no drafts", ->
|
||||
beforeEach ->
|
||||
|
|
|
@ -35,13 +35,13 @@ describe "TaskQueue", ->
|
|||
it "should fetch the queue from the database, reset flags and start processing", ->
|
||||
queue = [@processingTask, @unstartedTask]
|
||||
spyOn(DatabaseStore, 'findJSONBlob').andCallFake => Promise.resolve(queue)
|
||||
spyOn(TaskQueue, '_processQueue')
|
||||
spyOn(TaskQueue, '_updateSoon')
|
||||
|
||||
waitsForPromise =>
|
||||
TaskQueue._restoreQueue().then =>
|
||||
expect(TaskQueue._queue).toEqual(queue)
|
||||
expect(@processingTask.queueState.isProcessing).toEqual(false)
|
||||
expect(TaskQueue._processQueue).toHaveBeenCalled()
|
||||
expect(TaskQueue._updateSoon).toHaveBeenCalled()
|
||||
|
||||
describe "findTask", ->
|
||||
beforeEach ->
|
||||
|
|
|
@ -216,14 +216,12 @@ describe "SendDraftTask", ->
|
|||
expect(status[1]).toBe thrownError
|
||||
expect(Actions.draftSendingFailed).toHaveBeenCalled()
|
||||
|
||||
it "notifies of a permanent error on timeouts", ->
|
||||
it "retries on timeouts", ->
|
||||
thrownError = new APIError(statusCode: NylasAPI.TimeoutErrorCode, body: "err")
|
||||
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
|
||||
Promise.reject(thrownError)
|
||||
waitsForPromise => @task.performRemote().then (status) =>
|
||||
expect(status[0]).toBe Task.Status.Failed
|
||||
expect(status[1]).toBe thrownError
|
||||
expect(Actions.draftSendingFailed).toHaveBeenCalled()
|
||||
expect(status).toBe Task.Status.Retry
|
||||
|
||||
describe "checking the promise chain halts on errors", ->
|
||||
beforeEach ->
|
||||
|
|
|
@ -45,7 +45,7 @@ class ListSelection
|
|||
return unless item
|
||||
throw new Error("toggle must be called with a Model") unless item instanceof Model
|
||||
|
||||
without = _.reject @_items, (t) -> t.id is item.id
|
||||
without = _.reject @_items, (t) -> t.clientId is item.clientId
|
||||
if without.length < @_items.length
|
||||
@_items = without
|
||||
else
|
||||
|
@ -56,7 +56,7 @@ class ListSelection
|
|||
return unless item
|
||||
throw new Error("add must be called with a Model") unless item instanceof Model
|
||||
|
||||
updated = _.reject @_items, (t) -> t.id is item.id
|
||||
updated = _.reject @_items, (t) -> t.clientId is item.clientId
|
||||
updated.push(item)
|
||||
if updated.length isnt @_items.length
|
||||
@_items = updated
|
||||
|
@ -73,7 +73,7 @@ class ListSelection
|
|||
|
||||
itemIds = _.pluck(items, 'id')
|
||||
|
||||
without = _.reject @_items, (t) -> t.id in itemIds
|
||||
without = _.reject @_items, (t) -> t.clientId in itemIds
|
||||
if without.length < @_items.length
|
||||
@_items = without
|
||||
@trigger(@)
|
||||
|
@ -97,12 +97,12 @@ class ListSelection
|
|||
# items are in the _items array in the order they were selected.
|
||||
# (important for walking)
|
||||
relativeTo = @_items[@_items.length - 1]
|
||||
startIdx = @_view.indexOfId(relativeTo.id)
|
||||
endIdx = @_view.indexOfId(item.id)
|
||||
startIdx = @_view.indexOfId(relativeTo.clientId)
|
||||
endIdx = @_view.indexOfId(item.clientId)
|
||||
return if startIdx is -1 or endIdx is -1
|
||||
for idx in [startIdx..endIdx]
|
||||
item = @_view.get(idx)
|
||||
@_items = _.reject @_items, (t) -> t.id is item.id
|
||||
@_items = _.reject @_items, (t) -> t.clientId is item.clientId
|
||||
@_items.push(item)
|
||||
@trigger()
|
||||
|
||||
|
@ -116,7 +116,7 @@ class ListSelection
|
|||
|
||||
ids = @ids()
|
||||
noSelection = @_items.length is 0
|
||||
neitherSelected = (not current or ids.indexOf(current.id) is -1) and (not next or ids.indexOf(next.id) is -1)
|
||||
neitherSelected = (not current or ids.indexOf(current.clientId) is -1) and (not next or ids.indexOf(next.clientId) is -1)
|
||||
|
||||
if noSelection or neitherSelected
|
||||
@_items.push(current) if current
|
||||
|
@ -124,15 +124,15 @@ class ListSelection
|
|||
else
|
||||
selectionPostPopHeadId = null
|
||||
if @_items.length > 1
|
||||
selectionPostPopHeadId = @_items[@_items.length - 2].id
|
||||
selectionPostPopHeadId = @_items[@_items.length - 2].clientId
|
||||
|
||||
if next.id is selectionPostPopHeadId
|
||||
if next.clientId is selectionPostPopHeadId
|
||||
@_items.pop()
|
||||
else
|
||||
# Important: As you walk over this item, remove it and re-push it on the selected
|
||||
# array even if it's already there. That way, the items in _items are always
|
||||
# in the order you walked over them, and you can walk back to deselect them.
|
||||
@_items = _.reject @_items, (t) -> t.id is next.id
|
||||
@_items = _.reject @_items, (t) -> t.clientId is next.clientId
|
||||
@_items.push(next)
|
||||
|
||||
@trigger()
|
||||
|
@ -147,7 +147,7 @@ class ListSelection
|
|||
touched = 0
|
||||
for newer in change.objects
|
||||
for existing, idx in @_items
|
||||
if existing.id is newer.id
|
||||
if existing.clientId is newer.clientId
|
||||
@_items[idx] = newer
|
||||
touched += 1
|
||||
break
|
||||
|
|
|
@ -40,12 +40,12 @@ class ListTabular extends React.Component
|
|||
componentWillReceiveProps: (nextProps) =>
|
||||
if nextProps.dataSource isnt @props.dataSource
|
||||
@setupDataSource(nextProps.dataSource)
|
||||
@setState(@buildStateForRange(dataSource: nextProps.dataSource))
|
||||
|
||||
setupDataSource: (dataSource) =>
|
||||
@_unlisten?()
|
||||
@_unlisten = dataSource.listen =>
|
||||
@setState(@buildStateForRange())
|
||||
@setState(@buildStateForRange(dataSource: dataSource))
|
||||
|
||||
buildStateForRange: ({dataSource, start, end} = {}) =>
|
||||
start ?= @state.renderedRangeStart
|
||||
|
@ -117,12 +117,11 @@ class ListTabular extends React.Component
|
|||
if @props.emptyComponent
|
||||
emptyElement = <@props.emptyComponent visible={@state.loaded and @state.empty} />
|
||||
|
||||
<div className={@props.className}>
|
||||
<div className="list-container list-tabular #{@props.className}">
|
||||
<ScrollRegion
|
||||
ref="container"
|
||||
onScroll={@onScroll}
|
||||
tabIndex="-1"
|
||||
className="list-container list-tabular"
|
||||
scrollTooltipComponent={@props.scrollTooltipComponent}>
|
||||
<div className="list-rows" style={innerStyles} {...otherProps}>
|
||||
{@_rows()}
|
||||
|
|
|
@ -112,10 +112,9 @@ class MultiselectList extends React.Component
|
|||
props['data-item-id'] = item.id
|
||||
props
|
||||
|
||||
<KeyCommandsRegion globalHandlers={@_globalKeymapHandlers()} className="multiselect-list">
|
||||
<KeyCommandsRegion globalHandlers={@_globalKeymapHandlers()} className={className}>
|
||||
<ListTabular
|
||||
ref="list"
|
||||
className={className}
|
||||
columns={@state.computedColumns}
|
||||
dataSource={@props.dataSource}
|
||||
itemPropsProvider={@itemPropsProvider}
|
||||
|
|
|
@ -40,9 +40,12 @@ class ActionBridge
|
|||
|
||||
constructor: (ipc) ->
|
||||
@ipc = ipc
|
||||
@ipcLastSendTime = null
|
||||
@initiatorId = NylasEnv.getWindowType()
|
||||
@role = if NylasEnv.isWorkWindow() then Role.WORK else Role.SECONDARY
|
||||
|
||||
NylasEnv.onBeforeUnload(@onBeforeUnload)
|
||||
|
||||
# Listen for action bridge messages from other windows
|
||||
@ipc.on('action-bridge-message', @onIPCMessage)
|
||||
|
||||
|
@ -104,6 +107,16 @@ class ActionBridge
|
|||
|
||||
console.debug(printToConsole, "ActionBridge: #{@initiatorId} Action Bridge Broadcasting: #{name}")
|
||||
@ipc.send("action-bridge-rebroadcast-to-#{target}", @initiatorId, name, json)
|
||||
@ipcLastSendTime = Date.now()
|
||||
|
||||
onBeforeUnload: (readyToUnload) =>
|
||||
# Unfortunately, if you call ipc.send and then immediately close the window,
|
||||
# Electron won't actually send the message. To work around this, we wait an
|
||||
# arbitrary amount of time before closing the window after the last IPC event
|
||||
# was sent. https://github.com/atom/electron/issues/4366
|
||||
if @ipcLastSendTime and Date.now() - @ipcLastSendTime < 100
|
||||
setTimeout(readyToUnload, 100)
|
||||
return false
|
||||
return true
|
||||
|
||||
module.exports = ActionBridge
|
||||
|
|
|
@ -35,11 +35,12 @@ class QueryResultSet
|
|||
set
|
||||
|
||||
constructor: (other = {}) ->
|
||||
@_modelsHash = other._modelsHash ? {}
|
||||
@_offset = other._offset ? null
|
||||
@_query = other._query ? null
|
||||
@_ids = other._ids ? []
|
||||
@_idToIndexHash = other._idToIndexHash ? null
|
||||
# Clone, since the others may be frozen
|
||||
@_modelsHash = Object.assign({}, other._modelsHash ? {})
|
||||
@_ids = [].concat(other._ids ? [])
|
||||
|
||||
clone: ->
|
||||
new @constructor({
|
||||
|
|
|
@ -8,6 +8,7 @@ DraftStoreProxy = require './draft-store-proxy'
|
|||
DatabaseStore = require './database-store'
|
||||
AccountStore = require './account-store'
|
||||
ContactStore = require './contact-store'
|
||||
TaskQueueStatusStore = require './task-queue-status-store'
|
||||
FocusedPerspectiveStore = require './focused-perspective-store'
|
||||
FocusedContentStore = require './focused-content-store'
|
||||
|
||||
|
@ -161,7 +162,7 @@ class DraftStore
|
|||
for draftClientId, session of @_draftSessions
|
||||
@_doneWithSession(session)
|
||||
|
||||
_onBeforeUnload: =>
|
||||
_onBeforeUnload: (readyToUnload) =>
|
||||
promises = []
|
||||
|
||||
# Normally we'd just append all promises, even the ones already
|
||||
|
@ -180,7 +181,7 @@ class DraftStore
|
|||
# handler, so we need to always defer by one tick before re-firing close.
|
||||
Promise.settle(promises).then =>
|
||||
@_draftSessions = {}
|
||||
NylasEnv.finishUnload()
|
||||
readyToUnload()
|
||||
|
||||
# Stop and wait before closing
|
||||
return false
|
||||
|
@ -487,6 +488,11 @@ class DraftStore
|
|||
if session
|
||||
@_doneWithSession(session)
|
||||
|
||||
# Stop any pending SendDraftTasks
|
||||
for task in TaskQueueStatusStore.queue()
|
||||
if task instanceof SendDraftTask and task.draft.clientId is draftClientId
|
||||
Actions.dequeueTask(task.id)
|
||||
|
||||
# Queue the task to destroy the draft
|
||||
Actions.queueTask(new DestroyDraftTask(draftClientId: draftClientId))
|
||||
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
_ = require 'underscore'
|
||||
Rx = require 'rx-lite'
|
||||
DatabaseStore = require './database-store'
|
||||
Message = require '../models/message'
|
||||
QuerySubscriptionPool = require '../models/query-subscription-pool'
|
||||
QuerySubscription = require '../models/query-subscription'
|
||||
MutableQuerySubscription = require '../models/mutable-query-subscription'
|
||||
|
||||
{ListTabular} = require 'nylas-component-kit'
|
||||
|
||||
###
|
||||
|
|
27
src/flux/stores/outbox-store.es6
Normal file
27
src/flux/stores/outbox-store.es6
Normal file
|
@ -0,0 +1,27 @@
|
|||
import _ from 'underscore';
|
||||
import NylasStore from 'nylas-store';
|
||||
import SendDraftTask from '../tasks/send-draft';
|
||||
import TaskQueueStatusStore from './task-queue-status-store';
|
||||
|
||||
class OutboxStore extends NylasStore {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.listenTo(TaskQueueStatusStore, this._populate);
|
||||
this._populate();
|
||||
}
|
||||
|
||||
_populate() {
|
||||
this._tasks = TaskQueueStatusStore.queue().filter((task)=> {
|
||||
return task instanceof SendDraftTask;
|
||||
});
|
||||
this.trigger();
|
||||
}
|
||||
|
||||
itemsForAccount(accountId) {
|
||||
return this._tasks.filter((task)=> {
|
||||
return task.draft.accountId === accountId;
|
||||
});
|
||||
}
|
||||
}
|
||||
module.exports = new OutboxStore();
|
|
@ -73,6 +73,7 @@ class TaskQueue
|
|||
constructor: ->
|
||||
@_queue = []
|
||||
@_completed = []
|
||||
@_updatePeriodicallyTimeout = null
|
||||
|
||||
@_restoreQueue()
|
||||
|
||||
|
@ -169,12 +170,18 @@ class TaskQueue
|
|||
# Helper Methods
|
||||
|
||||
_processQueue: =>
|
||||
started = 0
|
||||
|
||||
for task in @_queue by -1
|
||||
if @_taskIsBlocked(task)
|
||||
task.queueState.debugStatus = Task.DebugStatus.WaitingOnDependency
|
||||
continue
|
||||
else
|
||||
@_processTask(task)
|
||||
started += 1
|
||||
|
||||
if started > 0
|
||||
@trigger()
|
||||
|
||||
_processTask: (task) =>
|
||||
return if task.queueState.isProcessing
|
||||
|
@ -267,7 +274,7 @@ class TaskQueue
|
|||
task.queueState ?= {}
|
||||
task.queueState.isProcessing = false
|
||||
@_queue = queue
|
||||
@_processQueue()
|
||||
@_updateSoon()
|
||||
|
||||
_updateSoon: =>
|
||||
@_updateSoonThrottled ?= _.throttle =>
|
||||
|
@ -275,9 +282,24 @@ class TaskQueue
|
|||
t.persistJSONBlob(JSONBlobStorageKey, @_queue ? [])
|
||||
_.defer =>
|
||||
@_processQueue()
|
||||
@trigger()
|
||||
@_ensurePeriodicUpdates()
|
||||
, 10
|
||||
|
||||
@_updateSoonThrottled()
|
||||
|
||||
_ensurePeriodicUpdates: =>
|
||||
anyIsProcessing = _.any @_queue, (task) -> task.queueState.isProcessing
|
||||
|
||||
# The task queue triggers periodically as tasks are processed, even if no
|
||||
# major events have occurred. This allows tasks which have state, like
|
||||
# SendDraftTask.progress to be propogated through the app and inspected.
|
||||
if anyIsProcessing and not @_updatePeriodicallyTimeout
|
||||
@_updatePeriodicallyTimeout = setInterval =>
|
||||
@_updateSoon()
|
||||
, 1000
|
||||
else if not anyIsProcessing and @_updatePeriodicallyTimeout
|
||||
clearTimeout(@_updatePeriodicallyTimeout)
|
||||
@_updatePeriodicallyTimeout = null
|
||||
|
||||
module.exports = new TaskQueue()
|
||||
module.exports.JSONBlobStorageKey = JSONBlobStorageKey
|
||||
|
|
|
@ -7,7 +7,7 @@ ThreadCountsStore = require './thread-counts-store'
|
|||
class UnreadBadgeStore extends NylasStore
|
||||
|
||||
constructor: ->
|
||||
@_count = FocusedPerspectiveStore.current().threadUnreadCount()
|
||||
@_count = FocusedPerspectiveStore.current().unreadCount()
|
||||
|
||||
@listenTo FocusedPerspectiveStore, @_updateCount
|
||||
@listenTo ThreadCountsStore, @_updateCount
|
||||
|
@ -26,7 +26,7 @@ class UnreadBadgeStore extends NylasStore
|
|||
_updateCount: =>
|
||||
current = FocusedPerspectiveStore.current()
|
||||
if current.isInbox()
|
||||
count = current.threadUnreadCount()
|
||||
count = current.unreadCount()
|
||||
return if @_count is count
|
||||
@_count = count
|
||||
@_setBadgeForCount()
|
||||
|
|
|
@ -10,30 +10,22 @@ SendDraftTask = require './send-draft'
|
|||
|
||||
module.exports =
|
||||
class DestroyDraftTask extends Task
|
||||
constructor: ({@draftClientId, @draftId} = {}) -> super
|
||||
constructor: ({@draftClientId} = {}) ->
|
||||
super
|
||||
|
||||
shouldDequeueOtherTask: (other) ->
|
||||
if @draftClientId
|
||||
(other instanceof DestroyDraftTask and other.draftClientId is @draftClientId) or
|
||||
(other instanceof SyncbackDraftTask and other.draftClientId is @draftClientId) or
|
||||
(other instanceof SendDraftTask and other.draftClientId is @draftClientId)
|
||||
else if @draftId
|
||||
(other instanceof DestroyDraftTask and other.draftClientId is @draftClientId)
|
||||
else
|
||||
false
|
||||
(other instanceof DestroyDraftTask and other.draftClientId is @draftClientId) or
|
||||
(other instanceof SyncbackDraftTask and other.draftClientId is @draftClientId) or
|
||||
(other instanceof SendDraftTask and other.draftClientId is @draftClientId)
|
||||
|
||||
isDependentTask: (other) ->
|
||||
(other instanceof SyncbackDraftTask and other.draftClientId is @draftClientId)
|
||||
|
||||
performLocal: ->
|
||||
if @draftClientId
|
||||
find = DatabaseStore.findBy(Message, clientId: @draftClientId)
|
||||
else if @draftId
|
||||
find = DatabaseStore.find(Message, @draftId)
|
||||
else
|
||||
unless @draftClientId
|
||||
return Promise.reject(new Error("Attempt to call DestroyDraftTask.performLocal without draftClientId"))
|
||||
|
||||
find.include(Message.attributes.body).then (draft) =>
|
||||
DatabaseStore.findBy(Message, clientId: @draftClientId).include(Message.attributes.body).then (draft) =>
|
||||
return Promise.resolve() unless draft
|
||||
@draft = draft
|
||||
DatabaseStore.inTransaction (t) =>
|
||||
|
@ -47,7 +39,10 @@ class DestroyDraftTask extends Task
|
|||
err = new Error("No valid draft to destroy!")
|
||||
return Promise.resolve([Task.Status.Failed, err])
|
||||
|
||||
if not @draft.serverId or not @draft.version?
|
||||
if not @draft.serverId
|
||||
return Promise.resolve(Task.Status.Continue)
|
||||
|
||||
if not @draft.version?
|
||||
err = new Error("Can't destroy draft without a version or serverId")
|
||||
return Promise.resolve([Task.Status.Failed, err])
|
||||
|
||||
|
|
|
@ -26,11 +26,14 @@ class MultiRequestProgressMonitor
|
|||
delete @_requests[filepath]
|
||||
delete @_expected[filepath]
|
||||
|
||||
requests: =>
|
||||
_.values(@_requests)
|
||||
|
||||
value: =>
|
||||
sent = 0
|
||||
expected = 1
|
||||
for filepath, req of @_requests
|
||||
sent += @req?.req?.connection?._bytesDispatched ? 0
|
||||
for filepath, request of @_requests
|
||||
sent += request.req?.connection?._bytesDispatched ? 0
|
||||
expected += @_expected[filepath]
|
||||
|
||||
return sent / expected
|
||||
|
@ -39,6 +42,7 @@ module.exports =
|
|||
class SendDraftTask extends Task
|
||||
|
||||
constructor: (@draft) ->
|
||||
@uploaded = []
|
||||
super
|
||||
|
||||
label: ->
|
||||
|
@ -75,15 +79,29 @@ class SendDraftTask extends Task
|
|||
Promise.resolve()
|
||||
|
||||
performRemote: ->
|
||||
@_uploadAttachments()
|
||||
.then(@_sendAndCreateMessage)
|
||||
.then(@_deleteRemoteDraft)
|
||||
.then(@_onSuccess)
|
||||
.catch(@_onError)
|
||||
@_uploadAttachments().then =>
|
||||
return Promise.resolve(Task.Status.Continue) if @_cancelled
|
||||
@_sendAndCreateMessage()
|
||||
.then(@_deleteRemoteDraft)
|
||||
.then(@_onSuccess)
|
||||
.catch(@_onError)
|
||||
|
||||
cancel: =>
|
||||
# Note that you can only cancel during the uploadAttachments phase. Once
|
||||
# we hit sendAndCreateMessage, nothing checks the cancelled bit and
|
||||
# performRemote will continue through to success.
|
||||
@_cancelled = true
|
||||
for request in @_attachmentUploadsMonitor.requests()
|
||||
request.abort()
|
||||
@
|
||||
|
||||
_uploadAttachments: =>
|
||||
progress = new MultiRequestProgressMonitor()
|
||||
Object.defineProperty(@, 'progress', { get: -> progress.value() })
|
||||
@_attachmentUploadsMonitor = new MultiRequestProgressMonitor()
|
||||
Object.defineProperty(@, 'progress', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: => @_attachmentUploadsMonitor.value()
|
||||
})
|
||||
|
||||
Promise.all @draft.uploads.map (upload) =>
|
||||
{targetPath, size} = upload
|
||||
|
@ -101,18 +119,20 @@ class SendDraftTask extends Task
|
|||
json: false
|
||||
formData: formData
|
||||
started: (req) =>
|
||||
progress.add(targetPath, size, req)
|
||||
@_attachmentUploadsMonitor.add(targetPath, size, req)
|
||||
timeout: 20 * 60 * 1000
|
||||
.finally =>
|
||||
progress.remove(targetPath)
|
||||
@_attachmentUploadsMonitor.remove(targetPath)
|
||||
.then (rawResponseString) =>
|
||||
json = JSON.parse(rawResponseString)
|
||||
file = (new File).fromJSON(json[0])
|
||||
@uploaded.push(upload)
|
||||
@draft.uploads.splice(@draft.uploads.indexOf(upload), 1)
|
||||
@draft.files.push(file)
|
||||
|
||||
# Deletes the attachment from the uploads folder
|
||||
Actions.attachmentUploaded(upload)
|
||||
# Note: We don't actually delete uploaded files until send completes,
|
||||
# because it's possible for the app to quit without saving state and
|
||||
# need to re-upload the file.
|
||||
|
||||
_sendAndCreateMessage: =>
|
||||
NylasAPI.makeRequest
|
||||
|
@ -169,6 +189,10 @@ class SendDraftTask extends Task
|
|||
Actions.sendDraftSuccess
|
||||
draftClientId: @draft.clientId
|
||||
|
||||
# Delete attachments from the uploads folder
|
||||
for upload in @uploaded
|
||||
Actions.attachmentUploaded(upload)
|
||||
|
||||
# Play the sending sound
|
||||
if NylasEnv.config.get("core.sending.sounds")
|
||||
SoundRegistry.playSound('send')
|
||||
|
@ -176,16 +200,12 @@ class SendDraftTask extends Task
|
|||
return Promise.resolve(Task.Status.Success)
|
||||
|
||||
_onError: (err) =>
|
||||
# OUTBOX COMING SOON!
|
||||
|
||||
msg = "Your message could not be sent. Check your network connection and try again."
|
||||
if err instanceof APIError and err.statusCode is NylasAPI.TimeoutErrorCode
|
||||
msg = "We lost internet connection just as we were trying to send your message! Please wait a little bit to see if it went through. If not, check your internet connection and try sending again."
|
||||
|
||||
Actions.draftSendingFailed
|
||||
threadId: @draft.threadId
|
||||
draftClientId: @draft.clientId,
|
||||
errorMessage: msg
|
||||
NylasEnv.reportError(err)
|
||||
|
||||
return Promise.resolve([Task.Status.Failed, err])
|
||||
if err instanceof APIError and not (err.statusCode in NylasAPI.PermanentErrorCodes)
|
||||
return Promise.resolve(Task.Status.Retry)
|
||||
else
|
||||
Actions.draftSendingFailed
|
||||
threadId: @draft.threadId
|
||||
draftClientId: @draft.clientId,
|
||||
errorMessage: "Your message could not be sent. Check your network connection and try again."
|
||||
NylasEnv.reportError(err)
|
||||
return Promise.resolve([Task.Status.Failed, err])
|
||||
|
|
|
@ -54,6 +54,7 @@ class NylasExports
|
|||
@load "DatabaseStore", 'flux/stores/database-store'
|
||||
@load "DatabaseTransaction", 'flux/stores/database-transaction'
|
||||
@load "QueryResultSet", 'flux/models/query-result-set'
|
||||
@load "MutableQueryResultSet", 'flux/models/mutable-query-result-set'
|
||||
@load "ObservableListDataSource", 'flux/stores/observable-list-data-source'
|
||||
@load "QuerySubscription", 'flux/models/query-subscription'
|
||||
@load "MutableQuerySubscription", 'flux/models/mutable-query-subscription'
|
||||
|
@ -115,6 +116,7 @@ class NylasExports
|
|||
# listen-only and not explicitly required from anywhere. Stores
|
||||
# currently set themselves up on require.
|
||||
@require "DraftStore", 'flux/stores/draft-store'
|
||||
@require "OutboxStore", 'flux/stores/outbox-store'
|
||||
@require "AccountStore", 'flux/stores/account-store'
|
||||
@require "MessageStore", 'flux/stores/message-store'
|
||||
@require "ContactStore", 'flux/stores/contact-store'
|
||||
|
|
|
@ -93,7 +93,7 @@ Rx.Observable.fromQuery = (query) =>
|
|||
observer.onNext(result)
|
||||
return Rx.Disposable.create(unsubscribe)
|
||||
|
||||
Rx.Observable.fromPrivateQuerySubscription = (name, subscription) =>
|
||||
Rx.Observable.fromNamedQuerySubscription = (name, subscription) =>
|
||||
return Rx.Observable.create (observer) =>
|
||||
unsubscribe = QuerySubscriptionPool.addPrivateSubscription name, subscription, (result) =>
|
||||
observer.onNext(result)
|
||||
|
|
|
@ -4,6 +4,7 @@ TaskFactory = require './flux/tasks/task-factory'
|
|||
AccountStore = require './flux/stores/account-store'
|
||||
CategoryStore = require './flux/stores/category-store'
|
||||
DatabaseStore = require './flux/stores/database-store'
|
||||
OutboxStore = require './flux/stores/outbox-store'
|
||||
SearchSubscription = require './search-subscription'
|
||||
ThreadCountsStore = require './flux/stores/thread-counts-store'
|
||||
MutableQuerySubscription = require './flux/models/mutable-query-subscription'
|
||||
|
@ -67,7 +68,7 @@ class MailboxPerspective
|
|||
threads: =>
|
||||
throw new Error("threads: Not implemented in base class.")
|
||||
|
||||
threadUnreadCount: =>
|
||||
unreadCount: =>
|
||||
0
|
||||
|
||||
# Public:
|
||||
|
@ -149,6 +150,11 @@ class DraftsMailboxPerspective extends MailboxPerspective
|
|||
threads: =>
|
||||
null
|
||||
|
||||
unreadCount: =>
|
||||
count = 0
|
||||
count += OutboxStore.itemsForAccount(aid).length for aid in @accountIds
|
||||
count
|
||||
|
||||
canReceiveThreads: =>
|
||||
false
|
||||
|
||||
|
@ -233,7 +239,7 @@ class CategoryMailboxPerspective extends MailboxPerspective
|
|||
|
||||
return new MutableQuerySubscription(query, {asResultSet: true})
|
||||
|
||||
threadUnreadCount: =>
|
||||
unreadCount: =>
|
||||
sum = 0
|
||||
for cat in @_categories
|
||||
sum += ThreadCountsStore.unreadCountForCategoryId(cat.id)
|
||||
|
|
|
@ -189,6 +189,8 @@ class NylasEnvConstructor extends Model
|
|||
if event.binding.command.indexOf('application:') is 0 and event.binding.selector.indexOf("body") is 0
|
||||
ipcRenderer.send('command', event.binding.command)
|
||||
|
||||
@windowEventHandler = new WindowEventHandler
|
||||
|
||||
unless @inSpecMode()
|
||||
@actionBridge = new ActionBridge(ipcRenderer)
|
||||
|
||||
|
@ -210,7 +212,6 @@ class NylasEnvConstructor extends Model
|
|||
@spellchecker = require('./nylas-spellchecker')
|
||||
|
||||
@subscribe @packages.onDidActivateInitialPackages => @watchThemes()
|
||||
@windowEventHandler = new WindowEventHandler
|
||||
|
||||
# This ties window.onerror and Promise.onPossiblyUnhandledRejection to
|
||||
# the publically callable `reportError` method. This will take care of
|
||||
|
@ -938,16 +939,6 @@ class NylasEnvConstructor extends Model
|
|||
onBeforeUnload: (callback) ->
|
||||
@windowEventHandler.addUnloadCallback(callback)
|
||||
|
||||
# Call this method to resume the close / quit process if you returned
|
||||
# false from a onBeforeUnload handler.
|
||||
#
|
||||
finishUnload: ->
|
||||
_.defer =>
|
||||
if remote.getGlobal('application').quitting
|
||||
remote.require('app').quit()
|
||||
else
|
||||
@close()
|
||||
|
||||
enhanceEventObject: ->
|
||||
overriddenStop = Event::stopPropagation
|
||||
Event::stopPropagation = ->
|
||||
|
|
|
@ -116,16 +116,27 @@ class WindowEventHandler
|
|||
@unloadCallbacks.push(callback)
|
||||
|
||||
runUnloadCallbacks: ->
|
||||
continueUnload = true
|
||||
unloadCallbacksRunning = 0
|
||||
unloadCallbackComplete = =>
|
||||
unloadCallbacksRunning -= 1
|
||||
if unloadCallbacksRunning is 0
|
||||
@runUnloadFinished()
|
||||
|
||||
for callback in @unloadCallbacks
|
||||
returnValue = callback()
|
||||
if returnValue is true
|
||||
continue
|
||||
else if returnValue is false
|
||||
continueUnload = false
|
||||
else
|
||||
returnValue = callback(unloadCallbackComplete)
|
||||
if returnValue is false
|
||||
unloadCallbacksRunning += 1
|
||||
else if returnValue isnt true
|
||||
console.warn "You registered an `onBeforeUnload` callback that does not return either exactly `true` or `false`. It returned #{returnValue}", callback
|
||||
return continueUnload
|
||||
|
||||
return (unloadCallbacksRunning > 0)
|
||||
|
||||
runUnloadFinished: ->
|
||||
_.defer =>
|
||||
if remote.getGlobal('application').quitting
|
||||
remote.require('app').quit()
|
||||
else
|
||||
@close()
|
||||
|
||||
# Wire commands that should be handled by Chromium for elements with the
|
||||
# `.override-key-bindings` class.
|
||||
|
|
Loading…
Add table
Reference in a new issue