fix(snooze): Correctly query and create snooze categories per account

Summary:
- Was not properly updating the references to snoozed categories when
  accounts were added or removed
- Update whenCategoriesReady to make sure we listen until category syncing has concluded (Move inside CategoryStore)
- #1676, #1658

Test Plan: - TODO

Reviewers: evan, drew, bengotow

Reviewed By: bengotow

Differential Revision: https://phab.nylas.com/D2723
This commit is contained in:
Juan Tejada 2016-03-14 15:34:16 -07:00
parent 734a52aa6a
commit cfbbcd23d3
13 changed files with 215 additions and 28 deletions

View file

@ -3,7 +3,8 @@
"globals": {
"NylasEnv": false,
"$n": false,
"waitsForPromise": false
"waitsForPromise": false,
"advanceClock": false
},
"env": {
"browser": true,

View file

@ -108,7 +108,7 @@ class ActivitySidebar extends React.Component
_getStateFromStores: =>
notifications: NotificationStore.notifications()
tasks: TaskQueueStatusStore.queue()
isInitialSyncComplete: NylasSyncStatusStore.isComplete()
isInitialSyncComplete: NylasSyncStatusStore.isSyncComplete()
_onDeltaReceived: (countDeltas) =>
tooSmallForNotification = countDeltas <= 10

View file

@ -1,5 +1,6 @@
import _ from 'underscore';
import React, {Component, PropTypes} from 'react';
import {FocusedPerspectiveStore} from 'nylas-exports';
import {RetinaImg, MailLabel} from 'nylas-component-kit';
import {SNOOZE_CATEGORY_NAME, PLUGIN_ID} from './snooze-constants';
import {snoozedUntilMessage} from './snooze-utils';
@ -15,6 +16,17 @@ class SnoozeMailLabel extends Component {
static containerRequired = false;
render() {
const current = FocusedPerspectiveStore.current()
const isSnoozedPerspective = (
current.categories &&
current.categories.length > 0 &&
current.categories[0].displayName === SNOOZE_CATEGORY_NAME
)
if (!isSnoozedPerspective) {
return <span />
}
const {thread} = this.props;
if (_.findWhere(thread.categories, {displayName: SNOOZE_CATEGORY_NAME})) {
const metadata = thread.metadataForPluginId(PLUGIN_ID);

View file

@ -15,11 +15,14 @@ class SnoozeStore {
constructor(pluginId = PLUGIN_ID, pluginName = PLUGIN_NAME) {
this.pluginId = pluginId
this.pluginName = pluginName
this.snoozeCategoriesPromise = getSnoozeCategoriesByAccount()
this.snoozeCategoriesPromise = getSnoozeCategoriesByAccount(AccountStore.accounts())
}
activate() {
this.unsubscribe = SnoozeActions.snoozeThreads.listen(this.onSnoozeThreads)
this.unsubscribers = [
AccountStore.listen(this.onAccountsChanged),
SnoozeActions.snoozeThreads.listen(this.onSnoozeThreads),
]
}
recordSnoozeEvent(threads, snoozeDate, label) {
@ -55,6 +58,10 @@ class SnoozeStore {
return Promise.resolve(threadsByAccountId);
};
onAccountsChanged = ()=> {
this.snoozeCategoriesPromise = getSnoozeCategoriesByAccount(AccountStore.accounts())
};
onSnoozeThreads = (threads, snoozeDate, label) => {
this.recordSnoozeEvent(threads, label)
@ -85,7 +92,7 @@ class SnoozeStore {
};
deactivate() {
this.unsubscribe()
this.unsubscribers.forEach(unsub => unsub())
}
}

View file

@ -58,23 +58,8 @@ const SnoozeUtils = {
})
},
whenCategoriesReady(accountId) {
const categoriesReady = ()=> CategoryStore.categories(accountId).length > 0;
if (!categoriesReady()) {
return new Promise((resolve)=> {
const unsubscribe = CategoryStore.listen(()=> {
if (categoriesReady()) {
unsubscribe()
resolve()
}
})
})
}
return Promise.resolve()
},
getSnoozeCategory(accountId, categoryName = SNOOZE_CATEGORY_NAME) {
return SnoozeUtils.whenCategoriesReady(accountId)
return CategoryStore.whenCategoriesReady(accountId)
.then(()=> {
const allCategories = CategoryStore.categories(accountId)
const category = _.findWhere(allCategories, {displayName: categoryName})

View file

@ -22,7 +22,7 @@ describe('Snooze Utils', ()=> {
beforeEach(()=> {
this.name = 'Snoozed Folder'
this.accId = 123
spyOn(SnoozeUtils, 'whenCategoriesReady').andReturn(Promise.resolve())
spyOn(CategoryStore, 'whenCategoriesReady').andReturn(Promise.resolve())
})
describe('snoozedUntilMessage', ()=> {

View file

@ -108,7 +108,7 @@ class NylasSyncWorker
needed = [
{model: 'threads'},
{model: "#{@_account.organizationUnit}s", initialPageSize: 1000}
{model: @_account.categoryCollection(), initialPageSize: 1000}
{model: 'drafts'},
{model: 'contacts'},
{model: 'calendars'},

View file

@ -0,0 +1,61 @@
import {
Rx,
AccountStore,
CategoryStore,
NylasSyncStatusStore,
} from 'nylas-exports';
describe('CategoryStore', ()=> {
beforeEach(()=> {
spyOn(AccountStore, 'accountForId').andReturn({categoryCollection: ()=> 'labels'})
});
describe('whenCategoriesReady', ()=> {
it('resolves immediately if sync is done and cache is populated', ()=> {
spyOn(NylasSyncStatusStore, 'isSyncCompleteForAccount').andReturn(true)
spyOn(CategoryStore, 'categories').andReturn([{name: 'inbox'}])
spyOn(Rx.Observable, 'fromStore')
waitsForPromise(()=> {
const promise = CategoryStore.whenCategoriesReady('a1')
expect(promise.isResolved()).toBe(true)
return promise.then(()=> {
expect(Rx.Observable.fromStore).not.toHaveBeenCalled()
})
})
});
it('resolves only when sync is done even if cache is already populated', ()=> {
spyOn(NylasSyncStatusStore, 'isSyncCompleteForAccount').andReturn(false)
spyOn(CategoryStore, 'categories').andReturn([{name: 'inbox'}])
waitsForPromise(()=> {
const promise = CategoryStore.whenCategoriesReady('a1')
expect(promise.isResolved()).toBe(false)
jasmine.unspy(NylasSyncStatusStore, 'isSyncCompleteForAccount')
spyOn(NylasSyncStatusStore, 'isSyncCompleteForAccount').andReturn(true)
NylasSyncStatusStore.trigger()
return promise.then(()=> {
expect(promise.isResolved()).toBe(true)
})
})
});
it('resolves only when cache is populated even if sync is done', ()=> {
spyOn(NylasSyncStatusStore, 'isSyncCompleteForAccount').andReturn(true)
spyOn(CategoryStore, 'categories').andReturn([])
waitsForPromise(()=> {
const promise = CategoryStore.whenCategoriesReady('a1')
expect(promise.isResolved()).toBe(false)
jasmine.unspy(CategoryStore, 'categories')
spyOn(CategoryStore, 'categories').andReturn([{name: 'inbox'}])
CategoryStore.trigger()
return promise.then(()=> {
expect(promise.isResolved()).toBe(true)
})
})
});
});
});

View file

@ -0,0 +1,76 @@
import {NylasSyncStatusStore} from 'nylas-exports'
const store = NylasSyncStatusStore
fdescribe('NylasSyncStatusStore', ()=> {
beforeEach(()=> {
store._statesByAccount = {}
});
describe('isSyncCompleteForAccount', ()=> {
describe('when model (collection) provided', ()=> {
it('returns true if syncing for the given model and account is complete', ()=> {
store._statesByAccount = {
a1: {
labels: {complete: true},
},
}
expect(store.isSyncCompleteForAccount('a1', 'labels')).toBe(true)
});
it('returns false otherwise', ()=> {
const states = [
{ a1: { labels: {complete: false} } },
{ a1: {} },
{},
]
states.forEach((state)=> {
store._statesByAccount = state
expect(store.isSyncCompleteForAccount('a1', 'labels')).toBe(false)
})
});
});
describe('when model not provided', ()=> {
it('returns true if sync is complete for all models for the given account', ()=> {
store._statesByAccount = {
a1: {
labels: {complete: true},
threads: {complete: true},
},
}
expect(store.isSyncCompleteForAccount('a1')).toBe(true)
});
it('returns false otherwise', ()=> {
store._statesByAccount = {
a1: {
labels: {complete: true},
threads: {complete: false},
},
}
expect(store.isSyncCompleteForAccount('a1')).toBe(false)
});
});
});
describe('isSyncComplete', ()=> {
it('returns true if sync is complete for all accounts', ()=> {
spyOn(store, 'isSyncCompleteForAccount').andReturn(true)
store._statesByAccount = {
a1: {},
a2: {},
}
expect(store.isSyncComplete('a1')).toBe(true)
});
it('returns false otherwise', ()=> {
spyOn(store, 'isSyncCompleteForAccount').andCallFake((acctId) => acctId === 'a1' ? true : false)
store._statesByAccount = {
a1: {},
a2: {},
}
expect(store.isSyncComplete('a1')).toBe(false)
});
});
});

View file

@ -111,6 +111,9 @@ class Account extends ModelWithMetadata
else
'Unknown'
categoryCollection: ->
"#{@organizationUnit}s"
categoryIcon: ->
if @usesFolders()
'folder.png'

View file

@ -1,10 +1,11 @@
_ = require 'underscore'
Rx = require 'rx-lite'
NylasStore = require 'nylas-store'
AccountStore = require './account-store'
NylasSyncStatusStore = require './nylas-sync-status-store'
Account = require '../models/account'
{StandardCategoryNames} = require '../models/category'
{Categories} = require 'nylas-observables'
Rx = require 'rx-lite'
asAccount = (a) ->
throw new Error("You must pass an Account or Account Id") unless a
@ -129,6 +130,36 @@ class CategoryStore extends NylasStore
getSpamCategory: (accountOrId) =>
@getStandardCategory(accountOrId, "spam")
# Public: Returns a promise that resolves when the categories for a given
# account have been loaded into the local cache
#
whenCategoriesReady: (accountOrId) =>
if not accountOrId
Promise.reject('whenCategoriesReady: must pass an account or accountId')
account = asAccount(accountOrId)
categoryCollection = account.categoryCollection()
categoriesReady = => (
@categories(account).length > 0 and
NylasSyncStatusStore.isSyncCompleteForAccount(account.id, categoryCollection)
)
if not categoriesReady()
return new Promise (resolve) =>
syncStatusObservable = Rx.Observable.fromStore(NylasSyncStatusStore)
categoryObservable = Rx.Observable.fromStore(@)
disposable = Rx.Observable.merge(
syncStatusObservable,
categoryObservable
).subscribe =>
if categoriesReady()
disposable.dispose()
resolve()
return Promise.resolve()
_onCategoriesChanged: (categories) =>
@_categoryResult = categories
@_categoryCache = {}

View file

@ -25,10 +25,17 @@ class NylasSyncStatusStore extends NylasStore
state: =>
@_statesByAccount
isComplete: ->
for acctId, state of @_statesByAccount
for model, modelState of state
return false if not modelState.complete
isSyncCompleteForAccount: (acctId, model) =>
return false unless @_statesByAccount[acctId]
if model
return @_statesByAccount[acctId][model]?.complete ? false
for _model, modelState of @_statesByAccount[acctId]
return false if not modelState.complete
return true
isSyncComplete: =>
for acctId of @_statesByAccount
return false if not @isSyncCompleteForAccount(acctId)
return true
busy: =>

View file

@ -42,6 +42,10 @@ class NylasExports
return exported
enumerable: true
# Make sure our custom observable helpers are defined immediately
# (fromStore, fromQuery, etc...)
require 'nylas-observables'
# Actions
@load "Actions", 'flux/actions'