diff --git a/exports/inbox-exports.coffee b/exports/inbox-exports.coffee index cc91d6371..2c371de73 100644 --- a/exports/inbox-exports.coffee +++ b/exports/inbox-exports.coffee @@ -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' diff --git a/internal_packages/message-list/lib/message-item.cjsx b/internal_packages/message-list/lib/message-item.cjsx index 25a3a5ee1..f562ac5cf 100644 --- a/internal_packages/message-list/lib/message-item.cjsx +++ b/internal_packages/message-list/lib/message-item.cjsx @@ -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 =
-
{@_messageTime()}
+
{ for Action in messageActions}
@@ -106,26 +106,14 @@ MessageItem = React.createClass attachments.map (file) => - _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 diff --git a/internal_packages/message-list/lib/message-list.cjsx b/internal_packages/message-list/lib/message-list.cjsx index 4e5b6db05..e8cbcda16 100755 --- a/internal_packages/message-list/lib/message-list.cjsx +++ b/internal_packages/message-list/lib/message-list.cjsx @@ -1,7 +1,6 @@ _ = require 'underscore-plus' React = require 'react' MessageItem = require "./message-item.cjsx" - {Actions, ThreadStore, MessageStore, ComponentRegistry} = require("inbox-exports") module.exports = diff --git a/internal_packages/message-list/lib/message-timestamp.cjsx b/internal_packages/message-list/lib/message-timestamp.cjsx new file mode 100644 index 000000000..72d41c89e --- /dev/null +++ b/internal_packages/message-list/lib/message-timestamp.cjsx @@ -0,0 +1,31 @@ +moment = require 'moment' +React = require 'react' + +module.exports = +MessageTimestamp = React.createClass + displayName: 'MessageTimestamp' + propTypes: + date: React.PropTypes.object.isRequired, + + render: -> +
{moment(@props.date).format(@_timeFormat())}
+ + _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() + + diff --git a/internal_packages/message-list/spec/message-item-spec.cjsx b/internal_packages/message-list/spec/message-item-spec.cjsx new file mode 100644 index 000000000..502030304 --- /dev/null +++ b/internal_packages/message-list/spec/message-item-spec.cjsx @@ -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: ->
}) +EmailFrameStub = React.createClass({render: ->
}) + +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( + + ) + + 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 = """ + \"A\" + \"B\" + \"C\" + + """ + @createComponent() + + it "should never include src=cid:// in the message body", -> + body = @component._formatBody() + expect(body.indexOf('cid')).toEqual(-1) + + it "should replace cid:// 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:// 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 +
+
+
+ ---- Forwarded Message ----- + blablalba +
+ """ + @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: +
+ Hi guys, take a look at this. Very relevant. -mg +
+
+
+ ---- Forwarded Message ----- + blablalba +
+
+ """ + @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) diff --git a/internal_packages/message-list/spec/message-list-spec.cjsx b/internal_packages/message-list/spec/message-list-spec.cjsx index 1d98c0e2b..2d9d11d00 100644 --- a/internal_packages/message-list/spec/message-list-spec.cjsx +++ b/internal_packages/message-list/spec/message-list-spec.cjsx @@ -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 diff --git a/internal_packages/message-list/spec/message-timestamp-spec.cjsx b/internal_packages/message-list/spec/message-timestamp-spec.cjsx new file mode 100644 index 000000000..57894f212 --- /dev/null +++ b/internal_packages/message-list/spec/message-timestamp-spec.cjsx @@ -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( + + ) + @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"