mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-12-29 11:52:34 +08:00
fix(composer): Stop parsing quoted text on each keystroke to prevent composer lag
Summary: We used to parse the quoted text on each keystroke in the composer for a reply so that we could continue to determine what was quoted text. However, that resulted in dramatically slow typing for replies to complex HTML emails. Now, the quoted text isn't a part of the reply until `prepareDraftForSyncback`, after all of the extensions have run their transformations—we use a marker to determine whether quoted text should be appended or not. The quoted text control is now a one-way operation—you can't hide the quoted text after showing it (Gmail-style). Test Plan: Tested locally (but didn't run unit tests because they won't run on my machine...) Reviewers: bengotow, juan Reviewed By: juan Differential Revision: https://phab.nylas.com/D3106
This commit is contained in:
parent
e984a0c222
commit
87dba382a8
6 changed files with 85 additions and 56 deletions
|
@ -1,6 +1,6 @@
|
|||
import _ from 'underscore';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import _ from 'underscore'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import {remote} from 'electron'
|
||||
|
||||
import {
|
||||
|
@ -9,8 +9,7 @@ import {
|
|||
DraftStore,
|
||||
DraftHelpers,
|
||||
FileDownloadStore,
|
||||
QuotedHTMLTransformer,
|
||||
} from 'nylas-exports';
|
||||
} from 'nylas-exports'
|
||||
|
||||
import {
|
||||
DropZone,
|
||||
|
@ -21,17 +20,17 @@ import {
|
|||
KeyCommandsRegion,
|
||||
OverlaidComponents,
|
||||
InjectedComponentSet,
|
||||
} from 'nylas-component-kit';
|
||||
} from 'nylas-component-kit'
|
||||
|
||||
import FileUpload from './file-upload';
|
||||
import ImageFileUpload from './image-file-upload';
|
||||
import FileUpload from './file-upload'
|
||||
import ImageFileUpload from './image-file-upload'
|
||||
|
||||
import ComposerEditor from './composer-editor';
|
||||
import ComposerHeader from './composer-header';
|
||||
import SendActionButton from './send-action-button';
|
||||
import ComposerEditor from './composer-editor'
|
||||
import ComposerHeader from './composer-header'
|
||||
import SendActionButton from './send-action-button'
|
||||
import ActionBarPlugins from './action-bar-plugins'
|
||||
|
||||
import Fields from './fields';
|
||||
import Fields from './fields'
|
||||
|
||||
// The ComposerView is a unique React component because it (currently) is a
|
||||
// singleton. Normally, the React way to do things would be to re-render the
|
||||
|
@ -55,6 +54,7 @@ export default class ComposerView extends React.Component {
|
|||
super(props)
|
||||
this.state = {
|
||||
showQuotedText: DraftHelpers.isForwardedMessage(props.draft),
|
||||
showQuotedTextControl: DraftHelpers.shouldAppendQuotedText(props.draft),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,9 +69,11 @@ export default class ComposerView extends React.Component {
|
|||
this._teardownForProps();
|
||||
this._setupForProps(nextProps);
|
||||
}
|
||||
if (DraftHelpers.isForwardedMessage(this.props.draft) !== DraftHelpers.isForwardedMessage(nextProps.draft)) {
|
||||
if (DraftHelpers.isForwardedMessage(this.props.draft) !== DraftHelpers.isForwardedMessage(nextProps.draft) ||
|
||||
DraftHelpers.shouldAppendQuotedText(this.props.draft) !== DraftHelpers.shouldAppendQuotedText(nextProps.draft)) {
|
||||
this.setState({
|
||||
showQuotedText: DraftHelpers.isForwardedMessage(nextProps.draft),
|
||||
showQuotedTextControl: DraftHelpers.shouldAppendQuotedText(nextProps.draft),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -118,6 +120,7 @@ export default class ComposerView extends React.Component {
|
|||
_setupForProps({draft, session}) {
|
||||
this.setState({
|
||||
showQuotedText: DraftHelpers.isForwardedMessage(draft),
|
||||
showQuotedTextControl: DraftHelpers.shouldAppendQuotedText(draft),
|
||||
});
|
||||
|
||||
// TODO: This is a dirty hack to save selection state into the undo/redo
|
||||
|
@ -203,7 +206,7 @@ export default class ComposerView extends React.Component {
|
|||
|
||||
_renderEditor() {
|
||||
const exposedProps = {
|
||||
body: this._removeQuotedText(this.props.draft.body),
|
||||
body: this.props.draft.body,
|
||||
draftClientId: this.props.draft.clientId,
|
||||
parentActions: {
|
||||
getComposerBoundingRect: this._getComposerBoundingRect,
|
||||
|
@ -239,20 +242,10 @@ export default class ComposerView extends React.Component {
|
|||
return ReactDOM.findDOMNode(this.refs.composerWrap).getBoundingClientRect()
|
||||
}
|
||||
|
||||
_removeQuotedText = (html) => {
|
||||
const {showQuotedText} = this.state;
|
||||
return showQuotedText ? html : QuotedHTMLTransformer.removeQuotedHTML(html);
|
||||
}
|
||||
|
||||
_showQuotedText = (html) => {
|
||||
const {showQuotedText} = this.state;
|
||||
return showQuotedText ? html : QuotedHTMLTransformer.appendQuotedHTML(html, this.props.draft.body);
|
||||
}
|
||||
|
||||
_renderQuotedTextControl() {
|
||||
if (QuotedHTMLTransformer.hasQuotedHTML(this.props.draft.body)) {
|
||||
if (this.state.showQuotedTextControl) {
|
||||
return (
|
||||
<a className="quoted-text-control" onClick={this._onToggleQuotedText}>
|
||||
<a className="quoted-text-control" onClick={this._onExpandQuotedText}>
|
||||
<span className="dots">•••</span>
|
||||
<span className="remove-quoted-text" onClick={this._onRemoveQuotedText}>
|
||||
<RetinaImg
|
||||
|
@ -267,17 +260,30 @@ export default class ComposerView extends React.Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
_onToggleQuotedText = () => {
|
||||
this.setState({showQuotedText: !this.state.showQuotedText});
|
||||
_onExpandQuotedText = () => {
|
||||
this.setState({
|
||||
showQuotedText: true,
|
||||
showQuotedTextControl: false,
|
||||
}, () => {
|
||||
DraftHelpers.appendQuotedTextToDraft(this.props.draft)
|
||||
.then((draftWithQuotedText) => {
|
||||
this.props.session.changes.add({
|
||||
body: `${draftWithQuotedText.body}<div id="n1-quoted-text-marker"></div>`,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_onRemoveQuotedText = (event) => {
|
||||
event.stopPropagation()
|
||||
const {session, draft} = this.props
|
||||
session.changes.add({
|
||||
body: QuotedHTMLTransformer.removeQuotedHTML(draft.body),
|
||||
body: `${draft.body}<div id="n1-quoted-text-marker"></div>`,
|
||||
})
|
||||
this.setState({
|
||||
showQuotedText: false,
|
||||
showQuotedTextControl: false,
|
||||
})
|
||||
this.setState({showQuotedText: false})
|
||||
}
|
||||
|
||||
_renderFooterRegions() {
|
||||
|
@ -508,7 +514,7 @@ export default class ComposerView extends React.Component {
|
|||
}
|
||||
|
||||
_onBodyChanged = (event) => {
|
||||
this.props.session.changes.add({body: this._showQuotedText(event.target.value)});
|
||||
this.props.session.changes.add({body: event.target.value});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ class DecryptMessageButton extends React.Component
|
|||
Actions.openPopover(
|
||||
<PrivateKeyPopover
|
||||
addresses={_.pluck(@props.message.to, "email")}
|
||||
callback={() => @_openPassphrasePopover(popoverTarget, @decryptPopoverDone)}/>,
|
||||
callback={=> @_openPassphrasePopover(popoverTarget, @decryptPopoverDone)}/>,
|
||||
{originRect: popoverTarget, direction: 'down'}
|
||||
)
|
||||
else
|
||||
|
@ -59,7 +59,7 @@ class DecryptMessageButton extends React.Component
|
|||
Actions.openPopover(
|
||||
<PrivateKeyPopover
|
||||
addresses={_.pluck(@props.message.to, "email")}
|
||||
callback={() => @_openPassphrasePopover(popoverTarget, @decryptAttachmentsPopoverDone)}/>,
|
||||
callback={=> @_openPassphrasePopover(popoverTarget, @decryptAttachmentsPopoverDone)}/>,
|
||||
{originRect: popoverTarget, direction: 'down'}
|
||||
)
|
||||
else
|
||||
|
@ -81,9 +81,9 @@ class DecryptMessageButton extends React.Component
|
|||
|
||||
_openPassphrasePopover: (target, callback) =>
|
||||
Actions.openPopover(
|
||||
<PassphrasePopover addresses={_.pluck(@props.message.to, "email")} onPopoverDone={callback} />,
|
||||
{originRect: target, direction: 'down'}
|
||||
)
|
||||
<PassphrasePopover addresses={_.pluck(@props.message.to, "email")} onPopoverDone={callback} />,
|
||||
{originRect: target, direction: 'down'}
|
||||
)
|
||||
|
||||
_noPrivateKeys: =>
|
||||
numKeys = 0
|
||||
|
|
|
@ -15,7 +15,7 @@ class PassphrasePopover extends React.Component
|
|||
mounted: true
|
||||
}
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: ->
|
||||
@_mounted = true
|
||||
|
||||
componentWillUnmount: ->
|
||||
|
@ -42,7 +42,7 @@ class PassphrasePopover extends React.Component
|
|||
if event.keyCode == 13
|
||||
@_validatePassphrase()
|
||||
|
||||
_validatePassphrase: () =>
|
||||
_validatePassphrase: =>
|
||||
passphrase = @state.passphrase
|
||||
for emailIndex of @props.addresses
|
||||
email = @props.addresses[emailIndex]
|
||||
|
|
|
@ -37,7 +37,7 @@ class PrivateKeyPopover extends React.Component
|
|||
{if (@state.import or @state.paste) and !@state.validKeyBody and @state.keyBody != "" then errorBar}
|
||||
{if @state.import or @state.paste then keyArea}
|
||||
<div className="picker-controls">
|
||||
<div style={{width: 80}}><button className="btn modal-cancel-button" onClick={() => Actions.closePopover()}>Cancel</button></div>
|
||||
<div style={{width: 80}}><button className="btn modal-cancel-button" onClick={=> Actions.closePopover()}>Cancel</button></div>
|
||||
<button className="btn modal-prefs-button" onClick={@_onClickAdvanced}>Advanced</button>
|
||||
<div style={{width: 80}}>{saveButton}</div>
|
||||
</div>
|
||||
|
|
|
@ -109,19 +109,9 @@ class DraftFactory
|
|||
cc: cc,
|
||||
from: [@_fromContactForReply(message)],
|
||||
threadId: thread.id,
|
||||
accountId: message.accountId
|
||||
accountId: message.accountId,
|
||||
replyToMessageId: message.id,
|
||||
body: """
|
||||
<br><br>
|
||||
<div class="gmail_quote">
|
||||
<br>
|
||||
#{DOMUtils.escapeHTMLCharacters(message.replyAttributionLine())}
|
||||
<br>
|
||||
<blockquote class="gmail_quote"
|
||||
style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">
|
||||
#{body}
|
||||
</blockquote>
|
||||
</div>"""
|
||||
body: ""
|
||||
)
|
||||
|
||||
createDraftForForward: ({thread, message}) =>
|
||||
|
@ -141,7 +131,9 @@ class DraftFactory
|
|||
threadId: thread.id,
|
||||
accountId: message.accountId,
|
||||
body: """
|
||||
<br><br><div class="gmail_quote">
|
||||
<br><br>
|
||||
<div class="gmail_quote">
|
||||
<br>
|
||||
---------- Forwarded message ---------
|
||||
<br><br>
|
||||
#{fields.join('<br>')}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import _ from 'underscore'
|
||||
import Actions from '../actions'
|
||||
import DatabaseStore from './database-store'
|
||||
import Message from '../models/message'
|
||||
import * as ExtensionRegistry from '../../extension-registry'
|
||||
import SyncbackDraftFilesTask from '../tasks/syncback-draft-files-task'
|
||||
import DOMUtils from '../../dom-utils'
|
||||
import QuotedHTMLTransformer from '../../services/quoted-html-transformer'
|
||||
|
||||
|
||||
|
@ -32,6 +34,10 @@ export function isForwardedMessage({body, subject} = {}) {
|
|||
return bodyForwarded || bodyFwd || subjectFwd
|
||||
}
|
||||
|
||||
export function shouldAppendQuotedText({body = ''} = {}) {
|
||||
return !body.includes('<div id="n1-quoted-text-marker">')
|
||||
}
|
||||
|
||||
export function messageMentionsAttachment({body} = {}) {
|
||||
if (body == null) { throw new Error('DraftHelpers::messageMentionsAttachment - Message has no body loaded') }
|
||||
let cleaned = QuotedHTMLTransformer.removeQuotedHTML(body.toLowerCase().trim());
|
||||
|
@ -48,6 +54,25 @@ export function queueDraftFileUploads(draft) {
|
|||
}
|
||||
}
|
||||
|
||||
export function appendQuotedTextToDraft(draft) {
|
||||
return DatabaseStore.find(Message, draft.replyToMessageId)
|
||||
.include(Message.attributes.body)
|
||||
.then((prevMessage) => {
|
||||
const quotedText = `
|
||||
<div class="gmail_quote">
|
||||
<br>
|
||||
${DOMUtils.escapeHTMLCharacters(prevMessage.replyAttributionLine())}
|
||||
<br>
|
||||
<blockquote class="gmail_quote"
|
||||
style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">
|
||||
${prevMessage.body}
|
||||
</blockquote>
|
||||
</div>`
|
||||
draft.body = draft.body + quotedText
|
||||
return Promise.resolve(draft)
|
||||
})
|
||||
}
|
||||
|
||||
export function applyExtensionTransformsToDraft(draft) {
|
||||
let latestTransformed = draft
|
||||
const extensions = ExtensionRegistry.Composer.extensions()
|
||||
|
@ -88,9 +113,15 @@ export function applyExtensionTransformsToDraft(draft) {
|
|||
export function prepareDraftForSyncback(session) {
|
||||
return session.ensureCorrectAccount({noSyncback: true})
|
||||
.then(() => applyExtensionTransformsToDraft(session.draft()))
|
||||
.then((transformed) => (
|
||||
DatabaseStore.inTransaction((t) => t.persistModel(transformed))
|
||||
.then(() => Promise.resolve(queueDraftFileUploads(transformed)))
|
||||
.thenReturn(transformed)
|
||||
.then((transformed) => {
|
||||
if (!shouldAppendQuotedText(transformed)) {
|
||||
return Promise.resolve(transformed)
|
||||
}
|
||||
return appendQuotedTextToDraft(transformed)
|
||||
})
|
||||
.then((draft) => (
|
||||
DatabaseStore.inTransaction((t) => t.persistModel(draft))
|
||||
.then(() => Promise.resolve(queueDraftFileUploads(draft)))
|
||||
.thenReturn(draft)
|
||||
))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue