fix(inline): radial progress, merge body with download data on render

This commit is contained in:
Ben Gotow 2016-03-31 12:13:46 -07:00
parent 6faa7d5e12
commit 78681f6ed1
4 changed files with 77 additions and 25 deletions

View file

@ -47,9 +47,8 @@ export default class EmailFrame extends React.Component {
} }
_writeContent = () => { _writeContent = () => {
this._lastComputedHeight = 0; const iframeNode = ReactDOM.findDOMNode(this.refs.iframe);
const domNode = ReactDOM.findDOMNode(this); const doc = iframeNode.contentDocument;
const doc = domNode.contentDocument;
if (!doc) { return; } if (!doc) { return; }
doc.open(); 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`) // Notify the EventedIFrame that we've replaced it's document (with `open`)
// so it can attach event listeners again. // so it can attach event listeners again.
this.refs.iframe.documentWasReplaced(); 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(); this._setFrameHeight();
} }
@ -90,31 +95,42 @@ export default class EmailFrame extends React.Component {
return; return;
} }
const domNode = ReactDOM.findDOMNode(this); // Q: What's up with this holder?
const height = this._getFrameHeight(domNode.contentDocument); // 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 // 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 // tracking pixels beneath that. In these scenarios, the scrollHeight of the
// message is always <100% + 1px>, which leads us to resize them constantly. // 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. // This is a hack, but I'm not sure of a better solution.
if (Math.abs(height - this._lastComputedHeight) > 5) { if (Math.abs(height - this._lastComputedHeight) > 5) {
domNode.height = `${height}px`; this.refs.iframe.setHeightQuietly(height);
holderNode.height = `${height}px`;
this._lastComputedHeight = height; this._lastComputedHeight = height;
} }
if (domNode.contentDocument.readyState !== 'complete') { if (iframeNode.contentDocument.readyState !== 'complete') {
_.defer(()=> this._setFrameHeight()); _.defer(()=> this._setFrameHeight());
} }
} }
render() { render() {
return ( return (
<EventedIFrame <div ref="iframeHeightHolder" style={{height: this._lastComputedHeight}}>
ref="iframe" <EventedIFrame
seamless="seamless" ref="iframe"
searchable seamless="seamless"
onResize={this._setFrameHeight} searchable
/> onResize={this._onResize}
/>
</div>
); );
} }
} }

View file

@ -2,6 +2,7 @@ React = require 'react'
_ = require 'underscore' _ = require 'underscore'
EmailFrame = require './email-frame' EmailFrame = require './email-frame'
{Utils, {Utils,
CanvasUtils,
NylasAPI, NylasAPI,
MessageUtils, MessageUtils,
MessageBodyProcessor, MessageBodyProcessor,
@ -25,7 +26,8 @@ class MessageItemBody extends React.Component
error: null error: null
componentWillMount: => componentWillMount: =>
@_unsub = MessageBodyProcessor.subscribe(@props.message, @_onBodyProcessed) @_unsub = MessageBodyProcessor.subscribe @props.message, (processedBody) =>
@setState({processedBody})
componentDidMount: => componentDidMount: =>
@_onFetchBody() if not _.isString(@props.message.body) @_onFetchBody() if not _.isString(@props.message.body)
@ -33,7 +35,8 @@ class MessageItemBody extends React.Component
componentWillReceiveProps: (nextProps) -> componentWillReceiveProps: (nextProps) ->
if nextProps.message.id isnt @props.message.id if nextProps.message.id isnt @props.message.id
@_unsub?() @_unsub?()
@_unsub = MessageBodyProcessor.subscribe(nextProps.message, @_onBodyProcessed) @_unsub = MessageBodyProcessor.subscribe nextProps.message, (processedBody) =>
@setState({processedBody})
componentWillUnmount: => componentWillUnmount: =>
@_unmounted = true @_unmounted = true
@ -52,7 +55,9 @@ class MessageItemBody extends React.Component
_renderBody: => _renderBody: =>
if _.isString(@props.message.body) and _.isString(@state.processedBody) if _.isString(@props.message.body) and _.isString(@state.processedBody)
<EmailFrame showQuotedText={@state.showQuotedText} content={@state.processedBody}/> finalBody = @_mergeBodyWithFiles(@state.processedBody)
<EmailFrame showQuotedText={@state.showQuotedText} content={finalBody}/>
else if @state.error else if @state.error
<div className="message-body-error"> <div className="message-body-error">
Sorry, this message could not be loaded. (Status code {@state.error.statusCode}) Sorry, this message could not be loaded. (Status code {@state.error.statusCode})
@ -90,9 +95,7 @@ class MessageItemBody extends React.Component
return if @_unmounted return if @_unmounted
@setState({error}) @setState({error})
_onBodyProcessed: (body) => _mergeBodyWithFiles: (body) =>
downloadingSpinnerPath = Utils.imageNamed('inline-loading-spinner.gif')
# Replace cid:// references with the paths to downloaded files # Replace cid:// references with the paths to downloaded files
for file in @props.message.files for file in @props.message.files
download = @props.downloads[file.id] download = @props.downloads[file.id]
@ -101,7 +104,8 @@ class MessageItemBody extends React.Component
if download and download.state isnt 'finished' if download and download.state isnt 'finished'
# Render a spinner and inject a `style` tag that injects object-position / object-fit # Render a spinner and inject a `style` tag that injects object-position / object-fit
body = body.replace cidRegexp, (text, quoteCharacter) -> 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 else
# Render the completed download # Render the completed download
body = body.replace cidRegexp, (text, quoteCharacter) -> body = body.replace cidRegexp, (text, quoteCharacter) ->
@ -112,7 +116,6 @@ class MessageItemBody extends React.Component
# no "missing image" region shown, just a space. # no "missing image" region shown, just a space.
body = body.replace(MessageUtils.cidRegex, "src=\"#{TransparentPixel}\"") body = body.replace(MessageUtils.cidRegex, "src=\"#{TransparentPixel}\"")
@setState return body
processedBody: body
module.exports = MessageItemBody module.exports = MessageItemBody

View file

@ -5,6 +5,11 @@ DragCanvas = document.createElement("canvas")
DragCanvas.style.position = "absolute" DragCanvas.style.position = "absolute"
document.body.appendChild(DragCanvas) document.body.appendChild(DragCanvas)
PercentLoadedCache = {}
PercentLoadedCanvas = document.createElement("canvas")
PercentLoadedCanvas.style.position = "absolute"
document.body.appendChild(PercentLoadedCanvas)
SystemTrayCanvas = document.createElement("canvas") SystemTrayCanvas = document.createElement("canvas")
CanvasUtils = CanvasUtils =
@ -23,6 +28,28 @@ CanvasUtils =
ctx.stroke() if stroke ctx.stroke() if stroke
ctx.fill() if fill 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) -> canvasWithThreadDragImage: (count) ->
canvas = DragCanvas canvas = DragCanvas
@ -33,7 +60,6 @@ CanvasUtils =
canvas.style.width = "58px" canvas.style.width = "58px"
canvas.style.height = "55px" canvas.style.height = "55px"
# necessary for setDragImage to work
ctx = canvas.getContext('2d') ctx = canvas.getContext('2d')
# mail background image # mail background image

View file

@ -56,10 +56,14 @@ class EventedIFrame extends React.Component
Public: Call this method if you replace the contents of the iframe's document. Public: Call this method if you replace the contents of the iframe's document.
This allows {EventedIframe} to re-attach it's event listeners. This allows {EventedIframe} to re-attach it's event listeners.
### ###
documentWasReplaced: => didReplaceDocument: =>
@_unsubscribeFromIFrameEvents() @_unsubscribeFromIFrameEvents()
@_subscribeToIFrameEvents() @_subscribeToIFrameEvents()
setHeightQuietly: (height) =>
@_ignoreNextResize = true
ReactDOM.findDOMNode(@).height = "#{height}px"
_onSearchableStoreChange: => _onSearchableStoreChange: =>
return unless @props.searchable return unless @props.searchable
node = ReactDOM.findDOMNode(@) node = ReactDOM.findDOMNode(@)
@ -116,6 +120,9 @@ class EventedIFrame extends React.Component
window.getSelection().empty() window.getSelection().empty()
_onIFrameResize: (event) => _onIFrameResize: (event) =>
if @_ignoreNextResize
@_ignoreNextResize = false
return
@props.onResize?(event) @props.onResize?(event)
# The iFrame captures events that take place over it, which causes some # The iFrame captures events that take place over it, which causes some