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