Unified inbox mailbox perspectives working (sidebar disabled atm)

This commit is contained in:
Ben Gotow 2016-01-18 00:47:04 -08:00
parent 31f808ef35
commit 96f429ff39
34 changed files with 357 additions and 457 deletions

View file

@ -19,7 +19,7 @@ _ = require 'underscore'
class AccountSidebarStore extends NylasStore
constructor: ->
@_sections = []
@_account = FocusedPerspectiveStore.current()?.account
@_account = AccountStore.accounts()[0]
@_registerListeners()
@_updateSections()
@ -97,7 +97,9 @@ class AccountSidebarStore extends NylasStore
category.name is "drafts"
standardCategoryItems = _.map standardCategories, (cat) => @_sidebarItemForCategory(cat)
starredItem = @_sidebarItemForMailView('starred', MailboxPerspective.forStarred(@_account))
starredPerspective = MailboxPerspective.forStarred([@_account.id])
starredItem = @_sidebarItemForMailView('starred', starredPerspective)
# Find root views and add them to the bottom of the list (Drafts, etc.)
standardItems = standardCategoryItems
@ -140,14 +142,13 @@ class AccountSidebarStore extends NylasStore
new WorkspaceStore.SidebarItem
id: category.id,
name: shortenedName || category.displayName
mailboxPerspective: MailboxPerspective.forCategory(@_account, category)
mailboxPerspective: MailboxPerspective.forCategory(category)
unreadCount: @_itemUnreadCount(category)
_createCategory: (displayName) ->
# TODO this needs an account param
return unless @_account?
CategoryClass = @_account.categoryClass()
category = new CategoryClass
category = new Category
displayName: displayName
accountId: @_account.id
Actions.queueTask(new SyncbackCategoryTask({category}))

View file

@ -1,18 +1,18 @@
React = require "react"
AccountSwitcher = require "./account-switcher"
AccountSidebar = require "./account-sidebar"
# AccountSwitcher = require "./account-switcher"
# AccountSidebar = require "./account-sidebar"
{ComponentRegistry, WorkspaceStore} = require "nylas-exports"
module.exports =
item: null # The DOM item the main React component renders into
activate: (@state) ->
ComponentRegistry.register AccountSwitcher,
location: WorkspaceStore.Location.RootSidebar
ComponentRegistry.register AccountSidebar,
location: WorkspaceStore.Location.RootSidebar
# ComponentRegistry.register AccountSwitcher,
# location: WorkspaceStore.Location.RootSidebar
#
# ComponentRegistry.register AccountSidebar,
# location: WorkspaceStore.Location.RootSidebar
deactivate: (@state) ->
ComponentRegistry.unregister(AccountSwitcher)
ComponentRegistry.unregister(AccountSidebar)
# ComponentRegistry.unregister(AccountSwitcher)
# ComponentRegistry.unregister(AccountSidebar)

View file

@ -220,8 +220,7 @@ class CategoryPicker extends React.Component
@refs.menu.setSelectedItem(null)
if item.newCategoryItem
CategoryClass = @_account.categoryClass()
category = new CategoryClass
category = new Category
displayName: @state.searchValue,
accountId: @_account.id
@ -313,7 +312,8 @@ class CategoryPicker extends React.Component
_isUserFacing: (allInInbox, category) =>
hiddenCategories = []
currentCategoryId = FocusedPerspectiveStore.current()?.categoryId()
currentCategories = FocusedPerspectiveStore.current().categories() ? []
currentCategoryIds = _.pluck(currentCategories, 'id')
if @_account?.usesLabels()
hiddenCategories = ["all", "drafts", "sent", "archive", "starred", "important"]
@ -322,7 +322,7 @@ class CategoryPicker extends React.Component
else if @_account?.usesFolders()
hiddenCategories = ["drafts", "sent"]
return (category.name not in hiddenCategories) and (category.id isnt currentCategoryId)
return (category.name not in hiddenCategories) and not (category.id in currentCategoryIds)
_allInInbox: (usageCount, numThreads) ->
return unless @_account?

View file

