);
}
_renderQuotedTextControl() {
if (!this.state.quotedTextPresent || !this.state.quotedTextHidden) {
return false;
}
return (
{
e.preventDefault();
e.stopPropagation();
this.setState({ quotedTextHidden: false });
}}
>
•••
);
}
_renderEditor() {
return (
{
if (el) {
this._els[Fields.Body] = el;
}
}}
className={this.state.quotedTextHidden && 'hiding-quoted-text'}
propsForPlugins={{ draft: this.props.draft, session: this.props.session }}
value={this.props.draft.bodyEditorState}
onFileReceived={this._onFileReceived}
onDrop={e => this._dropzone._onDrop(e)}
onChange={change => {
this.props.session.changes.add({ bodyEditorState: change.value });
}}
/>
);
}
// The contenteditable decides when to request a scroll based on the
// position of the cursor and its relative distance to this composer
// component. We provide it our boundingClientRect so it can calculate
// this value.
_getComposerBoundingRect = () => {
return ReactDOM.findDOMNode(this._els.composerWrap).getBoundingClientRect();
};
_renderFooterRegions() {
return (
);
}
// This lets us click outside of the `contenteditable`'s `contentBody`
// and simulate what happens when you click beneath the text *in* the
// contentEditable.
// Unfortunately, we need to manually keep track of the "click" in
// separate mouseDown, mouseUp events because we need to ensure that the
// start and end target are both not in the contenteditable. This ensures
// that this behavior doesn't interfear with a click and drag selection.
_onMouseDownComposerBody = event => {
if (ReactDOM.findDOMNode(this._els[Fields.Body]).contains(event.target)) {
this._mouseDownTarget = null;
} else {
this._mouseDownTarget = event.target;
}
};
_inFooterRegion(el) {
return el.closest && el.closest('.composer-footer-region');
}
_onMouseUpComposerBody = event => {
if (event.target === this._mouseDownTarget && !this._inFooterRegion(event.target)) {
// We don't set state directly here because we want the native
// contenteditable focus behavior. When the contenteditable gets focused
const bodyRect = ReactDOM.findDOMNode(this._els[Fields.Body]).getBoundingClientRect();
if (event.pageY < bodyRect.top) {
this._els[Fields.Body].focus();
} else {
if (this.state.quotedTextHidden) {
this._els[Fields.Body].focusEndReplyText();
} else {
this._els[Fields.Body].focusEndAbsolute();
}
}
}
this._mouseDownTarget = null;
};
_shouldAcceptDrop = event => {
// Ensure that you can't pick up a file and drop it on the same draft
const nonNativeFilePath = this._nonNativeFilePathForDrop(event);
const hasNativeFile = event.dataTransfer.types.includes('Files');
const hasNonNativeFilePath = nonNativeFilePath !== null;
return hasNativeFile || hasNonNativeFilePath;
};
_nonNativeFilePathForDrop = event => {
if (event.dataTransfer.types.includes('text/nylas-file-url')) {
const downloadURL = event.dataTransfer.getData('text/nylas-file-url');
const downloadFilePath = downloadURL.split('file://')[1];
if (downloadFilePath) {
return downloadFilePath;
}
}
// Accept drops of images from within the app
if (event.dataTransfer.types.includes('text/uri-list')) {
const uri = event.dataTransfer.getData('text/uri-list');
if (uri.indexOf('file://') === 0) {
return decodeURI(uri.split('file://')[1]);
}
}
return null;
};
_onDrop = event => {
// Accept drops of real files from other applications
for (const file of Array.from(event.dataTransfer.files)) {
this._onFileReceived(file.path);
}
// Accept drops from attachment components / images within the app
const uri = this._nonNativeFilePathForDrop(event);
if (uri) {
this._onFileReceived(uri);
}
};
_onFileReceived = filePath => {
// called from onDrop and onFilePaste - assume images should be inline
Actions.addAttachment({
filePath: filePath,
headerMessageId: this.props.draft.headerMessageId,
onCreated: file => {
if (Utils.shouldDisplayAsImage(file)) {
const { draft, session } = this.props;
const match = draft.files.find(f => f.id === file.id);
if (!match) {
return;
}
match.contentId = Utils.generateContentId();
session.changes.add({
files: [].concat(draft.files),
});
this._els[Fields.Body].insertInlineAttachment(file);
}
},
});
};
_isValidDraft = (options = {}) => {
// We need to check the `DraftStore` because the `DraftStore` is
// immediately and synchronously updated as soon as this function
// fires. Since `setState` is asynchronous, if we used that as our only
// check, then we might get a false reading.
if (DraftStore.isSendingDraft(this.props.draft.headerMessageId)) {
return false;
}
const dialog = remote.dialog;
const { session } = this.props;
const { errors, warnings } = session.validateDraftForSending();
if (errors.length > 0) {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
buttons: ['Edit Message', 'Cancel'],
message: 'Cannot Send',
detail: errors[0],
});
return false;
}
if (warnings.length > 0 && !options.force) {
const response = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
buttons: ['Send Anyway', 'Cancel'],
message: 'Are you sure?',
detail: `Send ${warnings.join(' and ')}?`,
});
if (response === 0) {
// response is button array index
return this._isValidDraft({ force: true });
}
return false;
}
return true;
};
_onPrimarySend = () => {
this._els.sendActionButton.primarySend();
};
_onDestroyDraft = () => {
Actions.destroyDraft(this.props.draft);
};
_onSelectAttachment = () => {
Actions.selectAttachment({ headerMessageId: this.props.draft.headerMessageId });
};
render() {
const dropCoverDisplay = this.state.isDropping ? 'block' : 'none';
return (