diff --git a/exports/nylas-exports.coffee b/exports/nylas-exports.coffee
index 4efe6ef66..812de4c56 100644
--- a/exports/nylas-exports.coffee
+++ b/exports/nylas-exports.coffee
@@ -62,6 +62,7 @@ Exports =
DraftCountStore: require '../src/flux/stores/draft-count-store'
DraftStoreExtension: require '../src/flux/stores/draft-store-extension'
MessageStore: require '../src/flux/stores/message-store'
+ MessageBodyProcessor: require '../src/flux/stores/message-body-processor'
EventStore: require '../src/flux/stores/event-store'
MessageStoreExtension: require '../src/flux/stores/message-store-extension'
ContactStore: require '../src/flux/stores/contact-store'
diff --git a/internal_packages/message-list/lib/email-frame.cjsx b/internal_packages/message-list/lib/email-frame.cjsx
index a6b3afed6..54c9c41a5 100644
--- a/internal_packages/message-list/lib/email-frame.cjsx
+++ b/internal_packages/message-list/lib/email-frame.cjsx
@@ -6,6 +6,9 @@ _ = require "underscore"
class EmailFrame extends React.Component
@displayName = 'EmailFrame'
+ @propTypes:
+ content: React.PropTypes.string.isRequired
+
render: =>
@@ -57,13 +60,11 @@ class EmailFrame extends React.Component
@_setFrameHeight()
_emailContent: =>
- email = @props.children
-
# When showing quoted text, always return the pure content
if @props.showQuotedText
- email
+ @props.content
else
- QuotedHTMLParser.hideQuotedHTML(email)
+ QuotedHTMLParser.hideQuotedHTML(@props.content)
module.exports = EmailFrame
diff --git a/internal_packages/message-list/lib/message-item.cjsx b/internal_packages/message-list/lib/message-item.cjsx
index b76aecda2..64ed7d309 100644
--- a/internal_packages/message-list/lib/message-item.cjsx
+++ b/internal_packages/message-list/lib/message-item.cjsx
@@ -10,6 +10,7 @@ MessageControls = require './message-controls'
MessageUtils,
NamespaceStore,
MessageStore,
+ MessageBodyProcessor,
QuotedHTMLParser,
ComponentRegistry,
FileDownloadStore} = require 'nylas-exports'
@@ -18,7 +19,6 @@ MessageControls = require './message-controls'
InjectedComponent} = require 'nylas-component-kit'
TransparentPixel = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNikAQAACIAHF/uBd8AAAAASUVORK5CYII="
-MessageBodyWidth = 740
class MessageItem extends React.Component
@displayName = 'MessageItem'
@@ -79,9 +79,7 @@ class MessageItem extends React.Component
{@_renderHeader()}
-
- {@_formatBody()}
-
+
{@_renderEvents()}
{@_renderAttachments()}
@@ -206,33 +204,8 @@ class MessageItem extends React.Component
_formatBody: =>
return "" unless @props.message and @props.message.body
- # Give each extension the message object to process the body, but don't
- # allow them to modify anything but the body for the time being.
- body = @props.message.body
- for extension in MessageStore.extensions()
- continue unless extension.formatMessageBody
- virtual = @props.message.clone()
- virtual.body = body
- extension.formatMessageBody(virtual)
- body = virtual.body
-
- # Find inline images and give them a calculated CSS height based on
- # html width and height, when available. This means nothing changes size
- # as the image is loaded, and we can estimate final height correctly.
- # Note that MessageBodyWidth must be updated if the UI is changed!
-
- while (result = MessageUtils.cidRegex.exec(body)) isnt null
- imgstart = body.lastIndexOf('<', result.index)
- imgend = body.indexOf('/>', result.index)
-
- if imgstart != -1 and imgend > imgstart
- imgtag = body.substr(imgstart, imgend - imgstart)
- width = imgtag.match(/width[ ]?=[ ]?['"]?(\d*)['"]?/)?[1]
- height = imgtag.match(/height[ ]?=[ ]?['"]?(\d*)['"]?/)?[1]
- if width and height
- scale = Math.min(1, MessageBodyWidth / width)
- style = " style=\"height:#{height * scale}px;\" "
- body = body.substr(0, imgend) + style + body.substr(imgend)
+ # Runs extensions, potentially asynchronous soon
+ body = MessageBodyProcessor.process(@props.message.id, @props.message)
# Replace cid:// references with the paths to downloaded files
for file in @props.message.files
diff --git a/internal_packages/message-list/spec/message-item-spec.cjsx b/internal_packages/message-list/spec/message-item-spec.cjsx
index 5ee1d6334..965e4941c 100644
--- a/internal_packages/message-list/spec/message-item-spec.cjsx
+++ b/internal_packages/message-list/spec/message-item-spec.cjsx
@@ -8,7 +8,8 @@ ReactTestUtils = React.addons.TestUtils
Thread,
Utils,
QuotedHTMLParser,
- FileDownloadStore} = require "nylas-exports"
+ FileDownloadStore,
+ MessageBodyProcessor} = require "nylas-exports"
EmailFrameStub = React.createClass({render: -> })
@@ -97,6 +98,8 @@ describe "MessageItem", ->
spyOn(FileDownloadStore, 'downloadDataForFiles').andCallFake (ids) ->
return {'file_1_id': download, 'file_inline_downloading_id': download_inline}
+ spyOn(MessageBodyProcessor, 'addToCache').andCallFake ->
+
@message = new Message
id: "111"
from: [user_1]
diff --git a/internal_packages/thread-list/lib/thread-list-quick-actions.cjsx b/internal_packages/thread-list/lib/thread-list-quick-actions.cjsx
index cf9a5b1e6..6daef3a07 100644
--- a/internal_packages/thread-list/lib/thread-list-quick-actions.cjsx
+++ b/internal_packages/thread-list/lib/thread-list-quick-actions.cjsx
@@ -13,7 +13,7 @@ class ThreadListQuickActions extends React.Component
@displayName: 'ThreadListQuickActions'
@propTypes:
thread: React.PropTypes.object
- categoryId: React.PropTypes.integer
+ categoryId: React.PropTypes.string
render: =>
actions = []
diff --git a/src/browser/application.coffee b/src/browser/application.coffee
index b3e30d656..a4ca7b8fa 100644
--- a/src/browser/application.coffee
+++ b/src/browser/application.coffee
@@ -123,6 +123,28 @@ class Application
# which is why this check is here.
throw error unless error.code is 'ENOENT'
+ # On Windows, removing a file can fail if a process still has it open. When
+ # we close windows and log out, we need to wait for these processes to completely
+ # exit and then delete the file. It's hard to tell when this happens, so we just
+ # retry the deletion a few times.
+ deleteFileWithRetry: (filePath, callback, retries = 5) ->
+ callbackWithRetry = (err) =>
+ if err
+ console.log("File Error: #{err.message} - retrying in 150msec")
+ setTimeout =>
+ @deleteFileWithRetry(filePath, callback, retries - 1)
+ , 150
+ else
+ callback(null)
+
+ if not fs.existsSync(filePath)
+ callback(null)
+
+ if retries > 0
+ fs.unlink(filePath, callbackWithRetry)
+ else
+ fs.unlink(filePath, callback)
+
# Configures required javascript environment flags.
setupJavaScriptArguments: ->
app.commandLine.appendSwitch 'js-flags', '--harmony'
@@ -131,7 +153,7 @@ class Application
@setDatabasePhase('close')
@windowManager.closeMainWindow()
@windowManager.unregisterAllHotWindows()
- fs.unlink path.join(configDirPath,'edgehill.db'), =>
+ @deleteFileWithRetry path.join(configDirPath,'edgehill.db'), =>
@config.set('nylas', null)
@config.set('edgehill', null)
@setDatabasePhase('setup')
@@ -156,7 +178,7 @@ class Application
message: 'Upgrading Nylas'
detail: 'Welcome back to Nylas! We need to rebuild your mailbox to support new features. Please wait a few moments while we re-sync your mail.'
buttons: ['OK']
- fs.unlink path.join(configDirPath,'edgehill.db'), (err) =>
+ @deleteFileWithRetry path.join(configDirPath,'edgehill.db'), =>
@setDatabasePhase('setup')
@windowManager.showMainWindow()
diff --git a/src/flux/stores/database-store.coffee b/src/flux/stores/database-store.coffee
index 80a4d7236..fcb43300c 100644
--- a/src/flux/stores/database-store.coffee
+++ b/src/flux/stores/database-store.coffee
@@ -196,7 +196,7 @@ class DatabaseStore extends NylasStore
console.error("DatabaseStore: Query #{query}, #{JSON.stringify(values)} failed #{err.toString()}")
else
duration = Date.now() - start
- metadata = {duration: duration, resultLength: result?.length}
+ metadata = {duration: duration, resultLength: results?.length}
console.debug(DEBUG_TO_LOG, "DatabaseStore: END (#{duration}) #{query}", metadata)
if query is COMMIT
diff --git a/src/flux/stores/message-body-processor.coffee b/src/flux/stores/message-body-processor.coffee
new file mode 100644
index 000000000..615f687c0
--- /dev/null
+++ b/src/flux/stores/message-body-processor.coffee
@@ -0,0 +1,60 @@
+MessageUtils = require '../models/message-utils'
+MessageStore = require './message-store'
+
+MessageBodyWidth = 740
+
+class MessageBodyProcessor
+
+ constructor: ->
+ @resetCache()
+
+ resetCache: ->
+ # Store an object for recently processed items. Put the item reference into
+ # both data structures so we can access it in O(1) and also delete in O(1)
+ @_recentlyProcessedA = []
+ @_recentlyProcessedD = {}
+
+ process: (key, message) ->
+ body = message.body
+ if @_recentlyProcessedD[key]
+ return @_recentlyProcessedD[key].body
+
+ # Give each extension the message object to process the body, but don't
+ # allow them to modify anything but the body for the time being.
+ for extension in MessageStore.extensions()
+ continue unless extension.formatMessageBody
+ virtual = message.clone()
+ virtual.body = body
+ extension.formatMessageBody(virtual)
+ body = virtual.body
+
+ # Find inline images and give them a calculated CSS height based on
+ # html width and height, when available. This means nothing changes size
+ # as the image is loaded, and we can estimate final height correctly.
+ # Note that MessageBodyWidth must be updated if the UI is changed!
+
+ while (result = MessageUtils.cidRegex.exec(body)) isnt null
+ imgstart = body.lastIndexOf('<', result.index)
+ imgend = body.indexOf('/>', result.index)
+
+ if imgstart != -1 and imgend > imgstart
+ imgtag = body.substr(imgstart, imgend - imgstart)
+ width = imgtag.match(/width[ ]?=[ ]?['"]?(\d*)['"]?/)?[1]
+ height = imgtag.match(/height[ ]?=[ ]?['"]?(\d*)['"]?/)?[1]
+ if width and height
+ scale = Math.min(1, MessageBodyWidth / width)
+ style = " style=\"height:#{height * scale}px;\" "
+ body = body.substr(0, imgend) + style + body.substr(imgend)
+
+ @addToCache(key, body)
+ body
+
+ addToCache: (key, body) ->
+ if @_recentlyProcessedA.length > 50
+ removed = @_recentlyProcessedA.pop()
+ delete @_recentlyProcessedD[removed.key]
+ item = {key, body}
+ @_recentlyProcessedA.unshift(item)
+ @_recentlyProcessedD[key] = item
+
+module.exports = new MessageBodyProcessor()
diff --git a/src/flux/stores/message-store.coffee b/src/flux/stores/message-store.coffee
index 3edc9a3b6..f79aa3bd2 100644
--- a/src/flux/stores/message-store.coffee
+++ b/src/flux/stores/message-store.coffee
@@ -53,6 +53,8 @@ class MessageStore extends NylasStore
#
registerExtension: (ext) =>
@_extensions.push(ext)
+ MessageBodyProcessor = require './message-body-processor'
+ MessageBodyProcessor.resetCache()
# Public: Unregisters the extension provided from the MessageStore.
#
@@ -60,6 +62,8 @@ class MessageStore extends NylasStore
#
unregisterExtension: (ext) =>
@_extensions = _.without(@_extensions, ext)
+ MessageBodyProcessor = require './message-body-processor'
+ MessageBodyProcessor.resetCache()
########### PRIVATE ####################################################
@@ -84,18 +88,23 @@ class MessageStore extends NylasStore
if change.objectClass is Message.name
inDisplayedThread = _.some change.objects, (obj) => obj.threadId is @_thread.id
if inDisplayedThread
- @_fetchFromCache()
# Are we most likely adding a new draft? If the item is a draft and we don't
# have it's local Id, optimistically add it to the set, resort, and trigger.
# Note: this can avoid 100msec+ of delay from "Reply" => composer onscreen,
item = change.objects[0]
- if change.objects.length is 1 and item.draft is true and @_itemsLocalIds[item.id] is null
+ itemAlreadyExists = _.some @_items, (msg) -> msg.id is item.id
+ if change.objects.length is 1 and item.draft is true and not itemAlreadyExists
DatabaseStore.localIdForModel(item).then (localId) =>
@_itemsLocalIds[item.id] = localId
- @_items.push(item)
+ # We need to create a new copy of the items array so that the message-list
+ # can compare new state to previous state.
+ @_items = [].concat(@_items, [item])
@_items = @_sortItemsForDisplay(@_items)
+ @_expandItemsToDefault()
@trigger()
+ else
+ @_fetchFromCache()
if change.objectClass is Thread.name
updatedThread = _.find change.objects, (obj) => obj.id is @_thread.id
diff --git a/src/flux/tasks/destroy-draft.coffee b/src/flux/tasks/destroy-draft.coffee
index 2f3e569be..9a9a7aed5 100644
--- a/src/flux/tasks/destroy-draft.coffee
+++ b/src/flux/tasks/destroy-draft.coffee
@@ -35,7 +35,7 @@ class DestroyDraftTask extends Task
# when we performed locally, or if the draft has never been synced to
# the server (id is still self-assigned)
return Promise.resolve(Task.Status.Finished) unless @draft
- return Promise.resolve(Task.Status.Finished) unless @draft.isSaved()
+ return Promise.resolve(Task.Status.Finished) unless @draft.isSaved() and @draft.version
NylasAPI.makeRequest
path: "/n/#{@draft.namespaceId}/drafts/#{@draft.id}"