fix(*): Minor performance tweaks and fixes to category picker

Summary:
fix(undo-redo): UndoRedoComponent does not take props

fix(category-picker):

- Use Actions.queueTask like the rest of the app so UndoRedoStore can see it. Can change this in the future but it's currently the only place in the app we directly queue tasks.

- Stop subscribing to the FocusedContentStore / FocusedCategoryStore (which are not used in setState?) since we receive threads as props

- Rename categoryDatum to item because it's not a category. (Was super confused that categories were becoming JSON in `_extendCategoryWithDisplayData`) Give item a category property so that tasks can specify items and not IDs (allows for better descriptions like "Moved one thread to Archive"

Add simple shouldComponentUpdate to retina-img

Test Plan: Run tests

Reviewers: evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D1832
This commit is contained in:
Ben Gotow 2015-08-03 17:05:31 -07:00
parent 8884435da0
commit 9b2f830348
8 changed files with 56 additions and 61 deletions

View file

@ -10,7 +10,6 @@ React = require 'react'
WorkspaceStore,
ChangeLabelsTask,
ChangeFolderTask,
FocusedContentStore,
FocusedCategoryStore} = require 'nylas-exports'
{Menu,
@ -33,8 +32,6 @@ class CategoryPicker extends React.Component
@unsubscribers = []
@unsubscribers.push CategoryStore.listen @_onStoreChanged
@unsubscribers.push NamespaceStore.listen @_onStoreChanged
@unsubscribers.push FocusedContentStore.listen @_onStoreChanged
@unsubscribers.push FocusedCategoryStore.listen @_onStoreChanged
@_commandUnsubscriber = atom.commands.add 'body',
"application:change-category": @_onOpenCategoryPopover
@ -94,7 +91,7 @@ class CategoryPicker extends React.Component
headerComponents={headerComponents}
footerComponents={[]}
items={@state.categoryData}
itemKey={ (categoryDatum) -> categoryDatum.id }
itemKey={ (item) -> item.id }
itemContent={@_renderItemContent}
onSelect={@_onSelectCategory}
defaultSelectedIndex={if @state.searchValue is "" then -1 else 0}
@ -107,55 +104,55 @@ class CategoryPicker extends React.Component
@refs.popover.open()
return
_renderItemContent: (categoryDatum) =>
if categoryDatum.divider
return <Menu.Item divider={categoryDatum.divider} />
_renderItemContent: (item) =>
if item.divider
return <Menu.Item divider={item.divider} />
if @_namespace?.usesLabels()
icon = @_renderCheckbox(categoryDatum)
icon = @_renderCheckbox(item)
else if @_namespace?.usesFolders()
icon = @_renderFolderIcon(categoryDatum)
icon = @_renderFolderIcon(item)
else return <span></span>
<div className="category-item">
{icon}
<div className="category-display-name">
{@_renderBoldedSearchResults(categoryDatum)}
{@_renderBoldedSearchResults(item)}
</div>
</div>
_renderCheckbox: (categoryDatum) ->
_renderCheckbox: (item) ->
styles = {}
styles.backgroundColor = categoryDatum.backgroundColor
styles.backgroundColor = item.backgroundColor
if categoryDatum.usage is 0
if item.usage is 0
checkStatus = <span></span>
else if categoryDatum.usage < categoryDatum.numThreads
else if item.usage < item.numThreads
checkStatus = <RetinaImg
className="check-img dash"
name="tagging-conflicted.png"
mode={RetinaImg.Mode.ContentPreserve}
onClick={=> @_onSelectCategory(categoryDatum)}/>
onClick={=> @_onSelectCategory(item)}/>
else
checkStatus = <RetinaImg
className="check-img check"
name="tagging-checkmark.png"
mode={RetinaImg.Mode.ContentPreserve}
onClick={=> @_onSelectCategory(categoryDatum)}/>
onClick={=> @_onSelectCategory(item)}/>
<div className="check-wrap" style={styles}>
<RetinaImg
className="check-img check"
name="tagging-checkbox.png"
mode={RetinaImg.Mode.ContentPreserve}
onClick={=> @_onSelectCategory(categoryDatum)}/>
onClick={=> @_onSelectCategory(item)}/>
{checkStatus}
</div>
_renderFolderIcon: (categoryDatum) ->
<RetinaImg name={"#{categoryDatum.name}.png"} fallback={'folder.png'} mode={RetinaImg.Mode.ContentIsMask} />
_renderFolderIcon: (item) ->
<RetinaImg name={"#{item.name}.png"} fallback={'folder.png'} mode={RetinaImg.Mode.ContentIsMask} />
_renderBoldedSearchResults: (categoryDatum) ->
name = categoryDatum.display_name
_renderBoldedSearchResults: (item) ->
name = item.display_name
searchTerm = (@state.searchValue ? "").trim()
return name if searchTerm.length is 0
@ -173,33 +170,35 @@ class CategoryPicker extends React.Component
else return part
return <span>{parts}</span>
_onSelectCategory: (categoryDatum) =>
_onSelectCategory: (item) =>
return unless @_threads().length > 0
return unless @_namespace
@refs.menu.setSelectedItem(null)
if @_namespace.usesLabels()
if categoryDatum.usage > 0
if item.usage > 0
task = new ChangeLabelsTask
labelsToRemove: [categoryDatum.id]
labelsToRemove: [item.category]
threadIds: @_threadIds()
else
task = new ChangeLabelsTask
labelsToAdd: [categoryDatum.id]
labelsToAdd: [item.category]
threadIds: @_threadIds()
Actions.queueTask(task)
else if @_namespace.usesFolders()
task = new ChangeFolderTask
folderOrId: categoryDatum.id
folderOrId: item.category
threadIds: @_threadIds()
if @props.thread
Actions.moveThread(@props.thread, task)
else if @props.items
Actions.moveThreads(@_threads(), task)
else throw new Error("Invalid organizationUnit")
else
throw new Error("Invalid organizationUnit")
@refs.popover.close()
TaskQueue.enqueue(task)
_onStoreChanged: =>
@setState @_recalculateState(@props)
@ -219,6 +218,7 @@ class CategoryPicker extends React.Component
numThreads = @_threads(props).length
if numThreads is 0
return {categoryData: [], searchValue}
@_namespace = NamespaceStore.current()
return unless @_namespace
@ -235,7 +235,7 @@ class CategoryPicker extends React.Component
categoryData = _.chain(categories)
.filter(_.partial(@_isUserFacing, allInInbox))
.filter(_.partial(@_isInSearch, searchValue))
.map(_.partial(@_extendCategoryWithDisplayData, displayData))
.map(_.partial(@_itemForCategory, displayData))
.value()
return {categoryData, searchValue}
@ -267,14 +267,15 @@ class CategoryPicker extends React.Component
inbox = CategoryStore.getStandardCategory("inbox")
return usageCount[inbox.id] is numThreads
_extendCategoryWithDisplayData: ({usageCount, numThreads}, category) ->
_itemForCategory: ({usageCount, numThreads}, category) ->
return category if category.divider
cat = category.toJSON()
usage = usageCount[cat.id] ? 0
cat.backgroundColor = LabelColorizer.backgroundColorDark(category)
cat.usage = usage
cat.numThreads = numThreads
return cat
item = category.toJSON()
item.category = category
item.backgroundColor = LabelColorizer.backgroundColorDark(category)
item.usage = usageCount[category.id] ? 0
item.numThreads = numThreads
item
_threadCategories: (thread) =>
if @_namespace.usesLabels()
@ -283,7 +284,7 @@ class CategoryPicker extends React.Component
return (thread.folders ? [])
else throw new Error("Invalid organizationUnit")
_threads: (props=@props) =>
_threads: (props = @props) =>
if props.items then return (props.items ? [])
else if props.thread then return [props.thread]
else return []

View file

@ -261,7 +261,6 @@ class ComposerView extends React.Component
tabIndex="108"
placeholder="Subject"
disabled={not @state.showsubject}
className="compose-field compose-subject"
value={@state.subject}
onChange={@_onChangeSubject}/>
</div>

View file

@ -134,7 +134,6 @@
.compose-subject-wrap {
position: relative;
z-index: 2;
padding: 11px @spacing-standard 11px 0;
margin: 0 23px;
border-bottom: 1px solid @border-color-divider;
flex-shrink:0;
@ -146,16 +145,15 @@
display: block;
}
input.compose-field {
input {
display: inline-block;
width: calc(~"100% - 61px");
padding: 0;
padding: 13px 0 9px 0;
margin: 0 0 0 5px;
min-width: 5em;
background-color: transparent;
border: none;
}
input.compose-field.compose-subject {
input {
&::-webkit-input-placeholder {
color: @text-color-very-subtle;
}

View file

@ -245,15 +245,12 @@ class MessageList extends React.Component
<MailLabel label={label} key={label.id} onRemove={ => @_onRemoveLabel(label) }/>
_renderReplyArea: =>
if @_hasReplyArea()
<div className="footer-reply-area-wrap" onClick={@_onClickReplyArea} key={Utils.generateTempId()}>
<div className="footer-reply-area">
<RetinaImg name="#{@_replyType()}-footer.png" mode={RetinaImg.Mode.ContentIsMask}/>
<span className="reply-text">Write a reply…</span>
</div>
<div className="footer-reply-area-wrap" onClick={@_onClickReplyArea} key='reply-area'>
<div className="footer-reply-area">
<RetinaImg name="#{@_replyType()}-footer.png" mode={RetinaImg.Mode.ContentIsMask}/>
<span className="reply-text">Write a reply…</span>
</div>
else
<div key={Utils.generateTempId()}></div>
</div>
_hasReplyArea: =>
not _.last(@state.messages)?.draft
@ -308,7 +305,8 @@ class MessageList extends React.Component
collapsed={collapsed}
isLastMsg={(messages.length - 1 is idx)} />
components.push @_renderReplyArea()
if @_hasReplyArea()
components.push @_renderReplyArea()
return components

View file

@ -129,7 +129,7 @@
margin: 0 auto;
padding: @message-spacing 0;
&:last-child {
padding-bottom: @message-spacing * 2;
padding-bottom: @spacing-double;
}
.message-item-white-wrap {
background: @background-primary;

View file

@ -12,10 +12,6 @@ NamespaceStore} = require 'nylas-exports'
class UndoRedoComponent extends React.Component
@displayName: 'UndoRedoComponent'
@propTypes:
task: React.PropTypes.object.isRequired
show: React.PropTypes.bool
constructor: (@props) ->
@state = @_getStateFromStores()
@_timeout = null

View file

@ -98,7 +98,10 @@ class RetinaImg extends React.Component
active: React.PropTypes.bool
resourcePath: React.PropTypes.string
render: ->
shouldComponentUpdate: (nextProps) =>
not _.isEqual(@props, nextProps)
render: =>
path = @props.url ? @_pathFor(@props.name) ? @_pathFor(@props.fallback) ? ''
pathIsRetina = path.indexOf('@2x') > 0
className = @props.className ? ''
@ -125,7 +128,7 @@ class RetinaImg extends React.Component
otherProps = _.omit(@props, _.keys(@constructor.propTypes))
<img className={className} src={path} style={style} {...otherProps} />
_pathFor: (name) ->
_pathFor: (name) =>
return null unless name and _.isString(name)
[basename, ext] = name.split('.')

View file

@ -17,7 +17,7 @@
margin: 0;
padding: 0;
border-bottom: 1px solid @border-color-divider;
min-height: 30px;
min-height: 42px;
position: relative;
.content-container {
@ -134,7 +134,7 @@
.tokenizing-field-input {
position: relative;
padding-left: 2.1em;
padding-left: 2.8em;
&:hover {
cursor: text;