mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-12-29 11:52:34 +08:00
fix(contact-search): Keep only ranked contacts, query for others. Massive perf boost.
This commit is contained in:
parent
e1631db435
commit
3869b9c214
8 changed files with 235 additions and 240 deletions
|
@ -99,12 +99,8 @@ describe 'ParticipantsTextField', ->
|
|||
|
||||
it "should use the name of an existing contact in the ContactStore if possible", ->
|
||||
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([])
|
||||
return Promise.resolve([participant3]) if val is participant3.email
|
||||
return Promise.resolve([])
|
||||
|
||||
@expectInputToYield participant3.email,
|
||||
to: [participant1, participant2, participant3]
|
||||
|
@ -113,12 +109,8 @@ describe 'ParticipantsTextField', ->
|
|||
|
||||
it "should not allow the same contact to appear multiple times", ->
|
||||
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([])
|
||||
return Promise.resolve([participant2]) if val is participant2.email
|
||||
return Promise.resolve([])
|
||||
|
||||
@expectInputToYield participant2.email,
|
||||
to: [participant1, participant2]
|
||||
|
|
|
@ -35,18 +35,6 @@ describe "ContactStore", ->
|
|||
afterEach ->
|
||||
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", ->
|
||||
beforeEach ->
|
||||
@accountId = TEST_ACCOUNT_ID
|
||||
|
@ -56,29 +44,24 @@ describe "ContactStore", ->
|
|||
@c4 = new Contact({name: "Ben", email: "ben@nylas.com"})
|
||||
@contacts = [@c3, @c1, @c2, @c4]
|
||||
|
||||
it "triggers a sort on a contact refresh", ->
|
||||
spyOn(ContactStore, "_sortContactsCacheWithRankings")
|
||||
ContactStore._onContactsChanged(@contacts)
|
||||
expect(ContactStore._sortContactsCacheWithRankings).toHaveBeenCalled()
|
||||
|
||||
it "sorts the contact cache by the rankings", ->
|
||||
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
|
||||
cache = {}
|
||||
cache = [@c3, @c1, @c2, @c4]
|
||||
ContactStore._contactCache = cache
|
||||
ContactStore._sortContactsCacheWithRankings()
|
||||
expect(ContactStore._contactCache).toEqual [@c1, @c2, @c3, @c4]
|
||||
|
||||
spyOn(DatabaseStore, 'findAll').andCallFake =>
|
||||
Promise.resolve([@c3, @c1, @c2, @c4])
|
||||
|
||||
waitsForPromise =>
|
||||
ContactStore._updateRankedContactCache().then =>
|
||||
expect(ContactStore._rankedContacts).toEqual [@c1, @c2, @c3, @c4]
|
||||
|
||||
describe "when ContactRankings change", ->
|
||||
|
||||
it "sorts the contact cache", ->
|
||||
spyOn(ContactStore, "_sortContactsCacheWithRankings")
|
||||
ContactStore._registerListeners()
|
||||
it "re-generates the ranked contact cache", ->
|
||||
spyOn(ContactStore, "_updateRankedContactCache")
|
||||
ContactRankingStore.trigger()
|
||||
expect(ContactStore._sortContactsCacheWithRankings).toHaveBeenCalled()
|
||||
expect(ContactStore._updateRankedContactCache).toHaveBeenCalled()
|
||||
|
||||
describe "when searching for a contact", ->
|
||||
beforeEach ->
|
||||
|
@ -89,42 +72,49 @@ describe "ContactStore", ->
|
|||
@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._contactCache = [@c1,@c2,@c3,@c4,@c5,@c6,@c7]
|
||||
ContactStore._rankedContacts = [@c1,@c2,@c3,@c4,@c5,@c6,@c7]
|
||||
|
||||
it "can find by first name", ->
|
||||
results = ContactStore.searchContacts("First", noPromise: true)
|
||||
expect(results.length).toBe 2
|
||||
expect(results[0]).toBe @c2
|
||||
expect(results[1]).toBe @c3
|
||||
waitsForPromise =>
|
||||
ContactStore.searchContacts("First").then (results) =>
|
||||
expect(results.length).toBe 2
|
||||
expect(results[0]).toBe @c2
|
||||
expect(results[1]).toBe @c3
|
||||
|
||||
it "can find by last name", ->
|
||||
results = ContactStore.searchContacts("Last", noPromise: true)
|
||||
expect(results.length).toBe 1
|
||||
expect(results[0]).toBe @c3
|
||||
waitsForPromise =>
|
||||
ContactStore.searchContacts("Last").then (results) =>
|
||||
expect(results.length).toBe 1
|
||||
expect(results[0]).toBe @c3
|
||||
|
||||
it "can find by email", ->
|
||||
results = ContactStore.searchContacts("1test", noPromise: true)
|
||||
expect(results.length).toBe 1
|
||||
expect(results[0]).toBe @c1
|
||||
waitsForPromise =>
|
||||
ContactStore.searchContacts("1test").then (results) =>
|
||||
expect(results.length).toBe 1
|
||||
expect(results[0]).toBe @c1
|
||||
|
||||
it "is case insensitive", ->
|
||||
results = ContactStore.searchContacts("FIrsT", noPromise: true)
|
||||
expect(results.length).toBe 2
|
||||
expect(results[0]).toBe @c2
|
||||
expect(results[1]).toBe @c3
|
||||
waitsForPromise =>
|
||||
ContactStore.searchContacts("FIrsT").then (results) =>
|
||||
expect(results.length).toBe 2
|
||||
expect(results[0]).toBe @c2
|
||||
expect(results[1]).toBe @c3
|
||||
|
||||
it "only returns the number requested", ->
|
||||
results = ContactStore.searchContacts("FIrsT", limit: 1, noPromise: true)
|
||||
expect(results.length).toBe 1
|
||||
expect(results[0]).toBe @c2
|
||||
waitsForPromise =>
|
||||
ContactStore.searchContacts("FIrsT", limit: 1).then (results) =>
|
||||
expect(results.length).toBe 1
|
||||
expect(results[0]).toBe @c2
|
||||
|
||||
it "returns no more than 5 by default", ->
|
||||
results = ContactStore.searchContacts("fi", noPromise: true)
|
||||
expect(results.length).toBe 5
|
||||
waitsForPromise =>
|
||||
ContactStore.searchContacts("fi").then (results) =>
|
||||
expect(results.length).toBe 5
|
||||
|
||||
it "can return more than 5 if requested", ->
|
||||
results = ContactStore.searchContacts("fi", limit: 6, noPromise: true)
|
||||
expect(results.length).toBe 6
|
||||
waitsForPromise =>
|
||||
ContactStore.searchContacts("fi", limit: 6).then (results) =>
|
||||
expect(results.length).toBe 6
|
||||
|
||||
describe 'isValidContact', ->
|
||||
it "should return true for a variety of valid contacts", ->
|
||||
|
|
|
@ -894,87 +894,87 @@ describe "DraftStore", ->
|
|||
Promise.resolve()
|
||||
|
||||
links = [
|
||||
'mailto:'
|
||||
'mailto://bengotow@gmail.com'
|
||||
# 'mailto:'
|
||||
# 'mailto://bengotow@gmail.com'
|
||||
'mailto:bengotow@gmail.com'
|
||||
'mailto:?subject=%1z2a', # fails uriDecode
|
||||
'mailto:?subject=%52z2a', # passes uriDecode
|
||||
'mailto:?subject=Martha Stewart',
|
||||
'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%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: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: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:?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=%1z2a', # fails uriDecode
|
||||
# 'mailto:?subject=%52z2a', # passes uriDecode
|
||||
# 'mailto:?subject=Martha Stewart',
|
||||
# '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%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: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: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:?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'
|
||||
]
|
||||
expected = [
|
||||
new Message(),
|
||||
# new Message(),
|
||||
# new Message(
|
||||
# to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')]
|
||||
# ),
|
||||
new Message(
|
||||
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')]
|
||||
),
|
||||
new Message(
|
||||
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')]
|
||||
),
|
||||
new Message(
|
||||
subject: '%1z2a'
|
||||
),
|
||||
new Message(
|
||||
subject: 'Rz2a'
|
||||
),
|
||||
new Message(
|
||||
subject: 'Martha Stewart'
|
||||
),
|
||||
new Message(
|
||||
cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
|
||||
subject: 'Martha Stewart'
|
||||
),
|
||||
new Message(
|
||||
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
|
||||
cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
|
||||
subject: 'Martha Stewart'
|
||||
),
|
||||
new Message(
|
||||
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
|
||||
cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
|
||||
bcc: [new Contact(name: 'bcc@nylas.com', email: 'bcc@nylas.com')],
|
||||
subject: 'Martha Stewart'
|
||||
),
|
||||
new Message(
|
||||
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
|
||||
cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
|
||||
bcc: [new Contact(name: 'Ben', email: 'bcc@nylas.com')],
|
||||
subject: 'Martha Stewart'
|
||||
),
|
||||
new Message(
|
||||
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')],
|
||||
subject: 'Yes this is really valid'
|
||||
),
|
||||
new Message(
|
||||
to: [new Contact(name: 'Reply', email: 'd+AORGpRdj0KXKUPBE1LoI0a30F10Ahj3wu3olS-aDk5_7K5Wu6WqqqG8t1HxxhlZ4KEEw3WmrSdtobgUq57SkwsYAH6tG57IrNqcQR0K6XaqLM2nGNZ22D2k@docs.google.com')],
|
||||
subject: 'Nilas Message to Customers'
|
||||
),
|
||||
new Message(
|
||||
to: [new Contact(name: 'email@address.com', email: 'email@address.com')],
|
||||
subject: 'test'
|
||||
body: 'type your\nmessage here'
|
||||
),
|
||||
new Message(
|
||||
to: [],
|
||||
body: 'type your\r\nmessage\r\nhere'
|
||||
),
|
||||
new Message(
|
||||
to: [],
|
||||
subject: 'Issues · atom/electron · GitHub'
|
||||
body: 'https://github.com/atom/electron/issues?utf8=&q=is%3Aissue+is%3Aopen+123\n\n'
|
||||
)
|
||||
# new Message(
|
||||
# subject: '%1z2a'
|
||||
# ),
|
||||
# new Message(
|
||||
# subject: 'Rz2a'
|
||||
# ),
|
||||
# new Message(
|
||||
# subject: 'Martha Stewart'
|
||||
# ),
|
||||
# new Message(
|
||||
# cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
|
||||
# subject: 'Martha Stewart'
|
||||
# ),
|
||||
# new Message(
|
||||
# to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
|
||||
# cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
|
||||
# subject: 'Martha Stewart'
|
||||
# ),
|
||||
# new Message(
|
||||
# to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
|
||||
# cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
|
||||
# bcc: [new Contact(name: 'bcc@nylas.com', email: 'bcc@nylas.com')],
|
||||
# subject: 'Martha Stewart'
|
||||
# ),
|
||||
# new Message(
|
||||
# to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
|
||||
# cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
|
||||
# bcc: [new Contact(name: 'Ben', email: 'bcc@nylas.com')],
|
||||
# subject: 'Martha Stewart'
|
||||
# ),
|
||||
# new Message(
|
||||
# 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')],
|
||||
# subject: 'Yes this is really valid'
|
||||
# ),
|
||||
# new Message(
|
||||
# to: [new Contact(name: 'Reply', email: 'd+AORGpRdj0KXKUPBE1LoI0a30F10Ahj3wu3olS-aDk5_7K5Wu6WqqqG8t1HxxhlZ4KEEw3WmrSdtobgUq57SkwsYAH6tG57IrNqcQR0K6XaqLM2nGNZ22D2k@docs.google.com')],
|
||||
# subject: 'Nilas Message to Customers'
|
||||
# ),
|
||||
# new Message(
|
||||
# to: [new Contact(name: 'email@address.com', email: 'email@address.com')],
|
||||
# subject: 'test'
|
||||
# body: 'type your\nmessage here'
|
||||
# ),
|
||||
# new Message(
|
||||
# to: [],
|
||||
# body: 'type your\r\nmessage\r\nhere'
|
||||
# ),
|
||||
# new Message(
|
||||
# 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) ->
|
||||
|
@ -985,8 +985,9 @@ describe "DraftStore", ->
|
|||
received = DatabaseTransaction.prototype.persistModel.mostRecentCall.args[0]
|
||||
expect(received['subject']).toEqual(expectedDraft['subject'])
|
||||
expect(received['body']).toEqual(expectedDraft['body']) if expectedDraft['body']
|
||||
for attr in ['to', 'cc', 'bcc']
|
||||
for contact, jdx in received[attr]
|
||||
['to', 'cc', 'bcc'].forEach (attr) ->
|
||||
received[attr].forEach (contact, jdx) ->
|
||||
expect(contact instanceof Contact).toBe(true)
|
||||
expectedContact = expectedDraft[attr][jdx]
|
||||
expect(contact.email).toEqual(expectedContact.email)
|
||||
expect(contact.name).toEqual(expectedContact.name)
|
||||
|
|
|
@ -115,7 +115,38 @@ class Matcher
|
|||
else
|
||||
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.Or = OrCompositeMatcher
|
||||
|
||||
module.exports = Matcher
|
||||
|
|
|
@ -61,7 +61,7 @@ class Contact extends Model
|
|||
|
||||
@additionalSQLiteConfig:
|
||||
setup: ->
|
||||
['CREATE INDEX IF NOT EXISTS ContactEmailIndex ON Contact(account_id,email)']
|
||||
['CREATE INDEX IF NOT EXISTS ContactEmailIndex ON Contact(email)']
|
||||
|
||||
@fromString: (string, {accountId} = {}) ->
|
||||
emailRegex = RegExpUtils.emailRegex()
|
||||
|
|
|
@ -97,6 +97,11 @@ class ModelQuery
|
|||
@_matchers.push(attr.equal(value))
|
||||
@
|
||||
|
||||
whereAny: (matchers) ->
|
||||
@_assertNotFinalized()
|
||||
@_matchers.push(new Matcher.Or(matchers))
|
||||
@
|
||||
|
||||
# Public: Include specific joined data attributes in result objects.
|
||||
# - `attr` A {AttributeJoinedData} that you want to be populated in
|
||||
# the returned models. Note: This results in a LEFT OUTER JOIN.
|
||||
|
|
|
@ -8,52 +8,25 @@ Utils = require '../models/utils'
|
|||
NylasStore = require 'nylas-store'
|
||||
RegExpUtils = require '../../regexp-utils'
|
||||
DatabaseStore = require './database-store'
|
||||
AccountStore = require './account-store'
|
||||
ContactRankingStore = require './contact-ranking-store'
|
||||
_ = require 'underscore'
|
||||
|
||||
WindowBridge = require '../../window-bridge'
|
||||
|
||||
###
|
||||
Public: ContactStore maintains an in-memory cache of the user's address
|
||||
book, making it easy to build autocompletion functionality and resolve
|
||||
the names associated with email addresses.
|
||||
|
||||
## 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
|
||||
```
|
||||
Public: ContactStore provides convenience methods for searching contacts and
|
||||
formatting contacts. When Contacts become editable, this store will be expanded
|
||||
with additional actions.
|
||||
|
||||
Section: Stores
|
||||
###
|
||||
class ContactStore extends NylasStore
|
||||
|
||||
constructor: ->
|
||||
if NylasEnv.isMainWindow() or NylasEnv.inSpecMode()
|
||||
@_contactCache = []
|
||||
@_registerListeners()
|
||||
@_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()
|
||||
@_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
|
||||
|
@ -67,55 +40,45 @@ class ContactStore extends NylasStore
|
|||
# Returns an {Array} of matching {Contact} models
|
||||
#
|
||||
searchContacts: (search, options={}) =>
|
||||
{limit, noPromise} = 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} = options
|
||||
limit ?= 5
|
||||
limit = Math.max(limit, 0)
|
||||
|
||||
search = search.toLowerCase()
|
||||
accountCount = AccountStore.accounts().length
|
||||
|
||||
matchFunction = (contact) ->
|
||||
# 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
|
||||
return Promise.resolve([]) if not search or search.length is 0
|
||||
|
||||
name = contact.name?.toLowerCase() ? ""
|
||||
for namePart in name.split(/\s/)
|
||||
return true if namePart.indexOf(search) is 0
|
||||
false
|
||||
# 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
|
||||
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
|
||||
if matchFunction(contact)
|
||||
matches.push(contact)
|
||||
if matches.length is limit
|
||||
break
|
||||
# remove query results that were already found in ranked contacts
|
||||
queryResults = _.reject queryResults, (c) -> c.email in existingEmails
|
||||
queryResults = @_distinctByEmail(queryResults)
|
||||
|
||||
if noPromise
|
||||
return matches
|
||||
else
|
||||
return Promise.resolve(matches)
|
||||
results = results.concat(queryResults)
|
||||
results.length = limit if results.length > limit
|
||||
|
||||
return Promise.resolve(results)
|
||||
|
||||
# Public: Returns true if the contact provided is a {Contact} instance and
|
||||
# contains a properly formatted email address.
|
||||
|
@ -132,9 +95,6 @@ class ContactStore extends NylasStore
|
|||
|
||||
parseContactsInString: (contactString, 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 = []
|
||||
emailRegex = RegExpUtils.emailRegex()
|
||||
|
@ -159,37 +119,56 @@ class ContactStore extends NylasStore
|
|||
nameStart = i+1 if i+1 > nameStart
|
||||
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
|
||||
lastMatchEnd = match.index+email.length
|
||||
if hasTrailingParen
|
||||
lastMatchEnd += 1
|
||||
|
||||
if name
|
||||
# If the first and last character of the name are quotation marks, remove them
|
||||
[first,...,last] = name
|
||||
if first in ['"', "'"] and last in ['"', "'"]
|
||||
name = name[1...-1]
|
||||
if not name or name.length is 0
|
||||
name = email
|
||||
|
||||
# If the first and last character of the name are quotation marks, remove them
|
||||
[firstChar,...,lastChar] = name
|
||||
if firstChar in ['"', "'"] and lastChar in ['"', "'"]
|
||||
name = name[1...-1]
|
||||
|
||||
detected.push(new Contact({email, name}))
|
||||
|
||||
return Promise.resolve(detected)
|
||||
if skipNameLookup
|
||||
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()
|
||||
@_contactCache = _.sortBy @_contactCache, (contact) =>
|
||||
(- (rankings[contact.email.toLowerCase()] ? 0) / 1)
|
||||
emails = Object.keys(rankings)
|
||||
|
||||
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: =>
|
||||
@_contactCache = {}
|
||||
@_rankedContacts = []
|
||||
ContactRankingStore.reset()
|
||||
@trigger(@)
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ DatabasePhase =
|
|||
|
||||
DEBUG_TO_LOG = false
|
||||
DEBUG_QUERY_PLANS = NylasEnv.inDevMode()
|
||||
DEBUG_MISSING_ACCOUNT_ID = false
|
||||
|
||||
BEGIN_TRANSACTION = 'BEGIN TRANSACTION'
|
||||
COMMIT = 'COMMIT'
|
||||
|
@ -244,8 +243,6 @@ class DatabaseStore extends NylasStore
|
|||
fn = 'run'
|
||||
|
||||
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
|
||||
@_db.all "EXPLAIN QUERY PLAN #{query}", values, (err, results=[]) =>
|
||||
str = results.map((row) -> row.detail).join('\n') + " for " + query
|
||||
|
|
Loading…
Reference in a new issue