mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-11-15 14:20:38 +08:00
1e02d8c9c0
Summary: - `from` participants now have their own line - `to` participants in collapsed mode merge `to`, `cc`, `bcc` as in gmail, and start in separate line from `from` - number of `to` participants in collapsed mode is limited, and also overflows with an ellipsis with css in case its too long - /some/ cleanup - Unsuccessfully tried to update the css for the message item header to convert to a flexbox. Wrapping the `from` address when the text is too long is still a TODO - Fixes #1113 Test Plan: - Manual Reviewers: bengotow, evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D2507
542 lines
18 KiB
CoffeeScript
542 lines
18 KiB
CoffeeScript
_ = require "underscore"
|
|
moment = require "moment"
|
|
proxyquire = require("proxyquire").noPreserveCache()
|
|
|
|
CSON = require "season"
|
|
React = require "react/addons"
|
|
TestUtils = React.addons.TestUtils
|
|
|
|
{Thread,
|
|
Contact,
|
|
Actions,
|
|
Message,
|
|
Account,
|
|
DraftStore,
|
|
MessageStore,
|
|
AccountStore,
|
|
NylasTestUtils,
|
|
ComponentRegistry} = require "nylas-exports"
|
|
|
|
{InjectedComponent} = require 'nylas-component-kit'
|
|
|
|
MessageParticipants = require "../lib/message-participants"
|
|
|
|
MessageItem = proxyquire("../lib/message-item", {
|
|
"./email-frame": React.createClass({render: -> <div></div>})
|
|
})
|
|
|
|
MessageItemContainer = proxyquire("../lib/message-item-container", {
|
|
"./message-item": MessageItem
|
|
"./pending-message-item": MessageItem
|
|
})
|
|
|
|
MessageList = proxyquire '../lib/message-list',
|
|
"./message-item-container": MessageItemContainer
|
|
|
|
# User_1 needs to be "me" so that when we calculate who we should reply
|
|
# to, it properly matches the AccountStore
|
|
user_1 = new Contact
|
|
name: TEST_ACCOUNT_NAME
|
|
email: TEST_ACCOUNT_EMAIL
|
|
user_2 = new Contact
|
|
name: "User Two"
|
|
email: "user2@nylas.com"
|
|
user_3 = new Contact
|
|
name: "User Three"
|
|
email: "user3@nylas.com"
|
|
user_4 = new Contact
|
|
name: "User Four"
|
|
email: "user4@nylas.com"
|
|
user_5 = new Contact
|
|
name: "User Five"
|
|
email: "user5@nylas.com"
|
|
|
|
m1 = (new Message).fromJSON({
|
|
"id" : "111",
|
|
"from" : [ user_1 ],
|
|
"to" : [ user_2 ],
|
|
"cc" : [ user_3, user_4 ],
|
|
"bcc" : null,
|
|
"body" : "Body One",
|
|
"date" : 1415814587,
|
|
"draft" : false
|
|
"files" : [],
|
|
"unread" : false,
|
|
"object" : "message",
|
|
"snippet" : "snippet one...",
|
|
"subject" : "Subject One",
|
|
"thread_id" : "thread_12345",
|
|
"account_id" : TEST_ACCOUNT_ID
|
|
})
|
|
m2 = (new Message).fromJSON({
|
|
"id" : "222",
|
|
"from" : [ user_2 ],
|
|
"to" : [ user_1 ],
|
|
"cc" : [ user_3, user_4 ],
|
|
"bcc" : null,
|
|
"body" : "Body Two",
|
|
"date" : 1415814587,
|
|
"draft" : false
|
|
"files" : [],
|
|
"unread" : false,
|
|
"object" : "message",
|
|
"snippet" : "snippet Two...",
|
|
"subject" : "Subject Two",
|
|
"thread_id" : "thread_12345",
|
|
"account_id" : TEST_ACCOUNT_ID
|
|
})
|
|
m3 = (new Message).fromJSON({
|
|
"id" : "333",
|
|
"from" : [ user_3 ],
|
|
"to" : [ user_1 ],
|
|
"cc" : [ user_2, user_4 ],
|
|
"bcc" : [],
|
|
"body" : "Body Three",
|
|
"date" : 1415814587,
|
|
"draft" : false
|
|
"files" : [],
|
|
"unread" : false,
|
|
"object" : "message",
|
|
"snippet" : "snippet Three...",
|
|
"subject" : "Subject Three",
|
|
"thread_id" : "thread_12345",
|
|
"account_id" : TEST_ACCOUNT_ID
|
|
})
|
|
m4 = (new Message).fromJSON({
|
|
"id" : "444",
|
|
"from" : [ user_4 ],
|
|
"to" : [ user_1 ],
|
|
"cc" : [],
|
|
"bcc" : [ user_5 ],
|
|
"body" : "Body Four",
|
|
"date" : 1415814587,
|
|
"draft" : false
|
|
"files" : [],
|
|
"unread" : false,
|
|
"object" : "message",
|
|
"snippet" : "snippet Four...",
|
|
"subject" : "Subject Four",
|
|
"thread_id" : "thread_12345",
|
|
"account_id" : TEST_ACCOUNT_ID
|
|
})
|
|
m5 = (new Message).fromJSON({
|
|
"id" : "555",
|
|
"from" : [ user_1 ],
|
|
"to" : [ user_4 ],
|
|
"cc" : [],
|
|
"bcc" : [],
|
|
"body" : "Body Five",
|
|
"date" : 1415814587,
|
|
"draft" : false
|
|
"files" : [],
|
|
"unread" : false,
|
|
"object" : "message",
|
|
"snippet" : "snippet Five...",
|
|
"subject" : "Subject Five",
|
|
"thread_id" : "thread_12345",
|
|
"account_id" : TEST_ACCOUNT_ID
|
|
})
|
|
testMessages = [m1, m2, m3, m4, m5]
|
|
draftMessages = [
|
|
(new Message).fromJSON({
|
|
"id" : "666",
|
|
"from" : [ user_1 ],
|
|
"to" : [ ],
|
|
"cc" : [ ],
|
|
"bcc" : null,
|
|
"body" : "Body One",
|
|
"date" : 1415814587,
|
|
"draft" : true
|
|
"files" : [],
|
|
"unread" : false,
|
|
"object" : "draft",
|
|
"snippet" : "draft snippet one...",
|
|
"subject" : "Draft One",
|
|
"thread_id" : "thread_12345",
|
|
"account_id" : TEST_ACCOUNT_ID
|
|
}),
|
|
]
|
|
|
|
test_thread = (new Thread).fromJSON({
|
|
"id": "12345"
|
|
"id" : "thread_12345"
|
|
"subject" : "Subject 12345",
|
|
"account_id" : TEST_ACCOUNT_ID
|
|
})
|
|
|
|
describe "MessageList", ->
|
|
beforeEach ->
|
|
MessageStore._items = []
|
|
MessageStore._threadId = null
|
|
spyOn(MessageStore, "itemsLoading").andCallFake ->
|
|
false
|
|
|
|
@messageList = TestUtils.renderIntoDocument(<MessageList />)
|
|
@messageList_node = React.findDOMNode(@messageList)
|
|
|
|
it "renders into the document", ->
|
|
expect(TestUtils.isCompositeComponentWithType(@messageList,
|
|
MessageList)).toBe true
|
|
|
|
it "by default has zero children", ->
|
|
items = TestUtils.scryRenderedComponentsWithType(@messageList,
|
|
MessageItemContainer)
|
|
|
|
expect(items.length).toBe 0
|
|
|
|
describe "Populated Message list", ->
|
|
beforeEach ->
|
|
MessageStore._items = testMessages
|
|
MessageStore._expandItemsToDefault()
|
|
MessageStore.trigger(MessageStore)
|
|
@messageList.setState(currentThread: test_thread)
|
|
|
|
NylasTestUtils.loadKeymap("keymaps/base")
|
|
|
|
it "renders all the correct number of messages", ->
|
|
items = TestUtils.scryRenderedComponentsWithType(@messageList,
|
|
MessageItemContainer)
|
|
expect(items.length).toBe 5
|
|
|
|
it "renders the correct number of expanded messages", ->
|
|
msgs = TestUtils.scryRenderedDOMComponentsWithClass(@messageList, "collapsed message-item-wrap")
|
|
expect(msgs.length).toBe 4
|
|
|
|
it "displays lists of participants on the page", ->
|
|
items = TestUtils.scryRenderedComponentsWithType(@messageList,
|
|
MessageParticipants)
|
|
expect(items.length).toBe 2
|
|
|
|
it "focuses new composers when a draft is added", ->
|
|
spyOn(@messageList, "_focusDraft")
|
|
msgs = @messageList.state.messages
|
|
|
|
@messageList.setState
|
|
messages: msgs.concat(draftMessages)
|
|
|
|
expect(@messageList._focusDraft).toHaveBeenCalled()
|
|
expect(@messageList._focusDraft.mostRecentCall.args[0].props.draftClientId).toEqual(draftMessages[0].draftClientId)
|
|
|
|
it "includes drafts as message item containers", ->
|
|
msgs = @messageList.state.messages
|
|
@messageList.setState
|
|
messages: msgs.concat(draftMessages)
|
|
items = TestUtils.scryRenderedComponentsWithType(@messageList,
|
|
MessageItemContainer)
|
|
expect(items.length).toBe 6
|
|
|
|
describe "MessageList with draft", ->
|
|
beforeEach ->
|
|
MessageStore._items = testMessages.concat draftMessages
|
|
MessageStore._thread = test_thread
|
|
MessageStore.trigger(MessageStore)
|
|
spyOn(@messageList, "_focusDraft")
|
|
|
|
it "renders the composer", ->
|
|
items = TestUtils.scryRenderedComponentsWithTypeAndProps(@messageList, InjectedComponent, matching: {role:"Composer"})
|
|
expect(@messageList.state.messages.length).toBe 6
|
|
expect(items.length).toBe 1
|
|
|
|
it "doesn't focus on initial load", ->
|
|
expect(@messageList._focusDraft).not.toHaveBeenCalled()
|
|
|
|
describe "reply type", ->
|
|
it "prompts for a reply when there's only one participant", ->
|
|
MessageStore._items = [m3, m5]
|
|
MessageStore._thread = test_thread
|
|
MessageStore.trigger()
|
|
expect(@messageList._replyType()).toBe "reply"
|
|
cs = TestUtils.scryRenderedDOMComponentsWithClass(@messageList, "footer-reply-area")
|
|
expect(cs.length).toBe 1
|
|
|
|
it "prompts for a reply-all when there's more than one participant and the default is reply-all", ->
|
|
spyOn(NylasEnv.config, "get").andReturn "reply-all"
|
|
MessageStore._items = [m5, m3]
|
|
MessageStore._thread = test_thread
|
|
MessageStore.trigger()
|
|
expect(@messageList._replyType()).toBe "reply-all"
|
|
cs = TestUtils.scryRenderedDOMComponentsWithClass(@messageList, "footer-reply-area")
|
|
expect(cs.length).toBe 1
|
|
|
|
it "prompts for a reply-all when there's more than one participant and the default is reply", ->
|
|
spyOn(NylasEnv.config, "get").andReturn "reply"
|
|
MessageStore._items = [m5, m3]
|
|
MessageStore._thread = test_thread
|
|
MessageStore.trigger()
|
|
expect(@messageList._replyType()).toBe "reply"
|
|
cs = TestUtils.scryRenderedDOMComponentsWithClass(@messageList, "footer-reply-area")
|
|
expect(cs.length).toBe 1
|
|
|
|
it "hides the reply type if the last message is a draft", ->
|
|
MessageStore._items = [m5, m3, draftMessages[0]]
|
|
MessageStore._thread = test_thread
|
|
MessageStore.trigger()
|
|
cs = TestUtils.scryRenderedDOMComponentsWithClass(@messageList, "footer-reply-area")
|
|
expect(cs.length).toBe 0
|
|
|
|
describe "reply behavior (_createReplyOrUpdateExistingDraft)", ->
|
|
beforeEach ->
|
|
@messageList.setState(currentThread: test_thread)
|
|
|
|
it "should throw an exception unless you provide `reply` or `reply-all`", ->
|
|
expect( => @messageList._createReplyOrUpdateExistingDraft('lala')).toThrow()
|
|
|
|
describe "when there is already a draft at the bottom of the thread", ->
|
|
beforeEach ->
|
|
@replyToMessage = new Message
|
|
id: "reply-id",
|
|
threadId: test_thread.id
|
|
accountId : TEST_ACCOUNT_ID
|
|
date: new Date()
|
|
@draft = new Message
|
|
id: "666",
|
|
draft: true,
|
|
date: new Date()
|
|
replyToMessage: @replyToMessage.id
|
|
accountId : TEST_ACCOUNT_ID
|
|
|
|
spyOn(@messageList, '_focusDraft')
|
|
spyOn(@replyToMessage, 'participantsForReplyAll').andCallFake ->
|
|
{to: [user_3], cc: [user_2, user_4] }
|
|
spyOn(@replyToMessage, 'participantsForReply').andCallFake ->
|
|
{to: [user_3], cc: [] }
|
|
|
|
MessageStore._items = [@replyToMessage, @draft]
|
|
MessageStore._thread = test_thread
|
|
MessageStore.trigger()
|
|
|
|
@sessionStub =
|
|
draft: => @draft
|
|
changes:
|
|
add: jasmine.createSpy('session.changes.add')
|
|
spyOn(DraftStore, 'sessionForClientId').andCallFake =>
|
|
Promise.resolve(@sessionStub)
|
|
|
|
it "should not fire a composer action", ->
|
|
spyOn(Actions, 'composeReplyAll')
|
|
@messageList._createReplyOrUpdateExistingDraft('reply-all')
|
|
advanceClock()
|
|
expect(Actions.composeReplyAll).not.toHaveBeenCalled()
|
|
|
|
it "should focus the existing draft", ->
|
|
@messageList._createReplyOrUpdateExistingDraft('reply-all')
|
|
advanceClock()
|
|
expect(@messageList._focusDraft).toHaveBeenCalled()
|
|
|
|
describe "when reply-all is passed", ->
|
|
it "should add missing participants", ->
|
|
@draft.to = [ user_3 ]
|
|
@draft.cc = []
|
|
@messageList._createReplyOrUpdateExistingDraft('reply-all')
|
|
advanceClock()
|
|
expect(@sessionStub.changes.add).toHaveBeenCalledWith({to: [user_3], cc: [user_2, user_4]})
|
|
|
|
it "should not blow away other participants who have been added to the draft", ->
|
|
user_random_a = new Contact(email: 'other-guy-a@gmail.com')
|
|
user_random_b = new Contact(email: 'other-guy-b@gmail.com')
|
|
@draft.to = [ user_3, user_random_a ]
|
|
@draft.cc = [ user_random_b ]
|
|
@messageList._createReplyOrUpdateExistingDraft('reply-all')
|
|
advanceClock()
|
|
expect(@sessionStub.changes.add).toHaveBeenCalledWith({to: [user_3, user_random_a], cc: [user_random_b, user_2, user_4]})
|
|
|
|
describe "when reply is passed", ->
|
|
it "should remove participants present in the reply-all participant set and not in the reply set", ->
|
|
@draft.to = [ user_3 ]
|
|
@draft.cc = [ user_2, user_4 ]
|
|
@messageList._createReplyOrUpdateExistingDraft('reply')
|
|
advanceClock()
|
|
expect(@sessionStub.changes.add).toHaveBeenCalledWith({to: [user_3], cc: []})
|
|
|
|
it "should not blow away other participants who have been added to the draft", ->
|
|
user_random_a = new Contact(email: 'other-guy-a@gmail.com')
|
|
user_random_b = new Contact(email: 'other-guy-b@gmail.com')
|
|
@draft.to = [ user_3, user_random_a ]
|
|
@draft.cc = [ user_2, user_4, user_random_b ]
|
|
@messageList._createReplyOrUpdateExistingDraft('reply')
|
|
advanceClock()
|
|
expect(@sessionStub.changes.add).toHaveBeenCalledWith({to: [user_3, user_random_a], cc: [user_random_b]})
|
|
|
|
describe "when there is not an existing draft at the bottom of the thread", ->
|
|
beforeEach ->
|
|
MessageStore._items = [m5, m3]
|
|
MessageStore._thread = test_thread
|
|
MessageStore.trigger()
|
|
|
|
it "should fire a composer action based on the reply type", ->
|
|
spyOn(Actions, 'composeReplyAll')
|
|
@messageList._createReplyOrUpdateExistingDraft('reply-all')
|
|
expect(Actions.composeReplyAll).toHaveBeenCalledWith(thread: test_thread, message: m3)
|
|
|
|
spyOn(Actions, 'composeReply')
|
|
@messageList._createReplyOrUpdateExistingDraft('reply')
|
|
expect(Actions.composeReply).toHaveBeenCalledWith(thread: test_thread, message: m3)
|
|
|
|
describe "Message minification", ->
|
|
beforeEach ->
|
|
@messageList.MINIFY_THRESHOLD = 3
|
|
@messageList.setState minified: true
|
|
@messages = [
|
|
{id: 'a'}, {id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}, {id: 'f'}, {id: 'g'}
|
|
]
|
|
|
|
it "ignores the first message if it's collapsed", ->
|
|
@messageList.setState messagesExpandedState:
|
|
a: false, b: false, c: false, d: false, e: false, f: false, g: "default"
|
|
|
|
out = @messageList._messagesWithMinification(@messages)
|
|
expect(out).toEqual [
|
|
{id: 'a'},
|
|
{
|
|
type: "minifiedBundle"
|
|
messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}]
|
|
},
|
|
{id: 'f'},
|
|
{id: 'g'}
|
|
]
|
|
|
|
it "ignores the first message if it's expanded", ->
|
|
@messageList.setState messagesExpandedState:
|
|
a: "default", b: false, c: false, d: false, e: false, f: false, g: "default"
|
|
|
|
out = @messageList._messagesWithMinification(@messages)
|
|
expect(out).toEqual [
|
|
{id: 'a'},
|
|
{
|
|
type: "minifiedBundle"
|
|
messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}]
|
|
},
|
|
{id: 'f'},
|
|
{id: 'g'}
|
|
]
|
|
|
|
it "doesn't minify the last collapsed message", ->
|
|
@messageList.setState messagesExpandedState:
|
|
a: false, b: false, c: false, d: false, e: false, f: "default", g: "default"
|
|
|
|
out = @messageList._messagesWithMinification(@messages)
|
|
expect(out).toEqual [
|
|
{id: 'a'},
|
|
{
|
|
type: "minifiedBundle"
|
|
messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}]
|
|
},
|
|
{id: 'e'},
|
|
{id: 'f'},
|
|
{id: 'g'}
|
|
]
|
|
|
|
it "allows explicitly expanded messages", ->
|
|
@messageList.setState messagesExpandedState:
|
|
a: false, b: false, c: false, d: false, e: false, f: "explicit", g: "default"
|
|
|
|
out = @messageList._messagesWithMinification(@messages)
|
|
expect(out).toEqual [
|
|
{id: 'a'},
|
|
{
|
|
type: "minifiedBundle"
|
|
messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}]
|
|
},
|
|
{id: 'f'},
|
|
{id: 'g'}
|
|
]
|
|
|
|
it "doesn't minify if the threshold isn't reached", ->
|
|
@messageList.setState messagesExpandedState:
|
|
a: false, b: "default", c: false, d: "default", e: false, f: "default", g: "default"
|
|
|
|
out = @messageList._messagesWithMinification(@messages)
|
|
expect(out).toEqual [
|
|
{id: 'a'},
|
|
{id: 'b'},
|
|
{id: 'c'},
|
|
{id: 'd'},
|
|
{id: 'e'},
|
|
{id: 'f'},
|
|
{id: 'g'}
|
|
]
|
|
|
|
it "doesn't minify if the threshold isn't reached due to the rule about not minifying the last collapsed messages", ->
|
|
@messageList.setState messagesExpandedState:
|
|
a: false, b: false, c: false, d: false, e: "default", f: "default", g: "default"
|
|
|
|
out = @messageList._messagesWithMinification(@messages)
|
|
expect(out).toEqual [
|
|
{id: 'a'},
|
|
{id: 'b'},
|
|
{id: 'c'},
|
|
{id: 'd'},
|
|
{id: 'e'},
|
|
{id: 'f'},
|
|
{id: 'g'}
|
|
]
|
|
|
|
it "minifies at the threshold if the message is explicitly expanded", ->
|
|
@messageList.setState messagesExpandedState:
|
|
a: false, b: false, c: false, d: false, e: "explicit", f: "default", g: "default"
|
|
|
|
out = @messageList._messagesWithMinification(@messages)
|
|
expect(out).toEqual [
|
|
{id: 'a'},
|
|
{
|
|
type: "minifiedBundle"
|
|
messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}]
|
|
},
|
|
{id: 'e'},
|
|
{id: 'f'},
|
|
{id: 'g'}
|
|
]
|
|
|
|
it "can have multiple minification blocks", ->
|
|
messages = [
|
|
{id: 'a'}, {id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}, {id: 'f'},
|
|
{id: 'g'}, {id: 'h'}, {id: 'i'}, {id: 'j'}, {id: 'k'}, {id: 'l'}
|
|
]
|
|
|
|
@messageList.setState messagesExpandedState:
|
|
a: false, b: false, c: false, d: false, e: false, f: "default",
|
|
g: false, h: false, i: false, j: false, k: false, l: "default"
|
|
|
|
out = @messageList._messagesWithMinification(messages)
|
|
expect(out).toEqual [
|
|
{id: 'a'},
|
|
{
|
|
type: "minifiedBundle"
|
|
messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}]
|
|
},
|
|
{id: 'e'},
|
|
{id: 'f'},
|
|
{
|
|
type: "minifiedBundle"
|
|
messages: [{id: 'g'}, {id: 'h'}, {id: 'i'}, {id: 'j'}]
|
|
},
|
|
{id: 'k'},
|
|
{id: 'l'}
|
|
]
|
|
|
|
it "can have multiple minification blocks next to explicitly expanded messages", ->
|
|
messages = [
|
|
{id: 'a'}, {id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}, {id: 'f'},
|
|
{id: 'g'}, {id: 'h'}, {id: 'i'}, {id: 'j'}, {id: 'k'}, {id: 'l'}
|
|
]
|
|
|
|
@messageList.setState messagesExpandedState:
|
|
a: false, b: false, c: false, d: false, e: "explicit", f: "default",
|
|
g: false, h: false, i: false, j: false, k: "explicit", l: "default"
|
|
|
|
out = @messageList._messagesWithMinification(messages)
|
|
expect(out).toEqual [
|
|
{id: 'a'},
|
|
{
|
|
type: "minifiedBundle"
|
|
messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}]
|
|
},
|
|
{id: 'e'},
|
|
{id: 'f'},
|
|
{
|
|
type: "minifiedBundle"
|
|
messages: [{id: 'g'}, {id: 'h'}, {id: 'i'}, {id: 'j'}]
|
|
},
|
|
{id: 'k'},
|
|
{id: 'l'}
|
|
]
|