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 3a947ccf54
commit e68a139f4d
8 changed files with 56 additions and 61 deletions

View file

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

View file

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

View file

@ -134,7 +134,6 @@
.compose-subject-wrap { .compose-subject-wrap {
position: relative; position: relative;
z-index: 2; z-index: 2;
padding: 11px @spacing-standard 11px 0;
margin: 0 23px; margin: 0 23px;
border-bottom: 1px solid @border-color-divider; border-bottom: 1px solid @border-color-divider;
flex-shrink:0; flex-shrink:0;
@ -146,16 +145,15 @@
display: block; display: block;
} }
input.compose-field { input {
display: inline-block; display: inline-block;
width: calc(~"100% - 61px"); padding: 13px 0 9px 0;
padding: 0;
margin: 0 0 0 5px; margin: 0 0 0 5px;
min-width: 5em; min-width: 5em;
background-color: transparent; background-color: transparent;
border: none; border: none;
} }
input.compose-field.compose-subject { input {
&::-webkit-input-placeholder { &::-webkit-input-placeholder {
color: @text-color-very-subtle; 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) }/> <MailLabel label={label} key={label.id} onRemove={ => @_onRemoveLabel(label) }/>
_renderReplyArea: => _renderReplyArea: =>
if @_hasReplyArea() <div className="footer-reply-area-wrap" onClick={@_onClickReplyArea} key='reply-area'>
<div className="footer-reply-area-wrap" onClick={@_onClickReplyArea} key={Utils.generateTempId()}>
<div className="footer-reply-area"> <div className="footer-reply-area">
<RetinaImg name="#{@_replyType()}-footer.png" mode={RetinaImg.Mode.ContentIsMask}/> <RetinaImg name="#{@_replyType()}-footer.png" mode={RetinaImg.Mode.ContentIsMask}/>
<span className="reply-text">Write a reply…</span> <span className="reply-text">Write a reply…</span>
</div> </div>
</div> </div>
else
<div key={Utils.generateTempId()}></div>
_hasReplyArea: => _hasReplyArea: =>
not _.last(@state.messages)?.draft not _.last(@state.messages)?.draft
@ -308,6 +305,7 @@ class MessageList extends React.Component
collapsed={collapsed} collapsed={collapsed}
isLastMsg={(messages.length - 1 is idx)} /> isLastMsg={(messages.length - 1 is idx)} />
if @_hasReplyArea()
components.push @_renderReplyArea() components.push @_renderReplyArea()
return components return components

View file

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

View file

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

View file

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

View file

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