feat(not-spam): Unmark items as junk / spam #2112

This commit is contained in:
Ben Gotow 2016-05-09 16:03:18 -07:00
parent 29c81221f2
commit c87680878a
6 changed files with 372 additions and 246 deletions

View file

@ -1,241 +0,0 @@
_ = require 'underscore'
React = require "react"
classNames = require 'classnames'
ThreadListStore = require './thread-list-store'
{RetinaImg} = require 'nylas-component-kit'
{Actions,
TaskFactory,
AccountStore,
CategoryStore,
FocusedContentStore,
FocusedPerspectiveStore} = require "nylas-exports"
class ArchiveButton extends React.Component
@displayName: 'ArchiveButton'
@containerRequired: false
@propTypes:
items: React.PropTypes.array.isRequired
render: ->
allowed = FocusedPerspectiveStore.current().canArchiveThreads(@props.items)
return <span /> unless allowed
<button
tabIndex={-1}
style={order:-107}
className="btn btn-toolbar"
title="Archive"
onClick={@_onArchive}>
<RetinaImg name="toolbar-archive.png" mode={RetinaImg.Mode.ContentIsMask} />
</button>
_onArchive: (event) =>
tasks = TaskFactory.tasksForArchiving
threads: @props.items
Actions.queueTasks(tasks)
Actions.popSheet()
event.stopPropagation()
return
class TrashButton extends React.Component
@displayName: 'TrashButton'
@containerRequired: false
@propTypes:
items: React.PropTypes.array.isRequired
render: ->
allowed = FocusedPerspectiveStore.current().canMoveThreadsTo(@props.items, 'trash')
return <span /> unless allowed
<button tabIndex={-1}
style={order:-106}
className="btn btn-toolbar"
title="Move to Trash"
onClick={@_onRemove}>
<RetinaImg name="toolbar-trash.png" mode={RetinaImg.Mode.ContentIsMask} />
</button>
_onRemove: (event) =>
tasks = TaskFactory.tasksForMovingToTrash
threads: @props.items
Actions.queueTasks(tasks)
Actions.popSheet()
event.stopPropagation()
return
class MarkAsSpamButton extends React.Component
@displayName: 'MarkAsSpamButton'
@containerRequired: false
@propTypes:
items: React.PropTypes.array.isRequired
render: ->
allowed = FocusedPerspectiveStore.current().canMoveThreadsTo(@props.items, 'spam')
return <span /> unless allowed
<button tabIndex={-1}
style={order:-105}
className="btn btn-toolbar"
title="Mark as Spam"
onClick={@_onClick}>
<RetinaImg name="toolbar-spam.png" mode={RetinaImg.Mode.ContentIsMask} />
</button>
_onClick: (event) =>
tasks = TaskFactory.tasksForMarkingAsSpam
threads: @props.items
Actions.queueTasks(tasks)
Actions.popSheet()
event.stopPropagation()
return
class ToggleStarredButton extends React.Component
@displayName: 'ToggleStarredButton'
@containerRequired: false
@propTypes:
items: React.PropTypes.array.isRequired
render: ->
postClickStarredState = _.every @props.items, (t) -> t.starred is false
title = "Unstar"
imageName = "toolbar-star-selected.png"
if postClickStarredState
title = "Star"
imageName = "toolbar-star.png"
<button tabIndex={-1}
style={order:-103}
className="btn btn-toolbar"
title={title}
onClick={@_onStar}>
<RetinaImg name={imageName} mode={RetinaImg.Mode.ContentIsMask} />
</button>
_onStar: (event) =>
task = TaskFactory.taskForInvertingStarred(threads: @props.items)
Actions.queueTask(task)
event.stopPropagation()
return
class ToggleUnreadButton extends React.Component
@displayName: 'ToggleUnreadButton'
@containerRequired: false
@propTypes:
items: React.PropTypes.array.isRequired
render: =>
postClickUnreadState = _.every @props.items, (t) -> _.isMatch(t, {unread: false})
fragment = if postClickUnreadState then "unread" else "read"
<button tabIndex={-1}
style={order:-104}
className="btn btn-toolbar"
title="Mark as #{fragment}"
onClick={@_onClick}>
<RetinaImg name="toolbar-markas#{fragment}.png"
mode={RetinaImg.Mode.ContentIsMask} />
</button>
_onClick: (event) =>
task = TaskFactory.taskForInvertingUnread(threads: @props.items)
Actions.queueTask(task)
Actions.popSheet()
event.stopPropagation()
return
ThreadNavButtonMixin =
getInitialState: ->
@_getStateFromStores()
componentDidMount: ->
@_unsubscribe = ThreadListStore.listen @_onStoreChange
@_unsubscribe_focus = FocusedContentStore.listen @_onStoreChange
isFirstThread: ->
selectedId = FocusedContentStore.focusedId('thread')
ThreadListStore.dataSource().get(0)?.id is selectedId
isLastThread: ->
selectedId = FocusedContentStore.focusedId('thread')
lastIndex = ThreadListStore.dataSource().count() - 1
ThreadListStore.dataSource().get(lastIndex)?.id is selectedId
componentWillUnmount: ->
@_unsubscribe()
@_unsubscribe_focus()
_onStoreChange: ->
@setState @_getStateFromStores()
DownButton = React.createClass
displayName: 'DownButton'
mixins: [ThreadNavButtonMixin]
render: ->
<div className={@_classSet()} onClick={@_onClick} title="Next thread">
<RetinaImg name="toolbar-down-arrow.png" mode={RetinaImg.Mode.ContentIsMask} />
</div>
_classSet: ->
classNames
"btn-icon": true
"message-toolbar-arrow": true
"down": true
"disabled": @state.disabled
_onClick: ->
return if @state.disabled
NylasEnv.commands.dispatch('core:next-item')
return
_getStateFromStores: ->
disabled: @isLastThread()
UpButton = React.createClass
displayName: 'UpButton'
mixins: [ThreadNavButtonMixin]
render: ->
<div className={@_classSet()} onClick={@_onClick} title="Previous thread">
<RetinaImg name="toolbar-up-arrow.png" mode={RetinaImg.Mode.ContentIsMask} />
</div>
_classSet: ->
classNames
"btn-icon": true
"message-toolbar-arrow": true
"up": true
"disabled": @state.disabled
_onClick: ->
return if @state.disabled
NylasEnv.commands.dispatch('core:previous-item')
return
_getStateFromStores: ->
disabled: @isFirstThread()
UpButton.containerRequired = false
DownButton.containerRequired = false
module.exports = {
UpButton,
DownButton,
TrashButton,
MarkAsSpamButton,
ArchiveButton,
ToggleStarredButton,
ToggleUnreadButton
}

