mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-04 11:44:47 +08:00
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:
parent
b481f15492
commit
97e9637068
5 changed files with 163 additions and 109 deletions
|
@ -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
|
||||
|
|
|
@ -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: </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: </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: </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: </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: </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: </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> </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>
|
||||
<span className="participant-secondary" onClick={@_selectBracketedText}><{c.email}>{comma}</span>
|
||||
</div>
|
||||
else
|
||||
<div key={c.email} className="participant selectable">
|
||||
<span className="participant-primary" onClick={@_selectCommaText}>{c.email}{comma}</span>
|
||||
</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>
|
||||
<span className="participant-secondary" onClick={@_selectBracketedText}><{c.email}>{comma}</span>
|
||||
</div>
|
||||
else
|
||||
<div key={"#{c.email}-#{i}"} className="participant selectable">
|
||||
<span className="participant-primary" onClick={@_selectCommaText}>{c.email}{comma}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
_renderExpandedField: (name, field, {includeLabel} = {}) =>
|
||||
includeLabel ?= true
|
||||
<div className="participant-type" key={"participant-type-#{name}"}>
|
||||
{
|
||||
if includeLabel
|
||||
<div className={"participant-label #{name}-label"}>To: </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: </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
|
||||
|
|
|
@ -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", ->
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue