mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-09 06:04:33 +08:00
feat(messages): expandable message headers
Summary: feat(messages): expandable message headers Test Plan: edgehill --test Reviewers: bengotow Reviewed By: bengotow Differential Revision: https://review.inboxapp.com/D1267
This commit is contained in:
parent
09dc8887b7
commit
b479e099c1
12 changed files with 264 additions and 146 deletions
|
@ -22,7 +22,7 @@ MessageItem = React.createClass
|
||||||
# keyed by a fileId. The value is the downloadData.
|
# keyed by a fileId. The value is the downloadData.
|
||||||
downloads: FileDownloadStore.downloadsForFileIds(@props.message.fileIds())
|
downloads: FileDownloadStore.downloadsForFileIds(@props.message.fileIds())
|
||||||
showQuotedText: @_messageIsEmptyForward()
|
showQuotedText: @_messageIsEmptyForward()
|
||||||
collapsed: @props.collapsed
|
detailedHeaders: false
|
||||||
|
|
||||||
componentDidMount: ->
|
componentDidMount: ->
|
||||||
@_storeUnlisten = FileDownloadStore.listen(@_onDownloadStoreChange)
|
@_storeUnlisten = FileDownloadStore.listen(@_onDownloadStoreChange)
|
||||||
|
@ -38,8 +38,13 @@ MessageItem = React.createClass
|
||||||
attachments = <div className="attachments-area">{attachments}</div>
|
attachments = <div className="attachments-area">{attachments}</div>
|
||||||
|
|
||||||
header =
|
header =
|
||||||
<header className="message-header" onClick={@_onToggleCollapsed}>
|
<header className="message-header">
|
||||||
<MessageTimestamp className="message-time" date={@props.message.date} />
|
|
||||||
|
<MessageTimestamp className="message-time"
|
||||||
|
onClick={=> @setState detailedHeaders: true}
|
||||||
|
isDetailed={@state.detailedHeaders}
|
||||||
|
date={@props.message.date} />
|
||||||
|
|
||||||
<div className="message-actions">
|
<div className="message-actions">
|
||||||
{<Action thread={@props.thread} message={@props.message} /> for Action in messageActions}
|
{<Action thread={@props.thread} message={@props.message} /> for Action in messageActions}
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,27 +52,27 @@ MessageItem = React.createClass
|
||||||
<MessageParticipants to={@props.message.to}
|
<MessageParticipants to={@props.message.to}
|
||||||
cc={@props.message.cc}
|
cc={@props.message.cc}
|
||||||
from={@props.message.from}
|
from={@props.message.from}
|
||||||
|
onClick={=> @setState detailedHeaders: true}
|
||||||
thread_participants={@props.thread_participants}
|
thread_participants={@props.thread_participants}
|
||||||
|
detailedParticipants={@state.detailedHeaders}
|
||||||
message_participants={@props.message.participants()} />
|
message_participants={@props.message.participants()} />
|
||||||
|
|
||||||
|
<div className="collapse-headers"
|
||||||
|
style={if @state.detailedHeaders then {display: "block"} else {display: "none"}}
|
||||||
|
onClick={=> @setState detailedHeaders: false}><i className="fa fa-chevron-up"></i>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
if @state.collapsed
|
<div className="message-item-wrap">
|
||||||
<div className="message-item-wrap collapsed">
|
<div className="message-item-area">
|
||||||
<div className="messsage-item-area">
|
{header}
|
||||||
{header}
|
{attachments}
|
||||||
</div>
|
<EmailFrame showQuotedText={@state.showQuotedText}>
|
||||||
</div>
|
{@_formatBody()}
|
||||||
else
|
</EmailFrame>
|
||||||
<div className="message-item-wrap">
|
<a className={@_quotedTextClasses()} onClick={@_toggleQuotedText}></a>
|
||||||
<div className="message-item-area">
|
|
||||||
{header}
|
|
||||||
{attachments}
|
|
||||||
<EmailFrame showQuotedText={@state.showQuotedText}>
|
|
||||||
{@_formatBody()}
|
|
||||||
</EmailFrame>
|
|
||||||
<a className={@_quotedTextClasses()} onClick={@_toggleQuotedText}></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
_quotedTextClasses: -> React.addons.classSet
|
_quotedTextClasses: -> React.addons.classSet
|
||||||
"quoted-text-control": true
|
"quoted-text-control": true
|
||||||
|
@ -122,7 +127,3 @@ MessageItem = React.createClass
|
||||||
_onDownloadStoreChange: ->
|
_onDownloadStoreChange: ->
|
||||||
@setState
|
@setState
|
||||||
downloads: FileDownloadStore.downloadsForFileIds(@props.message.fileIds())
|
downloads: FileDownloadStore.downloadsForFileIds(@props.message.fileIds())
|
||||||
|
|
||||||
_onToggleCollapsed: ->
|
|
||||||
@setState
|
|
||||||
collapsed: !@state.collapsed
|
|
||||||
|
|
|
@ -102,12 +102,6 @@ MessageList = React.createClass
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
thread_participants={@_threadParticipants()} />
|
thread_participants={@_threadParticipants()} />
|
||||||
|
|
||||||
# Start collapsing messages if we've loaded more than 15. This prevents
|
|
||||||
# us from trying to load an unbounded number of iframes until we have
|
|
||||||
# a better optimized message list.
|
|
||||||
if components.length > 10
|
|
||||||
collapsed = true
|
|
||||||
|
|
||||||
components
|
components
|
||||||
|
|
||||||
_onChange: ->
|
_onChange: ->
|
||||||
|
|
|
@ -6,39 +6,46 @@ MessageParticipants = React.createClass
|
||||||
displayName: 'MessageParticipants'
|
displayName: 'MessageParticipants'
|
||||||
|
|
||||||
render: ->
|
render: ->
|
||||||
<div className="participants message-participants">
|
classSet = React.addons.classSet
|
||||||
{@_formattedParticipants()}
|
"participants": true
|
||||||
|
"message-participants": true
|
||||||
|
"collapsed": not @props.detailedParticipants
|
||||||
|
|
||||||
|
<div className={classSet} onClick={@props.onClick}>
|
||||||
|
{if @props.detailedParticipants then @_renderExpanded() else @_renderCollapsed()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
_formattedParticipants: ->
|
_renderCollapsed: ->
|
||||||
<span>
|
<span className="collapsed-participants">
|
||||||
<span className="participant-label from-label">From:</span>
|
<span className="participant-name from-contact">{@_shortNames(@props.from)}</span>
|
||||||
<span className="participant-name from-contact">{@_joinNames(@props.from)}</span>
|
|
||||||
{if @_isToEveryone() then @_toEveryone() else @_toSome()}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
_toEveryone: ->
|
|
||||||
<span>
|
|
||||||
<span className="participant-label to-label"> > </span>
|
<span className="participant-label to-label"> > </span>
|
||||||
<span className="participant-name to-everyone">Everyone</span>
|
<span className="participant-name to-contact">{@_shortNames(@props.to)}</span>
|
||||||
</span>
|
<span style={if @props.cc.length > 0 then display:"inline" else display:"none"}>
|
||||||
|
<span className="participant-label cc-label">Cc: </span>
|
||||||
_toSome: ->
|
<span className="participant-name cc-contact">{@_shortNames(@props.cc)}</span>
|
||||||
if @props.cc.length > 0
|
|
||||||
cc_spans = <span>
|
|
||||||
<span className="participant-label cc-label">CC: </span>
|
|
||||||
<span className="participant-name cc-contact">{@_joinNames(@props.cc)}</span>
|
|
||||||
</span>
|
</span>
|
||||||
<span>
|
|
||||||
<span className="participant-label to-label"> > </span>
|
|
||||||
<span className="participant-name to-contact">{@_joinNames(@props.to)}</span>
|
|
||||||
{cc_spans}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
_joinNames: (contacts=[]) ->
|
_renderExpanded: ->
|
||||||
|
<div className="expanded-participants">
|
||||||
|
<div>
|
||||||
|
<div className="participant-label from-label">From: </div>
|
||||||
|
<div className="participant-name from-contact">{@_fullContact(@props.from)}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="participant-label to-label">To: </div>
|
||||||
|
<div className="participant-name to-contact">{@_fullContact(@props.to)}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={if @props.cc.length > 0 then display:"inline" else display:"none"}>
|
||||||
|
<div className="participant-label cc-label">Cc: </div>
|
||||||
|
<div className="participant-name cc-contact">{@_fullContact(@props.cc)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
_shortNames: (contacts=[]) ->
|
||||||
_.map(contacts, (c) -> c.displayFirstName()).join(", ")
|
_.map(contacts, (c) -> c.displayFirstName()).join(", ")
|
||||||
|
|
||||||
_isToEveryone: ->
|
_fullContact: (contacts=[]) ->
|
||||||
mp = _.map(@props.message_participants, (c) -> c.email)
|
_.map(contacts, (c) -> c.displayFullContact()).join(", ")
|
||||||
tp = _.map(@props.thread_participants, (c) -> c.email)
|
|
||||||
mp.length > 10 and _.difference(tp, mp).length is 0
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
moment = require 'moment'
|
moment = require 'moment-timezone'
|
||||||
React = require 'react'
|
React = require 'react'
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
|
@ -7,26 +7,37 @@ MessageTimestamp = React.createClass
|
||||||
propTypes:
|
propTypes:
|
||||||
date: React.PropTypes.object.isRequired,
|
date: React.PropTypes.object.isRequired,
|
||||||
className: React.PropTypes.string,
|
className: React.PropTypes.string,
|
||||||
|
isDetailed: React.PropTypes.bool
|
||||||
|
onClick: React.PropTypes.func
|
||||||
|
|
||||||
render: ->
|
render: ->
|
||||||
<div className={@props.className}>{moment(@props.date).format(@_timeFormat())}</div>
|
<div className={@props.className}
|
||||||
|
onClick={@props.onClick}>{@_formattedDate()}</div>
|
||||||
|
|
||||||
|
_formattedDate: ->
|
||||||
|
moment.tz(@props.date, @_currentTimezone()).format(@_timeFormat())
|
||||||
|
|
||||||
_timeFormat: ->
|
_timeFormat: ->
|
||||||
today = moment(@_today())
|
if @props.isDetailed
|
||||||
dayOfEra = today.dayOfYear() + today.year() * 365
|
return "ddd, MMM Do YYYY, h:mm:ss a z"
|
||||||
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
|
else
|
||||||
return "MMM D YYYY"
|
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`
|
# Stubbable for testing. Returns a `moment`
|
||||||
_today: -> moment()
|
_today: -> moment.tz(@_currentTimezone())
|
||||||
|
|
||||||
|
_currentTimezone: -> Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -66,6 +66,8 @@ EmailFrameStub = React.createClass({render: -> <div></div>})
|
||||||
MessageItem = proxyquire '../lib/message-item.cjsx',
|
MessageItem = proxyquire '../lib/message-item.cjsx',
|
||||||
'./email-frame': EmailFrameStub
|
'./email-frame': EmailFrameStub
|
||||||
|
|
||||||
|
MessageTimestamp = require '../lib/message-timestamp.cjsx'
|
||||||
|
|
||||||
|
|
||||||
describe "MessageItem", ->
|
describe "MessageItem", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
|
@ -98,7 +100,7 @@ describe "MessageItem", ->
|
||||||
namespaceId: "nsid"
|
namespaceId: "nsid"
|
||||||
|
|
||||||
@threadParticipants = [user_1, user_2, user_3, user_4]
|
@threadParticipants = [user_1, user_2, user_3, user_4]
|
||||||
|
|
||||||
# Generate the test component. Should be called after @message is configured
|
# Generate the test component. Should be called after @message is configured
|
||||||
# for the test, since MessageItem assumes attributes of the message will not
|
# for the test, since MessageItem assumes attributes of the message will not
|
||||||
# change after getInitialState runs.
|
# change after getInitialState runs.
|
||||||
|
@ -110,16 +112,31 @@ describe "MessageItem", ->
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
thread_participants={@threadParticipants} />
|
thread_participants={@threadParticipants} />
|
||||||
)
|
)
|
||||||
|
|
||||||
describe "when collapsed", ->
|
# TODO: We currently don't support collapsed messages
|
||||||
|
# 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 displaying detailed headers", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
@createComponent({collapsed: true})
|
@createComponent({collapsed: false})
|
||||||
|
@component.setState detailedHeaders: true
|
||||||
|
|
||||||
it "should not render the EmailFrame", ->
|
it "correctly sets the participant states", ->
|
||||||
expect( -> ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub)).toThrow()
|
participants = ReactTestUtils.findRenderedDOMComponentWithClass(@component, "expanded-participants")
|
||||||
|
expect(participants).toBeDefined()
|
||||||
|
expect(-> ReactTestUtils.findRenderedDOMComponentWithClass(@component, "collapsed-participants")).toThrow()
|
||||||
|
|
||||||
it "should have the `collapsed` class", ->
|
it "correctly sets the timestamp", ->
|
||||||
expect(@component.getDOMNode().className.indexOf('collapsed') >= 0).toBe(true)
|
ts = ReactTestUtils.findRenderedComponentWithType(@component, MessageTimestamp)
|
||||||
|
expect(ts.props.isDetailed).toBe true
|
||||||
|
|
||||||
describe "when not collapsed", ->
|
describe "when not collapsed", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
_ = require 'underscore-plus'
|
_ = require 'underscore-plus'
|
||||||
React = require "react/addons"
|
React = require "react/addons"
|
||||||
|
ReactTestUtils = React.addons.TestUtils
|
||||||
TestUtils = React.addons.TestUtils
|
TestUtils = React.addons.TestUtils
|
||||||
{Contact, Message} = require "inbox-exports"
|
{Contact, Message} = require "inbox-exports"
|
||||||
MessageParticipants = require "../lib/message-participants.cjsx"
|
MessageParticipants = require "../lib/message-participants.cjsx"
|
||||||
|
|
||||||
user_1 =
|
user_1 =
|
||||||
name: "User One"
|
name: "User One"
|
||||||
email: "user1@inboxapp.com"
|
email: "user1@nilas.com"
|
||||||
user_2 =
|
user_2 =
|
||||||
name: "User Two"
|
name: "User Two"
|
||||||
email: "user2@inboxapp.com"
|
email: "user2@nilas.com"
|
||||||
user_3 =
|
user_3 =
|
||||||
name: "User Three"
|
name: "User Three"
|
||||||
email: "user3@inboxapp.com"
|
email: "user3@nilas.com"
|
||||||
user_4 =
|
user_4 =
|
||||||
name: "User Four"
|
name: "User Four"
|
||||||
email: "user4@inboxapp.com"
|
email: "user4@nilas.com"
|
||||||
user_5 =
|
user_5 =
|
||||||
name: "User Five"
|
name: "User Five"
|
||||||
email: "user5@inboxapp.com"
|
email: "user5@nilas.com"
|
||||||
|
|
||||||
many_users = (new Contact({name: "User #{i}", email:"#{i}@app.com"}) for i in [0..100])
|
many_users = (new Contact({name: "User #{i}", email:"#{i}@app.com"}) for i in [0..100])
|
||||||
|
|
||||||
|
@ -54,34 +55,74 @@ thread2_participants = [
|
||||||
]
|
]
|
||||||
|
|
||||||
describe "MessageParticipants", ->
|
describe "MessageParticipants", ->
|
||||||
it "determines the message is to everyone", ->
|
describe "when collapsed", ->
|
||||||
p1 = TestUtils.renderIntoDocument(
|
beforeEach ->
|
||||||
<MessageParticipants to={big_test_message.to}
|
@participants = TestUtils.renderIntoDocument(
|
||||||
cc={big_test_message.cc}
|
<MessageParticipants to={test_message.to}
|
||||||
from={big_test_message.from}
|
cc={test_message.cc}
|
||||||
thread_participants={many_thread_users}
|
from={test_message.from}
|
||||||
message_participants={big_test_message.participants()} />
|
thread_participants={many_thread_users}
|
||||||
)
|
message_participants={test_message.participants()} />
|
||||||
expect(p1._isToEveryone()).toBe true
|
)
|
||||||
|
|
||||||
it "knows when the message isn't to everyone due to participant mismatch", ->
|
it "renders into the document", ->
|
||||||
p2 = TestUtils.renderIntoDocument(
|
participants = ReactTestUtils.findRenderedDOMComponentWithClass(@participants, "collapsed-participants")
|
||||||
<MessageParticipants to={test_message.to}
|
expect(participants).toBeDefined()
|
||||||
cc={test_message.cc}
|
|
||||||
from={test_message.from}
|
|
||||||
thread_participants={thread2_participants}
|
|
||||||
message_participants={test_message.participants()} />
|
|
||||||
)
|
|
||||||
# this should be false because we don't count bccs
|
|
||||||
expect(p2._isToEveryone()).toBe false
|
|
||||||
|
|
||||||
it "knows when the message isn't to everyone due to participant size", ->
|
it "uses short names", ->
|
||||||
p2 = TestUtils.renderIntoDocument(
|
to = ReactTestUtils.findRenderedDOMComponentWithClass(@participants, "to-contact")
|
||||||
<MessageParticipants to={test_message.to}
|
expect(to.getDOMNode().innerHTML).toBe "User"
|
||||||
cc={test_message.cc}
|
|
||||||
from={test_message.from}
|
describe "when expanded", ->
|
||||||
thread_participants={thread_participants}
|
beforeEach ->
|
||||||
message_participants={test_message.participants()} />
|
@participants = TestUtils.renderIntoDocument(
|
||||||
)
|
<MessageParticipants to={test_message.to}
|
||||||
# this should be false because we don't count bccs
|
cc={test_message.cc}
|
||||||
expect(p2._isToEveryone()).toBe false
|
from={test_message.from}
|
||||||
|
thread_participants={many_thread_users}
|
||||||
|
detailedParticipants={true}
|
||||||
|
message_participants={test_message.participants()} />
|
||||||
|
)
|
||||||
|
|
||||||
|
it "renders into the document", ->
|
||||||
|
participants = ReactTestUtils.findRenderedDOMComponentWithClass(@participants, "expanded-participants")
|
||||||
|
expect(participants).toBeDefined()
|
||||||
|
|
||||||
|
it "uses full names", ->
|
||||||
|
to = ReactTestUtils.findRenderedDOMComponentWithClass(@participants, "to-contact")
|
||||||
|
expect(to.getDOMNode().innerHTML).toBe "User Two <user2@nilas.com>"
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: We no longer display "to everyone"
|
||||||
|
#
|
||||||
|
# it "determines the message is to everyone", ->
|
||||||
|
# p1 = TestUtils.renderIntoDocument(
|
||||||
|
# <MessageParticipants to={big_test_message.to}
|
||||||
|
# cc={big_test_message.cc}
|
||||||
|
# from={big_test_message.from}
|
||||||
|
# thread_participants={many_thread_users}
|
||||||
|
# message_participants={big_test_message.participants()} />
|
||||||
|
# )
|
||||||
|
# expect(p1._isToEveryone()).toBe true
|
||||||
|
#
|
||||||
|
# it "knows when the message isn't to everyone due to participant mismatch", ->
|
||||||
|
# p2 = TestUtils.renderIntoDocument(
|
||||||
|
# <MessageParticipants to={test_message.to}
|
||||||
|
# cc={test_message.cc}
|
||||||
|
# from={test_message.from}
|
||||||
|
# thread_participants={thread2_participants}
|
||||||
|
# message_participants={test_message.participants()} />
|
||||||
|
# )
|
||||||
|
# # this should be false because we don't count bccs
|
||||||
|
# expect(p2._isToEveryone()).toBe false
|
||||||
|
#
|
||||||
|
# it "knows when the message isn't to everyone due to participant size", ->
|
||||||
|
# p2 = TestUtils.renderIntoDocument(
|
||||||
|
# <MessageParticipants to={test_message.to}
|
||||||
|
# cc={test_message.cc}
|
||||||
|
# from={test_message.from}
|
||||||
|
# thread_participants={thread_participants}
|
||||||
|
# message_participants={test_message.participants()} />
|
||||||
|
# )
|
||||||
|
# # this should be false because we don't count bccs
|
||||||
|
# expect(p2._isToEveryone()).toBe false
|
||||||
|
|
|
@ -11,8 +11,7 @@ describe "MessageTimestamp", ->
|
||||||
@item = TestUtils.renderIntoDocument(
|
@item = TestUtils.renderIntoDocument(
|
||||||
<MessageTimestamp date={testDate()} />
|
<MessageTimestamp date={testDate()} />
|
||||||
)
|
)
|
||||||
@itemNode = @item.getDOMNode()
|
|
||||||
|
|
||||||
# test messsage time is 1415814587
|
# test messsage time is 1415814587
|
||||||
it "displays the time from messages LONG ago", ->
|
it "displays the time from messages LONG ago", ->
|
||||||
spyOn(@item, "_today").andCallFake -> testDate().add(2, 'years')
|
spyOn(@item, "_today").andCallFake -> testDate().add(2, 'years')
|
||||||
|
@ -33,3 +32,10 @@ describe "MessageTimestamp", ->
|
||||||
it "displays the time from messages recently", ->
|
it "displays the time from messages recently", ->
|
||||||
spyOn(@item, "_today").andCallFake -> testDate().add(2, 'hours')
|
spyOn(@item, "_today").andCallFake -> testDate().add(2, 'hours')
|
||||||
expect(@item._timeFormat()).toBe "h:mm a"
|
expect(@item._timeFormat()).toBe "h:mm a"
|
||||||
|
|
||||||
|
it "displays the full time when in detailed timestamp mode", ->
|
||||||
|
itemDetailed = TestUtils.renderIntoDocument(
|
||||||
|
<MessageTimestamp date={testDate()} detailedTimestamp={true} />
|
||||||
|
)
|
||||||
|
spyOn(itemDetailed, "_today").andCallFake -> testDate()
|
||||||
|
expect(itemDetailed._timeFormat()).toBe "ddd, MMM Do YYYY, h:mm:ss a z"
|
||||||
|
|
|
@ -72,6 +72,7 @@
|
||||||
padding: @spacing-standard @spacing-double;
|
padding: @spacing-standard @spacing-double;
|
||||||
|
|
||||||
.message-header {
|
.message-header {
|
||||||
|
position: relative;
|
||||||
font-size: @font-size-small;
|
font-size: @font-size-small;
|
||||||
.message-actions {
|
.message-actions {
|
||||||
float:right;
|
float:right;
|
||||||
|
@ -88,41 +89,18 @@
|
||||||
|
|
||||||
.message-time, .message-indicator {
|
.message-time, .message-indicator {
|
||||||
color: @text-color-very-subtle;
|
color: @text-color-very-subtle;
|
||||||
margin-top: 3px;
|
|
||||||
float: right;
|
float: right;
|
||||||
|
margin-left: 1em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-indicator {
|
.message-indicator {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.to-label {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.to-label, .to-everyone, .cc-label, .cc-contact, .to-label, .to-contact {
|
|
||||||
position: relative;
|
|
||||||
top: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.from-label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.from-contact {
|
|
||||||
font-weight: @headings-font-weight;
|
|
||||||
color: @text-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.to-label, .cc-label {
|
|
||||||
color: @text-color-very-subtle;
|
|
||||||
}
|
|
||||||
.cc-label {
|
|
||||||
margin-left: @spacing-standard;
|
|
||||||
}
|
|
||||||
.to-contact, .cc-contact, .to-everyone {
|
|
||||||
color: @text-color-very-subtle;
|
|
||||||
}
|
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -131,7 +109,64 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.collapse-headers {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
color: @text-color-very-subtle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-headers:hover {cursor: pointer;}
|
||||||
|
|
||||||
}
|
}
|
||||||
.attachments-area {
|
.attachments-area {
|
||||||
padding-top: @spacing-standard;
|
padding-top: @spacing-standard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////
|
||||||
|
// message-participants.cjsx //
|
||||||
|
///////////////////////////////
|
||||||
|
.message-participants {
|
||||||
|
|
||||||
|
&.collapsed:hover {cursor: pointer;}
|
||||||
|
|
||||||
|
.from-contact {
|
||||||
|
font-weight: @headings-font-weight;
|
||||||
|
color: @text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.to-label, .cc-label {
|
||||||
|
color: @text-color-very-subtle;
|
||||||
|
}
|
||||||
|
.cc-label {
|
||||||
|
margin-left: @spacing-standard;
|
||||||
|
}
|
||||||
|
.to-contact, .cc-contact, .to-everyone {
|
||||||
|
color: @text-color-very-subtle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.to-label { font-weight: 600; }
|
||||||
|
|
||||||
|
.expanded-participants {
|
||||||
|
position: relative;
|
||||||
|
padding-right: 1.2em;
|
||||||
|
|
||||||
|
.from-label, .to-label, .cc-label {
|
||||||
|
float: left;
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.to-label, .cc-label {
|
||||||
|
margin-right: 1.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.from-contact, .to-contact, .cc-contact {
|
||||||
|
padding-left: 2.85em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
"marked": "^0.3",
|
"marked": "^0.3",
|
||||||
"mkdirp": "^0.5",
|
"mkdirp": "^0.5",
|
||||||
"moment": "^2.8",
|
"moment": "^2.8",
|
||||||
|
"moment-timezone": "^0.3",
|
||||||
"nslog": "^2.0.0",
|
"nslog": "^2.0.0",
|
||||||
"oniguruma": "^4.0.0",
|
"oniguruma": "^4.0.0",
|
||||||
"optimist": "0.4.0",
|
"optimist": "0.4.0",
|
||||||
|
|
|
@ -193,4 +193,4 @@ describe "DraftStore", ->
|
||||||
@_callNewMessageWithContext {threadId: fakeThread.id, messageId: fakeMessage1.id}
|
@_callNewMessageWithContext {threadId: fakeThread.id, messageId: fakeMessage1.id}
|
||||||
, (thread, message) ->
|
, (thread, message) ->
|
||||||
expect(message).toEqual(fakeMessage1)
|
expect(message).toEqual(fakeMessage1)
|
||||||
{}
|
{}
|
||||||
|
|
|
@ -21,6 +21,12 @@ class Contact extends Model
|
||||||
json['name'] ||= json['email']
|
json['name'] ||= json['email']
|
||||||
json
|
json
|
||||||
|
|
||||||
|
displayFullContact: ->
|
||||||
|
if @name and @name isnt @email
|
||||||
|
return "#{@name} <#{@email}>"
|
||||||
|
else
|
||||||
|
return @email
|
||||||
|
|
||||||
displayName: ->
|
displayName: ->
|
||||||
return "You" if @email == NamespaceStore.current().emailAddress
|
return "You" if @email == NamespaceStore.current().emailAddress
|
||||||
@_nameParts().join(' ')
|
@_nameParts().join(' ')
|
||||||
|
|
|
@ -88,7 +88,6 @@ Utils =
|
||||||
|
|
||||||
Utils.images = {}
|
Utils.images = {}
|
||||||
Utils.images[path.basename(file)] = file for file in files
|
Utils.images[path.basename(file)] = file for file in files
|
||||||
# console.log("Loaded image map in #{Date.now()-start}msec")
|
|
||||||
|
|
||||||
if window.devicePixelRatio > 1
|
if window.devicePixelRatio > 1
|
||||||
return Utils.images["#{name}@2x.#{ext}"] ? Utils.images[fullname] ? Utils.images["#{name}@1x.#{ext}"]
|
return Utils.images["#{name}@2x.#{ext}"] ? Utils.images[fullname] ? Utils.images["#{name}@1x.#{ext}"]
|
||||||
|
|
Loading…
Add table
Reference in a new issue