2017-07-12 15:12:19 +08:00
|
|
|
import {
|
2018-01-30 07:55:06 +08:00
|
|
|
Utils,
|
2017-09-27 02:33:08 +08:00
|
|
|
React,
|
|
|
|
PropTypes,
|
2017-07-12 15:12:19 +08:00
|
|
|
MessageUtils,
|
|
|
|
MessageBodyProcessor,
|
|
|
|
QuotedHTMLTransformer,
|
|
|
|
AttachmentStore,
|
2017-09-27 02:42:18 +08:00
|
|
|
} from 'mailspring-exports';
|
2017-09-27 02:46:00 +08:00
|
|
|
import { InjectedComponentSet, RetinaImg } from 'mailspring-component-kit';
|
2017-07-12 15:12:19 +08:00
|
|
|
|
|
|
|
import EmailFrame from './email-frame';
|
|
|
|
|
2017-09-27 02:33:08 +08:00
|
|
|
const TransparentPixel =
|
|
|
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNikAQAACIAHF/uBd8AAAAASUVORK5CYII=';
|
2017-07-12 15:12:19 +08:00
|
|
|
|
2017-11-07 03:26:27 +08:00
|
|
|
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">•••</span>
|
|
|
|
</a>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-12 15:12:19 +08:00
|
|
|
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,
|
2017-07-12 15:12:19 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
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(),
|
2017-08-29 03:28:43 +08:00
|
|
|
processedBody: MessageBodyProcessor.retrieveCached(this.props.message),
|
2017-07-12 15:12:19 +08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillMount() {
|
2017-08-29 03:28:43 +08:00
|
|
|
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 })
|
2017-07-12 15:12:19 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
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 })
|
2017-07-12 15:12:19 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
this._mounted = false;
|
2017-09-27 02:33:08 +08:00
|
|
|
if (this._unsub) {
|
|
|
|
this._unsub();
|
|
|
|
}
|
2017-07-12 15:12:19 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
_onToggleQuotedText = () => {
|
|
|
|
this.setState({
|
|
|
|
showQuotedText: !this.state.showQuotedText,
|
|
|
|
});
|
2017-09-27 02:33:08 +08:00
|
|
|
};
|
2017-07-12 15:12:19 +08:00
|
|
|
|
|
|
|
_mergeBodyWithFiles(body) {
|
|
|
|
let merged = body;
|
|
|
|
|
|
|
|
// Replace cid: references with the paths to downloaded files
|
2018-01-30 08:46:46 +08:00
|
|
|
this.props.message.files.filter(f => f.contentId).forEach(file => {
|
2017-07-12 15:12:19 +08:00
|
|
|
const download = this.props.downloads[file.id];
|
2018-01-30 07:55:06 +08:00
|
|
|
const safeContentId = Utils.escapeRegExp(file.contentId);
|
2017-07-12 15:12:19 +08:00
|
|
|
|
|
|
|
// 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(
|
2018-01-30 07:55:06 +08:00
|
|
|
`<\\s*img.*src=['"]cid:${safeContentId}['"][^>]*>`,
|
2017-09-27 02:33:08 +08:00
|
|
|
'gi'
|
|
|
|
);
|
2017-07-12 15:12:19 +08:00
|
|
|
// 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;">'
|
2017-07-12 15:12:19 +08:00
|
|
|
);
|
|
|
|
} else {
|
2018-01-30 07:55:06 +08:00
|
|
|
const cidRegexp = new RegExp(`cid:${safeContentId}(@[^'"]+)?`, 'gi');
|
|
|
|
merged = merged.replace(cidRegexp, `file://${AttachmentStore.pathForFile(file)}`);
|
2017-07-12 15:12:19 +08:00
|
|
|
}
|
2018-01-30 08:46:46 +08:00
|
|
|
});
|
2017-07-12 15:12:19 +08:00
|
|
|
|
|
|
|
// 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}"`);
|
2017-07-12 15:12:19 +08:00
|
|
|
|
|
|
|
return merged;
|
|
|
|
}
|
|
|
|
|
|
|
|
_renderBody() {
|
2017-09-27 02:33:08 +08:00
|
|
|
const { message } = this.props;
|
|
|
|
const { showQuotedText, processedBody } = this.state;
|
2017-07-12 15:12:19 +08:00
|
|
|
|
2017-09-27 02:33:08 +08:00
|
|
|
if (typeof message.body === 'string' && typeof processedBody === 'string') {
|
2017-07-12 15:12:19 +08:00
|
|
|
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 }}
|
2017-07-12 15:12:19 +08:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
return (
|
|
|
|
<span>
|
|
|
|
<InjectedComponentSet
|
2017-09-27 02:33:08 +08:00
|
|
|
matching={{ role: 'message:BodyHeader' }}
|
|
|
|
exposedProps={{ message: this.props.message }}
|
2017-07-12 15:12:19 +08:00
|
|
|
direction="column"
|
2017-09-27 02:33:08 +08:00
|
|
|
style={{ width: '100%' }}
|
2017-07-12 15:12:19 +08:00
|
|
|
/>
|
|
|
|
{this._renderBody()}
|
2017-11-07 03:26:27 +08:00
|
|
|
<ConditionalQuotedTextControl
|
2017-11-07 08:32:54 +08:00
|
|
|
body={this.props.message.body || ''}
|
2017-11-07 03:26:27 +08:00
|
|
|
onClick={this._onToggleQuotedText}
|
|
|
|
/>
|
2017-07-12 15:12:19 +08:00
|
|
|
</span>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|