update(extensions): Rename DraftStoreExtension and MessageStoreExtension

Summary:
- Rename DraftStoreExtension to ComposerExtension
- Rename MessageStoreExtension to MessageViewExtension
- Rename ContenteditablePlugin to ContenteditableExtension
  - Update Contenteditable to use new naming convention
  - Adds support for extension handlers as props
- Add ExtensionRegistry to register extensions:
  - ContenteditableExtensions will not be registered through the
    ExtensionRegistry. They are meant for internal use, or if anyone wants
    to use our Contenteditable component directly in their plugins.
  - Adds specs
- Refactors internal_packages and src to use new names and new ExtensionRegistry api
- Adds deprecation util function and deprecation notices for old api methods:
  - DraftStore.{registerExtension, unregisterExtension}
  - MessageStore.{registerExtension, unregisterExtension}
  - DraftStoreExtension.{onMouseUp, onTabDown}
  - MessageStoreExtension
- Adds and updates docs

Test Plan: - Unit tests

Reviewers: bengotow, evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D2293
This commit is contained in:
Juan Tejada 2015-11-27 11:49:24 -08:00
parent 02633ffd57
commit cfdc401c54
34 changed files with 682 additions and 369 deletions

View file

@ -14,18 +14,18 @@ This API allows your package to:
- Transform the draft and make additional changes before it is sent.
To create your own composer extensions, subclass {DraftStoreExtensions} and override the methods your extension needs.
To create your own composer extensions, subclass {ComposerExtension} and override the methods your extension needs.
In the sample packages repository, [templates]() is an example of a package which uses a DraftStoreExtension to enhance the composer experience.
In the sample packages repository, [templates]() is an example of a package which uses a ComposerExtension to enhance the composer experience.
### Example
This extension displays a warning before sending a draft that contains the names of competitors' products. If the user proceeds to send the draft containing the words, it appends a disclaimer.
```coffee
{DraftStoreExtension} = require 'nylas-exports'
{ComposerExtension} = require 'nylas-exports'
class ProductsExtension extends DraftStoreExtension
class ProductsExtension extends ComposerExtension
@warningsForSending: (draft) ->
words = ['acme', 'anvil', 'tunnel', 'rocket', 'dynamite']

View file

@ -43,7 +43,6 @@ class TemplatesDraftStoreExtension extends DraftStoreExtension {
}
static onTabDown(editableNode, range, event) {
console.log('tabbed');
if (event.shiftKey) {
return this.onTabSelectNextVar(editableNode, range, event, -1);
}

View file

@ -1,5 +1,5 @@
{PreferencesUIStore, DraftStore} = require 'nylas-exports'
SignatureDraftExtension = require './signature-draft-extension'
{PreferencesUIStore, ExtensionRegistry} = require 'nylas-exports'
SignatureComposerExtension = require './signature-composer-extension'
module.exports =
activate: (@state={}) ->
@ -8,11 +8,11 @@ module.exports =
displayName: "Signatures"
component: require "./preferences-signatures"
DraftStore.registerExtension(SignatureDraftExtension)
ExtensionRegistry.Composer.register(SignatureComposerExtension)
PreferencesUIStore.registerPreferencesTab(@preferencesTab)
deactivate: ->
DraftStore.unregisterExtension(SignatureDraftExtension)
ExtensionRegistry.Composer.unregister(SignatureComposerExtension)
PreferencesUIStore.unregisterPreferencesTab(@preferencesTab.sectionId)
serialize: -> @state

View file

@ -1,6 +1,6 @@
{DraftStoreExtension, AccountStore} = require 'nylas-exports'
{ComposerExtension, AccountStore} = require 'nylas-exports'
class SignatureDraftStoreExtension extends DraftStoreExtension
class SignatureComposerExtension extends ComposerExtension
@prepareNewDraft: (draft) ->
accountId = AccountStore.current().id
signature = NylasEnv.config.get("nylas.account-#{accountId}.signature")
@ -11,4 +11,4 @@ class SignatureDraftStoreExtension extends DraftStoreExtension
insertionPoint = draft.body.length
draft.body = draft.body.substr(0, insertionPoint-1) + "<br/>" + signature + draft.body.substr(insertionPoint)
module.exports = SignatureDraftStoreExtension
module.exports = SignatureComposerExtension

View file

@ -1,8 +1,8 @@
{Message} = require 'nylas-exports'
SignatureDraftStoreExtension = require '../lib/signature-draft-extension'
SignatureComposerExtension = require '../lib/signature-composer-extension'
describe "SignatureDraftStoreExtension", ->
describe "SignatureComposerExtension", ->
describe "prepareNewDraft", ->
describe "when a signature is defined", ->
beforeEach ->
@ -18,9 +18,9 @@ describe "SignatureDraftStoreExtension", ->
draft: true
body: 'This is a another test.'
SignatureDraftStoreExtension.prepareNewDraft(a)
SignatureComposerExtension.prepareNewDraft(a)
expect(a.body).toEqual("This is a test!<br/><div id='signature'>This is my signature.</div><blockquote>Hello world</blockquote>")
SignatureDraftStoreExtension.prepareNewDraft(b)
SignatureComposerExtension.prepareNewDraft(b)
expect(b.body).toEqual("This is a another test<br/><div id='signature'>This is my signature.</div>")
describe "when a signature is not defined", ->
@ -32,5 +32,5 @@ describe "SignatureDraftStoreExtension", ->
a = new Message
draft: true
body: 'This is a test! <blockquote>Hello world</blockquote>'
SignatureDraftStoreExtension.prepareNewDraft(a)
SignatureComposerExtension.prepareNewDraft(a)
expect(a.body).toEqual('This is a test! <blockquote>Hello world</blockquote>')

View file

@ -1,11 +1,11 @@
{ComponentRegistry, DraftStore} = require 'nylas-exports'
Extension = require './draft-extension'
{ExtensionRegistry} = require 'nylas-exports'
SpellcheckComposerExtension = require './spellcheck-composer-extension'
module.exports =
activate: (@state={}) ->
DraftStore.registerExtension(Extension)
ExtensionRegistry.Composer.register(SpellcheckComposerExtension)
deactivate: ->
DraftStore.unregisterExtension(Extension)
ExtensionRegistry.Composer.unregister(SpellcheckComposerExtension)
serialize: -> @state

View file

@ -1,4 +1,4 @@
{DraftStoreExtension, AccountStore, DOMUtils} = require 'nylas-exports'
{ComposerExtension, AccountStore, DOMUtils} = require 'nylas-exports'
_ = require 'underscore'
spellchecker = require('spellchecker')
remote = require('remote')
@ -6,16 +6,16 @@ MenuItem = remote.require('menu-item')
SpellcheckCache = {}
class SpellcheckDraftStoreExtension extends DraftStoreExtension
class SpellcheckComposerExtension extends ComposerExtension
@isMisspelled: (word) ->
SpellcheckCache[word] ?= spellchecker.isMisspelled(word)
SpellcheckCache[word]
@onInput: (editableNode) ->
@onInput: (editableNode) =>
@walkTree(editableNode)
@onShowContextMenu: (event, editableNode, selection, innerStateProxy, menu) =>
@onShowContextMenu: (event, editableNode, selection, menu) =>
range = DOMUtils.Mutating.getRangeAtAndSelectWord(selection, 0)
word = range.toString()
if @isMisspelled(word)
@ -123,6 +123,6 @@ class SpellcheckDraftStoreExtension extends DraftStoreExtension
if body != clean
session.changes.add(body: clean)
SpellcheckDraftStoreExtension.SpellcheckCache = SpellcheckCache
SpellcheckComposerExtension.SpellcheckCache = SpellcheckCache
module.exports = SpellcheckDraftStoreExtension
module.exports = SpellcheckComposerExtension

View file

@ -1,22 +1,22 @@
SpellcheckDraftStoreExtension = require '../lib/draft-extension'
SpellcheckComposerExtension = require '../lib/spellcheck-composer-extension'
fs = require 'fs'
_ = require 'underscore'
initialHTML = fs.readFileSync(__dirname + '/fixtures/california-with-misspellings-before.html').toString()
expectedHTML = fs.readFileSync(__dirname + '/fixtures/california-with-misspellings-after.html').toString()
describe "SpellcheckDraftStoreExtension", ->
describe "SpellcheckComposerExtension", ->
beforeEach ->
# Avoid differences between node-spellcheck on different platforms
spellings = JSON.parse(fs.readFileSync(__dirname + '/fixtures/california-spelling-lookup.json'))
spyOn(SpellcheckDraftStoreExtension, 'isMisspelled').andCallFake (word) ->
spyOn(SpellcheckComposerExtension, 'isMisspelled').andCallFake (word) ->
spellings[word]
describe "walkTree", ->
it "correctly walks a DOM tree and surrounds mispelled words", ->
dom = document.createElement('div')
dom.innerHTML = initialHTML
SpellcheckDraftStoreExtension.walkTree(dom)
SpellcheckComposerExtension.walkTree(dom)
expect(dom.innerHTML).toEqual(expectedHTML)
describe "finalizeSessionBeforeSending", ->
@ -27,7 +27,7 @@ describe "SpellcheckDraftStoreExtension", ->
changes:
add: jasmine.createSpy('add')
SpellcheckDraftStoreExtension.finalizeSessionBeforeSending(session)
SpellcheckComposerExtension.finalizeSessionBeforeSending(session)
expect(session.changes.add).toHaveBeenCalledWith(body: initialHTML)
module.exports = SpellcheckDraftStoreExtension
module.exports = SpellcheckComposerExtension

View file

@ -1,27 +0,0 @@
{DraftStore, DOMUtils, ContenteditablePlugin} = require 'nylas-exports'
class ComposerExtensionsPlugin extends ContenteditablePlugin
@onInput: (event, editableNode, selection, innerStateProxy) ->
for extension in DraftStore.extensions()
extension.onInput?(editableNode, event)
@onKeyDown: (event, editableNode, selection, innerStateProxy) ->
if event.key is "Tab"
range = DOMUtils.getRangeInScope(editableNode)
for extension in DraftStore.extensions()
extension.onTabDown?(editableNode, range, event)
@onShowContextMenu: (args...) ->
for extension in DraftStore.extensions()
extension.onShowContextMenu?(args...)
@onClick: (event, editableNode, selection, innerStateProxy) ->
range = DOMUtils.getRangeInScope(editableNode)
return unless range
try
for extension in DraftStore.extensions()
extension.onMouseUp?(editableNode, range, event)
catch e
console.error('DraftStore extension raised an error: '+e.toString())
module.exports = ComposerExtensionsPlugin

View file

@ -10,7 +10,8 @@ React = require 'react'
AccountStore,
FileUploadStore,
QuotedHTMLParser,
FileDownloadStore} = require 'nylas-exports'
FileDownloadStore,
ExtensionRegistry} = require 'nylas-exports'
{DropZone,
RetinaImg,
@ -29,8 +30,6 @@ CollapsedParticipants = require './collapsed-participants'
Fields = require './fields'
ComposerExtensionsPlugin = require './composer-extensions-plugin'
# The ComposerView is a unique React component because it (currently) is a
# singleton. Normally, the React way to do things would be to re-render the
# Composer with new props.
@ -68,6 +67,7 @@ class ComposerView extends React.Component
enabledFields: [] # Gets updated in @_initiallyEnabledFields
showQuotedText: false
uploads: FileUploadStore.uploadsForMessage(@props.draftClientId) ? []
extensions: ExtensionRegistry.Composer.extensions()
componentWillMount: =>
@_prepareForDraft(@props.draftClientId)
@ -80,6 +80,7 @@ class ComposerView extends React.Component
@_usubs = []
@_usubs.push FileUploadStore.listen @_onFileUploadStoreChange
@_usubs.push AccountStore.listen @_onAccountStoreChanged
@_usubs.push ExtensionRegistry.Composer.listen @_onExtensionsChanged
@_applyFieldFocus()
componentWillUnmount: =>
@ -300,7 +301,7 @@ class ComposerView extends React.Component
onScrollTo={@props.onRequestScrollTo}
onFilePaste={@_onFilePaste}
onScrollToBottom={@_onScrollToBottom()}
plugins={[ComposerExtensionsPlugin]}
extensions={@state.extensions}
getComposerBoundingRect={@_getComposerBoundingRect}
initialSelectionSnapshot={@_recoveredSelection} />
@ -529,6 +530,9 @@ class ComposerView extends React.Component
enabledFields.push Fields.Body
return enabledFields
_onExtensionsChanged: =>
@setState extensions: ExtensionRegistry.Composer.extensions()
# When the account store changes, the From field may or may not still
# be in scope. We need to make sure to update our enabled fields.
_onAccountStoreChanged: =>
@ -685,7 +689,7 @@ class ComposerView extends React.Component
warnings.push('without a body')
# Check third party warnings added via DraftStore extensions
for extension in DraftStore.extensions()
for extension in @state.extensions
continue unless extension.warningsForSending
warnings = warnings.concat(extension.warningsForSending(draft))

View file

@ -1,7 +1,7 @@
AutoloadImagesStore = require './autoload-images-store'
{MessageStoreExtension} = require 'nylas-exports'
{MessageViewExtension} = require 'nylas-exports'
class AutoloadImagesExtension extends MessageStoreExtension
class AutoloadImagesExtension extends MessageViewExtension
@formatMessageBody: (message) ->
if AutoloadImagesStore.shouldBlockImagesIn(message)

View file

@ -1,5 +1,5 @@
{ComponentRegistry,
MessageStore,
ExtensionRegistry,
WorkspaceStore} = require 'nylas-exports'
AutoloadImagesExtension = require './autoload-images-extension'
@ -10,12 +10,12 @@ module.exports =
activate: (@state={}) ->
# Register Message List Actions we provide globally
MessageStore.registerExtension(AutoloadImagesExtension)
ExtensionRegistry.MessageView.register AutoloadImagesExtension
ComponentRegistry.register AutoloadImagesHeader,
role: 'message:BodyHeader'
deactivate: ->
MessageStore.unregisterExtension(AutoloadImagesExtension)
ExtensionRegistry.MessageView.unregister AutoloadImagesExtension
ComponentRegistry.unregister(AutoloadImagesHeader)
serialize: -> @state

View file

@ -1,7 +1,7 @@
MessageList = require "./message-list"
MessageToolbarItems = require "./message-toolbar-items"
{ComponentRegistry,
MessageStore,
ExtensionRegistry,
WorkspaceStore} = require 'nylas-exports'
{SidebarContactCard,
@ -46,8 +46,8 @@ module.exports =
ComponentRegistry.register ThreadToggleUnreadButton,
role: 'message:Toolbar'
MessageStore.registerExtension(AutolinkerExtension)
MessageStore.registerExtension(TrackingPixelsExtension)
ExtensionRegistry.MessageView.register AutolinkerExtension
ExtensionRegistry.MessageView.register TrackingPixelsExtension
deactivate: ->
ComponentRegistry.unregister MessageList
@ -59,7 +59,7 @@ module.exports =
ComponentRegistry.unregister SidebarContactCard
ComponentRegistry.unregister SidebarSpacer
ComponentRegistry.unregister SidebarContactList
MessageStore.unregisterExtension(AutolinkerExtension)
MessageStore.unregisterExtension(TrackingPixelsExtension)
ExtensionRegistry.MessageView.unregister AutolinkerExtension
ExtensionRegistry.MessageView.unregister TrackingPixelsExtension
serialize: -> @state

View file

@ -1,7 +1,7 @@
Autolinker = require 'autolinker'
{MessageStoreExtension} = require 'nylas-exports'
{MessageViewExtension} = require 'nylas-exports'
class AutolinkerExtension extends MessageStoreExtension
class AutolinkerExtension extends MessageViewExtension
@formatMessageBody: (message) ->
# Apply the autolinker pass to make emails and links clickable

View file

@ -1,4 +1,4 @@
{MessageStoreExtension, RegExpUtils} = require 'nylas-exports'
{MessageViewExtension, RegExpUtils} = require 'nylas-exports'
TrackingBlacklist = [{
name: 'Sidekick',
@ -98,7 +98,7 @@ TrackingBlacklist = [{
homepage: 'http://salesloft.com'
}]
class TrackingPixelsExtension extends MessageStoreExtension
class TrackingPixelsExtension extends MessageViewExtension
@formatMessageBody: (message) ->
return unless message.isFromMe()

View file

@ -0,0 +1,63 @@
ExtensionRegistry = require '../src/extension-registry'
class TestExtension
@name: 'TestExtension'
describe 'ExtensionRegistry', ->
beforeEach ->
@originalAdapters = ExtensionRegistry._deprecationAdapters
@registry = new ExtensionRegistry.Registry('Test')
spyOn @registry, 'triggerDebounced'
describe 'Registry', ->
it 'has trigger and listen to defined', ->
expect(@registry.trigger).toBeDefined()
expect(@registry.listen).toBeDefined()
expect(@registry.listenTo).toBeDefined()
describe 'register', ->
it 'throws an exception if extension not passed', ->
expect(=> @registry.register(null)).toThrow()
it 'throws an exception if extension does not have a name', ->
expect(=> @registry.register({})).toThrow()
it 'throws an exception if extension is array', ->
expect(=> @registry.register([])).toThrow()
it 'throws an exception if extension is string', ->
expect(=> @registry.register('')).toThrow()
it 'returns itself', ->
expect(@registry.register(TestExtension)).toBe(@registry)
it 'registers extension and triggers', ->
@registry.register(TestExtension)
expect(@registry.extensions().length).toEqual 1
expect(@registry.triggerDebounced).toHaveBeenCalled()
it 'does not add extensions with the same name', ->
expect(@registry.extensions().length).toEqual 0
@registry.register(TestExtension)
expect(@registry.extensions().length).toEqual 1
@registry.register({name: 'TestExtension'})
expect(@registry.extensions().length).toEqual 1
it 'calls deprecationAdapters if present for a role', ->
adapterSpy = jasmine.createSpy('adapterSpy').andCallFake (ext) -> ext
@registry = new ExtensionRegistry.Registry('Test', adapterSpy)
spyOn @registry, 'triggerDebounced'
@registry.register(TestExtension)
expect(adapterSpy.calls.length).toEqual 1
describe 'unregister', ->
it 'unregisters the extension if it exists', ->
@registry.register(TestExtension)
@registry.unregister(TestExtension)
expect(@registry.extensions().length).toEqual 0
it 'throws if invalid extension passed', ->
expect( => @registry.unregister('Test')).toThrow()
expect( => @registry.unregister(null)).toThrow()
expect( => @registry.unregister([])).toThrow()
expect( => @registry.unregister({})).toThrow()

View file

@ -6,7 +6,7 @@ ModelQuery = require '../../src/flux/models/query'
AccountStore = require '../../src/flux/stores/account-store'
DatabaseStore = require '../../src/flux/stores/database-store'
DraftStore = require '../../src/flux/stores/draft-store'
DraftStoreExtension = require '../../src/flux/stores/draft-store-extension'
ComposerExtension = require '../../src/flux/extensions/composer-extension'
SendDraftTask = require '../../src/flux/tasks/send-draft'
DestroyDraftTask = require '../../src/flux/tasks/destroy-draft'
SoundRegistry = require '../../src/sound-registry'
@ -26,7 +26,7 @@ messageWithStyleTags = null
fakeMessages = null
fakeMessageWithFiles = null
class TestExtension extends DraftStoreExtension
class TestExtension extends ComposerExtension
@prepareNewDraft: (draft) ->
draft.body = "Edited by TestExtension!" + draft.body

View file

@ -1,5 +1,4 @@
_ = require 'underscore'
Actions = require './flux/actions'
{Listener, Publisher} = require './flux/modules/reflux-coffee'
CoffeeHelpers = require './flux/coffee-helpers'

View file

@ -1,26 +0,0 @@
###
ContenteditablePlugin is an abstract base class. Implementations of this
are used to make additional changes to a <Contenteditable /> component
beyond a user's input intents.
While some ContenteditablePlugins are included with the core
<Contenteditable /> component, others may be added via the `plugins`
prop.
###
class ContenteditablePlugin
# The onInput event can be triggered by a variety of events, some of
# which could have been already been looked at by a callback.
# Pretty much any DOM mutation will fire this.
# Sometimes those mutations are the cause of callbacks.
@onInput: (event, editableNode, selection, innerStateProxy) ->
@onBlur: (event, editableNode, selection, innerStateProxy) ->
@onFocus: (event, editableNode, selection, innerStateProxy) ->
@onClick: (event, editableNode, selection, innerStateProxy) ->
@onKeyDown: (event, editableNode, selection, innerStateProxy) ->
@onShowContextMenu: (event, editableNode, selection, innerStateProxy, menu) ->

View file

@ -36,25 +36,34 @@ class Contenteditable extends React.Component
initialSelectionSnapshot: React.PropTypes.object
# Passes an absolute top coordinate to scroll to.
# Handlers
onChange: React.PropTypes.func.isRequired
onFilePaste: React.PropTypes.func
# Passes an absolute top coordinate to scroll to.
onScrollTo: React.PropTypes.func
onScrollToBottom: React.PropTypes.func
# A list of objects that extend {ContenteditablePlugin}
plugins: React.PropTypes.array
# Extension DOM Mutating handlers. See {ContenteditableExtension}
onInput: React.PropTypes.func
onBlur: React.PropTypes.func
onFocus: React.PropTypes.func
onClick: React.PropTypes.func
onKeyDown: React.PropTypes.func
onShowContextMenu: React.PropTypes.func
# A list of objects that extend {ContenteditableExtension}
extensions: React.PropTypes.array
spellcheck: React.PropTypes.bool
floatingToolbar: React.PropTypes.bool
@defaultProps:
plugins: []
extensions: []
spellcheck: true
floatingToolbar: true
corePlugins: [ListManager]
coreExtensions: [ListManager]
# We allow extensions to read, and mutate the:
#
@ -70,7 +79,7 @@ class Contenteditable extends React.Component
innerStateProxy =
get: => return @innerState
set: (newInnerState) => @setInnerState(newInnerState)
args = [event, @_editableNode(), document.getSelection(), innerStateProxy, extraArgs...]
args = [event, @_editableNode(), document.getSelection(), extraArgs..., innerStateProxy]
editingFunction.apply(null, args)
@_setupSelectionListeners()
@ -162,7 +171,7 @@ class Contenteditable extends React.Component
selection.addRange(range)
# When some other component (like the `FloatingToolbar` or some
# `DraftStoreExtension`) wants to mutate the DOM, it declares a
# `ComposerExtension`) wants to mutate the DOM, it declares a
# `mutator` function. That mutator expects to be passed the latest DOM
# object (the `_editableNode()`) and will do mutations to it. Once those
# mutations are done, we need to be sure to notify that changes
@ -201,38 +210,42 @@ class Contenteditable extends React.Component
@_setupSelectionListeners()
@_onInput()
# Will execute the event handlers on each of the registerd and core plugins
# Will execute the event handlers on each of the registerd and core extensions
# In this context, event.preventDefault and event.stopPropagation don't refer
# to stopping default DOM behavior or prevent event bubbling through the DOM,
# but rather prevent our own Contenteditable default behavior, and preventing
# other plugins from being called.
# If any of the plugins calls event.preventDefault() it will prevent the
# other extensions from being called.
# If any of the extensions calls event.preventDefault() it will prevent the
# default behavior for the Contenteditable, which basically means preventing
# the core plugin handlers from being called.
# If any of the plugins calls event.stopPropagation(), it will prevent any
# other plugin handlers from being called.
# the core extension handlers from being called.
# If any of the extensions calls event.stopPropagation(), it will prevent any
# other extension handlers from being called.
#
# NOTE: It's possible for there to be no `event` passed in.
_runPluginHandlersForEvent: (method, event, args...) =>
executeCallback = (plugin) =>
return if not plugin[method]?
callback = plugin[method].bind(plugin)
_runExtensionHandlersForEvent: (method, event, args...) =>
executeCallback = (extension) =>
return if not extension[method]?
callback = extension[method].bind(extension)
@atomicEdit(callback, event, args...)
for plugin in @props.plugins
# Check if any of the extension handlers where passed as a prop and call
# that first
executeCallback(@props)
for extension in @props.extensions
break if event?.isPropagationStopped()
executeCallback(plugin)
executeCallback(extension)
return if event?.defaultPrevented or event?.isPropagationStopped()
for plugin in @corePlugins
for extension in @coreExtensions
break if event?.isPropagationStopped()
executeCallback(plugin)
executeCallback(extension)
_onKeyDown: (event) =>
@_runPluginHandlersForEvent("onKeyDown", event)
@_runExtensionHandlersForEvent("onKeyDown", event)
# This is a special case where we don't want to bubble up the event to the
# keymap manager if the plugin prevented the default behavior
# keymap manager if the extension prevented the default behavior
if event.defaultPrevented
event.stopPropagation()
return
@ -259,7 +272,7 @@ class Contenteditable extends React.Component
@_ignoreInputChanges = true
@_resetInnerStateOnInput()
@_runPluginHandlersForEvent("onInput", event)
@_runExtensionHandlersForEvent("onInput", event)
@_normalize()
@ -413,11 +426,12 @@ class Contenteditable extends React.Component
_onBlur: (event) =>
@setInnerState dragging: false
return if @_editableNode().parentElement.contains event.relatedTarget
@_runExtensionHandlersForEvent("onBlur", event)
@setInnerState editableFocused: false
_onFocus: (event) =>
@setInnerState editableFocused: true
@props.onFocus?(event)
@_runExtensionHandlersForEvent("onFocus", event)
_editableNode: =>
React.findDOMNode(@refs.contenteditable)
@ -616,7 +630,8 @@ class Contenteditable extends React.Component
MenuItem = remote.require('menu-item')
menu = new Menu()
@_runPluginHandlersForEvent("onShowContextMenu", event, menu)
@_runExtensionHandlersForEvent("onShowContextMenu", event, menu)
menu.append(new MenuItem({ label: 'Cut', role: 'cut'}))
menu.append(new MenuItem({ label: 'Copy', role: 'copy'}))
menu.append(new MenuItem({ label: 'Paste', role: 'paste'}))
@ -660,7 +675,7 @@ class Contenteditable extends React.Component
selection = document.getSelection()
return event unless DOMUtils.selectionInScope(selection, editableNode)
@_runPluginHandlersForEvent("onClick", event)
@_runExtensionHandlersForEvent("onClick", event)
return event
_onDragStart: (event) =>

View file

@ -3,7 +3,7 @@ React = require 'react/addons'
classNames = require 'classnames'
{CompositeDisposable} = require 'event-kit'
{RetinaImg} = require 'nylas-component-kit'
{DraftStore} = require 'nylas-exports'
{ExtensionRegistry} = require 'nylas-exports'
class FloatingToolbar extends React.Component
@displayName = "FloatingToolbar"
@ -29,9 +29,11 @@ class FloatingToolbar extends React.Component
@state =
urlInputValue: @_initialUrl() ? ""
componentWidth: 0
extensions: ExtensionRegistry.Composer.extensions()
componentDidMount: =>
@subscriptions = new CompositeDisposable()
@usubExtensions = ExtensionRegistry.Composer.listen @_onExtensionsChanged
componentWillReceiveProps: (nextProps) =>
@setState
@ -39,6 +41,7 @@ class FloatingToolbar extends React.Component
componentWillUnmount: =>
@subscriptions?.dispose()
@usubExtensions()
componentDidUpdate: =>
if @props.mode is "edit-link" and not @props.linkToModify
@ -88,12 +91,12 @@ class FloatingToolbar extends React.Component
<button className="btn btn-link toolbar-btn"
onClick={@props.onClickLinkEditBtn}
data-command-name="link"></button>
{@_toolbarExtensions()}
{@_toolbarExtensions(@state.extensions)}
</div>
_toolbarExtensions: ->
_toolbarExtensions: (extensions) ->
buttons = []
for extension in DraftStore.extensions()
for extension in extensions
toolbarItem = extension.composerToolbar?()
if toolbarItem
buttons.push(
@ -102,6 +105,9 @@ class FloatingToolbar extends React.Component
title="#{toolbarItem.tooltip}"><RetinaImg mode={RetinaImg.Mode.ContentIsMask} url="#{toolbarItem.iconUrl}" /></button>)
return buttons
_onExtensionsChanged: =>
@setState extensions: ExtensionRegistry.Composer.extensions()
_extensionMutateDom: (mutator) =>
@props.onDomMutator(mutator)

View file

@ -1,8 +1,7 @@
_str = require 'underscore.string'
{DOMUtils} = require 'nylas-exports'
ContenteditablePlugin = require './contenteditable-plugin'
{DOMUtils, ContenteditableExtension} = require 'nylas-exports'
class ListManager extends ContenteditablePlugin
class ListManager extends ContenteditableExtension
@onInput: (event, editableNode, selection) ->
if @_spaceEntered and @hasListStartSignature(selection)
@createList(event, selection)

View file

@ -0,0 +1,21 @@
_ = require 'underscore'
class DeprecateUtils
@warn: (condition, message) ->
console.warn message if condition
@deprecate: (fnName, newName, ctx, fn) ->
if NylasEnv.inDevMode
warn = true
newFn = =>
DeprecateUtils.warn(
warn,
"Deprecation warning! #{fnName} is deprecated and will be removed soon.
Use #{newName} instead."
)
warn = false
return fn.apply(ctx, arguments)
return _.extend(newFn, fn)
return fn
module.exports = DeprecateUtils

View file

@ -0,0 +1,63 @@
import _ from 'underscore';
import {Listener, Publisher} from './flux/modules/reflux-coffee';
import {includeModule} from './flux/coffee-helpers';
export class Registry {
static include = includeModule;
constructor(name, deprecationAdapter = (ext)=> ext) {
this.name = name;
this._deprecationAdapter = deprecationAdapter;
this._registry = new Map();
}
register(extension) {
this.validateExtension(extension, 'register');
this._registry.set(extension.name, this._deprecationAdapter(extension));
this.triggerDebounced();
return this;
}
unregister(extension) {
this.validateExtension(extension, 'unregister');
this._registry.delete(extension.name);
this.triggerDebounced();
}
extensions() {
return Array.from(this._registry.values());
}
clear() {
this._registry = new Map();
}
triggerDebounced() {
_.debounce(()=> this.trigger(), 1);
}
validateExtension(extension, method) {
if (!extension || Array.isArray(extension) || !_.isObject(extension)) {
throw new Error(`ExtensionRegistry.${this.name}.${method} requires a valid \\
extension object that implements one of the functions defined by ${this.name}Extension`);
}
if (!extension.name) {
throw new Error(`ExtensionRegistry.${this.name}.${method} requires a \\
\`name\` property defined on the extension object`);
}
}
}
Registry.include(Publisher);
Registry.include(Listener);
export const Composer = new Registry(
'Composer',
require('./flux/extensions/composer-extension-adapter')
);
export const MessageView = new Registry(
'MessageView',
);