View file

@ -0,0 +1,317 @@
import React from "react";
import classNames from 'classnames';
import ThreadListStore from './thread-list-store';
import {RetinaImg} from 'nylas-component-kit';
import {
Actions,
TaskFactory,
AccountStore,
CategoryStore,
FocusedContentStore,
FocusedPerspectiveStore,
} from "nylas-exports";
export class ArchiveButton extends React.Component {
static displayName = 'ArchiveButton';
static containerRequired = false;
static propTypes = {
items: React.PropTypes.array.isRequired,
}
_onArchive = (event) => {
const tasks = TaskFactory.tasksForArchiving({threads: this.props.items})
Actions.queueTasks(tasks);
Actions.popSheet();
event.stopPropagation();
return;
}
render() {
const allowed = FocusedPerspectiveStore.current().canArchiveThreads(this.props.items);
if (!allowed) {
return <span />;
}
return (
<button
tabIndex={-1}
style={{order: -107}}
className="btn btn-toolbar"
title="Archive"
onClick={this._onArchive}
>
<RetinaImg name="toolbar-archive.png" mode={RetinaImg.Mode.ContentIsMask} />
</button>
)
}
}
export class TrashButton extends React.Component {
static displayName = 'TrashButton'
static containerRequired = false;
static propTypes = {
items: React.PropTypes.array.isRequired,
}
_onRemove = (event) => {
const tasks = TaskFactory.tasksForMovingToTrash({threads: this.props.items});
Actions.queueTasks(tasks);
Actions.popSheet();
event.stopPropagation();
return;
}
render() {
const allowed = FocusedPerspectiveStore.current().canMoveThreadsTo(this.props.items, 'trash')
if (!allowed) {
return <span />;
}
return (
<button
tabIndex={-1}
style={{order: -106}}
className="btn btn-toolbar"
title="Move to Trash"
onClick={this._onRemove}
>
<RetinaImg name="toolbar-trash.png" mode={RetinaImg.Mode.ContentIsMask} />
</button>
);
}
}
export class MarkAsSpamButton extends React.Component {
static displayName = 'MarkAsSpamButton';
static containerRequired = false;
static propTypes = {
items: React.PropTypes.array.isRequired,
}
_allInSpam() {
return this.props.items.every(item => item.categories.map(c => c.name).includes('spam'));
}
_onNotSpam = (event) => {
const tasks = TaskFactory.tasksForApplyingCategories({
threads: this.props.items,
categoriesToAdd: (accountId) => {
const account = AccountStore.accountForId(accountId)
return account.usesFolders() ? [CategoryStore.getInboxCategory(accountId)] : [];
},
categoriesToRemove: (accountId) => {
return [CategoryStore.getSpamCategory(accountId)];
},
})
Actions.queueTasks(tasks);
Actions.popSheet();
event.stopPropagation();
return;
}
_onMarkAsSpam = (event) => {
const tasks = TaskFactory.tasksForMarkingAsSpam({threads: this.props.items});
Actions.queueTasks(tasks);
Actions.popSheet();
event.stopPropagation();
return;
}
render() {
if (this._allInSpam()) {
return (
<button
tabIndex={-1}
style={{order: -105}}
className="btn btn-toolbar"
title="Not Spam"
onClick={this._onNotSpam}
>
<RetinaImg name="toolbar-not-spam.png" mode={RetinaImg.Mode.ContentIsMask} />
</button>
)
}
const allowed = FocusedPerspectiveStore.current().canMoveThreadsTo(this.props.items, 'spam');
if (!allowed) {
return <span />;
}
return (
<button
tabIndex={-1}
style={{order: -105}}
className="btn btn-toolbar"
title="Mark as Spam"
onClick={this._onMarkAsSpam}
>
<RetinaImg name="toolbar-spam.png" mode={RetinaImg.Mode.ContentIsMask} />
</button>
);
}
}
export class ToggleStarredButton extends React.Component {
static displayName = 'ToggleStarredButton';
static containerRequired = false;
static propTypes = {
items: React.PropTypes.array.isRequired,
};
_onStar = (event) => {
const task = TaskFactory.taskForInvertingStarred({threads: this.props.items});
Actions.queueTask(task);
event.stopPropagation();
return;
}
render() {
const postClickStarredState = this.props.items.every((t) => t.starred === false);
const title = postClickStarredState ? "Star" : "Unstar";
const imageName = postClickStarredState ? "toolbar-star.png" : "toolbar-star-selected.png"
return (
<button
tabIndex={-1}
style={{order: -103}}
className="btn btn-toolbar"
title={title}
onClick={this._onStar}
>
<RetinaImg name={imageName} mode={RetinaImg.Mode.ContentIsMask} />
</button>
);
}
}
export class ToggleUnreadButton extends React.Component {
static displayName = 'ToggleUnreadButton';
static containerRequired = false;
static propTypes = {
items: React.PropTypes.array.isRequired,
}
_onClick = (event) => {
const task = TaskFactory.taskForInvertingUnread({threads: this.props.items});
Actions.queueTask(task);
Actions.popSheet();
event.stopPropagation();
return;
}
render() {
const postClickUnreadState = this.props.items.every(t => t.unread === false);
const fragment = postClickUnreadState ? "unread" : "read";
return (
<button
tabIndex={-1}
style={{order: -104}}
className="btn btn-toolbar"
title={`Mark as ${fragment}`}
onClick={this._onClick}
>
<RetinaImg
name={`toolbar-markas${fragment}.png`}
mode={RetinaImg.Mode.ContentIsMask}
/>
</button>
);
}
}
class ThreadArrowButton extends React.Component {
static propTypes = {
getStateFromStores: React.PropTypes.func,
direction: React.PropTypes.string,
command: React.PropTypes.string,
title: React.PropTypes.string,
}
constructor() {
this.state = this.props.getStateFromStores();
}
componentDidMount() {
this._unsubscribe = ThreadListStore.listen(this._onStoreChange);
this._unsubscribe_focus = FocusedContentStore.listen(this._onStoreChange);
}
componentWillUnmount() {
this._unsubscribe();
this._unsubscribe_focus();
}
_onClick() {
if (this.state.disabled) {
return;
}
NylasEnv.commands.dispatch(this.props.command);
return;
}
_onStoreChange() {
this.setState(this.props.getStateFromStores());
}
render() {
const {direction, title} = this.props;
const classes = classNames({
"btn-icon": true,
"message-toolbar-arrow": true,
"disabled": this.state.disabled,
});
return (
<div className={`${classes} ${direction}`} onClick={this._onClick} title={title}>
<RetinaImg name={`toolbar-${direction}-arrow.png`} mode={RetinaImg.Mode.ContentIsMask} />
</div>
);
}
}
export const DownButton = () => {
const getStateFromStores = () => {
const selectedId = FocusedContentStore.focusedId('thread');
const lastIndex = ThreadListStore.dataSource().count() - 1
const lastItem = ThreadListStore.dataSource().get(lastIndex);
return {
disabled: (lastItem && lastItem.id === selectedId),
};
}
return (
<ThreadArrowButton
getStateFromStores={getStateFromStores}
direction={"down"}
title={"Next thread"}
command={'core:next-item'}
/>
);
}
DownButton.displayName = 'DownButton';
DownButton.containerRequired = false;
export const UpButton = () => {
const getStateFromStores = () => {
const selectedId = FocusedContentStore.focusedId('thread');
const item = ThreadListStore.dataSource().get(0)
return {
disabled: (item && item.id === selectedId),
};
}
return (
<ThreadArrowButton
getStateFromStores={getStateFromStores}
direction={"up"}
title={"Previous thread"}
command={'core:previous-item'}
/>
);
}
UpButton.displayName = 'UpButton';
UpButton.containerRequired = false;

