feat(hidden-messages): Filter trash/spam messages. Fixes

Summary:
By default, the messages in a thread are now filtered to exclude
ones moved to trash or spam. You can choose to view those messages by clicking
the new bar in the message list.

When you view your spam or trash, we only show the messages on those threads
that have been marked as spam/trash.

Test Plan: Run a couple new tests

Reviewers: juan, evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D2662
This commit is contained in:
Ben Gotow 2016-03-02 10:05:17 -08:00
parent a950b40175
commit f413386b80
16 changed files with 240 additions and 69 deletions

View file

@ -17,6 +17,7 @@
"object-curly-spacing": [0],
"no-console": [0],
"no-loop-func": [0],
"no-constant-condition": [0],
"new-cap": [2, {"capIsNew": false}],
"no-shadow": [1],
"quotes": [0],

View file

@ -1,4 +1,5 @@
MessageList = require "./message-list"
MessageListHiddenMessagesToggle = require './message-list-hidden-messages-toggle'
MessageToolbarItems = require "./message-toolbar-items"
{ComponentRegistry,
ExtensionRegistry,
@ -46,6 +47,9 @@ module.exports =
ComponentRegistry.register ThreadToggleUnreadButton,
role: 'message:Toolbar'
ComponentRegistry.register MessageListHiddenMessagesToggle,
role: 'MessageListHeaders'
ExtensionRegistry.MessageView.register AutolinkerExtension
ExtensionRegistry.MessageView.register TrackingPixelsExtension

View file

@ -0,0 +1,64 @@
import {
React,
Actions,
MessageStore,
FocusedPerspectiveStore,
} from 'nylas-exports';
export default class MessageListHiddenMessagesToggle extends React.Component {
static displayName = 'MessageListHiddenMessagesToggle';
constructor() {
super();
this.state = {
numberOfHiddenItems: MessageStore.numberOfHiddenItems(),
};
}
componentDidMount() {
this._unlisten = MessageStore.listen(() => {
this.setState({
numberOfHiddenItems: MessageStore.numberOfHiddenItems(),
});
});
}
componentWillUnmount() {
this._unlisten();
}
render() {
const {numberOfHiddenItems} = this.state;
if (numberOfHiddenItems === 0) {
return false;
}
const viewing = FocusedPerspectiveStore.current().categoriesSharedName();
let message = null;
if (MessageStore.CategoryNamesHiddenByDefault.includes(viewing)) {
if (numberOfHiddenItems > 1) {
message = `There are ${numberOfHiddenItems} more messages in this thread that are not in spam or trash.`;
} else {
message = `There is one more message in this thread that is not in spam or trash.`;
}
} else {
if (numberOfHiddenItems > 1) {
message = `${numberOfHiddenItems} messages in this thread are hidden because it was moved to trash or spam.`;
} else {
message = `One message in this thread is hidden because it was moved to trash or spam.`;
}
}
return (
<div className="show-hidden-messages">
{message}
<a onClick={function toggle() { Actions.toggleHiddenMessages() }}>Show all messages</a>
</div>
);
}
}
MessageListHiddenMessagesToggle.containerRequired = false;

View file

@ -17,7 +17,7 @@ MessageItemContainer = require './message-item-container'
{Spinner,
RetinaImg,
MailLabel,
MailLabelSet,
ScrollRegion,
MailImportantIcon,
InjectedComponent,
@ -218,7 +218,7 @@ class MessageList extends React.Component
<MailImportantIcon thread={@state.currentThread}/>
<div style={flex: 1}>
<span className="message-subject">{subject}</span>
{@_renderLabels()}
<MailLabelSet thread={@state.currentThread} includeCurrentCategories={true} />
</div>
{@_renderIcons()}
</div>
@ -243,14 +243,6 @@ class MessageList extends React.Component
<RetinaImg name={"collapse.png"} title={"Collapse All"} mode={RetinaImg.Mode.ContentIsMask}/>
</div>
_renderLabels: =>
account = AccountStore.accountForId(@state.currentThread.accountId)
return false unless account.usesLabels()
labels = @state.currentThread.sortedCategories()
labels = _.reject labels, (l) -> l.name is 'important'
labels.map (label) =>
<MailLabel label={label} key={label.id} onRemove={ => @_onRemoveLabel(label) }/>
_renderReplyArea: =>
<div className="footer-reply-area-wrap" onClick={@_onClickReplyArea} key='reply-area'>
<div className="footer-reply-area">
@ -280,10 +272,6 @@ class MessageList extends React.Component
node = React.findDOMNode(@)
Actions.printThread(@state.currentThread, node.innerHTML)
_onRemoveLabel: (label) =>
task = new ChangeLabelsTask(thread: @state.currentThread, labelsToRemove: [label])
Actions.queueTask(task)
_onClickReplyArea: =>
return unless @state.currentThread
@_createReplyOrUpdateExistingDraft(@_replyType())

View file

@ -120,6 +120,17 @@ body.platform-win32 {
padding: 0;
order: 2;
.show-hidden-messages {
background-color: darken(@background-secondary, 4%);
border: 1px solid darken(@background-secondary, 8%);
border-radius: @border-radius-base;
color: @text-color-very-subtle;
margin-bottom: @padding-large-vertical;
cursor: default;
padding: @padding-base-vertical @padding-base-horizontal;
a { float: right; }
}
.message-subject-wrap {
width: calc(~"100% - 12px");
max-width: @message-max-width;
@ -155,6 +166,9 @@ body.platform-win32 {
margin-left: @padding-small-horizontal;
}
}
.thread-injected-mail-labels {
vertical-align: top;
}
.message-list-headers {
margin: 0 auto;
width: 100%;

View file

@ -4,14 +4,11 @@ classNames = require 'classnames'
{ListTabular,
RetinaImg,
MailLabel,
MailLabelSet,
MailImportantIcon,
InjectedComponentSet} = require 'nylas-component-kit'
{Thread,
AccountStore,
CategoryStore,
FocusedPerspectiveStore} = require 'nylas-exports'
{Thread} = require 'nylas-exports'
{ThreadArchiveQuickAction,
ThreadTrashQuickAction} = require './thread-list-quick-actions'
@ -57,36 +54,16 @@ c2 = new ListTabular.Column
else
<ThreadListParticipants thread={thread} />
c3LabelComponentCache = {}
c3 = new ListTabular.Column
name: "Message"
flex: 4
resolver: (thread) =>
attachment = []
labels = []
if thread.hasAttachments
attachment = <div className="thread-icon thread-icon-attachment"></div>
if AccountStore.accountForId(thread.accountId).usesLabels()
currentCategories = FocusedPerspectiveStore.current().categories() ? []
ignored = [].concat(currentCategories, CategoryStore.hiddenCategories(thread.accountId))
ignoredIds = _.pluck(ignored, 'id')
for label in (thread.sortedCategories())
continue if label.id in ignoredIds
c3LabelComponentCache[label.id] ?= <MailLabel label={label} key={label.id} />
labels.push c3LabelComponentCache[label.id]
<span className="details">
<InjectedComponentSet
inline
containersRequired={false}
children={labels}
matching={role: "ThreadList:Label"}
className="thread-injected-mail-labels"
exposedProps={thread: thread}/>
<MailLabelSet thread={thread} />
<span className="subject">{subject(thread.subject)}</span>
<span className="snippet">{thread.snippet}</span>
{attachment}
@ -127,17 +104,6 @@ cNarrow = new ListTabular.Column
if hasDraft
pencil = <RetinaImg name="icon-draft-pencil.png" className="draft-icon" mode={RetinaImg.Mode.ContentPreserve} />
labels = []
if AccountStore.accountForId(thread.accountId).usesLabels()
currentCategories = FocusedPerspectiveStore.current().categories() ? []
ignored = [].concat(currentCategories, CategoryStore.hiddenCategories(thread.accountId))
ignoredIds = _.pluck(ignored, 'id')
for label in (thread.sortedCategories())
continue if label.id in ignoredIds
c3LabelComponentCache[label.id] ?= <MailLabel label={label} key={label.id} />
labels.push c3LabelComponentCache[label.id]
<div>
<div style={display: 'flex', alignItems: 'center'}>
<ThreadListIcon thread={thread} />
@ -154,13 +120,7 @@ cNarrow = new ListTabular.Column
<div className="snippet-and-labels">
<div className="snippet">{thread.snippet}&nbsp;</div>
<div style={flex: 1, flexShrink: 1}></div>
<InjectedComponentSet
inline
containerRequired={false}
children={labels}
matching={role: "ThreadList:Label"}
className="thread-injected-mail-labels-narrow"
exposedProps={thread: thread}/>
<MailLabelSet thread={thread} />
</div>
</div>

View file

@ -446,6 +446,12 @@ body.platform-win32 {
padding: 1px 8px;
font-size: 0.8em;
line-height: 17px;
.inner {
position: inherit;
}
.x {
display: none;
}
}
.snippet {
font-size: @font-size-small;

View file

@ -11,7 +11,7 @@ export function activate() {
ComponentRegistry.register(ToolbarSnooze, {role: 'message:Toolbar'});
ComponentRegistry.register(SnoozeQuickActionButton, {role: 'ThreadListQuickAction'});
ComponentRegistry.register(BulkThreadSnooze, {role: 'thread:BulkAction'});
ComponentRegistry.register(SnoozeMailLabel, {role: 'ThreadList:Label'});
ComponentRegistry.register(SnoozeMailLabel, {role: 'Thread:MailLabel'});
}
export function deactivate() {

View file

@ -37,7 +37,6 @@ describe 'MailboxPerspective', ->
@perspective = MailboxPerspective.forCategories(@categories)
describe 'canReceiveThreads', ->
it 'returns true if the thread account ids are included in the current account ids', ->
expect(@perspective.canReceiveThreads(['a2'])).toBe true
@ -52,6 +51,22 @@ describe 'MailboxPerspective', ->
)
expect(@perspective.canReceiveThreads(['a2'])).toBe false
describe 'categoriesSharedName', ->
it "returns the name if all the categories on the perspective have the same name", ->
expect(MailboxPerspective.forCategories([
new Category(name: 'c1', accountId: 'a1')
new Category(name: 'c1', accountId: 'a2')
]).categoriesSharedName()).toEqual('c1')
it "returns null if there are no categories", ->
expect(MailboxPerspective.forStarred(['a1', 'a2']).categoriesSharedName()).toEqual(null)
it "returns null if the categories have different names", ->
expect(MailboxPerspective.forCategories([
new Category(name: 'c1', accountId: 'a1')
new Category(name: 'c2', accountId: 'a2')
]).categoriesSharedName()).toEqual(null)
describe 'receiveThreads', ->
# TODO

View file

@ -0,0 +1,68 @@
import React from 'react';
import FocusedPerspectiveStore from '../flux/stores/focused-perspective-store';
import CategoryStore from '../flux/stores/category-store';
import MessageStore from '../flux/stores/message-store';
import AccountStore from '../flux/stores/account-store';
import {MailLabel} from './mail-label';
import Actions from '../flux/actions';
import ChangeLabelsTask from '../flux/tasks/change-labels-task';
import InjectedComponentSet from './injected-component-set';
const LabelComponentCache = {};
export default class MailLabelSet extends React.Component {
static displayName = 'MailLabelSet';
static propTypes = {
thread: React.PropTypes.object.isRequired,
includeCurrentCategories: React.PropTypes.boolean,
};
_onRemoveLabel(label) {
const task = new ChangeLabelsTask({
thread: this.props.thread,
labelsToRemove: [label],
});
Actions.queueTask(task);
}
render() {
const {thread, includeCurrentCategories} = this.props;
const labels = [];
if (AccountStore.accountForId(thread.accountId).usesLabels()) {
const hidden = CategoryStore.hiddenCategories(thread.accountId);
let current = FocusedPerspectiveStore.current().categories();
if (includeCurrentCategories || !current) {
current = [];
}
const ignoredIds = [].concat(hidden, current).map(l=> l.id);
const ignoredNames = MessageStore.CategoryNamesHiddenByDefault;
for (const label of thread.sortedCategories()) {
if (ignoredNames.includes(label.name) || ignoredIds.includes(label.id)) {
continue;
}
if (LabelComponentCache[label.id] === undefined) {
LabelComponentCache[label.id] = (
<MailLabel
label={label}
key={label.id}
onRemove={()=> this._onRemoveLabel(label)}/>
);
}
labels.push(LabelComponentCache[label.id]);
}
}
return (<InjectedComponentSet
inline
containersRequired={false}
children={labels}
matching={{role: "Thread:MailLabel"}}
className="thread-injected-mail-labels"
exposedProps={{thread: thread}}/>
);
}
}

View file

@ -263,6 +263,12 @@ class Actions
###
@toggleMessageIdExpanded: ActionScopeWindow
###
Public: Toggle whether messages from trash and spam are shown in the current
message view.
###
@toggleHiddenMessages: ActionScopeWindow
###
Public: This action toggles wether to collapse or expand all messages in a
thread depending on if there are currently collapsed messages.

View file

@ -148,11 +148,10 @@ class Message extends ModelWithMetadata
modelKey: 'replyToMessageId'
jsonKey: 'reply_to_message_id'
'folder': Attributes.Object
modelKey: 'folder'
'categories': Attributes.Collection
modelKey: 'categories'
itemClass: Category
@naturalSortOrder: ->
Message.attributes.date.ascending()
@ -174,6 +173,7 @@ class Message extends ModelWithMetadata
@files ||= []
@uploads ||= []
@events ||= []
@categories ||= []
@
toJSON: (options) ->
@ -192,7 +192,12 @@ class Message extends ModelWithMetadata
if json.object?
@draft = (json.object is 'draft')
for attr in ['to', 'from', 'cc', 'bcc', 'files']
if json['folder']
@categories = @constructor.attributes.categories.fromJSON([json['folder']])
else if json['labels']
@categories = @constructor.attributes.categories.fromJSON(json['labels'])
for attr in ['to', 'from', 'cc', 'bcc', 'files', 'categories']
values = @[attr]
continue unless values and values instanceof Array
item.accountId = @accountId for item in values

View file

@ -16,7 +16,7 @@ DatabaseTransaction = require './database-transaction'
{ipcRenderer} = require 'electron'
DatabaseVersion = 18
DatabaseVersion = 19
DatabasePhase =
Setup: 'setup'
Ready: 'ready'

View file

@ -5,6 +5,7 @@ Thread = require "../models/thread"
Utils = require '../models/utils'
DatabaseStore = require "./database-store"
AccountStore = require "./account-store"
FocusedPerspectiveStore = require './focused-perspective-store'
FocusedContentStore = require "./focused-content-store"
ChangeUnreadTask = require '../tasks/change-unread-task'
NylasAPI = require '../nylas-api'
@ -13,6 +14,8 @@ ExtensionRegistry = require '../../extension-registry'
async = require 'async'
_ = require 'underscore'
CategoryNamesHiddenByDefault = ['spam', 'trash']
class MessageStore extends NylasStore
constructor: ->
@ -22,7 +25,16 @@ class MessageStore extends NylasStore
########### PUBLIC #####################################################
items: ->
@_items
return @_items if @_showingHiddenItems
viewing = FocusedPerspectiveStore.current().categoriesSharedName()
viewingHidden = viewing in CategoryNamesHiddenByDefault
return @_items.filter (item) ->
inHidden = _.any item.categories, (cat) -> cat.name in CategoryNamesHiddenByDefault
return false if viewingHidden and not inHidden
return false if not viewingHidden and inHidden
return true
threadId: -> @_thread?.id
@ -36,6 +48,9 @@ class MessageStore extends NylasStore
hasCollapsedItems: ->
_.size(@_itemsExpanded) < @_items.length
numberOfHiddenItems: ->
@_items.length - @items().length
itemClientIds: ->
_.pluck(@_items, "clientId")
@ -79,6 +94,7 @@ class MessageStore extends NylasStore
@_items = []
@_itemsExpanded = {}
@_itemsLoading = false
@_showingHiddenItems = false
@_thread = null
@_inflight = {}
@ -88,6 +104,11 @@ class MessageStore extends NylasStore
@listenTo FocusedContentStore, @_onFocusChanged
@listenTo Actions.toggleMessageIdExpanded, @_onToggleMessageIdExpanded
@listenTo Actions.toggleAllMessagesExpanded, @_onToggleAllMessagesExpanded
@listenTo Actions.toggleHiddenMessages, @_onToggleHiddenMessages
@listenTo FocusedPerspectiveStore, @_onPerspectiveChanged
_onPerspectiveChanged: =>
@trigger()
_onDataChanged: (change) =>
return unless @_thread
@ -151,6 +172,7 @@ class MessageStore extends NylasStore
@_thread = focused
@_items = []
@_itemsLoading = true
@_showingHiddenItems = false
@_itemsExpanded = {}
@trigger()
@ -187,6 +209,13 @@ class MessageStore extends NylasStore
@_items[...-1].forEach @_collapseItem
@trigger()
_onToggleHiddenMessages: =>
@_showingHiddenItems = !@_showingHiddenItems
@_expandItemsToDefault()
@_fetchExpandedBodies(@_items)
@_fetchExpandedAttachments(@_items)
@trigger()
_onToggleMessageIdExpanded: (id) =>
item = _.findWhere(@_items, {id})
return unless item
@ -269,8 +298,9 @@ class MessageStore extends NylasStore
# Expand all unread messages, all drafts, and the last message
_expandItemsToDefault: ->
for item, idx in @_items
if item.unread or item.draft or idx is @_items.length - 1
visibleItems = @items()
for item, idx in visibleItems
if item.unread or item.draft or idx is visibleItems.length - 1
@_itemsExpanded[item.id] = "default"
_fetchMessages: ->
@ -327,4 +357,6 @@ store.unregisterExtension = deprecate(
store,
store.unregisterExtension
)
store.CategoryNamesHiddenByDefault = CategoryNamesHiddenByDefault
module.exports = store

View file

@ -44,6 +44,7 @@ class NylasComponentKit
@loadFrom "MailLabel", "mail-label"
@loadFrom "LabelColorizer", "mail-label"
@load "MailLabelSet", "mail-label-set"
@load "MailImportantIcon", 'mail-important-icon'
@loadFrom "FormItem", "generated-form"

View file

@ -81,6 +81,13 @@ class MailboxPerspective
categories: =>
[]
categoriesSharedName: =>
cats = @categories()
return null unless cats and cats.length > 0
name = cats[0].name
return null unless _.every cats, (cat) -> cat.name is name
return name
category: =>
return null unless @categories().length is 1
return @categories()[0]
@ -282,7 +289,7 @@ class CategoryMailboxPerspective extends MailboxPerspective
@_categories
isInbox: =>
@_categories[0].name is 'inbox'
@categoriesSharedName() is 'inbox'
canReceiveThreads: =>
super and not _.any @_categories, (c) -> c.isLockedCategory()