feat(message): change long format timestamp

Summary: Minor UI fixes

Test Plan: edgehill --test

Reviewers: bengotow

Reviewed By: bengotow

Differential Revision: https://review.inboxapp.com/D1271
This commit is contained in:
Evan Morikawa 2015-03-10 10:08:04 -07:00
parent 6ec84561c4
commit 282cc40e9a
20 changed files with 220 additions and 78 deletions

View file

@ -4,8 +4,9 @@ module.exports =
# Models
Menu: require '../src/components/menu'
Popover: require '../src/components/popover'
TokenizingTextField: require '../src/components/tokenizing-text-field'
ResizableRegion: require '../src/components/resizable-region'
Tooltip: require '../src/components/tooltip'
Flexbox: require '../src/components/flexbox'
RetinaImg: require '../src/components/retina-img'
ListTabular: require '../src/components/list-tabular'
ResizableRegion: require '../src/components/resizable-region'
TokenizingTextField: require '../src/components/tokenizing-text-field'

View file

@ -3,13 +3,16 @@ _ = require 'underscore-plus'
EmailFrame = require './email-frame'
MessageParticipants = require "./message-participants.cjsx"
MessageTimestamp = require "./message-timestamp.cjsx"
{ComponentRegistry, FileDownloadStore, Utils} = require 'inbox-exports'
{ComponentRegistry, FileDownloadStore, Utils, Actions} = require 'inbox-exports'
{RetinaImg} = require 'ui-components'
Autolinker = require 'autolinker'
module.exports =
MessageItem = React.createClass
displayName: 'MessageItem'
propTypes:
thread: React.PropTypes.object.isRequired
message: React.PropTypes.object.isRequired
thread_participants: React.PropTypes.arrayOf(React.PropTypes.object)
collapsed: React.PropTypes.bool
@ -31,7 +34,6 @@ MessageItem = React.createClass
@_storeUnlisten() if @_storeUnlisten
render: ->
messageActions = ComponentRegistry.findAllViewsByRole('MessageAction')
messageIndicators = ComponentRegistry.findAllViewsByRole('MessageIndicator')
attachments = @_attachmentComponents()
if attachments.length > 0
@ -40,30 +42,29 @@ MessageItem = React.createClass
header =
<header className="message-header">
<MessageTimestamp className="message-time"
onClick={=> @setState detailedHeaders: true}
isDetailed={@state.detailedHeaders}
date={@props.message.date} />
<div className="message-header-right">
<MessageTimestamp className="message-time"
isDetailed={@state.detailedHeaders}
date={@props.message.date} />
<div className="message-actions">
{<Action thread={@props.thread} message={@props.message} /> for Action in messageActions}
{<div className="message-indicator"><Indicator message={@props.message}/></div> for Indicator in messageIndicators}
{if @state.detailedHeaders then @_renderMessageActionsInline() else @_renderMessageActionsTooltip()}
</div>
{<div className="message-indicator"><Indicator message={@props.message}/></div> for Indicator in messageIndicators}
<MessageParticipants to={@props.message.to}
cc={@props.message.cc}
from={@props.message.from}
onClick={=> @setState detailedHeaders: true}
thread_participants={@props.thread_participants}
detailedParticipants={@state.detailedHeaders}
isDetailed={@state.detailedHeaders}
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>
{@_renderCollapseControl()}
</header>
<div className="message-item-wrap">
<div className={@props.className}>
<div className="message-item-area">
{header}
{attachments}
@ -79,6 +80,58 @@ MessageItem = React.createClass
'no-quoted-text': !Utils.containsQuotedText(@props.message.body)
'show-quoted-text': @state.showQuotedText
_renderMessageActionsInline: ->
@_renderMessageActions()
_renderMessageActionsTooltip: ->
## TODO: Use Tooltip UI Component
<span className="msg-actions-tooltip"
onClick={=> @setState detailedHeaders: true}>
<RetinaImg name={"message-show-more.png"}/></span>
_renderMessageActions: ->
messageActions = ComponentRegistry.findAllViewsByRole('MessageAction')
<div className="message-actions">
<button className="btn btn-icon" onClick={@_onReply}>
<RetinaImg name={"message-reply.png"}/>
</button>
<button className="btn btn-icon" onClick={@_onReplyAll}>
<RetinaImg name={"message-reply-all.png"}/>
</button>
<button className="btn btn-icon" onClick={@_onForward}>
<RetinaImg name={"message-forward.png"}/>
</button>
{<Action thread={@props.thread} message={@props.message} /> for Action in messageActions}
</div>
_onReply: ->
tId = @props.thread.id; mId = @props.message.id
Actions.composeReply(threadId: tId, messageId: mId) if (tId and mId)
_onReplyAll: ->
tId = @props.thread.id; mId = @props.message.id
Actions.composeReplyAll(threadId: tId, messageId: mId) if (tId and mId)
_onForward: ->
tId = @props.thread.id; mId = @props.message.id
Actions.composeForward(threadId: tId, messageId: mId) if (tId and mId)
_renderCollapseControl: ->
if @state.detailedHeaders
<div className="collapse-control"
style={top: "-1px", left: "-17px"}
onClick={=> @setState detailedHeaders: false}>
<RetinaImg name={"message-disclosure-triangle-active.png"}/>
</div>
else
<div className="collapse-control inactive"
style={top: "-2px"}
onClick={=> @setState detailedHeaders: true}>
<RetinaImg name={"message-disclosure-triangle.png"}/>
</div>
# Eventually, _formatBody will run a series of registered body transformers.
# For now, it just runs a few we've hardcoded here, which are all synchronous.
_formatBody: ->

View file

@ -16,6 +16,8 @@ MessageList = React.createClass
@_unsubscribers = []
@_unsubscribers.push MessageStore.listen @_onChange
@_unsubscribers.push ThreadStore.listen @_onChange
@_lastHeight = -1
@_scrollToBottom()
componentWillUnmount: ->
unsubscribe() for unsubscribe in @_unsubscribers
@ -26,6 +28,8 @@ MessageList = React.createClass
@_focusComposerId = newDrafts[0]
componentDidUpdate: ->
@_lastHeight = -1
@_scrollToBottom()
if @_focusComposerId?
@_focusRef(@refs["composerItem-#{@_focusComposerId}"])
@_focusComposerId = null
@ -41,7 +45,7 @@ MessageList = React.createClass
return <div></div> if not @state.current_thread?
<div className="message-list" id="message-list">
<div tabIndex=1 className="messages-wrap">
<div tabIndex=1 ref="messageWrap" className="messages-wrap">
<div className="message-list-notification-bars">
{@_messageListNotificationBars()}
</div>
@ -84,7 +88,7 @@ MessageList = React.createClass
_messageComponents: ->
ComposerItem = @state.Composer
containsUnread = _.any @state.messages, (m) -> m.unread
# containsUnread = _.any @state.messages, (m) -> m.unread
collapsed = false
components = []
@ -94,12 +98,15 @@ MessageList = React.createClass
ref="composerItem-#{message.id}"
key={@state.messageLocalIds[message.id]}
localId={@state.messageLocalIds[message.id]}
containerClass="message-item-wrap"/>
containerClass="message-item-wrap draft-message"/>
else
className = "message-item-wrap"
if message.unread then className += " unread-message"
components.push <MessageItem key={message.id}
thread={@state.current_thread}
message={message}
collapsed={collapsed}
className={className}
thread_participants={@_threadParticipants()} />
components
@ -125,6 +132,35 @@ MessageList = React.createClass
participants[contact.email] = contact
return _.values(participants)
# There may be a lot of iframes to load which may take an indeterminate
# amount of time. As long as there is more content being painted onto
# the page, we keep trying to scroll to the bottom. We scroll to the top
# of the last message.
#
# We don't scroll if there's only 1 item.
# We don't screll if you're actively focused somewhere in the message
# list.
_scrollToBottom: ->
_.defer =>
if @isMounted()
messageWrap = @refs?.messageWrap?.getDOMNode?()
return if not messageWrap?
return if messageWrap.children <= 1
return if @getDOMNode().contains document.activeElement
msgToScroll = messageWrap.querySelector(".draft-message, .unread-message")
if not msgToScroll?
msgToScroll = messageWrap.children[messageWrap.children.length - 1]
currentHeight = messageWrap.getBoundingClientRect().height
if currentHeight isnt @_lastHeight
@_lastHeight = currentHeight
@_scrollToBottom()
else
scrollTo = currentHeight - msgToScroll.getBoundingClientRect().height
@getDOMNode().scrollTop = scrollTo
MessageList.minWidth = 600
MessageList.maxWidth = 900

View file

@ -9,10 +9,10 @@ MessageParticipants = React.createClass
classSet = React.addons.classSet
"participants": true
"message-participants": true
"collapsed": not @props.detailedParticipants
"collapsed": not @props.isDetailed
<div className={classSet} onClick={@props.onClick}>
{if @props.detailedParticipants then @_renderExpanded() else @_renderCollapsed()}
{if @props.isDetailed then @_renderExpanded() else @_renderCollapsed()}
</div>
_renderCollapsed: ->
@ -28,17 +28,18 @@ MessageParticipants = React.createClass
_renderExpanded: ->
<div className="expanded-participants">
<div>
<div className="participant-type">
<div className="participant-label from-label">From:&nbsp;</div>
<div className="participant-name from-contact">{@_fullContact(@props.from)}</div>
</div>
<div>
<div className="participant-type">
<div className="participant-label to-label">To:&nbsp;</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-type"
style={if @props.cc.length > 0 then display:"block" else display:"none"}>
<div className="participant-label cc-label">Cc:&nbsp;</div>
<div className="participant-name cc-contact">{@_fullContact(@props.cc)}</div>
</div>
@ -48,4 +49,14 @@ MessageParticipants = React.createClass
_.map(contacts, (c) -> c.displayFirstName()).join(", ")
_fullContact: (contacts=[]) ->
_.map(contacts, (c) -> c.displayFullContact()).join(", ")
_.map(contacts, (c) ->
if c.name?.length > 0 and c.name isnt c.email
<div key={c.email} className="participant">
<span className="participant-primary">{c.name}</span>&nbsp;
<span className="participant-secondary"><{c.email}></span>
</div>
else
<div key={c.email} className="participant">
<span className="participant-primary">{c.email}</span>
</div>
)

View file

@ -19,7 +19,7 @@ MessageTimestamp = React.createClass
_timeFormat: ->
if @props.isDetailed
return "ddd, MMM Do YYYY, h:mm:ss a z"
return "DD / MM / YYYY h:mm a z"
else
today = moment(@_today())
dayOfEra = today.dayOfYear() + today.year() * 365

View file

@ -80,7 +80,7 @@ describe "MessageParticipants", ->
cc={test_message.cc}
from={test_message.from}
thread_participants={many_thread_users}
detailedParticipants={true}
isDetailed={true}
message_participants={test_message.participants()} />
)
@ -90,7 +90,7 @@ describe "MessageParticipants", ->
it "uses full names", ->
to = ReactTestUtils.findRenderedDOMComponentWithClass(@participants, "to-contact")
expect(to.getDOMNode().innerHTML).toBe "User Two &lt;user2@nilas.com&gt;"
expect(to.getDOMNode().innerText).toEqual "User Two <user2@nilas.com>"
# TODO: We no longer display "to everyone"

View file

@ -35,7 +35,7 @@ describe "MessageTimestamp", ->
it "displays the full time when in detailed timestamp mode", ->
itemDetailed = TestUtils.renderIntoDocument(
<MessageTimestamp date={testDate()} detailedTimestamp={true} />
<MessageTimestamp date={testDate()} isDetailed={true} />
)
spyOn(itemDetailed, "_today").andCallFake -> testDate()
expect(itemDetailed._timeFormat()).toBe "ddd, MMM Do YYYY, h:mm:ss a z"
expect(itemDetailed._timeFormat()).toBe "DD / MM / YYYY h:mm a z"

View file

@ -65,42 +65,53 @@
}
}
.message-header {
position: relative;
font-size: @font-size-small;
.message-actions {
z-index: 4;
margin-top: 0.35em;
margin-left: 0.5em;
text-align: right;
.btn-icon {
padding: 0 @spacing-half;
&:last-child { padding-right: 0; }
margin-left: 10px;
margin-right: 0;
&:active {background: transparent;}
}
}
.message-time {
z-index: 2; position: relative;
display: inline-block;
}
.msg-actions-tooltip {
display: inline-block;
margin-left: 1em;
}
.message-participants { z-index: 1; position: relative; }
.message-time, .message-indicator {
color: @text-color-very-subtle;
}
.message-header-right {
z-index: 4; position: relative;
float: right;
}
}
.message-item-area {
width: 100%;
max-width: @message-max-width;
margin: 0 auto;
padding: @spacing-standard @spacing-double;
.message-header {
position: relative;
font-size: @font-size-small;
.message-actions {
float:right;
padding-right:15px;
.btn-icon {
font-size:16px;
color:rgba(35, 31, 32, 0.1);
&:hover {
color:rgba(35, 31, 32, 0.5);
}
}
}
}
.message-time, .message-indicator {
color: @text-color-very-subtle;
float: right;
margin-left: 1em;
&:hover {
cursor: pointer;
}
}
.message-indicator {
margin-right: 4px;
}
iframe {
width: 100%;
border: 0;
@ -111,14 +122,19 @@
}
.collapse-headers {
.collapse-control {
&.inactive { display: none; }
z-index: 3;
position: absolute;
bottom: 0;
right: 0;
top: 0;
left: -1 * @spacing-standard;
color: @text-color-very-subtle;
}
.message-item-wrap:hover {
.collapse-control.inactive { display: block; }
}
.collapse-headers:hover {cursor: pointer;}
.collapse-control:hover {cursor: pointer;}
}
.attachments-area {
@ -137,36 +153,44 @@
font-weight: @headings-font-weight;
color: @text-color;
}
.to-label, .cc-label {
.from-label, .to-label, .cc-label {
color: @text-color-very-subtle;
}
.cc-label {
margin-left: @spacing-standard;
}
.to-label { font-weight: 600; }
.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;
.participant-type {
margin-top: 0.5em;
&:first-child {margin-top: 0;}
}
.from-label, .to-label, .cc-label {
float: left;
display: block;
font-weight: 600;
font-weight: 400;
margin-left: 0;
}
.to-label, .cc-label {
margin-right: 1.15em;
.from-contact, .to-contact, .cc-contact {
padding-left: 3.85em;
font-weight: 500;
}
.from-contact, .to-contact, .cc-contact {
padding-left: 2.85em;
.from-label { margin-right: 1em; }
.to-label, .cc-label { margin-right: 2.15em; }
.participant-primary {
color: @text-color;
}
.participant-secondary {
color: @text-color-very-subtle;
}
}
}

View file

@ -0,0 +1,15 @@
_ = require 'underscore-plus'
React = require 'react/addons'
###
The Tooltip component displays a consistent hovering tooltip for use when
extra context information is required.
###
module.exports =
Tooltip = React.createClass
render: ->
<div className="tooltip" style={@_styles()}>{@props.children}</div>
_styles: ->
@props.modifierElement

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -196,7 +196,9 @@
//** Global color for active items (e.g., navs or dropdowns).
@component-active-color: #fff;
//** Global background color for active items (e.g., navs or dropdowns).
@component-active-bg: #3a3e44;
// @component-active-bg: #3a3e44;
// @component-active-bg: #116cd6;
@component-active-bg: @nilas-blue;
//============================== Tables ===============================//