mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-13 03:29:46 +08:00
394 lines
13 KiB
CoffeeScript
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
|