diff --git a/app/internal_packages/message-list/lib/email-frame.jsx b/app/internal_packages/message-list/lib/email-frame.jsx index dad978319..4cf179037 100644 --- a/app/internal_packages/message-list/lib/email-frame.jsx +++ b/app/internal_packages/message-list/lib/email-frame.jsx @@ -24,6 +24,12 @@ export default class EmailFrame extends React.Component { this._mounted = true; this._writeContent(); this._unlisten = EmailFrameStylesStore.listen(this._writeContent); + + // Update the iframe's size whenever it's content size changes. Doing this + // with ResizeObserver is /so/ elegant compared to polling for it's height. + const iframeEl = ReactDOM.findDOMNode(this._iframeComponent); + this._iframeDocObserver = new ResizeObserver(this._onReevaluateContentSize); + this._iframeDocObserver.observe(iframeEl.contentDocument.firstElementChild); } shouldComponentUpdate(nextProps) { @@ -31,9 +37,9 @@ export default class EmailFrame extends React.Component { const nextMessage = nextProps.message || {}; return ( + message.id !== nextMessage.id || content !== nextProps.content || showQuotedText !== nextProps.showQuotedText || - message.id !== nextMessage.id || !Utils.isEqualReact(message.pluginMetadata, nextMessage.pluginMetadata) ); } @@ -44,9 +50,8 @@ export default class EmailFrame extends React.Component { componentWillUnmount() { this._mounted = false; - if (this._unlisten) { - this._unlisten(); - } + if (this._iframeDocObserver) this._iframeDocObserver.disconnect(); + if (this._unlisten) this._unlisten(); } _emailContent = () => { @@ -60,71 +65,60 @@ export default class EmailFrame extends React.Component { }; _writeContent = () => { - const iframeNode = ReactDOM.findDOMNode(this._iframeComponent); - const doc = iframeNode.contentDocument; - if (!doc) { - return; - } - doc.open(); + const iframeEl = ReactDOM.findDOMNode(this._iframeComponent); + const doc = iframeEl.contentDocument; + if (!doc) return; // NOTE: The iframe must have a modern DOCTYPE. The lack of this line // will cause some bizzare non-standards compliant rendering with the // message bodies. This is particularly felt with elements use // the `border-collapse: collapse` css property while setting a // `padding`. - doc.write(''); const styles = EmailFrameStylesStore.styles(); - if (styles) { - doc.write(``); - } + doc.open(); doc.write( - `
${this._emailContent()}
` + `` + + (styles ? `` : '') + + `
${this._emailContent()}
` ); doc.close(); - iframeNode.addEventListener('load', this._onLoad); - - autolink(doc, { async: true }); - adjustImages(doc); - - for (const extension of MessageStore.extensions()) { - if (!extension.renderedMessageBodyIntoDocument) { - continue; - } - try { - extension.renderedMessageBodyIntoDocument({ - document: doc, - message: this.props.message, - iframe: iframeNode, - }); - } catch (e) { - AppEnv.reportError(e); - } - } - // Notify the EventedIFrame that we've replaced it's document (with `open`) // so it can attach event listeners again. this._iframeComponent.didReplaceDocument(); - this._onMustRecalculateFrameHeight(); + + window.requestAnimationFrame(() => { + autolink(doc, { async: true }); + adjustImages(doc); + + for (const extension of MessageStore.extensions()) { + if (!extension.renderedMessageBodyIntoDocument) { + continue; + } + try { + extension.renderedMessageBodyIntoDocument({ + document: doc, + message: this.props.message, + iframe: iframeEl, + }); + } catch (e) { + AppEnv.reportError(e); + } + } + }); }; - _onLoad = () => { - const iframeNode = ReactDOM.findDOMNode(this._iframeComponent); - iframeNode.removeEventListener('load', this._onLoad); - this._setFrameHeight(); - }; + _onReevaluateContentSize = () => { + const iframeEl = ReactDOM.findDOMNode(this._iframeComponent); + const doc = iframeEl && iframeEl.contentDocument; - _onMustRecalculateFrameHeight = () => { + // We must set the height to zero in order to get a valid scrollHeight + // if the document is wider and has a lower height now. this._iframeComponent.setHeightQuietly(0); - this._lastComputedHeight = 0; - this._setFrameHeight(); - }; - - _getFrameHeight = doc => { - let height = 0; // If documentElement has a scroll height, prioritize that as height // If not, fall back to body scroll height by setting it to auto + let height = 0; if (doc && doc.documentElement && doc.documentElement.scrollHeight > 0) { height = doc.documentElement.scrollHeight; } else if (doc && doc.body) { @@ -134,59 +128,28 @@ export default class EmailFrame extends React.Component { } height = doc.body.scrollHeight; } - return height; + + this._iframeComponent.setHeightQuietly(height); }; - _setFrameHeight = () => { - if (!this._mounted) { - return; - } - - // 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 iframeNode = ReactDOM.findDOMNode(this._iframeComponent); - let 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) { - this._iframeComponent.setHeightQuietly(height); - this._iframeHeightHolderEl.style.height = `${height}px`; - this._lastComputedHeight = height; - } - - if (iframeNode.contentDocument.readyState !== 'complete') { - window.requestAnimationFrame(() => { - this._setFrameHeight(); - }); - } + _onResize = () => { + const iframeEl = ReactDOM.findDOMNode(this._iframeComponent); + if (!iframeEl) return; + this._iframeDocObserver.disconnect(); + this._iframeDocObserver.observe(iframeEl.contentDocument.firstElementChild); }; render() { return ( -
{ - this._iframeHeightHolderEl = el; + { + this._iframeComponent = cm; }} - style={{ height: this._lastComputedHeight }} - > - { - this._iframeComponent = cm; - }} - seamless="seamless" - searchable - onResize={this._onMustRecalculateFrameHeight} - /> -
+ /> ); } } diff --git a/app/internal_packages/message-list/styles/message-list.less b/app/internal_packages/message-list/styles/message-list.less index f997e414e..e896865ef 100644 --- a/app/internal_packages/message-list/styles/message-list.less +++ b/app/internal_packages/message-list/styles/message-list.less @@ -483,16 +483,12 @@ body.platform-win32 { margin: 0 auto; padding: 0 20px @spacing-standard 20px; - .iframe-container { + iframe { margin-top: 10px; width: 100%; - - iframe { - width: 100%; - border: 0; - padding: 0; - overflow: auto; - } + border: 0; + padding: 0; + overflow: auto; } } diff --git a/app/src/components/evented-iframe.jsx b/app/src/components/evented-iframe.jsx index e5c6a141d..5b4a785a8 100644 --- a/app/src/components/evented-iframe.jsx +++ b/app/src/components/evented-iframe.jsx @@ -87,8 +87,11 @@ class EventedIFrame extends React.Component { } setHeightQuietly(height) { - this._ignoreNextResize = true; - ReactDOM.findDOMNode(this).height = `${height}px`; + const el = ReactDOM.findDOMNode(this); + if (el.style.height !== `${height}px`) { + this._ignoreNextResize = true; + el.style.height = `${height}px`; + } } _onSearchableStoreChange = () => {