View file

@ -1,8 +1,16 @@
React = require "react"
ReactDOM = require "react-dom"
ReactTestUtils = require 'react-addons-test-utils'
{Thread, FocusedContentStore, Actions, ChangeUnreadTask} = require "nylas-exports"
{ToggleStarredButton, ToggleUnreadButton} = require '../lib/thread-toolbar-buttons'
{
Thread,
FocusedContentStore,
Actions,
CategoryStore,
ChangeUnreadTask,
TaskFactory,
MailboxPerspective
} = require "nylas-exports"
{ToggleStarredButton, ToggleUnreadButton, MarkAsSpamButton} = require '../lib/thread-toolbar-buttons'
test_thread = (new Thread).fromJSON({
"id" : "thread_12345"
@ -63,3 +71,45 @@ describe "ThreadToolbarButtons", ->
ReactTestUtils.Simulate.click ReactDOM.findDOMNode(markUnreadBtn).childNodes[0]
expect(Actions.popSheet).toHaveBeenCalled()
describe "Marking as spam", ->
thread = null
markSpamButton = null
describe "when the thread is already in spam", ->
beforeEach ->
thread = new Thread({
id: "thread-id-lol-123",
accountId: TEST_ACCOUNT_ID,
categories: [{name: 'spam'}]
})
markSpamButton = ReactTestUtils.renderIntoDocument(
<MarkAsSpamButton items={[thread]} />
)
it "queues a task to remove spam", ->
spyOn(Actions, 'queueTasks')
spyOn(CategoryStore, 'getSpamCategory').andReturn(thread.categories[0])
ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(markSpamButton))
{labelsToAdd, labelsToRemove} = Actions.queueTasks.mostRecentCall.args[0][0]
expect(labelsToAdd).toEqual([])
expect(labelsToRemove).toEqual([thread.categories[0]])
describe "when the thread can be moved to spam", ->
beforeEach ->
spyOn(Actions, 'queueTasks')
spyOn(MailboxPerspective.prototype, 'canMoveThreadsTo').andReturn(true)
thread = new Thread(id: "thread-id-lol-123", accountId: TEST_ACCOUNT_ID, categories: [])
markSpamButton = ReactTestUtils.renderIntoDocument(
<MarkAsSpamButton items={[thread]} />
)
it "queues a task to mark as spam", ->
spyOn(TaskFactory, 'tasksForMarkingAsSpam')
ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(markSpamButton))
expect(TaskFactory.tasksForMarkingAsSpam).toHaveBeenCalledWith({threads: [thread]})
it "returns to the thread list", ->
spyOn(Actions, 'popSheet')
ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(markSpamButton))
expect(Actions.popSheet).toHaveBeenCalled()

View file

@ -11,7 +11,7 @@ import Category from '../models/category'
const TaskFactory = {
tasksForApplyingCategories({threads, categoriesToRemove = () => [], categoriesToAdd = () => [], taskDescription} = {}) {
tasksForApplyingCategories({threads, categoriesToRemove, categoriesToAdd, taskDescription}) {
const byAccount = {}
const tasks = []
@ -22,8 +22,8 @@ const TaskFactory = {
const {accountId} = thread
if (!byAccount[accountId]) {
byAccount[accountId] = {
categoriesToRemove: categoriesToRemove(accountId),
categoriesToAdd: categoriesToAdd(accountId),
categoriesToRemove: categoriesToRemove ? categoriesToRemove(accountId) : [],
categoriesToAdd: categoriesToAdd ? categoriesToAdd(accountId) : [],
threadsToUpdate: [],
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB