diff --git a/packages/client-app/internal_packages/contact-rankings/lib/contact-rankings-cache.es6 b/packages/client-app/internal_packages/contact-rankings/lib/contact-rankings-cache.es6 deleted file mode 100644 index bb3c51884..000000000 --- a/packages/client-app/internal_packages/contact-rankings/lib/contact-rankings-cache.es6 +++ /dev/null @@ -1,112 +0,0 @@ -import _ from 'underscore' -import moment from 'moment-timezone' -import { - IdentityStore, - AccountStore, - NylasAPI, - NylasAPIRequest, - FolderSyncProgressStore, -} from 'nylas-exports' -import RefreshingJSONCache from './refreshing-json-cache' - -// Stores contact rankings -class ContactRankingsCache extends RefreshingJSONCache { - constructor(accountId) { - super({ - key: `ContactRankingsFor${accountId}`, - version: 1, - refreshInterval: moment.duration(60, 'seconds').asMilliseconds(), - maxRefreshInterval: moment.duration(24, 'hours').asMilliseconds(), - }) - this._accountId = accountId - } - - _nextRefreshInterval() { - // For the first 15 minutes, refresh roughly once every minute so that the - // experience of composing drafts during initial is less annoying. - const initialLimit = (60 * 1000) + 15; - if (this.refreshInterval < initialLimit) { - return this.refreshInterval + 1; - } - // After the first 15 minutes, refresh twice as long each time up to the max. - return Math.min(this.refreshInterval * 2, this.maxRefreshInterval); - } - - fetchData = (callback) => { - if (NylasEnv.inSpecMode()) { return } - if (!IdentityStore.identity()) { return } - - const request = new NylasAPIRequest({ - api: NylasAPI, - options: { - accountId: this._accountId, - path: "/contacts/rankings", - }, - }) - - request.run() - .then((json) => { - if (!json || !(json instanceof Array)) return - - // Convert rankings into the format needed for quick lookup - const rankings = {} - for (const [email, rank] of json) { - rankings[email.toLowerCase()] = rank - } - callback(rankings) - - this.refreshInterval = this._nextRefreshInterval(); - }) - .catch((err) => { - console.warn(`Request for Contact Rankings failed for - account ${this._accountId}. ${err}`) - }) - } -} - -class ContactRankingsCacheManager { - constructor() { - this.accountCaches = {}; - this.unsubscribers = []; - this.onAccountsChanged = _.debounce(this.onAccountsChanged, 100); - } - - activate() { - this.onAccountsChanged(); - this.unsubscribers = [AccountStore.listen(this.onAccountsChanged)]; - } - - deactivate() { - this.unsubscribers.forEach(unsub => unsub()); - } - - onAccountsChanged = async () => { - const previousIDs = Object.keys(this.accountCaches); - const latestIDs = AccountStore.accounts().map(a => a.id); - if (_.isEqual(previousIDs, latestIDs)) { - return; - } - - const newIDs = _.difference(latestIDs, previousIDs); - const removedIDs = _.difference(previousIDs, latestIDs); - - console.log(`ContactRankingsCache: Updating contact rankings; added = ${latestIDs}, removed = ${removedIDs}`); - - for (const newID of newIDs) { - // Wait until the account has started syncing before trying to fetch - // contact rankings - await FolderSyncProgressStore.whenCategoryListSynced(newID) - this.accountCaches[newID] = new ContactRankingsCache(newID); - this.accountCaches[newID].start(); - } - - for (const removedID of removedIDs) { - if (this.accountCaches[removedID]) { - this.accountCaches[removedID].end(); - this.accountCaches[removedID] = null; - } - } - } -} - -export default new ContactRankingsCacheManager(); diff --git a/packages/client-app/internal_packages/contact-rankings/lib/main.es6 b/packages/client-app/internal_packages/contact-rankings/lib/main.es6 deleted file mode 100644 index 67e5d5a6c..000000000 --- a/packages/client-app/internal_packages/contact-rankings/lib/main.es6 +++ /dev/null @@ -1,10 +0,0 @@ -import ContactRankingsCache from './contact-rankings-cache' - - -export function activate() { - ContactRankingsCache.activate(); -} - -export function deactivate() { - ContactRankingsCache.deactivate(); -} diff --git a/packages/client-app/internal_packages/contact-rankings/lib/refreshing-json-cache.coffee b/packages/client-app/internal_packages/contact-rankings/lib/refreshing-json-cache.coffee deleted file mode 100644 index 80a48b6b5..000000000 --- a/packages/client-app/internal_packages/contact-rankings/lib/refreshing-json-cache.coffee +++ /dev/null @@ -1,57 +0,0 @@ -_ = require 'underscore' -{NylasStore, DatabaseStore} = require 'nylas-exports' - - -class RefreshingJSONCache - - constructor: ({@key, @version, @refreshInterval, @maxRefreshInterval}) -> - @_timeoutId = null - - start: -> - # Clear any scheduled actions - @end() - - # Look up existing data from db - DatabaseStore.findJSONBlob(@key).then (json) => - if json? and json.refreshInterval - @refreshInterval = json.refreshInterval - - # Refresh immediately if json is missing or version is outdated. Otherwise, - # compute next refresh time and schedule - timeUntilRefresh = 0 - if json? and json.version is @version - timeUntilRefresh = Math.max(0, @refreshInterval - (Date.now() - json.time)) - - @_timeoutId = setTimeout(@refresh, timeUntilRefresh) - - reset: -> - # Clear db value, turn off any scheduled actions - DatabaseStore.inTransaction (t) => t.persistJSONBlob(@key, {}) - @end() - - end: -> - # Turn off any scheduled actions - clearInterval(@_timeoutId) if @_timeoutId - @_timeoutId = null - - refresh: => - # Set up next refresh call - clearTimeout(@_timeoutId) if @_timeoutId - @_timeoutId = setTimeout(@refresh, @refreshInterval) - - # Call fetch data function, save it to the database - @fetchData (newValue) => - DatabaseStore.inTransaction (t) => - t.persistJSONBlob(@key, { - version: @version - time: Date.now() - value: newValue - refreshInterval: @refreshInterval - }) - - fetchData: (callback) => - throw new Error("Subclasses should override this method.") - - - -module.exports = RefreshingJSONCache diff --git a/packages/client-app/internal_packages/contact-rankings/package.json b/packages/client-app/internal_packages/contact-rankings/package.json deleted file mode 100755 index 54802f4a1..000000000 --- a/packages/client-app/internal_packages/contact-rankings/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "deltas", - "version": "0.1.0", - "main": "./lib/main", - "description": "Delta connection handling in Nylas Mail", - "license": "GPL-3.0", - "private": true, - "engines": { - "nylas": "*" - }, - "windowTypes": { - "work": true - } -} diff --git a/packages/client-app/spec/stores/contact-store-spec.coffee b/packages/client-app/spec/stores/contact-store-spec.coffee index 4f4610dbf..54f201012 100644 --- a/packages/client-app/spec/stores/contact-store-spec.coffee +++ b/packages/client-app/spec/stores/contact-store-spec.coffee @@ -5,7 +5,6 @@ Contact = require('../../src/flux/models/contact').default NylasAPI = require('../../src/flux/nylas-api').default NylasAPIRequest = require('../../src/flux/nylas-api-request').default ContactStore = require '../../src/flux/stores/contact-store' -ContactRankingStore = require '../../src/flux/stores/contact-ranking-store' DatabaseStore = require('../../src/flux/stores/database-store').default AccountStore = require('../../src/flux/stores/account-store').default @@ -15,65 +14,23 @@ xdescribe "ContactStore", -> beforeEach -> spyOn(NylasEnv, "isMainWindow").andReturn true - @rankings = [ - ["evanA@nylas.com", 10] - ["evanB@nylas.com", 1] - ["evanC@nylas.com", 0.1] - ] - - spyOn(NylasAPIRequest.prototype, "run").andCallFake (options) => - if options.path is "/contacts/rankings" - return Promise.resolve(@rankings) - else - throw new Error("Invalid request path!") - NylasEnv.testOrganizationUnit = "folder" ContactStore._contactCache = [] ContactStore._fetchOffset = 0 ContactStore._accountId = null - ContactRankingStore.reset() afterEach -> NylasEnv.testOrganizationUnit = null - describe "ranking contacts", -> - beforeEach -> - @accountId = TEST_ACCOUNT_ID - @c1 = new Contact({name: "Evan A", email: "evanA@nylas.com", @accountId}) - @c2 = new Contact({name: "Evan B", email: "evanB@nylas.com", @accountId}) - @c3 = new Contact({name: "Evan C", email: "evanC@nylas.com", @accountId}) - @c4 = new Contact({name: "Ben", email: "ben@nylas.com"}) - @contacts = [@c3, @c1, @c2, @c4] - - it "queries for, and sorts, contacts present in the rankings", -> - spyOn(ContactRankingStore, 'valuesForAllAccounts').andReturn - "evana@nylas.com": 10 - "evanb@nylas.com": 1 - "evanc@nylas.com": 0.1 - - spyOn(DatabaseStore, 'findAll').andCallFake => - return {background: => Promise.resolve([@c3, @c1, @c2, @c4])} - - waitsForPromise => - ContactStore._updateRankedContactCache().then => - expect(ContactStore._rankedContacts).toEqual [@c1, @c2, @c3, @c4] - - describe "when ContactRankings change", -> - it "re-generates the ranked contact cache", -> - spyOn(ContactStore, "_updateRankedContactCache") - ContactRankingStore.trigger() - expect(ContactStore._updateRankedContactCache).toHaveBeenCalled() - describe "when searching for a contact", -> beforeEach -> - @c1 = new Contact(name: "", email: "1test@nylas.com") - @c2 = new Contact(name: "First", email: "2test@nylas.com") - @c3 = new Contact(name: "First Last", email: "3test@nylas.com") - @c4 = new Contact(name: "Fit", email: "fit@nylas.com") - @c5 = new Contact(name: "Fins", email: "fins@nylas.com") - @c6 = new Contact(name: "Fill", email: "fill@nylas.com") - @c7 = new Contact(name: "Fin", email: "fin@nylas.com") - ContactStore._rankedContacts = [@c1,@c2,@c3,@c4,@c5,@c6,@c7] + @c1 = new Contact(name: "", email: "1test@nylas.com", refs: 7) + @c2 = new Contact(name: "First", email: "2test@nylas.com", refs: 6) + @c3 = new Contact(name: "First Last", email: "3test@nylas.com", refs: 5) + @c4 = new Contact(name: "Fit", email: "fit@nylas.com", refs: 4) + @c5 = new Contact(name: "Fins", email: "fins@nylas.com", refs: 3) + @c6 = new Contact(name: "Fill", email: "fill@nylas.com", refs: 2) + @c7 = new Contact(name: "Fin", email: "fin@nylas.com", refs: 1) it "can find by first name", -> waitsForPromise => diff --git a/packages/client-app/src/flux/models/contact.es6 b/packages/client-app/src/flux/models/contact.es6 index 174590d56..fabd76ae4 100644 --- a/packages/client-app/src/flux/models/contact.es6 +++ b/packages/client-app/src/flux/models/contact.es6 @@ -54,6 +54,11 @@ export default class Contact extends Model { modelKey: 'email', }), + refs: Attributes.Number({ + queryable: true, + modelKey: 'refs', + }), + // Contains the raw thirdPartyData (keyed by the vendor name) about // this contact. thirdPartyData: Attributes.Object({ diff --git a/packages/client-app/src/flux/models/thread.es6 b/packages/client-app/src/flux/models/thread.es6 index e637c0c74..ad28d28a1 100644 --- a/packages/client-app/src/flux/models/thread.es6 +++ b/packages/client-app/src/flux/models/thread.es6 @@ -42,7 +42,7 @@ Section: Models class Thread extends ModelWithMetadata { static attributes = _.extend({}, ModelWithMetadata.attributes, { - snippet: Attributes.String({ + snippet: Attributes.String({ // TODO NONFUNCTIONAL modelKey: 'snippet', }), diff --git a/packages/client-app/src/flux/stores/contact-ranking-store.coffee b/packages/client-app/src/flux/stores/contact-ranking-store.coffee deleted file mode 100644 index 771a00938..000000000 --- a/packages/client-app/src/flux/stores/contact-ranking-store.coffee +++ /dev/null @@ -1,63 +0,0 @@ -Rx = require 'rx-lite' -NylasStore = require 'nylas-store' -DatabaseStore = require('./database-store').default -AccountStore = require('./account-store').default - -class ContactRankingStore extends NylasStore - - constructor: -> - @_values = {} - @_valuesAllAccounts = null - @_disposables = {} - @listenTo AccountStore, @_onAccountsChanged - @_registerObservables(AccountStore.accounts()) - - _registerObservables: (accounts) => - nextDisposables = {} - - # Create new observables, reusing existing ones when possible - # (so they don't trigger with initial state unnecesarily) - for acct in accounts - if @_disposables[acct.id] - nextDisposables[acct.id] = @_disposables[acct.id] - delete @_disposables[acct.id] - else - query = DatabaseStore.findJSONBlob("ContactRankingsFor#{acct.id}") - callback = @_onRankingsChanged.bind(@, acct.id) - nextDisposables[acct.id] = Rx.Observable.fromQuery(query).subscribe(callback) - - # Remove unused observables in the old set - for key, disposable of @_disposables - disposable.dispose() - - @_disposables = nextDisposables - - _onRankingsChanged: (accountId, json) => - @_values[accountId] = if json then json.value else null - @_valuesAllAccounts = null - @trigger() - - _onAccountsChanged: => - @_registerObservables(AccountStore.accounts()) - - valueFor: (accountId) => - @_values[accountId] - - valuesForAllAccounts: => - unless @_valuesAllAccounts - combined = {} - for acctId, values of @_values - for email, score of values - if combined[email] - combined[email] = Math.max(combined[email], score) - else - combined[email] = score - @_valuesAllAccounts = combined - - return @_valuesAllAccounts - - reset: => - @_valuesAllAccounts = null - @_values = {} - -module.exports = new ContactRankingStore() diff --git a/packages/client-app/src/flux/stores/contact-store.coffee b/packages/client-app/src/flux/stores/contact-store.coffee index 5e13f5e7f..c2a834607 100644 --- a/packages/client-app/src/flux/stores/contact-store.coffee +++ b/packages/client-app/src/flux/stores/contact-store.coffee @@ -10,7 +10,6 @@ RegExpUtils = require '../../regexp-utils' DatabaseStore = require('./database-store').default AccountStore = require('./account-store').default ComponentRegistry = require('../../registries/component-registry') -ContactRankingStore = require './contact-ranking-store' _ = require 'underscore' WindowBridge = require '../../window-bridge' @@ -24,11 +23,6 @@ Section: Stores ### class ContactStore extends NylasStore - constructor: -> - @_rankedContacts = [] - @listenTo ContactRankingStore, => @_updateRankedContactCache() - @_updateRankedContactCache() - # Public: Search the user's contact list for the given search term. # This method compares the `search` string against each Contact's # `name` and `email`. @@ -47,19 +41,13 @@ class ContactStore extends NylasStore search = search.toLowerCase() accountCount = AccountStore.accounts().length + extensions = ComponentRegistry.findComponentsMatching({ + role: "ContactSearchResults" + }) if not search or search.length is 0 return Promise.resolve([]) - # Search ranked contacts which are stored in order in memory - results = [] - for contact in @_rankedContacts - if (contact.email.toLowerCase().indexOf(search) isnt -1 or - contact.name.toLowerCase().indexOf(search) isnt -1) - results.push(contact) - if results.length is limit - break - # If we haven't found enough items in memory, query for more from the # database. Note that we ask for LIMIT * accountCount because we want to # return contacts with distinct email addresses, and the same contact @@ -68,18 +56,10 @@ class ContactStore extends NylasStore query = DatabaseStore.findAll(Contact) .search(search) .limit(limit * accountCount) - query.then (queryResults) => - existingEmails = _.pluck(results, 'email') - + .order(Contact.attributes.refs.descending()) + query.then (results) => # remove query results that were already found in ranked contacts - queryResults = _.reject queryResults, (c) -> c.email in existingEmails - queryResults = @_distinctByEmail(queryResults) - - results = results.concat(queryResults) - - extensions = ComponentRegistry.findComponentsMatching({ - role: "ContactSearchResults" - }) + results = @_distinctByEmail(results) return Promise.each extensions, (ext) => return ext.findAdditionalContacts(search, results).then (contacts) => results = contacts @@ -141,26 +121,6 @@ class ContactStore extends NylasStore return match if match and match.email is contact.email return contact - _updateRankedContactCache: => - rankings = ContactRankingStore.valuesForAllAccounts() - emails = Object.keys(rankings) - - if emails.length is 0 - @_rankedContacts = [] - return - - # Sort the emails by rank and then clip to 400 so that our ranked cache - # has a bounded size. - emails = _.sortBy emails, (email) -> - (- (rankings[email.toLowerCase()] ? 0) / 1) - emails.length = 400 if emails.length > 400 - - DatabaseStore.findAll(Contact, {email: emails}).background().then (contacts) => - contacts = @_distinctByEmail(contacts) - for contact in contacts - contact._rank = (- (rankings[contact.email.toLowerCase()] ? 0) / 1) - @_rankedContacts = _.sortBy contacts, (contact) -> contact._rank - _distinctByEmail: (contacts) => # remove query results that are duplicates, prefering ones that have names uniq = {} @@ -172,9 +132,4 @@ class ContactStore extends NylasStore uniq[key] = contact _.values(uniq) - _resetCache: => - @_rankedContacts = [] - ContactRankingStore.reset() - @trigger(@) - module.exports = new ContactStore()