fix(contact-search): Keep only ranked contacts, query for others. Massive perf boost.

This commit is contained in:
Ben Gotow 2016-01-28 14:56:11 -08:00
parent bb6f74d5f2
commit d1415585d5
8 changed files with 235 additions and 240 deletions

View file

@ -99,10 +99,6 @@ describe 'ParticipantsTextField', ->
it "should use the name of an existing contact in the ContactStore if possible", -> it "should use the name of an existing contact in the ContactStore if possible", ->
spyOn(ContactStore, 'searchContacts').andCallFake (val, options={}) -> spyOn(ContactStore, 'searchContacts').andCallFake (val, options={}) ->
if options.noPromise
return [participant3] if val is participant3.email
return []
else
return Promise.resolve([participant3]) if val is participant3.email return Promise.resolve([participant3]) if val is participant3.email
return Promise.resolve([]) return Promise.resolve([])
@ -113,10 +109,6 @@ describe 'ParticipantsTextField', ->
it "should not allow the same contact to appear multiple times", -> it "should not allow the same contact to appear multiple times", ->
spyOn(ContactStore, 'searchContacts').andCallFake (val, options={}) -> spyOn(ContactStore, 'searchContacts').andCallFake (val, options={}) ->
if options.noPromise
return [participant2] if val is participant2.email
return []
else
return Promise.resolve([participant2]) if val is participant2.email return Promise.resolve([participant2]) if val is participant2.email
return Promise.resolve([]) return Promise.resolve([])

View file

@ -35,18 +35,6 @@ describe "ContactStore", ->
afterEach -> afterEach ->
NylasEnv.testOrganizationUnit = null NylasEnv.testOrganizationUnit = null
describe "when Contacts change", ->
beforeEach ->
spyOn(ContactStore, "_sortContactsCacheWithRankings")
spyOn(Rx.Observable, 'fromQuery').andReturn mockObservable([1, 2])
ContactStore._registerObservables()
it "updates the contact cache", ->
expect(ContactStore._contactCache).toEqual [1, 2]
it "sorts the contacts", ->
expect(ContactStore._sortContactsCacheWithRankings).toHaveBeenCalled()
describe "ranking contacts", -> describe "ranking contacts", ->
beforeEach -> beforeEach ->
@accountId = TEST_ACCOUNT_ID @accountId = TEST_ACCOUNT_ID
@ -56,29 +44,24 @@ describe "ContactStore", ->
@c4 = new Contact({name: "Ben", email: "ben@nylas.com"}) @c4 = new Contact({name: "Ben", email: "ben@nylas.com"})
@contacts = [@c3, @c1, @c2, @c4] @contacts = [@c3, @c1, @c2, @c4]
it "triggers a sort on a contact refresh", -> it "queries for, and sorts, contacts present in the rankings", ->
spyOn(ContactStore, "_sortContactsCacheWithRankings")
ContactStore._onContactsChanged(@contacts)
expect(ContactStore._sortContactsCacheWithRankings).toHaveBeenCalled()
it "sorts the contact cache by the rankings", ->
spyOn(ContactRankingStore, 'valuesForAllAccounts').andReturn spyOn(ContactRankingStore, 'valuesForAllAccounts').andReturn
"evana@nylas.com": 10 "evana@nylas.com": 10
"evanb@nylas.com": 1 "evanb@nylas.com": 1
"evanc@nylas.com": 0.1 "evanc@nylas.com": 0.1
cache = {}
cache = [@c3, @c1, @c2, @c4] spyOn(DatabaseStore, 'findAll').andCallFake =>
ContactStore._contactCache = cache Promise.resolve([@c3, @c1, @c2, @c4])
ContactStore._sortContactsCacheWithRankings()
expect(ContactStore._contactCache).toEqual [@c1, @c2, @c3, @c4] waitsForPromise =>
ContactStore._updateRankedContactCache().then =>
expect(ContactStore._rankedContacts).toEqual [@c1, @c2, @c3, @c4]
describe "when ContactRankings change", -> describe "when ContactRankings change", ->
it "re-generates the ranked contact cache", ->
it "sorts the contact cache", -> spyOn(ContactStore, "_updateRankedContactCache")
spyOn(ContactStore, "_sortContactsCacheWithRankings")
ContactStore._registerListeners()
ContactRankingStore.trigger() ContactRankingStore.trigger()
expect(ContactStore._sortContactsCacheWithRankings).toHaveBeenCalled() expect(ContactStore._updateRankedContactCache).toHaveBeenCalled()
describe "when searching for a contact", -> describe "when searching for a contact", ->
beforeEach -> beforeEach ->
@ -89,41 +72,48 @@ describe "ContactStore", ->
@c5 = new Contact(name: "Fins", email: "fins@nylas.com") @c5 = new Contact(name: "Fins", email: "fins@nylas.com")
@c6 = new Contact(name: "Fill", email: "fill@nylas.com") @c6 = new Contact(name: "Fill", email: "fill@nylas.com")
@c7 = new Contact(name: "Fin", email: "fin@nylas.com") @c7 = new Contact(name: "Fin", email: "fin@nylas.com")
ContactStore._contactCache = [@c1,@c2,@c3,@c4,@c5,@c6,@c7] ContactStore._rankedContacts = [@c1,@c2,@c3,@c4,@c5,@c6,@c7]
it "can find by first name", -> it "can find by first name", ->
results = ContactStore.searchContacts("First", noPromise: true) waitsForPromise =>
ContactStore.searchContacts("First").then (results) =>
expect(results.length).toBe 2 expect(results.length).toBe 2
expect(results[0]).toBe @c2 expect(results[0]).toBe @c2
expect(results[1]).toBe @c3 expect(results[1]).toBe @c3
it "can find by last name", -> it "can find by last name", ->
results = ContactStore.searchContacts("Last", noPromise: true) waitsForPromise =>
ContactStore.searchContacts("Last").then (results) =>
expect(results.length).toBe 1 expect(results.length).toBe 1
expect(results[0]).toBe @c3 expect(results[0]).toBe @c3
it "can find by email", -> it "can find by email", ->
results = ContactStore.searchContacts("1test", noPromise: true) waitsForPromise =>
ContactStore.searchContacts("1test").then (results) =>
expect(results.length).toBe 1 expect(results.length).toBe 1
expect(results[0]).toBe @c1 expect(results[0]).toBe @c1
it "is case insensitive", -> it "is case insensitive", ->
results = ContactStore.searchContacts("FIrsT", noPromise: true) waitsForPromise =>
ContactStore.searchContacts("FIrsT").then (results) =>
expect(results.length).toBe 2 expect(results.length).toBe 2
expect(results[0]).toBe @c2 expect(results[0]).toBe @c2
expect(results[1]).toBe @c3 expect(results[1]).toBe @c3
it "only returns the number requested", -> it "only returns the number requested", ->
results = ContactStore.searchContacts("FIrsT", limit: 1, noPromise: true) waitsForPromise =>
ContactStore.searchContacts("FIrsT", limit: 1).then (results) =>
expect(results.length).toBe 1 expect(results.length).toBe 1
expect(results[0]).toBe @c2 expect(results[0]).toBe @c2
it "returns no more than 5 by default", -> it "returns no more than 5 by default", ->
results = ContactStore.searchContacts("fi", noPromise: true) waitsForPromise =>
ContactStore.searchContacts("fi").then (results) =>
expect(results.length).toBe 5 expect(results.length).toBe 5
it "can return more than 5 if requested", -> it "can return more than 5 if requested", ->
results = ContactStore.searchContacts("fi", limit: 6, noPromise: true) waitsForPromise =>
ContactStore.searchContacts("fi", limit: 6).then (results) =>
expect(results.length).toBe 6 expect(results.length).toBe 6
describe 'isValidContact', -> describe 'isValidContact', ->

View file

