mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-14 00:24:33 +08:00
fix(composer): much better specs for composer & quoted text
Summary: Fixed a bug bug with the quoted text clearing the bodies on replies Test Plan: all the tests Reviewers: dillon, bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D1981
This commit is contained in:
parent
cfbbf717fa
commit
cea27cc8ce
9 changed files with 246 additions and 142 deletions
|
@ -106,9 +106,17 @@ class ContenteditableComponent extends React.Component
|
||||||
onInput={@_onInput}
|
onInput={@_onInput}
|
||||||
onKeyDown={@_onKeyDown}
|
onKeyDown={@_onKeyDown}
|
||||||
dangerouslySetInnerHTML={@_dangerouslySetInnerHTML()}></div>
|
dangerouslySetInnerHTML={@_dangerouslySetInnerHTML()}></div>
|
||||||
<a className={@_quotedTextClasses()} onClick={@_onToggleQuotedText}></a>
|
{@_renderQuotedTextControl()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
_renderQuotedTextControl: ->
|
||||||
|
if QuotedHTMLParser.hasQuotedHTML(@props.html)
|
||||||
|
text = if @props.mode?.showQuotedText then "Hide" else "Show"
|
||||||
|
<a className="quoted-text-control" onClick={@_onToggleQuotedText}>
|
||||||
|
<span className="dots">•••</span>{text} previous
|
||||||
|
</a>
|
||||||
|
else return null
|
||||||
|
|
||||||
focus: =>
|
focus: =>
|
||||||
@_editableNode().focus()
|
@_editableNode().focus()
|
||||||
|
|
||||||
|
@ -1096,7 +1104,5 @@ class ContenteditableComponent extends React.Component
|
||||||
|
|
||||||
_quotedTextClasses: => classNames
|
_quotedTextClasses: => classNames
|
||||||
"quoted-text-control": true
|
"quoted-text-control": true
|
||||||
"no-quoted-text": not QuotedHTMLParser.hasQuotedHTML(@props.html)
|
|
||||||
"show-quoted-text": @props.mode?.showQuotedText
|
|
||||||
|
|
||||||
module.exports = ContenteditableComponent
|
module.exports = ContenteditableComponent
|
||||||
|
|
|
@ -107,9 +107,11 @@ useDraft = (draftAttributes={}) ->
|
||||||
@draft = new Message _.extend({draft: true, body: ""}, draftAttributes)
|
@draft = new Message _.extend({draft: true, body: ""}, draftAttributes)
|
||||||
draft = @draft
|
draft = @draft
|
||||||
proxy = draftStoreProxyStub(DRAFT_CLIENT_ID, @draft)
|
proxy = draftStoreProxyStub(DRAFT_CLIENT_ID, @draft)
|
||||||
|
@proxy = proxy
|
||||||
|
|
||||||
|
|
||||||
spyOn(ComposerView.prototype, "componentWillMount").andCallFake ->
|
spyOn(ComposerView.prototype, "componentWillMount").andCallFake ->
|
||||||
|
# NOTE: This is called in the context of the component.
|
||||||
@_prepareForDraft(DRAFT_CLIENT_ID)
|
@_prepareForDraft(DRAFT_CLIENT_ID)
|
||||||
@_setupSession(proxy)
|
@_setupSession(proxy)
|
||||||
|
|
||||||
|
@ -118,8 +120,7 @@ useDraft = (draftAttributes={}) ->
|
||||||
# `componentWillMount`, we manually call sessionForClientId to make this
|
# `componentWillMount`, we manually call sessionForClientId to make this
|
||||||
# part of the test synchronous. We need to make the `then` block of the
|
# part of the test synchronous. We need to make the `then` block of the
|
||||||
# sessionForClientId do nothing so `_setupSession` is not called twice!
|
# sessionForClientId do nothing so `_setupSession` is not called twice!
|
||||||
spyOn(DraftStore, "sessionForClientId").andCallFake ->
|
spyOn(DraftStore, "sessionForClientId").andCallFake -> then: ->
|
||||||
then: ->
|
|
||||||
|
|
||||||
useFullDraft = ->
|
useFullDraft = ->
|
||||||
useDraft.call @,
|
useDraft.call @,
|
||||||
|
@ -141,6 +142,76 @@ describe "populated composer", ->
|
||||||
@isSending = {state: false}
|
@isSending = {state: false}
|
||||||
spyOn(DraftStore, "isSendingDraft").andCallFake => @isSending.state
|
spyOn(DraftStore, "isSendingDraft").andCallFake => @isSending.state
|
||||||
|
|
||||||
|
describe "when sending a new message", ->
|
||||||
|
it 'makes a request with the message contents', ->
|
||||||
|
useDraft.call @
|
||||||
|
makeComposer.call @
|
||||||
|
editableNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@composer, 'contentEditable'))
|
||||||
|
spyOn(@proxy.changes, "add")
|
||||||
|
editableNode.innerHTML = "Hello <strong>world</strong>"
|
||||||
|
ReactTestUtils.Simulate.input(editableNode)
|
||||||
|
expect(@proxy.changes.add).toHaveBeenCalled()
|
||||||
|
expect(@proxy.changes.add.calls.length).toBe 1
|
||||||
|
body = @proxy.changes.add.calls[0].args[0].body
|
||||||
|
expect(body).toBe "<head></head><body>Hello <strong>world</strong></body>"
|
||||||
|
|
||||||
|
describe "when sending a reply-to message", ->
|
||||||
|
beforeEach ->
|
||||||
|
@replyBody = """<blockquote class="gmail_quote">On Sep 3 2015, at 12:14 pm, Evan Morikawa <evan@evanmorikawa.com> wrote:<br>This is a test!</blockquote>"""
|
||||||
|
|
||||||
|
useDraft.call @,
|
||||||
|
from: [u1]
|
||||||
|
to: [u2]
|
||||||
|
subject: "Test Reply Message 1"
|
||||||
|
body: @replyBody
|
||||||
|
|
||||||
|
makeComposer.call @
|
||||||
|
@editableNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@composer, 'contentEditable'))
|
||||||
|
spyOn(@proxy.changes, "add")
|
||||||
|
|
||||||
|
it 'begins with the replying message collapsed', ->
|
||||||
|
expect(@editableNode.innerHTML).toBe ""
|
||||||
|
|
||||||
|
it 'saves the full new body, plus quoted text', ->
|
||||||
|
@editableNode.innerHTML = "Hello <strong>world</strong>"
|
||||||
|
ReactTestUtils.Simulate.input(@editableNode)
|
||||||
|
expect(@proxy.changes.add).toHaveBeenCalled()
|
||||||
|
expect(@proxy.changes.add.calls.length).toBe 1
|
||||||
|
body = @proxy.changes.add.calls[0].args[0].body
|
||||||
|
expect(body).toBe """<head></head><body>Hello <strong>world</strong>#{@replyBody}</body>"""
|
||||||
|
|
||||||
|
describe "when sending a forwarded message message", ->
|
||||||
|
beforeEach ->
|
||||||
|
@fwdBody = """<br><br><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">
|
||||||
|
Begin forwarded message:
|
||||||
|
<br><br>
|
||||||
|
From: Evan Morikawa <evan@evanmorikawa.com><br>Subject: Test Forward Message 1<br>Date: Sep 3 2015, at 12:14 pm<br>To: Evan Morikawa <evan@nylas.com>
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<meta content="text/html; charset=us-ascii">This is a test!
|
||||||
|
</blockquote>"""
|
||||||
|
|
||||||
|
useDraft.call @,
|
||||||
|
from: [u1]
|
||||||
|
to: [u2]
|
||||||
|
subject: "Fwd: Test Forward Message 1"
|
||||||
|
body: @fwdBody
|
||||||
|
|
||||||
|
makeComposer.call @
|
||||||
|
@editableNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@composer, 'contentEditable'))
|
||||||
|
spyOn(@proxy.changes, "add")
|
||||||
|
|
||||||
|
it 'begins with the forwarded message expanded', ->
|
||||||
|
expect(@editableNode.innerHTML).toBe @fwdBody
|
||||||
|
|
||||||
|
it 'saves the full new body, plus forwarded text', ->
|
||||||
|
@editableNode.innerHTML = "Hello <strong>world</strong>#{@fwdBody}"
|
||||||
|
ReactTestUtils.Simulate.input(@editableNode)
|
||||||
|
expect(@proxy.changes.add).toHaveBeenCalled()
|
||||||
|
expect(@proxy.changes.add.calls.length).toBe 1
|
||||||
|
body = @proxy.changes.add.calls[0].args[0].body
|
||||||
|
expect(body).toBe """Hello <strong>world</strong>#{@fwdBody}"""
|
||||||
|
|
||||||
describe "When displaying info from a draft", ->
|
describe "When displaying info from a draft", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
useFullDraft.apply(@)
|
useFullDraft.apply(@)
|
||||||
|
|
|
@ -11,106 +11,112 @@ ContenteditableComponent = require "../lib/contenteditable-component",
|
||||||
describe "ContenteditableComponent", ->
|
describe "ContenteditableComponent", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
@onChange = jasmine.createSpy('onChange')
|
@onChange = jasmine.createSpy('onChange')
|
||||||
@html = 'Test <strong>HTML</strong><br>'
|
@htmlNoQuote = 'Test <strong>HTML</strong><br>'
|
||||||
@component = ReactTestUtils.renderIntoDocument(
|
@htmlWithQuote = 'Test <strong>HTML</strong><br><blockquote class="gmail_quote">QUOTE</blockquote>'
|
||||||
<ContenteditableComponent html={@html} onChange={@onChange}/>
|
|
||||||
)
|
|
||||||
|
|
||||||
@htmlWithQuote = 'Test <strong>HTML</strong><br><br><blockquote class="gmail_quote">QUOTE</blockquote>'
|
# Must be called with the test's scope
|
||||||
@componentWithQuote = ReactTestUtils.renderIntoDocument(
|
setHTML = (newHTML) ->
|
||||||
<ContenteditableComponent html={@htmlWithQuote}
|
@$contentEditable.innerHTML = newHTML
|
||||||
onChange={@onChange}
|
ReactTestUtils.Simulate.input(@$contentEditable, {target: {value: newHTML}})
|
||||||
mode={showQuotedText: false}/>
|
|
||||||
)
|
|
||||||
|
|
||||||
describe "quoted-text-control", ->
|
describe "quoted-text-control toggle button", ->
|
||||||
it "should be rendered", ->
|
|
||||||
expect(ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-control')).toBeDefined()
|
|
||||||
|
|
||||||
it "should be visible if the html contains quoted text", ->
|
describe "when there's no quoted text", ->
|
||||||
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@componentWithQuote, 'quoted-text-control')
|
|
||||||
expect(@toggle.props.className.indexOf('no-quoted-text') >= 0).toBe(false)
|
|
||||||
|
|
||||||
it "should be have `show-quoted-text` if showQuotedText is true", ->
|
|
||||||
@componentWithQuote = ReactTestUtils.renderIntoDocument(
|
|
||||||
<ContenteditableComponent html={@htmlWithQuote} onChange={@onChange} mode={showQuotedText: true}/>
|
|
||||||
)
|
|
||||||
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@componentWithQuote, 'quoted-text-control')
|
|
||||||
expect(@toggle.props.className.indexOf('show-quoted-text') >= 0).toBe(true)
|
|
||||||
|
|
||||||
it "should not have `show-quoted-text` if showQuotedText is false", ->
|
|
||||||
@componentWithQuote.setState(showQuotedText: false)
|
|
||||||
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@componentWithQuote, 'quoted-text-control')
|
|
||||||
expect(@toggle.props.className.indexOf('show-quoted-text') >= 0).toBe(false)
|
|
||||||
|
|
||||||
it "should be hidden otherwise", ->
|
|
||||||
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-control')
|
|
||||||
expect(@toggle.props.className.indexOf('no-quoted-text') >= 0).toBe(true)
|
|
||||||
|
|
||||||
describe "when showQuotedText is false", ->
|
|
||||||
it "should not display quoted text", ->
|
|
||||||
@editDiv = ReactTestUtils.findRenderedDOMComponentWithAttr(@componentWithQuote, 'contentEditable')
|
|
||||||
expect(React.findDOMNode(@editDiv).innerHTML).toEqual @html
|
|
||||||
|
|
||||||
describe "when showQuotedText is true", ->
|
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
@componentWithQuote = ReactTestUtils.renderIntoDocument(
|
@contentEditable = ReactTestUtils.renderIntoDocument(
|
||||||
|
<ContenteditableComponent html={@htmlNoQuote}
|
||||||
|
onChange={@onChange}
|
||||||
|
mode={showQuotedText: true}/>)
|
||||||
|
@$contentEditable = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@contentEditable, 'contentEditable'))
|
||||||
|
|
||||||
|
it 'should not display any quoted text', ->
|
||||||
|
expect(@$contentEditable.innerHTML).toBe @htmlNoQuote
|
||||||
|
|
||||||
|
it "allows the text to update", ->
|
||||||
|
textToAdd = "MORE <strong>TEXT</strong>!"
|
||||||
|
expect(@$contentEditable.innerHTML).toBe @htmlNoQuote
|
||||||
|
setHTML.call(@, textToAdd + @htmlNoQuote)
|
||||||
|
ev = @onChange.mostRecentCall.args[0]
|
||||||
|
expect(ev.target.value).toEqual(textToAdd + @htmlNoQuote)
|
||||||
|
|
||||||
|
it 'should not render the quoted-text-control toggle', ->
|
||||||
|
toggles = ReactTestUtils.scryRenderedDOMComponentsWithClass(@contentEditable, 'quoted-text-control')
|
||||||
|
expect(toggles.length).toBe 0
|
||||||
|
|
||||||
|
|
||||||
|
describe 'when showQuotedText is true', ->
|
||||||
|
beforeEach ->
|
||||||
|
@contentEditable = ReactTestUtils.renderIntoDocument(
|
||||||
<ContenteditableComponent html={@htmlWithQuote}
|
<ContenteditableComponent html={@htmlWithQuote}
|
||||||
onChange={@onChange}
|
onChange={@onChange}
|
||||||
mode={showQuotedText: true}/>
|
mode={showQuotedText: true}/>)
|
||||||
)
|
@$contentEditable = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@contentEditable, 'contentEditable'))
|
||||||
|
|
||||||
it "should display all the HTML", ->
|
it 'should display the quoted text', ->
|
||||||
@componentWithQuote.setState(showQuotedText: true)
|
expect(@$contentEditable.innerHTML).toBe @htmlWithQuote
|
||||||
@editDiv = ReactTestUtils.findRenderedDOMComponentWithAttr(@componentWithQuote, 'contentEditable')
|
|
||||||
expect(React.findDOMNode(@editDiv).innerHTML.indexOf('gmail_quote') >= 0).toBe(true)
|
|
||||||
|
|
||||||
describe "showQuotedText", ->
|
it "should call `props.onChange` with the entire HTML string", ->
|
||||||
it "should default to false", ->
|
textToAdd = "MORE <strong>TEXT</strong>!"
|
||||||
expect(@component.props.mode?.showQuotedText).toBeUndefined()
|
expect(@$contentEditable.innerHTML).toBe @htmlWithQuote
|
||||||
|
setHTML.call(@, textToAdd + @htmlWithQuote)
|
||||||
|
ev = @onChange.mostRecentCall.args[0]
|
||||||
|
expect(ev.target.value).toEqual(textToAdd + @htmlWithQuote)
|
||||||
|
|
||||||
describe "when the html is changed", ->
|
it "should allow the quoted text to be changed", ->
|
||||||
beforeEach ->
|
newText = 'Test <strong>NEW 1 HTML</strong><blockquote class="gmail_quote">QUOTE CHANGED!!!</blockquote>'
|
||||||
@changedHtmlWithoutQuote = '<head></head><body>Changed <strong>NEW 1 HTML</strong><br><br></body>'
|
expect(@$contentEditable.innerHTML).toBe @htmlWithQuote
|
||||||
@changedHtmlWithQuote = 'Changed <strong>NEW 1 HTML</strong><br><br><blockquote class="gmail_quote">QUOTE</blockquote>'
|
setHTML.call(@, newText)
|
||||||
|
ev = @onChange.mostRecentCall.args[0]
|
||||||
|
expect(ev.target.value).toEqual(newText)
|
||||||
|
|
||||||
@performEdit = (newHTML, component = @componentWithQuote) =>
|
describe 'quoted text control toggle button', ->
|
||||||
editDiv = ReactTestUtils.findRenderedDOMComponentWithAttr(component, 'contentEditable')
|
|
||||||
React.findDOMNode(editDiv).innerHTML = newHTML
|
|
||||||
ReactTestUtils.Simulate.input(editDiv, {target: {value: newHTML}})
|
|
||||||
|
|
||||||
describe "when showQuotedText is true", ->
|
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
@componentWithQuote = ReactTestUtils.renderIntoDocument(
|
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@contentEditable, 'quoted-text-control')
|
||||||
<ContenteditableComponent html={@htmlWithQuote}
|
|
||||||
onChange={@onChange}
|
|
||||||
mode={showQuotedText: true}/>
|
|
||||||
)
|
|
||||||
|
|
||||||
it "should call `props.onChange` with the entire HTML string", ->
|
it 'should be rendered', ->
|
||||||
@componentWithQuote.setState(showQuotedText: true)
|
expect(@toggle).toBeDefined()
|
||||||
@performEdit(@changedHtmlWithQuote)
|
|
||||||
ev = @onChange.mostRecentCall.args[0]
|
|
||||||
expect(ev.target.value).toEqual(@changedHtmlWithQuote)
|
|
||||||
|
|
||||||
it "should allow the quoted text to be changed", ->
|
it 'prompts to hide the quote', ->
|
||||||
changed = 'Test <strong>NEW 1 HTML</strong><blockquote class="gmail_quote">QUOTE CHANGED!!!</blockquote>'
|
expect(React.findDOMNode(@toggle).textContent).toEqual "•••Hide previous"
|
||||||
@componentWithQuote.setState(showQuotedText: true)
|
|
||||||
@performEdit(changed)
|
|
||||||
ev = @onChange.mostRecentCall.args[0]
|
|
||||||
expect(ev.target.value).toEqual(changed)
|
|
||||||
|
|
||||||
describe "when showQuotedText is false", ->
|
describe 'when showQuotedText is false', ->
|
||||||
it "should let you change the text, and then append the quoted text part to the end before firing `onChange`", ->
|
beforeEach ->
|
||||||
@componentWithQuote.setState(showQuotedText: false)
|
@contentEditable = ReactTestUtils.renderIntoDocument(
|
||||||
@performEdit(@changedHtmlWithoutQuote)
|
<ContenteditableComponent html={@htmlWithQuote}
|
||||||
ev = @onChange.mostRecentCall.args[0]
|
onChange={@onChange}
|
||||||
withQuote = "<head></head><body>#{@changedHtmlWithQuote}</body>"
|
mode={showQuotedText: false}/>)
|
||||||
expect(ev.target.value).toEqual(withQuote)
|
@$contentEditable = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@contentEditable, 'contentEditable'))
|
||||||
|
|
||||||
it "should work if the component does not contain quoted text", ->
|
# The quoted text dom parser wraps stuff inertly in body tags
|
||||||
changed = '<head></head><body>Hallooo! <strong>NEW 1 HTML HTML HTML</strong><br></body>'
|
wrapBody = (html) -> "<head></head><body>#{html}</body>"
|
||||||
@component.setState(showQuotedText: true)
|
|
||||||
@performEdit(changed, @component)
|
it 'should not display any quoted text', ->
|
||||||
ev = @onChange.mostRecentCall.args[0]
|
expect(@$contentEditable.innerHTML).toBe @htmlNoQuote
|
||||||
expect(ev.target.value).toEqual(changed)
|
|
||||||
|
it "should let you change the text, and then append the quoted text part to the end before firing `onChange`", ->
|
||||||
|
textToAdd = "MORE <strong>TEXT</strong>!"
|
||||||
|
expect(@$contentEditable.innerHTML).toBe @htmlNoQuote
|
||||||
|
setHTML.call(@, textToAdd + @htmlNoQuote)
|
||||||
|
ev = @onChange.mostRecentCall.args[0]
|
||||||
|
# Note that we expect the version WITH a quote while setting the
|
||||||
|
# version withOUT a quote.
|
||||||
|
expect(ev.target.value).toEqual(wrapBody(textToAdd + @htmlWithQuote))
|
||||||
|
|
||||||
|
it "should let you add more html that looks like quoted text, and still properly appends the old quoted text", ->
|
||||||
|
textToAdd = "Yo <blockquote class=\"gmail_quote\">I'm a fake quote</blockquote>"
|
||||||
|
expect(@$contentEditable.innerHTML).toBe @htmlNoQuote
|
||||||
|
setHTML.call(@, textToAdd + @htmlNoQuote)
|
||||||
|
ev = @onChange.mostRecentCall.args[0]
|
||||||
|
# Note that we expect the version WITH a quote while setting the
|
||||||
|
# version withOUT a quote.
|
||||||
|
expect(ev.target.value).toEqual(wrapBody(textToAdd + @htmlWithQuote))
|
||||||
|
|
||||||
|
describe 'quoted text control toggle button', ->
|
||||||
|
beforeEach ->
|
||||||
|
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@contentEditable, 'quoted-text-control')
|
||||||
|
|
||||||
|
it 'should be rendered', ->
|
||||||
|
expect(@toggle).toBeDefined()
|
||||||
|
|
||||||
|
it 'prompts to hide the quote', ->
|
||||||
|
expect(React.findDOMNode(@toggle).textContent).toEqual "•••Show previous"
|
||||||
|
|
|
@ -35,7 +35,6 @@ class EmailFrame extends React.Component
|
||||||
!_.isEqual(newProps, @props)
|
!_.isEqual(newProps, @props)
|
||||||
|
|
||||||
_writeContent: =>
|
_writeContent: =>
|
||||||
wrapperClass = if @props.showQuotedText then "show-quoted-text" else ""
|
|
||||||
doc = React.findDOMNode(@).contentDocument
|
doc = React.findDOMNode(@).contentDocument
|
||||||
doc.open()
|
doc.open()
|
||||||
|
|
||||||
|
@ -50,7 +49,7 @@ class EmailFrame extends React.Component
|
||||||
EmailFixingStyles = EmailFixingStyles.replace(/.ignore-in-parent-frame/g, '')
|
EmailFixingStyles = EmailFixingStyles.replace(/.ignore-in-parent-frame/g, '')
|
||||||
if (EmailFixingStyles)
|
if (EmailFixingStyles)
|
||||||
doc.write("<style>#{EmailFixingStyles}</style>")
|
doc.write("<style>#{EmailFixingStyles}</style>")
|
||||||
doc.write("<div id='inbox-html-wrapper' class='#{wrapperClass}'>#{@_emailContent()}</div>")
|
doc.write("<div id='inbox-html-wrapper'>#{@_emailContent()}</div>")
|
||||||
doc.close()
|
doc.close()
|
||||||
|
|
||||||
# Notify the EventedIFrame that we've replaced it's document (with `open`)
|
# Notify the EventedIFrame that we've replaced it's document (with `open`)
|
||||||
|
@ -80,7 +79,7 @@ class EmailFrame extends React.Component
|
||||||
if @props.showQuotedText
|
if @props.showQuotedText
|
||||||
@props.content
|
@props.content
|
||||||
else
|
else
|
||||||
QuotedHTMLParser.hideQuotedHTML(@props.content)
|
QuotedHTMLParser.removeQuotedHTML(@props.content, keepIfWholeBodyIsQuote: true)
|
||||||
|
|
||||||
|
|
||||||
module.exports = EmailFrame
|
module.exports = EmailFrame
|
||||||
|
|
|
@ -80,13 +80,21 @@ class MessageItem extends React.Component
|
||||||
<div className="message-item-area">
|
<div className="message-item-area">
|
||||||
{@_renderHeader()}
|
{@_renderHeader()}
|
||||||
<EmailFrame showQuotedText={@state.showQuotedText} content={@_formatBody()}/>
|
<EmailFrame showQuotedText={@state.showQuotedText} content={@_formatBody()}/>
|
||||||
<a className={@_quotedTextClasses()} onClick={@_toggleQuotedText}></a>
|
{@_renderQuotedTextControl()}
|
||||||
{@_renderEvents()}
|
{@_renderEvents()}
|
||||||
{@_renderAttachments()}
|
{@_renderAttachments()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
_renderQuotedTextControl: ->
|
||||||
|
if QuotedHTMLParser.hasQuotedHTML(@props.message.body)
|
||||||
|
text = if @state.showQuotedText then "Hide" else "Show"
|
||||||
|
<a className="quoted-text-control" onClick={@_toggleQuotedText}>
|
||||||
|
<span className="dots">•••</span>{text} previous
|
||||||
|
</a>
|
||||||
|
else return null
|
||||||
|
|
||||||
_renderHeader: =>
|
_renderHeader: =>
|
||||||
classes = classNames
|
classes = classNames
|
||||||
"message-header": true
|
"message-header": true
|
||||||
|
@ -168,11 +176,6 @@ class MessageItem extends React.Component
|
||||||
else
|
else
|
||||||
<div></div>
|
<div></div>
|
||||||
|
|
||||||
_quotedTextClasses: => classNames
|
|
||||||
"quoted-text-control": true
|
|
||||||
'no-quoted-text': not QuotedHTMLParser.hasQuotedHTML(@props.message.body)
|
|
||||||
'show-quoted-text': @state.showQuotedText
|
|
||||||
|
|
||||||
_renderHeaderSideItems: ->
|
_renderHeaderSideItems: ->
|
||||||
styles =
|
styles =
|
||||||
position: "absolute"
|
position: "absolute"
|
||||||
|
|
|
@ -246,21 +246,33 @@ describe "MessageItem", ->
|
||||||
|
|
||||||
|
|
||||||
describe "showQuotedText", ->
|
describe "showQuotedText", ->
|
||||||
|
|
||||||
it "should be initialized to false", ->
|
it "should be initialized to false", ->
|
||||||
@createComponent()
|
@createComponent()
|
||||||
expect(@component.state.showQuotedText).toBe(false)
|
expect(@component.state.showQuotedText).toBe(false)
|
||||||
|
|
||||||
it "should show the `show quoted text` toggle in the off state", ->
|
it "shouldn't render the quoted text control if there's no quoted text", ->
|
||||||
|
@message.body = "no quotes here!"
|
||||||
@createComponent()
|
@createComponent()
|
||||||
toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-control')
|
toggles = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, 'quoted-text-control')
|
||||||
expect(React.findDOMNode(toggle).className.indexOf('show-quoted-text')).toBe(-1)
|
expect(toggles.length).toBe 0
|
||||||
|
|
||||||
it "should have the `no quoted text` class if there is no quoted text in the message", ->
|
describe 'quoted text control toggle button', ->
|
||||||
spyOn(QuotedHTMLParser, 'hasQuotedHTML').andReturn false
|
beforeEach ->
|
||||||
|
@message.body = """
|
||||||
|
Message
|
||||||
|
<blockquote class="gmail_quote">
|
||||||
|
Quoted message
|
||||||
|
</blockquote>
|
||||||
|
"""
|
||||||
|
@createComponent()
|
||||||
|
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-control')
|
||||||
|
|
||||||
@createComponent()
|
it 'should be rendered', ->
|
||||||
toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-control')
|
expect(@toggle).toBeDefined()
|
||||||
expect(React.findDOMNode(toggle).className.indexOf('no-quoted-text')).not.toBe(-1)
|
|
||||||
|
it 'prompts to hide the quote', ->
|
||||||
|
expect(React.findDOMNode(@toggle).textContent).toEqual "•••Show previous"
|
||||||
|
|
||||||
it "should be initialized to true if the message contains `Forwarded`...", ->
|
it "should be initialized to true if the message contains `Forwarded`...", ->
|
||||||
@message.body = """
|
@message.body = """
|
||||||
|
@ -294,14 +306,26 @@ describe "MessageItem", ->
|
||||||
@createComponent()
|
@createComponent()
|
||||||
expect(@component.state.showQuotedText).toBe(false)
|
expect(@component.state.showQuotedText).toBe(false)
|
||||||
|
|
||||||
describe "when true", ->
|
describe "when showQuotedText is true", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
|
@message.body = """
|
||||||
|
Message
|
||||||
|
<blockquote class="gmail_quote">
|
||||||
|
Quoted message
|
||||||
|
</blockquote>
|
||||||
|
"""
|
||||||
@createComponent()
|
@createComponent()
|
||||||
@component.setState(showQuotedText: true)
|
@component.setState(showQuotedText: true)
|
||||||
|
|
||||||
it "should show the `show quoted text` toggle in the on state", ->
|
describe 'quoted text control toggle button', ->
|
||||||
toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-control')
|
beforeEach ->
|
||||||
expect(React.findDOMNode(toggle).className.indexOf('show-quoted-text') > 0).toBe(true)
|
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-control')
|
||||||
|
|
||||||
|
it 'should be rendered', ->
|
||||||
|
expect(@toggle).toBeDefined()
|
||||||
|
|
||||||
|
it 'prompts to hide the quote', ->
|
||||||
|
expect(React.findDOMNode(@toggle).textContent).toEqual "•••Hide previous"
|
||||||
|
|
||||||
it "should pass the value into the EmailFrame", ->
|
it "should pass the value into the EmailFrame", ->
|
||||||
frame = ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub)
|
frame = ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub)
|
||||||
|
|
|
@ -12,8 +12,8 @@ describe "QuotedHTMLParser", ->
|
||||||
hideQuotedHTML = (fname) ->
|
hideQuotedHTML = (fname) ->
|
||||||
return QuotedHTMLParser.hideQuotedHTML(readFile(fname))
|
return QuotedHTMLParser.hideQuotedHTML(readFile(fname))
|
||||||
|
|
||||||
removeQuotedHTML = (fname) ->
|
removeQuotedHTML = (fname, opts={}) ->
|
||||||
return QuotedHTMLParser.removeQuotedHTML(readFile(fname))
|
return QuotedHTMLParser.removeQuotedHTML(readFile(fname), opts)
|
||||||
|
|
||||||
numQuotes = (html) ->
|
numQuotes = (html) ->
|
||||||
re = new RegExp(QuotedHTMLParser.annotationClass, 'g')
|
re = new RegExp(QuotedHTMLParser.annotationClass, 'g')
|
||||||
|
@ -21,7 +21,8 @@ describe "QuotedHTMLParser", ->
|
||||||
|
|
||||||
[1..16].forEach (n) ->
|
[1..16].forEach (n) ->
|
||||||
it "properly parses email_#{n}", ->
|
it "properly parses email_#{n}", ->
|
||||||
expect(removeQuotedHTML("email_#{n}.html")).toEqual readFile("email_#{n}_stripped.html")
|
opts = keepIfWholeBodyIsQuote: true
|
||||||
|
expect(removeQuotedHTML("email_#{n}.html", opts)).toEqual readFile("email_#{n}_stripped.html")
|
||||||
|
|
||||||
describe 'manual quote detection tests', ->
|
describe 'manual quote detection tests', ->
|
||||||
|
|
||||||
|
@ -258,7 +259,8 @@ describe "QuotedHTMLParser", ->
|
||||||
|
|
||||||
it 'works with these manual test cases', ->
|
it 'works with these manual test cases', ->
|
||||||
for {before, after} in tests
|
for {before, after} in tests
|
||||||
test = clean(QuotedHTMLParser.removeQuotedHTML(before))
|
opts = keepIfWholeBodyIsQuote: true
|
||||||
|
test = clean(QuotedHTMLParser.removeQuotedHTML(before, opts))
|
||||||
expect(test).toEqual clean(after)
|
expect(test).toEqual clean(after)
|
||||||
|
|
||||||
it 'removes all trailing <br> tags except one', ->
|
it 'removes all trailing <br> tags except one', ->
|
||||||
|
|
|
@ -8,20 +8,19 @@ class QuotedHTMLParser
|
||||||
|
|
||||||
# Given an html string, it will add the `annotationClass` to the DOM
|
# Given an html string, it will add the `annotationClass` to the DOM
|
||||||
# element
|
# element
|
||||||
hideQuotedHTML: (html) ->
|
hideQuotedHTML: (html, {keepIfWholeBodyIsQuote}={}) ->
|
||||||
doc = @_parseHTML(html)
|
doc = @_parseHTML(html)
|
||||||
quoteElements = @_findQuoteLikeElements(doc)
|
quoteElements = @_findQuoteLikeElements(doc)
|
||||||
if not @_wholeBodyIsQuote(doc, quoteElements)
|
if keepIfWholeBodyIsQuote and @_wholeBodyIsQuote(doc, quoteElements)
|
||||||
|
return doc.children[0].innerHTML
|
||||||
|
else
|
||||||
@_annotateElements(quoteElements)
|
@_annotateElements(quoteElements)
|
||||||
return doc.children[0].innerHTML
|
return doc.children[0].innerHTML
|
||||||
|
|
||||||
hasQuotedHTML: (html) ->
|
hasQuotedHTML: (html) ->
|
||||||
doc = @_parseHTML(html)
|
doc = @_parseHTML(html)
|
||||||
quoteElements = @_findQuoteLikeElements(doc)
|
quoteElements = @_findQuoteLikeElements(doc)
|
||||||
if @_wholeBodyIsQuote(doc, quoteElements)
|
return quoteElements.length > 0
|
||||||
return false
|
|
||||||
else
|
|
||||||
return quoteElements.length > 0
|
|
||||||
|
|
||||||
# Public: Removes quoted text from an HTML string
|
# Public: Removes quoted text from an HTML string
|
||||||
#
|
#
|
||||||
|
@ -34,12 +33,17 @@ class QuotedHTMLParser
|
||||||
# - `options`
|
# - `options`
|
||||||
# - `includeInline` Defaults false. If true, inline quotes are removed
|
# - `includeInline` Defaults false. If true, inline quotes are removed
|
||||||
# too
|
# too
|
||||||
|
# - `keepIfWholeBodyIsQuote` Defaults false. If true, then it will
|
||||||
|
# check to see if the whole html body is a giant quote. If so, it will
|
||||||
|
# preserve it.
|
||||||
#
|
#
|
||||||
# Returns HTML without quoted text
|
# Returns HTML without quoted text
|
||||||
removeQuotedHTML: (html, options={}) ->
|
removeQuotedHTML: (html, options={}) ->
|
||||||
doc = @_parseHTML(html)
|
doc = @_parseHTML(html)
|
||||||
quoteElements = @_findQuoteLikeElements(doc, options)
|
quoteElements = @_findQuoteLikeElements(doc, options)
|
||||||
if not @_wholeBodyIsQuote(doc, quoteElements)
|
if options.keepIfWholeBodyIsQuote and @_wholeBodyIsQuote(doc, quoteElements)
|
||||||
|
return doc.children[0].innerHTML
|
||||||
|
else
|
||||||
DOMUtils.removeElements(quoteElements, options)
|
DOMUtils.removeElements(quoteElements, options)
|
||||||
childNodes = doc.body.childNodes
|
childNodes = doc.body.childNodes
|
||||||
|
|
||||||
|
@ -53,14 +57,13 @@ class QuotedHTMLParser
|
||||||
break
|
break
|
||||||
|
|
||||||
DOMUtils.removeElements(extraTailBrTags)
|
DOMUtils.removeElements(extraTailBrTags)
|
||||||
return doc.children[0].innerHTML
|
return doc.children[0].innerHTML
|
||||||
|
|
||||||
appendQuotedHTML: (htmlWithoutQuotes, originalHTML) ->
|
appendQuotedHTML: (htmlWithoutQuotes, originalHTML) ->
|
||||||
doc = @_parseHTML(originalHTML)
|
doc = @_parseHTML(originalHTML)
|
||||||
quoteElements = @_findQuoteLikeElements(doc)
|
quoteElements = @_findQuoteLikeElements(doc)
|
||||||
if not @_wholeBodyIsQuote(doc, quoteElements)
|
doc = @_parseHTML(htmlWithoutQuotes)
|
||||||
doc = @_parseHTML(htmlWithoutQuotes)
|
doc.body.appendChild(node) for node in quoteElements
|
||||||
doc.body.appendChild(node) for node in quoteElements
|
|
||||||
return doc.children[0].innerHTML
|
return doc.children[0].innerHTML
|
||||||
|
|
||||||
restoreAnnotatedHTML: (html) ->
|
restoreAnnotatedHTML: (html) ->
|
||||||
|
|
|
@ -10,29 +10,19 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: @font-size-smaller;
|
font-size: @font-size-smaller;
|
||||||
|
|
||||||
&.no-quoted-text {
|
|
||||||
display:none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: @text-color-subtle;
|
color: @text-color-subtle;
|
||||||
border: 1px solid fade(@text-color-subtle, 35%);
|
border: 1px solid fade(@text-color-subtle, 35%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
.dots {
|
||||||
content:'\2022\2022\2022';
|
display: inline-block;
|
||||||
font-size: @font-size-smaller * 0.8;
|
font-size: @font-size-smaller * 0.8;
|
||||||
top:-1px;
|
top:-1px;
|
||||||
position:relative;
|
position:relative;
|
||||||
padding-right:8px;
|
padding-right:8px;
|
||||||
}
|
}
|
||||||
&.show-quoted-text:after {
|
|
||||||
content:'Hide previous';
|
|
||||||
}
|
|
||||||
&:after {
|
|
||||||
content:'Show previous';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-label {
|
.mail-label {
|
||||||
|
|
Loading…
Add table
Reference in a new issue