Mailspring/app/internal_packages/message-list/lib/message-item-body.jsx

173 lines
4.7 KiB
React
Raw Normal View History

import {
Utils,
2017-09-27 02:33:08 +08:00
React,
PropTypes,
MessageUtils,
MessageBodyProcessor,
QuotedHTMLTransformer,
AttachmentStore,
} from 'mailspring-exports';
2017-09-27 02:46:00 +08:00
import { InjectedComponentSet, RetinaImg } from 'mailspring-component-kit';
import EmailFrame from './email-frame';
2017-09-27 02:33:08 +08:00
const TransparentPixel =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNikAQAACIAHF/uBd8AAAAASUVORK5CYII=';
class ConditionalQuotedTextControl extends React.Component {
static displayName = 'ConditionalQuotedTextControl';
static propTypes = {
body: PropTypes.string.isRequired,
onClick: PropTypes.func,
};
shouldComponentUpdate(nextProps) {
return this.props.body !== nextProps.body;
}
render() {
if (!QuotedHTMLTransformer.hasQuotedHTML(this.props.body)) {
return null;
}
return (
<a className="quoted-text-control" onClick={this.props.onClick}>
<span className="dots">&bull;&bull;&bull;</span>
</a>
);
}
}
export default class MessageItemBody extends React.Component {
static displayName = 'MessageItemBody';
static propTypes = {
2017-09-27 02:33:08 +08:00
message: PropTypes.object.isRequired,
downloads: PropTypes.object.isRequired,
};
constructor(props, context) {
super(props, context);
this._mounted = false;
this.state = {
Totally overhauled composer based on Slate (#524) * Remove the composer contenteditable, replace with basic <textarea> * Beginning broader cleanup of draft session * DraftJS composer with color, style support * Serialization/unserialization of basic styles, toolbar working * WIP * Switch to draft-js-plugins approach, need to revisit HTML * Move HTML conversion functionality into plugins * Add spellcheck context menu to editor * Initial work on quoted text * Further work on quoted text * BLOCK approach * Entity approach - better, does not bump out to top level * Hiding and showing quoted text via CSS * Get rid of ability to inject another subject line component * Clean up specs, DraftFactory to ES6 * Remove old initial focus hack * Fix focusing, initial text selection * Remove participant “collapsing” support, it can be confusing * Correctly terminate links on carriage returns * Initial signature support, allow removal of uneditable blocks * Sync body string with body editorstate * Simplify draft editor session, finish signatures * Templates * Minor fixes * Simplify link/open tracking, ensure it works * Reorg composer, rework template editor * Omg the slowness is all the stupid emoji button * Polish and small fixes * Performance improvements, new templates UI * Don’t assume nodes are elements * Fix for sending drafts twice due to back-to-back saves * Fix order of operations on app quit to save drafts reliably * Improve DraftJS-Convert whitespace handling * Use contentID throughout attachment lifecycle * Try to fix images * Switch to Slate instead of DraftJS… much better * Fix newline handling * Bug fixes * Cleanup * Finish templates plugin * Clean up text editing / support for Gmail email styles * Support for color + size on the same node, clean trailing whitespace * Restore emoji typeahead / emoji picker * Fix scrolling in template editor * Fix specs * Fix newlines * Re-implement spellcheck to be faster * Make spellcheck decorator changes invisible to the undo/redo stack * Remove comment * Polish themplates panel * Fix #521
2018-01-12 07:55:56 +08:00
showQuotedText: this.props.message.isForwarded(),
processedBody: MessageBodyProcessor.retrieveCached(this.props.message),
};
}
componentWillMount() {
const needInitialCallback = this.state.processedBody === null;
2017-09-27 02:33:08 +08:00
this._unsub = MessageBodyProcessor.subscribe(
this.props.message,
needInitialCallback,
processedBody => this.setState({ processedBody })
);
}
componentDidMount() {
this._mounted = true;
}
componentWillReceiveProps(nextProps) {
if (nextProps.message.id !== this.props.message.id) {
2017-09-27 02:33:08 +08:00
if (this._unsub) {
this._unsub();
}
this._unsub = MessageBodyProcessor.subscribe(nextProps.message, true, processedBody =>
this.setState({ processedBody })
);
}
}
componentWillUnmount() {
this._mounted = false;
2017-09-27 02:33:08 +08:00
if (this._unsub) {
this._unsub();
}
}
_onToggleQuotedText = () => {
this.setState({
showQuotedText: !this.state.showQuotedText,
});
2017-09-27 02:33:08 +08:00
};
_mergeBodyWithFiles(body) {
let merged = body;
// Replace cid: references with the paths to downloaded files
this.props.message.files.filter(f => f.contentId).forEach(file => {
const download = this.props.downloads[file.id];
const safeContentId = Utils.escapeRegExp(file.contentId);
// Note: I don't like doing this with RegExp before the body is inserted into
// the DOM, but we want to avoid "could not load cid://" in the console.
if (download && download.state !== 'finished') {
2017-09-27 02:33:08 +08:00
const inlineImgRegexp = new RegExp(
`<\\s*img.*src=['"]cid:${safeContentId}['"][^>]*>`,
2017-09-27 02:33:08 +08:00
'gi'
);
// Render a spinner
2017-09-27 02:33:08 +08:00
merged = merged.replace(
inlineImgRegexp,
() =>
'<img alt="spinner.gif" src="mailspring://message-list/assets/spinner.gif" style="-webkit-user-drag: none;">'
);
} else {
const cidRegexp = new RegExp(`cid:${safeContentId}(@[^'"]+)?`, 'gi');
merged = merged.replace(cidRegexp, `file://${AttachmentStore.pathForFile(file)}`);
}
});
// Replace remaining cid: references - we will not display them since they'll
// throw "unknown ERR_UNKNOWN_URL_SCHEME". Show a transparent pixel so that there's
// no "missing image" region shown, just a space.
2017-09-27 02:33:08 +08:00
merged = merged.replace(MessageUtils.cidRegex, `src="${TransparentPixel}"`);
return merged;
}
_renderBody() {
2017-09-27 02:33:08 +08:00
const { message } = this.props;
const { showQuotedText, processedBody } = this.state;
2017-09-27 02:33:08 +08:00
if (typeof message.body === 'string' && typeof processedBody === 'string') {
return (
<EmailFrame
showQuotedText={showQuotedText}
content={this._mergeBodyWithFiles(processedBody)}
message={message}
/>
);
}
return (
<div className="message-body-loading">
<RetinaImg
name="inline-loading-spinner.gif"
mode={RetinaImg.Mode.ContentDark}
2017-09-27 02:33:08 +08:00
style={{ width: 14, height: 14 }}
/>
</div>
);
}
render() {
return (
<span>
<InjectedComponentSet
2017-09-27 02:33:08 +08:00
matching={{ role: 'message:BodyHeader' }}
exposedProps={{ message: this.props.message }}
direction="column"
2017-09-27 02:33:08 +08:00
style={{ width: '100%' }}
/>
{this._renderBody()}
<ConditionalQuotedTextControl
body={this.props.message.body || ''}
onClick={this._onToggleQuotedText}
/>
</span>
);
}
}