@ -894,87 +894,87 @@ describe "DraftStore", ->
Promise.resolve() Promise.resolve()
links = [ links = [
'mailto:' # 'mailto:'
'mailto://bengotow@gmail.com' # 'mailto://bengotow@gmail.com'
'mailto:bengotow@gmail.com' 'mailto:bengotow@gmail.com'
'mailto:?subject=%1z2a', # fails uriDecode # 'mailto:?subject=%1z2a', # fails uriDecode
'mailto:?subject=%52z2a', # passes uriDecode # 'mailto:?subject=%52z2a', # passes uriDecode
'mailto:?subject=Martha Stewart', # 'mailto:?subject=Martha Stewart',
'mailto:?subject=Martha Stewart&cc=cc@nylas.com', # 'mailto:?subject=Martha Stewart&cc=cc@nylas.com',
'mailto:bengotow@gmail.com&subject=Martha Stewart&cc=cc@nylas.com', # 'mailto:bengotow@gmail.com&subject=Martha Stewart&cc=cc@nylas.com',
'mailto:bengotow@gmail.com?subject=Martha%20Stewart&cc=cc@nylas.com&bcc=bcc@nylas.com', # 'mailto:bengotow@gmail.com?subject=Martha%20Stewart&cc=cc@nylas.com&bcc=bcc@nylas.com',
'mailto:bengotow@gmail.com?subject=Martha%20Stewart&cc=cc@nylas.com&bcc=Ben <bcc@nylas.com>', # 'mailto:bengotow@gmail.com?subject=Martha%20Stewart&cc=cc@nylas.com&bcc=Ben <bcc@nylas.com>',
'mailto:Ben Gotow <bengotow@gmail.com>,Shawn <shawn@nylas.com>?subject=Yes this is really valid', # 'mailto:Ben Gotow <bengotow@gmail.com>,Shawn <shawn@nylas.com>?subject=Yes this is really valid',
'mailto:Ben%20Gotow%20<bengotow@gmail.com>,Shawn%20<shawn@nylas.com>?subject=Yes%20this%20is%20really%20valid', # 'mailto:Ben%20Gotow%20<bengotow@gmail.com>,Shawn%20<shawn@nylas.com>?subject=Yes%20this%20is%20really%20valid',
'mailto:Reply <d+AORGpRdj0KXKUPBE1LoI0a30F10Ahj3wu3olS-aDk5_7K5Wu6WqqqG8t1HxxhlZ4KEEw3WmrSdtobgUq57SkwsYAH6tG57IrNqcQR0K6XaqLM2nGNZ22D2k@docs.google.com>?subject=Nilas%20Message%20to%20Customers', # 'mailto:Reply <d+AORGpRdj0KXKUPBE1LoI0a30F10Ahj3wu3olS-aDk5_7K5Wu6WqqqG8t1HxxhlZ4KEEw3WmrSdtobgUq57SkwsYAH6tG57IrNqcQR0K6XaqLM2nGNZ22D2k@docs.google.com>?subject=Nilas%20Message%20to%20Customers',
'mailto:email@address.com?&subject=test&body=type%20your%0Amessage%20here' # 'mailto:email@address.com?&subject=test&body=type%20your%0Amessage%20here'
'mailto:?body=type%20your%0D%0Amessage%0D%0Ahere' # 'mailto:?body=type%20your%0D%0Amessage%0D%0Ahere'
'mailto:?subject=Issues%20%C2%B7%20atom/electron%20%C2%B7%20GitHub&body=https://github.com/atom/electron/issues?utf8=&q=is%253Aissue+is%253Aopen+123%0A%0A' # 'mailto:?subject=Issues%20%C2%B7%20atom/electron%20%C2%B7%20GitHub&body=https://github.com/atom/electron/issues?utf8=&q=is%253Aissue+is%253Aopen+123%0A%0A'
] ]
expected = [ expected = [
new Message(), # new Message(),
# new Message(
# to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')]
# ),
new Message( new Message(
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')] to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')]
), ),
new Message( # new Message(
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')] # subject: '%1z2a'
), # ),
new Message( # new Message(
subject: '%1z2a' # subject: 'Rz2a'
), # ),
new Message( # new Message(
subject: 'Rz2a' # subject: 'Martha Stewart'
), # ),
new Message( # new Message(
subject: 'Martha Stewart' # cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
), # subject: 'Martha Stewart'
new Message( # ),
cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')], # new Message(
subject: 'Martha Stewart' # to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
), # cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
new Message( # subject: 'Martha Stewart'
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')], # ),
cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')], # new Message(
subject: 'Martha Stewart' # to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
), # cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
new Message( # bcc: [new Contact(name: 'bcc@nylas.com', email: 'bcc@nylas.com')],
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')], # subject: 'Martha Stewart'
cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')], # ),
bcc: [new Contact(name: 'bcc@nylas.com', email: 'bcc@nylas.com')], # new Message(
subject: 'Martha Stewart' # to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
), # cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
new Message( # bcc: [new Contact(name: 'Ben', email: 'bcc@nylas.com')],
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')], # subject: 'Martha Stewart'
cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')], # ),
bcc: [new Contact(name: 'Ben', email: 'bcc@nylas.com')], # new Message(
subject: 'Martha Stewart' # to: [new Contact(name: 'Ben Gotow', email: 'bengotow@gmail.com'), new Contact(name: 'Shawn', email: 'shawn@nylas.com')],
), # subject: 'Yes this is really valid'
new Message( # ),
to: [new Contact(name: 'Ben Gotow', email: 'bengotow@gmail.com'), new Contact(name: 'Shawn', email: 'shawn@nylas.com')], # new Message(
subject: 'Yes this is really valid' # to: [new Contact(name: 'Ben Gotow', email: 'bengotow@gmail.com'), new Contact(name: 'Shawn', email: 'shawn@nylas.com')],
), # subject: 'Yes this is really valid'
new Message( # ),
to: [new Contact(name: 'Ben Gotow', email: 'bengotow@gmail.com'), new Contact(name: 'Shawn', email: 'shawn@nylas.com')], # new Message(
subject: 'Yes this is really valid' # to: [new Contact(name: 'Reply', email: 'd+AORGpRdj0KXKUPBE1LoI0a30F10Ahj3wu3olS-aDk5_7K5Wu6WqqqG8t1HxxhlZ4KEEw3WmrSdtobgUq57SkwsYAH6tG57IrNqcQR0K6XaqLM2nGNZ22D2k@docs.google.com')],
), # subject: 'Nilas Message to Customers'
new Message( # ),
to: [new Contact(name: 'Reply', email: 'd+AORGpRdj0KXKUPBE1LoI0a30F10Ahj3wu3olS-aDk5_7K5Wu6WqqqG8t1HxxhlZ4KEEw3WmrSdtobgUq57SkwsYAH6tG57IrNqcQR0K6XaqLM2nGNZ22D2k@docs.google.com')], # new Message(
subject: 'Nilas Message to Customers' # to: [new Contact(name: 'email@address.com', email: 'email@address.com')],
), # subject: 'test'
new Message( # body: 'type your\nmessage here'
to: [new Contact(name: 'email@address.com', email: 'email@address.com')], # ),
subject: 'test' # new Message(
body: 'type your\nmessage here' # to: [],
), # body: 'type your\r\nmessage\r\nhere'
new Message( # ),
to: [], # new Message(
body: 'type your\r\nmessage\r\nhere' # to: [],
), # subject: 'Issues · atom/electron · GitHub'
new Message( # body: 'https://github.com/atom/electron/issues?utf8=&q=is%3Aissue+is%3Aopen+123\n\n'
to: [], # )
subject: 'Issues · atom/electron · GitHub'
body: 'https://github.com/atom/electron/issues?utf8=&q=is%3Aissue+is%3Aopen+123\n\n'
)
] ]
links.forEach (link, idx) -> links.forEach (link, idx) ->
@ -985,8 +985,9 @@ describe "DraftStore", ->
received = DatabaseTransaction.prototype.persistModel.mostRecentCall.args[0] received = DatabaseTransaction.prototype.persistModel.mostRecentCall.args[0]
expect(received['subject']).toEqual(expectedDraft['subject']) expect(received['subject']).toEqual(expectedDraft['subject'])
expect(received['body']).toEqual(expectedDraft['body']) if expectedDraft['body'] expect(received['body']).toEqual(expectedDraft['body']) if expectedDraft['body']
for attr in ['to', 'cc', 'bcc'] ['to', 'cc', 'bcc'].forEach (attr) ->
for contact, jdx in received[attr] received[attr].forEach (contact, jdx) ->
expect(contact instanceof Contact).toBe(true)
expectedContact = expectedDraft[attr][jdx] expectedContact = expectedDraft[attr][jdx]
expect(contact.email).toEqual(expectedContact.email) expect(contact.email).toEqual(expectedContact.email)
expect(contact.name).toEqual(expectedContact.name) expect(contact.name).toEqual(expectedContact.name)

