diff --git a/internal_packages/message-list/lib/message-item-body.cjsx b/internal_packages/message-list/lib/message-item-body.cjsx index d911c2d43..2ae5dc60f 100644 --- a/internal_packages/message-list/lib/message-item-body.cjsx +++ b/internal_packages/message-list/lib/message-item-body.cjsx @@ -22,20 +22,12 @@ class MessageItemBody extends React.Component processedBody: undefined componentWillMount: => - @_prepareBody(@props) + @_unsub = MessageBodyProcessor.processAndSubscribe(@props.message, @_onBodyChanged) - shouldComponentUpdate: (nextProps, nextState) -> - not Utils.isEqualReact(nextProps, @props) or - not Utils.isEqualReact(nextState, @state) - - componentWillUpdate: (nextProps, nextState) => - if not Utils.isEqualReact(nextProps.message, @props.message) - @_prepareBody(nextProps) - - _prepareBody: (props) -> - MessageBodyProcessor.resetCache(props.message) - @_unsub?() - @_unsub = MessageBodyProcessor.processAndSubscribe(props.message, @_onBodyChanged) + componentWillReceiveProps: (nextProps) -> + if nextProps.message.id isnt @props.message.id + @_unsub?() + @_unsub = MessageBodyProcessor.processAndSubscribe(nextProps.message, @_onBodyChanged) componentWillUnmount: => @_unsub?() diff --git a/src/flux/stores/message-body-processor.coffee b/src/flux/stores/message-body-processor.coffee deleted file mode 100644 index 2bacbbc96..000000000 --- a/src/flux/stores/message-body-processor.coffee +++ /dev/null @@ -1,96 +0,0 @@ -_ = require "underscore" -crypto = require "crypto" -MessageUtils = require '../models/message-utils' -MessageStore = require './message-store' - -MessageBodyWidth = 740 - -class MessageBodyProcessor - - constructor: -> - @_subscriptions = [] - @resetCache() - - resetCache: (msg) -> - if msg - key = @_key(msg) - delete @_recentlyProcessedD[key] - else - # 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 = {} - for {message, callback} in @_subscriptions - callback(@process(message)) - - # It's far safer to key off the hash of the body then the [id, version] - # pair. This is because it's theoretically possible for the body to - # change without the version updating. When drafts sent N1 used to - # optimistically display the message before the latest changes - # persisted. - _key: (message) -> - return message.id + crypto.createHash('md5').update(message.body ? "").digest('hex') - - version: -> - @_version - - processAndSubscribe: (message, callback) => - _.defer => callback(@process(message)) - sub = {message, callback} - @_subscriptions.push(sub) - return => - @_subscriptions.splice(@_subscriptions.indexOf(sub), 1) - - process: (message) => - body = message.body - return "" unless _.isString message.body - - key = @_key(message) - 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 - latestBody = body - try - virtual = message.clone() - virtual.body = body - extension.formatMessageBody({message: virtual}) - body = virtual.body - catch err - NylasEnv.reportError(err) - body = latestBody - - # 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-body-processor.es6 b/src/flux/stores/message-body-processor.es6 new file mode 100644 index 000000000..074311439 --- /dev/null +++ b/src/flux/stores/message-body-processor.es6 @@ -0,0 +1,150 @@ +import _ from 'underscore'; +import Message from '../models/message'; +import MessageUtils from '../models/message-utils'; +import MessageStore from './message-store'; +import DatabaseStore from './database-store'; + +const MessageBodyWidth = 740; + +class MessageBodyProcessor { + constructor() { + this._subscriptions = []; + this.resetCache(); + + DatabaseStore.listen((change) => { + if (change.objectClass === Message.name) { + change.objects.forEach(this.updateCacheForMessage); + } + }); + } + + 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) + this._recentlyProcessedA = []; + this._recentlyProcessedD = {}; + for (const {message, callback} of this._subscriptions) { + callback(this.process(message)); + } + } + + updateCacheForMessage = (changedMessage) => { + // check that the message exists in the cache + const changedKey = this._key(changedMessage); + if (!this._recentlyProcessedD[changedKey]) { + return; + } + + // remove the message from the cache + delete this._recentlyProcessedD[changedKey]; + this._recentlyProcessedA = this._recentlyProcessedA.filter(({key}) => + key !== changedKey + ); + + // reprocess any subscription using the new message data. Note that + // changedMessage may not have a loaded body if it wasn't changed. In + // that case, we use the previous body. + const subscriptions = this._subscriptions.filter(({message}) => + message.id === changedMessage.id + ); + + if (subscriptions.length > 0) { + const updatedMessage = changedMessage.clone(); + updatedMessage.body = updatedMessage.body || subscriptions[0].message.body; + const output = this.process(updatedMessage); + for (const subscription of subscriptions) { + subscription.callback(output); + subscription.message = updatedMessage; + } + } + } + + _key(message) { + // It's safe to key off of the message ID alone because we invalidate the + // cache whenever the message is persisted to the database. + return message.id; + } + + version() { + return this._version; + } + + processAndSubscribe(message, callback) { + _.defer(() => callback(this.process(message))); + const sub = {message, callback} + this._subscriptions.push(sub); + return () => { + this._subscriptions.splice(this._subscriptions.indexOf(sub), 1); + } + } + + process(message) { + let body = message.body; + if (!_.isString(message.body)) { + return ""; + } + + const key = this._key(message); + if (this._recentlyProcessedD[key]) { + return this._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 (const extension of MessageStore.extensions()) { + if (!extension.formatMessageBody) { + continue; + } + const previousBody = body; + try { + const virtual = message.clone(); + virtual.body = body; + extension.formatMessageBody({message: virtual}); + body = virtual.body; + } catch (err) { + NylasEnv.reportError(err); + body = previousBody; + } + } + + // 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! + let result = MessageUtils.cidRegex.exec(body); + + while (result !== null) { + const imgstart = body.lastIndexOf('<', result.index); + const imgend = body.indexOf('/>', result.index); + + if ((imgstart !== -1) && (imgend > imgstart)) { + const imgtag = body.substr(imgstart, imgend - imgstart); + const widthMatch = imgtag.match(/width[ ]?=[ ]?['"]?(\d*)['"]?/); + const width = widthMatch ? widthMatch[1] : null; + const heightMatch = imgtag.match(/height[ ]?=[ ]?['"]?(\d*)['"]?/); + const height = heightMatch ? heightMatch[1] : null; + if (width && height) { + const scale = Math.min(1, MessageBodyWidth / width); + const style = ` style="height:${height * scale}px;" ` + body = body.substr(0, imgend) + style + body.substr(imgend); + } + } + + result = MessageUtils.cidRegex.exec(body); + } + this.addToCache(key, body); + return body; + } + + addToCache(key, body) { + if (this._recentlyProcessedA.length > 50) { + const removed = this._recentlyProcessedA.pop() + delete this._recentlyProcessedD[removed.key] + } + const item = {key, body}; + this._recentlyProcessedA.unshift(item); + this._recentlyProcessedD[key] = item; + } +} + +module.exports = new MessageBodyProcessor();