Mailspring/src/mailbox-perspective.coffee
2016-10-27 18:48:33 -07:00

394 lines
13 KiB
CoffeeScript

_ = require 'underscore'
Utils = require './flux/models/utils'
TaskFactory = require('./flux/tasks/task-factory').default
AccountStore = require './flux/stores/account-store'
CategoryStore = require './flux/stores/category-store'
DatabaseStore = require('./flux/stores/database-store').default
OutboxStore = require('./flux/stores/outbox-store').default
ThreadCountsStore = require './flux/stores/thread-counts-store'
RecentlyReadStore = require('./flux/stores/recently-read-store').default
MutableQuerySubscription = require('./flux/models/mutable-query-subscription').default
UnreadQuerySubscription = require('./flux/models/unread-query-subscription').default
Matcher = require('./flux/attributes/matcher').default
Thread = require('./flux/models/thread').default
Category = require('./flux/models/category').default
Actions = require('./flux/actions').default
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
class MailboxPerspective
# Factory Methods
@forNothing: ->
new EmptyMailboxPerspective()
@forDrafts: (accountsOrIds) ->
new DraftsMailboxPerspective(accountsOrIds)
@forCategory: (category) ->
return @forNothing() unless category
new CategoryMailboxPerspective([category])
@forCategories: (categories) ->
return @forNothing() if categories.length is 0
new CategoryMailboxPerspective(categories)
@forStandardCategories: (accountsOrIds, names...) ->
# TODO this method is broken
categories = CategoryStore.getStandardCategories(accountsOrIds, names...)
@forCategories(categories)
@forStarred: (accountsOrIds) ->
new StarredMailboxPerspective(accountsOrIds)
@forUnread: (categories) ->
return @forNothing() if categories.length is 0
new UnreadMailboxPerspective(categories)
@forInbox: (accountsOrIds) =>
@forStandardCategories(accountsOrIds, 'inbox')
@fromJSON: (json) =>
try
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
return @forDrafts(json.accountIds)
else
return @forInbox(json.accountIds)
catch error
NylasEnv.reportError(new Error("Could not restore mailbox perspective: #{error}"))
return null
# Instance Methods
constructor: (@accountIds) ->
unless @accountIds instanceof Array and _.every(@accountIds, _.isString)
throw new Error("#{@constructor.name}: You must provide an array of string `accountIds`")
@
toJSON: =>
return {accountIds: @accountIds, type: @constructor.name}
isEqual: (other) =>
return false unless other and @constructor is other.constructor
return false unless other.name is @name
return false unless _.isEqual(@accountIds, other.accountIds)
true
isInbox: =>
@categoriesSharedName() is 'inbox'
isSent: =>
@categoriesSharedName() is 'sent'
isTrash: =>
@categoriesSharedName() is 'trash'
isArchive: =>
false
emptyMessage: =>
"Nothing to display"
categories: =>
[]
categoriesSharedName: =>
@_categoriesSharedName ?= Category.categoriesSharedName(@categories())
@_categoriesSharedName
category: =>
return null unless @categories().length is 1
return @categories()[0]
threads: =>
throw new Error("threads: Not implemented in base class.")
unreadCount: =>
0
# Public:
# - accountIds {Array} Array of unique account ids associated with the threads
# that want to be included in this perspective
#
# Returns true if the accountIds are part of the current ids, or false
# otherwise. This means that it checks if I am attempting to move threads
# between the same set of accounts:
#
# E.g.:
# perpective = Starred for accountIds: a1, a2
# thread1 has accountId a3
# thread2 has accountId a2
#
# perspective.canReceiveThreadsFromAccountIds([a2, a3]) -> false -> I cant move those threads to Starred
# perspective.canReceiveThreadsFromAccountIds([a2]) -> true -> I can move that thread to # Starred
canReceiveThreadsFromAccountIds: (accountIds) =>
return false unless accountIds and accountIds.length > 0
areIncomingIdsInCurrent = _.difference(accountIds, @accountIds).length is 0
return areIncomingIdsInCurrent
receiveThreads: (threadsOrIds) =>
throw new Error("receiveThreads: Not implemented in base class.")
canArchiveThreads: (threads) =>
return false if @isArchive()
accounts = AccountStore.accountsForItems(threads)
return _.every(accounts, (acc) -> acc.canArchiveThreads())
canTrashThreads: (threads) =>
@canMoveThreadsTo(threads, 'trash')
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
throw new Error("tasksForRemovingItems: you must pass an array of threads or thread ids")
[]
class DraftsMailboxPerspective extends MailboxPerspective
constructor: (@accountIds) ->
super(@accountIds)
@name = "Drafts"
@iconName = "drafts.png"
@drafts = true # The DraftListStore looks for this
@
threads: =>
null
unreadCount: =>
count = 0
count += OutboxStore.itemsForAccount(aid).length for aid in @accountIds
count
canReceiveThreadsFromAccountIds: =>
false
class StarredMailboxPerspective extends MailboxPerspective
constructor: (@accountIds) ->
super(@accountIds)
@name = "Starred"
@iconName = "starred.png"
@
threads: =>
query = DatabaseStore.findAll(Thread).where([
Thread.attributes.starred.equal(true),
Thread.attributes.inAllMail.equal(true),
]).limit(0)
# Adding a "account_id IN (a,b,c)" clause to our query can result in a full
# table scan. Don't add the where clause if we know we want results from all.
if @accountIds.length < AccountStore.accounts().length
query.where(Thread.attributes.accountId.in(@accountIds))
return new MutableQuerySubscription(query, {emitResultSet: true})
canReceiveThreadsFromAccountIds: =>
super
receiveThreads: (threadsOrIds) =>
ChangeStarredTask = require('./flux/tasks/change-starred-task').default
task = new ChangeStarredTask({threads:threadsOrIds, starred: true})
Actions.queueTask(task)
tasksForRemovingItems: (threads) =>
task = TaskFactory.taskForInvertingStarred(threads: threads)
return [task]
class EmptyMailboxPerspective extends MailboxPerspective
constructor: ->
@accountIds = []
threads: =>
# We need a Thread query that will not return any results and take no time.
# We use lastMessageReceivedTimestamp because it is the first column on an
# 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, {emitResultSet: true})
canReceiveThreadsFromAccountIds: =>
false
class CategoryMailboxPerspective extends MailboxPerspective
constructor: (@_categories) ->
super(_.uniq(_.pluck(@_categories, 'accountId')))
if @_categories.length is 0
throw new Error("CategoryMailboxPerspective: You must provide at least one category.")
# Note: We pick the display name and icon assuming that you won't create a
# perspective with Inbox and Sent or anything crazy like that... todo?
@name = @_categories[0].displayName
if @_categories[0].name
@iconName = "#{@_categories[0].name}.png"
else
account = AccountStore.accountForId(@accountIds[0])
@iconName = "folder.png"
@iconName = account.categoryIcon() if account
@
toJSON: =>
json = super
json.serializedCategories = JSON.stringify(@_categories, Utils.registeredObjectReplacer)
json
isEqual: (other) =>
super(other) and _.isEqual(_.pluck(@categories(), 'id'), _.pluck(other.categories(), 'id'))
threads: =>
query = DatabaseStore.findAll(Thread)
.where([Thread.attributes.categories.containsAny(_.pluck(@categories(), 'id'))])
.limit(0)
if @isSent()
query.order(Thread.attributes.lastMessageSentTimestamp.descending())
unless @categoriesSharedName() in ['spam', 'trash']
query.where(inAllMail: true)
if @_categories.length > 1 and @accountIds.length < @_categories.length
# The user has multiple categories in the same account selected, which
# means our result set could contain multiple copies of the same threads
# (since we do an inner join) and we need SELECT DISTINCT. Note that this
# can be /much/ slower and we shouldn't do it if we know we don't need it.
query.distinct()
return new MutableQuerySubscription(query, {emitResultSet: true})
unreadCount: =>
sum = 0
for cat in @_categories
sum += ThreadCountsStore.unreadCountForCategoryId(cat.id)
sum
categories: =>
@_categories
isArchive: =>
_.every(@_categories, (cat) -> cat.isArchive())
canReceiveThreadsFromAccountIds: =>
super and not _.any @_categories, (c) -> c.isLockedCategory()
receiveThreads: (threadsOrIds) =>
FocusedPerspectiveStore = require('./flux/stores/focused-perspective-store').default
current= FocusedPerspectiveStore.current()
# This assumes that the we don't have more than one category per accountId
# attached to this perspective
DatabaseStore.modelify(Thread, threadsOrIds).then (threads) =>
tasks = TaskFactory.tasksForApplyingCategories
threads: threads
categoriesToRemove: (accountId) ->
if current.categoriesSharedName() in Category.LockedCategoryNames
return []
return _.filter(current.categories(), _.matcher({accountId}))
categoriesToAdd: (accountId) => [_.findWhere(@_categories, {accountId})]
Actions.queueTasks(tasks)
# Public:
# Returns the tasks for removing threads from this perspective and moving them
# to a given target/destination based on a {RemovalTargetRuleset}.
#
# A RemovalTargetRuleset for categories is a map that represents the
# target/destination Category when removing threads from another given
# category, i.e., when removing them the current CategoryPerspective.
# Rulesets are of the form:
#
# [categoryName] -> function(accountId): Category
#
# Keys correspond to category names, e.g.`{'inbox', 'trash',...}`, which
# correspond to the name of the categories associated with the current perspective
# Values are functions with the following signature:
#
# `function(accountId): Category`
#
# If the value of the category name of the current perspective is null instead
# of a function, this method will return an empty array of tasks
#
# RemovalRulesets should also contain a key `other`, that is meant to be used
# when a key cannot be found for the current category name
#
# Example:
# perspective.tasksForRemovingItems(
# threads,
# {
# # Move to trash if the current perspective is inbox
# inbox: (accountId) -> CategoryStore.getTrashCategory(accountId),
#
# # Do nothing if the current perspective is trash
# trash: null,
# }
# )
#
tasksForRemovingItems: (threads, ruleset) =>
if not ruleset
throw new Error("tasksForRemovingItems: you must pass a ruleset object to determine the destination of the threads")
name = if @isArchive()
# TODO this is an awful hack
'archive'
else
@categoriesSharedName()
return [] if ruleset[name] is null
return TaskFactory.tasksForApplyingCategories(
threads: threads,
categoriesToRemove: (accountId) =>
# Remove all categories from this perspective that match the accountId
return _.filter(@_categories, _.matcher({accountId}))
categoriesToAdd: (accId) =>
category = (ruleset[name] ? ruleset.other)(accId)
return if category then [category] else []
)
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').default
task = new ChangeUnreadTask({threads:threadsOrIds, unread: true})
Actions.queueTask(task)
tasksForRemovingItems: (threads, ruleset) =>
ChangeUnreadTask ?= require('./flux/tasks/change-unread-task').default
tasks = super(threads, ruleset)
tasks.push new ChangeUnreadTask({threads, unread: false})
return tasks
module.exports = MailboxPerspective