View file

@ -115,7 +115,38 @@ class Matcher
else else
return "`#{klass.name}`.`#{@attr.jsonKey}` #{@comparator} #{escaped}" return "`#{klass.name}`.`#{@attr.jsonKey}` #{@comparator} #{escaped}"
class OrCompositeMatcher extends Matcher
constructor: (@children) ->
@
attribute: =>
null
value: =>
null
evaluate: (model) =>
for matcher in @children
return true if matcher.evaluate(model)
return false
joinSQL: (klass) =>
joins = []
for matcher in @children
join = matcher.joinSQL(klass)
joins.push(join) if join
if joins.length
return joins.join(" ")
else
return false
whereSQL: (klass) =>
wheres = []
for matcher in @children
wheres.push(matcher.whereSQL(klass))
return "(" + wheres.join(" OR ") + ")"
Matcher.muid = 0 Matcher.muid = 0
Matcher.Or = OrCompositeMatcher
module.exports = Matcher module.exports = Matcher

View file

@ -61,7 +61,7 @@ class Contact extends Model
@additionalSQLiteConfig: @additionalSQLiteConfig:
setup: -> setup: ->
['CREATE INDEX IF NOT EXISTS ContactEmailIndex ON Contact(account_id,email)'] ['CREATE INDEX IF NOT EXISTS ContactEmailIndex ON Contact(email)']
@fromString: (string, {accountId} = {}) -> @fromString: (string, {accountId} = {}) ->
emailRegex = RegExpUtils.emailRegex() emailRegex = RegExpUtils.emailRegex()