@ -5,8 +5,7 @@ CategoryPicker = require '../lib/category-picker'
{Popover} = require 'nylas-component-kit'
{Utils,
Label,
Folder,
Category,
Thread,
Actions,
AccountStore,
@ -29,17 +28,15 @@ describe 'CategoryPicker', ->
setupFor = (organizationUnit) ->
NylasEnv.testOrganizationUnit = organizationUnit
@categoryClass = if organizationUnit is "label" then Label else Folder
@account = {
id: TEST_ACCOUNT_ID
usesLabels: -> organizationUnit is "label"
usesFolders: -> organizationUnit isnt "label"
categoryClass: => @categoryClass
}
@inboxCategory = new @categoryClass(id: 'id-123', name: 'inbox', displayName: "INBOX")
@archiveCategory = new @categoryClass(id: 'id-456', name: 'archive', displayName: "ArCHIVe")
@userCategory = new @categoryClass(id: 'id-789', name: null, displayName: "MyCategory")
@inboxCategory = new Category(id: 'id-123', name: 'inbox', displayName: "INBOX")
@archiveCategory = new Category(id: 'id-456', name: 'archive', displayName: "ArCHIVe")
@userCategory = new Category(id: 'id-789', name: null, displayName: "MyCategory")
spyOn(Categories, "forAccount").andReturn NylasTestUtils.mockObservable(
[@inboxCategory, @archiveCategory, @userCategory]
@ -168,7 +165,7 @@ describe 'CategoryPicker', ->
expect(Actions.queueTask).toHaveBeenCalled()
syncbackTask = Actions.queueTask.calls[0].args[0]
newCategory = syncbackTask.category
expect(newCategory instanceof @categoryClass).toBe(true)
expect(newCategory instanceof Category).toBe(true)
expect(newCategory.displayName).toBe "teSTing!"
expect(newCategory.accountId).toBe TEST_ACCOUNT_ID

View file

@ -38,19 +38,19 @@ class SearchSuggestionStore extends NylasStore
_onQuerySubmitted: (query) =>
@_searchQuery = query
perspective = FocusedPerspectiveStore.current()
account = perspective.account
current = FocusedPerspectiveStore.current()
if @_searchQuery.trim().length > 0
@_perspectiveBeforeSearch ?= perspective
Actions.focusMailboxPerspective(MailboxPerspective.forSearch(account, @_searchQuery))
@_perspectiveBeforeSearch ?= current
next = MailboxPerspective.forSearch(current.accountIds, @_searchQuery)
Actions.focusMailboxPerspective(next)
else if @_searchQuery.trim().length is 0
if @_perspectiveBeforeSearch
Actions.focusMailboxPerspective(@_perspectiveBeforeSearch)
@_perspectiveBeforeSearch = null
else
Actions.focusDefaultMailboxPerspectiveForAccount(account)
Actions.focusDefaultMailboxPerspectiveForAccount(current.accountIds[0])
@_clearResults()

View file

@ -64,10 +64,10 @@ c3 = new ListTabular.Column
if thread.hasAttachments
attachment = <div className="thread-icon thread-icon-attachment"></div>
currentCategoryId = FocusedPerspectiveStore.current()?.categoryId()
account = FocusedPerspectiveStore.current()?.account
currentCategories = FocusedPerspectiveStore.current().categories() ? []
account = FocusedPerspectiveStore.current().account
ignoredIds = [currentCategoryId]
ignoredIds = _.pluck(currentCategories, 'id')
ignoredIds.push(cat.id) for cat in CategoryStore.hiddenCategories(account)
for label in (thread.sortedLabels())

View file

@ -56,7 +56,7 @@ module.exports =
else
NylasEnv.displayWindow()
MailboxPerspective filter = MailboxPerspective.forCategory(account, thread.categoryNamed('inbox'))
filter = MailboxPerspective.forCategory(thread.categoryNamed('inbox'))
Actions.focusMailboxPerspective(filter)
Actions.setFocus(collection: 'thread', item: thread)

View file

@ -24,20 +24,20 @@ describe "FocusedPerspectiveStore", ->
FocusedPerspectiveStore._perspective = null
FocusedPerspectiveStore._onCategoryStoreChanged()
expect(FocusedPerspectiveStore.current()).not.toBe(null)
expect(FocusedPerspectiveStore.current().categoryId()).toEqual(@inboxCategory.id)
expect(FocusedPerspectiveStore.current().categories()).toEqual([@inboxCategory])
it "should set the current category to Inbox when the current category no longer exists in the CategoryStore", ->
otherAccountInbox = @inboxCategory.clone()
otherAccountInbox.serverId = 'other-id'
FocusedPerspectiveStore._perspective = MailboxPerspective.forCategory(@account, otherAccountInbox)
FocusedPerspectiveStore._perspective = MailboxPerspective.forCategory(otherAccountInbox)
FocusedPerspectiveStore._onCategoryStoreChanged()
expect(FocusedPerspectiveStore.current().categoryId()).toEqual(@inboxCategory.id)
expect(FocusedPerspectiveStore.current().categories()).toEqual([@inboxCategory])
describe "_onFocusPerspective", ->
it "should focus the category and trigger when Actions.focusCategory is called", ->
FocusedPerspectiveStore._onFocusPerspective(@userFilter)
expect(FocusedPerspectiveStore.trigger).toHaveBeenCalled()
expect(FocusedPerspectiveStore.current().categoryId()).toEqual(@userCategory.id)
expect(FocusedPerspectiveStore.current().categories()).toEqual([@userCategory])
it "should do nothing if the category is already focused", ->
FocusedPerspectiveStore._onFocusPerspective(@inboxFilter)
@ -50,9 +50,9 @@ describe "FocusedPerspectiveStore", ->
NylasEnv.testOrganizationUnit = 'label'
@inboxCategory = new Label(id: 'id-123', name: 'inbox', displayName: "INBOX")
@inboxFilter = MailboxPerspective.forCategory(@account, @inboxCategory)
@inboxFilter = MailboxPerspective.forCategory(@inboxCategory)
@userCategory = new Label(id: 'id-456', name: null, displayName: "MyCategory")
@userFilter = MailboxPerspective.forCategory(@account, @userCategory)
@userFilter = MailboxPerspective.forCategory(@userCategory)
spyOn(CategoryStore, "getStandardCategory").andReturn @inboxCategory
spyOn(CategoryStore, "byId").andCallFake (id) =>
@ -67,9 +67,9 @@ describe "FocusedPerspectiveStore", ->
NylasEnv.testOrganizationUnit = 'folder'
@inboxCategory = new Folder(id: 'id-123', name: 'inbox', displayName: "INBOX")
@inboxFilter = MailboxPerspective.forCategory(@account, @inboxCategory)
@inboxFilter = MailboxPerspective.forCategory(@inboxCategory)
@userCategory = new Folder(id: 'id-456', name: null, displayName: "MyCategory")
@userFilter = MailboxPerspective.forCategory(@account, @userCategory)
@userFilter = MailboxPerspective.forCategory(@userCategory)
spyOn(CategoryStore, "getStandardCategory").andReturn @inboxCategory

View file

@ -2,8 +2,7 @@
NylasAPI,
Task,
APIError,
Label,
Folder,
Category,
DatabaseStore,
DatabaseTransaction} = require "nylas-exports"
@ -20,8 +19,8 @@ describe "DestroyCategoryTask", ->
nameOf = (fn) ->
fn.calls[0].args[0].body.display_name
makeTask = (CategoryClass) ->
category = new CategoryClass
makeTask = ->
category = new Category
displayName: "important emails"
accountId: "account 123"
serverId: "server-444"
@ -34,7 +33,7 @@ describe "DestroyCategoryTask", ->
describe "performLocal", ->
it "sets an `isDeleted` flag and persists the category", ->
task = makeTask(Folder)
task = makeTask()
runs =>
task.performLocal()
waitsFor =>
@ -47,7 +46,7 @@ describe "DestroyCategoryTask", ->
describe "performRemote", ->
it "throws error when no category present", ->
waitsForPromise ->
task = makeTask(Label)
task = makeTask()
task.category = null
task.performRemote()
.then ->
@ -57,7 +56,7 @@ describe "DestroyCategoryTask", ->
it "throws error when category does not have a serverId", ->
waitsForPromise ->
task = makeTask(Label)
task = makeTask()
task.category.serverId = undefined
task.performRemote()
.then ->
@ -70,22 +69,22 @@ describe "DestroyCategoryTask", ->
spyOn(NylasAPI, "makeRequest").andCallFake -> Promise.resolve("null")
it "sends API req to /labels if user uses labels", ->
task = makeTask(Label)
task = makeTask()
task.performRemote()
expect(pathOf(NylasAPI.makeRequest)).toBe "/labels/server-444"
it "sends API req to /folders if user uses folders", ->
task = makeTask(Folder)
task = makeTask()
task.performRemote()
expect(pathOf(NylasAPI.makeRequest)).toBe "/folders/server-444"
it "sends DELETE request", ->
task = makeTask(Folder)
task = makeTask()
task.performRemote()
expect(methodOf(NylasAPI.makeRequest)).toBe "DELETE"
it "sends the account id", ->
task = makeTask(Label)
task = makeTask()
task.performRemote()
expect(accountIdOf(NylasAPI.makeRequest)).toBe "account 123"
@ -97,7 +96,7 @@ describe "DestroyCategoryTask", ->
it "updates the isDeleted flag for the category and notifies error", ->
waitsForPromise ->
task = makeTask(Folder)
task = makeTask()
spyOn(task, "_notifyUserOfError")
task.performRemote().then (status) ->

View file

@ -1,6 +1,4 @@
{Label,
NylasAPI,
Folder,
{NylasAPI,
DatabaseStore,
SyncbackCategoryTask,
DatabaseTransaction} = require "nylas-exports"
@ -16,8 +14,8 @@ describe "SyncbackCategoryTask", ->
nameOf = (fn) ->
fn.calls[0].args[0].body.display_name
makeTask = (CategoryClass) ->
category = new CategoryClass
makeTask = ->
category = new Category
displayName: "important emails"
accountId: "account 123"
clientId: "local-444"
@ -30,29 +28,29 @@ describe "SyncbackCategoryTask", ->
spyOn(DatabaseTransaction.prototype, "_query").andCallFake => Promise.resolve([])
spyOn(DatabaseTransaction.prototype, "persistModel")
it "sends API req to /labels if user uses labels", ->
task = makeTask(Label)
it "sends API req to /labels if the account uses labels", ->
task = makeTask()
task.performRemote({})
expect(pathOf(NylasAPI.makeRequest)).toBe "/labels"
it "sends API req to /folders if user uses folders", ->
task = makeTask(Folder)
it "sends API req to /folders if the account uses folders", ->
task = makeTask()
task.performRemote({})
expect(pathOf(NylasAPI.makeRequest)).toBe "/folders"
it "sends the account id", ->
task = makeTask(Label)
task = makeTask()
task.performRemote({})
expect(accountIdOf(NylasAPI.makeRequest)).toBe "account 123"
it "sends the display name in the body", ->
task = makeTask(Label)
task = makeTask()
task.performRemote({})
expect(nameOf(NylasAPI.makeRequest)).toBe "important emails"
it "adds server id to the category, then saves the category", ->
waitsForPromise ->
task = makeTask(Label)
task = makeTask()
task.performRemote({})
.then ->
expect(DatabaseTransaction.prototype.persistModel).toHaveBeenCalled()

View file

@ -123,14 +123,14 @@ class ComponentRegistry
if not descriptor?
throw new Error("ComponentRegistry.findComponentsMatching called without descriptor")
cacheKey = JSON.stringify(descriptor)
return @_cache[cacheKey] if @_cache[cacheKey]
{locations, modes, roles} = @_pluralizeDescriptor(descriptor)
if not locations and not modes and not roles
throw new Error("ComponentRegistry.findComponentsMatching called with an empty descriptor")
cacheKey = JSON.stringify({locations, modes, roles})
return [].concat(@_cache[cacheKey]) if @_cache[cacheKey]
# Made into a convenience function because default
# values (`[]`) are necessary and it was getting messy.
overlaps = (entry = [], search = []) ->
@ -148,7 +148,8 @@ class ComponentRegistry
results = _.map entries, (entry) -> entry.component
@_cache[cacheKey] = results
return results
return [].concat(results)
triggerDebounced: _.debounce(( -> @trigger(@)), 1)

View file

@ -5,43 +5,43 @@ _ = require 'underscore'
module.exports =
class MultiselectListInteractionHandler
constructor: (@props) ->
{@onFocusItemItem, @onSetCursorPosition} = @props
{@onFocusItem, @onSetCursorPosition} = @props
cssClass: ->
cssClass: =>
'handler-list'
shouldShowFocus: ->
shouldShowFocus: =>
false
shouldShowCheckmarks: ->
shouldShowCheckmarks: =>
true
shouldShowKeyboardCursor: ->
shouldShowKeyboardCursor: =>
true
onClick: (item) ->
onClick: (item) =>
@onFocusItem(item)
onMetaClick: (item) ->
onMetaClick: (item) =>
@props.dataSource.selection.toggle(item)
@onSetCursorPosition(item)
onShiftClick: (item) ->
onShiftClick: (item) =>
@props.dataSource.selection.expandTo(item)
@onSetCursorPosition(item)
onEnter: ->
onEnter: =>
keyboardCursorId = @props.keyboardCursorId
if keyboardCursorId
item = @props.dataSource.getById(keyboardCursorId)
@onFocusItem(item)
onSelect: ->
onSelect: =>
{id} = @_keyboardContext()
return unless id
@props.dataSource.selection.toggle(@props.dataSource.getById(id))
onShift: (delta, options = {}) ->
onShift: (delta, options = {}) =>
{id, action} = @_keyboardContext()
current = @props.dataSource.getById(id)
@ -53,7 +53,7 @@ class MultiselectListInteractionHandler
if options.select
@props.dataSource.selection.walk({current, next})
_keyboardContext: ->
_keyboardContext: =>
if WorkspaceStore.topSheet().root
{id: @props.keyboardCursorId, action: @onSetCursorPosition}
else

View file

@ -7,39 +7,39 @@ class MultiselectSplitInteractionHandler
constructor: (@props) ->
{@onFocusItem, @onSetCursorPosition} = @props
cssClass: ->
cssClass: =>
'handler-split'
shouldShowFocus: ->
shouldShowFocus: =>
true
shouldShowCheckmarks: ->
shouldShowCheckmarks: =>
false
shouldShowKeyboardCursor: ->
shouldShowKeyboardCursor: =>
@props.dataSource.selection.count() > 1
onClick: (item) ->
onClick: (item) =>
@onFocusItem(item)
@props.dataSource.selection.clear()
@_checkSelectionAndFocusConsistency()
onMetaClick: (item) ->
onMetaClick: (item) =>
@_turnFocusIntoSelection()
@props.dataSource.selection.toggle(item)
@_checkSelectionAndFocusConsistency()
onShiftClick: (item) ->
onShiftClick: (item) =>
@_turnFocusIntoSelection()
@props.dataSource.selection.expandTo(item)
@_checkSelectionAndFocusConsistency()
onEnter: ->
onEnter: =>
onSelect: ->
onSelect: =>
@_checkSelectionAndFocusConsistency()
onShift: (delta, options) ->
onShift: (delta, options) =>
if options.select
@_turnFocusIntoSelection()
@ -63,12 +63,12 @@ class MultiselectSplitInteractionHandler
@_checkSelectionAndFocusConsistency()
_turnFocusIntoSelection: ->
_turnFocusIntoSelection: =>
focused = @props.focused
@onFocusItem(null)
@props.dataSource.selection.add(focused)
_checkSelectionAndFocusConsistency: ->
_checkSelectionAndFocusConsistency: =>
focused = @props.focused
selection = @props.dataSource.selection

View file

@ -73,7 +73,7 @@ class Matcher
joinSQL: (klass) ->
switch @comparator
when 'contains'
when 'contains', 'containsAny'
joinTable = tableNameForJoin(klass, @attr.itemClass)
return "INNER JOIN `#{joinTable}` AS `M#{@muid}` ON `M#{@muid}`.`id` = `#{klass.name}`.`id`"
else
@ -93,7 +93,9 @@ class Matcher
escaped = 0
else if val instanceof Array
escapedVals = []
escapedVals.push("'#{v.replace(/'/g, '\\\'')}'") for v in val
for v in val
throw new Error("#{@attr.jsonKey} value #{v} must be a string.") unless _.isString(v)
escapedVals.push("'#{v.replace(/'/g, '\\\'')}'")
escaped = "(#{escapedVals.join(',')})"
else
escaped = val
@ -106,7 +108,7 @@ class Matcher
when 'containsAny'
return "`M#{@muid}`.`value` IN #{escaped}"
else
return "`#{klass.tableName}`.`#{@attr.jsonKey}` #{@comparator} #{escaped}"
return "`#{klass.name}`.`#{@attr.jsonKey}` #{@comparator} #{escaped}"
Matcher.muid = 0

View file

@ -89,14 +89,6 @@ class Account extends Model
usesFolders: ->
@organizationUnit is "folder"
categoryClass: ->
if @usesLabels()
return require './label'
else if @usesFolders()
return require './folder'
else
return null
# Public: Returns the localized, properly capitalized provider name,
# like Gmail, Exchange, or Outlook 365
displayProvider: ->

View file

@ -82,6 +82,12 @@ class Category extends Model
super
@
displayType: ->
if AccountStore.accountForId(@category.accountId).usesLabels()
return 'label'
else
return 'folder'
hue: ->
return 0 unless @displayName
hue = 0

View file

@ -2,49 +2,4 @@ _ = require 'underscore'
Category = require './category'
Attributes = require '../attributes'
###
Public: The Folder model represents a Nylas Folder object. For more
information about Folder on the Nylas Platform, read the [Folder API
Documentation](https://nylas.com/docs/api#folders)
NOTE: This is different from a `Label`. A `Folder` is used for generic
IMAP and Exchange, while `Label`s are used for Gmail. The `Account` has
the filed `organizationUnit` which specifies if the current account uses
either "folder" or "label".
While the two appear fairly similar, they have different behavioral
semantics and are treated separately.
Nylas also exposes a set of standard types or categories of folders/
labels: an extended version of [rfc-6154]
(http://tools.ietf.org/html/rfc6154), returned as the name of the folder/
label:
- inbox
- all
- trash
- archive
- drafts
- sent
- spam
- important
NOTE: "starred" and "unread" are no longer folder nor labels. They are now
boolean values on messages and threads.
## Attributes
`name`: {AttributeString} The internal name of the folder. Queryable.
`displayName`: {AttributeString} The display-friendly name of the folder. Queryable.
Section: Models
###
class Folder extends Category
@additionalSQLiteConfig:
setup: ->
['CREATE INDEX IF NOT EXISTS FolderNameIndex ON Folder(account_id,name)',
'CREATE UNIQUE INDEX IF NOT EXISTS FolderClientIndex ON Folder(client_id)']
module.exports = Folder
module.exports = Category

View file

@ -2,48 +2,4 @@ _ = require 'underscore'
Category = require './category'
Attributes = require '../attributes'
###
Public: The Label model represents a Nylas Label object. For more
information about Label on the Nylas Platform, read the [Label API
Documentation](https://nylas.com/docs/api#folders)
NOTE: This is different from a `Folder`. A `Folder` is used for generic
IMAP and Exchange, while `Label`s are used for Gmail. The `Account` has
the field `organizationUnit` which specifies if the current account uses
either "folder" or "label".
While the two appear fairly similar, they have different behavioral
semantics and are treated separately.
Nylas also exposes a set of standard types or categories of folders/
labels: an extended version of [rfc-6154](http://tools.ietf.org/html/rfc6154),
returned as the name of the folder/
label:
- inbox
- all
- trash
- archive
- drafts
- sent
- spam
- important
NOTE: "starred" and "unread" are no longer folders nor labels. They are now
boolean values on messages and threads.
## Attributes
`name`: {AttributeString} The internal name of the label. Queryable.
`displayName`: {AttributeString} The display-friendly name of the label. Queryable.
Section: Models
###
class Label extends Category
@additionalSQLiteConfig:
setup: ->
['CREATE INDEX IF NOT EXISTS LabelNameIndex ON Label(account_id,name)',
'CREATE UNIQUE INDEX IF NOT EXISTS LabelClientIndex ON Label(client_id)']
module.exports = Label
module.exports = Category

View file

@ -3,9 +3,9 @@ moment = require 'moment'
File = require './file'
Utils = require './utils'
Folder = require './folder'
Model = require './model'
Event = require './event'
Category = require './category'
Contact = require './contact'
Attributes = require '../attributes'
AccountStore = require '../stores/account-store'
@ -146,7 +146,7 @@ class Message extends Model
'folder': Attributes.Object
modelKey: 'folder'
itemClass: Folder
itemClass: Category
@naturalSortOrder: ->

View file

@ -156,8 +156,9 @@ class QuerySubscription
console.warn("QuerySubscription: tried to publish a result set missing models.")
return
unless _.uniq(@_set.ids()).length is @_set.count()
throw new Error("")
ids = @_set.ids()
unless _.uniq(ids).length is ids.length
throw new Error("QuerySubscription: result set contains duplicate ids.")
if @_options.asResultSet
@_lastResult = @_set.immutableClone()

View file

@ -45,6 +45,7 @@ class ModelQuery
@_database || = require '../stores/database-store'
@_matchers = []
@_orders = []
@_distinct = false
@_range = QueryRange.infinite()
@_returnOne = false
@_returnIds = false
@ -57,11 +58,16 @@ class ModelQuery
q._orders = [].concat(@_orders)
q._includeJoinedData = [].concat(@_includeJoinedData)
q._range = @_range.clone()
q._distinct = @_distinct
q._returnOne = @_returnOne
q._returnIds = @_returnIds
q._count = @_count
q
distinct: ->
@_distinct = true
@
# Public: Add one or more where clauses to the query
#
# - `matchers` An {Array} of {Matcher} objects that add where clauses to the underlying query.
@ -74,6 +80,8 @@ class ModelQuery
if matchers instanceof Matcher
@_matchers.push(matchers)
else if matchers instanceof Array
for m in matchers
throw new Error("You must provide instances of `Matcher`") unless m instanceof Matcher
@_matchers = @_matchers.concat(matchers)
else if matchers instanceof Object
# Support a shorthand format of {id: '123', accountId: '123'}
@ -250,7 +258,9 @@ class ModelQuery
limit = ""
if @_range.offset?
limit += " OFFSET #{@_range.offset}"
"SELECT #{result} FROM `#{@_klass.name}` #{@_whereClause()} #{order} #{limit}"
distinct = if @_distinct then ' DISTINCT' else ''
"SELECT#{distinct} #{result} FROM `#{@_klass.name}` #{@_whereClause()} #{order} #{limit}"
_whereClause: ->
joins = []

View file

@ -1,7 +1,6 @@
_ = require 'underscore'
Label = require './label'
Folder = require './folder'
Category = require './category'
Model = require './model'
Contact = require './contact'
Actions = require '../actions'
@ -61,15 +60,10 @@ class Thread extends Model
queryable: true
modelKey: 'version'
'folders': Attributes.Collection
'categories': Attributes.Collection
queryable: true
modelKey: 'folders'
itemClass: Folder
'labels': Attributes.Collection
queryable: true
modelKey: 'labels'
itemClass: Label
modelKey: 'categories'
itemClass: Category
'participants': Attributes.Collection
modelKey: 'participants'
@ -83,6 +77,24 @@ class Thread extends Model
modelKey: 'lastMessageReceivedTimestamp'
jsonKey: 'last_message_received_timestamp'
Object.defineProperty @prototype, "labels",
enumerable: false
get: -> @categories
set: (v) -> @categories = v
Object.defineProperty @prototype, "folders",
enumerable: false
get: -> @categories
set: (v) -> @categories = v
Object.defineProperty @attributes, "labels",
enumerable: false
get: -> @categories
Object.defineProperty @attributes, "folders",
enumerable: false
get: -> @categories
@naturalSortOrder: ->
Thread.attributes.lastMessageReceivedTimestamp.descending()
@ -91,8 +103,19 @@ class Thread extends Model
['CREATE INDEX IF NOT EXISTS ThreadListIndex ON Thread(account_id, last_message_received_timestamp DESC, id)']
fromJSON: (json) ->
if json['labels']
@_jsonCategoryType = 'labels'
else
@_jsonCategoryType = 'folders'
json['categories'] = json[@_jsonCategoryType]
super(json)
toJSON: (options) ->
json = super(options)
json[@_jsonCategoryType] = json['categories']
delete json['categories']
json
# Public: Returns true if the thread has a {Category} with the given
# name. Note, only catgories of type `Category.Types.Standard` have valid
# `names`

View file

@ -1,10 +1,14 @@
_ = require 'underscore'
NylasStore = require 'nylas-store'
AccountStore = require './account-store'
Account = require '../models/account'
{StandardCategoryNames} = require '../models/category'
{Categories} = require 'nylas-observables'
Rx = require 'rx-lite'
asAccount = (a) -> if a instanceof Account then a else AccountStore.accountForId(a)
asAccountId = (a) -> if a instanceof Account then a.id else a
class CategoryStore extends NylasStore
constructor: ->
@ -12,19 +16,22 @@ class CategoryStore extends NylasStore
@_standardCategories = {}
@_userCategories = {}
@_hiddenCategories = {}
@_registerObservables(AccountStore.accounts())
@listenTo AccountStore, @_onAccountsChanged
byId: (account, categoryId) ->
@categories(account)[categoryId]
@_disposable = Categories
.forAllAccounts()
.sort()
.subscribe(@_onCategoriesChanged)
byId: (accountOrId, categoryId) ->
@categories(accountOrId)[categoryId]
# Public: Returns an array of all categories for an account, both
# standard and user generated. The items returned by this function will be
# either {Folder} or {Label} objects.
#
categories: (account) ->
if account
@_categoryCache[account.id] ? {}
categories: (accountOrId = null) ->
if accountOrId
@_categoryCache[asAccountId(accountOrId)] ? {}
else
all = []
for accountId, categories of @_categoryCache
@ -33,89 +40,76 @@ class CategoryStore extends NylasStore
# Public: Returns all of the standard categories for the current account.
#
standardCategories: (account) ->
return [] unless account
_.compact(
StandardCategoryNames.map (name) => @_standardCategories[account.id][name]
)
standardCategories: (accountOrId) ->
return [] unless accountOrId
@_standardCategories[asAccountId(accountOrId)]
hiddenCategories: (account) ->
return [] unless account
@_hiddenCategories[account.id]
hiddenCategories: (accountOrId) ->
return [] unless accountOrId
@_hiddenCategories[asAccountId(accountOrId)]
# Public: Returns all of the categories that are not part of the standard
# category set.
#
userCategories: (account) ->
return [] unless account
@_userCategories[account.id]
userCategories: (accountOrId) ->
return [] unless accountOrId
@_userCategories[asAccountId(accountOrId)]
# Public: Returns the Folder or Label object for a standard category name and
# for a given account.
# ('inbox', 'drafts', etc.) It's possible for this to return `null`.
# For example, Gmail likely doesn't have an `archive` label.
#
getStandardCategory: (account, name) ->
return null unless account?
if not name in StandardCategoryNames
getStandardCategory: (accountOrId, name) ->
return null unless accountOrId
unless name in StandardCategoryNames
throw new Error("'#{name}' is not a standard category")
return _.findWhere(@categories(account), {name})
return _.findWhere(@_standardCategories[asAccountId(accountOrId)], {name})
# Public: Returns the Folder or Label object that should be used for "Archive"
# actions. On Gmail, this is the "all" label. On providers using folders, it
# returns any available "Archive" folder, or null if no such folder exists.
#
getArchiveCategory: (account) ->
return null unless account
getArchiveCategory: (accountOrId) ->
return null unless accountOrId
account = asAccount(accountOrId)
if account.usesFolders()
return @getStandardCategory(account, "archive")
return @getStandardCategory(account.id, "archive")
else
return @getStandardCategory(account, "all")
return @getStandardCategory(account.id, "all")
# Public: Returns the Folder or Label object taht should be used for
# "Move to Trash", or null if no trash folder exists.
#
getTrashCategory: (account) ->
@getStandardCategory(account, "trash")
getTrashCategory: (accountOrId) ->
@getStandardCategory(accountOrId, "trash")
_onAccountsChanged: ->
accounts = AccountStore.accounts()
@_removeStaleCategories(accounts)
@_registerObservables(accounts)
_onCategoriesChanged: (categories) =>
@_categoryCache = {}
for cat in categories
@_categoryCache[cat.accountId] ?= {}
@_categoryCache[cat.accountId][cat.id] = cat
_onCategoriesChanged: (accountId, categories) =>
return unless categories
@_categoryCache[accountId] = {}
@_standardCategories[accountId] = {}
@_userCategories[accountId] = []
@_hiddenCategories[accountId] = []
filteredByAccount = (fn) ->
result = {}
for cat in categories
continue unless fn(cat)
result[cat.accountId] ?= []
result[cat.accountId].push(cat)
result
@_standardCategories = filteredByAccount (cat) -> cat.isStandardCategory()
@_userCategories = filteredByAccount (cat) -> cat.isUserCategory()
@_hiddenCategories = filteredByAccount (cat) -> cat.isHiddenCategory()
# Ensure standard categories are always sorted in the correct order
for accountId, items of @_standardCategories
@_standardCategories[accountId].sort (a, b) ->
StandardCategoryNames.indexOf(a.name) - StandardCategoryNames.indexOf(b.name)
for category in categories
@_categoryCache[accountId][category.id] = category
if category.isStandardCategory()
@_standardCategories[accountId][category.name] = category
if category.isUserCategory()
@_userCategories[accountId].push(category)
if category.isHiddenCategory()
@_hiddenCategories[accountId].push(category)
@trigger()
# Remove any category sets for removed accounts
# Will prevent memory leaks
_removeStaleCategories: (accounts) ->
accountIds = accounts.map (acc) -> acc.id
removedAccountIds = _.difference(_.keys(@_categoryCache), accountIds)
for accountId in removedAccountIds
delete @_categoryCache[accountId]
delete @_standardCategories[accountId]
delete @_userCategories[accountId]
delete @_hiddenCategories[accountId]
_registerObservables: (accounts) =>
@_disposables ?= []
@_disposables.forEach (disp) -> disp.dispose()
@_disposables = accounts.map (account) =>
Categories.forAccount(account).sort()
.subscribe(@_onCategoriesChanged.bind(@, account.id))
module.exports = new CategoryStore()

View file

@ -15,7 +15,7 @@ DatabaseTransaction = require './database-transaction'
{ipcRenderer} = require 'electron'
DatabaseVersion = 16
DatabaseVersion = 17
DatabasePhase =
Setup: 'setup'
Ready: 'ready'

View file

@ -32,8 +32,10 @@ class FocusedPerspectiveStore extends NylasStore
@_setPerspective(@_defaultPerspective())
else
account = @_current.account
catId = @_current.categoryId()
if catId and not CategoryStore.byId(account, catId)
cats = @_current.categories()
catExists = (cat) -> CategoryStore.byId(cat.accountId, cat.id)
if cats and not _.every(cats, catExists)
@_setPerspective(@_defaultPerspective(account))
_onFocusPerspective: (perspective) =>
@ -47,14 +49,13 @@ class FocusedPerspectiveStore extends NylasStore
return unless account
category = CategoryStore.getStandardCategory(account, "inbox")
return unless category
@_setPerspective(MailboxPerspective.forCategory(account, category))
@_setPerspective(MailboxPerspective.forCategory(category))
# TODO Update unified MailboxPerspective
_defaultPerspective: (account = AccountStore.accounts()[0])->
_defaultPerspective: (account = AccountStore.accounts()[0]) ->
return MailboxPerspective.forNothing() unless account
category = CategoryStore.getStandardCategory(account, "inbox")
return null unless category
return MailboxPerspective.forCategory(account, category)
# MailboxPerspective.unified()
return MailboxPerspective.forNothing() unless category
return MailboxPerspective.forCategory(category)
_setPerspective: (perspective) ->
return if perspective?.isEqual(@_current)

View file

@ -7,8 +7,7 @@ AccountStore = require './account-store'
DatabaseStore = require './database-store'
Actions = require '../actions'
Thread = require '../models/thread'
Folder = require '../models/folder'
Label = require '../models/label'
Category = require '../models/category'
WindowBridge = require '../../window-bridge'
JSONBlobKey = 'UnreadCounts-V2'
@ -19,15 +18,12 @@ class CategoryDatabaseMutationObserver
beforeDatabaseChange: (query, {type, objects, objectIds, objectClass}) =>
if objectClass is Thread.name
idString = "'" + objectIds.join("','") + "'"
Promise.props
labelData: query("SELECT `Thread`.id as id, `Thread-Label`.`value` as catId FROM `Thread` INNER JOIN `Thread-Label` ON `Thread`.`id` = `Thread-Label`.`id` WHERE `Thread`.id IN (#{idString}) AND `Thread`.unread = 1", [])
folderData: query("SELECT `Thread`.id as id, `Thread-Folder`.`value` as catId FROM `Thread` INNER JOIN `Thread-Folder` ON `Thread`.`id` = `Thread-Folder`.`id` WHERE `Thread`.id IN (#{idString}) AND `Thread`.unread = 1", [])
.then ({labelData, folderData}) =>
query("SELECT `Thread`.id as id, `Thread-Category`.`value` as catId FROM `Thread` INNER JOIN `Thread-Category` ON `Thread`.`id` = `Thread-Category`.`id` WHERE `Thread`.id IN (#{idString}) AND `Thread`.unread = 1", [])
.then (categoryData) =>
categories = {}
for collection in [labelData, folderData]
for {id, catId} in collection
categories[catId] ?= 0
categories[catId] -= 1
for {id, catId} in categoryData
categories[catId] ?= 0
categories[catId] -= 1
Promise.resolve({categories})
else
Promise.resolve()
@ -68,11 +64,8 @@ class ThreadCountsStore extends NylasStore
if NylasEnv.isWorkWindow()
DatabaseStore.findJSONBlob(JSONBlobKey).then(@_onCountsBlobRead)
Rx.Observable.combineLatest(
Rx.Observable.fromQuery(DatabaseStore.findAll(Label)),
Rx.Observable.fromQuery(DatabaseStore.findAll(Folder))
).subscribe ([labels, folders]) =>
@_categories = [].concat(labels, folders)
Rx.Observable.fromQuery(DatabaseStore.findAll(Category)).subscribe (categories) =>
@_categories = [].concat(categories)
@_fetchCountsMissing()
else
@ -137,17 +130,10 @@ class ThreadCountsStore extends NylasStore
@trigger()
_fetchCountForCategory: (cat) =>
if cat instanceof Label
categoryAttribute = Thread.attributes.labels
else if cat instanceof Folder
categoryAttribute = Thread.attributes.folders
else
throw new Error("Unexpected category class")
DatabaseStore.count(Thread, [
Thread.attributes.categories.contains(cat.id),
Thread.attributes.accountId.equal(cat.accountId),
Thread.attributes.unread.equal(true),
categoryAttribute.contains(cat.id)
])
module.exports = new ThreadCountsStore

View file

@ -66,7 +66,7 @@ class WorkspaceStore extends NylasStore
account = FocusedPerspectiveStore.current()?.account
category = CategoryStore.getStandardCategory(account, categoryName)
return unless category
view = MailboxPerspective.forCategory(account, category)
view = MailboxPerspective.forCategory(category)
return unless view
Actions.focusMailboxPerspective(view)
@ -74,15 +74,15 @@ class WorkspaceStore extends NylasStore
Actions.selectRootSheet(@Sheet.Drafts)
_selectAllView: ->
account = FocusedPerspectiveStore.current()?.account
category = CategoryStore.getArchiveCategory(account)
return unless category
view = MailboxPerspective.forCategory(account, category)
return unless view
accountIds = FocusedPerspectiveStore.current().accountIds
categories = accountIds.map (aid) -> CategoryStore.getArchiveCategory(aid)
view = MailboxPerspective.forCategories(categories)
Actions.focusMailboxPerspective(view)
_selectStarredView: ->
Actions.focusMailboxPerspective MailboxPerspective.forStarred()
accountIds = FocusedPerspectiveStore.current().accountIds
Actions.focusMailboxPerspective MailboxPerspective.forStarred(accountIds)
_resetInstanceVars: =>
@Location = Location = {}

View file

@ -1,6 +1,5 @@
_ = require 'underscore'
Task = require './task'
Folder = require '../models/folder'
Thread = require '../models/thread'
Message = require '../models/message'
DatabaseStore = require '../stores/database-store'
@ -30,9 +29,7 @@ class ChangeFolderTask extends ChangeMailTask
"Moving to folder…"
description: ->
folderText = ""
if @folder instanceof Folder
folderText = " to #{@folder.displayName}"
folderText = " to #{@folder.displayName}"
if @threads.length > 0
if @threads.length > 1
@ -58,7 +55,7 @@ class ChangeFolderTask extends ChangeMailTask
# Convert arrays of IDs or models to models.
# modelify returns immediately if no work is required
Promise.props(
folder: DatabaseStore.modelify(Folder, [@folder])
folder: DatabaseStore.modelify(Category, [@folder])
threads: DatabaseStore.modelify(Thread, @threads)
messages: DatabaseStore.modelify(Message, @messages)

View file

@ -1,6 +1,5 @@
_ = require 'underscore'
Task = require './task'
Label = require '../models/label'
Thread = require '../models/thread'
Message = require '../models/message'
DatabaseStore = require '../stores/database-store'
@ -10,8 +9,8 @@ SyncbackCategoryTask = require './syncback-category-task'
# Public: Create a new task to apply labels to a message or thread.
#
# Takes an options object of the form:
# - labelsToAdd: An {Array} of {Label}s or {Label} ids to add
# - labelsToRemove: An {Array} of {Label}s or {Label} ids to remove
# - labelsToAdd: An {Array} of {Category}s or {Category} ids to add
# - labelsToRemove: An {Array} of {Category}s or {Category} ids to remove
# - threads: An {Array} of {Thread}s or {Thread} ids
# - messages: An {Array} of {Message}s or {Message} ids
class ChangeLabelsTask extends ChangeMailTask
@ -28,9 +27,9 @@ class ChangeLabelsTask extends ChangeMailTask
type = "thread"
if @threads.length > 1
type = "threads"
if @labelsToAdd.length is 1 and @labelsToRemove.length is 0 and @labelsToAdd[0] instanceof Label
if @labelsToAdd.length is 1 and @labelsToRemove.length is 0 and @labelsToAdd[0] instanceof Category
return "Added #{@labelsToAdd[0].displayName} to #{@threads.length} #{type}"
if @labelsToAdd.length is 0 and @labelsToRemove.length is 1 and @labelsToRemove[0] instanceof Label
if @labelsToAdd.length is 0 and @labelsToRemove.length is 1 and @labelsToRemove[0] instanceof Category
return "Removed #{@labelsToRemove[0].displayName} from #{@threads.length} #{type}"
return "Changed labels on #{@threads.length} #{type}"
@ -50,8 +49,8 @@ class ChangeLabelsTask extends ChangeMailTask
# Convert arrays of IDs or models to models.
# modelify returns immediately if no work is required
Promise.props(
labelsToAdd: DatabaseStore.modelify(Label, @labelsToAdd)
labelsToRemove: DatabaseStore.modelify(Label, @labelsToRemove)
labelsToAdd: DatabaseStore.modelify(Category, @labelsToAdd)
labelsToRemove: DatabaseStore.modelify(Category, @labelsToRemove)
threads: DatabaseStore.modelify(Thread, @threads)
messages: DatabaseStore.modelify(Message, @messages)

View file

@ -1,6 +1,6 @@
DatabaseStore = require '../stores/database-store'
Label = require '../models/label'
Folder = require '../models/folder'
AccountStore = require '../stores/account-store'
Category = require '../models/category'
Task = require './task'
ChangeFolderTask = require './change-folder-task'
ChangeLabelTask = require './change-labels-task'
@ -14,11 +14,7 @@ class DestroyCategoryTask extends Task
super
label: ->
name = @category.displayName
if @category instanceof Label
"Deleting label #{name}..."
else
"Deleting folder #{name}..."
"Deleting #{@category.displayType()} #{@category.displayName}..."
isDependentTask: (other) ->
(other instanceof ChangeFolderTask) or
@ -39,7 +35,7 @@ class DestroyCategoryTask extends Task
if not @category.serverId
return Promise.reject(new Error("Attempt to call DestroyCategoryTask.performRemote without @category.serverId."))
if @category instanceof Label
if AccountStore.accountForId(@category.accountId).usesLabels()
path = "/labels/#{@category.serverId}"
else
path = "/folders/#{@category.serverId}"
@ -66,14 +62,12 @@ class DestroyCategoryTask extends Task
else
return Promise.resolve(Task.Status.Retry)
_displayType: ->
_notifyUserOfError: (category = @category) ->
displayName = category.displayName
displayType = if category instanceof Label
'label'
else
'folder'
msg = "The #{displayType} #{displayName} could not be deleted."
msg = "The #{category.displayType()} #{displayName} could not be deleted."
if displayType is 'folder'
msg += " Make sure the folder you want to delete is empty before deleting it."

View file

@ -1,7 +1,5 @@
CategoryStore = require '../stores/category-store'
DatabaseStore = require '../stores/database-store'
Label = require '../models/label'
Folder = require '../models/folder'
{generateTempId} = require '../models/utils'
Task = require './task'
NylasAPI = require '../nylas-api'
@ -13,10 +11,7 @@ module.exports = class SyncbackCategoryTask extends Task
super
label: ->
if @category instanceof Label
"Creating new label..."
else
"Creating new folder..."
"Creating new #{@category.displayType()}..."
performLocal: ->
# When we send drafts, we don't update anything in the app until
@ -32,7 +27,7 @@ module.exports = class SyncbackCategoryTask extends Task
t.persistModel @category
performRemote: ->
if @category instanceof Label
if AccountStore.accountForId(@category.accountId).usesLabels()
path = "/labels"
else
path = "/folders"
@ -51,7 +46,7 @@ module.exports = class SyncbackCategoryTask extends Task
# created serverId.
@category.serverId = json.id
DatabaseStore.inTransaction (t) =>
t.persistModel @category
t.persistModel(@category)
.then ->
return Promise.resolve(Task.Status.Success)
.catch APIError, (err) =>

View file

@ -1,5 +1,6 @@
Rx = require 'rx-lite'
_ = require 'underscore'
Category = require '../flux/models/category'
QuerySubscriptionPool = require '../flux/models/query-subscription-pool'
AccountStore = require '../flux/stores/account-store'
DatabaseStore = require '../flux/stores/database-store'
@ -31,21 +32,15 @@ CategoryOperators =
CategoryObservables =
forAllAccounts: =>
observable = Rx.Observable.fromStore(AccountStore).flatMapLatest ->
observables = AccountStore.accounts().map (account) ->
categoryClass = account.categoryClass()
Rx.Observable.fromQuery(DatabaseStore.findAll(categoryClass))
Rx.Observable.concat(observables)
observable = Rx.Observable.fromQuery(DatabaseStore.findAll(Category))
_.extend(observable, CategoryOperators)
observable
forAccount: (account) =>
if account
categoryClass = account.categoryClass()
observable = Rx.Observable.fromQuery(DatabaseStore.findAll(categoryClass)
.where(categoryClass.attributes.accountId.equal(account.id)))
observable = Rx.Observable.fromQuery(DatabaseStore.findAll(Category).where(accountId: account.id))
else
observable = CategoryObservables.forAllAccounts()
observable = Rx.Observable.fromQuery(DatabaseStore.findAll(Category))
_.extend(observable, CategoryOperators)
observable

View file

@ -2,8 +2,7 @@ _ = require 'underscore'
Task = require './flux/tasks/task'
Actions = require './flux/actions'
Label = require './flux/models/label'
Folder = require './flux/models/folder'
Category = require './flux/models/category'
Thread = require './flux/models/thread'
Message = require './flux/models/message'
AccountStore = require './flux/stores/account-store'
@ -25,7 +24,7 @@ information about the current view. Maybe after the unified inbox refactor...
###
MailRulesActions =
markAsImportant: (message, thread) ->
DatabaseStore.findBy(Label, {
DatabaseStore.findBy(Category, {
name: 'important',
accountId: thread.accountId
}).then (important) ->
@ -33,10 +32,10 @@ MailRulesActions =
return new ChangeLabelsTask(labelsToAdd: [important], threads: [thread])
moveToTrash: (message, thread) ->
if AccountStore.accountForId(thread.accountId).categoryClass() is Label
if AccountStore.accountForId(thread.accountId).usesLabels()
return MailRulesActions._applyStandardLabelRemovingInbox(message, thread, 'trash')
else
DatabaseStore.findBy(Folder, { name: 'trash', accountId: thread.accountId }).then (folder) ->
DatabaseStore.findBy(Category, { name: 'trash', accountId: thread.accountId }).then (folder) ->
return Promise.reject(new Error("The folder could not be found.")) unless folder
return new ChangeFolderTask(folder: folder, threads: [thread])
@ -48,13 +47,13 @@ MailRulesActions =
changeFolder: (message, thread, value) ->
return Promise.reject(new Error("A folder is required.")) unless value
DatabaseStore.findBy(Folder, { id: value, accountId: thread.accountId }).then (folder) ->
DatabaseStore.findBy(Category, { id: value, accountId: thread.accountId }).then (folder) ->
return Promise.reject(new Error("The folder could not be found.")) unless folder
return new ChangeFolderTask(folder: folder, threads: [thread])
applyLabel: (message, thread, value) ->
return Promise.reject(new Error("A label is required.")) unless value
DatabaseStore.findBy(Label, { id: value, accountId: thread.accountId }).then (label) ->
DatabaseStore.findBy(Category, { id: value, accountId: thread.accountId }).then (label) ->
return Promise.reject(new Error("The label could not be found.")) unless label
return new ChangeLabelsTask(labelsToAdd: [label], threads: [thread])
@ -65,8 +64,8 @@ MailRulesActions =
_applyStandardLabelRemovingInbox: (message, thread, value) ->
Promise.props(
inbox: DatabaseStore.findBy(Label, { name: 'inbox', accountId: thread.accountId })
newLabel: DatabaseStore.findBy(Label, { name: value, accountId: thread.accountId })
inbox: DatabaseStore.findBy(Category, { name: 'inbox', accountId: thread.accountId })
newLabel: DatabaseStore.findBy(Category, { name: value, accountId: thread.accountId })
).then ({inbox, newLabel}) ->
return Promise.reject(new Error("Could not find `inbox` or `#{value}` label")) unless inbox and newLabel
return new ChangeLabelsTask

View file

@ -15,11 +15,13 @@ Actions = require './flux/actions'
class MailboxPerspective
# Factory Methods
@forNothing: ->
new EmptyMailboxPerspective()
@forCategory: (accountIds, category) ->
@forCategory: (category) ->
new CategoryMailboxPerspective([category])
@forCategories: (accountIds, categories) ->
@forCategories: (categories) ->
new CategoryMailboxPerspective(categories)
@forStarred: (accountIds) ->
@ -29,171 +31,168 @@ class MailboxPerspective
new SearchMailboxPerspective(accountIds, query)
@forAll: (accountIds) ->
new AllMailboxPerspective(accountIds)
threads: ->
matchers = [@matchers()]
matchers.push Thread.attributes.accountId.in(@accountIds) if @accountIds
query = DatabaseStore.findAll(Thread).where(matchers).limit(0)
return new MutableQuerySubscription(query, {asResultSet: true})
categories = accountIds.map (aid) ->
CategoryStore.getStandardCategory(aid, "all")
new CategoryMailboxPerspective(_.compact(categories))
# 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`")
@
isEqual: (other) ->
return false unless other and @constructor.name is other.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)
matchers = @matchers() ? []
otherMatchers = other.matchers() ? []
return false if otherMatchers.length isnt matchers.length
for idx in [0...matchers.length]
if matchers[idx].value() isnt otherMatchers[idx].value()
return false
true
categoryId: ->
throw new Error("categoryId: Not implemented in base class.")
categories: =>
[]
matchers: ->
throw new Error("matchers: Not implemented in base class.")
threads: =>
throw new Error("threads: Not implemented in base class.")
canApplyToThreads: ->
canApplyToThreads: =>
throw new Error("canApplyToThreads: Not implemented in base class.")
applyToThreads: (threadsOrIds) ->
applyToThreads: (threadsOrIds) =>
throw new Error("applyToThreads: Not implemented in base class.")
# Whether or not the current MailboxPerspective can "archive" or "trash"
# Subclasses should call `super` if they override these methods
canArchiveThreads: ->
canArchiveThreads: =>
for aid in @accountIds
return false unless CategoryStore.getArchiveCategory(AccountStore.accountForId(aid))
return true
canTrashThreads: ->
canTrashThreads: =>
for aid in @accountIds
return false unless CategoryStore.getTrashCategory(AccountStore.accountForId(aid))
return true
class SearchMailboxPerspective extends MailboxPerspective
constructor: (@accountIds, @searchQuery) ->
super(@accountIds)
unless _.isString(@searchQuery)
throw new Error("SearchMailboxPerspective: Expected a `string` search query")
@
isEqual: (other) ->
isEqual: (other) =>
super(other) and other.searchQuery is @searchQuery
matchers: ->
null
canApplyToThreads: ->
false
canArchiveThreads: ->
false
canTrashThreads: ->
false
categoryIds: ->
null
threads: ->
threads: =>
new SearchSubscription(@searchQuery, @accountIds)
class AllMailboxPerspective extends MailboxPerspective
constructor: (@accountIds) ->
@name = "All"
@iconName = "all-mail.png"
@
matchers: ->
[Thread.attributes.accountId.in(@accountIds)]
canApplyToThreads: ->
true
canArchiveThreads: ->
canApplyToThreads: =>
false
canTrashThreads: ->
canArchiveThreads: =>
false
categoryIds: ->
@accountIds.map (aid) ->
CategoryStore.getStandardCategory(AccountStore.accountForId(aid), "all")?.id
canTrashThreads: =>
false
class StarredMailboxPerspective extends MailboxPerspective
constructor: (@accountIds) ->
super(@accountIds)
@name = "Starred"
@iconName = "starred.png"
@
matchers: ->
[Thread.attributes.starred.equal(true)]
threads: =>
query = DatabaseStore.findAll(Thread).where([
Thread.attributes.accountId.in(@accountIds),
Thread.attributes.starred.equal(true)
]).limit(0)
categoryIds: ->
null
return new MutableQuerySubscription(query, {asResultSet: true})
canApplyToThreads: ->
canApplyToThreads: =>
true
applyToThreads: (threadsOrIds) ->
applyToThreads: (threadsOrIds) =>
ChangeStarredTask = require './flux/tasks/change-starred-task'
task = new ChangeStarredTask({threads:threadsOrIds, starred: true})
Actions.queueTask(task)
class CategoryMailboxPerspective extends MailboxPerspective
constructor: (@categories) ->
unless @categories instanceof Array
throw new Error("CategoryMailboxPerspective: You must provide a `categories` array")
class EmptyMailboxPerspective extends MailboxPerspective
constructor: ->
@accountIds = _.uniq(_.pluck(@categories, 'accountId'))
threads: =>
query = DatabaseStore.findAll(Thread).where(accountId: -1).limit(0)
return new MutableQuerySubscription(query, {asResultSet: true})
canApplyToThreads: =>
false
canArchiveThreads: =>
false
canTrashThreads: =>
false
applyToThreads: (threadsOrIds) =>
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 @category[0].name
@iconName = "#{@category[0].name}.png"
@name = @_categories[0].displayName
if @_categories[0].name
@iconName = "#{@_categories[0].name}.png"
else
@iconName = CategoryHelpers.categoryIconName(@accountIds[0])
@
matchers: =>
matchers.push Thread.attributes.labels.containsAny(@categoryIds())
matchers
isEqual: (other) =>
super(other) and _.isEqual(@categories(), other.categories())
categoryIds: ->
_.pluck(@categories, 'id')
threads: =>
query = DatabaseStore
.findAll(Thread)
.where([Thread.attributes.categories.containsAny(_.pluck(@categories(), 'id'))])
.limit(0)
canApplyToThreads: ->
not _.any @categories, (c) -> c.isLockedCategory()
query.distinct() if @categories().length > 1
canArchiveThreads: ->
for cat in @categories
return new MutableQuerySubscription(query, {asResultSet: true})
categories: =>
@_categories
canApplyToThreads: =>
not _.any @_categories, (c) -> c.isLockedCategory()
canArchiveThreads: =>
for cat in @_categories
return false if cat.name in ["archive", "all", "sent"]
super
canTrashThreads: ->
for cat in @categories
canTrashThreads: =>
for cat in @_categories
return false if cat.name in ["trash"]
super
applyToThreads: (threadsOrIds) ->
applyToThreads: (threadsOrIds) =>
# TODO:
# categoryToApplyForAccount = {}
# for cat in @categories
# for cat in @_categories
# categoryToApplyForAccount[cat.accountId] = cat
#
# @categories.forEach (cat) ->
# @_categories.forEach (cat) ->
#
# if @account.usesLabels()
# FocusedPerspectiveStore = require './flux/stores/focused-perspective-store'