View file

@ -296,7 +296,7 @@ class Actions
###
Public: Send the draft with the given ID. This Action is handled by the {DraftStore},
which finalizes the {DraftChangeSet} and allows {DraftStoreExtension}s to display
which finalizes the {DraftChangeSet} and allows {ComposerExtension}s to display
warnings and do post-processing. To change send behavior, you should consider using
one of these objects rather than listening for the {sendDraft} action.

View file

@ -0,0 +1,51 @@
_ = require 'underscore'
{deprecate} = require '../../deprecate-utils'
DOMUtils = require '../../dom-utils'
ComposerExtensionAdapter = (extension) ->
if extension.onInput?.length <= 2
origInput = extension.onInput
extension.onInput = (event, editableNode, selection) ->
origInput(editableNode, event)
extension.onInput = deprecate(
"DraftStoreExtension.onInput",
"ComposerExtension.onInput",
extension,
extension.onInput
)
if extension.onTabDown?
origKeyDown = extension.onKeyDown
extension.onKeyDown = (event, editableNode, selection) ->
if event.key is "Tab"
range = DOMUtils.getRangeInScope(editableNode)
extension.onTabDown(editableNode, range, event)
else
origKeyDown?(event, editableNode, selection)
extension.onKeyDown = deprecate(
"DraftStoreExtension.onTabDown",
"ComposerExtension.onKeyDown",
extension,
extension.onKeyDown
)
if extension.onMouseUp?
origOnClick = extension.onClick
extension.onClick = (event, editableNode, selection) ->
range = DOMUtils.getRangeInScope(editableNode)
extension.onMouseUp(editableNode, range, event)
origOnClick?(event, editableNode, selection)
extension.onClick = deprecate(
"DraftStoreExtension.onMouseUp",
"ComposerExtension.onClick",
extension,
extension.onClick
)
return extension
module.exports = ComposerExtensionAdapter

