fix(quoted-text): Show quoted text for short forwarded msgs

Summary: tiny patch, but with lots of tests for message-item and factored out message-timestamp

Test Plan: Run brand new specs!

Reviewers: evan

Reviewed By: evan

Differential Revision: https://review.inboxapp.com/D1225
This commit is contained in:
Ben Gotow 2015-02-24 16:20:57 -08:00
parent 0468cb4b39
commit 2a93c70214
7 changed files with 319 additions and 52 deletions

View file

@ -26,6 +26,7 @@ module.exports =
# Models
Tag: require '../src/flux/models/tag'
File: require '../src/flux/models/file'
Thread: require '../src/flux/models/thread'
Contact: require '../src/flux/models/contact'
Message: require '../src/flux/models/message'

View file

@ -1,8 +1,8 @@
moment = require 'moment'
React = require 'react'
_ = require 'underscore-plus'
EmailFrame = require './email-frame'
MessageParticipants = require "./message-participants.cjsx"
MessageTimestamp = require "./message-timestamp.cjsx"
{ComponentRegistry, FileDownloadStore, Utils} = require 'inbox-exports'
Autolinker = require 'autolinker'
@ -21,7 +21,7 @@ MessageItem = React.createClass
# Holds the downloadData (if any) for all of our files. It's a hash
# keyed by a fileId. The value is the downloadData.
downloads: FileDownloadStore.downloadsForFileIds(@props.message.fileIds())
showQuotedText: false
showQuotedText: @_messageIsEmptyForward()
collapsed: @props.collapsed
componentDidMount: ->
@ -44,7 +44,7 @@ MessageItem = React.createClass
header =
<header className="message-header" onClick={@_onToggleCollapsed}>
<div className="message-time">{@_messageTime()}</div>
<MessageTimestamp date={@props.message.date} />
<div className="message-actions">
{<Action thread={@props.thread} message={@props.message} /> for Action in messageActions}
</div>
@ -106,26 +106,14 @@ MessageItem = React.createClass
attachments.map (file) =>
<AttachmentComponent file={file} download={@state.downloads[file.id]}/>
_messageTime: ->
moment(@props.message.date).format(@_timeFormat())
_timeFormat: ->
today = moment(@_today())
dayOfEra = today.dayOfYear() + today.year() * 365
msgDate = moment(@props.message.date)
msgDayOfEra = msgDate.dayOfYear() + msgDate.year() * 365
diff = dayOfEra - msgDayOfEra
if diff < 1
return "h:mm a"
if diff < 4
return "MMM D, h:mm a"
else if diff > 1 and diff <= 365
return "MMM D"
else
return "MMM D YYYY"
# Stubbable for testing. Returns a `moment`
_today: -> moment()
_messageIsEmptyForward: ->
# Returns true if the message contains "Forwarded" or "Fwd" in the first 250 characters.
# A strong indicator that the quoted text should be shown. Needs to be limited to first 250
# to prevent replies to forwarded messages from also being expanded.
body = @props.message.body.toLowerCase()
indexForwarded = body.indexOf('forwarded')
indexFwd = body.indexOf('fwd')
(indexForwarded >= 0 and indexForwarded < 250) or (indexFwd >= 0 and indexFwd < 250)
_onDownloadStoreChange: ->
@setState

View file

@ -1,7 +1,6 @@
_ = require 'underscore-plus'
React = require 'react'
MessageItem = require "./message-item.cjsx"
{Actions, ThreadStore, MessageStore, ComponentRegistry} = require("inbox-exports")
module.exports =

View file

@ -0,0 +1,31 @@
moment = require 'moment'
React = require 'react'
module.exports =
MessageTimestamp = React.createClass
displayName: 'MessageTimestamp'
propTypes:
date: React.PropTypes.object.isRequired,
render: ->
<div>{moment(@props.date).format(@_timeFormat())}</div>
_timeFormat: ->
today = moment(@_today())
dayOfEra = today.dayOfYear() + today.year() * 365
msgDate = moment(@props.date)
msgDayOfEra = msgDate.dayOfYear() + msgDate.year() * 365
diff = dayOfEra - msgDayOfEra
if diff < 1
return "h:mm a"
if diff < 4
return "MMM D, h:mm a"
else if diff > 1 and diff <= 365
return "MMM D"
else
return "MMM D YYYY"
# Stubbable for testing. Returns a `moment`
_today: -> moment()

View file

