From 78681f6ed1ec2a3b17b33c87fd1220eb66c37a69 Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Thu, 31 Mar 2016 12:13:46 -0700 Subject: [PATCH] fix(inline): radial progress, merge body with download data on render --- .../message-list/lib/email-frame.jsx | 44 +++++++++++++------ .../message-list/lib/message-item-body.cjsx | 21 +++++---- src/canvas-utils.coffee | 28 +++++++++++- src/components/evented-iframe.cjsx | 9 +++- 4 files changed, 77 insertions(+), 25 deletions(-) diff --git a/internal_packages/message-list/lib/email-frame.jsx b/internal_packages/message-list/lib/email-frame.jsx index 750d755c7..d430a34cb 100644 --- a/internal_packages/message-list/lib/email-frame.jsx +++ b/internal_packages/message-list/lib/email-frame.jsx @@ -47,9 +47,8 @@ export default class EmailFrame extends React.Component { } _writeContent = () => { - this._lastComputedHeight = 0; - const domNode = ReactDOM.findDOMNode(this); - const doc = domNode.contentDocument; + const iframeNode = ReactDOM.findDOMNode(this.refs.iframe); + const doc = iframeNode.contentDocument; if (!doc) { return; } doc.open(); @@ -71,7 +70,13 @@ export default class EmailFrame extends React.Component { // Notify the EventedIFrame that we've replaced it's document (with `open`) // so it can attach event listeners again. this.refs.iframe.documentWasReplaced(); - domNode.height = '0px'; + this._onMustRecalculateFrameHeight(); + } + + _onMustRecalculateFrameHeight = () => { + const iframeNode = ReactDOM.findDOMNode(this.refs.iframe); + iframeNode.height = `0px`; + this._lastComputedHeight = 0; this._setFrameHeight(); } @@ -90,31 +95,42 @@ export default class EmailFrame extends React.Component { return; } - const domNode = ReactDOM.findDOMNode(this); - const height = this._getFrameHeight(domNode.contentDocument); + // Q: What's up with this holder? + // A: If you resize the window, or do something to trigger setFrameHeight + // on an already-loaded message view, all the heights go to zero for a brief + // second while the heights are recomputed. This causes the ScrollRegion to + // reset it's scrollTop to ~0 (the new combined heiht of all children). + // To prevent this, the holderNode holds the last computed height until + // the new height is computed. + const holderNode = ReactDOM.findDOMNode(this.refs.iframeHeightHolder); + const iframeNode = ReactDOM.findDOMNode(this.refs.iframe); + const height = this._getFrameHeight(iframeNode.contentDocument); // Why 5px? Some emails have elements with a height of 100%, and then put // tracking pixels beneath that. In these scenarios, the scrollHeight of the // message is always <100% + 1px>, which leads us to resize them constantly. // This is a hack, but I'm not sure of a better solution. if (Math.abs(height - this._lastComputedHeight) > 5) { - domNode.height = `${height}px`; + this.refs.iframe.setHeightQuietly(height); + holderNode.height = `${height}px`; this._lastComputedHeight = height; } - if (domNode.contentDocument.readyState !== 'complete') { + if (iframeNode.contentDocument.readyState !== 'complete') { _.defer(()=> this._setFrameHeight()); } } render() { return ( - +
+ +
); } } diff --git a/internal_packages/message-list/lib/message-item-body.cjsx b/internal_packages/message-list/lib/message-item-body.cjsx index 0895613df..f60fa389b 100644 --- a/internal_packages/message-list/lib/message-item-body.cjsx +++ b/internal_packages/message-list/lib/message-item-body.cjsx @@ -2,6 +2,7 @@ React = require 'react' _ = require 'underscore' EmailFrame = require './email-frame' {Utils, + CanvasUtils, NylasAPI, MessageUtils, MessageBodyProcessor, @@ -25,7 +26,8 @@ class MessageItemBody extends React.Component error: null componentWillMount: => - @_unsub = MessageBodyProcessor.subscribe(@props.message, @_onBodyProcessed) + @_unsub = MessageBodyProcessor.subscribe @props.message, (processedBody) => + @setState({processedBody}) componentDidMount: => @_onFetchBody() if not _.isString(@props.message.body) @@ -33,7 +35,8 @@ class MessageItemBody extends React.Component componentWillReceiveProps: (nextProps) -> if nextProps.message.id isnt @props.message.id @_unsub?() - @_unsub = MessageBodyProcessor.subscribe(nextProps.message, @_onBodyProcessed) + @_unsub = MessageBodyProcessor.subscribe nextProps.message, (processedBody) => + @setState({processedBody}) componentWillUnmount: => @_unmounted = true @@ -52,7 +55,9 @@ class MessageItemBody extends React.Component _renderBody: => if _.isString(@props.message.body) and _.isString(@state.processedBody) - + finalBody = @_mergeBodyWithFiles(@state.processedBody) + + else if @state.error
Sorry, this message could not be loaded. (Status code {@state.error.statusCode}) @@ -90,9 +95,7 @@ class MessageItemBody extends React.Component return if @_unmounted @setState({error}) - _onBodyProcessed: (body) => - downloadingSpinnerPath = Utils.imageNamed('inline-loading-spinner.gif') - + _mergeBodyWithFiles: (body) => # Replace cid:// references with the paths to downloaded files for file in @props.message.files download = @props.downloads[file.id] @@ -101,7 +104,8 @@ class MessageItemBody extends React.Component if download and download.state isnt 'finished' # Render a spinner and inject a `style` tag that injects object-position / object-fit body = body.replace cidRegexp, (text, quoteCharacter) -> - "#{downloadingSpinnerPath}#{quoteCharacter} style=#{quoteCharacter} object-position: 50% 50%; object-fit: none; " + dataUri = CanvasUtils.dataURIForLoadedPercent(download.percent) + "#{dataUri}#{quoteCharacter} style=#{quoteCharacter} object-position: 50% 50%; object-fit: none; " else # Render the completed download body = body.replace cidRegexp, (text, quoteCharacter) -> @@ -112,7 +116,6 @@ class MessageItemBody extends React.Component # no "missing image" region shown, just a space. body = body.replace(MessageUtils.cidRegex, "src=\"#{TransparentPixel}\"") - @setState - processedBody: body + return body module.exports = MessageItemBody diff --git a/src/canvas-utils.coffee b/src/canvas-utils.coffee index 4a6363cf5..29f2a1ca7 100644 --- a/src/canvas-utils.coffee +++ b/src/canvas-utils.coffee @@ -5,6 +5,11 @@ DragCanvas = document.createElement("canvas") DragCanvas.style.position = "absolute" document.body.appendChild(DragCanvas) +PercentLoadedCache = {} +PercentLoadedCanvas = document.createElement("canvas") +PercentLoadedCanvas.style.position = "absolute" +document.body.appendChild(PercentLoadedCanvas) + SystemTrayCanvas = document.createElement("canvas") CanvasUtils = @@ -23,6 +28,28 @@ CanvasUtils = ctx.stroke() if stroke ctx.fill() if fill + dataURIForLoadedPercent: (percent) -> + percent = Math.floor(percent / 5.0) * 5.0 + cacheKey = "#{percent}%" + if not PercentLoadedCache[cacheKey] + canvas = PercentLoadedCanvas + scale = window.devicePixelRatio + canvas.width = 20 * scale + canvas.height = 20 * scale + canvas.style.width = "30px" + canvas.style.height = "30px" + + half = 10 * scale + ctx = canvas.getContext('2d') + ctx.strokeStyle = "#AAA" + ctx.lineWidth = 3 * scale + ctx.clearRect(0, 0, 20 * scale, 20 * scale) + ctx.beginPath() + ctx.arc(half, half, half - ctx.lineWidth, -0.5 * Math.PI, (-0.5 * Math.PI) + (2 * Math.PI) * percent / 100.0) + ctx.stroke() + PercentLoadedCache[cacheKey] = canvas.toDataURL() + return PercentLoadedCache[cacheKey] + canvasWithThreadDragImage: (count) -> canvas = DragCanvas @@ -33,7 +60,6 @@ CanvasUtils = canvas.style.width = "58px" canvas.style.height = "55px" - # necessary for setDragImage to work ctx = canvas.getContext('2d') # mail background image diff --git a/src/components/evented-iframe.cjsx b/src/components/evented-iframe.cjsx index 5772edbca..07f5bedd6 100644 --- a/src/components/evented-iframe.cjsx +++ b/src/components/evented-iframe.cjsx @@ -56,10 +56,14 @@ class EventedIFrame extends React.Component Public: Call this method if you replace the contents of the iframe's document. This allows {EventedIframe} to re-attach it's event listeners. ### - documentWasReplaced: => + didReplaceDocument: => @_unsubscribeFromIFrameEvents() @_subscribeToIFrameEvents() + setHeightQuietly: (height) => + @_ignoreNextResize = true + ReactDOM.findDOMNode(@).height = "#{height}px" + _onSearchableStoreChange: => return unless @props.searchable node = ReactDOM.findDOMNode(@) @@ -116,6 +120,9 @@ class EventedIFrame extends React.Component window.getSelection().empty() _onIFrameResize: (event) => + if @_ignoreNextResize + @_ignoreNextResize = false + return @props.onResize?(event) # The iFrame captures events that take place over it, which causes some