fix(message): Limit number of collapsed recipients in message

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
This commit is contained in:
Juan Tejada 2016-02-03 11:22:12 -08:00
parent b481f15492
commit 97e9637068
5 changed files with 163 additions and 109 deletions

View file

@ -91,33 +91,33 @@ class MessageItem extends React.Component
"pending": @props.pending
<header className={classes} onClick={@_onClickHeader}>
{@_renderHeaderSideItems()}
<div className="message-header-right">
<MessageTimestamp className="message-time"
isDetailed={@state.detailedHeaders}
date={@props.message.date} />
{@_renderMessageControls()}
<MessageControls thread={@props.thread} message={@props.message}/>
</div>
<MessageParticipants to={@props.message.to}
cc={@props.message.cc}
bcc={@props.message.bcc}
from={@props.message.from}
subject={@props.message.subject}
onClick={@_onClickParticipants}
isDetailed={@state.detailedHeaders}
message_participants={@props.message.participants()} />
{@_renderFromParticipants()}
{@_renderToParticipants()}
{@_renderFolder()}
{@_renderHeaderDetailToggle()}
</header>
_renderMessageControls: ->
<MessageControls thread={@props.thread} message={@props.message}/>
_renderFromParticipants: =>
<MessageParticipants
from={@props.message.from}
onClick={@_onClickParticipants}
isDetailed={@state.detailedHeaders} />
_renderToParticipants: =>
<MessageParticipants
to={@props.message.to}
cc={@props.message.cc}
bcc={@props.message.bcc}
onClick={@_onClickParticipants}
isDetailed={@state.detailedHeaders} />
_renderFolder: =>
return [] unless @state.detailedHeaders and @props.message.folder

View file

@ -1,95 +1,33 @@
_ = require 'underscore'
React = require "react"
classNames = require 'classnames'
classnames = require 'classnames'
{Contact} = require 'nylas-exports'
MAX_COLLAPSED = 5
class MessageParticipants extends React.Component
@displayName: 'MessageParticipants'
render: =>
classSet = classNames
"participants": true
"message-participants": true
"collapsed": not @props.isDetailed
@propTypes:
to: React.PropTypes.array
cc: React.PropTypes.array
bcc: React.PropTypes.array
from: React.PropTypes.array
onClick: React.PropTypes.func
isDetailed: React.PropTypes.bool
<div className={classSet} onClick={@props.onClick}>
{if @props.isDetailed then @_renderExpanded() else @_renderCollapsed()}
</div>
@defaultProps:
to: []
cc: []
bcc: []
from: []
_renderCollapsed: =>
childSpans = [
<span className="participant-name from-contact" key="from">{@_shortNames(@props.from)}</span>
]
if @props.to?.length > 0
childSpans.push(
<span className="participant-label to-label" key="to-label">To:&nbsp;</span>
<span className="participant-name to-contact" key="to-value">{@_shortNames(@props.to)}</span>
)
# Helpers
if @props.cc?.length > 0
childSpans.push(
<span className="participant-label cc-label" key="cc-label">Cc:&nbsp;</span>
<span className="participant-name cc-contact" key="cc-value">{@_shortNames(@props.cc)}</span>
)
if @props.bcc?.length > 0
childSpans.push(
<span className="participant-label bcc-label" key="bcc-label">Bcc:&nbsp;</span>
<span className="participant-name bcc-contact" key="bcc-value">{@_shortNames(@props.bcc)}</span>
)
<span className="collapsed-participants">
{childSpans}
</span>
_renderExpanded: =>
<div className="expanded-participants">
<div className="participant-type">
<div className="participant-name from-contact">{@_fullContact(@props.from)}</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 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>
<div className="participant-type"
style={if @props.bcc?.length > 0 then display:"block" else display:"none"}>
<div className="participant-label bcc-label">Bcc:&nbsp;</div>
<div className="participant-name cc-contact">{@_fullContact(@props.bcc)}</div>
</div>
</div>
_shortNames: (contacts=[]) =>
_.map(contacts, (c) -> c.displayName(includeAccountLabel: true, compact: true)).join(", ")
_fullContact: (contacts=[]) =>
if contacts.length is 0
# This is necessary to make the floats work properly
<div>&nbsp;</div>
else
_.map(contacts, (c, i) =>
if contacts.length is 1 then comma = ""
else if i is contacts.length-1 then comma = ""
else comma = ","
if c.name?.length > 0 and c.name isnt c.email
<div key={c.email} className="participant selectable">
<span className="participant-primary" onClick={@_selectPlainText}>{c.name}</span>&nbsp;
<span className="participant-secondary" onClick={@_selectBracketedText}><{c.email}>{comma}</span>&nbsp;
</div>
else
<div key={c.email} className="participant selectable">
<span className="participant-primary" onClick={@_selectCommaText}>{c.email}{comma}</span>&nbsp;
</div>
)
_allToParticipants: =>
_.union(@props.to, @props.cc, @props.bcc)
_selectPlainText: (e) =>
textNode = e.currentTarget.childNodes[0]
@ -111,6 +49,103 @@ class MessageParticipants extends React.Component
selection.removeAllRanges()
selection.addRange(range)
_shortNames: (contacts = [], max = MAX_COLLAPSED) =>
names = _.map(contacts, (c) -> c.displayName(includeAccountLabel: true, compact: true))
if names.length > max
extra = names.length - max
names = names.slice(0, max)
names.push("and #{extra} more")
names.join(", ")
# Renderers
_renderFullContacts: (contacts = []) =>
_.map(contacts, (c, i) =>
if contacts.length is 1 then comma = ""
else if i is contacts.length-1 then comma = ""
else comma = ","
if c.name?.length > 0 and c.name isnt c.email
<div key={"#{c.email}-#{i}"} className="participant selectable">
<span className="participant-primary" onClick={@_selectPlainText}>{c.name}</span>&nbsp;
<span className="participant-secondary" onClick={@_selectBracketedText}><{c.email}>{comma}</span>&nbsp;
</div>
else
<div key={"#{c.email}-#{i}"} className="participant selectable">
<span className="participant-primary" onClick={@_selectCommaText}>{c.email}{comma}</span>&nbsp;
</div>
)
_renderExpandedField: (name, field, {includeLabel} = {}) =>
includeLabel ?= true
<div className="participant-type" key={"participant-type-#{name}"}>
{
if includeLabel
<div className={"participant-label #{name}-label"}>To:&nbsp;</div>
else
undefined
}
<div className={"participant-name #{name}-contact"}>
{@_renderFullContacts(field)}
</div>
</div>
_renderExpanded: =>
expanded = []
if @props.from.length > 0
expanded.push(
@_renderExpandedField('from', @props.from, includeLabel: false)
)
if @props.to.length > 0
expanded.push(
@_renderExpandedField('to', @props.to)
)
if @props.cc.length > 0
expanded.push(
@_renderExpandedField('cc', @props.cc)
)
if @props.bcc.length > 0
expanded.push(
@_renderExpandedField('bcc', @props.bcc)
)
<div className="expanded-participants">
{expanded}
</div>
_renderCollapsed: =>
childSpans = []
toParticipants = @_allToParticipants()
if @props.from.length > 0
childSpans.push(
<span className="participant-name from-contact" key="from">{@_shortNames(@props.from)}</span>
)
if toParticipants.length > 0
childSpans.push(
<span className="participant-label to-label" key="to-label">To:&nbsp;</span>
<span className="participant-name to-contact" key="to-value">{@_shortNames(toParticipants)}</span>
)
<span className="collapsed-participants">
{childSpans}
</span>
render: =>
classSet = classnames
"participants": true
"message-participants": true
"collapsed": not @props.isDetailed
"from-participants": @props.from.length > 0
"to-participants": @_allToParticipants().length > 0
<div className={classSet} onClick={@props.onClick}>
{if @props.isDetailed then @_renderExpanded() else @_renderCollapsed()}
</div>
module.exports = MessageParticipants

