feat(thread-actions): Hover actions on thread list, improved drawing performance

Summary: Adds hover actions to threads in the thread list, uses overflow:hidden to improve thread list render times

Test Plan: Run tests

Reviewers: evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D1621
This commit is contained in:
Ben Gotow 2015-06-11 18:38:57 -07:00
parent aa10ddfd1c
commit f7f1ab3605
8 changed files with 120 additions and 17 deletions

View file

@ -0,0 +1,45 @@
_ = require 'underscore'
React = require 'react'
{Actions,
Utils,
Thread,
AddRemoveTagsTask,
NamespaceStore} = require 'nylas-exports'
{RetinaImg} = require 'nylas-component-kit'
class ThreadListQuickActions extends React.Component
@displayName: 'ThreadListQuickActions'
@propTypes:
thread: React.PropTypes.object
render: =>
actions = []
actions.push <div className="action" onClick={@_onReply}><RetinaImg name="toolbar-reply.png" mode={RetinaImg.Mode.ContentPreserve} /></div>
actions.push <div className="action" onClick={@_onForward}><RetinaImg name="toolbar-forward.png" mode={RetinaImg.Mode.ContentPreserve} /></div>
if not @props.thread.hasTagId('archive')
actions.push <div className="action" onClick={@_onArchive}><RetinaImg name="toolbar-archive.png" mode={RetinaImg.Mode.ContentPreserve} /></div>
<div className="inner">
{actions}
</div>
shouldComponentUpdate: (newProps, newState) ->
newProps.thread.id isnt @props?.thread.id
_onForward: (event) =>
Actions.composeForward({thread: @props.thread, popout: true})
# Don't trigger the thread row click
event.stopPropagation()
_onReply: (event) =>
Actions.composeReply({thread: @props.thread, popout: true})
# Don't trigger the thread row click
event.stopPropagation()
_onArchive: (event) =>
Actions.queueTask(new AddRemoveTagsTask(@props.thread, ['archive'], ['inbox']))
# Don't trigger the thread row click
event.stopPropagation()
module.exports = ThreadListQuickActions

View file

@ -10,6 +10,7 @@ classNames = require 'classnames'
NamespaceStore} = require 'nylas-exports'
ThreadListParticipants = require './thread-list-participants'
ThreadListQuickActions = require './thread-list-quick-actions'
ThreadListStore = require './thread-list-store'
ThreadListIcon = require './thread-list-icon'
@ -92,7 +93,12 @@ class ThreadList extends React.Component
resolver: (thread) =>
<span className="timestamp">{timestamp(thread.lastMessageTimestamp)}</span>
@wideColumns = [c1, c2, c3, c4]
c5 = new ListTabular.Column
name: "HoverActions"
resolver: (thread) =>
<ThreadListQuickActions thread={thread}/>
@wideColumns = [c1, c2, c3, c4, c5]
cNarrow = new ListTabular.Column
name: "Item"

View file

