mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-11-13 21:24:58 +08:00
45bb16561f
Summary: This diff does a couple things: - Undo redo with a new undo/redo store that maintains it's own queue of undo/redo tasks. This queue is separate from the TaskQueue because not all tasks should be considered for undo history! Right now just the AddRemoveTagsTask is undoable. - NylasAPI.makeRequest now returns a promise which resolves with the result or rejects with an error. For things that still need them, there's still `success` and `error` callbacks. I also added `started:(req) ->` which allows you to get the underlying request. - Aborting a NylasAPI request now makes it call it's error callback / promise reject. - You can now run code after perform local has completed using this syntax: ``` task = new AddRemoveTagsTask(focused, ['archive'], ['inbox']) task.waitForPerformLocal().then -> Actions.setFocus(collection: 'thread', item: nextFocus) Actions.setCursorPosition(collection: 'thread', item: nextKeyboard) Actions.queueTask(task) ``` - In specs, you can now use `advanceClock` to get through a Promise.then/catch/finally. Turns out it was using something low level and not using setTimeout(0). - The TaskQueue uses promises better and defers a lot of the complexity around queueState for performLocal/performRemote to a task subclass called APITask. APITask implements "perform" and breaks it into "performLocal" and "performRemote". - All tasks either resolve or reject. They're always removed from the queue, unless they resolve with Task.Status.Retry, which means they internally did a .catch (err) => Promise.resolve(Task.Status.Retry) and they want to be run again later. - API tasks retry until they succeed or receive a NylasAPI.PermanentErrorCode (400,404,500), in which case they revert and finish. - The AddRemoveTags Task can now take more than one thread! This is super cool because you can undo/redo a bulk action and also because we'll probably have a bulk tag modification API endpoint soon. Getting undo / redo working revealed that the thread versioning system we built isn't working because the server was incrementing things by more than 1 at a time. Now we count the number of unresolved "optimistic" changes we've made to a given model, and only accept the server's version of it once the number of optimistic changes is back at zero. Known Issues: - AddRemoveTagsTasks aren't dependent on each other, so if you (undo/redo x lots) and then come back online, all the tasks try to add / remove all the tags at the same time. To fix this we can either allow the tasks to be merged together into a minimal set or make them block on each other. - When Offline, you still get errors in the console for GET requests. Need to catch these and display an offline status bar. - The metadata tasks haven't been updated yet to the new API. Wanted to get it reviewed first! Test Plan: All the tests still pass! Reviewers: evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D1694
129 lines
4 KiB
CoffeeScript
129 lines
4 KiB
CoffeeScript
_ = require 'underscore'
|
|
React = require 'react'
|
|
classNames = require 'classnames'
|
|
{RetinaImg} = require 'nylas-component-kit'
|
|
{DatabaseView,
|
|
NamespaceStore,
|
|
NylasAPI,
|
|
WorkspaceStore} = require 'nylas-exports'
|
|
|
|
EmptyMessages = [{
|
|
"body":"The pessimist complains about the wind.\nThe optimist expects it to change.\nThe realist adjusts the sails."
|
|
"byline": "- William Arthur Ward"
|
|
},{
|
|
"body":"The best and most beautiful things in the world cannot be seen or even touched - they must be felt with the heart."
|
|
"byline": "- Hellen Keller"
|
|
},{
|
|
"body":"Believe you can and you're halfway there."
|
|
"byline": "- Theodore Roosevelt"
|
|
},{
|
|
"body":"Don't judge each day by the harvest you reap but by the seeds that you plant."
|
|
"byline": "- Robert Louis Stevenson"
|
|
}]
|
|
|
|
class ContentGeneric extends React.Component
|
|
render: ->
|
|
<div className="generic">
|
|
<div className="message">
|
|
{@props.messageOverride ? "No threads to display."}
|
|
</div>
|
|
</div>
|
|
|
|
class ContentQuotes extends React.Component
|
|
@displayName = 'Quotes'
|
|
|
|
constructor: (@props) ->
|
|
@state = {}
|
|
|
|
componentDidMount: ->
|
|
# Pick a random quote using the day as a seed. I know not all months have
|
|
# 31 days - this is good enough to generate one quote a day at random!
|
|
d = new Date()
|
|
r = d.getDate() + d.getMonth() * 31
|
|
message = EmptyMessages[r % EmptyMessages.length]
|
|
@setState(message: message)
|
|
|
|
render: ->
|
|
<div className="quotes">
|
|
{@_renderMessage()}
|
|
<RetinaImg mode={RetinaImg.Mode.ContentLight} url="nylas://thread-list/assets/blank-bottom-left@2x.png" className="bottom-left"/>
|
|
<RetinaImg mode={RetinaImg.Mode.ContentLight} url="nylas://thread-list/assets/blank-top-left@2x.png" className="top-left"/>
|
|
<RetinaImg mode={RetinaImg.Mode.ContentLight} url="nylas://thread-list/assets/blank-bottom-right@2x.png" className="bottom-right"/>
|
|
<RetinaImg mode={RetinaImg.Mode.ContentLight} url="nylas://thread-list/assets/blank-top-right@2x.png" className="top-right"/>
|
|
</div>
|
|
|
|
_renderMessage: ->
|
|
if @props.messageOverride
|
|
<div className="message">{@props.messageOverride}</div>
|
|
else
|
|
<div className="message">
|
|
{@state.message?.body}
|
|
<div className="byline">
|
|
{@state.message?.byline}
|
|
</div>
|
|
</div>
|
|
|
|
|
|
class EmptyState extends React.Component
|
|
@displayName = 'EmptyState'
|
|
@propTypes =
|
|
visible: React.PropTypes.bool.isRequired
|
|
dataView: React.PropTypes.object
|
|
|
|
constructor: (@props) ->
|
|
@state =
|
|
layoutMode: WorkspaceStore.layoutMode()
|
|
syncing: false
|
|
active: false
|
|
|
|
componentDidMount: ->
|
|
@_unlisteners = []
|
|
@_unlisteners.push WorkspaceStore.listen(@_onChange, @)
|
|
@_unlisteners.push NamespaceStore.listen(@_onNamespacesChanged, @)
|
|
@_onNamespacesChanged()
|
|
|
|
_onNamespacesChanged: ->
|
|
namespace = NamespaceStore.current()
|
|
@_worker = NylasAPI.workerForNamespace(namespace)
|
|
@_workerUnlisten() if @_workerUnlisten
|
|
@_workerUnlisten = @_worker.listen(@_onChange, @)
|
|
@setState(syncing: @_worker.busy())
|
|
|
|
componentWillUnmount: ->
|
|
unlisten() for unlisten in @_unlisteners
|
|
@_workerUnlisten() if @_workerUnlisten
|
|
|
|
componentDidUpdate: ->
|
|
if @props.visible and not @state.active
|
|
@setState(active:true)
|
|
|
|
componentWillReceiveProps: (newProps) ->
|
|
if newProps.visible is false
|
|
@setState(active:false)
|
|
|
|
render: ->
|
|
ContentComponent = ContentGeneric
|
|
messageOverride = null
|
|
|
|
if @props.dataView instanceof DatabaseView
|
|
if @state.layoutMode is 'list'
|
|
ContentComponent = ContentQuotes
|
|
if @state.syncing
|
|
messageOverride = "Please wait while we prepare your mailbox."
|
|
|
|
classes = classNames
|
|
'empty-state': true
|
|
'visible': @props.visible
|
|
'active': @state.active
|
|
|
|
<div className={classes}>
|
|
<ContentComponent messageOverride={messageOverride}/>
|
|
</div>
|
|
|
|
_onChange: ->
|
|
@setState
|
|
layoutMode: WorkspaceStore.layoutMode()
|
|
syncing: @_worker.busy()
|
|
|
|
|
|
module.exports = EmptyState
|