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
This commit is contained in:
Ben Gotow 2015-06-08 17:02:50 -07:00
parent 1f589be4ad
commit 79f0405148
34 changed files with 158 additions and 46 deletions

View file

@ -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)

View file

@ -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;

View file

@ -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 {

View file

@ -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

View file

@ -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}

View 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

View file

@ -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')

View file

@ -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"

View file

@ -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;
}

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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.

View file

@ -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...) ->

View file

@ -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 {

View file

@ -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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

View file

@ -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);

View file

@ -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 {