@ -0,0 +1,241 @@
proxyquire = require 'proxyquire'
React = require "react/addons"
ReactTestUtils = React.addons.TestUtils
{Contact,
Message,
File,
ComponentRegistry,
FileDownloadStore,
InboxTestUtils} = require "inbox-exports"
file = new File
id: 'file_1_id'
filename: 'a.png'
contentType: 'image/png'
size: 10
file_not_downloaded = new File
id: 'file_2_id'
filename: 'b.png'
contentType: 'image/png'
size: 10
file_inline = new File
id: 'file_inline_id'
filename: 'c.png'
contentId: 'file_inline_id'
contentType: 'image/png'
size: 10
file_inline_downloading = new File
id: 'file_inline_downloading_id'
filename: 'd.png'
contentId: 'file_inline_downloading_id'
contentType: 'image/png'
size: 10
file_inline_not_downloaded = new File
id: 'file_inline_not_downloaded_id'
filename: 'e.png'
contentId: 'file_inline_not_downloaded_id'
contentType: 'image/png'
size: 10
download =
fileId: 'file_1_id'
download_inline =
fileId: 'file_inline_downloading_id'
user_1 = new Contact
name: "User One"
email: "user1@inboxapp.com"
user_2 = new Contact
name: "User Two"
email: "user2@inboxapp.com"
user_3 = new Contact
name: "User Three"
email: "user3@inboxapp.com"
user_4 = new Contact
name: "User Four"
email: "user4@inboxapp.com"
user_5 = new Contact
name: "User Five"
email: "user5@inboxapp.com"
AttachmentStub = React.createClass({render: -> <div></div>})
EmailFrameStub = React.createClass({render: -> <div></div>})
MessageItem = proxyquire '../lib/message-item.cjsx',
'./email-frame': EmailFrameStub
describe "MessageItem", ->
beforeEach ->
ComponentRegistry.register
name: 'AttachmentComponent'
view: AttachmentStub
spyOn(FileDownloadStore, 'pathForFile').andCallFake (f) ->
return '/fake/path.png' if f.id is file.id
return '/fake/path-inline.png' if f.id is file_inline.id
return '/fake/path-downloading.png' if f.id is file_inline_downloading.id
return null
spyOn(FileDownloadStore, 'downloadsForFileIds').andCallFake (ids) ->
return {'file_1_id': download, 'file_inline_downloading_id': download_inline}
@message = new Message
id: "111"
from: [user_1]
to: [user_2]
cc: [user_3, user_4]
bcc: null
body: "Body One"
date: new Date(1415814587)
draft: false
files: []
unread: false
snippet: "snippet one..."
subject: "Subject One"
threadId: "thread_12345"
namespaceId: "nsid"
@threadParticipants = [user_1, user_2, user_3, user_4]
# Generate the test component. Should be called after @message is configured
# for the test, since MessageItem assumes attributes of the message will not
# change after getInitialState runs.
@createComponent = ({collapsed} = {}) =>
collapsed ?= false
@component = ReactTestUtils.renderIntoDocument(
<MessageItem key={@message.id}
message={@message}
collapsed={collapsed}
thread_participants={@threadParticipants} />
)
describe "when collapsed", ->
beforeEach ->
@createComponent({collapsed: true})
it "should not render the EmailFrame", ->
expect( -> ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub)).toThrow()
it "should have the `collapsed` class", ->
expect(@component.getDOMNode().className.indexOf('collapsed') >= 0).toBe(true)
describe "when not collapsed", ->
beforeEach ->
@createComponent({collapsed: false})
it "should render the EmailFrame", ->
frame = ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub)
expect(frame).toBeDefined()
it "should not have the `collapsed` class", ->
expect(@component.getDOMNode().className.indexOf('collapsed') >= 0).toBe(false)
describe "when the message contains attachments", ->
beforeEach ->
@message.files = [file, file_not_downloaded]
@createComponent()
it "should include the attachments area", ->
attachments = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'attachments-area')
expect(attachments).toBeDefined()
it "should render the registered AttachmentComponent for each attachment", ->
attachments = ReactTestUtils.scryRenderedComponentsWithType(@component, AttachmentStub)
expect(attachments.length).toEqual(2)
expect(attachments[0].props.file).toBe(file)
expect(attachments[1].props.file).toBe(file_not_downloaded)
it "should provide file download state to each AttachmentComponent", ->
attachments = ReactTestUtils.scryRenderedComponentsWithType(@component, AttachmentStub)
expect(attachments[0].props.download).toBe(download)
expect(attachments[1].props.download).toBe(undefined)
describe "when the message contains inline attachments", ->
beforeEach ->
@message.files = [
file,
file_inline,
file_inline_downloading,
file_inline_not_downloaded
]
@message.body = """
<img alt=\"A\" src=\"cid:#{file_inline.contentId}\"/>
<img alt=\"B\" src=\"cid:#{file_inline_downloading.contentId}\"/>
<img alt=\"C\" src=\"cid:#{file_inline_not_downloaded.contentId}\"/>
<img src=\"cid:missing-attachment\"/>
"""
@createComponent()
it "should never include src=cid:// in the message body", ->
body = @component._formatBody()
expect(body.indexOf('cid')).toEqual(-1)
it "should replace cid://<file.contentId> with the FileDownloadStore's path for the file", ->
body = @component._formatBody()
expect(body.indexOf('alt="A" src="/fake/path-inline.png"')).toEqual(@message.body.indexOf('alt="A"'))
it "should not replace cid://<file.contentId> with the FileDownloadStore's path if the download is in progress", ->
body = @component._formatBody()
expect(body.indexOf('/fake/path-downloading.png')).toEqual(-1)
it "should not include them in the attachments area", ->
attachments = ReactTestUtils.scryRenderedComponentsWithType(@component, AttachmentStub)
expect(attachments.length).toEqual(1)
expect(attachments[0].props.file).toBe(file)
describe "showQuotedText", ->
it "should be initialized to false", ->
@createComponent()
expect(@component.state.showQuotedText).toBe(false)
it "should show the `show quoted text` toggle in the off state", ->
@createComponent()
toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-toggle')
expect(toggle.getDOMNode().className.indexOf('state-on')).toBe(-1)
it "should be initialized to true if the message contains `Forwarded`...", ->
@message.body = """
Hi guys, take a look at this. Very relevant. -mg
<br>
<br>
<div class="gmail_quote">
---- Forwarded Message -----
blablalba
</div>
"""
@createComponent()
expect(@component.state.showQuotedText).toBe(true)
it "should be initialized to false if the message is a response to a Forwarded message", ->
@message.body = """
Thanks mg, that indeed looks very relevant. Will bring it up
with the rest of the team.
On Sunday, March 4th at 12:32AM, Michael Grinich Wrote:
<div class="gmail_quote">
Hi guys, take a look at this. Very relevant. -mg
<br>
<br>
<div class="gmail_quote">
---- Forwarded Message -----
blablalba
</div>
</div>
"""
@createComponent()
expect(@component.state.showQuotedText).toBe(false)
describe "when true", ->
beforeEach ->
@createComponent()
@component.setState(showQuotedText: true)
it "should show the `show quoted text` toggle in the on state", ->
toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-toggle')
expect(toggle.getDOMNode().className.indexOf('state-on') > 0).toBe(true)
it "should pass the value into the EmailFrame", ->
frame = ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub)
expect(frame.props.showQuotedText).toBe(true)