View file

@ -0,0 +1,109 @@
###
Public: To create ComposerExtensions that enhance the composer experience, you
should create objects that implement the interface defined at {ComposerExtension}.
{ComposerExtension} extends {ContenteditableExtension}, so you can also
implement the methods defined there to further enhance the composer
experience.
To register your extension with the ExtensionRegistry, call {ExtensionRegistry::Composer::register}.
When your package is being unloaded, you *must* call the corresponding
{ExtensionRegistry::Composer::unregister} to unhook your extension.
```coffee
activate: ->
ExtensionRegistry.Composer.register(MyExtension)
...
deactivate: ->
ExtensionRegistry.Composer.unregister(MyExtension)
```
Your ComposerExtension should be stateless. The user may have multiple drafts
open at any time, and the methods of your ComposerExtension may be called for different
drafts at any time. You should not expect that the session you receive in
{::finalizeSessionBeforeSending} is for the same draft you previously received in
{::warningsForSending}, etc.
The ComposerExtension API does not currently expose any asynchronous or {Promise}-based APIs.
This will likely change in the future. If you have a use-case for a ComposerExtension that
is not possible with the current API, please let us know.
Section: Extensions
###
class ComposerExtension
###
Public: Inspect the draft, and return any warnings that need to be displayed before
the draft is sent. Warnings should be string phrases, such as "without an attachment"
that fit into a message of the form: "Send #{phase1} and #{phase2}?"
- `draft`: A fully populated {Message} object that is about to be sent.
Returns a list of warning strings, or an empty array if no warnings need to be displayed.
###
@warningsForSending: (draft) ->
[]
###
Public: declare an icon to be displayed in the composer's toolbar (where
bold, italic, underline, etc are).
You must return an object that contains the following properties:
- `mutator`: A function that's called when your toolbar button is
clicked. This mutator function will be passed as its only argument the
`dom`. The `dom` is the full {DOM} object of the current composer. You
may mutate this in place. We don't care about the mutator's return
value.
- `tooltip`: A one or two word description of what your icon does
- `iconUrl`: The url of your icon. It should be in the `nylas://` scheme.
For example: `nylas://your-package-name/assets/my-icon@2x.png`. Note, we
will downsample your image by 2x (for Retina screens), so make sure it's
twice the resolution. The icon should be black and white. We will
directly pass the `url` prop of a {RetinaImg}
###
@composerToolbar: ->
return
###
Public: Override prepareNewDraft to modify a brand new draft before it is displayed
in a composer. This is one of the only places in the application where it's safe
to modify the draft object you're given directly to add participants to the draft,
add a signature, etc.
By default, new drafts are considered `pristine`. If the user leaves the composer
without making any changes, the draft is discarded. If your extension populates
the draft in a way that makes it "populated" in a valuable way, you should set
`draft.pristine = false` so the draft saves, even if no further changes are made.
###
@prepareNewDraft: (draft) ->
return
###
Public: Override finalizeSessionBeforeSending in your ComposerExtension subclass to
transform the {DraftStoreProxy} editing session just before the draft is sent. This method
gives you an opportunity to make any final substitutions or changes after any
{::warningsForSending} have been displayed.
- `session`: A {DraftStoreProxy} for the draft.
Example:
```coffee
# Remove any <code> tags found in the draft body
finalizeSessionBeforeSending: (session) ->
body = session.draft().body
clean = body.replace(/<\/?code[^>]*>/g, '')
if body != clean
session.changes.add(body: clean)
```
###
@finalizeSessionBeforeSending: (session) ->
return
module.exports = ComposerExtension

