feat(starring): Star and unstar threads in the thread list
Summary: When two or more buttons are grouped together, cut the padding off one interior edge so they're spaced more appropriately Remove source list graphics for active states we aren't using Starred in the sidebar Small fix to the feature that keeps the selected item visible as you scroll Test Plan: No new tests yet Reviewers: evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D1607
|
@ -75,7 +75,7 @@ AccountSidebarStore = Reflux.createStore
|
|||
# We ignore the trash tag because you can't trash anything
|
||||
tags = _.reject tags, (tag) -> tag.id is "trash"
|
||||
|
||||
mainTagIDs = ['inbox', 'drafts', 'sent', 'archive']
|
||||
mainTagIDs = ['inbox', 'starred', 'drafts', 'sent', 'archive']
|
||||
mainTags = _.filter tags, (tag) -> _.contains(mainTagIDs, tag.id)
|
||||
userTags = _.reject tags, (tag) -> _.contains(mainTagIDs, tag.id)
|
||||
|
||||
|
|
|
@ -20,7 +20,10 @@
|
|||
|
||||
.item {
|
||||
color: @text-color-subtle;
|
||||
img.content-mask { background-color: @text-color-subtle; }
|
||||
img.content-mask {
|
||||
background-color: @text-color-subtle;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
font-size: @font-size-small;
|
||||
font-weight: 400;
|
||||
padding: 0 @spacing-standard;
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
z-index: 1;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border-top:1px solid @border-color-divider;
|
||||
border-bottom: 0;
|
||||
|
||||
.composer-action-bar-content {
|
||||
|
|
|
@ -2,7 +2,7 @@ _ = require 'underscore'
|
|||
React = require "react"
|
||||
{ComponentRegistry, WorkspaceStore} = require "nylas-exports"
|
||||
|
||||
{DownButton, UpButton, ThreadBulkArchiveButton} = require "./thread-buttons"
|
||||
{DownButton, UpButton, ThreadBulkArchiveButton, ThreadBulkStarButton} = require "./thread-buttons"
|
||||
ThreadSelectionBar = require './thread-selection-bar'
|
||||
ThreadList = require './thread-list'
|
||||
|
||||
|
@ -34,6 +34,9 @@ module.exports =
|
|||
ComponentRegistry.register ThreadBulkArchiveButton,
|
||||
role: 'thread:BulkAction'
|
||||
|
||||
ComponentRegistry.register ThreadBulkStarButton,
|
||||
role: 'thread:BulkAction'
|
||||
|
||||
deactivate: ->
|
||||
ComponentRegistry.unregister DraftList
|
||||
ComponentRegistry.unregister DraftSelectionBar
|
||||
|
|
|
@ -23,6 +23,25 @@ class ThreadBulkArchiveButton extends React.Component
|
|||
Actions.archiveSelection()
|
||||
|
||||
|
||||
class ThreadBulkStarButton extends React.Component
|
||||
@displayName: 'ThreadBulkStarButton'
|
||||
@containerRequired: false
|
||||
|
||||
@propTypes:
|
||||
selection: React.PropTypes.object.isRequired
|
||||
|
||||
render: ->
|
||||
<button style={order:-100}
|
||||
className="btn btn-toolbar"
|
||||
data-tooltip="Star"
|
||||
onClick={@_onStar}>
|
||||
<RetinaImg name="toolbar-star.png" mode={RetinaImg.Mode.ContentIsMask} />
|
||||
</button>
|
||||
|
||||
_onStar: =>
|
||||
Actions.toggleStarSelection()
|
||||
|
||||
|
||||
ThreadNavButtonMixin =
|
||||
getInitialState: ->
|
||||
@_getStateFromStores()
|
||||
|
@ -98,4 +117,4 @@ UpButton = React.createClass
|
|||
UpButton.containerRequired = false
|
||||
DownButton.containerRequired = false
|
||||
|
||||
module.exports = {DownButton, UpButton, ThreadBulkArchiveButton}
|
||||
module.exports = {DownButton, UpButton, ThreadBulkArchiveButton, ThreadBulkStarButton}
|
||||
|
|
48
internal_packages/thread-list/lib/thread-list-icon.cjsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
_ = require 'underscore'
|
||||
React = require 'react'
|
||||
{Actions,
|
||||
Utils,
|
||||
Thread,
|
||||
AddRemoveTagsTask,
|
||||
NamespaceStore} = require 'nylas-exports'
|
||||
|
||||
class ThreadListIcon extends React.Component
|
||||
@displayName: 'ThreadListIcon'
|
||||
@propTypes:
|
||||
thread: React.PropTypes.object
|
||||
|
||||
_iconType: =>
|
||||
myEmail = NamespaceStore.current()?.emailAddress
|
||||
|
||||
msgs = @props.thread.metadata
|
||||
return '' unless msgs and msgs instanceof Array
|
||||
|
||||
msgs = _.filter msgs, (m) -> m.isSaved() and not m.draft
|
||||
msg = msgs[msgs.length - 1]
|
||||
return '' unless msgs.length > 0
|
||||
|
||||
if @props.thread.hasTagId('starred')
|
||||
return 'thread-icon-star'
|
||||
else if @props.thread.unread
|
||||
return 'thread-icon-unread thread-icon-star-on-hover'
|
||||
else if msg.from[0]?.email isnt myEmail or msgs.length is 1
|
||||
return 'thread-icon-star-on-hover'
|
||||
else if Utils.isForwardedMessage(msg)
|
||||
return 'thread-icon-forwarded thread-icon-star-on-hover'
|
||||
else
|
||||
return 'thread-icon-replied thread-icon-star-on-hover'
|
||||
|
||||
render: =>
|
||||
<div className="thread-icon #{@_iconType()}" onClick={@_onToggleStar}></div>
|
||||
|
||||
_onToggleStar: (event) =>
|
||||
if @props.thread.hasTagId('starred')
|
||||
star = new AddRemoveTagsTask(@props.thread, [], ['starred'])
|
||||
else
|
||||
star = new AddRemoveTagsTask(@props.thread, ['starred'], [])
|
||||
Actions.queueTask(star)
|
||||
|
||||
# Don't trigger the thread row click
|
||||
event.stopPropagation()
|
||||
|
||||
module.exports = ThreadListIcon
|
|
@ -31,6 +31,8 @@ ThreadListStore = Reflux.createStore
|
|||
@listenTo Actions.archiveSelection, @_onArchiveSelection
|
||||
@listenTo Actions.archive, @_onArchive
|
||||
|
||||
@listenTo Actions.toggleStarSelection, @_onToggleStarSelection
|
||||
|
||||
@listenTo DatabaseStore, @_onDataChanged
|
||||
@listenTo FocusedTagStore, @_onTagChanged
|
||||
@listenTo NamespaceStore, @_onNamespaceChanged
|
||||
|
@ -97,6 +99,23 @@ ThreadListStore = Reflux.createStore
|
|||
threadIds = _.uniq _.map change.objects, (m) -> m.threadId
|
||||
@_view.invalidateMetadataFor(threadIds)
|
||||
|
||||
_onToggleStarSelection: ->
|
||||
selected = @_view.selection.items()
|
||||
focusedId = FocusedContentStore.focusedId('thread')
|
||||
keyboardId = FocusedContentStore.keyboardCursorId('thread')
|
||||
|
||||
oneAlreadyStarred = false
|
||||
for thread in selected
|
||||
if thread.hasTagId('starred')
|
||||
oneAlreadyStarred = true
|
||||
|
||||
for thread in selected
|
||||
if oneAlreadyStarred
|
||||
task = new AddRemoveTagsTask(thread, [], ['starred'])
|
||||
else
|
||||
task = new AddRemoveTagsTask(thread, ['starred'], [])
|
||||
Actions.queueTask(task)
|
||||
|
||||
_onArchive: ->
|
||||
@_archiveAndShiftBy('auto')
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ classNames = require 'classnames'
|
|||
|
||||
ThreadListParticipants = require './thread-list-participants'
|
||||
ThreadListStore = require './thread-list-store'
|
||||
ThreadListIcon = require './thread-list-icon'
|
||||
|
||||
class ThreadListScrollTooltip extends React.Component
|
||||
@displayName: 'ThreadListScrollTooltip'
|
||||
|
@ -49,29 +50,10 @@ class ThreadList extends React.Component
|
|||
LabelComponent = label.view
|
||||
<LabelComponent thread={thread} />
|
||||
|
||||
lastMessageType = (thread) ->
|
||||
myEmail = NamespaceStore.current()?.emailAddress
|
||||
|
||||
msgs = thread.metadata
|
||||
return 'unknown' unless msgs and msgs instanceof Array
|
||||
|
||||
msgs = _.filter msgs, (m) -> m.isSaved() and not m.draft
|
||||
msg = msgs[msgs.length - 1]
|
||||
return 'unknown' unless msgs.length > 0
|
||||
|
||||
if thread.unread
|
||||
return 'unread'
|
||||
else if msg.from[0]?.email isnt myEmail or msgs.length is 1
|
||||
return 'other'
|
||||
else if Utils.isForwardedMessage(msg)
|
||||
return 'forwarded'
|
||||
else
|
||||
return 'replied'
|
||||
|
||||
c1 = new ListTabular.Column
|
||||
name: "★"
|
||||
resolver: (thread) =>
|
||||
<div className="thread-icon thread-icon-#{lastMessageType(thread)}"></div>
|
||||
<ThreadListIcon thread={thread} />
|
||||
|
||||
c2 = new ListTabular.Column
|
||||
name: "Name"
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
|
||||
.list-item {
|
||||
background-color: darken(@background-primary, 3%);
|
||||
background-color: darken(@background-primary, 2%);
|
||||
}
|
||||
|
||||
.list-column {
|
||||
|
@ -128,14 +128,12 @@
|
|||
}
|
||||
|
||||
.thread-icon {
|
||||
display:inline-block;
|
||||
width:15px;
|
||||
height:15px;
|
||||
background-size: 100%;
|
||||
background-size: 15px;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
top: 5px;
|
||||
vertical-align: top;
|
||||
background-position:center;
|
||||
padding:12px;
|
||||
}
|
||||
.thread-icon-attachment {
|
||||
background-image:url(../static/images/thread-list/icon-attachment-@2x.png);
|
||||
|
@ -149,7 +147,10 @@
|
|||
.thread-icon-forwarded {
|
||||
background-image:url(../static/images/thread-list/icon-forwarded-@2x.png);
|
||||
}
|
||||
|
||||
.thread-icon-star {
|
||||
background-size: 16px;
|
||||
background-image:url(../static/images/thread-list/icon-star-@2x.png);
|
||||
}
|
||||
.star-button {
|
||||
font-size: 16px;
|
||||
.fa-star {
|
||||
|
@ -168,3 +169,14 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.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-size: 16px;
|
||||
}
|
||||
.thread-list .list-item:hover .thread-icon-star-on-hover {
|
||||
background-image:url(../static/images/thread-list/icon-star-hover-@2x.png);
|
||||
background-size: 16px;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
_ = require 'underscore'
|
||||
React = require 'react/addons'
|
||||
ScrollRegion = require './scroll-region'
|
||||
{Utils} = require 'nylas-exports'
|
||||
|
||||
RangeChunkSize = 10
|
||||
|
||||
|
@ -203,6 +204,12 @@ class ListTabular extends React.Component
|
|||
onDoubleClick={@props.onDoubleClick} />
|
||||
rows
|
||||
|
||||
# Public: Scroll to the DOM node provided.
|
||||
#
|
||||
scrollTo: (node) =>
|
||||
@refs.container.scrollTo(node)
|
||||
|
||||
|
||||
ListTabular.Item = ListTabularItem
|
||||
ListTabular.Column = ListColumn
|
||||
|
||||
|
|
|
@ -184,7 +184,9 @@ class Menu extends React.Component
|
|||
componentDidUpdate: =>
|
||||
item = React.findDOMNode(@).querySelector(".selected")
|
||||
container = React.findDOMNode(@).querySelector(".content-container")
|
||||
Utils.scrollNodeToVisibleInContainer(item, container)
|
||||
adjustment = Utils.scrollAdjustmentToMakeNodeVisibleInContainer(item, container)
|
||||
if adjustment isnt 0
|
||||
container.scrollTop += adjustment
|
||||
|
||||
componentWillUnmount: =>
|
||||
@subscriptions?.dispose()
|
||||
|
|
|
@ -52,8 +52,7 @@ class MultiselectList extends React.Component
|
|||
|
||||
item = React.findDOMNode(@).querySelector(".focused")
|
||||
item ?= React.findDOMNode(@).querySelector(".keyboard-cursor")
|
||||
list = React.findDOMNode(@refs.list)
|
||||
Utils.scrollNodeToVisibleInContainer(item, list)
|
||||
@refs.list.scrollTo(item)
|
||||
|
||||
componentWillUnmount: =>
|
||||
@teardownForProps()
|
||||
|
|
|
@ -65,6 +65,13 @@ class ScrollRegion extends React.Component
|
|||
</div>
|
||||
</div>
|
||||
|
||||
# Public: Scroll to the DOM Node provided.
|
||||
#
|
||||
scrollTo: (node) =>
|
||||
container = React.findDOMNode(@)
|
||||
adjustment = Utils.scrollAdjustmentToMakeNodeVisibleInContainer(node, container)
|
||||
@scrollTop += adjustment if adjustment isnt 0
|
||||
|
||||
_scrollbarWrapStyles: =>
|
||||
position:'absolute'
|
||||
top: 0
|
||||
|
|
|
@ -317,6 +317,7 @@ class Actions
|
|||
@archiveSelection: ActionScopeWindow
|
||||
@archiveAndNext: ActionScopeWindow
|
||||
@archiveAndPrevious: ActionScopeWindow
|
||||
@toggleStarSelection: ActionScopeWindow
|
||||
|
||||
###
|
||||
Public: Updates the search query in the app's main search bar with the provided query text.
|
||||
|
|
|
@ -268,7 +268,7 @@ Utils =
|
|||
return false
|
||||
return true
|
||||
|
||||
scrollNodeToVisibleInContainer: (node, container) ->
|
||||
scrollAdjustmentToMakeNodeVisibleInContainer: (node, container) ->
|
||||
return unless node
|
||||
|
||||
nodeRect = node.getBoundingClientRect()
|
||||
|
@ -276,11 +276,13 @@ Utils =
|
|||
|
||||
distanceBelowBottom = (nodeRect.top + nodeRect.height) - (containerRect.top + containerRect.height)
|
||||
if distanceBelowBottom > 0
|
||||
container.scrollTop += distanceBelowBottom
|
||||
return distanceBelowBottom
|
||||
|
||||
distanceAboveTop = containerRect.top - nodeRect.top
|
||||
if distanceAboveTop > 0
|
||||
container.scrollTop -= distanceAboveTop
|
||||
return -distanceAboveTop
|
||||
|
||||
return 0
|
||||
|
||||
# True of all arguments have the same domains
|
||||
emailsHaveSameDomain: (args...) ->
|
||||
|
|
|
@ -11,8 +11,9 @@ button, html input[type="button"] {
|
|||
.btn {
|
||||
border: 0;
|
||||
padding: 0.33em 1em;
|
||||
border:1px solid rgba(0,0,0,0.18);
|
||||
border-radius: @border-radius-base;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.21);
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.10);
|
||||
cursor: default;
|
||||
display:inline-block;
|
||||
|
||||
|
@ -76,7 +77,7 @@ button, html input[type="button"] {
|
|||
}
|
||||
|
||||
.btn-toolbar {
|
||||
min-height:36px;
|
||||
min-height:34px;
|
||||
}
|
||||
|
||||
.btn-gradient {
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
height:49px;
|
||||
border-left:1px solid @border-color-divider;
|
||||
border-right:1px solid @border-color-divider;
|
||||
background-color: @gray-lighter;
|
||||
background-color: @background-primary;
|
||||
opacity:0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
pointer-events: none;
|
||||
|
|
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 371 B |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 15 KiB |
BIN
static/images/thread-list/icon-star-@2x.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
static/images/thread-list/icon-star-hover-@2x.png
Normal file
After Width: | Height: | Size: 810 B |
BIN
static/images/toolbar/toolbar-star@2x.png
Normal file
After Width: | Height: | Size: 246 KiB |
|
@ -44,7 +44,7 @@
|
|||
@accent-secondary: @nylas-yellow;
|
||||
|
||||
@background-primary: #ffffff;
|
||||
@background-off-primary: #fbfbfb;
|
||||
@background-off-primary: #fdfdfd;
|
||||
@background-secondary: #f6f6f6;
|
||||
@background-tertiary: #6d7987;
|
||||
|
||||
|
@ -302,11 +302,11 @@
|
|||
// ##
|
||||
|
||||
//** Background color on `.list-group-item`
|
||||
@source-list-bg: @background-primary;
|
||||
@source-list-bg: @panel-background-color;
|
||||
//** Background color of active list items
|
||||
@source-list-active-bg: @panel-background-color;
|
||||
//** Text color of active list items
|
||||
@source-list-active-color: @component-active-color;
|
||||
//** Background color of active list items
|
||||
@source-list-active-bg: @component-active-bg;
|
||||
|
||||
//== List
|
||||
//
|
||||
|
@ -471,7 +471,7 @@
|
|||
@body-bg: @white;
|
||||
//== Panels and Sidebars
|
||||
@panel-background-color: @gray-lighter;
|
||||
@toolbar-background-color: @white;
|
||||
@toolbar-background-color: @gray-lighter;
|
||||
|
||||
// Helpers for Specs - Do Not Remove
|
||||
@spec-test-variable: rgb(152,123,0);
|
||||
|
|
|
@ -159,12 +159,18 @@ body.is-blurred {
|
|||
}
|
||||
|
||||
.btn-toolbar {
|
||||
margin-top: @spacing-half * 0.94;
|
||||
margin-top: @spacing-half;
|
||||
margin-left: @spacing-three-quarters;
|
||||
margin-right: @spacing-three-quarters;
|
||||
flex-shrink: 0;
|
||||
height:32px;
|
||||
}
|
||||
.btn-toolbar:last-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
.btn-toolbar:only-child {
|
||||
margin-left: @spacing-three-quarters;
|
||||
}
|
||||
}
|
||||
|
||||
.sheet-toolbar-enter {
|
||||
|
|