mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-12-27 10:33:56 +08:00
feat(unread/spam): New items in the sidebar for unread and spam
Summary: Adds a new unified "Spam" folder and a unified "Unread" view, which shows all the messages in your inbox which are unread. Test Plan: Run tests Reviewers: evan, juan Reviewed By: juan Differential Revision: https://phab.nylas.com/D2901
This commit is contained in:
parent
0e2a875f4d
commit
237bad59d8
22 changed files with 268 additions and 66 deletions
|
@ -130,6 +130,14 @@ class SidebarItem
|
|||
id += "-#{opts.name}" if opts.name
|
||||
@forPerspective(id, perspective, opts)
|
||||
|
||||
@forUnread: (accountIds, opts = {}) ->
|
||||
categories = accountIds.map (accId) =>
|
||||
CategoryStore.getStandardCategory(accId, 'inbox')
|
||||
perspective = MailboxPerspective.forUnread(categories)
|
||||
id = 'Unread'
|
||||
id += "-#{opts.name}" if opts.name
|
||||
@forPerspective(id, perspective, opts)
|
||||
|
||||
@forDrafts: (accountIds, opts = {}) ->
|
||||
perspective = MailboxPerspective.forDrafts(accountIds)
|
||||
id = "Drafts-#{opts.name}"
|
||||
|
|
|
@ -37,12 +37,13 @@ class SidebarSection
|
|||
.reject(cats, (cat) -> cat.name is 'drafts')
|
||||
.map (cat) => SidebarItem.forCategories([cat], editable: false, deletable: false)
|
||||
|
||||
unreadItem = SidebarItem.forUnread([account.id])
|
||||
starredItem = SidebarItem.forStarred([account.id])
|
||||
draftsItem = SidebarItem.forDrafts([account.id])
|
||||
snoozedItem = SidebarItem.forSnoozed([account.id])
|
||||
|
||||
# Order correctly: Inbox, Starred, rest... , Drafts
|
||||
items.splice(1, 0, starredItem, snoozedItem)
|
||||
# Order correctly: Inbox, Unread, Starred, rest... , Drafts
|
||||
items.splice(1, 0, unreadItem, starredItem, snoozedItem)
|
||||
items.push(draftsItem)
|
||||
|
||||
return {
|
||||
|
@ -60,6 +61,7 @@ class SidebarSection
|
|||
'sent',
|
||||
['archive', 'all'],
|
||||
'trash'
|
||||
'spam'
|
||||
]
|
||||
items = []
|
||||
|
||||
|
@ -83,6 +85,9 @@ class SidebarSection
|
|||
starredItem = SidebarItem.forStarred(accountIds,
|
||||
children: accounts.map (acc) -> SidebarItem.forStarred([acc.id], name: acc.label)
|
||||
)
|
||||
unreadItem = SidebarItem.forUnread(accountIds,
|
||||
children: accounts.map (acc) -> SidebarItem.forUnread([acc.id], name: acc.label)
|
||||
)
|
||||
draftsItem = SidebarItem.forDrafts(accountIds,
|
||||
children: accounts.map (acc) -> SidebarItem.forDrafts([acc.id], name: acc.label)
|
||||
)
|
||||
|
@ -90,8 +95,8 @@ class SidebarSection
|
|||
children: accounts.map (acc) -> SidebarItem.forSnoozed([acc.id], name: acc.label)
|
||||
)
|
||||
|
||||
# Order correctly: Inbox, Starred, rest... , Drafts
|
||||
items.splice(1, 0, starredItem, snoozedItem)
|
||||
# Order correctly: Inbox, Unread, Starred, rest... , Drafts
|
||||
items.splice(1, 0, unreadItem, starredItem, snoozedItem)
|
||||
items.push(draftsItem)
|
||||
|
||||
return {
|
||||
|
|
|
@ -38,7 +38,7 @@ class DraftListStore extends NylasStore
|
|||
.where(draft: true, accountId: mailboxPerspective.accountIds)
|
||||
.page(0, 1)
|
||||
|
||||
subscription = new MutableQuerySubscription(query, {asResultSet: true})
|
||||
subscription = new MutableQuerySubscription(query, {emitResultSet: true})
|
||||
$resultSet = Rx.Observable.fromNamedQuerySubscription('draft-list', subscription)
|
||||
$resultSet = Rx.Observable.combineLatest [
|
||||
$resultSet,
|
||||
|
|
|
@ -15,8 +15,8 @@ class ThreadTrashButton extends React.Component
|
|||
thread: React.PropTypes.object.isRequired
|
||||
|
||||
render: =>
|
||||
canTrashThreads = FocusedPerspectiveStore.current().canTrashThreads([@props.thread])
|
||||
return <span /> unless canTrashThreads
|
||||
allowed = FocusedPerspectiveStore.current().canMoveThreadsTo([@props.thread], 'trash')
|
||||
return <span /> unless allowed
|
||||
|
||||
<button className="btn btn-toolbar"
|
||||
style={order: -106}
|
||||
|
|
|
@ -11,6 +11,7 @@ SelectedItemsStack = require './selected-items-stack'
|
|||
DownButton,
|
||||
TrashButton,
|
||||
ArchiveButton,
|
||||
MarkAsSpamButton,
|
||||
ToggleUnreadButton,
|
||||
ToggleStarredButton} = require "./thread-toolbar-buttons"
|
||||
|
||||
|
@ -45,6 +46,9 @@ module.exports =
|
|||
ComponentRegistry.register TrashButton,
|
||||
role: 'ThreadActionsToolbarButton'
|
||||
|
||||
ComponentRegistry.register MarkAsSpamButton,
|
||||
role: 'ThreadActionsToolbarButton'
|
||||
|
||||
ComponentRegistry.register ToggleStarredButton,
|
||||
role: 'ThreadActionsToolbarButton'
|
||||
|
||||
|
@ -58,6 +62,7 @@ module.exports =
|
|||
ComponentRegistry.unregister MessageListToolbar
|
||||
ComponentRegistry.unregister ArchiveButton
|
||||
ComponentRegistry.unregister TrashButton
|
||||
ComponentRegistry.unregister MarkAsSpamButton
|
||||
ComponentRegistry.unregister ToggleUnreadButton
|
||||
ComponentRegistry.unregister ToggleStarredButton
|
||||
ComponentRegistry.unregister UpButton
|
||||
|
|
|
@ -92,8 +92,8 @@ export default class ThreadListContextMenu {
|
|||
|
||||
archiveItem() {
|
||||
const perspective = FocusedPerspectiveStore.current()
|
||||
const canArchiveThreads = perspective.canArchiveThreads(this.threads)
|
||||
if (!canArchiveThreads) {
|
||||
const allowed = perspective.canArchiveThreads(this.threads)
|
||||
if (!allowed) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
|
@ -109,8 +109,8 @@ export default class ThreadListContextMenu {
|
|||
|
||||
trashItem() {
|
||||
const perspective = FocusedPerspectiveStore.current()
|
||||
const canTrashThreads = perspective.canTrashThreads(this.threads)
|
||||
if (!canTrashThreads) {
|
||||
const allowed = perspective.canMoveThreadsTo(this.threads, 'trash')
|
||||
if (!allowed) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
|
|
|
@ -60,7 +60,7 @@ _flatMapJoiningMessages = ($threadsResultSet) =>
|
|||
|
||||
_observableForThreadMessages = (id, initialModels) ->
|
||||
subscription = new QuerySubscription(DatabaseStore.findAll(Message, threadId: id), {
|
||||
asResultSet: true,
|
||||
emitResultSet: true,
|
||||
initialModels: initialModels
|
||||
})
|
||||
Rx.Observable.fromNamedQuerySubscription('message-'+id, subscription)
|
||||
|
|
|
@ -11,8 +11,8 @@ class ThreadArchiveQuickAction extends React.Component
|
|||
thread: React.PropTypes.object
|
||||
|
||||
render: =>
|
||||
canArchiveThreads = FocusedPerspectiveStore.current().canArchiveThreads([@props.thread])
|
||||
return <span /> unless canArchiveThreads
|
||||
allowed = FocusedPerspectiveStore.current().canArchiveThreads([@props.thread])
|
||||
return <span /> unless allowed
|
||||
|
||||
<div
|
||||
key="archive"
|
||||
|
@ -38,8 +38,8 @@ class ThreadTrashQuickAction extends React.Component
|
|||
thread: React.PropTypes.object
|
||||
|
||||
render: =>
|
||||
canTrashThreads = FocusedPerspectiveStore.current().canTrashThreads([@props.thread])
|
||||
return <span /> unless canTrashThreads
|
||||
allowed = FocusedPerspectiveStore.current().canMoveThreadsTo([@props.thread], 'trash')
|
||||
return <span /> unless allowed
|
||||
|
||||
<div
|
||||
key="remove"
|
||||
|
|
|
@ -18,8 +18,8 @@ class ArchiveButton extends React.Component
|
|||
items: React.PropTypes.array.isRequired
|
||||
|
||||
render: ->
|
||||
canArchiveThreads = FocusedPerspectiveStore.current().canArchiveThreads(@props.items)
|
||||
return <span /> unless canArchiveThreads
|
||||
allowed = FocusedPerspectiveStore.current().canArchiveThreads(@props.items)
|
||||
return <span /> unless allowed
|
||||
|
||||
<button
|
||||
tabIndex={-1}
|
||||
|
@ -38,6 +38,7 @@ class ArchiveButton extends React.Component
|
|||
event.stopPropagation()
|
||||
return
|
||||
|
||||
|
||||
class TrashButton extends React.Component
|
||||
@displayName: 'TrashButton'
|
||||
@containerRequired: false
|
||||
|
@ -46,8 +47,8 @@ class TrashButton extends React.Component
|
|||
items: React.PropTypes.array.isRequired
|
||||
|
||||
render: ->
|
||||
canTrashThreads = FocusedPerspectiveStore.current().canTrashThreads(@props.items)
|
||||
return <span /> unless canTrashThreads
|
||||
allowed = FocusedPerspectiveStore.current().canMoveThreadsTo(@props.items, 'trash')
|
||||
return <span /> unless allowed
|
||||
|
||||
<button tabIndex={-1}
|
||||
style={order:-106}
|
||||
|
@ -66,6 +67,34 @@ class TrashButton extends React.Component
|
|||
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
|
||||
|
@ -83,7 +112,7 @@ class ToggleStarredButton extends React.Component
|
|||
imageName = "toolbar-star.png"
|
||||
|
||||
<button tabIndex={-1}
|
||||
style={order:-104}
|
||||
style={order:-103}
|
||||
className="btn btn-toolbar"
|
||||
title={title}
|
||||
onClick={@_onStar}>
|
||||
|
@ -109,7 +138,7 @@ class ToggleUnreadButton extends React.Component
|
|||
fragment = if postClickUnreadState then "unread" else "read"
|
||||
|
||||
<button tabIndex={-1}
|
||||
style={order:-105}
|
||||
style={order:-104}
|
||||
className="btn btn-toolbar"
|
||||
title="Mark as #{fragment}"
|
||||
onClick={@_onClick}>
|
||||
|
@ -205,6 +234,7 @@ module.exports = {
|
|||
UpButton,
|
||||
DownButton,
|
||||
TrashButton,
|
||||
MarkAsSpamButton,
|
||||
ArchiveButton,
|
||||
ToggleStarredButton,
|
||||
ToggleUnreadButton
|
||||
|
|
|
@ -13,7 +13,7 @@ import SearchActions from './search-actions'
|
|||
class SearchQuerySubscription extends MutableQuerySubscription {
|
||||
|
||||
constructor(searchQuery, accountIds) {
|
||||
super(null, {asResultSet: true})
|
||||
super(null, {emitResultSet: true})
|
||||
this._searchQuery = searchQuery
|
||||
this._accountIds = accountIds
|
||||
|
||||
|
@ -152,19 +152,11 @@ class SearchQuerySubscription extends MutableQuerySubscription {
|
|||
this.resetData()
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
onLastCallbackRemoved() {
|
||||
this.reportSearchMetrics();
|
||||
this._connections.forEach((conn) => conn.end())
|
||||
this._unsubscribers.forEach((unsub) => unsub())
|
||||
}
|
||||
|
||||
removeCallback(callback) {
|
||||
super.removeCallback(callback)
|
||||
|
||||
if (this.callbackCount() === 0) {
|
||||
this.reportSearchMetrics()
|
||||
this.cleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchQuerySubscription
|
||||
|
|
|
@ -11,7 +11,7 @@ proxyquire = require 'proxyquire'
|
|||
Utils,
|
||||
} = require 'nylas-exports'
|
||||
|
||||
ParticipantsTextField = proxyquire '../src/components/participants-text-field',
|
||||
ParticipantsTextField = proxyquire '../../src/components/participants-text-field',
|
||||
'nylas-exports': {Contact, ContactStore}
|
||||
|
||||
participant1 = new Contact
|
||||
|
|
|
@ -63,35 +63,38 @@ describe('MailboxPerspective', ()=> {
|
|||
});
|
||||
});
|
||||
|
||||
describe('canTrashThreads', ()=> {
|
||||
it('returns false if the perspective is trash', ()=> {
|
||||
describe('canMoveThreadsTo', ()=> {
|
||||
it('returns false if the perspective is the target folder', ()=> {
|
||||
const accounts = [
|
||||
{canTrashThreads: () => true},
|
||||
{canTrashThreads: () => true},
|
||||
{id: 'a'},
|
||||
{id: 'b'},
|
||||
]
|
||||
spyOn(AccountStore, 'accountsForItems').andReturn(accounts)
|
||||
spyOn(this.perspective, 'isTrash').andReturn(true)
|
||||
expect(this.perspective.canTrashThreads()).toBe(false)
|
||||
spyOn(this.perspective, 'categoriesSharedName').andReturn('trash')
|
||||
expect(this.perspective.canMoveThreadsTo([], 'trash')).toBe(false)
|
||||
});
|
||||
|
||||
it('returns false if one of the accounts associated with the threads cannot archive', ()=> {
|
||||
it('returns false if one of the accounts associated with the threads does not have the folder', ()=> {
|
||||
const accounts = [
|
||||
{canTrashThreads: () => true},
|
||||
{canTrashThreads: () => false},
|
||||
{id: 'a'},
|
||||
{id: 'b'},
|
||||
]
|
||||
spyOn(CategoryStore, 'getStandardCategory').andReturn(null)
|
||||
spyOn(AccountStore, 'accountsForItems').andReturn(accounts)
|
||||
spyOn(this.perspective, 'isTrash').andReturn(false)
|
||||
expect(this.perspective.canTrashThreads()).toBe(false)
|
||||
spyOn(this.perspective, 'categoriesSharedName').andReturn('inbox')
|
||||
expect(this.perspective.canMoveThreadsTo([], 'trash')).toBe(false)
|
||||
});
|
||||
|
||||
it('returns true otherwise', ()=> {
|
||||
const accounts = [
|
||||
{canTrashThreads: () => true},
|
||||
{canTrashThreads: () => true},
|
||||
{id: 'a'},
|
||||
{id: 'b'},
|
||||
]
|
||||
const category = {id: 'cat'};
|
||||
spyOn(CategoryStore, 'getStandardCategory').andReturn(category)
|
||||
spyOn(AccountStore, 'accountsForItems').andReturn(accounts)
|
||||
spyOn(this.perspective, 'isTrash').andReturn(false)
|
||||
expect(this.perspective.canTrashThreads()).toBe(true)
|
||||
spyOn(this.perspective, 'categoriesSharedName').andReturn('inbox')
|
||||
expect(this.perspective.canMoveThreadsTo([], 'trash')).toBe(true)
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ export default class QuerySubscription {
|
|||
|
||||
if (this._query) {
|
||||
if (this._query._count) {
|
||||
throw new Error("QuerySubscriptionPool::add - You cannot listen to count queries.")
|
||||
throw new Error("QuerySubscription::constructor - You cannot listen to count queries.")
|
||||
}
|
||||
|
||||
this._query.finalize();
|
||||
|
@ -62,6 +62,13 @@ export default class QuerySubscription {
|
|||
throw new Error("QuerySubscription:removeCallback - expects a function, received #{callback}")
|
||||
}
|
||||
this._callbacks = _.without(this._callbacks, callback);
|
||||
if (this.callbackCount() === 0) {
|
||||
this.onLastCallbackRemoved()
|
||||
}
|
||||
}
|
||||
|
||||
onLastCallbackRemoved = () => {
|
||||
|
||||
}
|
||||
|
||||
callbackCount = () => {
|
||||
|
@ -270,7 +277,7 @@ export default class QuerySubscription {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this._options.asResultSet) {
|
||||
if (this._options.emitResultSet) {
|
||||
this._set.setQuery(this._query);
|
||||
this._lastResult = this._set.immutableClone();
|
||||
} else {
|
||||
|
|
|
@ -271,9 +271,10 @@ class ModelQuery
|
|||
limit += " OFFSET #{@_range.offset}"
|
||||
|
||||
distinct = if @_distinct then ' DISTINCT' else ''
|
||||
allMatchers = @matchersFlattened()
|
||||
|
||||
joins = @_matchers.filter (matcher) -> matcher.attr instanceof AttributeCollection
|
||||
if joins.length is 1 and @_canSubselectForJoin(joins[0])
|
||||
joins = allMatchers.filter (matcher) -> matcher.attr instanceof AttributeCollection
|
||||
if joins.length is 1 and @_canSubselectForJoin(joins[0], allMatchers)
|
||||
subSql = @_subselectSQL(joins[0], @_matchers, order, limit)
|
||||
return "SELECT#{distinct} #{result} FROM `#{@_klass.name}` WHERE `id` IN (#{subSql}) #{order}"
|
||||
else
|
||||
|
@ -287,13 +288,13 @@ class ModelQuery
|
|||
#
|
||||
# Note: This is currently only intended for use in the thread list
|
||||
#
|
||||
_canSubselectForJoin: (matcher) ->
|
||||
_canSubselectForJoin: (matcher, allMatchers) ->
|
||||
joinAttribute = matcher.attribute()
|
||||
|
||||
return unless @_range.limit?
|
||||
|
||||
allMatchersOnJoinTable = _.every @_matchers, (m) ->
|
||||
m is matcher or joinAttribute.joinQueryableBy.indexOf(m.attr.modelKey) isnt -1
|
||||
allMatchersOnJoinTable = _.every allMatchers, (m) ->
|
||||
m is matcher or joinAttribute.joinQueryableBy.indexOf(m.attr.modelKey) isnt -1 or m.attr.modelKey is 'id'
|
||||
allOrdersOnJoinTable = _.every @_orders, (o) ->
|
||||
joinAttribute.joinQueryableBy.indexOf(o.attr.modelKey) isnt -1
|
||||
|
||||
|
@ -364,6 +365,18 @@ class ModelQuery
|
|||
matchers: ->
|
||||
@_matchers
|
||||
|
||||
matchersFlattened: ->
|
||||
all = []
|
||||
traverse = (matchers) ->
|
||||
return unless matchers instanceof Array
|
||||
for m in matchers
|
||||
if m.children
|
||||
traverse(m.children)
|
||||
else
|
||||
all.push(m)
|
||||
traverse(@_matchers)
|
||||
all
|
||||
|
||||
matcherValueForModelKey: (key) ->
|
||||
matcher = _.find @_matchers, (m) -> m.attr.modelKey = key
|
||||
matcher?.val
|
||||
|
|
48
src/flux/models/unread-query-subscription.es6
Normal file
48
src/flux/models/unread-query-subscription.es6
Normal file
|
@ -0,0 +1,48 @@
|
|||
import MutableQuerySubscription from './mutable-query-subscription';
|
||||
import DatabaseStore from '../stores/database-store';
|
||||
import RecentlyReadStore from '../stores/recently-read-store';
|
||||
import Matcher from '../attributes/matcher';
|
||||
import Thread from '../models/thread';
|
||||
|
||||
const buildQuery = (categoryIds) => {
|
||||
const unreadMatchers = new Matcher.And([
|
||||
Thread.attributes.categories.containsAny(categoryIds),
|
||||
Thread.attributes.unread.equal(true),
|
||||
Thread.attributes.inAllMail.equal(true),
|
||||
]);
|
||||
|
||||
const query = DatabaseStore.findAll(Thread).limit(0);
|
||||
|
||||
// The "Unread" view shows all threads which are unread. When you read a thread,
|
||||
// it doesn't disappear until you leave the view and come back. This behavior
|
||||
// is implemented by keeping track of messages being rea and manually
|
||||
// whitelisting them in the query.
|
||||
if (RecentlyReadStore.ids.length === 0) {
|
||||
query.where(unreadMatchers);
|
||||
} else {
|
||||
query.where(new Matcher.Or([
|
||||
unreadMatchers,
|
||||
Thread.attributes.id.in(RecentlyReadStore.ids),
|
||||
]));
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
export default class UnreadQuerySubscription extends MutableQuerySubscription {
|
||||
|
||||
constructor(categoryIds) {
|
||||
super(buildQuery(categoryIds), {emitResultSet: true})
|
||||
this._categoryIds = categoryIds;
|
||||
this._unlisten = RecentlyReadStore.listen(this.onRecentlyReadChanged);
|
||||
}
|
||||
|
||||
onRecentlyReadChanged = () => {
|
||||
const {limit, offset} = this._query.range()
|
||||
this._query = buildQuery(this._categoryIds).limit(limit).offset(offset);
|
||||
}
|
||||
|
||||
onLastCallbackRemoved = () => {
|
||||
this._unlisten();
|
||||
}
|
||||
}
|
|
@ -235,7 +235,8 @@ FileDownloadStore = Reflux.createStore
|
|||
defaultPath = @_defaultSaveDir()
|
||||
options = {
|
||||
defaultPath,
|
||||
properties: ['openDirectory'],
|
||||
title: 'Save Into...',
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
}
|
||||
|
||||
NylasEnv.showOpenDialog options, (selected) =>
|
||||
|
|
53
src/flux/stores/recently-read-store.es6
Normal file
53
src/flux/stores/recently-read-store.es6
Normal file
|
@ -0,0 +1,53 @@
|
|||
import NylasStore from 'nylas-store';
|
||||
import ChangeUnreadTask from '../tasks/change-unread-task';
|
||||
import ChangeLabelsTask from '../tasks/change-labels-task';
|
||||
import ChangeFolderTask from '../tasks/change-folder-task';
|
||||
import Actions from '../actions';
|
||||
|
||||
// The "Unread" view shows all threads which are unread. When you read a thread,
|
||||
// it doesn't disappear until you leave the view and come back. This behavior
|
||||
// is implemented by keeping track of messages being rea and manually
|
||||
// whitelisting them in the query.
|
||||
|
||||
class RecentlyReadStore extends NylasStore {
|
||||
constructor() {
|
||||
super();
|
||||
this.ids = [];
|
||||
this.listenTo(Actions.focusMailboxPerspective, () => {
|
||||
this.ids = [];
|
||||
this.trigger();
|
||||
});
|
||||
this.listenTo(Actions.queueTasks, (tasks) => {
|
||||
this.tasksQueued(tasks);
|
||||
});
|
||||
this.listenTo(Actions.queueTask, (task) => {
|
||||
this.tasksQueued([task]);
|
||||
});
|
||||
}
|
||||
|
||||
tasksQueued(tasks) {
|
||||
let changed = false;
|
||||
|
||||
tasks.filter(task =>
|
||||
task instanceof ChangeUnreadTask
|
||||
).forEach(({threads}) => {
|
||||
const threadIds = threads.map(t => t.id ? t.id : t);
|
||||
this.ids = this.ids.concat(threadIds);
|
||||
changed = true;
|
||||
});
|
||||
|
||||
tasks.filter(task =>
|
||||
task instanceof ChangeLabelsTask || task instanceof ChangeFolderTask
|
||||
).forEach(({threads}) => {
|
||||
const threadIds = threads.map(t => t.id ? t.id : t);
|
||||
this.ids = this.ids.filter(id => !threadIds.includes(id));
|
||||
changed = true;
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
this.trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new RecentlyReadStore();
|
|
@ -7,10 +7,14 @@ CategoryStore = require './flux/stores/category-store'
|
|||
DatabaseStore = require './flux/stores/database-store'
|
||||
OutboxStore = require './flux/stores/outbox-store'
|
||||
ThreadCountsStore = require './flux/stores/thread-counts-store'
|
||||
RecentlyReadStore = require './flux/stores/recently-read-store'
|
||||
MutableQuerySubscription = require './flux/models/mutable-query-subscription'
|
||||
UnreadQuerySubscription = require './flux/models/unread-query-subscription'
|
||||
Matcher = require './flux/attributes/matcher'
|
||||
Thread = require './flux/models/thread'
|
||||
Category = require './flux/models/category'
|
||||
Actions = require './flux/actions'
|
||||
ChangeUnreadTask = null
|
||||
|
||||
# This is a class cluster. Subclasses are not for external use!
|
||||
# https://developer.apple.com/library/ios/documentation/General/Conceptual/CocoaEncyclopedia/ClassClusters/ClassClusters.html
|
||||
|
@ -41,6 +45,9 @@ class MailboxPerspective
|
|||
@forStarred: (accountsOrIds) ->
|
||||
new StarredMailboxPerspective(accountsOrIds)
|
||||
|
||||
@forUnread: (categories) ->
|
||||
new UnreadMailboxPerspective(categories)
|
||||
|
||||
@forInbox: (accountsOrIds) =>
|
||||
@forStandardCategories(accountsOrIds, 'inbox')
|
||||
|
||||
|
@ -49,6 +56,9 @@ class MailboxPerspective
|
|||
if json.type is CategoryMailboxPerspective.name
|
||||
categories = JSON.parse(json.serializedCategories, Utils.registeredObjectReviver)
|
||||
return @forCategories(categories)
|
||||
else if json.type is UnreadMailboxPerspective.name
|
||||
categories = JSON.parse(json.serializedCategories, Utils.registeredObjectReviver)
|
||||
return @forUnread(categories)
|
||||
else if json.type is StarredMailboxPerspective.name
|
||||
return @forStarred(json.accountIds)
|
||||
else if json.type is DraftsMailboxPerspective.name
|
||||
|
@ -131,14 +141,14 @@ class MailboxPerspective
|
|||
throw new Error("receiveThreads: Not implemented in base class.")
|
||||
|
||||
canArchiveThreads: (threads) =>
|
||||
return false if @isArchive()
|
||||
accounts = AccountStore.accountsForItems(threads)
|
||||
accountsCanArchiveThreads = _.every(accounts, (acc) -> acc.canArchiveThreads())
|
||||
return (not @isArchive()) and accountsCanArchiveThreads
|
||||
return _.every(accounts, (acc) -> acc.canArchiveThreads())
|
||||
|
||||
canTrashThreads: (threads) =>
|
||||
accounts = AccountStore.accountsForItems(threads)
|
||||
accountCanTrashThreads = _.every(accounts, (acc) -> acc.canTrashThreads())
|
||||
return (not @isTrash()) and accountCanTrashThreads
|
||||
canMoveThreadsTo: (threads, standardCategoryName) =>
|
||||
return false if @categoriesSharedName() is standardCategoryName
|
||||
return _.every AccountStore.accountsForItems(threads), (acc) ->
|
||||
CategoryStore.getStandardCategory(acc, standardCategoryName)?
|
||||
|
||||
tasksForRemovingItems: (threads) =>
|
||||
if not threads instanceof Array
|
||||
|
@ -180,7 +190,7 @@ class StarredMailboxPerspective extends MailboxPerspective
|
|||
Thread.attributes.inAllMail.equal(true),
|
||||
]).limit(0)
|
||||
|
||||
return new MutableQuerySubscription(query, {asResultSet: true})
|
||||
return new MutableQuerySubscription(query, {emitResultSet: true})
|
||||
|
||||
canReceiveThreadsFromAccountIds: =>
|
||||
super
|
||||
|
@ -205,7 +215,7 @@ class EmptyMailboxPerspective extends MailboxPerspective
|
|||
# index so this returns zero items nearly instantly. In the future, we might
|
||||
# want to make a Query.forNothing() to go along with MailboxPerspective.forNothing()
|
||||
query = DatabaseStore.findAll(Thread).where(lastMessageReceivedTimestamp: -1).limit(0)
|
||||
return new MutableQuerySubscription(query, {asResultSet: true})
|
||||
return new MutableQuerySubscription(query, {emitResultSet: true})
|
||||
|
||||
canReceiveThreadsFromAccountIds: =>
|
||||
false
|
||||
|
@ -253,7 +263,7 @@ class CategoryMailboxPerspective extends MailboxPerspective
|
|||
# can be /much/ slower and we shouldn't do it if we know we don't need it.
|
||||
query.distinct()
|
||||
|
||||
return new MutableQuerySubscription(query, {asResultSet: true})
|
||||
return new MutableQuerySubscription(query, {emitResultSet: true})
|
||||
|
||||
unreadCount: =>
|
||||
sum = 0
|
||||
|
@ -344,4 +354,31 @@ class CategoryMailboxPerspective extends MailboxPerspective
|
|||
)
|
||||
|
||||
|
||||
class UnreadMailboxPerspective extends CategoryMailboxPerspective
|
||||
constructor: (categories) ->
|
||||
super(categories)
|
||||
@name = "Unread"
|
||||
@iconName = "unread.png"
|
||||
@
|
||||
|
||||
threads: =>
|
||||
return new UnreadQuerySubscription(_.pluck(@categories(), 'id'))
|
||||
|
||||
unreadCount: =>
|
||||
0
|
||||
|
||||
receiveThreads: (threadsOrIds) =>
|
||||
super(threadsOrIds)
|
||||
|
||||
ChangeUnreadTask ?= require './flux/tasks/change-unread-task'
|
||||
task = new ChangeUnreadTask({threads:threadsOrIds, unread: true})
|
||||
Actions.queueTask(task)
|
||||
|
||||
tasksForRemovingItems: (threads, ruleset) =>
|
||||
ChangeUnreadTask ?= require './flux/tasks/change-unread-task'
|
||||
tasks = super(threads, ruleset)
|
||||
tasks.push new ChangeUnreadTask({threads, unread: false})
|
||||
return tasks
|
||||
|
||||
|
||||
module.exports = MailboxPerspective
|
||||
|
|
BIN
static/images/source-list/unread@1x.png
Normal file
BIN
static/images/source-list/unread@1x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
static/images/source-list/unread@2x.png
Normal file
BIN
static/images/source-list/unread@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
static/images/toolbar/toolbar-spam@1x.png
Normal file
BIN
static/images/toolbar/toolbar-spam@1x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
BIN
static/images/toolbar/toolbar-spam@2x.png
Normal file
BIN
static/images/toolbar/toolbar-spam@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
Loading…
Reference in a new issue