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
|
@ -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'
|
||||
|
|
|
@ -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: ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: </div>
|
||||
<div className="participant-name from-contact">{@_fullContact(@props.from)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="participant-type">
|
||||
<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-type"
|
||||
style={if @props.cc.length > 0 then display:"block" else display:"none"}>
|
||||
<div className="participant-label cc-label">Cc: </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>
|
||||
<span className="participant-secondary"><{c.email}></span>
|
||||
</div>
|
||||
else
|
||||
<div key={c.email} className="participant">
|
||||
<span className="participant-primary">{c.email}</span>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <user2@nilas.com>"
|
||||
expect(to.getDOMNode().innerText).toEqual "User Two <user2@nilas.com>"
|
||||
|
||||
|
||||
# TODO: We no longer display "to everyone"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
15
src/components/tooltip.cjsx
Normal 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
|
After Width: | Height: | Size: 345 B |
BIN
static/images/message-list/message-disclosure-triangle@2x.png
Normal file
After Width: | Height: | Size: 349 B |
BIN
static/images/message-list/message-forward@1x.png
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
static/images/message-list/message-forward@2x.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
static/images/message-list/message-reply-all@1x.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
static/images/message-list/message-reply-all@2x.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
static/images/message-list/message-reply@1x.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
static/images/message-list/message-reply@2x.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
static/images/message-list/message-show-more@1x.png
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
static/images/message-list/message-show-more@2x.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
|
@ -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 ===============================//
|
||||
|
|