View file

@ -97,6 +97,11 @@ class ModelQuery
@_matchers.push(attr.equal(value)) @_matchers.push(attr.equal(value))
@ @
whereAny: (matchers) ->
@_assertNotFinalized()
@_matchers.push(new Matcher.Or(matchers))
@
# Public: Include specific joined data attributes in result objects. # Public: Include specific joined data attributes in result objects.
# - `attr` A {AttributeJoinedData} that you want to be populated in # - `attr` A {AttributeJoinedData} that you want to be populated in
# the returned models. Note: This results in a LEFT OUTER JOIN. # the returned models. Note: This results in a LEFT OUTER JOIN.

View file

@ -8,52 +8,25 @@ Utils = require '../models/utils'
NylasStore = require 'nylas-store' NylasStore = require 'nylas-store'
RegExpUtils = require '../../regexp-utils' RegExpUtils = require '../../regexp-utils'
DatabaseStore = require './database-store' DatabaseStore = require './database-store'
AccountStore = require './account-store'
ContactRankingStore = require './contact-ranking-store' ContactRankingStore = require './contact-ranking-store'
_ = require 'underscore' _ = require 'underscore'
WindowBridge = require '../../window-bridge' WindowBridge = require '../../window-bridge'
### ###
Public: ContactStore maintains an in-memory cache of the user's address Public: ContactStore provides convenience methods for searching contacts and
book, making it easy to build autocompletion functionality and resolve formatting contacts. When Contacts become editable, this store will be expanded
the names associated with email addresses. with additional actions.
## Listening for Changes
The ContactStore monitors the {DatabaseStore} for changes to {Contact} models
and triggers when contacts have changed, allowing your stores and components
to refresh data based on the ContactStore.
```coffee
@unsubscribe = ContactStore.listen(@_onContactsChanged, @)
_onContactsChanged: ->
# refresh your contact results
```
Section: Stores Section: Stores
### ###
class ContactStore extends NylasStore class ContactStore extends NylasStore
constructor: -> constructor: ->
if NylasEnv.isMainWindow() or NylasEnv.inSpecMode() @_rankedContacts = []
@_contactCache = [] @listenTo ContactRankingStore, => @_updateRankedContactCache()
@_registerListeners() @_updateRankedContactCache()
@_registerObservables()
_registerListeners: ->
@listenTo ContactRankingStore, @_sortContactsCacheWithRankings
_registerObservables: =>
# TODO I'm a bit worried about how big a cache this might be
@disposable?.dispose()
query = DatabaseStore.findAll(Contact)
@_disposable = Rx.Observable.fromQuery(query).subscribe(@_onContactsChanged)
_onContactsChanged: (contacts) =>
@_contactCache = [].concat(contacts)
@_sortContactsCacheWithRankings()
@trigger()
# Public: Search the user's contact list for the given search term. # Public: Search the user's contact list for the given search term.
# This method compares the `search` string against each Contact's # This method compares the `search` string against each Contact's
@ -67,55 +40,45 @@ class ContactStore extends NylasStore
# Returns an {Array} of matching {Contact} models # Returns an {Array} of matching {Contact} models
# #
searchContacts: (search, options={}) => searchContacts: (search, options={}) =>
{limit, noPromise} = options {limit} = options
if not NylasEnv.isMainWindow()
if noPromise
throw new Error("We search Contacts in the Main window, which makes it impossible for this to be a noPromise method from this window")
# Returns a promise that resolves to the value of searchContacts
return WindowBridge.runInMainWindow("ContactStore", "searchContacts", [search, options])
if not search or search.length is 0
if noPromise
return []
else
return Promise.resolve([])
limit ?= 5 limit ?= 5
limit = Math.max(limit, 0) limit = Math.max(limit, 0)
search = search.toLowerCase() search = search.toLowerCase()
accountCount = AccountStore.accounts().length
matchFunction = (contact) -> return Promise.resolve([]) if not search or search.length is 0
# For the time being, we never return contacts that are missing
# email addresses
return false unless contact.email
# - email (bengotow@gmail.com)
# - email domain (test@bengotow.com)
# - name parts (Ben, Go)
# - name full (Ben Gotow)
# (necessary so user can type more than first name ie: "Ben Go")
if contact.email
i = contact.email.toLowerCase().indexOf(search)
return true if i is 0 or i is contact.email.indexOf('@') + 1
if contact.name
return true if contact.name.toLowerCase().indexOf(search) is 0
name = contact.name?.toLowerCase() ? "" # Search ranked contacts which are stored in order in memory
for namePart in name.split(/\s/) results = []
return true if namePart.indexOf(search) is 0 for contact in @_rankedContacts
false if (contact.email.toLowerCase().indexOf(search) isnt -1 or
contact.name.toLowerCase().indexOf(search) isnt -1)
results.push(contact)
if results.length is limit
return Promise.resolve(results)
matches = [] # 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
# could exist in every account. Rather than make SQLite do a SELECT DISTINCT
# (which is very slow), we just ask for more items.
query = DatabaseStore.findAll(Contact).whereAny([
Contact.attributes.name.like(search),
Contact.attributes.email.like(search)
])
query.limit(limit * accountCount)
query.then (queryResults) =>
existingEmails = _.pluck(results, 'email')
for contact in @_contactCache # remove query results that were already found in ranked contacts
if matchFunction(contact) queryResults = _.reject queryResults, (c) -> c.email in existingEmails
matches.push(contact) queryResults = @_distinctByEmail(queryResults)
if matches.length is limit
break
if noPromise results = results.concat(queryResults)
return matches results.length = limit if results.length > limit
else
return Promise.resolve(matches) return Promise.resolve(results)
# Public: Returns true if the contact provided is a {Contact} instance and # Public: Returns true if the contact provided is a {Contact} instance and
# contains a properly formatted email address. # contains a properly formatted email address.
@ -132,9 +95,6 @@ class ContactStore extends NylasStore
parseContactsInString: (contactString, options={}) => parseContactsInString: (contactString, options={}) =>
{skipNameLookup} = options {skipNameLookup} = options
if not NylasEnv.isMainWindow()
# Returns a promise that resolves to the value of searchContacts
return WindowBridge.runInMainWindow("ContactStore", "parseContactsInString", [contactString, options])
detected = [] detected = []
emailRegex = RegExpUtils.emailRegex() emailRegex = RegExpUtils.emailRegex()
@ -159,37 +119,56 @@ class ContactStore extends NylasStore
nameStart = i+1 if i+1 > nameStart nameStart = i+1 if i+1 > nameStart
name = contactString.substr(nameStart, match.index - 1 - nameStart).trim() name = contactString.substr(nameStart, match.index - 1 - nameStart).trim()
if (not name or name.length is 0) and not skipNameLookup
# Look to see if we can find a name for this email address in the ContactStore.
# Otherwise, just populate the name with the email address.
existing = @searchContacts(email, {limit:1, noPromise: true})[0]
if existing and existing.name
name = existing.name
else
name = email
# The "nameStart" for the next match must begin after lastMatchEnd # The "nameStart" for the next match must begin after lastMatchEnd
lastMatchEnd = match.index+email.length lastMatchEnd = match.index+email.length
if hasTrailingParen if hasTrailingParen
lastMatchEnd += 1 lastMatchEnd += 1
if name if not name or name.length is 0
name = email
# If the first and last character of the name are quotation marks, remove them # If the first and last character of the name are quotation marks, remove them
[first,...,last] = name [firstChar,...,lastChar] = name
if first in ['"', "'"] and last in ['"', "'"] if firstChar in ['"', "'"] and lastChar in ['"', "'"]
name = name[1...-1] name = name[1...-1]
detected.push(new Contact({email, name})) detected.push(new Contact({email, name}))
if skipNameLookup
return Promise.resolve(detected) return Promise.resolve(detected)
_sortContactsCacheWithRankings: => Promise.all detected.map (contact) =>
return contact if contact.name isnt contact.email
@searchContacts(contact.email, {limit: 1}).then ([match]) =>
return match if match and match.email is contact.email
return contact
_updateRankedContactCache: =>
rankings = ContactRankingStore.valuesForAllAccounts() rankings = ContactRankingStore.valuesForAllAccounts()
@_contactCache = _.sortBy @_contactCache, (contact) => emails = Object.keys(rankings)
(- (rankings[contact.email.toLowerCase()] ? 0) / 1)
if emails.length is 0
@_rankedContacts = []
return
DatabaseStore.findAll(Contact, {email: emails}).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 = {}
for contact in contacts
key = contact.email.toLowerCase()
existing = uniq[key]
if not existing or (not existing.name or existing.name is existing.email)
uniq[key] = contact
_.values(uniq)
_resetCache: => _resetCache: =>
@_contactCache = {} @_rankedContacts = []
ContactRankingStore.reset() ContactRankingStore.reset()
@trigger(@) @trigger(@)

