+
\ No newline at end of file
diff --git a/internal_packages/thread-list/lib/draft-selection-bar.cjsx b/internal_packages/thread-list/lib/draft-selection-bar.cjsx
deleted file mode 100644
index 391d99176..000000000
--- a/internal_packages/thread-list/lib/draft-selection-bar.cjsx
+++ /dev/null
@@ -1,17 +0,0 @@
-React = require "react/addons"
-DraftListStore = require './draft-list-store'
-{MultiselectActionBar, FluxContainer} = require 'nylas-component-kit'
-
-class DraftSelectionBar extends React.Component
- @displayName: 'DraftSelectionBar'
-
- render: =>
- dataSource: DraftListStore.dataSource() }>
-
-
-
-module.exports = DraftSelectionBar
diff --git a/internal_packages/thread-list/lib/formatting-utils.cjsx b/internal_packages/thread-list/lib/formatting-utils.cjsx
deleted file mode 100644
index c9f2bbb00..000000000
--- a/internal_packages/thread-list/lib/formatting-utils.cjsx
+++ /dev/null
@@ -1,13 +0,0 @@
-{Utils} = require 'nylas-exports'
-React = require 'react'
-
-module.exports =
- timestamp: (time) ->
- Utils.shortTimeString(time)
-
- subject: (subj) ->
- if (subj ? "").trim().length is 0
- return (No Subject)
- else
- return subj
-
diff --git a/internal_packages/thread-list/lib/injects-toolbar-buttons.jsx b/internal_packages/thread-list/lib/injects-toolbar-buttons.jsx
new file mode 100644
index 000000000..53ed8e795
--- /dev/null
+++ b/internal_packages/thread-list/lib/injects-toolbar-buttons.jsx
@@ -0,0 +1,63 @@
+import React, {Component, PropTypes} from 'react'
+import {ListensToObservable, InjectedComponentSet} from 'nylas-component-kit'
+import ThreadListStore from './thread-list-store'
+
+
+export const ToolbarRole = 'ThreadActionsToolbarButton'
+
+
+function defaultObservable() {
+ return ThreadListStore.selectionObservable()
+}
+
+function InjectsToolbarButtons(ToolbarComponent, {getObservable, extraRoles = []}) {
+ const roles = [ToolbarRole].concat(extraRoles)
+
+ class ComposedComponent extends Component {
+ static displayName = ToolbarComponent.displayName;
+
+ static propTypes = {
+ items: PropTypes.array,
+ };
+
+ static containerRequired = false;
+
+ render() {
+ const {items} = this.props;
+ const {selection} = ThreadListStore.dataSource()
+
+ // Keep all of the exposed props from deprecated regions that now map to this one
+ const exposedProps = {
+ items,
+ selection,
+ thread: items[0],
+ }
+ const injectedButtons = (
+
+ )
+ return (
+
+ )
+ }
+ }
+
+ const getStateFromObservable = (items) => {
+ if (!items) {
+ return {items: []}
+ }
+ return {items}
+ }
+ return ListensToObservable(ComposedComponent, {
+ getObservable: getObservable || defaultObservable,
+ getStateFromObservable,
+ })
+}
+
+export default InjectsToolbarButtons
diff --git a/internal_packages/thread-list/lib/main.cjsx b/internal_packages/thread-list/lib/main.cjsx
index 67b540bb2..685c58e07 100644
--- a/internal_packages/thread-list/lib/main.cjsx
+++ b/internal_packages/thread-list/lib/main.cjsx
@@ -2,32 +2,34 @@ _ = require 'underscore'
React = require "react"
{ComponentRegistry, WorkspaceStore} = require "nylas-exports"
-{DownButton, UpButton, ThreadBulkArchiveButton, ThreadBulkTrashButton,
- ThreadBulkStarButton, ThreadBulkToggleUnreadButton} = require "./thread-buttons"
-{DraftDeleteButton} = require "./draft-buttons"
-ThreadSelectionBar = require './thread-selection-bar'
ThreadList = require './thread-list'
+ThreadListToolbar = require './thread-list-toolbar'
+MessageListToolbar = require './message-list-toolbar'
+SelectedItemsStack = require './selected-items-stack'
-DraftSelectionBar = require './draft-selection-bar'
-DraftList = require './draft-list'
-DraftListSendStatus = require './draft-list-send-status'
+{UpButton,
+ DownButton,
+ TrashButton,
+ ArchiveButton,
+ ToggleUnreadButton,
+ ToggleStarredButton} = require "./thread-toolbar-buttons"
module.exports =
activate: (@state={}) ->
- WorkspaceStore.defineSheet 'Drafts', {root: true},
- list: ['RootSidebar', 'DraftList']
-
ComponentRegistry.register ThreadList,
location: WorkspaceStore.Location.ThreadList
- ComponentRegistry.register ThreadSelectionBar,
+ ComponentRegistry.register SelectedItemsStack,
+ location: WorkspaceStore.Location.MessageList
+ modes: ['split']
+
+ # Toolbars
+ ComponentRegistry.register ThreadListToolbar,
location: WorkspaceStore.Location.ThreadList.Toolbar
+ modes: ['list']
- ComponentRegistry.register DraftList,
- location: WorkspaceStore.Location.DraftList
-
- ComponentRegistry.register DraftSelectionBar,
- location: WorkspaceStore.Location.DraftList.Toolbar
+ ComponentRegistry.register MessageListToolbar,
+ location: WorkspaceStore.Location.MessageList.Toolbar
ComponentRegistry.register DownButton,
location: WorkspaceStore.Location.MessageList.Toolbar
@@ -37,33 +39,26 @@ module.exports =
location: WorkspaceStore.Location.MessageList.Toolbar
modes: ['list']
- ComponentRegistry.register ThreadBulkArchiveButton,
- role: 'thread:BulkAction'
+ ComponentRegistry.register ArchiveButton,
+ role: 'ThreadActionsToolbarButton'
- ComponentRegistry.register ThreadBulkTrashButton,
- role: 'thread:BulkAction'
+ ComponentRegistry.register TrashButton,
+ role: 'ThreadActionsToolbarButton'
- ComponentRegistry.register ThreadBulkStarButton,
- role: 'thread:BulkAction'
+ ComponentRegistry.register ToggleStarredButton,
+ role: 'ThreadActionsToolbarButton'
- ComponentRegistry.register ThreadBulkToggleUnreadButton,
- role: 'thread:BulkAction'
-
- ComponentRegistry.register DraftDeleteButton,
- role: 'draft:BulkAction'
-
- ComponentRegistry.register DraftListSendStatus,
- role: 'DraftList:DraftStatus'
+ ComponentRegistry.register ToggleUnreadButton,
+ role: 'ThreadActionsToolbarButton'
deactivate: ->
- ComponentRegistry.unregister DraftList
- ComponentRegistry.unregister DraftSelectionBar
ComponentRegistry.unregister ThreadList
- ComponentRegistry.unregister ThreadSelectionBar
- ComponentRegistry.unregister ThreadBulkArchiveButton
- ComponentRegistry.unregister ThreadBulkTrashButton
- ComponentRegistry.unregister ThreadBulkToggleUnreadButton
- ComponentRegistry.unregister DownButton
+ ComponentRegistry.unregister SelectedItemsStack
+ ComponentRegistry.unregister ThreadListToolbar
+ ComponentRegistry.unregister MessageListToolbar
+ ComponentRegistry.unregister ArchiveButton
+ ComponentRegistry.unregister TrashButton
+ ComponentRegistry.unregister ToggleUnreadButton
+ ComponentRegistry.unregister ToggleStarredButton
ComponentRegistry.unregister UpButton
- ComponentRegistry.unregister DraftDeleteButton
- ComponentRegistry.unregister DraftListSendStatus
+ ComponentRegistry.unregister DownButton
diff --git a/internal_packages/thread-list/lib/message-list-toolbar.jsx b/internal_packages/thread-list/lib/message-list-toolbar.jsx
new file mode 100644
index 000000000..437ccf926
--- /dev/null
+++ b/internal_packages/thread-list/lib/message-list-toolbar.jsx
@@ -0,0 +1,61 @@
+import Rx from 'rx-lite'
+import React, {Component, PropTypes} from 'react'
+import {FocusedContentStore} from 'nylas-exports'
+import {TimeoutTransitionGroup} from 'nylas-component-kit'
+import ThreadListStore from './thread-list-store'
+import InjectsToolbarButtons, {ToolbarRole} from './injects-toolbar-buttons'
+
+
+function getObservable() {
+ return (
+ Rx.Observable.merge(
+ Rx.Observable.fromStore(FocusedContentStore),
+ ThreadListStore.selectionObservable(),
+ )
+ .map((data) => {
+ const storeChanged = data === FocusedContentStore
+ const selectionChanged = data instanceof Array
+
+ if (storeChanged) {
+ const focusedThread = FocusedContentStore.focused('thread')
+ if (focusedThread) {
+ return [focusedThread]
+ }
+ } else if (selectionChanged) {
+ return data
+ }
+ return []
+ })
+ )
+}
+
+class MessageListToolbar extends Component {
+ static displayName = 'MessageListToolbar';
+
+ static propTypes = {
+ items: PropTypes.array,
+ injectedButtons: PropTypes.element,
+ };
+
+ render() {
+ const {items, injectedButtons} = this.props
+ const shouldRender = items.length > 0
+
+ return (
+
+ {shouldRender ? injectedButtons : undefined}
+
+ )
+ }
+}
+
+const toolbarProps = {
+ getObservable,
+ extraRoles: [`MessageList:${ToolbarRole}`],
+}
+
+export default InjectsToolbarButtons(MessageListToolbar, toolbarProps)
diff --git a/internal_packages/thread-list/lib/selected-items-stack.jsx b/internal_packages/thread-list/lib/selected-items-stack.jsx
new file mode 100644
index 000000000..5933e5bb5
--- /dev/null
+++ b/internal_packages/thread-list/lib/selected-items-stack.jsx
@@ -0,0 +1,73 @@
+import _ from 'underscore'
+import React, {Component, PropTypes} from 'react'
+import {ListensToObservable} from 'nylas-component-kit'
+import ThreadListStore from './thread-list-store'
+
+
+function getObservable() {
+ return (
+ ThreadListStore.selectionObservable()
+ .map(items => items.length)
+ )
+}
+
+function getStateFromObservable(selectionCount) {
+ if (!selectionCount) {
+ return {selectionCount: 0}
+ }
+ return {selectionCount}
+}
+
+class SelectedItemsStack extends Component {
+ static displayName = "SelectedItemsStack";
+
+ static propTypes = {
+ selectionCount: PropTypes.number,
+ };
+
+ onClearSelection = ()=> {
+ ThreadListStore.dataSource().selection.clear()
+ };
+
+ static containerRequired = false;
+
+ render() {
+ const {selectionCount} = this.props
+ if (selectionCount <= 1) {
+ return
+ }
+ const cardCount = Math.min(5, selectionCount)
+
+ return (
+
+
+
+ {_.times(cardCount, (idx) => {
+ let deg = idx * 0.9;
+
+ if (idx === 1) {
+ deg += 0.5
+ }
+ let transform = `rotate(${deg}deg)`
+ if (idx === cardCount - 1) {
+ transform += ' translate(2px, 3px)'
+ }
+ const style = {
+ transform,
+ zIndex: 5 - idx,
+ }
+ return
+ })}
+
+
+
{selectionCount}
+
messages selected
+
clear selection
+
+
+
+ )
+ }
+}
+
+export default ListensToObservable(SelectedItemsStack, {getObservable, getStateFromObservable})
diff --git a/internal_packages/thread-list/lib/thread-list-columns.cjsx b/internal_packages/thread-list/lib/thread-list-columns.cjsx
index e560df014..7359d64f6 100644
--- a/internal_packages/thread-list/lib/thread-list-columns.cjsx
+++ b/internal_packages/thread-list/lib/thread-list-columns.cjsx
@@ -8,23 +8,26 @@ classNames = require 'classnames'
MailImportantIcon,
InjectedComponentSet} = require 'nylas-component-kit'
-{Thread, FocusedPerspectiveStore} = require 'nylas-exports'
+{Thread, FocusedPerspectiveStore, Utils} = require 'nylas-exports'
{ThreadArchiveQuickAction,
ThreadTrashQuickAction} = require './thread-list-quick-actions'
-{timestamp,
- subject} = require './formatting-utils'
-
ThreadListParticipants = require './thread-list-participants'
ThreadListStore = require './thread-list-store'
ThreadListIcon = require './thread-list-icon'
TimestampComponentForPerspective = (thread) ->
if FocusedPerspectiveStore.current().isSent()
- {timestamp(thread.lastMessageSentTimestamp)}
+ {Utils.shortTimeString(thread.lastMessageSentTimestamp)}
else
- {timestamp(thread.lastMessageReceivedTimestamp)}
+ {Utils.shortTimeString(thread.lastMessageReceivedTimestamp)}
+
+subject = (subj) ->
+ if (subj ? "").trim().length is 0
+ return (No Subject)
+ else
+ return subj
c1 = new ListTabular.Column
diff --git a/internal_packages/thread-list/lib/thread-list-scroll-tooltip.cjsx b/internal_packages/thread-list/lib/thread-list-scroll-tooltip.cjsx
index 59fca65ce..405d50940 100644
--- a/internal_packages/thread-list/lib/thread-list-scroll-tooltip.cjsx
+++ b/internal_packages/thread-list/lib/thread-list-scroll-tooltip.cjsx
@@ -1,7 +1,6 @@
React = require 'react'
+{Utils} = require 'nylas-exports'
ThreadListStore = require './thread-list-store'
-{timestamp} = require './formatting-utils'
-
class ThreadListScrollTooltip extends React.Component
@displayName: 'ThreadListScrollTooltip'
@@ -26,7 +25,7 @@ class ThreadListScrollTooltip extends React.Component
render: ->
if @state.item
- content = timestamp(@state.item.lastMessageReceivedTimestamp)
+ content = Utils.shortTimeString(@state.item.lastMessageReceivedTimestamp)
else
content = "Loading..."
diff --git a/internal_packages/thread-list/lib/thread-list-store.coffee b/internal_packages/thread-list/lib/thread-list-store.coffee
index a9ea61f6f..70a121d71 100644
--- a/internal_packages/thread-list/lib/thread-list-store.coffee
+++ b/internal_packages/thread-list/lib/thread-list-store.coffee
@@ -1,7 +1,8 @@
_ = require 'underscore'
NylasStore = require 'nylas-store'
-{Thread,
+{Rx,
+ Thread,
Message,
Actions,
DatabaseStore,
@@ -42,6 +43,9 @@ class ThreadListStore extends NylasStore
@trigger(@)
Actions.setFocus(collection: 'thread', item: null)
+ selectionObservable: =>
+ return Rx.Observable.fromListSelection(@)
+
# Inbound Events
_onPerspectiveChanged: =>
diff --git a/internal_packages/thread-list/lib/thread-list-toolbar.jsx b/internal_packages/thread-list/lib/thread-list-toolbar.jsx
new file mode 100644
index 000000000..50600238f
--- /dev/null
+++ b/internal_packages/thread-list/lib/thread-list-toolbar.jsx
@@ -0,0 +1,39 @@
+import React, {Component, PropTypes} from 'react'
+import {MultiselectToolbar} from 'nylas-component-kit'
+import InjectsToolbarButtons, {ToolbarRole} from './injects-toolbar-buttons'
+
+
+class ThreadListToolbar extends Component {
+ static displayName = 'ThreadListToolbar';
+
+ static propTypes = {
+ items: PropTypes.array,
+ selection: PropTypes.shape({
+ clear: PropTypes.func,
+ }),
+ injectedButtons: PropTypes.element,
+ };
+
+ onClearSelection = ()=> {
+ this.props.selection.clear()
+ };
+
+ render() {
+ const {injectedButtons, items} = this.props
+
+ return (
+
+ )
+ }
+}
+
+const toolbarProps = {
+ extraRoles: [`ThreadList:${ToolbarRole}`],
+}
+
+export default InjectsToolbarButtons(ThreadListToolbar, toolbarProps)
diff --git a/internal_packages/thread-list/lib/thread-list.cjsx b/internal_packages/thread-list/lib/thread-list.cjsx
index 7c02573b0..e9c6f6543 100644
--- a/internal_packages/thread-list/lib/thread-list.cjsx
+++ b/internal_packages/thread-list/lib/thread-list.cjsx
@@ -2,7 +2,10 @@ _ = require 'underscore'
React = require 'react'
classNames = require 'classnames'
-{MultiselectList, FluxContainer} = require 'nylas-component-kit'
+{MultiselectList,
+ FocusContainer,
+ EmptyListState,
+ FluxContainer} = require 'nylas-component-kit'
{Actions,
Thread,
@@ -20,8 +23,6 @@ classNames = require 'classnames'
ThreadListColumns = require './thread-list-columns'
ThreadListScrollTooltip = require './thread-list-scroll-tooltip'
ThreadListStore = require './thread-list-store'
-FocusContainer = require './focus-container'
-EmptyState = require './empty-state'
ThreadListContextMenu = require './thread-list-context-menu'
CategoryRemovalTargetRulesets = require './category-removal-target-rulesets'
@@ -96,7 +97,7 @@ class ThreadList extends React.Component
itemHeight={itemHeight}
className="thread-list thread-list-#{@state.style}"
scrollTooltipComponent={ThreadListScrollTooltip}
- emptyComponent={EmptyState}
+ emptyComponent={EmptyListState}
keymapHandlers={@_keymapHandlers()}
onDragStart={@_onDragStart}
onDragEnd={@_onDragEnd}
diff --git a/internal_packages/thread-list/lib/thread-selection-bar.cjsx b/internal_packages/thread-list/lib/thread-selection-bar.cjsx
deleted file mode 100644
index 29c9e9c5e..000000000
--- a/internal_packages/thread-list/lib/thread-selection-bar.cjsx
+++ /dev/null
@@ -1,17 +0,0 @@
-React = require "react/addons"
-ThreadListStore = require './thread-list-store'
-{MultiselectActionBar, FluxContainer} = require 'nylas-component-kit'
-
-class ThreadSelectionBar extends React.Component
- @displayName: 'ThreadSelectionBar'
-
- render: =>
- dataSource: ThreadListStore.dataSource() }>
-
-
-
-module.exports = ThreadSelectionBar
diff --git a/internal_packages/thread-list/lib/thread-buttons.cjsx b/internal_packages/thread-list/lib/thread-toolbar-buttons.cjsx
similarity index 77%
rename from internal_packages/thread-list/lib/thread-buttons.cjsx
rename to internal_packages/thread-list/lib/thread-toolbar-buttons.cjsx
index cb49372ac..8beb9d07e 100644
--- a/internal_packages/thread-list/lib/thread-buttons.cjsx
+++ b/internal_packages/thread-list/lib/thread-toolbar-buttons.cjsx
@@ -10,15 +10,15 @@ ThreadListStore = require './thread-list-store'
FocusedContentStore,
FocusedPerspectiveStore} = require "nylas-exports"
-class ThreadBulkArchiveButton extends React.Component
- @displayName: 'ThreadBulkArchiveButton'
+class ArchiveButton extends React.Component
+ @displayName: 'ArchiveButton'
@containerRequired: false
@propTypes:
- selection: React.PropTypes.object.isRequired
+ items: React.PropTypes.array.isRequired
render: ->
- canArchiveThreads = FocusedPerspectiveStore.current().canArchiveThreads(@props.selection.items())
+ canArchiveThreads = FocusedPerspectiveStore.current().canArchiveThreads(@props.items)
return unless canArchiveThreads
- _onArchive: =>
+ _onArchive: (event) =>
tasks = TaskFactory.tasksForArchiving
- threads: @props.selection.items()
+ threads: @props.items
Actions.queueTasks(tasks)
+ Actions.popSheet()
+ event.stopPropagation()
return
-class ThreadBulkTrashButton extends React.Component
- @displayName: 'ThreadBulkTrashButton'
+class TrashButton extends React.Component
+ @displayName: 'TrashButton'
@containerRequired: false
@propTypes:
- selection: React.PropTypes.object.isRequired
+ items: React.PropTypes.array.isRequired
render: ->
- canTrashThreads = FocusedPerspectiveStore.current().canTrashThreads(@props.selection.items())
+ canTrashThreads = FocusedPerspectiveStore.current().canTrashThreads(@props.items)
return unless canTrashThreads
- _onRemove: =>
+ _onRemove: (event) =>
tasks = TaskFactory.tasksForMovingToTrash
- threads: @props.selection.items()
+ threads: @props.items
Actions.queueTasks(tasks)
+ Actions.popSheet()
+ event.stopPropagation()
return
-class ThreadBulkStarButton extends React.Component
- @displayName: 'ThreadBulkStarButton'
+class ToggleStarredButton extends React.Component
+ @displayName: 'ToggleStarredButton'
@containerRequired: false
@propTypes:
- selection: React.PropTypes.object.isRequired
+ items: React.PropTypes.array.isRequired
render: ->
- postClickStarredState = _.every @props.selection.items(), (t) -> t.starred is false
+ postClickStarredState = _.every @props.items, (t) -> t.starred is false
title = "Remove stars from all"
imageName = "toolbar-star-selected.png"
@@ -82,21 +86,22 @@ class ThreadBulkStarButton extends React.Component
- _onStar: =>
- task = TaskFactory.taskForInvertingStarred(threads: @props.selection.items())
+ _onStar: (event) =>
+ task = TaskFactory.taskForInvertingStarred(threads: @props.items)
Actions.queueTask(task)
+ event.stopPropagation()
return
-class ThreadBulkToggleUnreadButton extends React.Component
- @displayName: 'ThreadBulkToggleUnreadButton'
+class ToggleUnreadButton extends React.Component
+ @displayName: 'ToggleUnreadButton'
@containerRequired: false
@propTypes:
- selection: React.PropTypes.object.isRequired
+ items: React.PropTypes.array.isRequired
render: =>
- postClickUnreadState = _.every @props.selection.items(), (t) -> _.isMatch(t, {unread: false})
+ postClickUnreadState = _.every @props.items, (t) -> _.isMatch(t, {unread: false})
fragment = if postClickUnreadState then "unread" else "read"
- _onClick: =>
- task = TaskFactory.taskForInvertingUnread(threads: @props.selection.items())
+ _onClick: (event) =>
+ task = TaskFactory.taskForInvertingUnread(threads: @props.items)
Actions.queueTask(task)
+ Actions.popSheet()
+ event.stopPropagation()
return
-
ThreadNavButtonMixin =
getInitialState: ->
@_getStateFromStores()
@@ -191,10 +197,10 @@ UpButton.containerRequired = false
DownButton.containerRequired = false
module.exports = {
- DownButton,
UpButton,
- ThreadBulkArchiveButton,
- ThreadBulkTrashButton,
- ThreadBulkStarButton,
- ThreadBulkToggleUnreadButton
+ DownButton,
+ TrashButton,
+ ArchiveButton,
+ ToggleStarredButton,
+ ToggleUnreadButton
}
diff --git a/internal_packages/thread-list/spec/thread-toolbar-buttons-spec.cjsx b/internal_packages/thread-list/spec/thread-toolbar-buttons-spec.cjsx
new file mode 100644
index 000000000..58d9d5c1c
--- /dev/null
+++ b/internal_packages/thread-list/spec/thread-toolbar-buttons-spec.cjsx
@@ -0,0 +1,65 @@
+React = require "react/addons"
+ReactTestUtils = React.addons.TestUtils
+TestUtils = React.addons.TestUtils
+{Thread, FocusedContentStore, Actions, ChangeUnreadTask} = require "nylas-exports"
+{ToggleStarredButton, ToggleUnreadButton} = require '../lib/thread-toolbar-buttons'
+
+test_thread = (new Thread).fromJSON({
+ "id" : "thread_12345"
+ "account_id": TEST_ACCOUNT_ID
+ "subject" : "Subject 12345"
+ "starred": false
+})
+
+test_thread_starred = (new Thread).fromJSON({
+ "id" : "thread_starred_12345"
+ "account_id": TEST_ACCOUNT_ID
+ "subject" : "Subject 12345"
+ "starred": true
+})
+
+describe "ThreadToolbarButtons", ->
+
+ describe "Starring", ->
+ it "stars a thread if the star button is clicked and thread is unstarred", ->
+ spyOn(Actions, 'queueTask')
+ starButton = TestUtils.renderIntoDocument()
+
+ TestUtils.Simulate.click React.findDOMNode(starButton)
+
+ expect(Actions.queueTask.mostRecentCall.args[0].threads).toEqual([test_thread])
+ expect(Actions.queueTask.mostRecentCall.args[0].starred).toEqual(true)
+
+ it "unstars a thread if the star button is clicked and thread is starred", ->
+ spyOn(Actions, 'queueTask')
+ starButton = TestUtils.renderIntoDocument()
+
+ TestUtils.Simulate.click React.findDOMNode(starButton)
+
+ expect(Actions.queueTask.mostRecentCall.args[0].threads).toEqual([test_thread_starred])
+ expect(Actions.queueTask.mostRecentCall.args[0].starred).toEqual(false)
+
+ describe "Marking as unread", ->
+ thread = null
+ markUnreadBtn = null
+
+ beforeEach ->
+ thread = new Thread(id: "thread-id-lol-123", accountId: TEST_ACCOUNT_ID, unread: false)
+ markUnreadBtn = ReactTestUtils.renderIntoDocument(
+
+ )
+
+ it "queues a task to change unread status to true", ->
+ spyOn Actions, "queueTask"
+ ReactTestUtils.Simulate.click React.findDOMNode(markUnreadBtn).childNodes[0]
+
+ changeUnreadTask = Actions.queueTask.calls[0].args[0]
+ expect(changeUnreadTask instanceof ChangeUnreadTask).toBe true
+ expect(changeUnreadTask.unread).toBe true
+ expect(changeUnreadTask.threads[0].id).toBe thread.id
+
+ it "returns to the thread list", ->
+ spyOn Actions, "popSheet"
+ ReactTestUtils.Simulate.click React.findDOMNode(markUnreadBtn).childNodes[0]
+
+ expect(Actions.popSheet).toHaveBeenCalled()
diff --git a/internal_packages/thread-list/stylesheets/selected-items-stack.less b/internal_packages/thread-list/stylesheets/selected-items-stack.less
new file mode 100644
index 000000000..00f70cd9a
--- /dev/null
+++ b/internal_packages/thread-list/stylesheets/selected-items-stack.less
@@ -0,0 +1,53 @@
+@import "ui-variables";
+@img-path: "../internal_packages/thread-list/assets/graphic-stackable-card-filled.svg";
+
+.selected-items-stack {
+ display: flex;
+ align-self: center;
+ align-items: center;
+ height: 100%;
+
+ .selected-items-stack-content {
+ display: flex;
+ position: relative;
+ align-items: center;
+ justify-content: center;
+ width: 198px;
+ height: 268px;
+
+ .stack {
+ .card {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 198px;
+ height: 268px;
+ background: url(@img-path);
+ background-size: 198px 268px;
+ }
+ }
+
+ .count-info {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ z-index: 6;
+
+ .count {
+ font-size: 4em;
+ font-weight: 200;
+ color: @text-color-very-subtle;
+ }
+ .count-message {
+ padding-top: @padding-base-vertical;
+ color: @text-color-very-subtle;
+ }
+ .clear {
+ padding-top: @padding-large-vertical * 2;
+ color: @text-color-link;
+ cursor: default;
+ }
+ }
+ }
+}
+
diff --git a/internal_packages/thread-list/stylesheets/thread-list.less b/internal_packages/thread-list/stylesheets/thread-list.less
index 64ebb7ea7..43dd8fc63 100644
--- a/internal_packages/thread-list/stylesheets/thread-list.less
+++ b/internal_packages/thread-list/stylesheets/thread-list.less
@@ -487,61 +487,3 @@ body.is-blurred {
}
}
}
-
-@keyframes sending-progress-move {
- 0% {
- background-position: 0 0;
- }
- 100% {
- background-position: 50px 50px;
- }
-}
-
-.draft-list {
- .sending {
- background-color: @background-primary;
- &:hover {
- background-color: @background-primary;
- }
- }
-
- .sending-progress {
- display: block;
- height:7px;
- align-self: center;
- background-color: @background-primary;
- border-bottom:1px solid @border-color-divider;
- position: relative;
-
- .filled {
- display: block;
- background: @component-active-color;
- height:6px;
- width: 0; //overridden by style
- transition: width 1000ms linear;
- }
- .indeterminate {
- display: block;
- background: @component-active-color;
- height:6px;
- width: 100%;
- }
- .indeterminate:after {
- content: "";
- position: absolute;
- top: 0; left: 0; bottom: 0; right: 0;
- background-image: linear-gradient(
- -45deg,
- rgba(255, 255, 255, .2) 25%,
- transparent 25%,
- transparent 50%,
- rgba(255, 255, 255, .2) 50%,
- rgba(255, 255, 255, .2) 75%,
- transparent 75%,
- transparent
- );
- background-size: 50px 50px;
- animation: sending-progress-move 2s linear infinite;
- }
- }
-}
diff --git a/internal_packages/thread-snooze/lib/main.js b/internal_packages/thread-snooze/lib/main.js
index 25f6ce7e3..502a76440 100644
--- a/internal_packages/thread-snooze/lib/main.js
+++ b/internal_packages/thread-snooze/lib/main.js
@@ -1,6 +1,6 @@
/** @babel */
import {ComponentRegistry} from 'nylas-exports';
-import {ToolbarSnooze, BulkThreadSnooze, QuickActionSnooze} from './snooze-buttons';
+import {ToolbarSnooze, QuickActionSnooze} from './snooze-buttons';
import SnoozeMailLabel from './snooze-mail-label'
import SnoozeStore from './snooze-store'
@@ -9,16 +9,14 @@ export function activate() {
this.snoozeStore = new SnoozeStore()
this.snoozeStore.activate()
- ComponentRegistry.register(ToolbarSnooze, {role: 'message:Toolbar'});
+ ComponentRegistry.register(ToolbarSnooze, {role: 'ThreadActionsToolbarButton'});
ComponentRegistry.register(QuickActionSnooze, {role: 'ThreadListQuickAction'});
- ComponentRegistry.register(BulkThreadSnooze, {role: 'thread:BulkAction'});
ComponentRegistry.register(SnoozeMailLabel, {role: 'Thread:MailLabel'});
}
export function deactivate() {
ComponentRegistry.unregister(ToolbarSnooze);
ComponentRegistry.unregister(QuickActionSnooze);
- ComponentRegistry.unregister(BulkThreadSnooze);
ComponentRegistry.unregister(SnoozeMailLabel);
this.snoozeStore.deactivate()
}
diff --git a/internal_packages/thread-snooze/lib/snooze-buttons.jsx b/internal_packages/thread-snooze/lib/snooze-buttons.jsx
index 755857394..0b2be045c 100644
--- a/internal_packages/thread-snooze/lib/snooze-buttons.jsx
+++ b/internal_packages/thread-snooze/lib/snooze-buttons.jsx
@@ -95,8 +95,8 @@ export class QuickActionSnooze extends Component {
}
-export class BulkThreadSnooze extends Component {
- static displayName = 'BulkThreadSnooze';
+export class ToolbarSnooze extends Component {
+ static displayName = 'ToolbarSnooze';
static propTypes = {
items: PropTypes.array,
@@ -113,22 +113,3 @@ export class BulkThreadSnooze extends Component {
);
}
}
-
-export class ToolbarSnooze extends Component {
- static displayName = 'ToolbarSnooze';
-
- static propTypes = {
- thread: PropTypes.object,
- };
-
- static containerRequired = false;
-
- render() {
- if (!FocusedPerspectiveStore.current().isInbox()) {
- return ;
- }
- return (
-
- );
- }
-}
diff --git a/src/component-registry.coffee b/src/component-registry.coffee
index a1e9d9140..a057ba89c 100644
--- a/src/component-registry.coffee
+++ b/src/component-registry.coffee
@@ -3,6 +3,13 @@ _ = require 'underscore'
{Listener, Publisher} = require './flux/modules/reflux-coffee'
CoffeeHelpers = require './flux/coffee-helpers'
+DeprecatedRoles = {
+ 'thread:BulkAction': 'ThreadActionsToolbarButton',
+ 'draft:BulkAction': 'DraftActionsToolbarButton',
+ 'message:Toolbar': 'ThreadActionsToolbarButton',
+ 'thread:Toolbar': 'ThreadActionsToolbarButton',
+}
+
###
Public: The ComponentRegistry maintains an index of React components registered
by Nylas packages. Components can use {InjectedComponent} and {InjectedComponentSet}
@@ -62,6 +69,8 @@ class ComponentRegistry
if @_registry[component.displayName] and @_registry[component.displayName].component isnt component
throw new Error("ComponentRegistry.register(): A different component was already registered with the name #{component.displayName}")
+ roles = @_removeDeprecatedRoles(component.displayName, roles) if roles
+
@_cache = {}
@_registry[component.displayName] = {component, locations, modes, roles}
@@ -153,6 +162,15 @@ class ComponentRegistry
triggerDebounced: _.debounce(( -> @trigger(@)), 1)
+ _removeDeprecatedRoles: (displayName, roles) ->
+ newRoles = _.clone(roles)
+ roles.forEach (role, idx) ->
+ if role of DeprecatedRoles
+ instead = DeprecatedRoles[role]
+ console.warn("Deprecation warning! The role `#{role}` has been deprecated.
+ Register `#{displayName}` for the role `#{instead}` instead.")
+ newRoles.splice(idx, 1, instead)
+ return newRoles
_pluralizeDescriptor: (descriptor) ->
{locations, modes, roles} = descriptor
diff --git a/internal_packages/thread-list/lib/empty-state.cjsx b/src/components/empty-list-state.cjsx
similarity index 97%
rename from internal_packages/thread-list/lib/empty-state.cjsx
rename to src/components/empty-list-state.cjsx
index 05fef5d5d..4ca2ec6a9 100644
--- a/internal_packages/thread-list/lib/empty-state.cjsx
+++ b/src/components/empty-list-state.cjsx
@@ -78,8 +78,8 @@ class InboxZero extends React.Component
-class EmptyState extends React.Component
- @displayName = 'EmptyState'
+class EmptyListState extends React.Component
+ @displayName = 'EmptyListState'
@propTypes =
visible: React.PropTypes.bool.isRequired
@@ -137,4 +137,4 @@ class EmptyState extends React.Component
layoutMode: WorkspaceStore.layoutMode()
syncing: NylasSyncStatusStore.busy()
-module.exports = EmptyState
+module.exports = EmptyListState
diff --git a/internal_packages/thread-list/lib/focus-container.cjsx b/src/components/focus-container.cjsx
similarity index 100%
rename from internal_packages/thread-list/lib/focus-container.cjsx
rename to src/components/focus-container.cjsx
diff --git a/src/components/listens-to-observable.jsx b/src/components/listens-to-observable.jsx
new file mode 100644
index 000000000..3e5c4c903
--- /dev/null
+++ b/src/components/listens-to-observable.jsx
@@ -0,0 +1,38 @@
+import React, {Component} from 'react'
+
+function ListensToObservable(ComposedComponent, {getObservable, getStateFromObservable}) {
+ return class extends Component {
+ static displayName = ComposedComponent.displayName;
+
+ static containerRequired = ComposedComponent.containerRequired;
+
+ constructor() {
+ super()
+ this.state = getStateFromObservable()
+ this.observable = getObservable()
+ }
+
+ componentDidMount() {
+ this.unmounted = false
+ this.disposable = this.observable.subscribe(this.onObservableChanged)
+ }
+
+ componentWillUnmount() {
+ this.unmounted = true
+ this.disposable.dispose()
+ }
+
+ onObservableChanged = (data) => {
+ if (this.unmounted) return;
+ this.setState(getStateFromObservable(data))
+ };
+
+ render() {
+ return (
+
+ )
+ }
+ }
+}
+
+export default ListensToObservable
diff --git a/src/components/multiselect-action-bar.cjsx b/src/components/multiselect-action-bar.cjsx
index 7a4ac666b..554596d0e 100644
--- a/src/components/multiselect-action-bar.cjsx
+++ b/src/components/multiselect-action-bar.cjsx
@@ -2,8 +2,7 @@ React = require "react/addons"
_ = require 'underscore'
{Utils,
- Actions,
- WorkspaceStore} = require "nylas-exports"
+ Actions} = require "nylas-exports"
InjectedComponentSet = require './injected-component-set'
TimeoutTransitionGroup = require './timeout-transition-group'
RetinaImg = require './retina-img'
@@ -32,7 +31,7 @@ collection name. To add an item to the bar created in the example above, registe
```coffee
ComponentRegistry.register ThreadBulkTrashButton,
- role: 'thread:BulkAction'
+ role: 'thread:Toolbar'
```
Section: Component Kit
@@ -73,7 +72,6 @@ class MultiselectActionBar extends React.Component
setupForProps: (props) =>
@_unsubscribers = []
- @_unsubscribers.push WorkspaceStore.listen @_onChange
@_unsubscribers.push props.dataSource.listen @_onChange
shouldComponentUpdate: (nextProps, nextState) =>
@@ -109,7 +107,7 @@ class MultiselectActionBar extends React.Component
_renderActions: =>
return unless @props.dataSource
-
_label: =>
diff --git a/src/components/multiselect-toolbar.jsx b/src/components/multiselect-toolbar.jsx
new file mode 100644
index 000000000..5b1ba1b6a
--- /dev/null
+++ b/src/components/multiselect-toolbar.jsx
@@ -0,0 +1,78 @@
+import {Utils} from 'nylas-exports'
+import React, {Component, PropTypes} from 'react'
+import TimeoutTransitionGroup from './timeout-transition-group'
+
+
+/**
+ * MultiselectToolbar renders a toolbar inside a horizontal bar and displays
+ * a selection count and a button to clear the selection.
+ *
+ * The toolbar, or set of buttons, must be passed in as props.toolbarElement
+ *
+ * It will also animate its mounting and unmounting
+ * @class MultiselectToolbar
+ */
+class MultiselectToolbar extends Component {
+ static displayName = 'MultiselectToolbar';
+
+ static propTypes = {
+ toolbarElement: PropTypes.element.isRequired,
+ collection: PropTypes.string.isRequired,
+ onClearSelection: PropTypes.func.isRequired,
+ selectionCount: PropTypes.node,
+ };
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ !Utils.isEqualReact(nextProps, this.props) ||
+ !Utils.isEqualReact(nextState, this.state)
+ )
+ }
+
+ selectionLabel = () => {
+ const {selectionCount, collection} = this.props
+ if (selectionCount > 1) {
+ return `${selectionCount} ${collection}s selected`
+ } else if (selectionCount === 1) {
+ return `${selectionCount} ${collection} selected`
+ }
+ return ''
+ };
+
+ renderToolbar() {
+ const {toolbarElement, onClearSelection} = this.props
+ return (
+
+
+ {toolbarElement}
+
+ {this.selectionLabel()}
+
+
+
+
+
+ )
+ }
+
+ render() {
+ const {selectionCount} = this.props
+ return (
+
+ {selectionCount > 0 ? this.renderToolbar() : undefined}
+
+ )
+ }
+}
+
+export default MultiselectToolbar
diff --git a/src/global/nylas-component-kit.coffee b/src/global/nylas-component-kit.coffee
index cb97ae3b0..2da023d72 100644
--- a/src/global/nylas-component-kit.coffee
+++ b/src/global/nylas-component-kit.coffee
@@ -29,6 +29,8 @@ class NylasComponentKit
@load "RetinaImg", 'retina-img'
@load "SwipeContainer", 'swipe-container'
@load "FluxContainer", 'flux-container'
+ @load "FocusContainer", 'focus-container'
+ @load "EmptyListState", 'empty-list-state'
@load "ListTabular", 'list-tabular'
@load "DraggableImg", 'draggable-img'
@load "EventedIFrame", 'evented-iframe'
@@ -38,7 +40,8 @@ class NylasComponentKit
@load "KeyCommandsRegion", 'key-commands-region'
@load "InjectedComponent", 'injected-component'
@load "TokenizingTextField", 'tokenizing-text-field'
- @load "MultiselectActionBar", 'multiselect-action-bar'
+ @loadDeprecated "MultiselectActionBar", 'multiselect-action-bar', instead: 'MultiselectToolbar'
+ @load "MultiselectToolbar", 'multiselect-toolbar'
@load "InjectedComponentSet", 'injected-component-set'
@load "TimeoutTransitionGroup", 'timeout-transition-group'
@load "MetadataComposerToggleButton", 'metadata-composer-toggle-button'
@@ -64,4 +67,7 @@ class NylasComponentKit
@load "ScenarioEditor", 'scenario-editor'
@load "NewsletterSignup", 'newsletter-signup'
+ # Higher order components
+ @load "ListensToObservable", 'listens-to-observable'
+
module.exports = new NylasComponentKit()
diff --git a/src/global/nylas-exports.coffee b/src/global/nylas-exports.coffee
index 02b4aaa8d..af6293bc2 100644
--- a/src/global/nylas-exports.coffee
+++ b/src/global/nylas-exports.coffee
@@ -165,6 +165,7 @@ class NylasExports
@load "PriorityUICoordinator", 'priority-ui-coordinator'
# Utils
+ @load "DeprecateUtils", 'deprecate-utils'
@load "Utils", 'flux/models/utils'
@load "DOMUtils", 'dom-utils'
@load "VirtualDOMUtils", 'virtual-dom-utils'
diff --git a/src/global/nylas-observables.coffee b/src/global/nylas-observables.coffee
index ddf1a09b6..d046031d1 100644
--- a/src/global/nylas-observables.coffee
+++ b/src/global/nylas-observables.coffee
@@ -67,6 +67,33 @@ Rx.Observable.fromStore = (store) =>
observer.onNext(store)
return Rx.Disposable.create(unsubscribe)
+# Takes a store that provides an {ObservableListDataSource} via `dataSource()`
+# Returns an observable that provides array of selected items on subscription
+Rx.Observable.fromListSelection = (originStore) =>
+ return Rx.Observable.create((observer) =>
+ dataSourceDisposable = null
+ storeObservable = Rx.Observable.fromStore(originStore)
+
+ disposable = storeObservable.subscribe( =>
+ dataSource = originStore.dataSource()
+ dataSourceObservable = Rx.Observable.fromStore(dataSource)
+
+ if dataSourceDisposable
+ dataSourceDisposable.dispose()
+
+ dataSourceDisposable = dataSourceObservable.subscribe( =>
+ observer.onNext(dataSource.selection.items())
+ )
+ return
+ )
+ return {
+ dispose: =>
+ if dataSourceDisposable
+ dataSourceDisposable.dispose()
+ disposable.dispose()
+ }
+ )
+
Rx.Observable.fromConfig = (configKey) =>
return Rx.Observable.create (observer) =>
disposable = NylasEnv.config.onDidChange configKey, =>
diff --git a/internal_packages/thread-list/stylesheets/empty-state.less b/static/components/empty-list-state.less
similarity index 100%
rename from internal_packages/thread-list/stylesheets/empty-state.less
rename to static/components/empty-list-state.less
diff --git a/static/index.less b/static/index.less
index 5833e4cfe..ece819d96 100644
--- a/static/index.less
+++ b/static/index.less
@@ -33,3 +33,4 @@
@import "components/fixed-popover";
@import "components/modal";
@import "components/date-input";
+@import "components/empty-list-state";