From f413386b801ac976f792302173275cb51427d474 Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Wed, 2 Mar 2016 10:05:17 -0800 Subject: [PATCH] feat(hidden-messages): Filter trash/spam messages. Fixes #1135 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 --- build/config/eslint.json | 1 + internal_packages/message-list/lib/main.cjsx | 4 ++ .../message-list-hidden-messages-toggle.jsx | 64 +++++++++++++++++ .../message-list/lib/message-list.cjsx | 16 +---- .../stylesheets/message-list.less | 14 ++++ .../thread-list/lib/thread-list-columns.cjsx | 48 ++----------- .../thread-list/stylesheets/thread-list.less | 6 ++ internal_packages/thread-snooze/lib/main.js | 2 +- spec/mailbox-perspective-spec.coffee | 17 ++++- src/components/mail-label-set.jsx | 68 +++++++++++++++++++ src/flux/actions.coffee | 6 ++ src/flux/models/message.coffee | 13 ++-- src/flux/stores/database-store.coffee | 2 +- src/flux/stores/message-store.coffee | 38 ++++++++++- src/global/nylas-component-kit.coffee | 1 + src/mailbox-perspective.coffee | 9 ++- 16 files changed, 240 insertions(+), 69 deletions(-) create mode 100644 internal_packages/message-list/lib/message-list-hidden-messages-toggle.jsx create mode 100644 src/components/mail-label-set.jsx diff --git a/build/config/eslint.json b/build/config/eslint.json index 6da4a4a36..992ea2ddc 100644 --- a/build/config/eslint.json +++ b/build/config/eslint.json @@ -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], diff --git a/internal_packages/message-list/lib/main.cjsx b/internal_packages/message-list/lib/main.cjsx index f4716d8ae..6ef52e3f3 100644 --- a/internal_packages/message-list/lib/main.cjsx +++ b/internal_packages/message-list/lib/main.cjsx @@ -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 diff --git a/internal_packages/message-list/lib/message-list-hidden-messages-toggle.jsx b/internal_packages/message-list/lib/message-list-hidden-messages-toggle.jsx new file mode 100644 index 000000000..f8edbe8e1 --- /dev/null +++ b/internal_packages/message-list/lib/message-list-hidden-messages-toggle.jsx @@ -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 ( +
+ {message} + Show all messages +
+ ); + } +} + +MessageListHiddenMessagesToggle.containerRequired = false; diff --git a/internal_packages/message-list/lib/message-list.cjsx b/internal_packages/message-list/lib/message-list.cjsx index 1cd3e05c0..1cdc9b4a7 100755 --- a/internal_packages/message-list/lib/message-list.cjsx +++ b/internal_packages/message-list/lib/message-list.cjsx @@ -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
{subject} - {@_renderLabels()} +
{@_renderIcons()} @@ -243,14 +243,6 @@ class MessageList extends React.Component - _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) => - @_onRemoveLabel(label) }/> - _renderReplyArea: =>
@@ -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()) diff --git a/internal_packages/message-list/stylesheets/message-list.less b/internal_packages/message-list/stylesheets/message-list.less index f22eb409e..e28b4afb1 100644 --- a/internal_packages/message-list/stylesheets/message-list.less +++ b/internal_packages/message-list/stylesheets/message-list.less @@ -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%; diff --git a/internal_packages/thread-list/lib/thread-list-columns.cjsx b/internal_packages/thread-list/lib/thread-list-columns.cjsx index af046ca80..8279c3a51 100644 --- a/internal_packages/thread-list/lib/thread-list-columns.cjsx +++ b/internal_packages/thread-list/lib/thread-list-columns.cjsx @@ -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 -c3LabelComponentCache = {} - c3 = new ListTabular.Column name: "Message" flex: 4 resolver: (thread) => attachment = [] - labels = [] - if thread.hasAttachments attachment =
- 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] ?= - labels.push c3LabelComponentCache[label.id] - - + {subject(thread.subject)} {thread.snippet} {attachment} @@ -127,17 +104,6 @@ cNarrow = new ListTabular.Column if hasDraft pencil = - 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] ?= - labels.push c3LabelComponentCache[label.id] -
@@ -154,13 +120,7 @@ cNarrow = new ListTabular.Column
{thread.snippet} 
- +
diff --git a/internal_packages/thread-list/stylesheets/thread-list.less b/internal_packages/thread-list/stylesheets/thread-list.less index cf361eef1..63790f961 100644 --- a/internal_packages/thread-list/stylesheets/thread-list.less +++ b/internal_packages/thread-list/stylesheets/thread-list.less @@ -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; diff --git a/internal_packages/thread-snooze/lib/main.js b/internal_packages/thread-snooze/lib/main.js index 74fbd7c08..6b2775db6 100644 --- a/internal_packages/thread-snooze/lib/main.js +++ b/internal_packages/thread-snooze/lib/main.js @@ -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() { diff --git a/spec/mailbox-perspective-spec.coffee b/spec/mailbox-perspective-spec.coffee index 0148011a7..8db33bf17 100644 --- a/spec/mailbox-perspective-spec.coffee +++ b/spec/mailbox-perspective-spec.coffee @@ -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 diff --git a/src/components/mail-label-set.jsx b/src/components/mail-label-set.jsx new file mode 100644 index 000000000..36723fd87 --- /dev/null +++ b/src/components/mail-label-set.jsx @@ -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] = ( + this._onRemoveLabel(label)}/> + ); + } + labels.push(LabelComponentCache[label.id]); + } + } + return ( + ); + } +} diff --git a/src/flux/actions.coffee b/src/flux/actions.coffee index b1b57557b..ee0a4a7be 100644 --- a/src/flux/actions.coffee +++ b/src/flux/actions.coffee @@ -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. diff --git a/src/flux/models/message.coffee b/src/flux/models/message.coffee index b13283122..e23578435 100644 --- a/src/flux/models/message.coffee +++ b/src/flux/models/message.coffee @@ -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 diff --git a/src/flux/stores/database-store.coffee b/src/flux/stores/database-store.coffee index 652df380e..8899bfc80 100644 --- a/src/flux/stores/database-store.coffee +++ b/src/flux/stores/database-store.coffee @@ -16,7 +16,7 @@ DatabaseTransaction = require './database-transaction' {ipcRenderer} = require 'electron' -DatabaseVersion = 18 +DatabaseVersion = 19 DatabasePhase = Setup: 'setup' Ready: 'ready' diff --git a/src/flux/stores/message-store.coffee b/src/flux/stores/message-store.coffee index b6a1742ef..9d75fa2f1 100644 --- a/src/flux/stores/message-store.coffee +++ b/src/flux/stores/message-store.coffee @@ -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 diff --git a/src/global/nylas-component-kit.coffee b/src/global/nylas-component-kit.coffee index 4ef18a29f..e8e7d0a41 100644 --- a/src/global/nylas-component-kit.coffee +++ b/src/global/nylas-component-kit.coffee @@ -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" diff --git a/src/mailbox-perspective.coffee b/src/mailbox-perspective.coffee index 9a6608a97..3051879c9 100644 --- a/src/mailbox-perspective.coffee +++ b/src/mailbox-perspective.coffee @@ -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()