View file

@ -0,0 +1,146 @@
###
Public: ContenteditableExtension is an abstract base class. Implementations of this
are used to make additional changes to a <Contenteditable /> component
beyond a user's input intents. The hooks in this class provide the contenteditable
DOM Node itself, allowing you to adjust selection ranges and change content
as necessary.
While some ContenteditableExtension are included with the core
<{Contenteditable} /> component, others may be added via the `plugins`
prop when you use it inside your own components.
Example:
```javascript
render() {
return(
<div>
<Contenteditable extensions={[MyAwesomeExtension]}>
</div>
);
}
```
If you specifically want to enhance the Composer experience you should register
a {ComposerExtension}
Section: Extensions
###
class ContenteditableExtension
###
Public: Override onInput in your Contenteditable subclass to implement custom
behavior as the user types in the contenteditable's body field. You may mutate
the contenteditable in place, we do not expect any return value from this method.
The onInput event can be triggered by a variety of events, some of which could
have been already been looked at by a callback. Almost any DOM mutation will
fire this event. Sometimes those mutations are the cause of other callbacks.
- event: DOM event fired on the contenteditable
- editableNode: DOM node that represents the current contenteditable.This object
can be mutated in place to modify the Contenteditable's content
- selection: [Selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection)
object that represents the current selection on the contenteditable
Example:
The Nylas `templates` package uses this method to see if the user has populated a
`<code>` tag placed in the body and change it's CSS class to reflect that it is no
longer empty.
```coffee
onInput: (event, editableNode, selection) ->
isWithinNode = (node) ->
test = selection.baseNode
while test isnt editableNode
return true if test is node
test = test.parentNode
return false
codeTags = editableNode.querySelectorAll('code.var.empty')
for codeTag in codeTags
if selection.containsNode(codeTag) or isWithinNode(codeTag)
codeTag.classList.remove('empty')
```
###
@onInput: (event, editableNode, selection) ->
###
Public: Override onBlur to mutate the contenteditable DOM node whenever the
onBlur event is fired on it. You may mutate the contenteditable in place, we
not expect any return value from this method.
- event: DOM event fired on the contenteditable
- editableNode: DOM node that represents the current contenteditable.This object
can be mutated in place to modify the Contenteditable's content
- selection: [Selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection)
object that represents the current selection on the contenteditable
###
@onBlur: (event, editableNode, selection) ->
###
Public: Override onFocus to mutate the contenteditable DOM node whenever the
onFocus event is fired on it. You may mutate the contenteditable in place, we
not expect any return value from this method.
- event: DOM event fired on the contenteditable
- editableNode: DOM node that represents the current contenteditable.This object
can be mutated in place to modify the Contenteditable's content
- selection: [Selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection)
object that represents the current selection on the contenteditable
###
@onFocus: (event, editableNode, selection) ->
###
Public: Override onClick to mutate the contenteditable DOM node whenever the
onClick event is fired on it. You may mutate the contenteditable in place, we
not expect any return value from this method.
- event: DOM event fired on the contenteditable
- editableNode: DOM node that represents the current contenteditable.This object
can be mutated in place to modify the Contenteditable's content
- selection: [Selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection)
object that represents the current selection on the contenteditable
###
@onClick: (event, editableNode, selection) ->
###
Public: Override onKeyDown to mutate the contenteditable DOM node whenever the
onKeyDown event is fired on it.
Public: Called when the user presses a key while focused on the contenteditable's body field.
Override onKeyDown in your ContenteditableExtension to adjust the selection or
perform other actions.
If your package implements key down behavior for a particular scenario, you
should prevent the default behavior of the key via `event.preventDefault()`.
You may mutate the contenteditable in place, we not expect any return value
from this method.
Important: You should prevent the default key down behavior with great care.
- event: DOM event fired on the contenteditable
- editableNode: DOM node that represents the current contenteditable.This object
can be mutated in place to modify the Contenteditable's content
- selection: [Selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection)
object that represents the current selection on the contenteditable
###
@onKeyDown: (event, editableNode, selection) ->
###
Public: Override onInput to mutate the contenteditable DOM node whenever the
onInput event is fired on it.You may mutate the contenteditable in place, we
not expect any return value from this method.
- event: DOM event fired on the contenteditable
- editableNode: DOM node that represents the current contenteditable.This object
can be mutated in place to modify the Contenteditable's content
- selection: [Selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection)
object that represents the current selection on the contenteditable
- menu: [Menu](https://github.com/atom/electron/blob/master/docs/api/menu.md)
object you can mutate in order to add new [MenuItems](https://github.com/atom/electron/blob/master/docs/api/menu-item.md)
to the context menu that will be displayed when you right click the contenteditable.
###
@onShowContextMenu: (event, editableNode, selection, menu) ->
module.exports = ContenteditableExtension

View file

@ -0,0 +1,34 @@
###
Public: To create MessageViewExtension that customize message viewing, you
should create objects that implement the interface defined at {MessageViewExtension}.
To register your extension with the ExtensionRegistry, call {ExtensionRegistry::MessageView::registerExtension}.
When your package is being unloaded, you *must* call the corresponding
{ExtensionRegistry::MessageView::unregisterExtension} to unhook your extension.
```coffee
activate: ->
ExtensionRegistry.MessageView.register(MyExtension)
...
deactivate: ->
ExtensionRegistry.MessageView.unregister(MyExtension)
```
The MessageViewExtension API does not currently expose any asynchronous or {Promise}-based APIs.
This will likely change in the future. If you have a use-case for a Message Store extension that
is not possible with the current API, please let us know.
Section: Extensions
###
class MessageViewExtension
###
Public: Transform the message body HTML provided in `body` and return HTML
that should be displayed for the message.
###
@formatMessageBody: (body) ->
return body
module.exports = MessageViewExtension

View file

@ -1,174 +1,7 @@
###
Public: DraftStoreExtension is an abstract base class. To create DraftStoreExtensions
that enhance the composer experience, you should subclass {DraftStoreExtension} and
implement the class methods your plugin needs.
To register your extension with the DraftStore, call {DraftStore::registerExtension}.
When your package is being unloaded, you *must* call the corresponding
{DraftStore::unregisterExtension} to unhook your extension.
```coffee
activate: ->
DraftStore.registerExtension(MyExtension)
...
deactivate: ->
DraftStore.unregisterExtension(MyExtension)
```
Your DraftStoreExtension subclass should be stateless. The user may have multiple drafts
open at any time, and the methods of your DraftStoreExtension may be called for different
drafts at any time. You should not expect that the session you receive in
{::finalizeSessionBeforeSending} is for the same draft you previously received in
{::warningsForSending}, etc.
The DraftStoreExtension API does not currently expose any asynchronous or {Promise}-based APIs.
This will likely change in the future. If you have a use-case for a Draft Store extension that
is not possible with the current API, please let us know.
Section: Drafts
Public: DraftStoreExtension is deprecated. Use {ComposerExtension} instead.
Section: Extensions
###
class DraftStoreExtension
###
Public: Inspect the draft, and return any warnings that need to be displayed before
the draft is sent. Warnings should be string phrases, such as "without an attachment"
that fit into a message of the form: "Send #{phase1} and #{phase2}?"
- `draft`: A fully populated {Message} object that is about to be sent.
Returns a list of warning strings, or an empty array if no warnings need to be displayed.
###
@warningsForSending: (draft) ->
[]
###
Public: declare an icon to be displayed in the composer's toolbar (where
bold, italic, underline, etc are).
You must declare the following properties:
- `mutator`: A function that's called when your toolbar button is
clicked. This mutator function will be passed as its only argument the
`dom`. The `dom` is the full {DOM} object of the current composer. You
may mutate this in place. We don't care about the mutator's return
value.
- `tooltip`: A one or two word description of what your icon does
- `iconUrl`: The url of your icon. It should be in the `nylas://` scheme.
For example: `nylas://your-package-name/assets/my-icon@2x.png`. Note, we
will downsample your image by 2x (for Retina screens), so make sure it's
twice the resolution. The icon should be black and white. We will
directly pass the `url` prop of a {RetinaImg}
###
@composerToolbar: ->
return
###
Public: Override prepareNewDraft to modify a brand new draft before it is displayed
in a composer. This is one of the only places in the application where it's safe
to modify the draft object you're given directly to add participants to the draft,
add a signature, etc.
By default, new drafts are considered `pristine`. If the user leaves the composer
without making any changes, the draft is discarded. If your extension populates
the draft in a way that makes it "populated" in a valuable way, you should set
`draft.pristine = false` so the draft saves, even if no further changes are made.
###
@prepareNewDraft: (draft) ->
return
###
Public: Override finalizeSessionBeforeSending in your DraftStoreExtension subclass to transform
the {DraftStoreProxy} editing session just before the draft is sent. This method
gives you an opportunity to make any final substitutions or changes after any
{::warningsForSending} have been displayed.
- `session`: A {DraftStoreProxy} for the draft.
Example:
```coffee
# Remove any <code> tags found in the draft body
finalizeSessionBeforeSending: (session) ->
body = session.draft().body
clean = body.replace(/<\/?code[^>]*>/g, '')
if body != clean
session.changes.add(body: clean)
```
###
@finalizeSessionBeforeSending: (session) ->
return
###
Public: Override onMouseUp in your DraftStoreExtension subclass to
listen for mouse up events sent to the composer's body text area. This
hook provides the contenteditable DOM Node itself, allowing you to
adjust selection ranges and change content as necessary.
- `editableNode` The composer's contenteditable {Node}
that received the event.
- `range`: The currently selected {Range} in the `editableNode`
- `event`: The mouse up event.
###
@onMouseUp: (editableNode, range, event) ->
return
###
Public: Called when the user presses `Tab` while focused on the composer's body field.
Override onTabDown in your DraftStoreExtension to adjust the selection or perform
other actions. If your package implements Tab behavior in a particular scenario, you
should prevent the default behavior of Tab via `event.preventDefault()`.
Important: You should prevent the default tab behavior with great care.
- `editableNode` The composer's contenteditable {Node} that received the event.
- `range`: The currently selected {Range} in the `editableNode`
- `event`: The mouse up event.
###
@onTabDown: (editableNode, range, event) ->
return
###
Public: Override onInput in your DraftStoreExtension subclass to
implement custom behavior as the user types in the composer's
contenteditable body field.
As the first argument you are passed the entire DOM object of the
composer. You may mutate this object and edit it in place.
Example:
The Nylas `templates` package uses this method to see if the user has populated a
`<code>` tag placed in the body and change it's CSS class to reflect that it is no
longer empty.
```coffee
onInput: (editableNode, event) ->
selection = document.getSelection()
isWithinNode = (node) ->
test = selection.baseNode
while test isnt editableNode
return true if test is node
test = test.parentNode
return false
codeTags = editableNode.querySelectorAll('code.var.empty')
for codeTag in codeTags
if selection.containsNode(codeTag) or isWithinNode(codeTag)
codeTag.classList.remove('empty')
```
###
@onInput: (editableNode, event) ->
return
module.exports = DraftStoreExtension

View file

@ -29,6 +29,9 @@ CoffeeHelpers = require '../coffee-helpers'
DOMUtils = require '../../dom-utils'
RegExpUtils = require '../../regexp-utils'
ExtensionRegistry = require '../../extension-registry'
{deprecate} = require '../../deprecate-utils'
###
Public: DraftStore responds to Actions that interact with Drafts and exposes
public getter methods to return Draft objects and sessions.
@ -72,7 +75,6 @@ class DraftStore
NylasEnv.onBeforeUnload @_onBeforeUnload
@_draftSessions = {}
@_extensions = []
@_inlineStylePromises = {}
@_inlineStyleResolvers = {}
@ -131,23 +133,25 @@ class DraftStore
# Public: Returns the extensions registered with the DraftStore.
extensions: =>
@_extensions
ExtensionRegistry.Composer.extensions()
# Public: Registers a new extension with the DraftStore. DraftStore extensions
# Public: Deprecated, use {ExtensionRegistry.Composer.register} instead.
# Registers a new extension with the DraftStore. DraftStore extensions
# make it possible to extend the editor experience, modify draft contents,
# display warnings before draft are sent, and more.
#
# - `ext` A {DraftStoreExtension} instance.
# - `ext` A {ComposerExtension} instance.
#
registerExtension: (ext) =>
@_extensions.push(ext)
ExtensionRegistry.Composer.register(ext)
# Public: Unregisters the extension provided from the DraftStore.
# Public: Deprecated, use {ExtensionRegistry.Composer.unregister} instead.
# Unregisters the extension provided from the DraftStore.
#
# - `ext` A {DraftStoreExtension} instance.
# - `ext` A {ComposerExtension} instance.
#
unregisterExtension: (ext) =>
@_extensions = _.without(@_extensions, ext)
ExtensionRegistry.Composer.unregister(ext)
########### PRIVATE ####################################################
@ -228,7 +232,7 @@ class DraftStore
_finalizeAndPersistNewMessage: (draft) =>
# Give extensions an opportunity to perform additional setup to the draft
for extension in @_extensions
for extension in @extensions()
continue unless extension.prepareNewDraft
extension.prepareNewDraft(draft)
@ -516,7 +520,7 @@ class DraftStore
# Give third-party plugins an opportunity to sanitize draft data
_runExtensionsBeforeSend: (session) ->
for extension in @_extensions
for extension in @extensions()
continue unless extension.finalizeSessionBeforeSending
extension.finalizeSessionBeforeSending(session)
@ -537,4 +541,19 @@ class DraftStore
NylasEnv.showErrorDialog(errorMessage)
, 100
module.exports = new DraftStore()
# Deprecations
store = new DraftStore()
store.registerExtension = deprecate(
'DraftStore.registerExtension',
'ExtensionRegistry.Composer.register',
store,
store.registerExtension
)
store.unregisterExtension = deprecate(
'DraftStore.unregisterExtension',
'ExtensionRegistry.Composer.unregister',
store,
store.unregisterExtension
)
module.exports = store

View file

@ -1,35 +1,7 @@
###
Public: MessageStoreExtension is an abstract base class. To create MessageStoreExtension
that customize message viewing, you should subclass {MessageStoreExtension} and
implement the class methods your plugin needs.
To register your extension with the MessageStore, call {MessageStore::registerExtension}.
When your package is being unloaded, you *must* call the corresponding
{MessageStore::unregisterExtension} to unhook your extension.
```coffee
activate: ->
MessageStore.registerExtension(MyExtension)
...
deactivate: ->
MessageStore.unregisterExtension(MyExtension)
```
The MessageStoreExtension API does not currently expose any asynchronous or {Promise}-based APIs.
This will likely change in the future. If you have a use-case for a Message Store extension that
is not possible with the current API, please let us know.
Section: Stores
Public: MessageStoreExtension is deprecated. Use {MessageViewExtension} instead.
Section: Extensions
###
class MessageStoreExtension
###
Public: Transform the message body HTML provided in `body` and return HTML
that should be displayed for the message.
###
@formatMessageBody: (body) ->
return body
module.exports = MessageStoreExtension

View file

@ -8,6 +8,8 @@ AccountStore = require "./account-store"
FocusedContentStore = require "./focused-content-store"
ChangeUnreadTask = require '../tasks/change-unread-task'
NylasAPI = require '../nylas-api'
ExtensionRegistry = require '../../extension-registry'
{deprecate} = require '../../deprecate-utils'
async = require 'async'
_ = require 'underscore'
@ -43,25 +45,27 @@ class MessageStore extends NylasStore
# Public: Returns the extensions registered with the MessageStore.
extensions: =>
@_extensions
ExtensionRegistry.MessageView.extensions()
# Public: Registers a new extension with the MessageStore. MessageStore extensions
# Public: Deprecated, use {ExtensionRegistry.MessageView.register} instead.
# Registers a new extension with the MessageStore. MessageStore extensions
# make it possible to customize message body parsing, and will do more in
# the future.
#
# - `ext` A {MessageStoreExtension} instance.
# - `ext` A {MessageViewExtension} instance.
#
registerExtension: (ext) =>
@_extensions.push(ext)
MessageBodyProcessor = require './message-body-processor'
MessageBodyProcessor.resetCache()
ExtensionRegistry.MessageView.register(ext)
# Public: Unregisters the extension provided from the MessageStore.
# Public: Deprecated, use {ExtensionRegistry.MessageView.unregister} instead.
# Unregisters the extension provided from the MessageStore.
#
# - `ext` A {MessageStoreExtension} instance.
# - `ext` A {MessageViewExtension} instance.
#
unregisterExtension: (ext) =>
@_extensions = _.without(@_extensions, ext)
ExtensionRegistry.MessageView.unregister(ext)
_onExtensionsChanged: (role) ->
MessageBodyProcessor = require './message-body-processor'
MessageBodyProcessor.resetCache()
@ -73,10 +77,10 @@ class MessageStore extends NylasStore
@_itemsExpanded = {}
@_itemsLoading = false
@_thread = null
@_extensions = []
@_inflight = {}
_registerListeners: ->
@listenTo ExtensionRegistry.MessageView, @_onExtensionsChanged
@listenTo DatabaseStore, @_onDataChanged
@listenTo FocusedContentStore, @_onFocusChanged
@listenTo Actions.toggleMessageIdExpanded, @_onToggleMessageIdExpanded
@ -287,4 +291,17 @@ class MessageStore extends NylasStore
items
module.exports = new MessageStore()
store = new MessageStore()
store.registerExtension = deprecate(
'MessageStore.registerExtension',
'ExtensionRegistry.MessageView.register',
store,
store.registerExtension
)
store.unregisterExtension = deprecate(
'MessageStore.unregisterExtension',
'ExtensionRegistry.MessageView.unregister',
store,
store.unregisterExtension
)
module.exports = store

View file

@ -33,6 +33,15 @@ class NylasExports
NylasExports.registerSerializable(exported)
@[prop] = exported
@requireDeprecated = (prop, path, {instead} = {}) ->
{deprecate} = require '../deprecate-utils'
Object.defineProperty @, prop,
get: deprecate prop, instead, @, ->
exported = require "../#{path}"
NylasExports.registerSerializable(exported)
return exported
enumerable: true
# Actions
@load "Actions", 'flux/actions'
@ -108,14 +117,24 @@ class NylasExports
@require "ThreadCountsStore", 'flux/stores/thread-counts-store'
@require "UnreadBadgeStore", 'flux/stores/unread-badge-store'
@require "FileDownloadStore", 'flux/stores/file-download-store'
@require "DraftStoreExtension", 'flux/stores/draft-store-extension'
@require "FocusedContentStore", 'flux/stores/focused-content-store'
@require "FocusedMailViewStore", 'flux/stores/focused-mail-view-store'
@require "FocusedContactsStore", 'flux/stores/focused-contacts-store'
@require "MessageBodyProcessor", 'flux/stores/message-body-processor'
@require "MessageStoreExtension", 'flux/stores/message-store-extension'
@require "PreferencesUIStore", 'flux/stores/preferences-ui-store'
# Deprecated
@requireDeprecated "DraftStoreExtension", 'flux/stores/draft-store-extension',
instead: 'ComposerExtension'
@requireDeprecated "MessageStoreExtension", 'flux/stores/message-store-extension',
instead: 'MessageViewExtension'
# Extensions
@require "ExtensionRegistry", 'extension-registry'
@require "ContenteditableExtension", 'flux/extensions/contenteditable-extension'
@require "ComposerExtension", 'flux/extensions/composer-extension'
@require "MessageViewExtension", 'flux/extensions/message-view-extension'
# React Components
@get "React", -> require 'react' # Our version of React for 3rd party use
@load "ReactRemote", 'react-remote/react-remote-parent'
@ -146,9 +165,6 @@ class NylasExports
@load "BufferedProcess", 'buffered-process'
@get "APMWrapper", -> require('../apm-wrapper')
# Contenteditable
@load "ContenteditablePlugin", 'components/contenteditable/contenteditable-plugin'
# Testing
@get "NylasTestUtils", -> require '../../spec/nylas-test-utils'