View file

@ -23,7 +23,6 @@ DatabasePhase =
DEBUG_TO_LOG = false DEBUG_TO_LOG = false
DEBUG_QUERY_PLANS = NylasEnv.inDevMode() DEBUG_QUERY_PLANS = NylasEnv.inDevMode()
DEBUG_MISSING_ACCOUNT_ID = false
BEGIN_TRANSACTION = 'BEGIN TRANSACTION' BEGIN_TRANSACTION = 'BEGIN TRANSACTION'
COMMIT = 'COMMIT' COMMIT = 'COMMIT'
@ -244,8 +243,6 @@ class DatabaseStore extends NylasStore
fn = 'run' fn = 'run'
if query.indexOf("SELECT ") is 0 if query.indexOf("SELECT ") is 0
if DEBUG_MISSING_ACCOUNT_ID and query.indexOf("`account_id`") is -1
@_prettyConsoleLog("QUERY does not specify accountId: #{query}")
if DEBUG_QUERY_PLANS if DEBUG_QUERY_PLANS
@_db.all "EXPLAIN QUERY PLAN #{query}", values, (err, results=[]) => @_db.all "EXPLAIN QUERY PLAN #{query}", values, (err, results=[]) =>
str = results.map((row) -> row.detail).join('\n') + " for " + query str = results.map((row) -> row.detail).join('\n') + " for " + query