Mailspring/src/view-registry.coffee
Ben Gotow 1e8fd46342 fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:

1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
   - View thread with draft, edit draft
   - Move to another thread
   - Move back to thread with draft
   - Move to another thread. Notice that one or more messages from thread with draft are still there.

There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.

2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:

   - In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.

   - Previously, when you added a contact to To/CC/BCC, this happened:

     <input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates

Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.

To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!

This diff includes a few minor fixes as well:

1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React

Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet

Reviewers: evan

Reviewed By: evan

Differential Revision: https://review.inboxapp.com/D1125
2015-02-03 16:24:31 -08:00

152 lines
5.2 KiB
CoffeeScript

{find} = require 'underscore-plus'
Grim = require 'grim'
{Disposable} = require 'event-kit'
# Essential: `ViewRegistry` handles the association between model and view
# types in Atom. We call this association a View Provider. As in, for a given
# model, this class can provide a view via {::getView}, as long as the
# model/view association was registered via {::addViewProvider}
#
# If you're adding your own kind of pane item, a good strategy for all but the
# simplest items is to separate the model and the view. The model handles
# application logic and is the primary point of API interaction. The view
# just handles presentation.
#
# View providers inform the workspace how your model objects should be
# presented in the DOM. A view provider must always return a DOM node, which
# makes [HTML 5 custom elements](http://www.html5rocks.com/en/tutorials/webcomponents/customelements/)
# an ideal tool for implementing views in Atom.
#
# You can access the `ViewRegistry` object via `atom.views`.
#
# ## Examples
#
# ### Getting the workspace element
#
# ```coffee
# workspaceElement = atom.views.getView(atom.workspace)
# ```
#
# ### Getting An Editor Element
#
# ```coffee
# textEditor = atom.workspace.getActiveTextEditor()
# textEditorElement = atom.views.getView(textEditor)
# ```
#
# ### Getting A Pane Element
#
# ```coffee
# pane = atom.workspace.getActivePane()
# paneElement = atom.views.getView(pane)
# ```
module.exports =
class ViewRegistry
constructor: ->
@views = new WeakMap
@providers = []
# Essential: Add a provider that will be used to construct views in the
# workspace's view layer based on model objects in its model layer.
#
# ## Examples
#
# Text editors are divided into a model and a view layer, so when you interact
# with methods like `atom.workspace.getActiveTextEditor()` you're only going
# to get the model object. We display text editors on screen by teaching the
# workspace what view constructor it should use to represent them:
#
# ```coffee
# atom.views.addViewProvider
# modelConstructor: TextEditor
# viewConstructor: TextEditorElement
# ```
#
# * `providerSpec` {Object} containing the following keys:
# * `modelConstructor` Constructor {Function} for your model.
# * `viewConstructor` (Optional) Constructor {Function} for your view. It
# should be a subclass of `HTMLElement` (that is, your view should be a
# DOM node) and have a `::setModel()` method which will be called
# immediately after construction. If you don't supply this property, you
# must supply the `createView` property with a function that never returns
# `undefined`.
# * `createView` (Optional) Factory {Function} that must return a subclass
# of `HTMLElement` or `undefined`. If this property is not present or the
# function returns `undefined`, the view provider will fall back to the
# `viewConstructor` property. If you don't provide this property, you must
# provider a `viewConstructor` property.
#
# Returns a {Disposable} on which `.dispose()` can be called to remove the
# added provider.
addViewProvider: (modelConstructor, createView) ->
if arguments.length is 1
Grim.deprecate("atom.views.addViewProvider now takes 2 arguments: a model constructor and a createView function. See docs for details.")
provider = modelConstructor
else
provider = {modelConstructor, createView}
@providers.push(provider)
new Disposable =>
@providers = @providers.filter (p) -> p isnt provider
# Essential: Get the view associated with an object in the workspace.
#
# If you're just *using* the workspace, you shouldn't need to access the view
# layer, but view layer access may be necessary if you want to perform DOM
# manipulation that isn't supported via the model API.
#
# ## Examples
#
# ### Getting An Editor Element
#
# ```coffee
# textEditor = atom.workspace.getActiveTextEditor()
# textEditorElement = atom.views.getView(textEditor)
# ```
#
# ### Getting A Pane Element
#
# ```coffee
# pane = atom.workspace.getActivePane()
# paneElement = atom.views.getView(pane)
# ```
#
# ### Getting The Workspace Element
#
# ```coffee
# workspaceElement = atom.views.getView(atom.workspace)
# ```
#
# * `object` The object for which you want to retrieve a view. This can be a
# pane item, a pane, or the workspace itself.
#
# Returns a DOM element.
getView: (object) ->
return unless object?
if view = @views.get(object)
view
else
view = @createView(object)
@views.set(object, view)
view
createView: (object) ->
if object instanceof HTMLElement
object
else if object?.jquery
object[0]
else if provider = @findProvider(object)
element = provider.createView?(object)
unless element?
element = new provider.viewConstructor
element.initialize?(object) ? element.setModel?(object)
element
else if viewConstructor = object?.getViewClass?()
view = new viewConstructor(object)
view[0]
else
throw new Error("Can't create a view for #{object.constructor.name} instance. Please register a view provider.")
findProvider: (object) ->
find @providers, ({modelConstructor}) -> object instanceof modelConstructor