View file

@ -151,8 +151,8 @@ describe "MessageItem", ->
@component.setState detailedHeaders: true
it "correctly sets the participant states", ->
participants = ReactTestUtils.findRenderedDOMComponentWithClass(@component, "expanded-participants")
expect(participants).toBeDefined()
participants = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, "expanded-participants")
expect(participants.length).toBe 2
expect(-> ReactTestUtils.findRenderedDOMComponentWithClass(@component, "collapsed-participants")).toThrow()
it "correctly sets the timestamp", ->

View file

@ -205,7 +205,7 @@ describe "MessageList", ->
it "displays lists of participants on the page", ->
items = TestUtils.scryRenderedComponentsWithType(@messageList,
MessageParticipants)
expect(items.length).toBe 1
expect(items.length).toBe 2
it "focuses new composers when a draft is added", ->
spyOn(@messageList, "_focusDraft")

View file

@ -266,9 +266,6 @@ body.platform-win32 {
}
.collapsed-participants {
padding: 10px 10px 10px 0;
}
.message-item-divider {
border:0; // remove default hr border left, right
@ -409,7 +406,6 @@ body.platform-win32 {
display: inline-block;
margin-left: 1em;
}
.message-participants { z-index: 1; position: relative; }
.message-time, .message-indicator {
color: @text-color-very-subtle;
@ -519,7 +515,9 @@ body.platform-win32 {
}
}
.message-participants {
z-index: 1;
position: relative;
display: flex;
transition: padding-left 150ms;
transition-timing-function: ease-in-out;
@ -529,17 +527,38 @@ body.platform-win32 {
font-weight: @headings-font-weight;
color: @text-color;
}
.from-label, .to-label, .cc-label, .bcc-label, .subject-label {
.from-label, .to-label, .cc-label, .bcc-label {
color: @text-color-very-subtle;
}
.to-label, .cc-label, .bcc-label, .subject-label { margin-left: @spacing-standard; }
.to-contact, .cc-contact, .bcc-contact, .to-everyone {
color: @text-color-very-subtle;
}
&.to-participants {
width: 100%;
.collapsed-participants {
width: 100%;
margin-top: -8px;
}
}
.collapsed-participants {
display: flex;
align-items: center;
.to-contact {
display: inline-block;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
.expanded-participants {
position: relative;
padding-right: 1.2em;
width: 100%;
.participant {
display: inline-block;
@ -551,7 +570,7 @@ body.platform-win32 {
&:first-child {margin-top: 0;}
}
.from-label, .to-label, .cc-label, .bcc-label, .subject-label {
.from-label, .to-label, .cc-label, .bcc-label {
float: left;
display: block;
font-weight: @font-weight-normal;