View file

@ -259,38 +259,10 @@ describe "MessageList", ->
MessageItem)
item = items.filter (message) -> message.props.message.id is "111"
@message_item = item[0]
@message_date = moment([2010, 1, 14, 15, 25, 50, 125])
@message_item.props.message.date = moment(@message_date)
it "finds the message by id", ->
expect(@message_item.props.message.id).toBe "111"
# test messsage time is 1415814587
it "displays the time from messages LONG ago", ->
spyOn(@message_item, "_today").andCallFake =>
@message_date.add(2, 'years')
expect(@message_item._timeFormat()).toBe "MMM D YYYY"
it "displays the time and date from messages a bit ago", ->
spyOn(@message_item, "_today").andCallFake =>
@message_date.add(2, 'days')
expect(@message_item._timeFormat()).toBe "MMM D, h:mm a"
it "displays the time and date messages exactly a day ago", ->
spyOn(@message_item, "_today").andCallFake =>
@message_date.add(1, 'day')
expect(@message_item._timeFormat()).toBe "MMM D, h:mm a"
it "displays the time from messages yesterday with the day, even though it's less than 24 hours ago", ->
spyOn(@message_item, "_today").andCallFake ->
moment([2010, 1, 15, 2, 25, 50, 125])
expect(@message_item._timeFormat()).toBe "MMM D, h:mm a"
it "displays the time from messages recently", ->
spyOn(@message_item, "_today").andCallFake =>
@message_date.add(2, 'hours')
expect(@message_item._timeFormat()).toBe "h:mm a"
describe "MessageList with draft", ->
beforeEach ->
MessageStore._items = testMessages.concat draftMessages

View file

@ -0,0 +1,35 @@
moment = require 'moment'
React = require 'react/addons'
TestUtils = React.addons.TestUtils
MessageTimestamp = require '../lib/message-timestamp.cjsx'
testDate = ->
moment([2010, 1, 14, 15, 25, 50, 125])
describe "MessageTimestamp", ->
beforeEach ->
@item = TestUtils.renderIntoDocument(
<MessageTimestamp date={testDate()} />
)
@itemNode = @item.getDOMNode()
# test messsage time is 1415814587
it "displays the time from messages LONG ago", ->
spyOn(@item, "_today").andCallFake -> testDate().add(2, 'years')
expect(@item._timeFormat()).toBe "MMM D YYYY"
it "displays the time and date from messages a bit ago", ->
spyOn(@item, "_today").andCallFake -> testDate().add(2, 'days')
expect(@item._timeFormat()).toBe "MMM D, h:mm a"
it "displays the time and date messages exactly a day ago", ->
spyOn(@item, "_today").andCallFake -> testDate().add(1, 'day')
expect(@item._timeFormat()).toBe "MMM D, h:mm a"
it "displays the time from messages yesterday with the day, even though it's less than 24 hours ago", ->
spyOn(@item, "_today").andCallFake -> moment([2010, 1, 15, 2, 25, 50, 125])
expect(@item._timeFormat()).toBe "MMM D, h:mm a"
it "displays the time from messages recently", ->
spyOn(@item, "_today").andCallFake -> testDate().add(2, 'hours')
expect(@item._timeFormat()).toBe "h:mm a"