mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-31 12:30:14 +08:00
Use ResizeObserver to monitor email content size for better performance
This commit is contained in:
parent
9f2b5217bc
commit
209ba58e3e
3 changed files with 66 additions and 104 deletions
|
@ -24,6 +24,12 @@ export default class EmailFrame extends React.Component {
|
||||||
this._mounted = true;
|
this._mounted = true;
|
||||||
this._writeContent();
|
this._writeContent();
|
||||||
this._unlisten = EmailFrameStylesStore.listen(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) {
|
shouldComponentUpdate(nextProps) {
|
||||||
|
@ -31,9 +37,9 @@ export default class EmailFrame extends React.Component {
|
||||||
const nextMessage = nextProps.message || {};
|
const nextMessage = nextProps.message || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
message.id !== nextMessage.id ||
|
||||||
content !== nextProps.content ||
|
content !== nextProps.content ||
|
||||||
showQuotedText !== nextProps.showQuotedText ||
|
showQuotedText !== nextProps.showQuotedText ||
|
||||||
message.id !== nextMessage.id ||
|
|
||||||
!Utils.isEqualReact(message.pluginMetadata, nextMessage.pluginMetadata)
|
!Utils.isEqualReact(message.pluginMetadata, nextMessage.pluginMetadata)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -44,9 +50,8 @@ export default class EmailFrame extends React.Component {
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._mounted = false;
|
this._mounted = false;
|
||||||
if (this._unlisten) {
|
if (this._iframeDocObserver) this._iframeDocObserver.disconnect();
|
||||||
this._unlisten();
|
if (this._unlisten) this._unlisten();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_emailContent = () => {
|
_emailContent = () => {
|
||||||
|
@ -60,71 +65,60 @@ export default class EmailFrame extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
_writeContent = () => {
|
_writeContent = () => {
|
||||||
const iframeNode = ReactDOM.findDOMNode(this._iframeComponent);
|
const iframeEl = ReactDOM.findDOMNode(this._iframeComponent);
|
||||||
const doc = iframeNode.contentDocument;
|
const doc = iframeEl.contentDocument;
|
||||||
if (!doc) {
|
if (!doc) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
doc.open();
|
|
||||||
|
|
||||||
// NOTE: The iframe must have a modern DOCTYPE. The lack of this line
|
// NOTE: The iframe must have a modern DOCTYPE. The lack of this line
|
||||||
// will cause some bizzare non-standards compliant rendering with the
|
// will cause some bizzare non-standards compliant rendering with the
|
||||||
// message bodies. This is particularly felt with <table> elements use
|
// message bodies. This is particularly felt with <table> elements use
|
||||||
// the `border-collapse: collapse` css property while setting a
|
// the `border-collapse: collapse` css property while setting a
|
||||||
// `padding`.
|
// `padding`.
|
||||||
doc.write('<!DOCTYPE html>');
|
|
||||||
const styles = EmailFrameStylesStore.styles();
|
const styles = EmailFrameStylesStore.styles();
|
||||||
if (styles) {
|
doc.open();
|
||||||
doc.write(`<style>${styles}</style>`);
|
|
||||||
}
|
|
||||||
doc.write(
|
doc.write(
|
||||||
`<div id='inbox-html-wrapper' class="${process.platform}">${this._emailContent()}</div>`
|
`<!DOCTYPE html>` +
|
||||||
|
(styles ? `<style>${styles}</style>` : '') +
|
||||||
|
`<div id='inbox-html-wrapper' class="${process.platform}">${this._emailContent()}</div>`
|
||||||
);
|
);
|
||||||
doc.close();
|
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`)
|
// 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._iframeComponent.didReplaceDocument();
|
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 = () => {
|
_onReevaluateContentSize = () => {
|
||||||
const iframeNode = ReactDOM.findDOMNode(this._iframeComponent);
|
const iframeEl = ReactDOM.findDOMNode(this._iframeComponent);
|
||||||
iframeNode.removeEventListener('load', this._onLoad);
|
const doc = iframeEl && iframeEl.contentDocument;
|
||||||
this._setFrameHeight();
|
|
||||||
};
|
|
||||||
|
|
||||||
_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._iframeComponent.setHeightQuietly(0);
|
||||||
this._lastComputedHeight = 0;
|
|
||||||
this._setFrameHeight();
|
|
||||||
};
|
|
||||||
|
|
||||||
_getFrameHeight = doc => {
|
|
||||||
let height = 0;
|
|
||||||
|
|
||||||
// If documentElement has a scroll height, prioritize that as height
|
// If documentElement has a scroll height, prioritize that as height
|
||||||
// If not, fall back to body scroll height by setting it to auto
|
// If not, fall back to body scroll height by setting it to auto
|
||||||
|
let height = 0;
|
||||||
if (doc && doc.documentElement && doc.documentElement.scrollHeight > 0) {
|
if (doc && doc.documentElement && doc.documentElement.scrollHeight > 0) {
|
||||||
height = doc.documentElement.scrollHeight;
|
height = doc.documentElement.scrollHeight;
|
||||||
} else if (doc && doc.body) {
|
} else if (doc && doc.body) {
|
||||||
|
@ -134,59 +128,28 @@ export default class EmailFrame extends React.Component {
|
||||||
}
|
}
|
||||||
height = doc.body.scrollHeight;
|
height = doc.body.scrollHeight;
|
||||||
}
|
}
|
||||||
return height;
|
|
||||||
|
this._iframeComponent.setHeightQuietly(height);
|
||||||
};
|
};
|
||||||
|
|
||||||
_setFrameHeight = () => {
|
_onResize = () => {
|
||||||
if (!this._mounted) {
|
const iframeEl = ReactDOM.findDOMNode(this._iframeComponent);
|
||||||
return;
|
if (!iframeEl) return;
|
||||||
}
|
this._iframeDocObserver.disconnect();
|
||||||
|
this._iframeDocObserver.observe(iframeEl.contentDocument.firstElementChild);
|
||||||
// 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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div
|
<EventedIFrame
|
||||||
className="iframe-container"
|
searchable
|
||||||
ref={el => {
|
onResize={this._onResize}
|
||||||
this._iframeHeightHolderEl = el;
|
seamless="seamless"
|
||||||
|
style={{ height: 0 }}
|
||||||
|
ref={cm => {
|
||||||
|
this._iframeComponent = cm;
|
||||||
}}
|
}}
|
||||||
style={{ height: this._lastComputedHeight }}
|
/>
|
||||||
>
|
|
||||||
<EventedIFrame
|
|
||||||
ref={cm => {
|
|
||||||
this._iframeComponent = cm;
|
|
||||||
}}
|
|
||||||
seamless="seamless"
|
|
||||||
searchable
|
|
||||||
onResize={this._onMustRecalculateFrameHeight}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -483,16 +483,12 @@ body.platform-win32 {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 20px @spacing-standard 20px;
|
padding: 0 20px @spacing-standard 20px;
|
||||||
|
|
||||||
.iframe-container {
|
iframe {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
iframe {
|
padding: 0;
|
||||||
width: 100%;
|
overflow: auto;
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,8 +87,11 @@ class EventedIFrame extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
setHeightQuietly(height) {
|
setHeightQuietly(height) {
|
||||||
this._ignoreNextResize = true;
|
const el = ReactDOM.findDOMNode(this);
|
||||||
ReactDOM.findDOMNode(this).height = `${height}px`;
|
if (el.style.height !== `${height}px`) {
|
||||||
|
this._ignoreNextResize = true;
|
||||||
|
el.style.height = `${height}px`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSearchableStoreChange = () => {
|
_onSearchableStoreChange = () => {
|
||||||
|
|
Loading…
Reference in a new issue