@ -171,6 +171,48 @@
}
// quick actions
.thread-list .list-item .list-column:last-child {
display:none;
.action {
margin:8px;
display:inline-block;
}
.action:last-child {
margin-right:20px;
}
}
.thread-list .list-item:hover .list-column:last-child {
width: 0;
padding: 0;
display:block;
overflow: visible;
height:100%;
.inner {
position:relative;
width:300px;
height:100%;
left: -300px;
}
}
.thread-list .list-item:hover .list-column:last-child .inner {
background-image: -webkit-linear-gradient(left, fade(darken(@list-bg, 5%), 0%) 0%, darken(@list-bg, 5%) 50%, darken(@list-bg, 5%) 100%);
}
.thread-list .list-item.selected:hover .list-column:last-child .inner {
background-image: -webkit-linear-gradient(left, fade(@list-selected-bg, 0%) 0%, @list-selected-bg 50%, @list-selected-bg 100%);
}
.thread-list .list-item.focused:hover .list-column:last-child .inner {
background-image: -webkit-linear-gradient(left, fade(@list-focused-bg, 0%) 0%, @list-focused-bg 50%, @list-focused-bg 100%);
}
// stars
.thread-icon-star-on-hover:hover,
.thread-list .list-item:hover .thread-icon-star-on-hover:hover {
background-image:url(../static/images/thread-list/icon-star-@2x.png);

View file

@ -32,7 +32,7 @@ class ListTabularItem extends React.Component
className = "list-item list-tabular-item #{@props.itemProps?.className}"
props = _.omit(@props.itemProps ? {}, 'className')
<div {...props} className={className} onClick={@_onClick} style={position:'absolute', top: @props.metrics.top, width:'100%', height:@props.metrics.height}>
<div {...props} className={className} onClick={@_onClick} style={position:'absolute', top: @props.metrics.top, width:'100%', height:@props.metrics.height, overflow: 'hidden'}>
{@_columns()}
</div>

View file

@ -136,16 +136,20 @@ class ScrollRegion extends React.Component
@_onHandleMove(event)
_onScroll: (event) =>
@_recomputeDimensions()
@props.onScroll?(event)
if not @_requestedAnimationFrame
@_requestedAnimationFrame = true
window.requestAnimationFrame =>
@_requestedAnimationFrame = false
@_recomputeDimensions()
@props.onScroll?(event)
if not @state.scrolling
@setState(scrolling: true)
if not @state.scrolling
@setState(scrolling: true)
@_onStoppedScroll ?= _.debounce =>
@setState(scrolling: false)
, 250
@_onStoppedScroll()
@_onStoppedScroll ?= _.debounce =>
@setState(scrolling: false)
, 250
@_onStoppedScroll()
module.exports = ScrollRegion

View file

@ -205,7 +205,7 @@ class DraftStore
@_newMessageWithContext context, (thread, message) ->
forwardMessage: message
_newMessageWithContext: ({thread, threadId, message, messageId}, attributesCallback) =>
_newMessageWithContext: ({thread, threadId, message, messageId, popout}, attributesCallback) =>
return unless NamespaceStore.current()
# We accept all kinds of context. You can pass actual thread and message objects,
@ -227,7 +227,7 @@ class DraftStore
queries.message = DatabaseStore.find(Message, messageId)
queries.message.include(Message.attributes.body)
else
queries.message = DatabaseStore.findBy(Message, {threadId: threadId}).order(Message.attributes.date.descending()).limit(1)
queries.message = DatabaseStore.findBy(Message, {threadId: threadId ? thread.id}).order(Message.attributes.date.descending()).limit(1)
queries.message.include(Message.attributes.body)
# Waits for the query promises to resolve and then resolve with a hash
@ -304,7 +304,9 @@ class DraftStore
@_draftSessions[draftLocalId] = new DraftStoreProxy(draftLocalId, draft)
DatabaseStore.bindToLocalId(draft, draftLocalId)
DatabaseStore.persistModel(draft)
DatabaseStore.persistModel(draft).then =>
Actions.composePopoutDraft(draftLocalId) if popout
# Eventually we'll want a nicer solution for inline attachments
_formatBodyForQuoting: (body="") =>

View file

@ -13,6 +13,7 @@
transform: translate(-15px, 0);
position: relative;
white-space:nowrap;
pointer-events: none;
}
.scroll-tooltip:after, .scroll-tooltip:before {
left: 100%;
@ -75,10 +76,12 @@
border-radius:8px;
.tooltip {
opacity: 0;
display:none;
transition: opacity 0.3s;
top: 50%;
transform: translate(-100%, -50%);
position: absolute;
pointer-events: none;
}
}
}
@ -98,7 +101,8 @@
border:1px solid lighten(@gray, 20%);
.tooltip {
opacity: 1;
display:block;
}
}
}
}
}

View file

@ -334,11 +334,11 @@
//** Text color of active list items
@list-selected-color: inherit;
//** Background color of active list items
@list-selected-bg: fade(@component-active-color, 17%);
@list-selected-bg: mix(@component-active-color, @list-bg, 17%);
//** Border color of active list elements
@list-selected-border: fade(@component-active-color, 50%);
@list-selected-border: mix(@component-active-color, @list-bg, 50%);
//** Text color for content within active list items
@list-selected-color-muted: lighten(@list-selected-bg, 40%);
@list-selected-color-muted: lighten(@list-selected-bg, 40%);
//** Text color of disabled list items
@list-disabled-color: @gray-light;