mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-10-06 03:14:39 +08:00
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:
parent
aa10ddfd1c
commit
f7f1ab3605
8 changed files with 120 additions and 17 deletions
|
@ -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
|
|
@ -10,6 +10,7 @@ classNames = require 'classnames'
|
||||||
NamespaceStore} = require 'nylas-exports'
|
NamespaceStore} = require 'nylas-exports'
|
||||||
|
|
||||||
ThreadListParticipants = require './thread-list-participants'
|
ThreadListParticipants = require './thread-list-participants'
|
||||||
|
ThreadListQuickActions = require './thread-list-quick-actions'
|
||||||
ThreadListStore = require './thread-list-store'
|
ThreadListStore = require './thread-list-store'
|
||||||
ThreadListIcon = require './thread-list-icon'
|
ThreadListIcon = require './thread-list-icon'
|
||||||
|
|
||||||
|
@ -92,7 +93,12 @@ class ThreadList extends React.Component
|
||||||
resolver: (thread) =>
|
resolver: (thread) =>
|
||||||
<span className="timestamp">{timestamp(thread.lastMessageTimestamp)}</span>
|
<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
|
cNarrow = new ListTabular.Column
|
||||||
name: "Item"
|
name: "Item"
|
||||||
|
|
|
@ -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-icon-star-on-hover:hover,
|
||||||
.thread-list .list-item:hover .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);
|
background-image:url(../static/images/thread-list/icon-star-@2x.png);
|
||||||
|
|
|
@ -32,7 +32,7 @@ class ListTabularItem extends React.Component
|
||||||
className = "list-item list-tabular-item #{@props.itemProps?.className}"
|
className = "list-item list-tabular-item #{@props.itemProps?.className}"
|
||||||
props = _.omit(@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()}
|
{@_columns()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -136,16 +136,20 @@ class ScrollRegion extends React.Component
|
||||||
@_onHandleMove(event)
|
@_onHandleMove(event)
|
||||||
|
|
||||||
_onScroll: (event) =>
|
_onScroll: (event) =>
|
||||||
@_recomputeDimensions()
|
if not @_requestedAnimationFrame
|
||||||
@props.onScroll?(event)
|
@_requestedAnimationFrame = true
|
||||||
|
window.requestAnimationFrame =>
|
||||||
|
@_requestedAnimationFrame = false
|
||||||
|
@_recomputeDimensions()
|
||||||
|
@props.onScroll?(event)
|
||||||
|
|
||||||
if not @state.scrolling
|
if not @state.scrolling
|
||||||
@setState(scrolling: true)
|
@setState(scrolling: true)
|
||||||
|
|
||||||
@_onStoppedScroll ?= _.debounce =>
|
@_onStoppedScroll ?= _.debounce =>
|
||||||
@setState(scrolling: false)
|
@setState(scrolling: false)
|
||||||
, 250
|
, 250
|
||||||
@_onStoppedScroll()
|
@_onStoppedScroll()
|
||||||
|
|
||||||
|
|
||||||
module.exports = ScrollRegion
|
module.exports = ScrollRegion
|
||||||
|
|
|
@ -205,7 +205,7 @@ class DraftStore
|
||||||
@_newMessageWithContext context, (thread, message) ->
|
@_newMessageWithContext context, (thread, message) ->
|
||||||
forwardMessage: message
|
forwardMessage: message
|
||||||
|
|
||||||
_newMessageWithContext: ({thread, threadId, message, messageId}, attributesCallback) =>
|
_newMessageWithContext: ({thread, threadId, message, messageId, popout}, attributesCallback) =>
|
||||||
return unless NamespaceStore.current()
|
return unless NamespaceStore.current()
|
||||||
|
|
||||||
# We accept all kinds of context. You can pass actual thread and message objects,
|
# 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 = DatabaseStore.find(Message, messageId)
|
||||||
queries.message.include(Message.attributes.body)
|
queries.message.include(Message.attributes.body)
|
||||||
else
|
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)
|
queries.message.include(Message.attributes.body)
|
||||||
|
|
||||||
# Waits for the query promises to resolve and then resolve with a hash
|
# 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)
|
@_draftSessions[draftLocalId] = new DraftStoreProxy(draftLocalId, draft)
|
||||||
|
|
||||||
DatabaseStore.bindToLocalId(draft, draftLocalId)
|
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
|
# Eventually we'll want a nicer solution for inline attachments
|
||||||
_formatBodyForQuoting: (body="") =>
|
_formatBodyForQuoting: (body="") =>
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
transform: translate(-15px, 0);
|
transform: translate(-15px, 0);
|
||||||
position: relative;
|
position: relative;
|
||||||
white-space:nowrap;
|
white-space:nowrap;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.scroll-tooltip:after, .scroll-tooltip:before {
|
.scroll-tooltip:after, .scroll-tooltip:before {
|
||||||
left: 100%;
|
left: 100%;
|
||||||
|
@ -75,10 +76,12 @@
|
||||||
border-radius:8px;
|
border-radius:8px;
|
||||||
.tooltip {
|
.tooltip {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
display:none;
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(-100%, -50%);
|
transform: translate(-100%, -50%);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,7 +101,8 @@
|
||||||
border:1px solid lighten(@gray, 20%);
|
border:1px solid lighten(@gray, 20%);
|
||||||
.tooltip {
|
.tooltip {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
display:block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -334,11 +334,11 @@
|
||||||
//** Text color of active list items
|
//** Text color of active list items
|
||||||
@list-selected-color: inherit;
|
@list-selected-color: inherit;
|
||||||
//** Background color of active list items
|
//** 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
|
//** 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
|
//** 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
|
//** Text color of disabled list items
|
||||||
@list-disabled-color: @gray-light;
|
@list-disabled-color: @gray-light;
|
||||||
|
|
Loading…
Add table
Reference in a new issue